taskchef 7.9.1 → 7.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +13 -6
- package/docs/firstmate-taskchef-comparison.md +5 -2
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/spec.md +35 -22
- package/docs/workflows.md +47 -8
- package/package.json +2 -1
- package/skills/taskchef-executor/SKILL.md +11 -4
- package/skills/taskchef-report/SKILL.md +9 -5
- package/src/dashboard/app.js +5 -3
- package/src/dashboard/state.js +24 -0
- package/src/dashboard/styles.css +2 -0
- package/src/mcp.js +5 -3
- package/src/workspace.js +71 -20
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ The canonical workspace is `~/.agents/taskchef`. TaskChef owns only:
|
|
|
49
49
|
```text
|
|
50
50
|
AGENTS.md managed dispatcher instructions plus user additions
|
|
51
51
|
taskchef.json schema-2 configured projects and routing metadata
|
|
52
|
-
tasks.jsonl one task snapshot per line (schema
|
|
52
|
+
tasks.jsonl one task snapshot per line (schema 8; schema 4/5/6/7 migration supported)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
List or change routing targets conversationally:
|
|
@@ -123,7 +123,12 @@ appends a `turns` entry that pairs that request with a null result while working
|
|
|
123
123
|
then fills the same entry with the semantic outcome. A follow-up therefore shows
|
|
124
124
|
its own request with “In progress,” never the preceding turn's result. Returned
|
|
125
125
|
tasks still derive `results` and `lastResult` as compatibility projections.
|
|
126
|
-
|
|
126
|
+
If Codex crashes, an MCP call is lost, or the app restarts before that terminal
|
|
127
|
+
report, the next newer `working` report atomically marks the unfinished turn
|
|
128
|
+
`interrupted` and appends the new active turn. `interrupted` is TaskChef-authored
|
|
129
|
+
timeline evidence, not semantic `failed`, and it never enters `results` or
|
|
130
|
+
`lastResult`. TaskChef stores only a fixed interruption summary; it does not
|
|
131
|
+
store transcripts, hidden reasoning, crash output, or other non-semantic events.
|
|
127
132
|
|
|
128
133
|
Delegated tasks created by earlier TaskChef versions remain compatible: their
|
|
129
134
|
inline executor protocol still parses, self-links, and may use the deprecated
|
|
@@ -175,7 +180,7 @@ taskchef dashboard --port 3211
|
|
|
175
180
|
The loopback dashboard watches `tasks.jsonl`, groups current states, and opens
|
|
176
181
|
linked Codex tasks. List snapshots and SSE events carry only the latest
|
|
177
182
|
request/result pair; opening task details fetches the full newest-first activity
|
|
178
|
-
timeline.
|
|
183
|
+
timeline, including clearly labeled interrupted turns.
|
|
179
184
|
The header shows the running TaskChef package version reported by the same
|
|
180
185
|
bounded health identity used for compatible-listener checks.
|
|
181
186
|
Task and result times are relative through 29 days (with minute detail for the
|
|
@@ -205,6 +210,8 @@ no task data, credentials, environment variables, process control, or secrets.
|
|
|
205
210
|
|
|
206
211
|

|
|
207
212
|
|
|
213
|
+

|
|
214
|
+
|
|
208
215
|
## Common recovery
|
|
209
216
|
|
|
210
217
|
Check the managed workspace:
|
|
@@ -218,8 +225,8 @@ taskchef doctor
|
|
|
218
225
|
|
|
219
226
|
`doctor` is read-only. `workspace init` creates missing files and refreshes
|
|
220
227
|
managed instructions. `workspace migrate` explicitly upgrades supported schema
|
|
221
|
-
4/5/6 task lines to schema
|
|
222
|
-
source and converted log before writing, creates an exclusive `tasks.jsonl.pre-
|
|
228
|
+
4/5/6/7 task lines to schema 8 under the workspace lock. It validates the complete
|
|
229
|
+
source and converted log before writing, creates an exclusive `tasks.jsonl.pre-v8-*.bak`
|
|
223
230
|
backup, atomically replaces the log, validates the result, and becomes an
|
|
224
231
|
idempotent no-op after migration. If replacement fails, the original remains
|
|
225
232
|
or the reported backup can be restored; unsupported or invalid input is rejected
|
|
@@ -230,7 +237,7 @@ executor so its first action can retry `link_task`. Do not guess an identity
|
|
|
230
237
|
or edit `tasks.jsonl`. If native task creation failed, the record is retained
|
|
231
238
|
as `failed` with null thread and turn IDs.
|
|
232
239
|
|
|
233
|
-
Schemas other than 4, 5, 6, and
|
|
240
|
+
Schemas other than 4, 5, 6, 7, and 8 remain unsupported. Retain such a workspace
|
|
234
241
|
unchanged and create a current workspace; the migration command deliberately
|
|
235
242
|
does not guess how to convert unknown formats.
|
|
236
243
|
|
|
@@ -162,8 +162,11 @@ backends have different verification or experimental status.
|
|
|
162
162
|
|
|
163
163
|
**Fact:** Persisted statuses are `working`, `needs_input`, `completed`, and
|
|
164
164
|
`failed`. Executors report
|
|
165
|
-
`working` at turn start and a semantic state before ending. Schema
|
|
166
|
-
the
|
|
165
|
+
`working` at turn start and a semantic state before ending. Schema 8 preserves
|
|
166
|
+
the paired turn timeline and semantic-only compatibility results while a newer
|
|
167
|
+
turn is working. If a terminal report is lost, the next valid working start
|
|
168
|
+
closes the predecessor with a timeline-only `interrupted` outcome rather than
|
|
169
|
+
semantic `failed`.
|
|
167
170
|
`needs_input` is reserved for a real semantic decision, not a native approval
|
|
168
171
|
prompt.
|
|
169
172
|
|
|
Binary file
|
package/docs/spec.md
CHANGED
|
@@ -23,7 +23,8 @@ is dated research, not contract.
|
|
|
23
23
|
| **Self-linking** | The executor's one-way registration of its own canonical Codex UUIDv7 from `CODEX_THREAD_ID`. |
|
|
24
24
|
| **Link-pending** | A working task whose `threadId` is null and `updatedBy` is `dispatcher`. |
|
|
25
25
|
| **Current execution state** | The latest reported executor turn and its `working`, `needs_input`, `completed`, or `failed` status. |
|
|
26
|
-
| **Turn timeline** | The ordered collection pairing each turn's concise request summary with its
|
|
26
|
+
| **Turn timeline** | The ordered collection pairing each turn's concise request summary with its semantic result, TaskChef-generated interruption outcome, or current in-progress state. |
|
|
27
|
+
| **Interrupted turn** | A formerly active turn that lacked a terminal report when a newer valid turn started; TaskChef closes it with the fixed timeline-only `interrupted` outcome. |
|
|
27
28
|
| **Last semantic result** | The final result-history entry, exposed through the derived `lastResult` compatibility alias. |
|
|
28
29
|
| **Current turn ID** | The canonical Codex UUIDv7 returned by an exact native read of the linked executor for the turn being reported. |
|
|
29
30
|
| **Dashboard** | The loopback, read-only UI derived from validated workspace snapshots and bounded native actions. |
|
|
@@ -68,9 +69,9 @@ Names and paths MUST be unique. Git projects MUST be exact Git roots.
|
|
|
68
69
|
Repository URLs MUST canonicalize to `https://github.com/<owner>/<repository>`
|
|
69
70
|
and be case-insensitively deduplicated.
|
|
70
71
|
|
|
71
|
-
`tasks.jsonl` MUST contain zero or more newline-terminated schema-4
|
|
72
|
-
schema-
|
|
73
|
-
migration/read formats; every new record and state mutation MUST write schema
|
|
72
|
+
`tasks.jsonl` MUST contain zero or more newline-terminated schema-4 through
|
|
73
|
+
schema-8 records, one per line. Schemas 4 through 7 are supported
|
|
74
|
+
migration/read formats; every new record and state mutation MUST write schema 8. Other schemas
|
|
74
75
|
or unsupported fields MUST be rejected without conversion.
|
|
75
76
|
Reads and writes MUST reject symlinked managed files. Mutations
|
|
76
77
|
MUST hold the shared workspace lock and replace state atomically; read-only
|
|
@@ -82,7 +83,7 @@ Every record MUST contain exactly these fields:
|
|
|
82
83
|
|
|
83
84
|
| Field | Contract |
|
|
84
85
|
| --- | --- |
|
|
85
|
-
| `schemaVersion` | Integer `
|
|
86
|
+
| `schemaVersion` | Integer `8`; schema-4/5/6/7 records remain readable until explicit migration or their next mutation. |
|
|
86
87
|
| `id` | Unique safe TaskChef ID; delegation uses a lowercase full UUID. |
|
|
87
88
|
| `project` | Immutable configured-project snapshot. |
|
|
88
89
|
| `title` | Non-empty display title. |
|
|
@@ -94,12 +95,14 @@ Every record MUST contain exactly these fields:
|
|
|
94
95
|
| `turnId` | Null before turn reporting; otherwise the current reported turn. Linked MCP journeys use a canonical Codex UUIDv7. |
|
|
95
96
|
| `updatedAt` | ISO 8601 timestamp not earlier than `createdAt` or the prior `updatedAt`; clock rollback cannot backdate a transition. |
|
|
96
97
|
| `updatedBy` | `dispatcher` or `mcp`. |
|
|
97
|
-
| `turns` | Ordered oldest-first array of `{turnId, requestSummary, startedAt, result}`. `requestSummary` is null only for migrated/compatibility turns
|
|
98
|
+
| `turns` | Ordered oldest-first array of `{turnId, requestSummary, startedAt, result}`. `requestSummary` is null only for migrated/compatibility turns. `result` is null only for the latest working turn, a semantic `{status, summary, updatedAt}` result, or the TaskChef-generated `{status: "interrupted", summary: "Turn interrupted before a terminal report.", updatedAt}` outcome. Canonical self-linking turn IDs are unique. A migrated low-level opaque record may retain one final reused ID to represent its legacy completed-result-plus-working-state ambiguity. |
|
|
98
99
|
|
|
99
100
|
Returned Task objects MUST additionally expose `latestTurn` as null for an empty
|
|
100
|
-
timeline or the final `turns` entry. They MUST derive `results` from
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
timeline or the final `turns` entry. They MUST derive `results` only from
|
|
102
|
+
semantic `needs_input`, `completed`, and `failed` turn results and `lastResult`
|
|
103
|
+
from the final derived semantic result. Interrupted outcomes MUST be excluded.
|
|
104
|
+
These projections MUST NOT be persisted in schema 8 and remain compatibility
|
|
105
|
+
aliases for existing callers.
|
|
103
106
|
|
|
104
107
|
Task IDs and non-null thread identities MUST be unique. The immutable intent
|
|
105
108
|
fields MUST NOT change after recording.
|
|
@@ -129,7 +132,9 @@ the final link, preserving the delegate skill's immediate-return contract.
|
|
|
129
132
|
8. Before ending, it MUST call `report_state` for the same working turn with a
|
|
130
133
|
semantic status and concise summary.
|
|
131
134
|
9. A follow-up MUST report `working` with its new current turn ID before work.
|
|
132
|
-
It MUST NOT reuse a prior turn.
|
|
135
|
+
It MUST NOT reuse a prior turn. If the preceding turn is still unfinished,
|
|
136
|
+
TaskChef MUST atomically close it as `interrupted` before appending the new
|
|
137
|
+
working turn; the executor MUST NOT report semantic `failed` for recovery.
|
|
133
138
|
|
|
134
139
|
If native creation fails after recording, the dispatcher MUST call
|
|
135
140
|
`report_state` with `failed`, null thread/turn IDs, and a bounded summary.
|
|
@@ -238,7 +243,7 @@ new preparation values, though it writes no state.
|
|
|
238
243
|
|
|
239
244
|
**Structured output:** `{ task: Task }`.
|
|
240
245
|
|
|
241
|
-
The returned task has schema
|
|
246
|
+
The returned task has schema 8, `working`, null summary/turn/thread/latestTurn/lastResult,
|
|
242
247
|
empty `turns` and derived `results` arrays,
|
|
243
248
|
`updatedBy: dispatcher`, and equal creation/update timestamps. Duplicate IDs,
|
|
244
249
|
unknown projects, malformed markers, and invalid input fail. Repeating a
|
|
@@ -283,16 +288,23 @@ marker, or ineligible state fails.
|
|
|
283
288
|
|
|
284
289
|
**Structured output:** `{ task: Task }`.
|
|
285
290
|
|
|
286
|
-
For a linked self-linking journey, `working` MUST identify a turn
|
|
287
|
-
the current turn and last semantic result.
|
|
288
|
-
|
|
289
|
-
|
|
291
|
+
For a linked self-linking journey, a new `working` state MUST identify a turn
|
|
292
|
+
newer than the current turn and last semantic result. An exact retry of any
|
|
293
|
+
already-recorded working start MUST return the current task without mutation.
|
|
294
|
+
A semantic state MUST match the current working turn. Conflicting or stale
|
|
295
|
+
state fails. A null-identity record accepts only a fresh executor
|
|
290
296
|
creation `failed` state with both IDs null. Starting work appends one turn with
|
|
291
|
-
its request and a null result
|
|
297
|
+
its request and a null result. When the previous latest turn is unfinished, the
|
|
298
|
+
same locked atomic rewrite MUST first fill it with the fixed TaskChef-generated
|
|
299
|
+
`interrupted` outcome. The semantic report fills the active turn's result.
|
|
292
300
|
An identical retry for any settled turn returns success without an append. A
|
|
293
301
|
different request or result for the same turn, a stale turn, or a semantic
|
|
294
302
|
result that does not match the active working turn MUST fail.
|
|
295
303
|
|
|
304
|
+
`interrupted` MUST NOT be accepted as MCP input or projected as task `status`.
|
|
305
|
+
It MUST NOT alter `summary`, `results`, or `lastResult`, and its fixed summary
|
|
306
|
+
MUST contain no crash output, transcript, user text, or inferred failure cause.
|
|
307
|
+
|
|
296
308
|
**Annotations:** `readOnlyHint: false`, `destructiveHint: true`,
|
|
297
309
|
`openWorldHint: false`.
|
|
298
310
|
|
|
@@ -302,8 +314,8 @@ result that does not match the active working turn MUST fail.
|
|
|
302
314
|
temporary compatibility alias. It implicitly accepts a fresh supplied turn and
|
|
303
315
|
stores its semantic result in a request-unknown turn, including for supported schema-4/5/6 records and
|
|
304
316
|
low-level opaque direct records. It does not accept `working`. New executor
|
|
305
|
-
instructions MUST use `report_state`. Successful mutation upgrades schema 4/5/6
|
|
306
|
-
to schema
|
|
317
|
+
instructions MUST use `report_state`. Successful mutation upgrades schema 4/5/6/7
|
|
318
|
+
to schema 8; unsupported schemas remain rejected.
|
|
307
319
|
|
|
308
320
|
## Reporting and dashboard
|
|
309
321
|
|
|
@@ -359,12 +371,13 @@ its displayed summary when present, event time, and missing-task state.
|
|
|
359
371
|
|
|
360
372
|
## Task-log migration
|
|
361
373
|
|
|
362
|
-
`workspace migrate` MUST explicitly convert every supported schema-4/5/6 record
|
|
374
|
+
`workspace migrate` MUST explicitly convert every supported schema-4/5/6/7 record
|
|
363
375
|
under the shared lock. Each legacy semantic result becomes a request-unknown
|
|
364
|
-
completed turn; a newer working state becomes a final unfinished turn
|
|
365
|
-
|
|
376
|
+
completed turn; a newer working state becomes a final unfinished turn, and a
|
|
377
|
+
schema-7 timeline is preserved losslessly. Migration MUST validate the complete
|
|
378
|
+
source and complete schema-8 candidate before changing the task log,
|
|
366
379
|
create and read back an exclusive recovery backup, atomically replace the log,
|
|
367
|
-
and validate the installed result. A fully schema-
|
|
380
|
+
and validate the installed result. A fully schema-8 log MUST be an idempotent
|
|
368
381
|
no-op without another backup. Invalid/unsupported input MUST remain untouched;
|
|
369
382
|
failures after backup creation MUST report the backup path and MUST never
|
|
370
383
|
partially rewrite individual lines.
|
package/docs/workflows.md
CHANGED
|
@@ -89,7 +89,7 @@ sequenceDiagram
|
|
|
89
89
|
D->>D: Choose one configured and native project
|
|
90
90
|
D->>M: record_task(id, project, title, instruction, null)
|
|
91
91
|
M->>W: recordTask()
|
|
92
|
-
W->>W: Lock, validate, append schema-
|
|
92
|
+
W->>W: Lock, validate, append schema-8 snapshot
|
|
93
93
|
W-->>M: working link-pending task
|
|
94
94
|
M-->>D: task
|
|
95
95
|
D->>C: Create executor with marked instruction
|
|
@@ -189,6 +189,43 @@ sequenceDiagram
|
|
|
189
189
|
The executor contract therefore requires a new exact read on every follow-up;
|
|
190
190
|
cached or inherited turn IDs are invalid.
|
|
191
191
|
|
|
192
|
+
## Interrupted-turn recovery
|
|
193
|
+
|
|
194
|
+
A crash, MCP failure, app restart, or upgrade can leave the latest turn with a
|
|
195
|
+
null result. A newer valid `working` report is the durable recovery signal. The
|
|
196
|
+
workspace handles it inside the same lock and atomic replacement as every
|
|
197
|
+
other lifecycle mutation:
|
|
198
|
+
|
|
199
|
+
```mermaid
|
|
200
|
+
sequenceDiagram
|
|
201
|
+
autonumber
|
|
202
|
+
participant E as Resumed executor
|
|
203
|
+
participant M as TaskChef MCP
|
|
204
|
+
participant W as workspace.js
|
|
205
|
+
participant F as tasks.jsonl
|
|
206
|
+
E->>M: report_state(..., turnB, working, requestB)
|
|
207
|
+
M->>W: reportTaskState()
|
|
208
|
+
W->>W: Acquire workspace lock and validate turnB > turnA
|
|
209
|
+
W->>W: Close unfinished turnA as interrupted
|
|
210
|
+
W->>W: Append turnB with requestB and null result
|
|
211
|
+
W->>F: One atomic schema-8 replacement
|
|
212
|
+
W-->>M: working task projected from turnB
|
|
213
|
+
M-->>E: Idempotent recovery success
|
|
214
|
+
E->>M: late semantic result for turnA
|
|
215
|
+
M->>W: Validate active turn and historical outcome
|
|
216
|
+
W-->>M: Reject stale result
|
|
217
|
+
M-->>E: Visible tool error
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The interrupted outcome uses only the fixed TaskChef-authored summary. It is
|
|
221
|
+
visible in CLI and detail timelines but excluded from semantic `results` and
|
|
222
|
+
`lastResult`. Compact dashboard cards therefore show request B with “In
|
|
223
|
+
progress.” Notification reconciliation observes one new working event and does
|
|
224
|
+
not manufacture a failed-result event. Exact retries of either working start
|
|
225
|
+
return the current snapshot without reopening or duplicating a turn. Concurrent
|
|
226
|
+
newer starts serialize under the lock, leaving one ordered timeline whose only
|
|
227
|
+
unfinished entry is the latest turn.
|
|
228
|
+
|
|
192
229
|
## Link-pending and failure paths
|
|
193
230
|
|
|
194
231
|
A failed or interrupted link never authorizes substantive work. The record
|
|
@@ -306,14 +343,15 @@ all, and ordinary rerendering do not re-announce retained history. Toast action
|
|
|
306
343
|
labels remain concise while `aria-describedby` connects the visible summary,
|
|
307
344
|
event time, and missing-task explanation for assistive technology.
|
|
308
345
|
|
|
309
|
-
## Schema 4/5/6 migration
|
|
346
|
+
## Schema 4/5/6/7 migration
|
|
310
347
|
|
|
311
348
|
`taskchef workspace migrate` acquires the same workspace lock as lifecycle
|
|
312
349
|
writers, validates the complete legacy log, converts schema-4/5/6 results into
|
|
313
350
|
request-unknown completed turns and preserves a newer working turn, then validates the
|
|
314
351
|
complete candidate. Before replacement it writes and reads back an exclusive
|
|
315
|
-
`tasks.jsonl.pre-
|
|
316
|
-
|
|
352
|
+
`tasks.jsonl.pre-v8-*.bak` file. Schema-7 timelines are copied losslessly into
|
|
353
|
+
schema 8. The task log is replaced atomically and validated again. A second run
|
|
354
|
+
sees only schema 8 and returns unchanged without
|
|
317
355
|
another backup. Unsupported or malformed input fails before backup/rewrite;
|
|
318
356
|
after a later filesystem failure, the reported backup is the recovery source.
|
|
319
357
|
|
|
@@ -330,8 +368,9 @@ summary is cryptographically authenticated; this is a local single-user trust
|
|
|
330
368
|
model. Managed files, instructions, project snapshots, MCP inputs, and dashboard
|
|
331
369
|
requests are validated at every action boundary.
|
|
332
370
|
|
|
333
|
-
Configuration schema 2 and task schemas 4, 5, 6, and
|
|
334
|
-
4/5/6 are read/migration compatibility until an explicit migration or lifecycle
|
|
335
|
-
mutation upgrades each record to schema
|
|
336
|
-
|
|
371
|
+
Configuration schema 2 and task schemas 4, 5, 6, 7, and 8 are accepted. Schemas
|
|
372
|
+
4/5/6/7 are read/migration compatibility until an explicit migration or lifecycle
|
|
373
|
+
mutation upgrades each record to schema 8. Schema 8 persists `turns`, including
|
|
374
|
+
timeline-only interrupted outcomes, and derives semantic-only `results` and
|
|
375
|
+
`lastResult` plus `latestTurn` for compact compatibility. Other schemas
|
|
337
376
|
are rejected without rewrite.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskchef",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.10.0",
|
|
4
4
|
"description": "A non-blocking interactive dispatcher for visible Codex tasks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Favo Yang",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"docs/images/dashboard-identity.jpg",
|
|
30
30
|
"docs/images/notification-event-snapshots.jpg",
|
|
31
31
|
"docs/images/result-history-dashboard.jpg",
|
|
32
|
+
"docs/images/interrupted-turn-recovery.jpg",
|
|
32
33
|
"index.js",
|
|
33
34
|
"mcp",
|
|
34
35
|
"scripts/benchmark-dispatch-prepare.js",
|
|
@@ -35,6 +35,12 @@ Complete this lifecycle setup before substantive assignment work:
|
|
|
35
35
|
current turn ID, `status: working`, an omitted or null result summary, and a
|
|
36
36
|
concise `requestSummary` describing this turn's assignment or follow-up.
|
|
37
37
|
|
|
38
|
+
If the preceding TaskChef turn is still unfinished because its terminal report
|
|
39
|
+
was lost, this newer valid working report atomically records that predecessor
|
|
40
|
+
as interrupted and starts the current turn. Continue the real assignment from
|
|
41
|
+
the current request. Do not manufacture a semantic `failed` result for the old
|
|
42
|
+
turn and do not retry an old terminal report.
|
|
43
|
+
|
|
38
44
|
If `CODEX_THREAD_ID`, exact native thread reading, or a required TaskChef tool
|
|
39
45
|
is unavailable, or if linking or the working-state report fails, report the
|
|
40
46
|
failure visibly and stop before substantive work. Retry on a later turn. Never
|
|
@@ -53,10 +59,11 @@ the current turn with one semantic status and a concise summary:
|
|
|
53
59
|
A live native approval prompt is Codex state, not semantic `needs_input`; leave
|
|
54
60
|
the approval live instead of storing it as a TaskChef result. Never invent or
|
|
55
61
|
reuse a turn ID after a follow-up. If a final-report response is lost, an
|
|
56
|
-
identical retry is safe only while the same turn remains current. On
|
|
57
|
-
turn, run the start lifecycle with its new current turn ID
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
identical terminal retry is safe only while the same turn remains current. On
|
|
63
|
+
a later turn, run the start lifecycle with its new current turn ID; TaskChef
|
|
64
|
+
will preserve the predecessor as interrupted, and only the new turn may receive
|
|
65
|
+
a semantic result. Say reporting failures visibly instead of claiming a tracked
|
|
66
|
+
outcome.
|
|
60
67
|
|
|
61
68
|
Request and result summaries must omit secrets, transcripts, raw command output, hidden reasoning,
|
|
62
69
|
and unnecessary personal data. Identical lifecycle retries are safe; never
|
|
@@ -44,14 +44,18 @@ all deterministic task-log operations.
|
|
|
44
44
|
detailed read. Native approval is live Codex state, not a `needs_input`
|
|
45
45
|
callback. An inactive status never proves semantic completion; it only
|
|
46
46
|
permits a trustworthy cached MCP result to stand.
|
|
47
|
-
4. In schema
|
|
48
|
-
pairs a bounded `requestSummary` with
|
|
49
|
-
while work is in progress.
|
|
47
|
+
4. In schema 8, treat `turns` as the ordered request/result timeline. Each turn
|
|
48
|
+
pairs a bounded `requestSummary` with one semantic result, a TaskChef-generated
|
|
49
|
+
`interrupted` outcome, or null while work is in progress. Describe an
|
|
50
|
+
interrupted historical turn plainly as interrupted/abandoned, never failed.
|
|
51
|
+
`latestTurn` is the compact current pair.
|
|
50
52
|
`results` and `lastResult` remain derived compatibility projections; do not
|
|
51
|
-
|
|
53
|
+
expect interrupted outcomes in either projection and do not pair the latest
|
|
54
|
+
request with an earlier result. Treat a failed result with
|
|
52
55
|
null thread and turn IDs as a fresh executor-creation failure. Schema 4/5/6
|
|
53
56
|
snapshots normalize their semantic results into legacy turns with a null
|
|
54
|
-
request summary without rewriting their log line.
|
|
57
|
+
request summary without rewriting their log line. Schema 7 timelines remain
|
|
58
|
+
readable but cannot contain the schema-8 interrupted outcome.
|
|
55
59
|
No live read is possible or needed for that creation failure. When identity is certain and
|
|
56
60
|
metadata says the thread is inactive, trust the latest semantic result by
|
|
57
61
|
default in a broad overview unless a newer working state makes it historical.
|
package/src/dashboard/app.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
reconcileNotifications,
|
|
13
13
|
taskStatusLabel,
|
|
14
14
|
taskWithinDateFilter,
|
|
15
|
+
turnPresentation,
|
|
15
16
|
} from "./state.js";
|
|
16
17
|
import { openTaskFromControl } from "./actions.js";
|
|
17
18
|
import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
@@ -237,11 +238,12 @@ function turnTimeline(task) {
|
|
|
237
238
|
const header = document.createElement("div");
|
|
238
239
|
header.className = "result-history-header";
|
|
239
240
|
const status = document.createElement("span");
|
|
240
|
-
const
|
|
241
|
+
const presentation = turnPresentation(turn);
|
|
242
|
+
const turnStatus = presentation.status;
|
|
241
243
|
status.className = `status status-${turnStatus}`;
|
|
242
244
|
status.textContent = turnStatus.replaceAll("_", " ");
|
|
243
245
|
const turnKey = turn.turnId ?? `no-turn:${index}`;
|
|
244
|
-
const timestamp = timestampControl(
|
|
246
|
+
const timestamp = timestampControl(presentation.updatedAt, {
|
|
245
247
|
accessibleName: `Turn updated time for ${turnStatus.replaceAll("_", " ")}`,
|
|
246
248
|
key: `detail:${task.id}:turn:${turnKey}`,
|
|
247
249
|
});
|
|
@@ -255,7 +257,7 @@ function turnTimeline(task) {
|
|
|
255
257
|
resultLabel.textContent = "Result";
|
|
256
258
|
const result = document.createElement("p");
|
|
257
259
|
result.className = "preserve-lines";
|
|
258
|
-
result.textContent =
|
|
260
|
+
result.textContent = presentation.summary;
|
|
259
261
|
const turnMetadata = document.createElement("p");
|
|
260
262
|
turnMetadata.className = "result-history-turn";
|
|
261
263
|
turnMetadata.textContent = turn.turnId
|
package/src/dashboard/state.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const MAX_NOTIFICATIONS = 50;
|
|
2
|
+
const INTERRUPTED_TURN_SUMMARY = "Turn interrupted before a terminal report.";
|
|
2
3
|
export const KNOWN_TASK_STATUSES = [
|
|
3
4
|
"working",
|
|
4
5
|
"needs input",
|
|
@@ -49,6 +50,15 @@ export function latestTurnPresentation(task) {
|
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export function turnPresentation(turn) {
|
|
54
|
+
const result = turn.result ?? null;
|
|
55
|
+
return {
|
|
56
|
+
status: result?.status ?? "working",
|
|
57
|
+
summary: result?.summary ?? "In progress",
|
|
58
|
+
updatedAt: result?.updatedAt ?? turn.startedAt,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
export function mergeProjectedTurns(task, preservedTurns = []) {
|
|
53
63
|
if (Array.isArray(task.turns)) return task.turns;
|
|
54
64
|
if (!task.latestTurn) return preservedTurns;
|
|
@@ -57,6 +67,20 @@ export function mergeProjectedTurns(task, preservedTurns = []) {
|
|
|
57
67
|
if (lastIndex >= 0 && turns[lastIndex].turnId === task.latestTurn.turnId) {
|
|
58
68
|
turns[lastIndex] = task.latestTurn;
|
|
59
69
|
} else {
|
|
70
|
+
if (
|
|
71
|
+
task.schemaVersion === 8
|
|
72
|
+
&& lastIndex >= 0
|
|
73
|
+
&& turns[lastIndex].result === null
|
|
74
|
+
) {
|
|
75
|
+
turns[lastIndex] = {
|
|
76
|
+
...turns[lastIndex],
|
|
77
|
+
result: {
|
|
78
|
+
status: "interrupted",
|
|
79
|
+
summary: INTERRUPTED_TURN_SUMMARY,
|
|
80
|
+
updatedAt: task.latestTurn.startedAt,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
60
84
|
turns.push(task.latestTurn);
|
|
61
85
|
}
|
|
62
86
|
return turns;
|
package/src/dashboard/styles.css
CHANGED
|
@@ -94,6 +94,7 @@ time, .timestamp-missing { font-size: 0.78rem; }
|
|
|
94
94
|
.status-completed { background: var(--accent-soft); color: var(--accent); }
|
|
95
95
|
.status-failed { background: #f3dfdd; color: var(--danger); }
|
|
96
96
|
.status-needs_input { background: #f4e9ce; color: var(--warning); }
|
|
97
|
+
.status-interrupted { background: #e8e5df; color: #625d54; }
|
|
97
98
|
|
|
98
99
|
.empty-state { padding: 60px 20px; border: 1px dashed var(--border); border-radius: 10px; text-align: center; color: var(--muted); }
|
|
99
100
|
.empty-state h2 { margin-bottom: 6px; color: var(--text); }
|
|
@@ -177,4 +178,5 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
|
|
|
177
178
|
.dashboard-message { border-color: #66592e; background: #38321d; color: #f1d98c; }
|
|
178
179
|
.status-failed { background: #4e2927; }
|
|
179
180
|
.status-needs_input { background: #4c3b20; }
|
|
181
|
+
.status-interrupted { background: #37332f; color: #d6d0c7; }
|
|
180
182
|
}
|
package/src/mcp.js
CHANGED
|
@@ -21,7 +21,9 @@ const projectSchema = z.object({
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
const taskSchema = z.object({
|
|
24
|
-
schemaVersion: z.union([
|
|
24
|
+
schemaVersion: z.union([
|
|
25
|
+
z.literal(4), z.literal(5), z.literal(6), z.literal(7), z.literal(8),
|
|
26
|
+
]),
|
|
25
27
|
id: z.string(),
|
|
26
28
|
project: projectSchema,
|
|
27
29
|
title: z.string(),
|
|
@@ -38,7 +40,7 @@ const taskSchema = z.object({
|
|
|
38
40
|
requestSummary: z.string().nullable(),
|
|
39
41
|
startedAt: z.string(),
|
|
40
42
|
result: z.object({
|
|
41
|
-
status: z.enum(["needs_input", "completed", "failed"]),
|
|
43
|
+
status: z.enum(["needs_input", "completed", "failed", "interrupted"]),
|
|
42
44
|
summary: z.string(),
|
|
43
45
|
updatedAt: z.string(),
|
|
44
46
|
}).nullable(),
|
|
@@ -48,7 +50,7 @@ const taskSchema = z.object({
|
|
|
48
50
|
requestSummary: z.string().nullable(),
|
|
49
51
|
startedAt: z.string(),
|
|
50
52
|
result: z.object({
|
|
51
|
-
status: z.enum(["needs_input", "completed", "failed"]),
|
|
53
|
+
status: z.enum(["needs_input", "completed", "failed", "interrupted"]),
|
|
52
54
|
summary: z.string(),
|
|
53
55
|
updatedAt: z.string(),
|
|
54
56
|
}).nullable(),
|
package/src/workspace.js
CHANGED
|
@@ -38,8 +38,9 @@ const DISPATCH_FILE_NAME = "tasks.jsonl";
|
|
|
38
38
|
const WORKSPACE_LOCK_NAME = ".taskchef-workspace.lock";
|
|
39
39
|
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
40
40
|
const CURRENT_CONFIG_SCHEMA_VERSION = 2;
|
|
41
|
-
const CURRENT_TASK_SCHEMA_VERSION =
|
|
42
|
-
const PREVIOUS_TASK_SCHEMA_VERSION =
|
|
41
|
+
const CURRENT_TASK_SCHEMA_VERSION = 8;
|
|
42
|
+
const PREVIOUS_TASK_SCHEMA_VERSION = 7;
|
|
43
|
+
const LEGACY_RESULTS_TASK_SCHEMA_VERSION = 6;
|
|
43
44
|
const FIRST_SELF_LINKING_TASK_SCHEMA_VERSION = 4;
|
|
44
45
|
const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
|
|
45
46
|
const PROJECT_FIELDS = new Set([
|
|
@@ -75,6 +76,7 @@ const RECORD_DISPATCH_FIELDS = new Set([
|
|
|
75
76
|
"threadId",
|
|
76
77
|
]);
|
|
77
78
|
const RESULT_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
79
|
+
const TURN_RESULT_STATUSES = new Set([...RESULT_STATUSES, "interrupted"]);
|
|
78
80
|
const TASK_STATUSES = new Set(["working", ...RESULT_STATUSES]);
|
|
79
81
|
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "mcp"]);
|
|
80
82
|
const MAX_RESULT_SUMMARY_LENGTH = 2_000;
|
|
@@ -82,6 +84,7 @@ const MAX_REQUEST_SUMMARY_LENGTH = 1_000;
|
|
|
82
84
|
const RESULT_FIELDS = new Set(["status", "summary", "turnId", "updatedAt"]);
|
|
83
85
|
const TURN_RESULT_FIELDS = new Set(["status", "summary", "updatedAt"]);
|
|
84
86
|
const TURN_FIELDS = new Set(["turnId", "requestSummary", "startedAt", "result"]);
|
|
87
|
+
const INTERRUPTED_TURN_SUMMARY = "Turn interrupted before a terminal report.";
|
|
85
88
|
|
|
86
89
|
function requireExactFields(value, fields, name) {
|
|
87
90
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -228,7 +231,7 @@ async function appendDispatchesAtomic(workspaceRoot, dispatches) {
|
|
|
228
231
|
const dispatchPath = path.join(workspaceRoot, DISPATCH_FILE_NAME);
|
|
229
232
|
const content = await readFile(dispatchPath, "utf8");
|
|
230
233
|
const appended = dispatches
|
|
231
|
-
.map((dispatch) => `${JSON.stringify(
|
|
234
|
+
.map((dispatch) => `${JSON.stringify(currentSchemaTask(dispatch))}\n`)
|
|
232
235
|
.join("");
|
|
233
236
|
await writeTextAtomic(dispatchPath, `${content}${appended}`);
|
|
234
237
|
}
|
|
@@ -689,6 +692,7 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
689
692
|
const supportedVersions = [
|
|
690
693
|
FIRST_SELF_LINKING_TASK_SCHEMA_VERSION,
|
|
691
694
|
5,
|
|
695
|
+
LEGACY_RESULTS_TASK_SCHEMA_VERSION,
|
|
692
696
|
PREVIOUS_TASK_SCHEMA_VERSION,
|
|
693
697
|
CURRENT_TASK_SCHEMA_VERSION,
|
|
694
698
|
];
|
|
@@ -697,9 +701,9 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
697
701
|
}
|
|
698
702
|
requireExactFields(
|
|
699
703
|
dispatch,
|
|
700
|
-
dispatch.schemaVersion
|
|
704
|
+
dispatch.schemaVersion >= PREVIOUS_TASK_SCHEMA_VERSION
|
|
701
705
|
? DISPATCH_FIELDS
|
|
702
|
-
: dispatch.schemaVersion ===
|
|
706
|
+
: dispatch.schemaVersion === LEGACY_RESULTS_TASK_SCHEMA_VERSION
|
|
703
707
|
? SCHEMA_6_DISPATCH_FIELDS
|
|
704
708
|
: dispatch.schemaVersion === 5
|
|
705
709
|
? SCHEMA_5_DISPATCH_FIELDS
|
|
@@ -746,7 +750,13 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
746
750
|
if (result === null) return null;
|
|
747
751
|
requireExactFields(result, TURN_RESULT_FIELDS, resultName);
|
|
748
752
|
const normalizedResult = {
|
|
749
|
-
status: requireEnum(
|
|
753
|
+
status: requireEnum(
|
|
754
|
+
result.status,
|
|
755
|
+
dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
756
|
+
? TURN_RESULT_STATUSES
|
|
757
|
+
: RESULT_STATUSES,
|
|
758
|
+
`${resultName}.status`,
|
|
759
|
+
),
|
|
750
760
|
summary: optionalString(result.summary, `${resultName}.summary`, {
|
|
751
761
|
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
752
762
|
}),
|
|
@@ -755,6 +765,12 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
755
765
|
if (normalizedResult.summary === null) {
|
|
756
766
|
throw new Error(`${resultName}.summary must be a non-empty string`);
|
|
757
767
|
}
|
|
768
|
+
if (
|
|
769
|
+
normalizedResult.status === "interrupted"
|
|
770
|
+
&& normalizedResult.summary !== INTERRUPTED_TURN_SUMMARY
|
|
771
|
+
) {
|
|
772
|
+
throw new Error(`${resultName}.summary must use the TaskChef interrupted-turn summary`);
|
|
773
|
+
}
|
|
758
774
|
return normalizedResult;
|
|
759
775
|
};
|
|
760
776
|
const normalizeTurn = (turn, turnName) => {
|
|
@@ -770,10 +786,10 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
770
786
|
};
|
|
771
787
|
let legacyResults = [];
|
|
772
788
|
let turns = [];
|
|
773
|
-
if (dispatch.schemaVersion
|
|
789
|
+
if (dispatch.schemaVersion >= PREVIOUS_TASK_SCHEMA_VERSION) {
|
|
774
790
|
if (!Array.isArray(dispatch.turns)) throw new Error(`${name}.turns must be an array`);
|
|
775
791
|
turns = dispatch.turns.map((turn, index) => normalizeTurn(turn, `${name}.turns[${index}]`));
|
|
776
|
-
} else if (dispatch.schemaVersion ===
|
|
792
|
+
} else if (dispatch.schemaVersion === LEGACY_RESULTS_TASK_SCHEMA_VERSION) {
|
|
777
793
|
if (!Array.isArray(dispatch.results)) throw new Error(`${name}.results must be an array`);
|
|
778
794
|
legacyResults = dispatch.results.map((result, index) => (
|
|
779
795
|
normalizeResult(result, `${name}.results[${index}]`)
|
|
@@ -785,7 +801,7 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
785
801
|
} else if (RESULT_STATUSES.has(status)) {
|
|
786
802
|
legacyResults = [{ status, summary, turnId, updatedAt }];
|
|
787
803
|
}
|
|
788
|
-
if (dispatch.schemaVersion
|
|
804
|
+
if (dispatch.schemaVersion < PREVIOUS_TASK_SCHEMA_VERSION) {
|
|
789
805
|
turns = legacyResults.map((result) => ({
|
|
790
806
|
turnId: result.turnId,
|
|
791
807
|
requestSummary: null,
|
|
@@ -807,7 +823,9 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
807
823
|
turns.push({ turnId, requestSummary: null, startedAt: updatedAt, result: null });
|
|
808
824
|
}
|
|
809
825
|
}
|
|
810
|
-
const results = turns.flatMap((turn) =>
|
|
826
|
+
const results = turns.flatMap((turn) => (
|
|
827
|
+
turn.result === null || !RESULT_STATUSES.has(turn.result.status)
|
|
828
|
+
) ? [] : [{
|
|
811
829
|
...turn.result,
|
|
812
830
|
turnId: turn.turnId,
|
|
813
831
|
}]);
|
|
@@ -847,7 +865,9 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
847
865
|
);
|
|
848
866
|
}
|
|
849
867
|
}
|
|
850
|
-
normalized.results = normalized.turns.flatMap((turn) =>
|
|
868
|
+
normalized.results = normalized.turns.flatMap((turn) => (
|
|
869
|
+
turn.result === null || !RESULT_STATUSES.has(turn.result.status)
|
|
870
|
+
) ? [] : [{
|
|
851
871
|
...turn.result,
|
|
852
872
|
turnId: turn.turnId,
|
|
853
873
|
}]);
|
|
@@ -1076,7 +1096,7 @@ export async function parseTaskLogContent(workspaceRoot, content) {
|
|
|
1076
1096
|
return (await parseDispatchRecordsUnlocked(root, content)).map((record) => record.normalized);
|
|
1077
1097
|
}
|
|
1078
1098
|
|
|
1079
|
-
function
|
|
1099
|
+
function currentSchemaTask(dispatch, patch = {}) {
|
|
1080
1100
|
const {
|
|
1081
1101
|
latestTurn: _latestTurn,
|
|
1082
1102
|
results: _results,
|
|
@@ -1115,13 +1135,13 @@ export async function migrateTaskLog(workspaceRoot, {
|
|
|
1115
1135
|
backupPath: null,
|
|
1116
1136
|
};
|
|
1117
1137
|
}
|
|
1118
|
-
const lines = records.map((record) => JSON.stringify(
|
|
1138
|
+
const lines = records.map((record) => JSON.stringify(currentSchemaTask(record.normalized)));
|
|
1119
1139
|
const migrated = lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
|
1120
1140
|
await parseDispatchRecordsUnlocked(root, migrated);
|
|
1121
1141
|
const timestamp = requireTimestamp(now(), "migration timestamp")
|
|
1122
1142
|
.replaceAll(":", "-")
|
|
1123
1143
|
.replaceAll(".", "-");
|
|
1124
|
-
const backupPath = `${dispatchPath}.pre-
|
|
1144
|
+
const backupPath = `${dispatchPath}.pre-v8-${timestamp}-${randomUUID()}.bak`;
|
|
1125
1145
|
await writeFile(backupPath, original, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
1126
1146
|
if (await readFile(backupPath, "utf8") !== original) {
|
|
1127
1147
|
throw new Error(`task log backup validation failed: ${backupPath}`);
|
|
@@ -1209,7 +1229,7 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1209
1229
|
throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
|
|
1210
1230
|
}
|
|
1211
1231
|
if (dispatch.threadId === durableThreadId) return dispatch;
|
|
1212
|
-
const canonical = await validateDispatchShape(
|
|
1232
|
+
const canonical = await validateDispatchShape(currentSchemaTask(dispatch, {
|
|
1213
1233
|
threadId: durableThreadId,
|
|
1214
1234
|
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
1215
1235
|
updatedBy: "mcp",
|
|
@@ -1238,7 +1258,7 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1238
1258
|
))) {
|
|
1239
1259
|
throw new Error(`threadId is already recorded: ${durableThreadId}`);
|
|
1240
1260
|
}
|
|
1241
|
-
const linked = await validateDispatchShape(
|
|
1261
|
+
const linked = await validateDispatchShape(currentSchemaTask(dispatch, {
|
|
1242
1262
|
threadId: durableThreadId,
|
|
1243
1263
|
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
1244
1264
|
updatedBy: "mcp",
|
|
@@ -1252,7 +1272,7 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1252
1272
|
}
|
|
1253
1273
|
|
|
1254
1274
|
function dispatchLineWithState(dispatch, patch) {
|
|
1255
|
-
return JSON.stringify(
|
|
1275
|
+
return JSON.stringify(currentSchemaTask(dispatch, patch));
|
|
1256
1276
|
}
|
|
1257
1277
|
|
|
1258
1278
|
function normalizeTaskStateInput(input, { allowWorking }) {
|
|
@@ -1374,6 +1394,21 @@ async function reportTaskStateInternal(
|
|
|
1374
1394
|
}
|
|
1375
1395
|
if (status === "working") {
|
|
1376
1396
|
const sameWorkingTurn = dispatch.status === "working" && stateTurnId === dispatch.turnId;
|
|
1397
|
+
const recordedTurn = dispatch.turns.find((turn) => turn.turnId === stateTurnId);
|
|
1398
|
+
if (!sameWorkingTurn && recordedTurn) {
|
|
1399
|
+
if (
|
|
1400
|
+
requestSummary !== null
|
|
1401
|
+
&& recordedTurn.requestSummary !== requestSummary
|
|
1402
|
+
) {
|
|
1403
|
+
throw new Error(`working turn already has a different requestSummary: ${id}`);
|
|
1404
|
+
}
|
|
1405
|
+
if (
|
|
1406
|
+
recordedTurn.requestSummary !== null
|
|
1407
|
+
|| recordedTurn.result?.status === "interrupted"
|
|
1408
|
+
) {
|
|
1409
|
+
return dispatch;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1377
1412
|
if (sameWorkingTurn) {
|
|
1378
1413
|
const storedRequest = dispatch.latestTurn?.requestSummary ?? null;
|
|
1379
1414
|
if (
|
|
@@ -1401,17 +1436,33 @@ async function reportTaskStateInternal(
|
|
|
1401
1436
|
const updatedAt = sameWorkingTurn
|
|
1402
1437
|
? dispatch.updatedAt
|
|
1403
1438
|
: transitionTimestamp(now, dispatch.updatedAt);
|
|
1439
|
+
const recoveredTurns = (
|
|
1440
|
+
!sameWorkingTurn
|
|
1441
|
+
&& dispatch.status === "working"
|
|
1442
|
+
&& dispatch.latestTurn?.result === null
|
|
1443
|
+
)
|
|
1444
|
+
? dispatch.turns.map((turn, turnIndex) => turnIndex === dispatch.turns.length - 1
|
|
1445
|
+
? {
|
|
1446
|
+
...turn,
|
|
1447
|
+
result: {
|
|
1448
|
+
status: "interrupted",
|
|
1449
|
+
summary: INTERRUPTED_TURN_SUMMARY,
|
|
1450
|
+
updatedAt,
|
|
1451
|
+
},
|
|
1452
|
+
}
|
|
1453
|
+
: turn)
|
|
1454
|
+
: dispatch.turns;
|
|
1404
1455
|
const turns = sameWorkingTurn
|
|
1405
1456
|
? dispatch.turns.map((turn, turnIndex) => turnIndex === dispatch.turns.length - 1
|
|
1406
1457
|
? { ...turn, requestSummary: turn.requestSummary ?? requestSummary }
|
|
1407
1458
|
: turn)
|
|
1408
|
-
: [...
|
|
1459
|
+
: [...recoveredTurns, {
|
|
1409
1460
|
turnId: stateTurnId,
|
|
1410
1461
|
requestSummary,
|
|
1411
1462
|
startedAt: updatedAt,
|
|
1412
1463
|
result: null,
|
|
1413
1464
|
}];
|
|
1414
|
-
const updated = await validateDispatchShape(
|
|
1465
|
+
const updated = await validateDispatchShape(currentSchemaTask(dispatch, {
|
|
1415
1466
|
status,
|
|
1416
1467
|
summary: null,
|
|
1417
1468
|
turnId: stateTurnId,
|
|
@@ -1465,7 +1516,7 @@ async function reportTaskStateInternal(
|
|
|
1465
1516
|
? { ...turn, result: turnResult }
|
|
1466
1517
|
: turn);
|
|
1467
1518
|
}
|
|
1468
|
-
const candidate =
|
|
1519
|
+
const candidate = currentSchemaTask(dispatch, {
|
|
1469
1520
|
status,
|
|
1470
1521
|
summary,
|
|
1471
1522
|
turnId: stateTurnId,
|