feat(tasks): SessionStart reads the claim — a compaction gets its work back (milestone 381 step 3)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Canceled after 45s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Canceled after 45s
The claim now pays rent to the session that set it. The SessionStart hook sends the host's `source` and the session id; the server renders a claim section by source (task_claims.render_claims): - compact / clear: the work this session had claimed, each with its two latest log entries, so a compacted session resumes from the record rather than from a count of open tasks. - startup / clear: other sessions' live claims (may still be running) and in-progress tasks whose claim went quiet (abandoned mid-task). - fork: the same, framed as "two sessions may now hold this". - resume, or no source sent: nothing. The task sidebar shows a live claim as "Being worked" and a dead one on open work as "Went quiet", so the operator sees a session die mid-task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,10 @@ export interface Note {
|
||||
due_date: string | null;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
// Which session is working this task now (milestone 381). Null when no
|
||||
// session ever claimed it; `live` false when the claim's lease ran out —
|
||||
// a session that went quiet mid-task, which is worth seeing, not hiding.
|
||||
claim?: TaskClaim | null;
|
||||
recurrence_rule: Record<string, unknown> | null;
|
||||
recurrence_next_spawn_at: string | null;
|
||||
is_task: boolean;
|
||||
@@ -53,3 +57,11 @@ export interface NoteListResponse {
|
||||
notes: Note[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TaskClaim {
|
||||
held_by: number | null;
|
||||
session: string | null;
|
||||
since: string | null;
|
||||
touched: string | null;
|
||||
live: boolean;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import type { TaskStatus, TaskPriority } from "@/types/task";
|
||||
import type { TaskKind } from "@/types/note";
|
||||
import { useSystemsStore } from "@/stores/systems";
|
||||
import type { System } from "@/api/systems";
|
||||
import type { Note } from "@/types/note";
|
||||
import type { Note, TaskClaim } from "@/types/note";
|
||||
import { relativeTime } from "@/composables/useRelativeTime";
|
||||
import type { Editor } from "@tiptap/vue-3";
|
||||
import MarkdownToolbar from "@/components/MarkdownToolbar.vue";
|
||||
import TiptapEditor from "@/components/TiptapEditor.vue";
|
||||
@@ -54,6 +55,7 @@ const parentId = ref<number | null>(null);
|
||||
const parentTitle = ref("");
|
||||
const startedAt = ref<string | null>(null);
|
||||
const completedAt = ref<string | null>(null);
|
||||
const claim = ref<TaskClaim | null>(null);
|
||||
const recurrenceRule = ref<Record<string, unknown> | null>(null);
|
||||
const parentSearchQuery = ref("");
|
||||
const parentSearchResults = ref<{ id: number; title: string }[]>([]);
|
||||
@@ -318,6 +320,7 @@ onMounted(async () => {
|
||||
const noteTask = store.currentTask as unknown as Note;
|
||||
startedAt.value = noteTask.started_at ?? null;
|
||||
completedAt.value = noteTask.completed_at ?? null;
|
||||
claim.value = noteTask.claim ?? null;
|
||||
recurrenceRule.value = noteTask.recurrence_rule ?? null;
|
||||
savedTitle = title.value;
|
||||
savedBody = body.value;
|
||||
@@ -592,7 +595,21 @@ useEditorGuards(dirty, save);
|
||||
<option v-if="kind === 'plan'" value="plan">Plan (legacy)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="startedAt || completedAt" class="sb-timestamps">
|
||||
<div v-if="startedAt || completedAt || claim" class="sb-timestamps">
|
||||
<div v-if="claim && claim.live" class="sb-timestamp">
|
||||
<span class="sb-ts-label">Being worked</span>
|
||||
<span class="sb-ts-value">
|
||||
by a session{{ claim.session ? ` (${claim.session.slice(0, 8)})` : "" }},
|
||||
last active {{ claim.touched ? relativeTime(claim.touched) : "recently" }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else-if="claim" class="sb-timestamp">
|
||||
<span class="sb-ts-label">Went quiet</span>
|
||||
<span class="sb-ts-value">
|
||||
the session working this stopped
|
||||
{{ claim.touched ? relativeTime(claim.touched) : "" }} without finishing it
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="startedAt" class="sb-timestamp">
|
||||
<span class="sb-ts-label">Started</span>
|
||||
<span class="sb-ts-value">{{ new Date(startedAt).toLocaleString() }}</span>
|
||||
|
||||
Reference in New Issue
Block a user