sortie-dogs 0.16.4 → 0.16.5

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.16.4-compaction-model-view-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.16.5-mission-direct-retry-v2";
7
7
  export declare const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
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.16.4-compaction-model-view-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.16.5-mission-direct-retry-v2";
7
7
  export const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
@@ -64,6 +64,8 @@ export interface MissionAttempt {
64
64
  /** Fingerprint of the settled scoped candidate against which a later Rescue is proposed. */
65
65
  candidateID?: string;
66
66
  kind: "implementation" | "normal_remediation" | "astra_rescue" | "reviewer_correction" | "direct_execution";
67
+ /** A direct execution may itself be the one ordinary remediation. */
68
+ normalRemediation?: boolean;
67
69
  status: "pending" | "dispatched" | "succeeded" | "failed" | "cancelled" | "unconfirmed" | "handed-off";
68
70
  callID?: string;
69
71
  childSessionID?: string;
@@ -964,7 +964,7 @@ export function missionPacket(mission, run, includeWorkQuantity = true) {
964
964
  : run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
965
965
  ? "Coordinator: inspect units[].dispatch_denial before changing the plan. Correct only its diagnosed cause; do not repeat an unchanged refused Task or replan for a host-state mismatch. Report an unresolved runtime mismatch with the loaded runtime identity; preserve requirements and cumulative spend."
966
966
  : run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && !unit.normalRemediationUsed)
967
- ? "Coordinator: one exact declared validation failed. Reconcile the returned Worker and cumulative budget, then call retry_mission_unit once for that unit before replanning. This preserves the same scope and acceptance."
967
+ ? `Coordinator: one exact declared validation failed. Reconcile the ${run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && !unit.normalRemediationUsed && unit.directExecution) ? "settled direct execution" : "returned Worker"} and cumulative budget, then call retry_mission_unit once for that unit before replanning. This preserves the same scope and acceptance.`
968
968
  : run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && unit.normalRemediationUsed && !unit.terminalRescue)
969
969
  ? "Coordinator: the one ordinary remediation failed the declared validation again. If its native terminal, writer release, exact candidate and cumulative budget permit, call rescue_mission_unit once; inspect and report a non_rescue reason, then continue ordinary correction within budget. Rescue does not bypass validation, review or root acceptance."
970
970
  : "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative.")
@@ -155,6 +155,12 @@ interface UnitState {
155
155
  handedOffAt: string;
156
156
  settledCallID?: string;
157
157
  })[];
158
+ /** Settled direct attempts are historical, not handoffs or validation for a later attempt. */
159
+ priorDirectExecutions?: (NonNullable<UnitState["directExecution"]> & {
160
+ callID: string;
161
+ resultClass: string | null;
162
+ failure?: SerialDispatchSettlement["failure"];
163
+ })[];
158
164
  resultClass: string | null;
159
165
  failure?: SerialDispatchSettlement["failure"];
160
166
  normalRemediationUsed?: boolean;
@@ -393,6 +393,7 @@ export class OperatorRuntime {
393
393
  ...(unit.reviewerCorrection ? { reviewerCorrection: sizedChecks(unit.reviewerCorrection) } : {}),
394
394
  ...(unit.validationRegistration ? { validationRegistration: sizedChecks(unit.validationRegistration) } : {}),
395
395
  ...(Array.isArray(unit.directHandoffs) ? { directHandoffs: unit.directHandoffs.map(sizedChecks) } : {}),
396
+ ...(Array.isArray(unit.priorDirectExecutions) ? { priorDirectExecutions: unit.priorDirectExecutions.map(sizedChecks) } : {}),
396
397
  };
397
398
  }) : execution.units };
398
399
  if (Buffer.byteLength(JSON.stringify(sized)) > OPERATOR_LIMITS.planBytes * 4)
@@ -2000,11 +2001,19 @@ export class OperatorRuntime {
2000
2001
  state.units.some(candidate => candidate.terminalRescue))
2001
2002
  throw new Error("operator-mission-normal-remediation-unavailable");
2002
2003
  await this.verifyControls(unit);
2004
+ if (unit.directExecution) {
2005
+ if (!unit.directExecution.finishedAt || !unit.callID)
2006
+ throw new Error("operator-mission-normal-remediation-unavailable");
2007
+ (unit.priorDirectExecutions ??= []).push({ ...structuredClone(unit.directExecution), callID: unit.callID,
2008
+ resultClass: unit.resultClass, failure: structuredClone(unit.failure) });
2009
+ delete unit.directExecution;
2010
+ }
2003
2011
  // The ordinary retry keeps its exact scope and validation, but the next Worker needs the
2004
2012
  // host-observed failed command/exit rather than reconstructing it from an earlier session.
