taskchef 5.12.0 → 6.1.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 +5 -2
- package/BACKLOG.md +6 -15
- package/README.md +30 -40
- package/SPEC.md +92 -369
- package/assets/e2e-benchmark-example.json +37 -33
- package/assets/taskchef-dark.svg +9 -0
- package/assets/taskchef.svg +9 -0
- package/docs/delegation-design.md +219 -170
- package/index.js +3 -11
- package/package.json +2 -2
- package/scripts/e2e-benchmark.js +101 -412
- package/skills/taskchef-bootstrap/SKILL.md +2 -3
- package/skills/taskchef-delegate/SKILL.md +60 -138
- package/skills/taskchef-report/SKILL.md +12 -8
- package/src/cli.js +2 -1
- package/src/delegation.js +24 -201
- package/src/mcp.js +10 -10
- package/src/workspace.js +128 -61
- package/hooks/hooks.json +0 -18
- package/hooks/taskchef-initial-prompt.js +0 -17
- package/src/hook.js +0 -96
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: taskchef-delegate
|
|
3
|
-
description: "Dispatch actionable requests through the per-user TaskChef workspace into independently openable Codex project tasks. Use automatically for actionable work received in the canonical TaskChef dispatcher workspace. From any other project, use only when the user explicitly asks to delegate or split separate work into Codex tasks; TaskChef-related subject matter alone is not delegation intent. Record before creation,
|
|
3
|
+
description: "Dispatch actionable requests through the per-user TaskChef workspace into independently openable Codex project tasks. Use automatically for actionable work received in the canonical TaskChef dispatcher workspace. From any other project, use only when the user explicitly asks to delegate or split separate work into Codex tasks; TaskChef-related subject matter alone is not delegation intent. Record before creation, require executor self-linking, and never wait for executor completion."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# TaskChef Delegate
|
|
@@ -12,154 +12,76 @@ and return immediately.
|
|
|
12
12
|
|
|
13
13
|
A task whose initial structured `codexDelegation.input` starts with an exact
|
|
14
14
|
`<!-- taskchef_id=<full UUID> -->` marker already owns that delegated
|
|
15
|
-
assignment. Execute
|
|
16
|
-
|
|
15
|
+
assignment. Execute it in the current task. Do not re-dispatch it merely
|
|
16
|
+
because it concerns TaskChef or a configured project. Explicit requests to
|
|
17
|
+
delegate separate work remain valid.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Use the bundled TaskChef `prepare_dispatch`, `record_task`, `resolve_task`, and `report_result`
|
|
23
|
-
tools for deterministic workspace and task-record operations. Call them
|
|
24
|
-
directly; never probe for them or fall back to shell CLI writes. If a required
|
|
25
|
-
tool is unavailable, stop and report that the TaskChef plugin must be reloaded
|
|
26
|
-
or reinstalled. Resolve the plugin root from this skill only when writing an
|
|
27
|
-
explicitly requested benchmark artifact.
|
|
19
|
+
Use the bundled `prepare_dispatch`, `record_task`, `link_task`, and
|
|
20
|
+
`report_result` MCP tools directly. Never fall back to shell writes. If a
|
|
21
|
+
required tool is unavailable, stop and report that the TaskChef plugin must be
|
|
22
|
+
reloaded or installed.
|
|
28
23
|
|
|
29
24
|
## Boundaries
|
|
30
25
|
|
|
31
|
-
- Keep
|
|
32
|
-
- Keep only `AGENTS.md`, `taskchef.json`, and `tasks.jsonl` in a dispatcher
|
|
26
|
+
- Keep only `AGENTS.md`, `taskchef.json`, and `tasks.jsonl` in the dispatcher
|
|
33
27
|
workspace.
|
|
34
28
|
- Use real Codex tasks, never collaboration or subagent tools.
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
The only permitted thread discovery is the bounded exact-marker resolution
|
|
41
|
-
after a provisional creation result.
|
|
42
|
-
- Never wait for delegated work after executor creation.
|
|
43
|
-
- Never collect transcripts or hidden reasoning.
|
|
29
|
+
- Never use hooks, schedules, daemons, background monitors, recent-task
|
|
30
|
+
searches, transcripts, hidden reasoning, or polling for identity.
|
|
31
|
+
- Never wait for delegated work after native creation.
|
|
32
|
+
- Treat executor-supplied identity as a cooperative assertion in TaskChef's
|
|
33
|
+
local single-user trust boundary, not transport-authenticated proof.
|
|
44
34
|
|
|
45
35
|
## Dispatch
|
|
46
36
|
|
|
47
37
|
1. Split the request into the smallest independently useful outcomes. Include
|
|
48
38
|
constraints, expected testing, and reporting in every instruction.
|
|
49
|
-
2. In parallel, list
|
|
50
|
-
exactly once
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
exact
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`~/.agents/taskchef`; do not substitute the current project. Reject the
|
|
58
|
-
dispatcher workspace itself as a target. Never take a pre-creation thread
|
|
59
|
-
snapshot.
|
|
60
|
-
3. Classify against configured `name`, every URL in the `githubRepos` list, and
|
|
61
|
-
`description`. Use `path` only as checkout identity. Managed `*-workspace`
|
|
62
|
-
projects advertise their child or sub-repositories in this list.
|
|
63
|
-
When the prompt contains a GitHub issue or pull-request URL, canonicalize
|
|
64
|
-
its case-insensitive owner/repository identity, ignoring `http` versus
|
|
65
|
-
`https`, an optional `www`, a trailing slash or `.git`, and the issue or PR
|
|
66
|
-
suffix. Check that identity against every repository URL of every configured
|
|
67
|
-
project. Route on this evidence only when exactly one configured project
|
|
68
|
-
matches. Ask instead of guessing when no project or several projects match.
|
|
69
|
-
4. Resolve the selected configured path against the already-loaded native
|
|
70
|
-
projects and require an exact match. Do not list native projects again.
|
|
71
|
-
5. For each outcome, use only its corresponding task ID, preparation time, and
|
|
72
|
-
marker returned by its own preparation call.
|
|
73
|
-
Prefix the complete executor instruction with
|
|
74
|
-
exactly `<!-- taskchef_id=<full UUID> -->` as the first line, followed by a
|
|
75
|
-
blank line, this executor-role paragraph, another blank line, the result
|
|
76
|
-
paragraph below, another blank line, and the instruction body:
|
|
39
|
+
2. In parallel, list native Codex projects once and call `prepare_dispatch`
|
|
40
|
+
exactly once per outcome. Never reuse a task ID or marker. Do not take a
|
|
41
|
+
pre-creation thread snapshot.
|
|
42
|
+
3. Route against configured project `name`, `description`, and canonical
|
|
43
|
+
`githubRepos`; use `path` only as checkout identity. Require exactly one
|
|
44
|
+
match and an exact native-project path. Ask instead of guessing.
|
|
45
|
+
4. Prefix each executor instruction with the preparation's exact marker as the
|
|
46
|
+
first line, a blank line, and these required paragraphs before the body:
|
|
77
47
|
|
|
78
48
|
> This task owns the delegated assignment. Execute it in this task; do not re-dispatch it merely because it concerns TaskChef or a configured project. Explicit requests to delegate separate work remain valid.
|
|
79
49
|
|
|
80
|
-
> Before
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
8. If creation
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
identity step.
|
|
119
|
-
Treat a nullable record as preserved but not yet linked. Do not read an
|
|
120
|
-
executor for progress and never wait for executor work completion.
|
|
121
|
-
|
|
122
|
-
The bundled structured tools are the only dispatch-time path to the canonical
|
|
123
|
-
workspace. They reuse the same exact-field validation, locking, atomic writes,
|
|
124
|
-
and one-way nullable resolution as the CLI without shell parsing, stdin, or
|
|
125
|
-
per-command filesystem escalation. The package exports repository routing,
|
|
126
|
-
marker, and record-before-create orchestration helpers for deterministic tests.
|
|
127
|
-
Keep the CLI for bootstrap, manual inspection, and recovery outside delegation.
|
|
128
|
-
|
|
129
|
-
## Later resolution
|
|
130
|
-
|
|
131
|
-
The trusted initial hook never resolves identity from its `session_id`. It uses
|
|
132
|
-
only the child ID already stored by immediate or bounded exact-marker
|
|
133
|
-
resolution. For manual recovery, require one exact structured marker match,
|
|
134
|
-
then call `resolve_task` once. Never edit `tasks.jsonl` directly. Resolution is
|
|
135
|
-
an idempotent one-way transition from `threadId: null` to one unique durable
|
|
136
|
-
child thread ID.
|
|
137
|
-
|
|
138
|
-
## Legacy benchmark compatibility
|
|
139
|
-
|
|
140
|
-
The schema-v1 benchmark fixtures below describe the removed snapshot resolver
|
|
141
|
-
and remain only for historical-result validation. Do not use that resolver for
|
|
142
|
-
new delegation benchmarks. A future benchmark schema should measure record,
|
|
143
|
-
creation, bounded exact-marker resolution, and result callback without
|
|
144
|
-
indefinite polling. The
|
|
145
|
-
legacy fixture starts from
|
|
146
|
-
`<plugin-root>/assets/e2e-benchmark-example.json`. Capture one ISO start/end
|
|
147
|
-
interval for the parallel preparation/project-list operation, followed by
|
|
148
|
-
sequential non-overlapping creation, recording, and optional provisional
|
|
149
|
-
resolution stages. Follow the complete schema and workflow transitions in
|
|
150
|
-
`<plugin-root>/SPEC.md`; the example is the durable-success starting shape.
|
|
151
|
-
Omit later stages when an earlier operation stops the workflow.
|
|
152
|
-
Fallback snapshot observations additionally require `recentTaskCount`,
|
|
153
|
-
`candidateCount`, `exactMatchCount`, and `resolveWriteMs`. For each snapshot,
|
|
154
|
-
also record `resolveWriteOutcome` as `not-attempted`, `succeeded`, or `failed`;
|
|
155
|
-
a unique match whose atomic write fails remains unresolved. After the bounded
|
|
156
|
-
workflow ends, verify the canonical task record once and, when resolved, read
|
|
157
|
-
the task once to verify the requested output. A failed verification remains a
|
|
158
|
-
false validation flag even when the corresponding operation succeeded. Feed
|
|
159
|
-
one exact JSON value to
|
|
160
|
-
`node <plugin-root>/scripts/e2e-benchmark.js write <output-directory>` on closed,
|
|
161
|
-
non-interactive stdin. Keep timestamped results in the TaskChef source
|
|
162
|
-
repository's ignored `reports/e2e-benchmarks/` directory. Never include hidden
|
|
163
|
-
reasoning or transcripts. Use the script's `clean` command before establishing
|
|
164
|
-
a replacement baseline. Mark candidate filtering effective only when the
|
|
165
|
-
fallback snapshot narrows reads below the complete recent-task window.
|
|
50
|
+
> Before any other work, read this executor's own durable Codex thread ID from the current task's CODEX_THREAD_ID environment value and call the TaskChef link_task MCP tool with that thread ID and the marked TaskChef task ID. Never use CODEX_SESSION_ID or the parent or delegator thread ID. If linking fails, CODEX_THREAD_ID is unavailable, or the tool is unavailable, report the failure visibly and retry on a later turn; do not guess an identity or continue substantive work while the task is link-pending.
|
|
51
|
+
|
|
52
|
+
> Before ending, call the TaskChef report_result MCP tool with the marked task ID, this executor's self-linked thread ID, the current turn ID from an exact native read of that same thread, completed, needs_input, or failed, and a concise summary. Never reuse a prior turn ID after a follow-up. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.
|
|
53
|
+
|
|
54
|
+
5. Before creating each executor, call `record_task` exactly once with `id`,
|
|
55
|
+
`project`, `title`, the exact marked `instruction`, and `threadId: null`.
|
|
56
|
+
6. Create one real Codex task using the exact configured project, an appropriate
|
|
57
|
+
native environment, the marked instruction, and a short title.
|
|
58
|
+
7. Return immediately. Preserve a returned provisional client ID only for the
|
|
59
|
+
created-thread directive. Do not call `link_task` from the dispatcher even
|
|
60
|
+
when creation returns a durable ID; the child must self-link.
|
|
61
|
+
8. If creation fails after recording, call `report_result` with `failed`, null
|
|
62
|
+
thread/turn IDs, and a bounded summary before returning the failure.
|
|
63
|
+
|
|
64
|
+
## Executor contract
|
|
65
|
+
|
|
66
|
+
The executor must make `link_task(taskId, threadId)` its first TaskChef action.
|
|
67
|
+
It obtains its own durable ID from the current task's `CODEX_THREAD_ID`, never
|
|
68
|
+
from the delegation's `sourceThreadId`, `CODEX_SESSION_ID`, inherited session
|
|
69
|
+
metadata, title matching, or a parent task.
|
|
70
|
+
Identical retries are safe. A rejected link, unavailable tool, or interrupted
|
|
71
|
+
initial turn leaves the record visibly link-pending and retryable; the executor
|
|
72
|
+
must not guess or do substantive work first.
|
|
73
|
+
|
|
74
|
+
For every semantic result, the executor supplies its linked thread ID and the
|
|
75
|
+
current turn ID obtained by reading that exact thread. A follow-up must use the
|
|
76
|
+
new turn ID. Do not reuse the initial turn ID. `needs_input` is only for a real
|
|
77
|
+
user decision, not live approval UI.
|
|
78
|
+
|
|
79
|
+
The filesystem watcher surfaces `link_task` and `report_result` writes to the
|
|
80
|
+
dashboard. The linked child ID drives the exact Codex deep link.
|
|
81
|
+
|
|
82
|
+
## Legacy recovery
|
|
83
|
+
|
|
84
|
+
`taskchef task resolve` exists only for unresolved records created before the
|
|
85
|
+
self-linking schema. Require an exact marker match and a unique durable child
|
|
86
|
+
ID. The command rejects new self-linking records. Never edit `tasks.jsonl`
|
|
87
|
+
directly.
|
|
@@ -47,11 +47,12 @@ all deterministic task-log operations.
|
|
|
47
47
|
4. Treat `updatedBy: mcp`, `status: failed`, and null thread/turn IDs as a fresh
|
|
48
48
|
executor-creation failure. No live read is possible or needed; report the
|
|
49
49
|
stored failure summary, not unresolved. Otherwise, only a snapshot with
|
|
50
|
-
`updatedBy: mcp
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
`updatedBy: mcp`, a result status, a non-null summary, and a non-null turn ID
|
|
51
|
+
is a cached semantic result. Any `working` snapshot has no semantic
|
|
52
|
+
callback, including a self-linked `updatedBy: mcp` snapshot, and requires
|
|
53
|
+
one live task query when selected; if the task is inactive and no callback
|
|
54
|
+
exists, report the outcome as unknown rather than treating `working` as
|
|
55
|
+
fresh. When identity is certain and metadata says the thread is
|
|
55
56
|
inactive, trust the latest MCP result by default in a broad overview. Do not
|
|
56
57
|
read every idle terminal task in an overview merely because native
|
|
57
58
|
`updatedAt` is later: callbacks normally run before Codex finalizes the same
|
|
@@ -74,9 +75,12 @@ all deterministic task-log operations.
|
|
|
74
75
|
completed, failed, unresolved, or unknown. Show the cached summary when it
|
|
75
76
|
remains fresh. If a newer turn exists without a callback, describe the live
|
|
76
77
|
state and label the cached result stale rather than overwriting it.
|
|
77
|
-
6. Never edit `tasks.jsonl` directly during reporting. The
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
6. Never edit `tasks.jsonl` directly during reporting. The task API preserves
|
|
79
|
+
each record's persisted `schemaVersion`: a schema 4 null identity is executor
|
|
80
|
+
link-pending and must be retried by that executor, while a schema 1-3 null
|
|
81
|
+
identity is a legacy recovery candidate. Manual recovery may call
|
|
82
|
+
`task resolve` only for the latter after one exact structured marker match.
|
|
83
|
+
Never persist inferred status,
|
|
80
84
|
transcripts, prose classifications, or hidden reasoning. Do not poll or wait.
|
|
81
85
|
|
|
82
86
|
If the task history is empty, say that TaskChef has not recorded any tasks. If
|
package/src/cli.js
CHANGED
|
@@ -464,12 +464,13 @@ Usage:
|
|
|
464
464
|
taskchef project remove <name> [--json] [--workspace <path>]
|
|
465
465
|
taskchef dispatch prepare [--json] [--workspace <path>]
|
|
466
466
|
taskchef task record [--json] [--workspace <path>]
|
|
467
|
-
taskchef task resolve <task-id> --thread-id <thread-id> [--json] [--workspace <path>]
|
|
467
|
+
taskchef task resolve <legacy-task-id> --thread-id <thread-id> [--json] [--workspace <path>]
|
|
468
468
|
taskchef task show <task-id-or-8-character-prefix> [--json] [--workspace <path>]
|
|
469
469
|
taskchef task list [--project <name-or-path>] [--ascending] [--full-id] [--json] [--workspace <path>]
|
|
470
470
|
taskchef task summary [--json] [--workspace <path>]
|
|
471
471
|
|
|
472
472
|
Task record reads one JSON value from closed, non-interactive standard input.
|
|
473
|
+
Task resolve is a legacy migration command and rejects self-linking task records.
|
|
473
474
|
Task show accepts a full task ID or the exact 8-character ID printed by task list.
|
|
474
475
|
Task show prints human-readable details by default; --json prints the complete task object.
|
|
475
476
|
Project import reads a JSON
|
package/src/delegation.js
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
// Bounded discovery parameters used when Codex returns only a provisional
|
|
4
|
-
// client ID. Candidate content still needs exact-marker verification.
|
|
5
|
-
export const THREAD_RESOLUTION_CHECKPOINTS_MS = Object.freeze([10_000, 30_000]);
|
|
6
|
-
export const THREAD_RESOLUTION_TIMEOUT_MS = 30_000;
|
|
7
|
-
export const THREAD_RESOLUTION_RECENT_LIMIT = 50;
|
|
8
|
-
export const THREAD_RESOLUTION_CLOCK_SKEW_MS = 5_000;
|
|
9
3
|
export const EXECUTOR_OWNERSHIP_PARAGRAPH = "This task owns the delegated assignment. Execute it in this task; do not re-dispatch it merely because it concerns TaskChef or a configured project. Explicit requests to delegate separate work remain valid.";
|
|
10
|
-
export const
|
|
4
|
+
export const EXECUTOR_LINK_PARAGRAPH = "Before any other work, read this executor's own durable Codex thread ID from the current task's CODEX_THREAD_ID environment value and call the TaskChef link_task MCP tool with that thread ID and the marked TaskChef task ID. Never use CODEX_SESSION_ID or the parent or delegator thread ID. If linking fails, CODEX_THREAD_ID is unavailable, or the tool is unavailable, report the failure visibly and retry on a later turn; do not guess an identity or continue substantive work while the task is link-pending.";
|
|
5
|
+
export const EXECUTOR_RESULT_PARAGRAPH = "Before ending, call the TaskChef report_result MCP tool with the marked task ID, this executor's self-linked thread ID, the current turn ID from an exact native read of that same thread, completed, needs_input, or failed, and a concise summary. Never reuse a prior turn ID after a follow-up. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.";
|
|
11
6
|
|
|
12
7
|
const UUID_SOURCE = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
13
8
|
const UUID_PATTERN = new RegExp(`^${UUID_SOURCE}$`);
|
|
9
|
+
const CODEX_UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
14
10
|
const TASKCHEF_MARKER_PATTERN = new RegExp(`^<!-- taskchef_id=(${UUID_SOURCE}) -->$`);
|
|
15
11
|
const LEGACY_TASKCHEF_MARKER_PATTERN = new RegExp(`^# taskchef_id=(${UUID_SOURCE})$`);
|
|
16
12
|
|
|
17
13
|
function requireObject(value, name) {
|
|
18
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
19
|
-
throw new Error(`${name} must be an object`);
|
|
20
|
-
}
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object`);
|
|
21
15
|
return value;
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
function requireString(value, name) {
|
|
25
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
26
|
-
throw new Error(`${name} must be a non-empty string`);
|
|
27
|
-
}
|
|
19
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${name} must be a non-empty string`);
|
|
28
20
|
return value;
|
|
29
21
|
}
|
|
30
22
|
|
|
@@ -44,11 +36,6 @@ function parseToolResult(value, name) {
|
|
|
44
36
|
}
|
|
45
37
|
}
|
|
46
38
|
|
|
47
|
-
function timestampMilliseconds(value) {
|
|
48
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
49
|
-
return value < 1_000_000_000_000 ? value * 1_000 : value;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
39
|
function toolIdentifier(value) {
|
|
53
40
|
if (typeof value !== "string") return null;
|
|
54
41
|
const normalized = value.trim();
|
|
@@ -64,10 +51,7 @@ function attachCreationRecovery(error, taskId, resultReporting) {
|
|
|
64
51
|
});
|
|
65
52
|
return creationError;
|
|
66
53
|
} catch {
|
|
67
|
-
const wrapped = new Error(
|
|
68
|
-
`Executor creation failed for recorded TaskChef task ${taskId}.`,
|
|
69
|
-
{ cause: creationError },
|
|
70
|
-
);
|
|
54
|
+
const wrapped = new Error(`Executor creation failed for recorded TaskChef task ${taskId}.`, { cause: creationError });
|
|
71
55
|
wrapped.taskChefTaskId = taskId;
|
|
72
56
|
wrapped.taskChefResultReporting = resultReporting;
|
|
73
57
|
return wrapped;
|
|
@@ -82,12 +66,17 @@ export function isProvisionalThreadId(value) {
|
|
|
82
66
|
export function normalizeDurableThreadId(value, name = "threadId") {
|
|
83
67
|
const id = toolIdentifier(value);
|
|
84
68
|
if (id === null) throw new Error(`${name} must be a non-empty string`);
|
|
85
|
-
if (isProvisionalThreadId(id)) {
|
|
86
|
-
throw new Error(`${name} must be a durable thread ID, not a provisional local ID`);
|
|
87
|
-
}
|
|
69
|
+
if (isProvisionalThreadId(id)) throw new Error(`${name} must be a durable thread ID, not a provisional local ID`);
|
|
88
70
|
return id;
|
|
89
71
|
}
|
|
90
72
|
|
|
73
|
+
export function normalizeCodexThreadId(value, name = "threadId") {
|
|
74
|
+
const id = normalizeDurableThreadId(value, name).toLowerCase();
|
|
75
|
+
if (!CODEX_UUID_V7_PATTERN.test(id)) {
|
|
76
|
+
throw new Error(`${name} must be a canonical Codex UUIDv7`);
|
|
77
|
+
}
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
91
80
|
|
|
92
81
|
export function taskChefMarker(taskId) {
|
|
93
82
|
return `<!-- taskchef_id=${requireUuid(taskId)} -->`;
|
|
@@ -107,149 +96,35 @@ export function parseTaskChefMarker(instruction, { allowLegacyHeading = false }
|
|
|
107
96
|
|
|
108
97
|
export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
|
|
109
98
|
requireString(instruction, "instruction");
|
|
110
|
-
if (parseTaskChefMarker(instruction) !== null)
|
|
111
|
-
throw new Error("instruction already contains a TaskChef marker");
|
|
112
|
-
}
|
|
99
|
+
if (parseTaskChefMarker(instruction) !== null) throw new Error("instruction already contains a TaskChef marker");
|
|
113
100
|
const id = requireUuid(taskId);
|
|
114
101
|
return {
|
|
115
102
|
id,
|
|
116
|
-
instruction: `${taskChefMarker(id)}\n\n${EXECUTOR_OWNERSHIP_PARAGRAPH}\n\n${EXECUTOR_RESULT_PARAGRAPH}\n\n${instruction}`,
|
|
103
|
+
instruction: `${taskChefMarker(id)}\n\n${EXECUTOR_OWNERSHIP_PARAGRAPH}\n\n${EXECUTOR_LINK_PARAGRAPH}\n\n${EXECUTOR_RESULT_PARAGRAPH}\n\n${instruction}`,
|
|
117
104
|
};
|
|
118
105
|
}
|
|
119
106
|
|
|
120
|
-
export function listThreadEntries(result) {
|
|
121
|
-
const parsed = parseToolResult(result, "list_threads result");
|
|
122
|
-
const entries = [...(parsed.pinnedThreads ?? []), ...(parsed.threads ?? [])];
|
|
123
|
-
const unique = new Map();
|
|
124
|
-
for (const entry of entries) {
|
|
125
|
-
const id = toolIdentifier(entry?.id);
|
|
126
|
-
if (id !== null && !unique.has(id)) unique.set(id, { ...entry, id });
|
|
127
|
-
}
|
|
128
|
-
return [...unique.values()];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function structuredDelegatedInputs(result) {
|
|
132
|
-
const parsed = parseToolResult(result, "read_thread result");
|
|
133
|
-
const inputs = [];
|
|
134
|
-
for (const turn of parsed.turns ?? []) {
|
|
135
|
-
for (const item of turn?.items ?? []) {
|
|
136
|
-
if (item?.type !== "userMessage") continue;
|
|
137
|
-
for (const part of item.content ?? []) {
|
|
138
|
-
if (typeof part?.codexDelegation?.input === "string") {
|
|
139
|
-
inputs.push(part.codexDelegation.input);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return inputs;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function hasExactTaskChefMarker(result, taskId) {
|
|
148
|
-
const id = requireUuid(taskId);
|
|
149
|
-
return structuredDelegatedInputs(result).some(
|
|
150
|
-
(input) => parseTaskChefMarker(input) === id,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function filterThreadCandidates(result, {
|
|
155
|
-
baselineThreadIds = new Set(),
|
|
156
|
-
excludedThreadIds = new Set(),
|
|
157
|
-
hostId = null,
|
|
158
|
-
projectId = null,
|
|
159
|
-
title = null,
|
|
160
|
-
createdAfter = null,
|
|
161
|
-
environmentType = null,
|
|
162
|
-
} = {}) {
|
|
163
|
-
const minimumCreatedAt = createdAfter === null
|
|
164
|
-
? null
|
|
165
|
-
: timestampMilliseconds(createdAfter) - THREAD_RESOLUTION_CLOCK_SKEW_MS;
|
|
166
|
-
const candidates = listThreadEntries(result).filter((thread) => {
|
|
167
|
-
if (
|
|
168
|
-
baselineThreadIds.has(thread.id)
|
|
169
|
-
|| excludedThreadIds.has(thread.id)
|
|
170
|
-
|| isProvisionalThreadId(thread.id)
|
|
171
|
-
|| thread.kind !== "codex"
|
|
172
|
-
) return false;
|
|
173
|
-
if (hostId !== null && thread.hostId !== undefined && thread.hostId !== hostId) return false;
|
|
174
|
-
if (projectId !== null && thread.projectId !== undefined && thread.projectId !== projectId) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
const candidateEnvironmentType = thread.environment?.type
|
|
178
|
-
?? thread.environmentType
|
|
179
|
-
?? thread.backing?.environment?.type;
|
|
180
|
-
if (
|
|
181
|
-
environmentType !== null
|
|
182
|
-
&& candidateEnvironmentType !== undefined
|
|
183
|
-
&& candidateEnvironmentType !== environmentType
|
|
184
|
-
) return false;
|
|
185
|
-
const candidateTime = timestampMilliseconds(thread.createdAt ?? thread.updatedAt);
|
|
186
|
-
if (minimumCreatedAt !== null && candidateTime !== null && candidateTime < minimumCreatedAt) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
return true;
|
|
190
|
-
});
|
|
191
|
-
if (title === null) return candidates;
|
|
192
|
-
return candidates.sort((left, right) =>
|
|
193
|
-
Number(right.title === title) - Number(left.title === title));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
107
|
export async function createAndRecordDelegation(input) {
|
|
197
|
-
const {
|
|
198
|
-
project,
|
|
199
|
-
title,
|
|
200
|
-
instruction,
|
|
201
|
-
target,
|
|
202
|
-
createThread,
|
|
203
|
-
recordTask,
|
|
204
|
-
resolveRecordedTask = null,
|
|
205
|
-
reportRecordedResult = null,
|
|
206
|
-
taskId = randomUUID(),
|
|
207
|
-
} = input ?? {};
|
|
108
|
+
const { project, title, instruction, target, createThread, recordTask, reportRecordedResult = null, taskId = randomUUID() } = input ?? {};
|
|
208
109
|
requireString(project, "project");
|
|
209
110
|
requireString(title, "title");
|
|
210
111
|
requireObject(target, "target");
|
|
211
|
-
for (const [value, name] of [
|
|
212
|
-
[createThread, "createThread"],
|
|
213
|
-
[recordTask, "recordTask"],
|
|
214
|
-
]) {
|
|
112
|
+
for (const [value, name] of [[createThread, "createThread"], [recordTask, "recordTask"]]) {
|
|
215
113
|
if (typeof value !== "function") throw new Error(`${name} must be a function`);
|
|
216
114
|
}
|
|
217
|
-
|
|
218
|
-
[resolveRecordedTask, "resolveRecordedTask"],
|
|
219
|
-
[reportRecordedResult, "reportRecordedResult"],
|
|
220
|
-
]) {
|
|
221
|
-
if (value !== null && typeof value !== "function") {
|
|
222
|
-
throw new Error(`${name} must be a function or null`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
115
|
+
if (reportRecordedResult !== null && typeof reportRecordedResult !== "function") throw new Error("reportRecordedResult must be a function or null");
|
|
225
116
|
|
|
226
117
|
const prepared = prepareDelegation(instruction, { taskId });
|
|
227
|
-
await recordTask({
|
|
228
|
-
id: prepared.id,
|
|
229
|
-
project,
|
|
230
|
-
title,
|
|
231
|
-
instruction: prepared.instruction,
|
|
232
|
-
threadId: null,
|
|
233
|
-
});
|
|
118
|
+
await recordTask({ id: prepared.id, project, title, instruction: prepared.instruction, threadId: null });
|
|
234
119
|
|
|
235
120
|
let createResult;
|
|
236
121
|
try {
|
|
237
|
-
createResult = parseToolResult(await createThread({
|
|
238
|
-
prompt: prepared.instruction,
|
|
239
|
-
title,
|
|
240
|
-
target,
|
|
241
|
-
}), "create_thread result");
|
|
122
|
+
createResult = parseToolResult(await createThread({ prompt: prepared.instruction, title, target }), "create_thread result");
|
|
242
123
|
} catch (error) {
|
|
243
124
|
let resultReporting = "unavailable";
|
|
244
125
|
if (reportRecordedResult !== null) {
|
|
245
126
|
try {
|
|
246
|
-
await reportRecordedResult({
|
|
247
|
-
taskId: prepared.id,
|
|
248
|
-
threadId: null,
|
|
249
|
-
turnId: null,
|
|
250
|
-
status: "failed",
|
|
251
|
-
summary: "Executor creation failed before the executor started.",
|
|
252
|
-
});
|
|
127
|
+
await reportRecordedResult({ taskId: prepared.id, threadId: null, turnId: null, status: "failed", summary: "Executor creation failed before the executor started." });
|
|
253
128
|
resultReporting = "recorded";
|
|
254
129
|
} catch {
|
|
255
130
|
resultReporting = "failed";
|
|
@@ -261,58 +136,6 @@ export async function createAndRecordDelegation(input) {
|
|
|
261
136
|
const returnedThreadId = toolIdentifier(createResult.threadId);
|
|
262
137
|
const clientThreadId = toolIdentifier(createResult.clientThreadId);
|
|
263
138
|
const pendingWorktreeId = toolIdentifier(createResult.pendingWorktreeId);
|
|
264
|
-
const provisional = clientThreadId
|
|
265
|
-
|
|
266
|
-
?? (isProvisionalThreadId(returnedThreadId) ? returnedThreadId : null);
|
|
267
|
-
const durableThreadId = returnedThreadId !== null
|
|
268
|
-
&& !isProvisionalThreadId(returnedThreadId)
|
|
269
|
-
&& returnedThreadId !== clientThreadId
|
|
270
|
-
&& returnedThreadId !== pendingWorktreeId
|
|
271
|
-
? returnedThreadId
|
|
272
|
-
: null;
|
|
273
|
-
|
|
274
|
-
if (durableThreadId !== null) {
|
|
275
|
-
if (resolveRecordedTask !== null) {
|
|
276
|
-
try {
|
|
277
|
-
await resolveRecordedTask({ id: prepared.id, threadId: durableThreadId });
|
|
278
|
-
return {
|
|
279
|
-
status: "recorded",
|
|
280
|
-
resolution: "immediate",
|
|
281
|
-
...prepared,
|
|
282
|
-
threadId: durableThreadId,
|
|
283
|
-
provisional,
|
|
284
|
-
hostId: createResult.hostId ?? null,
|
|
285
|
-
};
|
|
286
|
-
} catch (error) {
|
|
287
|
-
return {
|
|
288
|
-
status: "recorded-unresolved",
|
|
289
|
-
reason: "task-resolution-failed",
|
|
290
|
-
resolutionError: error instanceof Error ? error.message : String(error),
|
|
291
|
-
...prepared,
|
|
292
|
-
threadId: null,
|
|
293
|
-
createdThreadId: durableThreadId,
|
|
294
|
-
provisional,
|
|
295
|
-
hostId: createResult.hostId ?? null,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return {
|
|
300
|
-
status: "recorded-unresolved",
|
|
301
|
-
reason: "task-resolution-unavailable",
|
|
302
|
-
...prepared,
|
|
303
|
-
threadId: null,
|
|
304
|
-
createdThreadId: durableThreadId,
|
|
305
|
-
provisional,
|
|
306
|
-
hostId: createResult.hostId ?? null,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
status: "recorded-unresolved",
|
|
312
|
-
reason: "awaiting-bounded-marker-resolution",
|
|
313
|
-
...prepared,
|
|
314
|
-
threadId: null,
|
|
315
|
-
provisional,
|
|
316
|
-
hostId: createResult.hostId ?? null,
|
|
317
|
-
};
|
|
139
|
+
const provisional = clientThreadId ?? pendingWorktreeId ?? (isProvisionalThreadId(returnedThreadId) ? returnedThreadId : null);
|
|
140
|
+
return { status: "recorded-link-pending", resolution: "executor-self-link", ...prepared, threadId: null, provisional, hostId: createResult.hostId ?? null };
|
|
318
141
|
}
|