taskchef 7.9.0 → 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 -7
- package/docs/firstmate-taskchef-comparison.md +5 -2
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/spec.md +43 -29
- package/docs/workflows.md +47 -8
- package/package.json +2 -1
- package/skills/taskchef-delegate/SKILL.md +3 -2
- package/skills/taskchef-executor/SKILL.md +25 -16
- 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/delegation.js +10 -6
- 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:
|
|
@@ -104,7 +104,6 @@ For example, TaskChef generates this shape:
|
|
|
104
104
|
|
|
105
105
|
```text
|
|
106
106
|
Fix duplicate charges after a retry and add a regression test.
|
|
107
|
-
|
|
108
107
|
<!-- taskchef_id=c0f010ff-84f2-4838-a69d-0ff1f5d721d7 -->
|
|
109
108
|
Use $taskchef-executor to execute and report this delegated TaskChef assignment.
|
|
110
109
|
```
|
|
@@ -124,7 +123,12 @@ appends a `turns` entry that pairs that request with a null result while working
|
|
|
124
123
|
then fills the same entry with the semantic outcome. A follow-up therefore shows
|
|
125
124
|
its own request with “In progress,” never the preceding turn's result. Returned
|
|
126
125
|
tasks still derive `results` and `lastResult` as compatibility projections.
|
|
127
|
-
|
|
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.
|
|
128
132
|
|
|
129
133
|
Delegated tasks created by earlier TaskChef versions remain compatible: their
|
|
130
134
|
inline executor protocol still parses, self-links, and may use the deprecated
|
|
@@ -176,7 +180,7 @@ taskchef dashboard --port 3211
|
|
|
176
180
|
The loopback dashboard watches `tasks.jsonl`, groups current states, and opens
|
|
177
181
|
linked Codex tasks. List snapshots and SSE events carry only the latest
|
|
178
182
|
request/result pair; opening task details fetches the full newest-first activity
|
|
179
|
-
timeline.
|
|
183
|
+
timeline, including clearly labeled interrupted turns.
|
|
180
184
|
The header shows the running TaskChef package version reported by the same
|
|
181
185
|
bounded health identity used for compatible-listener checks.
|
|
182
186
|
Task and result times are relative through 29 days (with minute detail for the
|
|
@@ -206,6 +210,8 @@ no task data, credentials, environment variables, process control, or secrets.
|
|
|
206
210
|
|
|
207
211
|

|
|
208
212
|
|
|
213
|
+

|
|
214
|
+
|
|
209
215
|
## Common recovery
|
|
210
216
|
|
|
211
217
|
Check the managed workspace:
|
|
@@ -219,8 +225,8 @@ taskchef doctor
|
|
|
219
225
|
|
|
220
226
|
`doctor` is read-only. `workspace init` creates missing files and refreshes
|
|
221
227
|
managed instructions. `workspace migrate` explicitly upgrades supported schema
|
|
222
|
-
4/5/6 task lines to schema
|
|
223
|
-
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`
|
|
224
230
|
backup, atomically replaces the log, validates the result, and becomes an
|
|
225
231
|
idempotent no-op after migration. If replacement fails, the original remains
|
|
226
232
|
or the reported backup can be restored; unsupported or invalid input is rejected
|
|
@@ -231,7 +237,7 @@ executor so its first action can retry `link_task`. Do not guess an identity
|
|
|
231
237
|
or edit `tasks.jsonl`. If native task creation failed, the record is retained
|
|
232
238
|
as `failed` with null thread and turn IDs.
|
|
233
239
|
|
|
234
|
-
Schemas other than 4, 5, 6, and
|
|
240
|
+
Schemas other than 4, 5, 6, 7, and 8 remain unsupported. Retain such a workspace
|
|
235
241
|
unchanged and create a current workspace; the migration command deliberately
|
|
236
242
|
does not guess how to convert unknown formats.
|
|
237
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.
|
|
@@ -116,10 +119,10 @@ the final link, preserving the delegate skill's immediate-return contract.
|
|
|
116
119
|
1. The dispatcher MUST call `prepare_dispatch` once per outcome.
|
|
117
120
|
2. It MUST choose exactly one configured project and exact native-project path.
|
|
118
121
|
3. It MUST build the instruction with the user's outcome beginning on line 1
|
|
119
|
-
and remaining uninterrupted, followed by one
|
|
120
|
-
marker, and exactly one concise explicit `$taskchef-executor`
|
|
121
|
-
the final line. It MUST NOT
|
|
122
|
-
instruction.
|
|
122
|
+
and remaining uninterrupted, followed by exactly one newline, the returned
|
|
123
|
+
marker, one newline, and exactly one concise explicit `$taskchef-executor`
|
|
124
|
+
invocation on the final line. It MUST NOT place a blank line before or after
|
|
125
|
+
the marker or inline the executor protocol into a new instruction.
|
|
123
126
|
4. It MUST call `record_task` with `threadId: null` before native creation.
|
|
124
127
|
5. It MUST create exactly one native Codex executor and return immediately.
|
|
125
128
|
6. The executor MUST read its own `CODEX_THREAD_ID` and call `link_task`
|
|
@@ -129,16 +132,19 @@ 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.
|
|
136
141
|
A link failure MUST remain visible and retryable; the executor MUST report it
|
|
137
142
|
visibly and MUST NOT continue substantive work.
|
|
138
143
|
|
|
139
|
-
Previously recorded instructions with
|
|
140
|
-
first-line
|
|
141
|
-
|
|
144
|
+
Previously recorded instructions with the former blank line before a trailing
|
|
145
|
+
marker, a first-line HTML marker, the older first-line
|
|
146
|
+
`# taskchef_id=<full UUID>` heading, or the former blank line and inline
|
|
147
|
+
executor protocol MUST remain marker-readable and executable. Their
|
|
142
148
|
`report_result` calls MUST remain supported by the deprecated alias. New
|
|
143
149
|
instructions MUST use the trailing marker and explicit executor skill contract
|
|
144
150
|
above. A historical first-line instruction with an executor-skill invocation
|
|
@@ -237,7 +243,7 @@ new preparation values, though it writes no state.
|
|
|
237
243
|
|
|
238
244
|
**Structured output:** `{ task: Task }`.
|
|
239
245
|
|
|
240
|
-
The returned task has schema
|
|
246
|
+
The returned task has schema 8, `working`, null summary/turn/thread/latestTurn/lastResult,
|
|
241
247
|
empty `turns` and derived `results` arrays,
|
|
242
248
|
`updatedBy: dispatcher`, and equal creation/update timestamps. Duplicate IDs,
|
|
243
249
|
unknown projects, malformed markers, and invalid input fail. Repeating a
|
|
@@ -282,16 +288,23 @@ marker, or ineligible state fails.
|
|
|
282
288
|
|
|
283
289
|
**Structured output:** `{ task: Task }`.
|
|
284
290
|
|
|
285
|
-
For a linked self-linking journey, `working` MUST identify a turn
|
|
286
|
-
the current turn and last semantic result.
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
289
296
|
creation `failed` state with both IDs null. Starting work appends one turn with
|
|
290
|
-
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.
|
|
291
300
|
An identical retry for any settled turn returns success without an append. A
|
|
292
301
|
different request or result for the same turn, a stale turn, or a semantic
|
|
293
302
|
result that does not match the active working turn MUST fail.
|
|
294
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
|
+
|
|
295
308
|
**Annotations:** `readOnlyHint: false`, `destructiveHint: true`,
|
|
296
309
|
`openWorldHint: false`.
|
|
297
310
|
|
|
@@ -301,8 +314,8 @@ result that does not match the active working turn MUST fail.
|
|
|
301
314
|
temporary compatibility alias. It implicitly accepts a fresh supplied turn and
|
|
302
315
|
stores its semantic result in a request-unknown turn, including for supported schema-4/5/6 records and
|
|
303
316
|
low-level opaque direct records. It does not accept `working`. New executor
|
|
304
|
-
instructions MUST use `report_state`. Successful mutation upgrades schema 4/5/6
|
|
305
|
-
to schema
|
|
317
|
+
instructions MUST use `report_state`. Successful mutation upgrades schema 4/5/6/7
|
|
318
|
+
to schema 8; unsupported schemas remain rejected.
|
|
306
319
|
|
|
307
320
|
## Reporting and dashboard
|
|
308
321
|
|
|
@@ -358,12 +371,13 @@ its displayed summary when present, event time, and missing-task state.
|
|
|
358
371
|
|
|
359
372
|
## Task-log migration
|
|
360
373
|
|
|
361
|
-
`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
|
|
362
375
|
under the shared lock. Each legacy semantic result becomes a request-unknown
|
|
363
|
-
completed turn; a newer working state becomes a final unfinished turn
|
|
364
|
-
|
|
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,
|
|
365
379
|
create and read back an exclusive recovery backup, atomically replace the log,
|
|
366
|
-
and validate the installed result. A fully schema-
|
|
380
|
+
and validate the installed result. A fully schema-8 log MUST be an idempotent
|
|
367
381
|
no-op without another backup. Invalid/unsupported input MUST remain untouched;
|
|
368
382
|
failures after backup creation MUST report the backup path and MUST never
|
|
369
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",
|
|
@@ -52,8 +52,9 @@ stop and report that the TaskChef plugin must be reloaded or installed.
|
|
|
52
52
|
|
|
53
53
|
- Begin with the actual assignment on the first line and keep its complete
|
|
54
54
|
body uninterrupted.
|
|
55
|
-
- After the assignment, add one
|
|
56
|
-
marker on its own line.
|
|
55
|
+
- After the assignment's final character, add exactly one newline and the
|
|
56
|
+
preparation's exact marker on its own line. Do not add a blank line before
|
|
57
|
+
or after the marker.
|
|
57
58
|
- Immediately after the marker, end the instruction with exactly:
|
|
58
59
|
`Use $taskchef-executor to execute and report this delegated TaskChef assignment.`
|
|
59
60
|
- Include exactly one marker and exactly one executor-skill invocation.
|
|
@@ -9,12 +9,13 @@ Own and execute the delegated assignment in the current Codex task. Do not
|
|
|
9
9
|
re-dispatch it merely because it concerns TaskChef or a configured project.
|
|
10
10
|
Explicit requests to delegate separate work remain valid.
|
|
11
11
|
|
|
12
|
-
New instructions present the complete assignment first, followed by one
|
|
13
|
-
|
|
14
|
-
explicit skill invocation.
|
|
15
|
-
|
|
16
|
-
marker and invocation are lifecycle scaffolding, not
|
|
17
|
-
Require exactly one marker and do not infer an ID from
|
|
12
|
+
New instructions present the complete assignment first, followed by exactly one
|
|
13
|
+
newline, the exact `<!-- taskchef_id=<full UUID> -->` marker, one newline, and
|
|
14
|
+
the final explicit skill invocation. There is no blank line before or after the
|
|
15
|
+
marker. Treat that UUID as the TaskChef task ID. The assignment is everything
|
|
16
|
+
before the marker; the marker and invocation are lifecycle scaffolding, not
|
|
17
|
+
part of the deliverable. Require exactly one marker and do not infer an ID from
|
|
18
|
+
similar prose.
|
|
18
19
|
|
|
19
20
|
## Start every execution turn
|
|
20
21
|
|
|
@@ -34,6 +35,12 @@ Complete this lifecycle setup before substantive assignment work:
|
|
|
34
35
|
current turn ID, `status: working`, an omitted or null result summary, and a
|
|
35
36
|
concise `requestSummary` describing this turn's assignment or follow-up.
|
|
36
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
|
+
|
|
37
44
|
If `CODEX_THREAD_ID`, exact native thread reading, or a required TaskChef tool
|
|
38
45
|
is unavailable, or if linking or the working-state report fails, report the
|
|
39
46
|
failure visibly and stop before substantive work. Retry on a later turn. Never
|
|
@@ -52,10 +59,11 @@ the current turn with one semantic status and a concise summary:
|
|
|
52
59
|
A live native approval prompt is Codex state, not semantic `needs_input`; leave
|
|
53
60
|
the approval live instead of storing it as a TaskChef result. Never invent or
|
|
54
61
|
reuse a turn ID after a follow-up. If a final-report response is lost, an
|
|
55
|
-
identical retry is safe only while the same turn remains current. On
|
|
56
|
-
turn, run the start lifecycle with its new current turn ID
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
59
67
|
|
|
60
68
|
Request and result summaries must omit secrets, transcripts, raw command output, hidden reasoning,
|
|
61
69
|
and unnecessary personal data. Identical lifecycle retries are safe; never
|
|
@@ -69,12 +77,13 @@ Existing delegated tasks may include the former inline ownership, linking, and
|
|
|
69
77
|
re-dispatching. Prefer `report_state` when available. If an older installed
|
|
70
78
|
TaskChef exposes only `report_result`, follow its inline protocol; after an
|
|
71
79
|
upgrade, the deprecated `report_result` alias remains available for exact
|
|
72
|
-
legacy retries. Also accept historical instructions
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
the
|
|
77
|
-
|
|
80
|
+
legacy retries. Also accept historical instructions with the former blank line
|
|
81
|
+
before a trailing marker, an exact HTML marker on the first line with or
|
|
82
|
+
without the former blank line, or the older exact
|
|
83
|
+
first-line `# taskchef_id=<full UUID>` heading. These compatibility forms do not
|
|
84
|
+
change the identity or lifecycle rules above. For either first-line form, the
|
|
85
|
+
assignment follows the marker. Ignore the final executor invocation and any
|
|
86
|
+
recognizable former inline ownership, linking, working-state, or
|
|
78
87
|
result-reporting paragraphs as lifecycle scaffolding; execute the remaining
|
|
79
88
|
task-specific body. Require non-whitespace task-specific content and never
|
|
80
89
|
treat an invocation by itself as an assignment.
|
|
@@ -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/delegation.js
CHANGED
|
@@ -162,14 +162,18 @@ export function parseTaskChefMarker(instruction) {
|
|
|
162
162
|
return hasHistoricalAssignment() ? id : null;
|
|
163
163
|
}
|
|
164
164
|
const executorSkillReferences = instruction.match(/\$taskchef-executor\b/gi) ?? [];
|
|
165
|
+
const hasCompactBoundary = index >= 1
|
|
166
|
+
&& lines.at(index - 1).trim().length > 0;
|
|
167
|
+
const hasHistoricalBlankBoundary = index >= 2
|
|
168
|
+
&& lines.at(index - 1) === ""
|
|
169
|
+
&& lines.at(index - 2).trim().length > 0;
|
|
170
|
+
const assignmentEnd = hasCompactBoundary ? index : index - 1;
|
|
165
171
|
const isTrailingScaffold = index === lines.length - 2
|
|
166
|
-
&&
|
|
172
|
+
&& (hasCompactBoundary || hasHistoricalBlankBoundary)
|
|
167
173
|
&& lines[0].trim().length > 0
|
|
168
|
-
&& lines.at(-3) === ""
|
|
169
|
-
&& lines.at(-4).trim().length > 0
|
|
170
174
|
&& lines.at(-1) === EXECUTOR_SKILL_INVOCATION
|
|
171
|
-
&& hasTaskSpecificContent(lines.slice(0,
|
|
172
|
-
&& !lines.slice(0,
|
|
175
|
+
&& hasTaskSpecificContent(lines.slice(0, assignmentEnd))
|
|
176
|
+
&& !lines.slice(0, assignmentEnd).some((line) => HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line))
|
|
173
177
|
&& executorSkillReferences.length === 1;
|
|
174
178
|
return isTrailingScaffold ? id : null;
|
|
175
179
|
}
|
|
@@ -198,7 +202,7 @@ export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
|
|
|
198
202
|
const id = requireUuid(taskId);
|
|
199
203
|
return {
|
|
200
204
|
id,
|
|
201
|
-
instruction: `${body}\n
|
|
205
|
+
instruction: `${body}\n${taskChefMarker(id)}\n${EXECUTOR_SKILL_INVOCATION}`,
|
|
202
206
|
};
|
|
203
207
|
}
|
|
204
208
|
|
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,
|