2005
2013
  unit.task = { ...unit.task, prompt: `${unit.task.prompt}\nnormal_remediation_prior_validation: ${JSON.stringify({
2006
2014
  command: unit.failure.command, exit_code: unit.failure.exitCode
2007
- })}` };
2015
+ })}${unit.validationRegistration
2016
+ ? `\nnormal_remediation_validation: ${JSON.stringify({ validation: unitValidation(unit), validation_cwd: unitValidationCwd(unit) })}` : ""}` };
2008
2017
  unit.normalRemediationUsed = true;
2009
2018
  unit.status = "pending";
2010
2019
  unit.callID = null;
@@ -2048,6 +2057,13 @@ export class OperatorRuntime {
2048
2057
  await this.verifyControls(unit);
2049
2058
  // The Rescue route is sealed in host-owned operator state and the returned Task reference.
2050
2059
  // Do not alter the registered handoff or expose the stable runtime's rescue capability.
2060
+ if (unit.directExecution) {
2061
+ if (!unit.directExecution.finishedAt || !unit.callID)
2062
+ throw new Error("operator-mission-terminal-rescue-unavailable");
2063
+ (unit.priorDirectExecutions ??= []).push({ ...structuredClone(unit.directExecution), callID: unit.callID,
2064
+ resultClass: unit.resultClass, failure: structuredClone(unit.failure) });
2065
+ delete unit.directExecution;
2066
+ }
2051
2067
  unit.task = { ...unit.task, prompt: unit.task.prompt.replace(header, line => `${line}\nterminal_rescue_attempt: ${input.attempt_id}`) };
2052
2068
  unit.terminalRescue = { attempt_id: input.attempt_id, selected_model: input.selected_model,
2053
2069
  selected_variant: input.selected_variant, accepted_base: structuredClone(base), prior_failure: structuredClone(unit.failure), status: "prepared" };
@@ -7942,6 +7942,56 @@ export const SortieDogsPlugin = async (input, options) => {
7942
7942
  }
7943
7943
  },
7944
7944
  };
7945
+ // Both direct and Worker retries must prove the exact reservation settled and its writer released.
7946
+ // A direct actor is not a native child Task and must not acquire a fabricated terminal record.
7947
+ async function missionDispatchWriterReleased(root, taskID, callID, actor, writeScopes) {
7948
+ const snapshot = await (await goalLedger(root)).readGoal();
7949
+ const origin = await input.runtimeBridge?.missionDispatchOrigin?.(root, taskID, callID);
7950
+ const reservationID = goalFingerprint({ goal_id: snapshot.state.goal_id, unit_id: taskID, call_id: origin?.callID ?? callID });
7951
+ if (snapshot.state.outstanding_reservations.some(item => item.reservation_id === reservationID) ||
7952
+ !snapshot.records.some(({ event }) => event.kind === "unit.settled" && event.goal_id === snapshot.state.goal_id &&
7953
+ event.unit_id === taskID && event.reservation_id === reservationID)) {
7954
+ return { ready: false, reason: "exact_dispatch_settlement_missing" };
7955
+ }
7956
+ const active = activeSessions.get(actor), authorization = sessionAuthorizations.get(actor);
7957
+ if (authorization && (authorization.rootSessionID !== root || authorization.taskID !== taskID ||
7958
+ authorization.dispatchCallID !== callID))
7959
+ return { ready: false, reason: "writer_owned_by_other_dispatch" };
7960
+ if (active?.inFlightCalls.size) {
7961
+ const response = await input.client?.session?.messages?.({ path: { id: actor }, query: { directory: input.directory } }).catch(() => undefined);
7962
+ const history = isRecord(response) && Array.isArray(response.data) ? response.data : undefined;
7963
+ if (!history)
7964
+ return { ready: false, reason: "tool_terminal_records_unavailable" };
7965
+ for (const pendingID of active.inFlightCalls) {
7966
+ const parts = history.flatMap(message => isRecord(message) && isRecord(message.info) &&
7967
+ message.info.sessionID === actor && Array.isArray(message.parts) ? message.parts : [])
7968
+ .filter(part => isRecord(part) && part.type === "tool" && part.callID === pendingID);
7969
+ if (parts.length !== 1 || !isRecord(parts[0]) || !isRecord(parts[0].state) ||
7970
+ !["completed", "error"].includes(String(parts[0].state.status)))
7971
+ return { ready: false, reason: "tool_dispatch_active_or_unproven" };
7972
+ }
7973
+ active.inFlightCalls.clear();
7974
+ }
7975
+ const released = JSON.parse(await releaseWriteGate(actor));
7976
+ if (released.status === "denied")
7977
+ return { ready: false, reason: released.reason };
7978
+ if (active)
7979
+ active.released = true;
7980
+ const scopeRoot = await durableScopeRoot(project?.root ?? input.directory);
7981
+ if (scopeRoot === undefined)
7982
+ return active?.parallel === "valid"
7983
+ ? { ready: false, reason: "writer_registry_unavailable" } : { ready: true };
7984
+ try {
7985
+ const local = writeScopes.map(path => relative(project?.root ?? input.directory, resolve(project?.root ?? input.directory, normalizeManifestScope(path).path)).replaceAll("\\", "/"))
7986
+ .filter(path => path !== ".." && !path.startsWith("../") && !isAbsolute(path));
7987
+ const scope = normalizeWorktreeScope({ read: [], write: local });
7988
+ return await new ScopeLeaseRegistry(scopeRoot).hasConflictingLease(scope)
7989
+ ? { ready: false, reason: "writer_lease_owned_elsewhere" } : { ready: true };
7990
+ }
7991
+ catch {
7992
+ return { ready: false, reason: "writer_registry_unavailable" };
7993
+ }
7994
+ }
7945
7995
  input.runtimeBridge?.connected?.({
7946
7996
  renderReturnReport: async (root, text, expectedReceipt, missionReview, reviewEvidenceGaps) => {
7947
7997
  let rendered;
@@ -8571,56 +8621,8 @@ export const SortieDogsPlugin = async (input, options) => {
8571
8621
  throw new Error("mission-activation-grant-stale");
8572
8622
  }
8573
8623
  }),
8574
- missionWorkerTerminal: async (root, terminal, writeScopes) => {
8575
- const childSessionID = terminal.childSessionID;
8576
- const snapshot = await (await goalLedger(root)).readGoal();
8577
- const origin = await input.runtimeBridge?.missionDispatchOrigin?.(root, terminal.taskID, terminal.callID);
8578
- const reservationID = goalFingerprint({ goal_id: snapshot.state.goal_id, unit_id: terminal.taskID, call_id: origin?.callID ?? terminal.callID });
8579
- if (snapshot.state.outstanding_reservations.some(item => item.reservation_id === reservationID) ||
8580
- !snapshot.records.some(({ event }) => event.kind === "unit.settled" && event.goal_id === snapshot.state.goal_id &&
8581
- event.unit_id === terminal.taskID && event.reservation_id === reservationID)) {
8582
- return { ready: false, reason: "exact_dispatch_settlement_missing" };
8583
- }
8584
- const active = activeSessions.get(childSessionID), authorization = sessionAuthorizations.get(childSessionID);
8585
- if (authorization && (authorization.rootSessionID !== root || authorization.taskID !== terminal.taskID ||
8586
- authorization.dispatchCallID !== terminal.callID)) {
8587
- return { ready: false, reason: "writer_owned_by_other_dispatch" };
8588
- }
8589
- if (active?.inFlightCalls.size) {
8590
- const response = await input.client?.session?.messages?.({ path: { id: childSessionID }, query: { directory: input.directory } }).catch(() => undefined);
8591
- const history = isRecord(response) && Array.isArray(response.data) ? response.data : undefined;
8592
- if (!history)
8593
- return { ready: false, reason: "tool_terminal_records_unavailable" };
8594
- for (const callID of active.inFlightCalls) {
8595
- const parts = history.flatMap(message => isRecord(message) && isRecord(message.info) &&
8596
- message.info.sessionID === childSessionID && Array.isArray(message.parts) ? message.parts : [])
8597
- .filter(part => isRecord(part) && part.type === "tool" && part.callID === callID);
8598
- if (parts.length !== 1 || !isRecord(parts[0]) || !isRecord(parts[0].state) ||
8599
- !["completed", "error"].includes(String(parts[0].state.status)))
8600
- return { ready: false, reason: "tool_dispatch_active_or_unproven" };
8601
- }
8602
- active.inFlightCalls.clear();
8603
- }
8604
- const released = JSON.parse(await releaseWriteGate(childSessionID));
8605
- if (released.status === "denied")
8606
- return { ready: false, reason: released.reason };
8607
- if (active)
8608
- active.released = true;
8609
- const scopeRoot = await durableScopeRoot(project?.root ?? input.directory);
8610
- if (scopeRoot === undefined)
8611
- return active?.parallel === "valid"
8612
- ? { ready: false, reason: "writer_registry_unavailable" } : { ready: true };
8613
- try {
8614
- const local = writeScopes.map(path => relative(project?.root ?? input.directory, resolve(project?.root ?? input.directory, normalizeManifestScope(path).path)).replaceAll("\\", "/"))
8615
- .filter(path => path !== ".." && !path.startsWith("../") && !isAbsolute(path));
8616
- const scope = normalizeWorktreeScope({ read: [], write: local });
8617
- return await new ScopeLeaseRegistry(scopeRoot).hasConflictingLease(scope)
8618
- ? { ready: false, reason: "writer_lease_owned_elsewhere" } : { ready: true };
8619
- }
8620
- catch {
8621
- return { ready: false, reason: "writer_registry_unavailable" };
8622
- }
8623
- },
8624
+ missionWorkerTerminal: (root, terminal, writeScopes) => missionDispatchWriterReleased(root, terminal.taskID, terminal.callID, terminal.childSessionID, writeScopes),
8625
+ missionDirectTerminal: (root, dispatch, writeScopes) => missionDispatchWriterReleased(root, dispatch.taskID, dispatch.callID, dispatch.actor, writeScopes),
8624
8626
  expandMissionWriteGate,
8625
8627
  registerGoalDeclaration: async (root, prompt, missionRevision) => {
8626
8628
  if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
@@ -940,7 +940,8 @@ export function createProfiledPlugin(profile, assetVersion) {
940
940
  : undefined;
941
941
  const attemptFailure = result.disposition === "cancelled"
942
942
  ? { category: "cancellation", code: "native-task-cancelled" }
943
- : result.resultClass === "acceptance" && result.failure?.outcome === "fail" && observed.outcome === "completed"
943
+ : result.resultClass === "acceptance" && result.failure?.outcome === "fail" &&
944
+ (observed.outcome === "completed" || !!unit.directExecution?.finishedAt)
944
945
  ? { category: "implementation",
945
946
  code: `validation-failed:${result.failure.exitCode ?? "unknown"}` }
946
947
  : result.resultClass === "process-defect"
@@ -1085,10 +1086,28 @@ export function createProfiledPlugin(profile, assetVersion) {
1085
1086
  unit.childSessionID !== attempt.childSessionID || /^task_id: (.+)$/m.exec(unit.task.prompt)?.[1] !== attempt.taskID) {
1086
1087
  return { status: "non_rescue", reason: "terminal_identity_conflict" };
1087
1088
  }
1088
- if (unit.directExecution)
1089
- return unit.directExecution.finishedAt && unit.status !== "running"
1090
- ? { status: "ready", child: unit.directExecution.actor }
1091
- : { status: "non_rescue", reason: "direct_unit_still_running" };
1089
+ if (unit.directExecution) {
1090
+ if (!unit.directExecution.finishedAt || unit.status === "running")
1091
+ return { status: "non_rescue", reason: "direct_unit_still_running" };
1092
+ if (unit.directExecution.actor !== attempt.childSessionID)
1093
+ return { status: "non_rescue", reason: "terminal_identity_conflict" };
1094
+ if (checkLiveOutcome) {
1095
+ try {
1096
+ await control.assertActiveGoal(root, await operators.completionGoalFingerprint(run));
1097
+ }
1098
+ catch {
1099
+ return { status: "non_rescue", reason: "goal_not_active" };
1100
+ }
1101
+ const budget = await control.currentBudget(root, { reconcileUsage: false });
1102
+ if (!budget || budget.reserved_units !== 0)
1103
+ return { status: "non_rescue", reason: "goal_reservation_unsettled" };
1104
+ const released = await control.missionDirectTerminal(root, { taskID: attempt.taskID, callID: attempt.callID,
1105
+ actor: unit.directExecution.actor }, unit.unit.write);
1106
+ if (!released.ready)
1107
+ return { status: "non_rescue", reason: released.reason ?? "writer_not_released" };
1108
+ }
1109
+ return { status: "ready", child: unit.directExecution.actor };
1110
+ }
1092
1111
  const observed = await observeWorkerTerminal(mission, run, attempt, !checkLiveOutcome);
1093
1112
  if (observed.status !== "ready")
1094
1113
  return observed;
@@ -1306,7 +1325,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1306
1325
  run.units[0]?.status === "failed" && run.units[0]?.resultClass === "acceptance" &&
1307
1326
  run.units[0]?.failure?.outcome === "fail" && !run.units[0]?.normalRemediationUsed) {
1308
1327
  return { ...packet, task: missions.task(mission),
1309
- next_action: `Fast-lane: declared validation failed. After the Worker returns, for the same scope and command ` +
1328
+ next_action: `Fast-lane: declared validation failed. After the ${run.units[0]?.directExecution ? "direct execution settles" : "Worker returns"}, for the same scope and command ` +
1310
1329
  `call ${retryMissionUnit} once, then ${startDirectUnit} to correct and formally validate here when you have the context; ` +
1311
1330
  `delegate its Worker only when useful. For a changed contract use ${planUnits} with executor=self, reason and one ` +
1312
1331
  `corrective unit. The Coordinator remains available for actual coordination. Keep cumulative budget and failed history.` };
@@ -2398,8 +2417,10 @@ export function createProfiledPlugin(profile, assetVersion) {
2398
2417
  const unit = run.units.find(item => item.directExecution?.actor === actor && item.status === "running");
2399
2418
  const taskID = /^task_id: (.+)$/mu.exec(unit.task.prompt)[1];
2400
2419
  await missions.update(root, mission => {
2420
+ const previous = [...(mission.attempts ?? [])].reverse().find(item => item.runID === run.runID && item.unitID === unit.unit.id);
2401
2421
  (mission.attempts ??= []).push({ attemptID: randomUUID(), runID: run.runID, unitID: unit.unit.id,
2402
- taskID, kind: "direct_execution", status: "dispatched", callID: unit.callID, childSessionID: actor });
2422
+ taskID, kind: "direct_execution", ...(unit.normalRemediationUsed ? { normalRemediation: true } : {}),
2423
+ ...(previous ? { predecessorAttemptID: previous.attemptID } : {}), status: "dispatched", callID: unit.callID, childSessionID: actor });
2403
2424
  });
2404
2425
  direct = await directExecution(actor);
2405
2426
  }
@@ -2416,7 +2437,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2416
2437
  throw new Error("mission-run-registration-incomplete: retry plan_units with the same plan to finish goal registration and Mission binding; no unit admitted");
2417
2438
  await operators.verifyContinuityControls(run);
2418
2439
  }
2419
- async function assertDirectPlanningQuiescent(actor, planningCallID) {
2440
+ async function assertDirectPlanningQuiescent(actor, planningCallID, planningTool = planUnits) {
2420
2441
  await control.assertDirectUnitQuiescent(actor);
2421
2442
  // Cold hook state is not proof. Only the exact current planner may be pending;
2422
2443
  // a completed background launch or another Task remains an active/unproven writer.
@@ -2433,7 +2454,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2433
2454
  throw new Error("mission-replan-terminal-unreconciled:tool_terminal_records_unavailable");
2434
2455
  if (part.type !== "tool")
2435
2456
  continue;
2436
- if (part.callID === planningCallID && planningCallID && part.tool === planUnits)
2457
+ if (part.callID === planningCallID && planningCallID && part.tool === planningTool)
2437
2458
  continue;
2438
2459
  if (typeof part.callID !== "string")
2439
2460
  throw new Error("mission-replan-terminal-unreconciled:tool_identity_unavailable");
@@ -2703,129 +2724,158 @@ export function createProfiledPlugin(profile, assetVersion) {
2703
2724
  return JSON.stringify({ status: "recorded", consultation: consultation.consultations?.at(-1) });
2704
2725
  } };
2705
2726
  const retryMissionUnit = `${profile.toolPrefix}retry_mission_unit`, rescueMissionUnit = `${profile.toolPrefix}rescue_mission_unit`;
2706
- tools[retryMissionUnit] = { description: "Owning Coordinator or Fast-lane Operator: after a host-classified Worker validation failure, prepare one same-scope remediation. Use start_direct_unit to correct and formally validate in this session, or dispatch the returned Worker when useful. Native child termination, released reservation/writer, and remaining cumulative unit budget are required; acceptance and scope remain intact.",
2727
+ tools[retryMissionUnit] = { description: "Owning Coordinator or Fast-lane Operator: after a host-classified direct or Worker validation failure, prepare one same-scope remediation. Use start_direct_unit to correct and formally validate in this session, or dispatch the returned Worker when useful. Exact settlement, a quiescent and released writer, and remaining cumulative unit budget are required; acceptance and scope remain intact.",
2707
2728
  args: { unit_id: stringSchema }, execute: async (args, context) => {
2708
- const { root, mission } = await missionAuthority(context.sessionID);
2709
- if (context.sessionID !== (mission.coordinator ?? root) ||
2710
- (await identity(context.sessionID)).role !== (mission.coordinator ? "dog-operator" : "dog-coordinator")) {
2711
- throw new Error("mission-remediation-coordinator-required");
2712
- }
2713
- const run = await operators.required(root), unitID = String(args.unit_id);
2714
- const unit = run.units.find(item => item.unit.id === unitID);
2715
- const attempt = [...(mission.attempts ?? [])].reverse().find(item => item.runID === run.runID && item.unitID === unitID);
2716
- if (!unit || unit.status !== "failed" || unit.resultClass !== "acceptance" || unit.failure?.outcome !== "fail" ||
2717
- unit.normalRemediationUsed || !attempt || attempt.kind !== "implementation" || attempt.status !== "failed" ||
2718
- attempt.failure?.category !== "implementation")
2719
- throw new Error("operator-mission-normal-remediation-unavailable");
2720
- const terminal = await missionWorkerTerminalProof(root, mission, run, attempt, true);
2721
- if (terminal.status !== "ready") {
2722
- const packet = missionPacket(mission, run);
2723
- return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason: terminal.reason });
2724
- }
2725
- const budget = await control.currentBudget(root);
2726
- if (!budget || budget.remaining_units < 1) {
2727
- const packet = missionPacket(mission, run);
2728
- return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason: "budget_exhausted", budget });
2729
- }
2730
- const prepared = await operators.prepareMissionNormalRemediation(root, run.runID, unitID);
2731
- control.enableUnits(root, 1);
2732
- const next = await operators.next(root, context.sessionID);
2733
- return JSON.stringify({ status: "normal_remediation_prepared", run_id: prepared.runID,
2734
- unit_id: unitID, task: record(next) ? next.task : undefined, budget: await control.currentBudget(root) });
2729
+ const { root } = await missionAuthority(context.sessionID);
2730
+ return serializeDispatchTransition(root, async () => {
2731
+ const { mission } = await missionAuthority(context.sessionID);
2732
+ if (context.sessionID !== (mission.coordinator ?? root) ||
2733
+ (await identity(context.sessionID)).role !== (mission.coordinator ? "dog-operator" : "dog-coordinator")) {
2734
+ throw new Error("mission-remediation-coordinator-required");
2735
+ }
2736
+ const run = await operators.required(root), unitID = String(args.unit_id);
2737
+ const unit = run.units.find(item => item.unit.id === unitID);
2738
+ const attempt = [...(mission.attempts ?? [])].reverse().find(item => item.runID === run.runID && item.unitID === unitID);
2739
+ const oldDirectUnknown = attempt?.kind === "direct_execution" && attempt.failure?.category === "unknown" &&
2740
+ attempt.failure.code === "acceptance" && typeof unit?.failure?.exitCode === "number" && unit.failure.exitCode !== 0 &&
2741
+ unit.directExecution?.checks.some(check => check.exitCode === unit.failure?.exitCode &&
2742
+ JSON.stringify(check.command) === JSON.stringify(unit.failure?.command));
2743
+ if (!unit || unit.status !== "failed" || unit.resultClass !== "acceptance" || unit.failure?.outcome !== "fail" ||
2744
+ unit.normalRemediationUsed || !attempt || !["implementation", "direct_execution"].includes(attempt.kind) || attempt.status !== "failed" ||
2745
+ (attempt.failure?.category !== "implementation" && !oldDirectUnknown))
2746
+ throw new Error("operator-mission-normal-remediation-unavailable");
2747
+ if (attempt.kind === "direct_execution") {
2748
+ try {
2749
+ await assertDirectPlanningQuiescent(context.sessionID, context.callID, retryMissionUnit);
2750
+ }
2751
+ catch (error) {
2752
+ const packet = missionPacket(mission, run);
2753
+ return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue",
2754
+ reason: error instanceof Error ? error.message : "direct_unit_tools_unreconciled" });
2755
+ }
2756
+ }
2757
+ const terminal = await missionWorkerTerminalProof(root, mission, run, attempt, true);
2758
+ if (terminal.status !== "ready") {
2759
+ const packet = missionPacket(mission, run);
2760
+ return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason: terminal.reason });
2761
+ }
2762
+ const budget = await control.currentBudget(root);
2763
+ if (!budget || budget.remaining_units < 1) {
2764
+ const packet = missionPacket(mission, run);
2765
+ return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason: "budget_exhausted", budget });
2766
+ }
2767
+ const prepared = await operators.prepareMissionNormalRemediation(root, run.runID, unitID);
2768
+ control.enableUnits(root, 1);
2769
+ const next = await operators.next(root, context.sessionID);
2770
+ return JSON.stringify({ status: "normal_remediation_prepared", run_id: prepared.runID,
2771
+ unit_id: unitID, task: record(next) ? next.task : undefined, budget: await control.currentBudget(root) });
2772
+ });
2735
2773
  } };
2736
2774
  tools[rescueMissionUnit] = { description: "Coordinator: only after the same unit's one ordinary remediation failed the same declared validation, evaluate one optional Astra Rescue. The host requires the current Mission failure class, reconciled native terminal, exact current candidate/acceptance/scope/validation, discovered model availability, one-use and cumulative budget; the returned Worker must still pass ordinary validation, independent review, and root acceptance. This is not the legacy sortie_execute_terminal_rescue tool.",
2737
2775
  args: { unit_id: stringSchema }, execute: async (args, context) => {
2738
- const { root, mission } = await missionAuthority(context.sessionID);
2739
- if (context.sessionID !== mission.coordinator || (await identity(context.sessionID)).role !== "dog-operator") {
2740
- throw new Error("mission-rescue-coordinator-required");
2741
- }
2742
- const run = await operators.required(root), unitID = String(args.unit_id);
2743
- const unit = run.units.find(item => item.unit.id === unitID);
2744
- const attempt = [...(mission.attempts ?? [])].reverse().find(item => item.runID === run.runID && item.unitID === unitID);
2745
- const nonRescue = async (reason) => {
2776
+ const { root } = await missionAuthority(context.sessionID);
2777
+ return serializeDispatchTransition(root, async () => {
2778
+ const { mission } = await missionAuthority(context.sessionID);
2779
+ if (context.sessionID !== mission.coordinator || (await identity(context.sessionID)).role !== "dog-operator") {
2780
+ throw new Error("mission-rescue-coordinator-required");
2781
+ }
2782
+ const run = await operators.required(root), unitID = String(args.unit_id);
2783
+ const unit = run.units.find(item => item.unit.id === unitID);
2784
+ const attempt = [...(mission.attempts ?? [])].reverse().find(item => item.runID === run.runID && item.unitID === unitID);
2785
+ const nonRescue = async (reason) => {
2786
+ await missions.update(root, state => {
2787
+ state.rescue = { attemptID: attempt?.attemptID ?? randomUUID(),
2788
+ runID: run.runID, unitID, status: "non_rescue", reason };
2789
+ });
2790
+ const packet = missionPacket(await missions.required(root), await operators.read(root));
2791
+ return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason });
2792
+ };
2793
+ if (!unit || unit.status !== "failed" || unit.resultClass !== "acceptance" || unit.failure?.outcome !== "fail" ||
2794
+ unit.normalRemediationUsed !== true || unit.terminalRescue || !attempt ||
2795
+ !(attempt.kind === "normal_remediation" || attempt.kind === "direct_execution" && attempt.normalRemediation === true) ||
2796
+ attempt.status !== "failed" || attempt.resultClass !== "acceptance" || attempt.failure?.category !== "implementation") {
2797
+ return nonRescue("normal_remediation_not_exhausted");
2798
+ }
2799
+ if (attempt.kind === "direct_execution") {
2800
+ try {
2801
+ await assertDirectPlanningQuiescent(context.sessionID, context.callID, rescueMissionUnit);
2802
+ }
2803
+ catch (error) {
2804
+ return nonRescue(error instanceof Error ? error.message : "direct_unit_tools_unreconciled");
2805
+ }
2806
+ }
2807
+ const terminal = await missionWorkerTerminalProof(root, mission, run, attempt, true);
2808
+ if (terminal.status !== "ready")
2809
+ return nonRescue(terminal.reason);
2810
+ const candidate = await missionReviewSource(input.directory, run, [], mission.reviewBaseline, mission.reviewScope);
2811
+ if (!attempt.candidateID || attempt.candidateID !== candidate.fingerprint)
2812
+ return nonRescue("accepted_candidate_changed");
2813
+ const budget = await control.currentBudget(root);
2814
+ if (!budget || budget.reserved_units !== 0)
2815
+ return nonRescue("budget_exhausted");
2816
+ const budgetDetails = budget;
2817
+ const hasTimeLimit = typeof budgetDetails.time_limit_ms === "number";
2818
+ const timeLimit = hasTimeLimit ? budgetDetails.time_limit_ms : Number.MAX_SAFE_INTEGER;
2819
+ const consumedTime = hasTimeLimit ? budgetDetails.settled_time_ms : 0;
2820
+ const hasCostLimit = typeof budgetDetails.cost_limit_usd === "number";
2821
+ const costLimit = hasCostLimit ? budgetDetails.cost_limit_usd : Number.MAX_VALUE;
2822
+ const consumedCost = hasCostLimit ? budgetDetails.settled_cost_usd : (budgetDetails.settled_cost_usd ?? 0);
2823
+ if (!Number.isSafeInteger(timeLimit) || timeLimit < 0 || !Number.isSafeInteger(consumedTime) || consumedTime < 0 ||
2824
+ !Number.isFinite(costLimit) || costLimit < 0 || typeof consumedCost !== "number" || !Number.isFinite(consumedCost) || consumedCost < 0) {
2825
+ return nonRescue("budget_exhausted");
2826
+ }
2827
+ if ((hasTimeLimit && consumedTime >= timeLimit) || (hasCostLimit && consumedCost >= costLimit)) {
2828
+ return nonRescue("budget_exhausted");
2829
+ }
2830
+ const nativeModels = await readHostModels(input.client);
2831
+ const target = nativeModels?.has(DEFAULT_TERMINAL_RESCUE_MODEL)
2832
+ ? { model: DEFAULT_TERMINAL_RESCUE_MODEL, variant: null } : null;
2833
+ const terminalIdentity = { run_id: run.runID, unit_id: unitID, attempt_id: attempt.attemptID,
2834
+ predecessor_attempt_id: attempt.predecessorAttemptID ?? null, candidate_id: candidate.fingerprint,
2835
+ route_id: `${profile.id}:mission:${unitID}`, child_id: terminal.child, call_id: attempt.callID };
2836
+ const priorFailure = attempt.failure;
2837
+ const policy = proposeTerminalRescue({ prior_attempt: { identity: terminalIdentity, role: "implementation", recovery_kind: "normal_remediation",
2838
+ disposition: "failed", failure: priorFailure ? { category: priorFailure.category, code: priorFailure.code } : null },
2839
+ terminal_reconciliation: { current: terminalIdentity, observation: { identity: terminalIdentity, disposition: "failed" }, evidence: {
2840
+ terminal: "satisfied", tools_quiescent: "satisfied", artifact_window_closed: "satisfied", writer_released: "satisfied",
2841
+ gate_released: "satisfied", lease_released: "satisfied", worktree_released: "satisfied"
2842
+ } },
2843
+ accepted_base: { candidate_id: candidate.fingerprint, contract_id: run.acceptanceFingerprint, scope: [...unit.unit.write],
2844
+ acceptance: [...run.acceptance], validation: [...unit.unit.validation] }, available_target: target,
2845
+ explicit_override: (attempt.selectedModel !== undefined && attempt.selectedModel !== PREVIEW_WORKER_ROUTE.model) ||
2846
+ (attempt.observedModel !== undefined && attempt.observedModel !== PREVIEW_WORKER_ROUTE.model) ||
2847
+ (attempt.observedVariant !== undefined && attempt.observedVariant !== PREVIEW_WORKER_ROUTE.variant),
2848
+ unit_rescue_count: (mission.attempts ?? []).filter(item => item.runID === run.runID && item.unitID === unitID && item.kind === "astra_rescue").length,
2849
+ active_run_rescue_count: (mission.attempts ?? []).filter(item => item.runID === run.runID && item.kind === "astra_rescue").length,
2850
+ budget_limits: { counters: { recovery_actions: budget.max_units, probe_iterations: 0, model_attempts: budget.max_units },
2851
+ resources: { time_ms: timeLimit, cost_usd: costLimit } },
2852
+ budget_consumed: { counters: { recovery_actions: budget.consumed_units + budget.reserved_units, probe_iterations: 0,
2853
+ model_attempts: budget.consumed_units + budget.reserved_units },
2854
+ resources: { time_ms: consumedTime, cost_usd: consumedCost } },
2855
+ budget_request: { counters: { recovery_actions: 1, probe_iterations: 0, model_attempts: 1 },
2856
+ // Current Mission charges native time/cost through its cumulative goal ledger at normal dispatch settlement;
2857
+ // this policy reservation consumes one existing unit, not an invented parallel resource allowance.
2858
+ resources: { time_ms: 0, cost_usd: 0 } } });
2859
+ if (policy.status !== "proposed")
2860
+ return nonRescue(policy.reason);
2861
+ const rescueAttemptID = randomUUID();
2862
+ const prepared = await operators.prepareMissionTerminalRescue(root, run.runID, unitID, {
2863
+ attempt_id: rescueAttemptID, selected_model: policy.proposal.target.model,
2864
+ selected_variant: policy.proposal.target.variant,
2865
+ accepted_base: { candidate_id: policy.proposal.accepted_base_candidate_id, contract_id: policy.proposal.contract_id,
2866
+ scope: policy.proposal.scope, acceptance: policy.proposal.acceptance, validation: policy.proposal.validation },
2867
+ });
2868
+ control.enableUnits(root, 1);
2746
2869
  await missions.update(root, state => {
2747
- state.rescue = { attemptID: attempt?.attemptID ?? randomUUID(),
2748
- runID: run.runID, unitID, status: "non_rescue", reason };
2870
+ state.rescue = { attemptID: rescueAttemptID, runID: run.runID, unitID,
2871
+ selectedModel: policy.proposal.target.model, selectedVariant: policy.proposal.target.variant, status: "prepared" };
2749
2872
  });
2750
- const packet = missionPacket(await missions.required(root), await operators.read(root));
2751
- return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason });
2752
- };
2753
- if (!unit || unit.status !== "failed" || unit.resultClass !== "acceptance" || unit.failure?.outcome !== "fail" ||
2754
- unit.normalRemediationUsed !== true || unit.terminalRescue || !attempt || attempt.kind !== "normal_remediation" ||
2755
- attempt.status !== "failed" || attempt.resultClass !== "acceptance" || attempt.failure?.category !== "implementation") {
2756
- return nonRescue("normal_remediation_not_exhausted");
2757
- }
2758
- const terminal = await missionWorkerTerminalProof(root, mission, run, attempt, true);
2759
- if (terminal.status !== "ready")
2760
- return nonRescue(terminal.reason);
2761
- const candidate = await missionReviewSource(input.directory, run, [], mission.reviewBaseline, mission.reviewScope);
2762
- if (!attempt.candidateID || attempt.candidateID !== candidate.fingerprint)
2763
- return nonRescue("accepted_candidate_changed");
2764
- const budget = await control.currentBudget(root);
2765
- if (!budget || budget.reserved_units !== 0)
2766
- return nonRescue("budget_exhausted");
2767
- const budgetDetails = budget;
2768
- const hasTimeLimit = typeof budgetDetails.time_limit_ms === "number";
2769
- const timeLimit = hasTimeLimit ? budgetDetails.time_limit_ms : Number.MAX_SAFE_INTEGER;
2770
- const consumedTime = hasTimeLimit ? budgetDetails.settled_time_ms : 0;
2771
- const hasCostLimit = typeof budgetDetails.cost_limit_usd === "number";
2772
- const costLimit = hasCostLimit ? budgetDetails.cost_limit_usd : Number.MAX_VALUE;
2773
- const consumedCost = hasCostLimit ? budgetDetails.settled_cost_usd : (budgetDetails.settled_cost_usd ?? 0);
2774
- if (!Number.isSafeInteger(timeLimit) || timeLimit < 0 || !Number.isSafeInteger(consumedTime) || consumedTime < 0 ||
2775
- !Number.isFinite(costLimit) || costLimit < 0 || typeof consumedCost !== "number" || !Number.isFinite(consumedCost) || consumedCost < 0) {
2776
- return nonRescue("budget_exhausted");
2777
- }
2778
- if ((hasTimeLimit && consumedTime >= timeLimit) || (hasCostLimit && consumedCost >= costLimit)) {
2779
- return nonRescue("budget_exhausted");
2780
- }
2781
- const nativeModels = await readHostModels(input.client);
2782
- const target = nativeModels?.has(DEFAULT_TERMINAL_RESCUE_MODEL)
2783
- ? { model: DEFAULT_TERMINAL_RESCUE_MODEL, variant: null } : null;
2784
- const terminalIdentity = { run_id: run.runID, unit_id: unitID, attempt_id: attempt.attemptID,
2785
- predecessor_attempt_id: attempt.predecessorAttemptID ?? null, candidate_id: candidate.fingerprint,
2786
- route_id: `${profile.id}:mission:${unitID}`, child_id: terminal.child, call_id: attempt.callID };
2787
- const priorFailure = attempt.failure;
2788
- const policy = proposeTerminalRescue({ prior_attempt: { identity: terminalIdentity, role: "implementation", recovery_kind: "normal_remediation",
2789
- disposition: "failed", failure: priorFailure ? { category: priorFailure.category, code: priorFailure.code } : null },
2790
- terminal_reconciliation: { current: terminalIdentity, observation: { identity: terminalIdentity, disposition: "failed" }, evidence: {
2791
- terminal: "satisfied", tools_quiescent: "satisfied", artifact_window_closed: "satisfied", writer_released: "satisfied",
2792
- gate_released: "satisfied", lease_released: "satisfied", worktree_released: "satisfied"
2793
- } },
2794
- accepted_base: { candidate_id: candidate.fingerprint, contract_id: run.acceptanceFingerprint, scope: [...unit.unit.write],
2795
- acceptance: [...run.acceptance], validation: [...unit.unit.validation] }, available_target: target,
2796
- explicit_override: (attempt.selectedModel !== undefined && attempt.selectedModel !== PREVIEW_WORKER_ROUTE.model) ||
2797
- (attempt.observedModel !== undefined && attempt.observedModel !== PREVIEW_WORKER_ROUTE.model) ||
2798
- (attempt.observedVariant !== undefined && attempt.observedVariant !== PREVIEW_WORKER_ROUTE.variant),
2799
- unit_rescue_count: (mission.attempts ?? []).filter(item => item.runID === run.runID && item.unitID === unitID && item.kind === "astra_rescue").length,
2800
- active_run_rescue_count: (mission.attempts ?? []).filter(item => item.runID === run.runID && item.kind === "astra_rescue").length,
2801
- budget_limits: { counters: { recovery_actions: budget.max_units, probe_iterations: 0, model_attempts: budget.max_units },
2802
- resources: { time_ms: timeLimit, cost_usd: costLimit } },
2803
- budget_consumed: { counters: { recovery_actions: budget.consumed_units + budget.reserved_units, probe_iterations: 0,
2804
- model_attempts: budget.consumed_units + budget.reserved_units },
2805
- resources: { time_ms: consumedTime, cost_usd: consumedCost } },
2806
- budget_request: { counters: { recovery_actions: 1, probe_iterations: 0, model_attempts: 1 },
2807
- // Current Mission charges native time/cost through its cumulative goal ledger at normal dispatch settlement;
2808
- // this policy reservation consumes one existing unit, not an invented parallel resource allowance.
2809
- resources: { time_ms: 0, cost_usd: 0 } } });
2810
- if (policy.status !== "proposed")
2811
- return nonRescue(policy.reason);
2812
- const rescueAttemptID = randomUUID();
2813
- const prepared = await operators.prepareMissionTerminalRescue(root, run.runID, unitID, {
2814
- attempt_id: rescueAttemptID, selected_model: policy.proposal.target.model,
2815
- selected_variant: policy.proposal.target.variant,
2816
- accepted_base: { candidate_id: policy.proposal.accepted_base_candidate_id, contract_id: policy.proposal.contract_id,
2817
- scope: policy.proposal.scope, acceptance: policy.proposal.acceptance, validation: policy.proposal.validation },
2818
- });
2819
- control.enableUnits(root, 1);
2820
- await missions.update(root, state => {
2821
- state.rescue = { attemptID: rescueAttemptID, runID: run.runID, unitID,
2822
- selectedModel: policy.proposal.target.model, selectedVariant: policy.proposal.target.variant, status: "prepared" };
2873
+ const next = await operators.next(root, context.sessionID);
2874
+ return JSON.stringify({ status: "prepared", rescue: { attempt_id: rescueAttemptID, selected_model: policy.proposal.target.model,
2875
+ selected_variant: policy.proposal.target.variant, accepted_base_candidate_id: policy.proposal.accepted_base_candidate_id,
2876
+ obligations: policy.proposal.obligations }, run_id: prepared.runID, unit_id: unitID,
2877
+ task: record(next) ? next.task : undefined, budget: await control.currentBudget(root) });
2823
2878
  });
2824
- const next = await operators.next(root, context.sessionID);
2825
- return JSON.stringify({ status: "prepared", rescue: { attempt_id: rescueAttemptID, selected_model: policy.proposal.target.model,
2826
- selected_variant: policy.proposal.target.variant, accepted_base_candidate_id: policy.proposal.accepted_base_candidate_id,
2827
- obligations: policy.proposal.obligations }, run_id: prepared.runID, unit_id: unitID,
2828
- task: record(next) ? next.task : undefined, budget: await control.currentBudget(root) });
2829
2879
  } };
2830
2880
  function declaredMissionExecution(mission, execution) {
2831
2881
  const operation = mission.execution;
@@ -239,6 +239,14 @@ export interface RuntimeBridge {
239
239
  ready: boolean;
240
240
  reason?: string;
241
241
  }>;
242
+ missionDirectTerminal(rootSessionID: string, dispatch: {
243
+ taskID: string;
244
+ callID: string;
245
+ actor: string;
246
+ }, writeScopes: readonly string[]): Promise<{
247
+ ready: boolean;
248
+ reason?: string;
249
+ }>;
242
250
  restoreReviewerCorrectionChild(rootSessionID: string, childSessionID: string, callID: string, taskID: string): Promise<void>;
243
251
  activateMissionWorker(rootSessionID: string, childSessionID: string, handoffPath: string, admission: MissionImplementationAdmission): Promise<void>;
244
252
  activateDirectUnit(rootSessionID: string, actorSessionID: string, prompt: string, handoffPath: string): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.16.4",
3
+ "version": "0.16.5",
4
4
  "description": "Adaptive execution harness for OpenCode and Codex: goal retention, validation, review, and evidence-backed handoff",
5
5
  "keywords": [
6
6
  "opencode",