sortie-dogs 0.14.1 → 0.14.2

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 CHANGED
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
19
19
  [![Node.js](https://img.shields.io/node/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
20
20
  [![MIT License](https://img.shields.io/npm/l/sortie-dogs)](LICENSE)
21
21
 
22
- **Current release: [v0.14.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.1)**
23
- · [Release notes](docs/release-v0.14.1.md) · [Execution vision](docs/v0.14.0-execution-vision.md)
22
+ **Current release: [v0.14.2](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.2)**
23
+ · [Release notes](docs/release-v0.14.2.md) · [Execution vision](docs/v0.14.0-execution-vision.md)
24
24
 
25
25
  [Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
26
26
  [Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
@@ -57,7 +57,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
57
57
  available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
58
58
 
59
59
  ```sh
60
- npm install --save-dev sortie-dogs@0.14.1
60
+ npm install --save-dev sortie-dogs@0.14.2
61
61
  npx --no-install sortie-dogs codex init .
62
62
  ```
63
63
 
@@ -86,7 +86,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
86
86
  Use **OpenCode V2**. In the target project:
87
87
 
88
88
  ```sh
89
- npm install --save-dev sortie-dogs@0.14.1
89
+ npm install --save-dev sortie-dogs@0.14.2
90
90
  npx --no-install sortie-dogs init .
91
91
  ```
92
92
 
@@ -242,7 +242,7 @@ Install the desired package version and rerun **your host's** initializer:
242
242
 
243
243
  An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
244
244
  update it. [Global/update instructions](docs/configuration.md#global-availability).
245
- The current Mission asset marker is `0.14.1-review-continuation-v1`; the unchanged Codex skill marker is
245
+ The current Mission asset marker is `0.14.2-cold-resume-v1`; the unchanged Codex skill marker is
246
246
  `0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
247
247
 
248
248
  There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
@@ -258,7 +258,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
258
258
  [SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
259
259
  - **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
260
260
  [Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
261
- - **Releases:** [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
261
+ - **Releases:** [v0.14.2](docs/release-v0.14.2.md) · [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
262
262
  [v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
263
263
 
264
264
  ## Community
@@ -3,6 +3,6 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export declare const V010_RUNTIME_ASSET_VERSION = "0.14.1-review-continuation-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.14.2-cold-resume-v1";
7
7
  export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
8
8
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
@@ -3,5 +3,5 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export const V010_RUNTIME_ASSET_VERSION = "0.14.1-review-continuation-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.14.2-cold-resume-v1";
7
7
  export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
@@ -71,6 +71,9 @@ export function codexMissionProgress(event) {
71
71
  native_status: event.params.turn?.status };
72
72
  if (event.method === "sortie/continuation")
73
73
  return { thread_id: event.threadId, turn_id: event.params.turnId, phase: event.params.phase, calls: event.params.calls };
74
+ if (event.method === "sortie/coordinatorRestored")
75
+ return { thread_id: event.threadId, phase: "coordinator-restored", mission_id: event.params.missionId,
76
+ coordinator_id: event.params.coordinatorId, call_id: event.params.callId, native_outcome: event.params.outcome, next_action: event.params.action };
74
77
  if (event.method === "sortie/modelRoute")
75
78
  return { thread_id: event.threadId, phase: "model-route", agent: event.params.agent, model: event.params.model,
76
79
  effort: event.params.effort, service_tier: event.params.serviceTier };
@@ -79,8 +79,9 @@ export declare class CodexMissionSession {
79
79
  close(): Promise<void>;
80
80
  private claimOwner;
81
81
  private rootOf;
82
+ private restoreCoordinator;
82
83
  private recoverNotStarted;
83
- private bindWorkerDispatch;
84
+ private bindTaskDispatch;
84
85
  private recoverCompletedWorker;
85
86
  private required;
86
87
  private executeHostCommand;
@@ -62,6 +62,7 @@ export class CodexMissionSession {
62
62
  this.observations = missions.find(item => item.root === resume)?.codexUsage ?? [];
63
63
  await this.restoreSession(resume, "dog-operator");
64
64
  this.root = resume;
65
+ await this.restoreCoordinator(resume);
65
66
  await this.claimOwner(resume);
66
67
  await this.hooks.tool?.sortie_v010_operator_status?.execute({ view: "full" }, { sessionID: resume, agent: "dog-operator" });
67
68
  }
@@ -118,6 +119,45 @@ export class CodexMissionSession {
118
119
  current = this.required(current.parentID);
119
120
  return current.id;
120
121
  }
122
+ async restoreCoordinator(root) {
123
+ const missions = new OperatorMissionRuntime(this.directory, profile);
124
+ const mission = await missions.required(root);
125
+ if (!mission.coordinator || ["completed", "cancelled"].includes(mission.phase))
126
+ return;
127
+ if (!await codexOwnerGone(mission.codexOwner))
128
+ throw new Error("codex-mission-owner-active:no-resend");
129
+ // Codex can persist claim() and the child turn but omit the interrupted parent
130
+ // Task item. The existing Mission binding still owns this same Coordinator.
131
+ const coordinator = await this.restoreSession(mission.coordinator, "dogs-coordinator", root);
132
+ if (coordinator.parentID !== root || coordinator.agent !== "dogs-coordinator")
133
+ throw new Error("Retained Coordinator lineage mismatch.");
134
+ if (mission.dispatchOpen && mission.callID) {
135
+ // A retained terminal parent receipt already has the shared reconciliation path.
136
+ // The new binding is required only when that exact receipt is missing.
137
+ const parentReceipts = this.required(root).history.flatMap(message => Array.isArray(message.parts) ? message.parts.filter(object) : [])
138
+ .filter(part => part.type === "tool" && part.tool === "task" && part.callID === mission.callID && object(part.state) &&
139
+ ["completed", "error"].includes(String(part.state.status)) && object(part.state.input) &&
140
+ part.state.input.subagent_type === coordinator.agent &&
141
+ (part.state.input.task_id === undefined || part.state.input.task_id === coordinator.id));
142
+ if (parentReceipts.length === 1)
143
+ return;
144
+ const binding = mission.codexCoordinatorDispatch;
145
+ if (!binding || binding.missionID !== mission.id || binding.threadID !== root || binding.callID !== mission.callID ||
146
+ binding.childThreadID !== coordinator.id || binding.ownerGeneration !== mission.codexOwner?.generation ||
147
+ coordinator.history.filter(message => object(message.info) && message.info.role === "user" &&
148
+ message.info.id === binding.clientUserMessageID).length !== 1)
149
+ throw new Error("Retained Coordinator has unproven execution for the current dispatch; no resend. Reconcile its exact native turn before resuming.");
150
+ await missions.codexRecovery(root, async (current) => {
151
+ if (current.codexOwner?.generation !== binding.ownerGeneration || !await codexOwnerGone(current.codexOwner) ||
152
+ JSON.stringify(current.codexCoordinatorDispatch) !== JSON.stringify(binding))
153
+ throw new Error("codex-mission-recovery-generation-stale");
154
+ await missions.reconcileFinishedDispatch(root, mission.id, mission.callID);
155
+ });
156
+ await this.options.onEvent?.({ method: "sortie/coordinatorRestored", threadId: root,
157
+ params: { missionId: mission.id, coordinatorId: coordinator.id, callId: mission.callID,
158
+ outcome: coordinator.outcome, action: "resume-existing-task" } });
159
+ }
160
+ }
121
161
  async recoverNotStarted(threadID, turn, item) {
122
162
  if (item.tool !== "task" || !object(item.arguments) || item.arguments.task_id || typeof item.id !== "string" || typeof turn.id !== "string" ||
123
163
  !["completed", "interrupted", "failed"].includes(String(turn.status)))
@@ -156,7 +196,18 @@ export class CodexMissionSession {
156
196
  }
157
197
  return false;
158
198
  }
159
- async bindWorkerDispatch(parent, child, call, messageID) {
199
+ async bindTaskDispatch(parent, child, call, messageID) {
200
+ if (child.agent === "dogs-coordinator") {
201
+ await new OperatorMissionRuntime(this.directory, profile).update(this.rootOf(parent), state => {
202
+ if (state.root !== call.threadId || state.coordinator !== child.id || state.callID !== call.callId || !state.dispatchOpen ||
203
+ state.codexOwner?.generation !== this.owner.generation || state.codexOwner.closed)
204
+ throw new Error("codex-mission-dispatch-owner-stale");
205
+ state.codexCoordinatorDispatch = { missionID: state.id, threadID: call.threadId, turnID: call.turnId, callID: call.callId,
206
+ inputHash: createHash("sha256").update(JSON.stringify(call.arguments)).digest("hex"),
207
+ childThreadID: child.id, clientUserMessageID: messageID, ownerGeneration: this.owner.generation };
208
+ });
209
+ return;
210
+ }
160
211
  if (child.agent !== "dog-worker-v010" || !object(call.arguments) || call.arguments.task_id)
161
212
  return;
162
213
  const root = this.rootOf(parent), missions = new OperatorMissionRuntime(this.directory, profile);
@@ -365,7 +416,7 @@ export class CodexMissionSession {
365
416
  throw new Error(`Codex thread ${id} has no matching repository history.`);
366
417
  const turns = thread.turns.filter(object);
367
418
  for (const turn of turns) {
368
- if (!Array.isArray(turn.items) || turn.status === "inProgress" || turn.itemsView !== "full")
419
+ if (!Array.isArray(turn.items) || !["completed", "failed", "interrupted", "cancelled"].includes(String(turn.status)) || turn.itemsView !== "full")
369
420
  throw new Error(`Codex thread ${id} still has unproven execution; no resend. Reconcile its native execution before resuming.`);
370
421
  for (const item of turn.items.filter(object)) {
371
422
  // A terminal parent turn does not prove that its native executions or children stopped.
@@ -373,7 +424,8 @@ export class CodexMissionSession {
373
424
  ["commandExecution", "fileChange", "mcpToolCall"].includes(String(item.type)) &&
374
425
  !["completed", "failed", ...(item.type === "mcpToolCall" ? [] : ["declined"])].includes(String(item.status)))
375
426
  throw new Error(`Codex thread ${id} has an unresolved native execution; no resend. Reconcile its native execution before resuming.`);
376
- if (item.type !== "dynamicToolCall" || item.status === "completed" && (turn.status === "completed" || item.success === true))
427
+ if (item.type !== "dynamicToolCall" || ["completed", "failed"].includes(String(item.status)) &&
428
+ (turn.status === "completed" || item.success === true))
377
429
  continue;
378
430
  const unique = turns.flatMap(entry => Array.isArray(entry.items) ? entry.items.filter(object) : []).filter(entry => entry.id === item.id).length === 1;
379
431
  if (!unique)
@@ -475,7 +527,7 @@ export class CodexMissionSession {
475
527
  result = await this.promptTurn(session, "The previous native response ended while existing host tools were still running. Those exact calls have now returned:\n" +
476
528
  JSON.stringify(result.deferredToolResults) +
477
529
  "\nContinue this same Mission from these results through the remaining validation, delivery, review and root acceptance. " +
478
- "Do not resend the original request or redispatch the completed Tasks. Preserve failures, ownership and cumulative budget; use existing Mission status for the next action.", undefined, true);
530
+ "Do not resend the original request or redispatch the completed Tasks. Preserve failures, ownership and cumulative budget; use existing Mission status for the next action.", session.agent === "dogs-coordinator" ? onDispatch : undefined, true);
479
531
  }
480
532
  return result;
481
533
  }
@@ -577,7 +629,7 @@ export class CodexMissionSession {
577
629
  });
578
630
  await this.hooks["tool.execute.after"]?.({ tool: call.tool, sessionID: session.id, callID: call.callId, args }, { output: "", metadata: { sessionId: child.id, status: "running" } });
579
631
  const result = await this.prompt(child, String(args.prompt), async (messageID) => {
580
- await this.bindWorkerDispatch(session, child, call, messageID);
632
+ await this.bindTaskDispatch(session, child, call, messageID);
581
633
  dispatched = true;
582
634
  });
583
635
  text = result.finalResponse ?? "";
@@ -49,6 +49,7 @@ export interface CodexWorkerDispatch {
49
49
  clientUserMessageID: string;
50
50
  ownerGeneration: string;
51
51
  }
52
+ export type CodexCoordinatorDispatch = Omit<CodexWorkerDispatch, "runID" | "unitID">;
52
53
  export interface CodexCompletedTaskProof {
53
54
  dispatch: CodexWorkerDispatch;
54
55
  childTurnID: string;
@@ -209,6 +210,8 @@ export interface OperatorMission {
209
210
  codexNotStarted?: CodexNotStartedProof[];
210
211
  /** Missing parent receipts reconciled from exact completed native Worker turns. */
211
212
  codexCompletedTasks?: CodexCompletedTaskProof[];
213
+ /** Current Coordinator dispatch bound before its exact native turn is sent. */
214
+ codexCoordinatorDispatch?: CodexCoordinatorDispatch;
212
215
  version: "0.12";
213
216
  id: string;
214
217
  root: string;
@@ -1228,6 +1228,9 @@ export function createProfiledPlugin(profile, assetVersion) {
1228
1228
  return { ...packet, next_action: "Fast-lane: dispatch the exact Reviewer Task from review_mission if not yet active; otherwise wait for that Reviewer. Do not start a Coordinator or another Worker while review is pending." };
1229
1229
  }
1230
1230
  if (mission.runID === run?.runID && run?.units.some(unit => unit.status === "running")) {
1231
+ if (mission.coordinator && !mission.dispatchOpen && !["completed", "cancelled"].includes(mission.phase))
1232
+ return { ...packet, task: missions.task(mission),
1233
+ next_action: "Resume this existing Coordinator Task with its task_id to continue the retained unit. Do not dispatch a replacement or wait for background work that has already ended." };
1231
1234
  return { ...packet, next_action: "The current unit is active. Continue its owning session; do not dispatch a replacement Worker or Coordinator. Native background completion will return to the parent." };
1232
1235
  }
1233
1236
  if (mission.coordinator === null && mission.runID === run?.runID && !mission.dispatchOpen &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",