codex-workflow-v2 2.0.0-beta.13.2 → 2.0.0-beta.13.3
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/README.md +1 -1
- package/dist/reviewer-runtime-build.json +7 -7
- package/dist/src/alpha6/remediation.d.ts +1 -0
- package/dist/src/alpha6/remediation.js +12 -3
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/state/corrective-yield-executor.js +10 -1
- package/dist/src/state/corrective-yield-executor.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.js +6 -6
- package/dist/src/workflow.js.map +1 -1
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +2 -2
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +3 -3
- package/docs/release.md +16 -2
- package/docs/updating-existing-project.md +10 -2
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +128 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +4 -0
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +185 -0
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +43 -0
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +50 -3
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +28 -4
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -463,7 +463,7 @@ Restart Docker Desktop требует отдельного разрешения
|
|
|
463
463
|
# 10. Operational checklist
|
|
464
464
|
|
|
465
465
|
- exact package version установлен, bundled gateway соответствует release и переустановлен;
|
|
466
|
-
- handshake подтверждает `packageVersion=2.0.0-beta.13.
|
|
466
|
+
- handshake подтверждает `packageVersion=2.0.0-beta.13.3`, protocol 2, state schema 2 и beta.13
|
|
467
467
|
capabilities;
|
|
468
468
|
- routine observation начинается с компактного `status`; `status --full` запрашивается только для
|
|
469
469
|
расследования, которому действительно нужна полная historical projection;
|
|
@@ -57,7 +57,7 @@ AGENTS.md
|
|
|
57
57
|
-> doctor только как дополнительная диагностика
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
Handshake beta.13.1 должен сообщать `packageVersion=2.0.0-beta.13.
|
|
60
|
+
Handshake beta.13.1 должен сообщать `packageVersion=2.0.0-beta.13.3`, `protocolVersion=2`,
|
|
61
61
|
`stateSchemaVersion=2`, dependency DAG, initial Plan transaction, mechanical feasibility,
|
|
62
62
|
Milestone autonomy, host-safe credential reference, failed-Step continuation transaction,
|
|
63
63
|
cause-bound rolling refinement, structural production edge, compact status, reviewer runtime preflight,
|
|
@@ -482,7 +482,7 @@ Task/project DGR. `dependencyBinding` для standalone handoff равен null.
|
|
|
482
482
|
# 14. Итоговый checklist пользователя
|
|
483
483
|
|
|
484
484
|
- beta.13.1 exact package и новый bundled gateway установлены; handshake показывает
|
|
485
|
-
`packageVersion=2.0.0-beta.13.
|
|
485
|
+
`packageVersion=2.0.0-beta.13.3`, protocol/state schema `2/2` и beta.13 capabilities;
|
|
486
486
|
- новый Coordinator chat не является fork старого Milestone;
|
|
487
487
|
- bootstrap DGR, если нужен, выдан отдельным exact human confirmation;
|
|
488
488
|
- Discovery не materialized при blocking unknowns;
|
|
@@ -15,7 +15,7 @@ authority сами: mutating permission определяется runtime state
|
|
|
15
15
|
|
|
16
16
|
| Параметр beta.13 | Значение |
|
|
17
17
|
|---|---|
|
|
18
|
-
| npm package | `codex-workflow-v2@2.0.0-beta.13.
|
|
18
|
+
| npm package | `codex-workflow-v2@2.0.0-beta.13.3` |
|
|
19
19
|
| protocolVersion | 2 |
|
|
20
20
|
| stateSchemaVersion | 2 |
|
|
21
21
|
| lifecycle epoch | 2 |
|
|
@@ -333,7 +333,7 @@ local reviewer внутри Codex App sandbox.
|
|
|
333
333
|
| Повтор stable reviewer `causeId` с `planConflict=true` | Root-cause replan той же Task обязателен |
|
|
334
334
|
| Похожий prose, номер попытки или reviewer cause без Plan conflict | Не создаёт replan authority |
|
|
335
335
|
| Finding `route=fix` или route отсутствует | Продолжить ту же Task/Step независимо от attempt count |
|
|
336
|
-
| Finding `route=replan` с exact Plan conflict |
|
|
336
|
+
| Finding `route=replan` с exact Plan conflict, включая первый провал | Штатный corrective yield/replan; повторный run запрещён, меняются только implementation Steps |
|
|
337
337
|
| `route=replan` без exact conflict | Невалидный review result, state не меняется |
|
|
338
338
|
| Explicit `split-required` | Structural replacement stop |
|
|
339
339
|
| Exact beta.11 attempt-four `stop-escalate` | Append-only Human-confirmed compatibility override |
|
|
@@ -767,7 +767,7 @@ dirty update.
|
|
|
767
767
|
observed-route matrix/compact-status/reviewer-runtime tests, legacy compatibility regressions,
|
|
768
768
|
`npm run test:signal-t10-check-support-e2e`, `npm run test:signal-remediation-e2e`,
|
|
769
769
|
`npm run pack:check`, `npm run smoke:downstream`, `npm run plugin:check`, official plugin/skill validators,
|
|
770
|
-
`npm run docs:pdf:check` и `npm run release:check` с exact `v2.0.0-beta.13.
|
|
770
|
+
`npm run docs:pdf:check` и `npm run release:check` с exact `v2.0.0-beta.13.3` tag binding и append-only
|
|
771
771
|
real-agent lifecycle log. Candidate проверяется из clean source checkout; missing built module не должен
|
|
772
772
|
превращаться в stack trace, а обязан быть детерминированно собран или диагностирован release gate.
|
|
773
773
|
|
package/docs/release.md
CHANGED
|
@@ -4,6 +4,20 @@ The npm package is `codex-workflow-v2` with public visibility. Consumers must pi
|
|
|
4
4
|
an exact version. Stable compatibility covers the latest minor release of the current
|
|
5
5
|
major only; older minors are unsupported unless a release note explicitly says otherwise.
|
|
6
6
|
|
|
7
|
+
## beta.13.3 explicit Plan conflict recovery
|
|
8
|
+
|
|
9
|
+
A first independently recorded reviewer `route=replan` with a validated Plan conflict
|
|
10
|
+
blocks current execution immediately and exposes corrective replan. Historical
|
|
11
|
+
rolling-cause classification remains unchanged; valid older histories are not
|
|
12
|
+
reclassified as corrupt. Ordinary new fix findings still continue in the same Task.
|
|
13
|
+
The failure transaction yields C1 and releases the exact writer lease. An older
|
|
14
|
+
failed Task with a retained C1 claim follows credential recovery and
|
|
15
|
+
`task corrective-yield` before planning; it cannot run or change its Plan while
|
|
16
|
+
the claim remains. Recovery binds the validated remediation event and preserves
|
|
17
|
+
the Task, review evidence, and existing worker identity.
|
|
18
|
+
The release also includes durable App dispatch and explicit role/phase model selection;
|
|
19
|
+
see the packaged `references/chat-dispatch.md`.
|
|
20
|
+
|
|
7
21
|
## beta.13.2 review recovery gate
|
|
8
22
|
|
|
9
23
|
The patch preserves protocol 2 and canonical entity schema 2. In addition to the
|
|
@@ -63,8 +77,8 @@ surface and fails unless all of these release contracts remain present:
|
|
|
63
77
|
adds C1 yield before root-cause replan; the Signal-shaped E2E continues through recovery, the next
|
|
64
78
|
ordinary Step, Task completion, and next-Task dispatch;
|
|
65
79
|
- five distinct or unclassified review findings continue in the same Task without a numeric cap,
|
|
66
|
-
while
|
|
67
|
-
|
|
80
|
+
while repeated normalized mechanical failures still require root-cause replan; a current
|
|
81
|
+
explicit external reviewer Plan conflict requires replan immediately, without a synthetic retry;
|
|
68
82
|
- a transitive same-component check owner may refine the same Task only under the unchanged semantic
|
|
69
83
|
boundary; for a multi-Step `runtime-composition` consumer the independent PRA classification,
|
|
70
84
|
not a planner opt-in, requires an edge from a transitive owner to an exact executable consumer
|
|
@@ -636,8 +636,16 @@ title в registry readback и применяйте только возвращё
|
|
|
636
636
|
быть уникален и содержать exact `#NNN`, membership ordinal и ID. Input должен состоять из
|
|
637
637
|
TaskContextPacket; допустима только добавленная Codex App служебная `codex_delegation`-обёртка с
|
|
638
638
|
`source_thread_id`, но не parent turns или transcript. Если title отсутствует или нормализован
|
|
639
|
-
неверно, переименуйте и перепроверьте.
|
|
640
|
-
|
|
639
|
+
неверно, переименуйте и перепроверьте. Pending `clientThreadId`, тайм-аут или отсутствие в
|
|
640
|
+
`list_threads` не доказывают, что создание не состоялось, и не разрешают повторный `create_thread`.
|
|
641
|
+
До первого вызова сохраните `dispatch-begin`; после него используйте `dispatch-result/status`,
|
|
642
|
+
поиск коррелированного сеанса и `dispatch-observe` с фактическим `read_thread`. На одну
|
|
643
|
+
резервацию выдаётся только одно разрешение создания. Не обходите его новой резервацией или fork.
|
|
644
|
+
Полный контракт восстановления, сохранения supervisor/cursor и явного выбора `model`/`thinking`
|
|
645
|
+
по роли, фазе и сложности находится в
|
|
646
|
+
`plugins/codex-workflow-gateway/references/chat-dispatch.md`. Выбор модели требует соответствующего
|
|
647
|
+
поручения пользователя и актуального списка поддерживаемых пар на целевом host; нельзя молча
|
|
648
|
+
наследовать дорогую конфигурацию координатора или снижать уровень независимого аудита.
|
|
641
649
|
Создавайте или переиспользуйте Task-чат непосредственно перед dispatch этой Task, а не как пустой
|
|
642
650
|
placeholder для всего membership; `T<NN>` берите только из утверждённого membership order.
|
|
643
651
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Durable App dispatch and explicit model selection
|
|
2
|
+
|
|
3
|
+
This is an App orchestration journal, not Workflow authority. It does not call the App, approve
|
|
4
|
+
Git writes, renew grants, mint credentials, or replace Core status/next. All commands below use
|
|
5
|
+
`node <exact-package>/plugins/codex-workflow-gateway/scripts/chat-registry.mjs` and the existing
|
|
6
|
+
24-hex Workflow `--project-id`. The App project ID is a separate field.
|
|
7
|
+
|
|
8
|
+
## One reservation, one creation attempt
|
|
9
|
+
|
|
10
|
+
`allocate` reuses the same non-abandoned role/entity/membership/attempt reservation. Task and
|
|
11
|
+
Coordinator allocations also reuse it when a caller changes the attempt number. Review attempts
|
|
12
|
+
advance only after the actual Workflow review cycle advances. Never invent a new attempt or
|
|
13
|
+
reservation to bypass unresolved creation. Existing v1 registries remain readable; legacy bound
|
|
14
|
+
threads remain usable with explicit model/thinking arguments from the same policy; `model-select`
|
|
15
|
+
and `supervision-record` currently require a tracked dispatch and do not retrofit legacy evidence.
|
|
16
|
+
Legacy unbound reservations may have already created a chat: reconcile
|
|
17
|
+
those using real App/source evidence before using the new dispatch flow.
|
|
18
|
+
|
|
19
|
+
1. Save the closed context packet to a private local file. Do not include bearer credentials,
|
|
20
|
+
parent transcript, or approval narratives. Prepare a private dispatch request JSON:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"repositoryRoot": "/absolute/canonical/repository",
|
|
25
|
+
"appProjectId": "project-id-from-list-projects",
|
|
26
|
+
"hostId": "local",
|
|
27
|
+
"promptFile": "/private/tmp/task-context.txt",
|
|
28
|
+
"modelRequest": {
|
|
29
|
+
"phase": "planning",
|
|
30
|
+
"complexity": "standard",
|
|
31
|
+
"highRisk": false,
|
|
32
|
+
"rationale": "New multi-file implementation plan with established requirements.",
|
|
33
|
+
"availableModels": [
|
|
34
|
+
{"id": "gpt-5.6-sol", "reasoningEfforts": ["low", "medium", "high", "xhigh", "max", "ultra"]}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Populate `availableModels` from the destination App tool's actual model/effort contract, not
|
|
41
|
+
this example, the parent's settings, API pricing pages, or a guessed alias. Explicit user
|
|
42
|
+
authorization to select models is required; journal input is not user authorization.
|
|
43
|
+
2. Run `dispatch-begin --reservation-id ID --file request.json` **before** `create_thread`.
|
|
44
|
+
Save its complete output to a mode-600 file. It atomically persists a creation marker, packet
|
|
45
|
+
hashes, repository binding and requested model selection, then returns `createAllowed:true`
|
|
46
|
+
and exact `createArgs` including `model` and App field `thinking`.
|
|
47
|
+
3. Only the process receiving that one permission may make **one** `create_thread(createArgs)`
|
|
48
|
+
call. A second begin, even after restart or timeout, returns `createAllowed:false` and no
|
|
49
|
+
create arguments. Do not reissue the external call after a lost/ambiguous response. The marker
|
|
50
|
+
prepended to the packet is routing provenance, not parent history.
|
|
51
|
+
4. Save the full App result locally, normalize its successful tool text JSON to
|
|
52
|
+
`{"result":{"clientThreadId":"client-new-thread:...","hostId":"local"}}` or
|
|
53
|
+
`{"result":{"threadId":"actual-uuid","hostId":"local"}}`, and run `dispatch-result`.
|
|
54
|
+
Preserve raw evidence separately. Never parse truncated displayed stdout as the full receipt.
|
|
55
|
+
Invalid JSON cannot change the journal. A pending client ID is never a usable thread ID.
|
|
56
|
+
5. Use `dispatch-status` to resume. Missing `list_threads` results are not proof of failure.
|
|
57
|
+
If the actual ID is unavailable, identify candidate rollout files from creation-time/worktree
|
|
58
|
+
evidence and run `dispatch-discover --file candidates.json`, where the file is
|
|
59
|
+
`{"sessionFiles":["/absolute/rollout.jsonl"]}`. The helper reads only supplied files, checks
|
|
60
|
+
actual session metadata, exact original dispatched prompt (including the App's routing XML),
|
|
61
|
+
and the Git common directory. It ignores guardian/subagent sessions and unrelated prompts.
|
|
62
|
+
It prints candidate IDs/paths only, never transcripts or reasoning. Use bounded candidate
|
|
63
|
+
lists, not vault searches. Malformed, oversized or partially written files need a later fresh
|
|
64
|
+
read; they never authorize another creation.
|
|
65
|
+
6. Confirm a correlated candidate using `read_thread`. Save its full response as
|
|
66
|
+
`{"observation": <parsed read_thread JSON>}` and run `dispatch-observe`. It checks actual UUID,
|
|
67
|
+
host, repository/worktree and App project when supplied. One candidate still requires App
|
|
68
|
+
readback; several candidates produce a duplicate conflict and require explicit disposition,
|
|
69
|
+
not arbitrary selection. After confirming one canonical child, inspect every other candidate,
|
|
70
|
+
reconcile any work it performed, and archive only idle duplicates. Record
|
|
71
|
+
`dispatch-resolve-duplicates` with `canonicalThreadId` and `dispositions`, one object per duplicate:
|
|
72
|
+
`{threadId, observation: <read_thread JSON>, archiveResult: {threadId, archived: true}}`.
|
|
73
|
+
Preserve the actual archive tool receipt; never fabricate it. Repeat canonical readback and bind.
|
|
74
|
+
Candidate history is retained and no creation permit is restored. This is a local evidence
|
|
75
|
+
check, not independent App attestation.
|
|
76
|
+
7. Run ordinary title `readback`/rename until exact match. After any rename, repeat
|
|
77
|
+
`dispatch-observe` with fresh App readback, then `readback` and `bind`. Binding a tracked
|
|
78
|
+
dispatch requires the same actual ID, host and observed title. Rebinding it to another child,
|
|
79
|
+
binding a client ID or sharing one child between reservations is rejected.
|
|
80
|
+
8. Run `supervision-record --file report.json` with `threadId`, `ownerThreadId`, `state`
|
|
81
|
+
(`active|attention|completed`), `cursor` and `nextAction`. Persist significant wait boundaries;
|
|
82
|
+
only that supervisor waits for this child. The journal stores routing state, not a Workflow
|
|
83
|
+
verdict. Do not put secrets, CREFs or confirmation codes in it. On restart, read the child and
|
|
84
|
+
fresh Core status/next before continuing. Worker reports stay local; the coordinator pulls.
|
|
85
|
+
|
|
86
|
+
A crash between durable begin and actual submission is deliberately ambiguous. There is no
|
|
87
|
+
App idempotency-key API here, so this provides **at most one create permit per reservation**,
|
|
88
|
+
not an exactly-once guarantee. Never clear the marker, abandon it, or expire it by time to retry.
|
|
89
|
+
If no positive creation evidence can be found, report that exact unresolved App operation. A
|
|
90
|
+
permission denial is a separate real gate; do not reroute it through another chat. Fresh user
|
|
91
|
+
approval does not establish that an ambiguous prior creation never happened.
|
|
92
|
+
|
|
93
|
+
## Model matrix
|
|
94
|
+
|
|
95
|
+
The versioned policy is `scripts/chat-model-policy.mjs`. It expresses a conservative relative
|
|
96
|
+
cost/latency preference, not measured savings or guaranteed account availability:
|
|
97
|
+
|
|
98
|
+
| Work | Preferred model | App thinking | Compatible fallback |
|
|
99
|
+
| --- | --- | --- | --- |
|
|
100
|
+
| Routine mechanical transitions, formatting, retrieval | gpt-5.4-mini | low | gpt-5.6-luna / low |
|
|
101
|
+
| Coordinator supervision; small implementation | gpt-5.6-luna | medium | gpt-5.6-terra / medium |
|
|
102
|
+
| Standard implementation | gpt-5.6-terra | medium | gpt-5.6-sol / medium |
|
|
103
|
+
| Task planning | gpt-5.6-sol | medium | gpt-6-astra / medium |
|
|
104
|
+
| Routine Step review | gpt-5.6-terra | medium | gpt-5.6-sol / medium |
|
|
105
|
+
| Standard Step review, final review, plan/corrective audit | gpt-5.6-sol | high | gpt-6-astra / high |
|
|
106
|
+
| Complex work or high risk, any role | gpt-6-astra | high | gpt-5.6-sol / high |
|
|
107
|
+
|
|
108
|
+
`routine` means a small, well-specified change or mechanical action with an existing check;
|
|
109
|
+
`standard` is ordinary multi-file work; `complex` covers new architecture, unresolved concurrency,
|
|
110
|
+
recovery/provenance or distributed state. Mark `highRisk` for authorization, security-sensitive
|
|
111
|
+
paths, data-loss exposure or irreversible migrations. The coordinator records the concrete
|
|
112
|
+
rationale; classification is a human/agent judgment, not an automatic risk detector. Unknown
|
|
113
|
+
complexity is not routine. Review/audit role floors override a falsely mechanical phase. No
|
|
114
|
+
automatic xhigh/max/ultra, and no escalation merely because a review failed or a tool timed out.
|
|
115
|
+
|
|
116
|
+
The policy chooses only a listed supported pair. If neither preferred nor fallback pair is
|
|
117
|
+
available, stop selection rather than silently inheriting the coordinator or lowering the review
|
|
118
|
+
floor. API docs explain the reasoning/latency tradeoff, while App availability and accepted field
|
|
119
|
+
names come from the live App tool contract:
|
|
120
|
+
https://developers.openai.com/api/docs/guides/latest-model
|
|
121
|
+
|
|
122
|
+
For a phase change in an existing bound chat, call `model-select` with its same `threadId` and
|
|
123
|
+
fresh `modelRequest`; copy the returned `followUpArgs` into `send_message_to_thread` with the
|
|
124
|
+
closed follow-up prompt. For example, planning uses Sol/medium, implementation Terra/medium,
|
|
125
|
+
then mechanical lifecycle work Mini/low. Do not create another Task just to switch model.
|
|
126
|
+
Keep the previous selection unless the phase/risk/scope changes materially. The journal records
|
|
127
|
+
requested settings; tool acceptance/runtime readback, when available, is required to claim the
|
|
128
|
+
actual model used. A model change never changes role, scope, credentials or approval authority.
|
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
progress tables. It preserves cancelled/replacement Tasks as distinct membership rows.
|
|
68
68
|
- `scripts/chat-registry.mjs` atomically allocates project-monotonic `#NNN`, builds all six title
|
|
69
69
|
families, verifies exact readback, supplies deterministic fallbacks, and binds the verified chat.
|
|
70
|
+
It also journals a single creation permit, pending/actual IDs, evidence-based resolution and
|
|
71
|
+
supervision cursors. Missing App listings never permit a retry. See `chat-dispatch.md` for
|
|
72
|
+
dispatch commands and the explicit role/phase model + thinking policy. These are App routing
|
|
73
|
+
helpers; they do not change Workflow protocol/state schema or grant approval authority.
|
|
70
74
|
|
|
71
75
|
## Handshake
|
|
72
76
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { selectChatModel } from './chat-model-policy.mjs';
|
|
6
|
+
|
|
7
|
+
const hash = text => createHash('sha256').update(text).digest('hex');
|
|
8
|
+
const text = (value, name) => {
|
|
9
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} is required.`);
|
|
10
|
+
return value.trim();
|
|
11
|
+
};
|
|
12
|
+
const actualId = value => {
|
|
13
|
+
const id = text(value, 'actual threadId');
|
|
14
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(id)) throw new Error('Expected actual thread UUID, not clientThreadId.');
|
|
15
|
+
return id;
|
|
16
|
+
};
|
|
17
|
+
function jsonFile(file) {
|
|
18
|
+
if (statSync(file).size > 16 * 1024 * 1024) throw new Error('Input file exceeds 16 MiB.');
|
|
19
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
function commonGitDir(cwd) {
|
|
22
|
+
return realpathSync(execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
23
|
+
cwd, encoding: 'utf8', timeout: 5000, maxBuffer: 1024 * 1024,
|
|
24
|
+
}).trim());
|
|
25
|
+
}
|
|
26
|
+
function promptMatches(value, digest) {
|
|
27
|
+
if (typeof value !== 'string') return false;
|
|
28
|
+
if (hash(value) === digest) return true;
|
|
29
|
+
const wrapped = /^<codex_delegation>\s*<source_thread_id>[^<]+<\/source_thread_id>\s*<input>([\s\S]*)<\/input>\s*<\/codex_delegation>$/.exec(value);
|
|
30
|
+
return wrapped !== null && hash(wrapped[1]) === digest;
|
|
31
|
+
}
|
|
32
|
+
function view(entry) {
|
|
33
|
+
const d = entry.dispatch;
|
|
34
|
+
return {
|
|
35
|
+
reservationId: entry.reservationId, state: d?.state ?? (entry.threadId ? 'legacy-bound' : 'untracked'),
|
|
36
|
+
createAllowed: false, dispatchId: d?.id ?? null, clientThreadId: d?.clientThreadId ?? null,
|
|
37
|
+
threadId: entry.threadId ?? d?.candidateThreadId ?? null, hostId: d?.hostId ?? entry.hostId ?? null,
|
|
38
|
+
selection: d?.selection ?? null, supervision: d?.supervision ?? null,
|
|
39
|
+
candidates: d?.candidates ?? [],
|
|
40
|
+
nextAction: entry.threadId ? 'supervise-existing' : d?.state === 'resolved' ? 'title-readback-and-bind'
|
|
41
|
+
: d ? 'resolve-existing-creation' : 'dispatch-begin',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function dispatchAction(action, entry, input = {}) {
|
|
45
|
+
if (action === 'dispatch-status') return view(entry);
|
|
46
|
+
if (action === 'dispatch-begin') {
|
|
47
|
+
// A durable marker is committed BEFORE the external call. Even a lost response never permits another call.
|
|
48
|
+
if (entry.dispatch || entry.threadId) return view(entry);
|
|
49
|
+
if (entry.status === 'abandoned' || entry.status === 'blocked') throw new Error('Reservation is not dispatchable.');
|
|
50
|
+
const repositoryRoot = realpathSync(text(input.repositoryRoot, 'repositoryRoot'));
|
|
51
|
+
const gitDir = commonGitDir(repositoryRoot);
|
|
52
|
+
const appProjectId = text(input.appProjectId, 'appProjectId');
|
|
53
|
+
const hostId = text(input.hostId, 'hostId');
|
|
54
|
+
const promptPath = realpathSync(text(input.promptFile, 'promptFile'));
|
|
55
|
+
if (statSync(promptPath).size > 1024 * 1024) throw new Error('TaskContextPacket exceeds 1 MiB.');
|
|
56
|
+
const prompt = readFileSync(promptPath, 'utf8');
|
|
57
|
+
if (!prompt.trim() || !prompt.includes(entry.entityId)) throw new Error('Prompt must include the exact entity ID.');
|
|
58
|
+
const selection = selectChatModel(entry.type, input.modelRequest);
|
|
59
|
+
const id = randomUUID();
|
|
60
|
+
const marker = `[workflow-dispatch:${id}:${hash(prompt)}]`;
|
|
61
|
+
const dispatchedPrompt = `${marker}\n${prompt}`;
|
|
62
|
+
entry.dispatch = {
|
|
63
|
+
version: 1, id, state: 'creating', appProjectId, hostId, repositoryRoot, gitDir,
|
|
64
|
+
promptPath, promptHash: hash(prompt), dispatchedPromptHash: hash(dispatchedPrompt), marker,
|
|
65
|
+
selection, selectionHistory: [selection], clientThreadId: null, candidateThreadId: null,
|
|
66
|
+
candidates: [], observations: [], createdAt: new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
return { ...view(entry), createAllowed: true, nextAction: 'create-thread-once', createArgs: {
|
|
69
|
+
target: { type: 'project', projectId: appProjectId, environment: { type: 'worktree' } },
|
|
70
|
+
title: entry.requestedTitle, prompt: dispatchedPrompt, model: selection.model, thinking: selection.thinking,
|
|
71
|
+
} };
|
|
72
|
+
}
|
|
73
|
+
const d = entry.dispatch;
|
|
74
|
+
if (!d) throw new Error('No tracked dispatch; reconcile legacy chats manually, never assume creation failed.');
|
|
75
|
+
if (action === 'dispatch-result') {
|
|
76
|
+
const result = input.result;
|
|
77
|
+
if (!result || result.hostId !== d.hostId) throw new Error('Create result host mismatch.');
|
|
78
|
+
if (result.threadId && result.clientThreadId) throw new Error('Ambiguous create result.');
|
|
79
|
+
if (result.threadId) {
|
|
80
|
+
const id = actualId(result.threadId);
|
|
81
|
+
if (d.candidateThreadId && d.candidateThreadId !== id) throw new Error('Conflicting actual thread IDs.');
|
|
82
|
+
d.candidateThreadId = id;
|
|
83
|
+
} else {
|
|
84
|
+
const id = text(result.clientThreadId, 'clientThreadId');
|
|
85
|
+
if (!id.startsWith('client-new-thread:')) throw new Error('Invalid pending clientThreadId.');
|
|
86
|
+
if (d.clientThreadId && d.clientThreadId !== id) throw new Error('Conflicting creation attempts; reconcile duplicates.');
|
|
87
|
+
d.clientThreadId = id;
|
|
88
|
+
}
|
|
89
|
+
if (d.state === 'creating') d.state = 'pending';
|
|
90
|
+
return view(entry);
|
|
91
|
+
}
|
|
92
|
+
if (action === 'dispatch-discover') {
|
|
93
|
+
// Read only specifically supplied session files; never scan credential stores or print transcripts/reasoning.
|
|
94
|
+
if (!Array.isArray(input.sessionFiles) || input.sessionFiles.length > 200) throw new Error('Supply at most 200 candidate session files.');
|
|
95
|
+
const found = new Map(d.candidates.map(c => [c.threadId, c]));
|
|
96
|
+
for (const file of input.sessionFiles) {
|
|
97
|
+
if (statSync(file).size > 16 * 1024 * 1024) throw new Error('Session exceeds 16 MiB; narrow evidence required.');
|
|
98
|
+
const lines = readFileSync(file, 'utf8').split('\n');
|
|
99
|
+
const meta = JSON.parse(lines[0]).payload;
|
|
100
|
+
if (!meta || !['vscode', 'cli'].includes(meta.source) || typeof meta.cwd !== 'string') continue;
|
|
101
|
+
const rows = lines.slice(1).filter(Boolean).map(line => JSON.parse(line));
|
|
102
|
+
const bound = rows.some(row => {
|
|
103
|
+
if (row.type !== 'response_item') return false;
|
|
104
|
+
const p = row.payload;
|
|
105
|
+
// Actual App rollout format: a create_thread function output containing routing-only XML.
|
|
106
|
+
if (p?.type === 'function_call_output' && p.name === 'create_thread' && p.namespace === 'codex_app') {
|
|
107
|
+
return promptMatches(p.output, d.dispatchedPromptHash);
|
|
108
|
+
}
|
|
109
|
+
return p?.type === 'message' && p.role === 'user' && p.content?.some(c =>
|
|
110
|
+
['input_text', 'text'].includes(c.type) && promptMatches(c.text, d.dispatchedPromptHash));
|
|
111
|
+
});
|
|
112
|
+
if (!bound) continue;
|
|
113
|
+
if (commonGitDir(meta.cwd) !== d.gitDir) throw new Error('Session belongs to another repository.');
|
|
114
|
+
const id = actualId(meta.id);
|
|
115
|
+
found.set(id, { threadId: id, cwd: realpathSync(meta.cwd), sourceFile: realpathSync(file) });
|
|
116
|
+
}
|
|
117
|
+
d.candidates = [...found.values()];
|
|
118
|
+
if (d.candidates.length > 1) d.state = 'duplicate-conflict';
|
|
119
|
+
return view(entry);
|
|
120
|
+
}
|
|
121
|
+
if (action === 'dispatch-resolve-duplicates') {
|
|
122
|
+
const canonical = actualId(input.canonicalThreadId);
|
|
123
|
+
if (entry.threadId && entry.threadId !== canonical) throw new Error('Cannot replace a bound child.');
|
|
124
|
+
if (d.candidateThreadId && d.candidateThreadId !== canonical) throw new Error('Cannot replace the correlated create result.');
|
|
125
|
+
if (!d.candidates.some(c => c.threadId === canonical)) throw new Error('Canonical child must be a discovered candidate.');
|
|
126
|
+
const others = d.candidates.filter(c => c.threadId !== canonical);
|
|
127
|
+
if (!others.length || !Array.isArray(input.dispositions) || input.dispositions.length !== others.length) {
|
|
128
|
+
throw new Error('Require one idle readback and archive receipt for every duplicate.');
|
|
129
|
+
}
|
|
130
|
+
const receipts = others.map(candidate => {
|
|
131
|
+
const matching = input.dispositions.filter(x => x?.threadId === candidate.threadId);
|
|
132
|
+
if (matching.length !== 1) throw new Error('Duplicate disposition missing or repeated.');
|
|
133
|
+
const item = matching[0];
|
|
134
|
+
const t = item.observation?.thread;
|
|
135
|
+
if (t?.id !== candidate.threadId || t.kind !== 'codex' || t.hostId !== d.hostId
|
|
136
|
+
|| t.status?.type !== 'idle' || realpathSync(text(t.cwd, 'duplicate cwd')) !== candidate.cwd
|
|
137
|
+
|| commonGitDir(t.cwd) !== d.gitDir || item.archiveResult?.threadId !== candidate.threadId
|
|
138
|
+
|| item.archiveResult?.archived !== true) throw new Error('Duplicate requires matching idle readback and successful archive receipt.');
|
|
139
|
+
return { threadId: candidate.threadId, archived: true, recordedAt: new Date().toISOString() };
|
|
140
|
+
});
|
|
141
|
+
d.duplicateDisposition = { canonicalThreadId: canonical, archived: receipts };
|
|
142
|
+
d.state = 'pending';
|
|
143
|
+
return view(entry);
|
|
144
|
+
}
|
|
145
|
+
if (action === 'dispatch-observe') {
|
|
146
|
+
const observation = input.observation;
|
|
147
|
+
const t = observation?.thread;
|
|
148
|
+
if (!t || t.kind !== 'codex' || t.hostId !== d.hostId) throw new Error('Require actual Codex read_thread observation on the bound host.');
|
|
149
|
+
const id = actualId(t.id);
|
|
150
|
+
if (d.candidates.length > 1 && (d.duplicateDisposition?.canonicalThreadId !== id
|
|
151
|
+
|| d.candidates.some(c => c.threadId !== id && !d.duplicateDisposition.archived.some(a => a.threadId === c.threadId)))) {
|
|
152
|
+
throw new Error('Multiple candidates: explicit duplicate disposition required before binding.');
|
|
153
|
+
}
|
|
154
|
+
const candidate = d.candidates.find(c => c.threadId === id);
|
|
155
|
+
if (id !== d.candidateThreadId && !candidate) throw new Error('Uncorrelated thread; require create result or exact session prompt evidence.');
|
|
156
|
+
if (commonGitDir(text(t.cwd, 'thread cwd')) !== d.gitDir
|
|
157
|
+
|| (candidate && realpathSync(t.cwd) !== candidate.cwd)) throw new Error('Readback repository/cwd mismatch.');
|
|
158
|
+
if (t.projectId !== undefined && t.projectId !== d.appProjectId) throw new Error('Readback App project mismatch.');
|
|
159
|
+
if (d.candidateThreadId && d.candidateThreadId !== id) throw new Error('Readback differs from create result.');
|
|
160
|
+
d.candidateThreadId = id;
|
|
161
|
+
d.state = 'resolved';
|
|
162
|
+
d.observations.push({ threadId: id, hostId: t.hostId, cwd: realpathSync(t.cwd),
|
|
163
|
+
title: text(t.title, 'observed title'), observedAt: new Date().toISOString() });
|
|
164
|
+
return view(entry);
|
|
165
|
+
}
|
|
166
|
+
if (action === 'supervision-record') {
|
|
167
|
+
if (!entry.threadId || input.threadId !== entry.threadId || input.ownerThreadId === entry.threadId) throw new Error('Require bound child and separate supervisor.');
|
|
168
|
+
const owner = actualId(input.ownerThreadId);
|
|
169
|
+
if (d.supervision && d.supervision.ownerThreadId !== owner) throw new Error('Supervisor already assigned; no parallel wait owner.');
|
|
170
|
+
if (!['active', 'attention', 'completed'].includes(input.state)) throw new Error('Invalid supervision state.');
|
|
171
|
+
d.supervision = { ownerThreadId: owner, state: input.state, cursor: input.cursor == null ? null : text(input.cursor, 'cursor'),
|
|
172
|
+
nextAction: text(input.nextAction, 'nextAction'), updatedAt: new Date().toISOString() };
|
|
173
|
+
return view(entry);
|
|
174
|
+
}
|
|
175
|
+
if (action === 'model-select') {
|
|
176
|
+
if (!entry.threadId || input.threadId !== entry.threadId) throw new Error('Phase changes require the same bound thread.');
|
|
177
|
+
const selection = selectChatModel(entry.type, input.modelRequest);
|
|
178
|
+
d.selection = selection;
|
|
179
|
+
d.selectionHistory.push(selection);
|
|
180
|
+
return { ...view(entry), followUpArgs: { threadId: entry.threadId, hostId: entry.hostId,
|
|
181
|
+
model: selection.model, thinking: selection.thinking } };
|
|
182
|
+
}
|
|
183
|
+
throw new Error(`Unknown dispatch action: ${action}`);
|
|
184
|
+
}
|
|
185
|
+
export function dispatchInput(file) { return jsonFile(file); }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Relative cost/latency policy, not a price list or an availability oracle.
|
|
2
|
+
export const MODEL_POLICY_VERSION = 'chat-model-policy-v1';
|
|
3
|
+
const PROFILES = {
|
|
4
|
+
mechanical: [['gpt-5.4-mini', 'low'], ['gpt-5.6-luna', 'low']],
|
|
5
|
+
scoped: [['gpt-5.6-luna', 'medium'], ['gpt-5.6-terra', 'medium']],
|
|
6
|
+
implementation: [['gpt-5.6-terra', 'medium'], ['gpt-5.6-sol', 'medium']],
|
|
7
|
+
planning: [['gpt-5.6-sol', 'medium'], ['gpt-6-astra', 'medium']],
|
|
8
|
+
review: [['gpt-5.6-sol', 'high'], ['gpt-6-astra', 'high']],
|
|
9
|
+
'scoped-review': [['gpt-5.6-terra', 'medium'], ['gpt-5.6-sol', 'medium']],
|
|
10
|
+
complex: [['gpt-6-astra', 'high'], ['gpt-5.6-sol', 'high']],
|
|
11
|
+
};
|
|
12
|
+
const TYPES = ['coordinator', 'task', 'step-review', 'final-review', 'corrective-audit', 'plan-audit'];
|
|
13
|
+
export function selectChatModel(type, request) {
|
|
14
|
+
if (!TYPES.includes(type)) throw new Error('Unknown chat type.');
|
|
15
|
+
if (!request || !['routine', 'standard', 'complex'].includes(request.complexity)
|
|
16
|
+
|| !['planning', 'implementation', 'mechanical', 'review', 'coordination'].includes(request.phase)
|
|
17
|
+
|| typeof request.rationale !== 'string' || !request.rationale.trim()
|
|
18
|
+
|| typeof request.highRisk !== 'boolean') throw new Error('Explicit phase, complexity, highRisk and rationale are required.');
|
|
19
|
+
if (!Array.isArray(request.availableModels) || request.availableModels.length === 0
|
|
20
|
+
|| request.availableModels.some(m => !m || typeof m.id !== 'string' || !Array.isArray(m.reasoningEfforts)
|
|
21
|
+
|| m.reasoningEfforts.some(e => typeof e !== 'string'))
|
|
22
|
+
|| new Set(request.availableModels.map(m => m.id)).size !== request.availableModels.length) {
|
|
23
|
+
throw new Error('Provide the destination host model/effort catalog; never infer availability.');
|
|
24
|
+
}
|
|
25
|
+
const reviewRole = type.endsWith('review') || type.endsWith('audit');
|
|
26
|
+
let profile;
|
|
27
|
+
if (request.highRisk || request.complexity === 'complex') profile = 'complex';
|
|
28
|
+
else if (type === 'step-review' && request.complexity === 'routine') profile = 'scoped-review';
|
|
29
|
+
else if (reviewRole || request.phase === 'review') profile = 'review';
|
|
30
|
+
else if (request.phase === 'planning') profile = 'planning';
|
|
31
|
+
else if (request.phase === 'mechanical') {
|
|
32
|
+
if (request.complexity !== 'routine') throw new Error('Mechanical phase requires routine complexity.');
|
|
33
|
+
profile = 'mechanical';
|
|
34
|
+
} else if (type === 'coordinator' || request.phase === 'coordination' || request.complexity === 'routine') profile = 'scoped';
|
|
35
|
+
else profile = 'implementation';
|
|
36
|
+
const selected = PROFILES[profile].find(([id, effort]) => request.availableModels.some(m => m.id === id && m.reasoningEfforts.includes(effort)));
|
|
37
|
+
if (!selected) throw new Error(`No supported model/effort for ${profile}; explicit policy revision required, never inherit coordinator settings.`);
|
|
38
|
+
return {
|
|
39
|
+
policyVersion: MODEL_POLICY_VERSION, profile, model: selected[0], thinking: selected[1],
|
|
40
|
+
phase: request.phase, complexity: request.complexity, highRisk: request.highRisk,
|
|
41
|
+
rationale: request.rationale.trim(), fallback: selected !== PROFILES[profile][0],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -12,6 +12,7 @@ import os from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import process from 'node:process';
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { dispatchAction, dispatchInput } from './chat-dispatch.mjs';
|
|
15
16
|
|
|
16
17
|
const TYPES = new Set(['coordinator', 'task', 'step-review', 'final-review', 'corrective-audit', 'plan-audit']);
|
|
17
18
|
const LOCK_STALE_MS = 30_000;
|
|
@@ -25,6 +26,14 @@ export function allocateChat(options) {
|
|
|
25
26
|
const sequence = registry.nextSequence;
|
|
26
27
|
const reservationId = randomUUID();
|
|
27
28
|
const candidates = buildTitleCandidates({ ...options, sequence });
|
|
29
|
+
const existing = registry.entries.find(entry => entry.status !== 'abandoned'
|
|
30
|
+
&& entry.type === options.type && entry.entityId === options.entityId
|
|
31
|
+
&& entry.milestoneOrdinal === requiredOrdinal(options.milestoneOrdinal, 'M')
|
|
32
|
+
&& entry.taskOrdinal === optionalOrdinal(options.taskOrdinal, 'T')
|
|
33
|
+
&& entry.stepOrdinal === optionalOrdinal(options.stepOrdinal, 'S')
|
|
34
|
+
&& (['task', 'coordinator'].includes(options.type)
|
|
35
|
+
|| entry.attempt === positiveInteger(options.attempt ?? 1, 'attempt')));
|
|
36
|
+
if (existing) return { ...publicEntry(existing), reused: true };
|
|
28
37
|
const entry = {
|
|
29
38
|
sequence,
|
|
30
39
|
reservationId,
|
|
@@ -92,6 +101,17 @@ export function bindChat(options) {
|
|
|
92
101
|
const registry = readRegistry(file, options.projectId);
|
|
93
102
|
const entry = requireEntry(registry, options.reservationId);
|
|
94
103
|
if (!entry.titleVerified) throw new Error('Chat title must pass exact readback before binding.');
|
|
104
|
+
if (options.threadId?.startsWith('client-')) throw new Error('Cannot bind a pending clientThreadId.');
|
|
105
|
+
if (entry.threadId && (entry.threadId !== options.threadId || entry.hostId !== options.hostId)) {
|
|
106
|
+
throw new Error('Reservation already bound to another thread.');
|
|
107
|
+
}
|
|
108
|
+
if (registry.entries.some(other => other !== entry && other.threadId === options.threadId
|
|
109
|
+
&& other.hostId === options.hostId)) throw new Error('Thread already belongs to another reservation.');
|
|
110
|
+
if (entry.dispatch && (entry.dispatch.state !== 'resolved' || entry.dispatch.candidateThreadId !== options.threadId
|
|
111
|
+
|| entry.dispatch.hostId !== options.hostId
|
|
112
|
+
|| entry.dispatch.observations.at(-1)?.title !== entry.observedTitle)) {
|
|
113
|
+
throw new Error('Tracked creation must pass correlated read_thread observation before binding.');
|
|
114
|
+
}
|
|
95
115
|
entry.threadId = requiredText(options.threadId, 'threadId');
|
|
96
116
|
entry.hostId = requiredText(options.hostId, 'hostId');
|
|
97
117
|
entry.status = 'bound';
|
|
@@ -106,6 +126,9 @@ export function retitleChat(options) {
|
|
|
106
126
|
return withRegistryLock(options.projectId, options.registryHome, (file) => {
|
|
107
127
|
const registry = readRegistry(file, options.projectId);
|
|
108
128
|
const entry = requireEntry(registry, options.reservationId);
|
|
129
|
+
if (entry.dispatch && options.entityId !== entry.entityId) {
|
|
130
|
+
throw new Error('Tracked dispatch entity is immutable; retitle cannot replace its identity.');
|
|
131
|
+
}
|
|
109
132
|
entry.entityId = requiredText(options.entityId, 'entityId');
|
|
110
133
|
entry.semanticTitle = requiredText(options.semanticTitle, 'semanticTitle');
|
|
111
134
|
entry.candidates = buildTitleCandidates({
|
|
@@ -134,6 +157,7 @@ export function abandonChat(options) {
|
|
|
134
157
|
return withRegistryLock(options.projectId, options.registryHome, (file) => {
|
|
135
158
|
const registry = readRegistry(file, options.projectId);
|
|
136
159
|
const entry = requireEntry(registry, options.reservationId);
|
|
160
|
+
if (entry.dispatch || entry.threadId) throw new Error('Cannot abandon a started creation or bound chat to enable a duplicate.');
|
|
137
161
|
entry.status = 'abandoned';
|
|
138
162
|
entry.updatedAt = new Date().toISOString();
|
|
139
163
|
writeRegistry(file, registry);
|
|
@@ -141,6 +165,20 @@ export function abandonChat(options) {
|
|
|
141
165
|
});
|
|
142
166
|
}
|
|
143
167
|
|
|
168
|
+
export function dispatchChat(options) {
|
|
169
|
+
validateProjectId(options.projectId);
|
|
170
|
+
return withRegistryLock(options.projectId, options.registryHome, file => {
|
|
171
|
+
const registry = readRegistry(file, options.projectId);
|
|
172
|
+
const entry = requireEntry(registry, options.reservationId);
|
|
173
|
+
const result = dispatchAction(options.action, entry, options.input);
|
|
174
|
+
if (options.action !== 'dispatch-status') {
|
|
175
|
+
entry.updatedAt = new Date().toISOString();
|
|
176
|
+
writeRegistry(file, registry);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
144
182
|
export function showRegistry(options) {
|
|
145
183
|
validateProjectId(options.projectId);
|
|
146
184
|
const file = registryFile(options.projectId, options.registryHome);
|
|
@@ -217,7 +255,10 @@ function withRegistryLock(projectId, registryHome, operation) {
|
|
|
217
255
|
|
|
218
256
|
function lockIsStale(lock) {
|
|
219
257
|
try {
|
|
220
|
-
|
|
258
|
+
if (Date.now() - statSync(lock).mtimeMs <= LOCK_STALE_MS) return false;
|
|
259
|
+
const owner = JSON.parse(readFileSync(path.join(lock, 'owner.json'), 'utf8'));
|
|
260
|
+
if (!Number.isSafeInteger(owner.pid) || owner.pid < 1) return false;
|
|
261
|
+
try { process.kill(owner.pid, 0); return false; } catch (error) { return error?.code === 'ESRCH'; }
|
|
221
262
|
} catch {
|
|
222
263
|
return false;
|
|
223
264
|
}
|
|
@@ -253,7 +294,7 @@ function readRegistry(file, projectId) {
|
|
|
253
294
|
|
|
254
295
|
function writeRegistry(file, registry) {
|
|
255
296
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
256
|
-
writeFileSync(temporary, `${JSON.stringify(registry, null, 2)}\n`, { flag: 'wx' });
|
|
297
|
+
writeFileSync(temporary, `${JSON.stringify(registry, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
257
298
|
renameSync(temporary, file);
|
|
258
299
|
}
|
|
259
300
|
|
|
@@ -275,6 +316,7 @@ function publicEntry(entry) {
|
|
|
275
316
|
threadId: entry.threadId,
|
|
276
317
|
hostId: entry.hostId,
|
|
277
318
|
status: entry.status,
|
|
319
|
+
dispatchState: entry.dispatch?.state ?? null,
|
|
278
320
|
};
|
|
279
321
|
}
|
|
280
322
|
|
|
@@ -334,6 +376,11 @@ function cli() {
|
|
|
334
376
|
projectId: option(args, 'project-id'),
|
|
335
377
|
registryHome: option(args, 'registry-home'),
|
|
336
378
|
};
|
|
379
|
+
if (action?.startsWith('dispatch-') || action === 'supervision-record' || action === 'model-select') {
|
|
380
|
+
const file = option(args, 'file');
|
|
381
|
+
return dispatchChat({ ...common, action, reservationId: option(args, 'reservation-id'),
|
|
382
|
+
input: file ? dispatchInput(file) : {} });
|
|
383
|
+
}
|
|
337
384
|
if (action === 'allocate') return allocateChat({
|
|
338
385
|
...common,
|
|
339
386
|
type: option(args, 'type'),
|
|
@@ -363,7 +410,7 @@ function cli() {
|
|
|
363
410
|
});
|
|
364
411
|
if (action === 'abandon') return abandonChat({ ...common, reservationId: option(args, 'reservation-id') });
|
|
365
412
|
if (action === 'show') return showRegistry(common);
|
|
366
|
-
throw new Error('Use allocate, retitle, readback, bind, abandon, or
|
|
413
|
+
throw new Error('Use allocate, retitle, readback, bind, abandon, show, dispatch-begin/result/discover/resolve-duplicates/observe/status, supervision-record, or model-select.');
|
|
367
414
|
}
|
|
368
415
|
|
|
369
416
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
|