sortie-dogs 0.13.21 → 0.13.22

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.
@@ -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.13.21-operation-recovery-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.13.22-owned-task-lifecycle-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.13.21-operation-recovery-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.13.22-owned-task-lifecycle-v1";
7
7
  export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
@@ -47,7 +47,7 @@ export async function runCodexMissionCommand(argv) {
47
47
  const result = await adapter.run(prompt);
48
48
  process.stdout.write(`${JSON.stringify({ status: result.accepted ? "succeeded" : "incomplete", root_session_id: result.rootSessionID,
49
49
  thread_id: result.turn.threadID, turn_id: result.turn.turnID, native_status: result.turn.status,
50
- response: result.turn.finalResponse, sessions: result.sessions, cost: null })}\n`);
50
+ response: result.turn.finalResponse, lifecycle: result.lifecycle, sessions: result.sessions, cost: null })}\n`);
51
51
  return signalExit ?? (result.accepted ? 0 : 1);
52
52
  }
53
53
  catch (error) {
@@ -66,6 +66,11 @@ export async function runCodexMissionCommand(argv) {
66
66
  }
67
67
  /** Bounded user-facing progress; do not dump control packets or hidden reasoning. */
68
68
  export function codexMissionProgress(event) {
69
+ if (event.method === "turn/completed")
70
+ return { thread_id: event.threadId, phase: "native-turn-terminal", turn_id: event.params.turn?.id,
71
+ native_status: event.params.turn?.status };
72
+ if (event.method === "sortie/continuation")
73
+ return { thread_id: event.threadId, turn_id: event.params.turnId, phase: event.params.phase, calls: event.params.calls };
69
74
  if (event.method === "sortie/modelRoute")
70
75
  return { thread_id: event.threadId, phase: "model-route", agent: event.params.agent, model: event.params.model,
71
76
  effort: event.params.effort, service_tier: event.params.serviceTier };
@@ -46,6 +46,14 @@ export interface CodexTurnResult {
46
46
  readonly finalResponse?: string;
47
47
  readonly items: readonly JsonObject[];
48
48
  readonly usage?: JsonObject;
49
+ /** Host tools still running at native turn completion, drained without resending them. */
50
+ readonly deferredToolResults?: readonly CodexDeferredToolResult[];
51
+ }
52
+ export interface CodexDeferredToolResult {
53
+ readonly callID: string;
54
+ readonly tool: string;
55
+ readonly success: boolean;
56
+ readonly output: string;
49
57
  }
50
58
  export interface CodexAuthenticationState {
51
59
  readonly type: string | null;
@@ -188,7 +188,7 @@ export class CodexAppServerHost {
188
188
  if (!turnID)
189
189
  throw new CodexHostError("protocol-invalid-response", "turn/start returned no turn id.");
190
190
  return new Promise((resolve, reject) => {
191
- this.active = { threadID, turnID, items: [], observer: Promise.resolve(), onEvent: options.onEvent, resolve, reject };
191
+ this.active = { threadID, turnID, items: [], observer: Promise.resolve(), tools: new Set(), onEvent: options.onEvent, resolve, reject };
192
192
  const backlog = this.notificationBacklog.splice(0);
193
193
  void backlog.reduce((prior, event) => prior.then(() => this.notification(event.method, event.params)), Promise.resolve())
194
194
  .catch(error => { this.active?.reject(error); this.active = undefined; });
@@ -289,20 +289,34 @@ export class CodexAppServerHost {
289
289
  const params = object(message.params) ? message.params : {};
290
290
  if (method === "item/tool/call") {
291
291
  const active = this.active;
292
- let result;
292
+ if (!active || active.completing || params.threadId !== active.threadID || params.turnId !== active.turnID ||
293
+ (params.namespace != null) || typeof params.callId !== "string" || typeof params.tool !== "string" || !this.options.dynamicTool) {
294
+ this.transport.send({ id: message.id, result: { contentItems: [{ type: "inputText",
295
+ text: "Dynamic tool call does not belong to an active Sortie turn." }], success: false } });
296
+ return;
297
+ }
298
+ const call = { threadId: active.threadID, turnId: active.turnID, callId: params.callId, tool: params.tool, arguments: params.arguments };
299
+ const pending = Promise.resolve().then(async () => {
300
+ let output, success;
301
+ try {
302
+ output = await this.options.dynamicTool(call);
303
+ success = true;
304
+ }
305
+ catch (error) {
306
+ output = error instanceof Error ? error.message : "Dynamic tool failed.";
307
+ success = false;
308
+ }
309
+ if (!this.closed && !this.fault)
310
+ this.transport.send({ id: message.id, result: { contentItems: [{ type: "inputText", text: output }], success } });
311
+ return { callID: call.callId, tool: call.tool, success, output };
312
+ });
313
+ active.tools.add(pending);
293
314
  try {
294
- if (!active || active.completing || params.threadId !== active.threadID || params.turnId !== active.turnID ||
295
- (params.namespace != null) || typeof params.callId !== "string" || typeof params.tool !== "string" || !this.options.dynamicTool)
296
- throw new Error("Dynamic tool call does not belong to an active Sortie turn.");
297
- const output = await this.options.dynamicTool({ threadId: active.threadID, turnId: active.turnID,
298
- callId: params.callId, tool: params.tool, arguments: params.arguments });
299
- result = { contentItems: [{ type: "inputText", text: output }], success: true };
315
+ await pending;
300
316
  }
301
- catch (error) {
302
- result = { contentItems: [{ type: "inputText", text: error instanceof Error ? error.message : "Dynamic tool failed." }], success: false };
317
+ finally {
318
+ active.tools.delete(pending);
303
319
  }
304
- if (!this.closed)
305
- this.transport.send({ id: message.id, result });
306
320
  return;
307
321
  }
308
322
  if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
@@ -374,14 +388,23 @@ export class CodexAppServerHost {
374
388
  // requests such as interrupt(), whose response must be read by this pump.
375
389
  // Keep the completing turn active until observers settle so close() can
376
390
  // reject it and a later turn cannot receive an interrupt meant for it.
377
- void active.observer.then(() => {
391
+ const deferred = status === "completed" ? [...active.tools] : [];
392
+ // Native response completion is not host execution completion. Keep pumping
393
+ // command responses and child Tasks; explicit close/transport loss can still
394
+ // reject this turn. Failed/interrupted native turns never auto-continue.
395
+ void Promise.all([active.observer, Promise.all(deferred)]).then(([, tools]) => {
378
396
  if (this.active !== active)
379
397
  return;
380
398
  this.active = undefined;
381
399
  if (active.observerError)
382
400
  active.reject(new CodexHostError("request-failed", "Codex turn event observer failed.", active.observerError));
383
401
  else
384
- active.resolve(result);
402
+ active.resolve({ ...result, ...(tools.length ? { deferredToolResults: tools } : {}) });
403
+ }).catch(error => {
404
+ if (this.active !== active)
405
+ return;
406
+ this.active = undefined;
407
+ active.reject(error);
385
408
  });
386
409
  }
387
410
  }
@@ -67,6 +67,12 @@ export declare class CodexMissionSession {
67
67
  rootSessionID: string;
68
68
  turn: CodexTurnResult;
69
69
  accepted: boolean;
70
+ lifecycle: {
71
+ root_turn_terminal: boolean;
72
+ owned_tasks_settled: boolean;
73
+ mission_phase: string | null;
74
+ mission_accepted: boolean;
75
+ };
70
76
  sessions: readonly JsonObject[];
71
77
  }>;
72
78
  private stopHosts;
@@ -85,6 +91,7 @@ export declare class CodexMissionSession {
85
91
  private restoreSession;
86
92
  private tools;
87
93
  private prompt;
94
+ private promptTurn;
88
95
  private execute;
89
96
  }
90
97
  export {};
@@ -72,8 +72,11 @@ export class CodexMissionSession {
72
72
  const previous = await new OperatorMissionRuntime(this.directory, profile).read(session.id);
73
73
  const turn = await this.prompt(session, prompt);
74
74
  const mission = await new OperatorMissionRuntime(this.directory, profile).read(session.id);
75
- return { rootSessionID: session.id, turn, accepted: turn.status === "completed" && mission?.phase === "completed" &&
76
- (previous?.phase !== "completed" || previous.id !== mission.id), sessions: [...this.sessions.values()].map(item => ({ thread_id: item.id, agent: item.agent, model: item.model, usage: item.usage ?? null, usage_scope: "native-thread-cumulative", cost: null })) };
75
+ const ownedTasksSettled = [...this.sessions.values()].every(item => !["running", "unknown"].includes(item.outcome));
76
+ const accepted = turn.status === "completed" && ownedTasksSettled && mission?.phase === "completed" &&
77
+ (previous?.phase !== "completed" || previous.id !== mission.id);
78
+ return { rootSessionID: session.id, turn, accepted, lifecycle: { root_turn_terminal: ["completed", "failed", "interrupted", "cancelled"].includes(turn.status),
79
+ owned_tasks_settled: ownedTasksSettled, mission_phase: mission?.phase ?? null, mission_accepted: accepted }, sessions: [...this.sessions.values()].map(item => ({ thread_id: item.id, agent: item.agent, model: item.model, usage: item.usage ?? null, usage_scope: "native-thread-cumulative", cost: null })) };
77
80
  }
78
81
  async stopHosts() {
79
82
  this.closed = true;
@@ -459,11 +462,33 @@ export class CodexMissionSession {
459
462
  return tools;
460
463
  }
461
464
  async prompt(session, prompt, onDispatch) {
465
+ let result = await this.promptTurn(session, prompt, onDispatch);
466
+ while (result.status === "completed" && result.deferredToolResults?.length) {
467
+ if (this.closed)
468
+ throw new Error("Codex Mission adapter is closed.");
469
+ const mission = await new OperatorMissionRuntime(this.directory, profile).read(this.rootOf(session));
470
+ if (!mission || ["completed", "cancelled"].includes(mission.phase))
471
+ break;
472
+ await this.claimOwner(this.rootOf(session));
473
+ await this.options.onEvent?.({ method: "sortie/continuation", threadId: session.id,
474
+ params: { turnId: result.turnID, phase: "owned-tools-settled", calls: result.deferredToolResults.map(item => ({ callId: item.callID, tool: item.tool, success: item.success })) } });
475
+ 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
+ JSON.stringify(result.deferredToolResults) +
477
+ "\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);
479
+ }
480
+ return result;
481
+ }
482
+ async promptTurn(session, prompt, onDispatch, continuation = false) {
462
483
  const startedAt = Date.now();
463
484
  const messageID = randomUUID();
464
485
  const message = { id: messageID, agent: session.agent, model: { ...session.model } };
465
- const parts = [{ type: "text", text: prompt }];
466
- await this.hooks["chat.message"]?.({ sessionID: session.id, messageID, agent: session.agent }, { message, parts });
486
+ const parts = [{ type: "text", text: prompt, ...(continuation ? { synthetic: true } : {}) }];
487
+ // Only this host's drained results can continue an already-admitted execution.
488
+ // Reclaiming that Task with the result prompt rejects Workers' exact handoff;
489
+ // treating it as a real root request also grants spurious user-turn authority.
490
+ if (!continuation)
491
+ await this.hooks["chat.message"]?.({ sessionID: session.id, messageID, agent: session.agent }, { message, parts });
467
492
  // Shared Task routing may select a model; explicit adapter overrides still govern every native turn.
468
493
  if (message.model.providerID !== "openai")
469
494
  throw new Error("Codex Mission cannot use a non-OpenAI model route.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.13.21",
3
+ "version": "0.13.22",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",