sortie-dogs 0.12.19 → 0.12.21

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
@@ -97,6 +97,8 @@ Official SWE-bench Lite `dev` results on the same 23 public instances:
97
97
  | v0.12.8 (`84ccdf1` main adapter) | 6 / 23 (26.1%) | 2 | Fresh 23-task run; four slots; 30-minute timeout | [Main-integrated run](docs/swebench-main-84ccdf1-dev23-2026-09-26.md) |
98
98
  | v0.12.15 (`0c9690d`) | 5 / 23 (21.7%) | 3 | Fresh 23-task ext4 retry; four slots; 40-minute timeout | [Campaign](docs/swebench-v01215-dev23-2026-09-27.md) |
99
99
  | v0.12.16 (`9b05a34` release; `b1a6c0e` runner) | 4 / 23 (17.4%) | 5 | Fresh 23-task run; eight slots; 40-minute timeout | [Campaign](docs/swebench-v01216-dev23-2026-09-27.md) |
100
+ | v0.12.19 (`24f5386` release; matched rerun) | 8 / 23 (34.8%) | 0 | Eight slots; effective $2/instance; 40-minute timeout; one inference timeout | [Official result and provenance](docs/benchmarks/swebench-v01220-operation-observability-2026-09-28.md) |
101
+ | v0.12.20 (`628eb81` release) | 7 / 23 (30.4%) | 0 | Eight slots; effective $2/instance; 40-minute timeout | [Official result and caveat](docs/benchmarks/swebench-v01220-operation-observability-2026-09-28.md) |
100
102
 
101
103
  Every row has 23 submitted official predictions; an empty patch counts against
102
104
  the score, not as a missing evaluation. The v0.10.14 report does not separately
@@ -110,6 +112,11 @@ estimated $15.75 in model cost and a 15.2-minute median agent runtime.
110
112
  The v0.12.16 run is the first eight-slot inference score in this table;
111
113
  five runners timed out, and this does not establish a model-quality regression
112
114
  against runs with different concurrency and conditions.
115
+ The v0.12.19 row is the corrected run with an effective $2 per-instance cap;
116
+ an earlier v0.12.19 run scored 7/23 but had no effective per-instance cap and
117
+ is not a same-condition comparison. The v0.12.20 run lost the
118
+ `sqlfluff__sqlfluff-2419` resolution relative to the corrected run; this
119
+ single run-to-run difference does not establish causation.
113
120
 
114
121
  Historical qualification references remain in [benchmark reference](docs/benchmark-reference.md).
115
122
 
@@ -3,5 +3,5 @@
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.12.19-focused-behavior-review-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.12.21-validation-recovery-v1";
7
7
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION;
@@ -3,4 +3,4 @@
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.12.19-focused-behavior-review-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.12.21-validation-recovery-v1";
@@ -20,6 +20,55 @@ export interface MissionReviewScope {
20
20
  read: string[];
21
21
  write: string[];
22
22
  }
23
+ export interface MissionConsultation {
24
+ id: string;
25
+ role: "advisor" | "scout";
26
+ disposition: "skipped" | "dispatched";
27
+ reason: string;
28
+ at: string;
29
+ trigger?: string;
30
+ question?: string;
31
+ callID?: string;
32
+ childSessionID?: string;
33
+ observedModel?: string;
34
+ observedVariant?: string;
35
+ outcome?: "completed" | "failed" | "unknown";
36
+ result?: string;
37
+ }
38
+ export interface MissionAttempt {
39
+ attemptID: string;
40
+ runID: string;
41
+ unitID: string;
42
+ taskID: string;
43
+ predecessorAttemptID?: string | null;
44
+ /** Fingerprint of the settled scoped candidate against which a later Rescue is proposed. */
45
+ candidateID?: string;
46
+ kind: "implementation" | "normal_remediation" | "astra_rescue";
47
+ status: "pending" | "dispatched" | "succeeded" | "failed" | "cancelled" | "unconfirmed";
48
+ callID?: string;
49
+ childSessionID?: string;
50
+ nativeOutcome?: "completed" | "failed" | "unknown";
51
+ observedModel?: string;
52
+ observedVariant?: string;
53
+ failure?: {
54
+ category: "infrastructure" | "authorization" | "contract" | "cancellation" | "implementation" | "unknown";
55
+ code: string;
56
+ };
57
+ resultClass?: string;
58
+ selectedModel?: string;
59
+ }
60
+ export interface MissionTerminalRescue {
61
+ attemptID: string;
62
+ runID: string;
63
+ unitID: string;
64
+ selectedModel?: string;
65
+ selectedVariant?: string | null;
66
+ status: "prepared" | "dispatched" | "succeeded" | "failed" | "cancelled" | "unconfirmed" | "non_rescue";
67
+ reason?: string;
68
+ observedModel?: string;
69
+ observedVariant?: string;
70
+ outcome?: "succeeded" | "failed" | "cancelled" | "unknown";
71
+ }
23
72
  export interface MissionExecution {
24
73
  commands: string[];
25
74
  directory: string;
@@ -61,6 +110,12 @@ export interface OperatorMission {
61
110
  reviewBaseline?: string;
62
111
  /** Cumulative declared review inputs/outputs, including units that failed after writing source. */
63
112
  reviewScope?: MissionReviewScope;
113
+ /** Optional Advisor/Scout decisions and native consultation outcomes; never an admission gate. */
114
+ consultations?: MissionConsultation[];
115
+ /** Host-observed current-Mission Worker attempts; only an implementation failure followed by a failed remediation can qualify for Rescue. */
116
+ attempts?: MissionAttempt[];
117
+ /** One bounded Astra Rescue per eligible unit; validation, independent review and root acceptance remain separate. */
118
+ rescue?: MissionTerminalRescue;
64
119
  /** A cancelled run replaced by a later real user turn; never reuse its acceptance or evidence. */
65
120
  supersededRunID?: string;
66
121
  plans: number;
@@ -95,6 +150,7 @@ export interface OperatorMission {
95
150
  * Worker unit, so after this many evidence-only reviews the candidate may be submitted with the gaps listed.
96
151
  */
97
152
  export declare const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
153
+ export declare const MISSION_CONSULTATION_LIMIT = 32;
98
154
  /** Review coverage survives a narrower replan; it is not a Worker write grant. */
99
155
  export declare function missionReviewScope(previous: MissionReviewScope | undefined, ...runs: OperatorState[]): MissionReviewScope;
100
156
  /** Classify an independent Reviewer's first line. Anything else is a finding. */
@@ -133,6 +189,20 @@ export declare class OperatorMissionRuntime {
133
189
  cancelledRunID?: string;
134
190
  }): Promise<OperatorMission>;
135
191
  update(root: string, change: (state: OperatorMission) => void): Promise<OperatorMission>;
192
+ recordConsultationSkip(root: string, role: MissionConsultation["role"], reason: string): Promise<OperatorMission>;
193
+ recordConsultationDispatch(root: string, input: {
194
+ role: MissionConsultation["role"];
195
+ callID: string;
196
+ reason: string;
197
+ trigger?: string;
198
+ question?: string;
199
+ }): Promise<OperatorMission>;
200
+ settleConsultation(root: string, callID: string, outcome: MissionConsultation["outcome"], details?: {
201
+ childSessionID?: string;
202
+ observedModel?: string;
203
+ observedVariant?: string;
204
+ result?: string;
205
+ }): Promise<OperatorMission>;
136
206
  /** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
137
207
  carryForward(root: string, missionID: string, acceptance: readonly string[], supersededRunID?: string): Promise<OperatorMission>;
138
208
  task(state: OperatorMission): OperatorTask;
@@ -23,6 +23,7 @@ export function missionValidationCommand(command) {
23
23
  * Worker unit, so after this many evidence-only reviews the candidate may be submitted with the gaps listed.
24
24
  */
25
25
  export const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
26
+ export const MISSION_CONSULTATION_LIMIT = 32;
26
27
  /** Review coverage survives a narrower replan; it is not a Worker write grant. */
27
28
  export function missionReviewScope(previous, ...runs) {
28
29
  return {
@@ -267,6 +268,41 @@ export class OperatorMissionRuntime {
267
268
  return state;
268
269
  });
269
270
  }
271
+ recordConsultationSkip(root, role, reason) {
272
+ if ((role !== "advisor" && role !== "scout") || typeof reason !== "string" || !reason.trim() ||
273
+ reason.length > 1000 || /[\r\n]/u.test(reason))
274
+ throw new Error("mission-consultation-input-invalid");
275
+ return this.update(root, state => {
276
+ state.consultations = [...(state.consultations ?? []), { id: randomUUID(), role, disposition: "skipped",
277
+ reason: reason.trim(), at: new Date().toISOString() }].slice(-MISSION_CONSULTATION_LIMIT);
278
+ });
279
+ }
280
+ recordConsultationDispatch(root, input) {
281
+ if ((input.role !== "advisor" && input.role !== "scout") || !input.callID || !input.reason.trim() ||
282
+ input.reason.length > 1000 || /[\r\n]/u.test(input.reason) ||
283
+ (input.trigger !== undefined && (input.trigger.length > 128 || /[\r\n]/u.test(input.trigger))) ||
284
+ (input.question !== undefined && (input.question.length > 1000 || /[\r\n]/u.test(input.question)))) {
285
+ throw new Error("mission-consultation-input-invalid");
286
+ }
287
+ return this.update(root, state => {
288
+ state.consultations = [...(state.consultations ?? []), { id: randomUUID(), role: input.role,
289
+ disposition: "dispatched", reason: input.reason.trim(), at: new Date().toISOString(), callID: input.callID,
290
+ ...(input.trigger === undefined ? {} : { trigger: input.trigger }),
291
+ ...(input.question === undefined ? {} : { question: input.question }) }].slice(-MISSION_CONSULTATION_LIMIT);
292
+ });
293
+ }
294
+ settleConsultation(root, callID, outcome, details = {}) {
295
+ if (!callID || !["completed", "failed", "unknown"].includes(String(outcome)))
296
+ throw new Error("mission-consultation-input-invalid");
297
+ return this.update(root, state => {
298
+ const item = [...(state.consultations ?? [])].reverse().find(entry => entry.callID === callID);
299
+ if (!item)
300
+ return;
301
+ if (details.result !== undefined && details.result.length > 4000)
302
+ details.result = details.result.slice(0, 4000);
303
+ Object.assign(item, { outcome, ...details });
304
+ });
305
+ }
270
306
  /** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
271
307
  carryForward(root, missionID, acceptance, supersededRunID) {
272
308
  return this.update(root, state => {
@@ -418,7 +454,8 @@ export function missionPacket(mission, run) {
418
454
  completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
419
455
  note: "Historical results and spend are retained; they do not complete the current requirements." } } : {}),
420
456
  requirements: mission.requirements, original_request_refs: mission.requests.map(item => `user:${item.id}`),
421
- submission: mission.submission, progress: mission.progress,
457
+ submission: mission.submission, progress: mission.progress, consultations: mission.consultations ?? [],
458
+ attempts: mission.attempts ?? [], ...(mission.rescue ? { rescue: mission.rescue } : {}),
422
459
  operation: { kind: mission.kind ?? "implementation", status: missionExecutionStatus(mission),
423
460
  ...(mission.execution ? { ...mission.execution } : {}) },
424
461
  execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
@@ -438,13 +475,19 @@ export function missionPacket(mission, run) {
438
475
  child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence,
439
476
  handoff_path: unit.handoffPath, operation_manifest_path: unit.manifestPath,
440
477
  scope_read: unit.unit.read, scope_write: unit.unit.write, validation: unit.unit.validation,
478
+ normal_remediation_used: unit.normalRemediationUsed ?? false,
479
+ ...(unit.failure ? { failure: { outcome: unit.failure.outcome, exit_code: unit.failure.exitCode } } : {}),
441
480
  ...(unit.dispatchDenial ? { dispatch_denial: unit.dispatchDenial } : {}) })) } : {}),
442
481
  next_action: mission.phase === "completed" ? "Mission completed. Report the accepted result and retained review gaps; no further dispatch or completion call is needed."
443
482
  : mission.phase === "submitted" && mission.submission?.status === "ready"
444
483
  ? "Operator: compare the submitted candidate with the original requirements and actual evidence, then complete_mission if satisfied. Report remaining evidence gaps; they are not a review PASS."
445
484
  : run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
446
485
  ? "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."
447
- : "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative.")
486
+ : run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && !unit.normalRemediationUsed)
487
+ ? "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."
488
+ : run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && unit.normalRemediationUsed && !unit.terminalRescue)
489
+ ? "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."
490
+ : "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative.")
448
491
  : run?.phase === "awaiting-acceptance" && !operationComplete
449
492
  ? `Requested operation is ${operationStatus}. Continue the actual operation or report its blocker; auxiliary checks and review disposition cannot complete it.`
450
493
  : run?.phase === "awaiting-acceptance" ? (currentReview && mission.review?.verdict === "evidence-gaps" && !reviewAccepted
@@ -106,15 +106,32 @@ export type OperatorProposal = {
106
106
  type OperatorPhase = "prepared" | "running" | "awaiting-decision" | "awaiting-acceptance" | "completed" | "cancelled";
107
107
  interface UnitState {
108
108
  readonly unit: OperatorUnit;
109
- readonly task: OperatorTask;
109
+ task: OperatorTask;
110
110
  readonly handoffPath: string;
111
111
  readonly manifestPath: string;
112
- hashes: readonly string[];
112
+ hashes: string[];
113
113
  status: "pending" | "running" | "succeeded" | "failed" | "cancelled";
114
114
  callID: string | null;
115
115
  childSessionID: string | null;
116
116
  evidence: readonly GoalEvidence[];
117
117
  resultClass: string | null;
118
+ failure?: SerialDispatchSettlement["failure"];
119
+ normalRemediationUsed?: boolean;
120
+ terminalRescue?: {
121
+ readonly attempt_id: string;
122
+ readonly selected_model: string;
123
+ readonly selected_variant: string | null;
124
+ readonly accepted_base: {
125
+ readonly candidate_id: string;
126
+ readonly contract_id: string;
127
+ readonly scope: readonly string[];
128
+ readonly acceptance: readonly string[];
129
+ readonly validation: readonly string[];
130
+ };
131
+ readonly prior_failure: SerialDispatchSettlement["failure"];
132
+ status: "prepared" | "dispatched" | "settled";
133
+ disposition?: "succeeded" | "failed" | "cancelled";
134
+ };
118
135
  dispatchDenial?: string;
119
136
  repairValidationAttempts: number;
120
137
  repairValidation: {
@@ -319,6 +336,21 @@ export declare class OperatorRuntime {
319
336
  private operatorRejectedOnce;
320
337
  settled(result: SerialDispatchSettlement): Promise<void>;
321
338
  private settledOnce;
339
+ /** One ordinary same-scope Mission retry before any optional model Rescue is considered. */
340
+ prepareMissionNormalRemediation(root: string, runID: string, unitID: string): Promise<OperatorState>;
341
+ /** Requeue a host-classified normal-remediation failure through the ordinary Mission Worker gates. */
342
+ prepareMissionTerminalRescue(root: string, runID: string, unitID: string, input: {
343
+ attempt_id: string;
344
+ selected_model: string;
345
+ selected_variant: string | null;
346
+ accepted_base: {
347
+ candidate_id: string;
348
+ contract_id: string;
349
+ scope: readonly string[];
350
+ acceptance: readonly string[];
351
+ validation: readonly string[];
352
+ };
353
+ }): Promise<OperatorState>;
322
354
  observeChild(root: string, callID: string, childID: string): Promise<void>;
323
355
  private observeChildOnce;
324
356
  operatorReturned(root: string): Promise<OperatorState>;
@@ -1237,6 +1237,8 @@ export class OperatorRuntime {
1237
1237
  state.dispatched++;
1238
1238
  unit.status = "running";
1239
1239
  unit.callID = callID;
1240
+ if (unit.terminalRescue)
1241
+ unit.terminalRescue.status = "dispatched";
1240
1242
  state.phase = "running";
1241
1243
  await this.save(state);
1242
1244
  return admitted;
@@ -1528,7 +1530,13 @@ export class OperatorRuntime {
1528
1530
  unit.resultClass = result.resultClass;
1529
1531
  if (result.failure !== undefined)
1530
1532
  unit.failure = result.failure;
1533
+ else
1534
+ delete unit.failure;
1531
1535
  unit.status = result.disposition;
1536
+ if (unit.terminalRescue) {
1537
+ unit.terminalRescue.status = "settled";
1538
+ unit.terminalRescue.disposition = result.disposition;
1539
+ }
1532
1540
  if (result.disposition !== "succeeded") {
1533
1541
  state.phase = "awaiting-decision";
1534
1542
  state.decision = result.resultClass === "acceptance" ? ACCEPTANCE_REMEDIATION_DECISION : result.resultClass;
@@ -1540,6 +1548,76 @@ export class OperatorRuntime {
1540
1548
  }
1541
1549
  await this.save(state);
1542
1550
  }
1551
+ /** One ordinary same-scope Mission retry before any optional model Rescue is considered. */
1552
+ prepareMissionNormalRemediation(root, runID, unitID) {
1553
+ return this.serial(root, async () => {
1554
+ const state = await this.required(root);
1555
+ const unit = state.units.find(candidate => candidate.unit.id === unitID);
1556
+ if (state.runID !== runID || state.phase !== "awaiting-decision" || state.units.some(candidate => candidate.status === "running") ||
1557
+ !unit || unit.status !== "failed" || unit.resultClass !== "acceptance" || !unit.failure ||
1558
+ unit.failure.outcome !== "fail" || unit.normalRemediationUsed || unit.terminalRescue ||
1559
+ state.units.some(candidate => candidate.terminalRescue))
1560
+ throw new Error("operator-mission-normal-remediation-unavailable");
1561
+ await this.verifyControls(unit);
1562
+ unit.normalRemediationUsed = true;
1563
+ unit.status = "pending";
1564
+ unit.callID = null;
1565
+ unit.childSessionID = null;
1566
+ unit.evidence = [];
1567
+ unit.resultClass = null;
1568
+ delete unit.failure;
1569
+ unit.repairValidation = null;
1570
+ state.phase = "prepared";
1571
+ state.decision = null;
1572
+ await this.save(state);
1573
+ return structuredClone(state);
1574
+ });
1575
+ }
1576
+ /** Requeue a host-classified normal-remediation failure through the ordinary Mission Worker gates. */
1577
+ prepareMissionTerminalRescue(root, runID, unitID, input) {
1578
+ return this.serial(root, async () => {
1579
+ const state = await this.required(root);
1580
+ const unit = state.units.find(candidate => candidate.unit.id === unitID);
1581
+ const base = input.accepted_base;
1582
+ if (state.runID !== runID || state.phase !== "awaiting-decision" || state.units.some(candidate => candidate.status === "running") ||
1583
+ !unit || unit.status !== "failed" || unit.resultClass !== "acceptance" || !unit.failure ||
1584
+ unit.failure.outcome !== "fail" || unit.normalRemediationUsed !== true || unit.terminalRescue ||
1585
+ state.units.some(candidate => candidate.terminalRescue) ||
1586
+ !identifier(input.attempt_id) || !text(input.selected_model) ||
1587
+ (input.selected_variant !== null && !text(input.selected_variant))) {
1588
+ throw new Error("operator-mission-terminal-rescue-unavailable");
1589
+ }
1590
+ if (!/^sha256:[0-9a-f]{64}$/u.test(base.candidate_id) ||
1591
+ !text(base.contract_id) || base.contract_id !== state.acceptanceFingerprint ||
1592
+ !strings(base.scope, true) || JSON.stringify(base.scope) !== JSON.stringify(unit.unit.write) ||
1593
+ !strings(base.acceptance, true) || base.acceptance.length !== state.acceptance.length ||
1594
+ base.acceptance.some((criterion, index) => criterion !== state.acceptance[index]) ||
1595
+ !strings(base.validation, true) || JSON.stringify(base.validation) !== JSON.stringify(unit.unit.validation)) {
1596
+ throw new Error("operator-mission-terminal-rescue-contract-mismatch");
1597
+ }
1598
+ const header = /^task_id: [^\r\n]+$/mu;
1599
+ if (!header.test(unit.task.prompt) || /^terminal_rescue_attempt:/mu.test(unit.task.prompt)) {
1600
+ throw new Error("operator-mission-terminal-rescue-task-invalid");
1601
+ }
1602
+ await this.verifyControls(unit);
1603
+ // The Rescue route is sealed in host-owned operator state and the returned Task reference.
1604
+ // Do not alter the registered handoff or expose the stable runtime's rescue capability.
1605
+ unit.task = { ...unit.task, prompt: unit.task.prompt.replace(header, line => `${line}\nterminal_rescue_attempt: ${input.attempt_id}`) };
1606
+ unit.terminalRescue = { attempt_id: input.attempt_id, selected_model: input.selected_model,
1607
+ selected_variant: input.selected_variant, accepted_base: structuredClone(base), prior_failure: structuredClone(unit.failure), status: "prepared" };
1608
+ unit.status = "pending";
1609
+ unit.callID = null;
1610
+ unit.childSessionID = null;
1611
+ unit.evidence = [];
1612
+ unit.resultClass = null;
1613
+ delete unit.failure;
1614
+ unit.repairValidation = null;
1615
+ state.phase = "prepared";
1616
+ state.decision = null;
1617
+ await this.save(state);
1618
+ return structuredClone(state);
1619
+ });
1620
+ }
1543
1621
  observeChild(root, callID, childID) {
1544
1622
  return this.serial(root, () => this.observeChildOnce(root, callID, childID));
1545
1623
  }
@@ -1910,6 +1988,10 @@ export class OperatorRuntime {
1910
1988
  handoff_path: unit.handoffPath, operation_manifest_path: unit.manifestPath,
1911
1989
  scope_read: unit.unit.read, scope_write: unit.unit.write, validation: unit.unit.validation,
1912
1990
  acceptance_indices: unit.unit.acceptance_indices,
1991
+ normal_remediation_used: unit.normalRemediationUsed ?? false,
1992
+ ...(unit.terminalRescue ? { terminal_rescue: { attempt_id: unit.terminalRescue.attempt_id,
1993
+ selected_model: unit.terminalRescue.selected_model, selected_variant: unit.terminalRescue.selected_variant,
1994
+ status: unit.terminalRescue.status, ...(unit.terminalRescue.disposition ? { disposition: unit.terminalRescue.disposition } : {}) } } : {}),
1913
1995
  evidence: unit.evidence.map(item => ({ id: item.evidence_id, criteria: item.measurement.criterion_ids,
1914
1996
  candidate: item.identity.candidate, command: item.execution.command, outcome: item.execution.outcome, exit_code: item.execution.exit_code })),
1915
1997
  repair_validation: unit.repairValidation === null ? null : { repair_fingerprint: unit.repairValidation.repair_fingerprint,
@@ -51,7 +51,7 @@ import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetri
51
51
  import { STABLE_RUNTIME_PROFILE } from "../core/runtime-profile.js";
52
52
  import { evidenceFromObservedExecution } from "../core/observed-goal-evidence.js";
53
53
  import { receiptBoundTerminalText } from "./receipt-presentation.js";
54
- import { operationInputSnapshot, protectedSnapshot, refreshProtectedSnapshot } from "./protected-snapshot.js";
54
+ import { operationInputSnapshot, protectedSnapshot, refreshProtectedSnapshot, validationInputSnapshot } from "./protected-snapshot.js";
55
55
  import { settledUnitUsage } from "./unit-usage.js";
56
56
  import { goalCompletionReadiness } from "./goal-completion.js";
57
57
  const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
@@ -1481,11 +1481,14 @@ export const SortieDogsPlugin = async (input, options) => {
1481
1481
  const operationInputs = mission?.kind === "operation" && mission.execution?.commands.includes(rawCommand) &&
1482
1482
  resolve(input.directory, typeof args?.workdir === "string" ? args.workdir : ".") === mission.execution.directory
1483
1483
  ? await operationInputSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
1484
+ const validationInputs = validation !== undefined && operationInputs === undefined
1485
+ ? await validationInputSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
1484
1486
  hostGoalExecutions.set(toolInput.callID, { root, projectRoot: authorization.projectRoot,
1485
1487
  sessionID: toolInput.sessionID,
1486
1488
  callID: toolInput.callID, tool: toolInput.tool, command: [rawCommand], startedAt: new Date().toISOString(),
1487
1489
  binding: snapshot.binding, source: snapshot.source, candidate: snapshot.candidate,
1488
1490
  ...(operationInputs === undefined ? {} : { operationInputs }),
1491
+ ...(validationInputs === undefined ? {} : { validationInputs }),
1489
1492
  owner: validation?.request.owner ?? "worker", validation,
1490
1493
  ...(reusedEvidence === undefined ? {} : { reusedEvidence }) });
1491
1494
  }
@@ -1513,11 +1516,13 @@ export const SortieDogsPlugin = async (input, options) => {
1513
1516
  ? new Date(timing.start).toISOString() : execution.startedAt;
1514
1517
  const endedAt = typeof timing?.end === "number" && Number.isFinite(timing.end)
1515
1518
  ? new Date(timing.end).toISOString() : new Date().toISOString();
1516
- // Existing operations produce declared outputs. Preserve their read-only inputs during execution,
1517
- // then bind ordinary evidence to all post-operation bytes, including those outputs.
1518
- const fresh = refreshed !== undefined && (execution.operationInputs === undefined
1519
- ? refreshed.source === execution.source
1520
- : await operationInputSnapshot(execution.projectRoot, execution.binding).catch(() => undefined) === execution.operationInputs);
1519
+ // Declared outputs can change during a successful validation. Read inputs must not;
1520
+ // the evidence still binds to the complete post-command candidate and source.
1521
+ const fresh = refreshed !== undefined && (execution.operationInputs !== undefined
1522
+ ? await operationInputSnapshot(execution.projectRoot, execution.binding).catch(() => undefined) === execution.operationInputs
1523
+ : execution.validationInputs !== undefined
1524
+ ? await validationInputSnapshot(execution.projectRoot, execution.binding).catch(() => undefined) === execution.validationInputs
1525
+ : refreshed.source === execution.source);
1521
1526
  const immutableRef = outcome === undefined || execution.reusedEvidence !== undefined ? undefined : goalFingerprint({ root: execution.root,
1522
1527
  child_session_id: execution.sessionID, call_id: execution.callID, command: execution.command,
1523
1528
  started_at: observedStartedAt, ended_at: endedAt, exit_code: exitCode ?? null, outcome,
@@ -6259,14 +6264,24 @@ export const SortieDogsPlugin = async (input, options) => {
6259
6264
  let terminalRescueTarget;
6260
6265
  if (selectedAgent === SERIAL_WORKER_AGENT) {
6261
6266
  const text = explicitTaskText(output);
6262
- const rescueRequested = text !== undefined && handoffValue(handoffEntries(text), ["terminal_rescue_attempt"]) !== undefined;
6267
+ const rescueAttemptID = text === undefined ? undefined : handoffValue(handoffEntries(text), ["terminal_rescue_attempt"]);
6268
+ const rescueRequested = rescueAttemptID !== undefined;
6269
+ const missionSelection = chatInput.missionRescueSelection;
6270
+ const missionRescueTarget = runtimeProfile.id === "v010" && rescueRequested && isRecord(missionSelection) &&
6271
+ missionSelection.attempt_id === rescueAttemptID && typeof missionSelection.model === "string" &&
6272
+ missionSelection.model.includes("/") &&
6273
+ (missionSelection.variant === null || typeof missionSelection.variant === "string")
6274
+ ? { model: missionSelection.model,
6275
+ ...(missionSelection.variant === null ? {} : { variant: missionSelection.variant }) }
6276
+ : undefined;
6263
6277
  if (text !== undefined && !rescueRequested)
6264
6278
  terminalRescueHandoffs.delete(chatInput.sessionID);
6265
6279
  const suppliedPath = !rescueRequested ? undefined : handoffValue(handoffEntries(text), ["handoff_path"]);
6266
6280
  const path = suppliedPath === undefined ? terminalRescueHandoffs.get(chatInput.sessionID) : unquoteValue(suppliedPath);
6267
6281
  if (path !== undefined) {
6268
- const identity = await inspect(path, undefined, { report: true, rescueSessionID: chatInput.sessionID });
6269
- terminalRescueTarget = identity?.terminalRescueTarget;
6282
+ const identity = await inspect(path, undefined, { report: true,
6283
+ ...(missionRescueTarget === undefined ? { rescueSessionID: chatInput.sessionID } : {}) });
6284
+ terminalRescueTarget = missionRescueTarget ?? identity?.terminalRescueTarget;
6270
6285
  if (rescueRequested && terminalRescueTarget === undefined)
6271
6286
  throw new Error("rescue-binding-unavailable");
6272
6287
  if (terminalRescueTarget !== undefined) {
@@ -6586,10 +6601,11 @@ export const SortieDogsPlugin = async (input, options) => {
6586
6601
  diagnosis.accepting = false;
6587
6602
  diagnosisCalls.delete(toolInput.callID ?? "");
6588
6603
  }
6589
- const returnedMissionWorker = completedChildSessionID !== undefined &&
6590
- await input.runtimeBridge?.allowsInvestigativeShell?.(completedChildSessionID);
6591
6604
  parallelCalls.delete(toolInput.callID ?? "");
6592
6605
  activeSessions.get(toolInput.sessionID ?? "")?.inFlightCalls.delete(toolInput.callID ?? "");
6606
+ // Snapshot Mission ownership before settling a failed validation changes its durable phase to awaiting-decision.
6607
+ const returnedMissionWorker = completedChildSessionID !== undefined &&
6608
+ await input.runtimeBridge?.allowsInvestigativeShell?.(completedChildSessionID);
6593
6609
  if (coordinatorTaskFinished && diagnosis === undefined) {
6594
6610
  if (toolInput.callID !== undefined) {
6595
6611
  await settleGoalDispatch(toolInput.callID, output).catch((error) => {
@@ -6603,16 +6619,16 @@ export const SortieDogsPlugin = async (input, options) => {
6603
6619
  if (repair?.callID === toolInput.callID)
6604
6620
  operatorContractRepairResumes.delete(toolInput.sessionID);
6605
6621
  }
6622
+ if (completedChildSessionID !== undefined && activeSessions.get(completedChildSessionID)?.parallel !== "valid" &&
6623
+ returnedMissionWorker) {
6624
+ // Mission failures also leave the run awaiting-decision before this return hook completes. Release the
6625
+ // serial writer before settlement/correction can observe terminal ownership, then evict as usual.
6626
+ await releaseWriteGate(completedChildSessionID);
6627
+ }
6606
6628
  if (completedChildSessionID !== undefined && !recoverableWorkerChildren.has(completedChildSessionID)) {
6607
6629
  evictSession(completedChildSessionID);
6608
6630
  void childLifecycles.get(completedChildSessionID)?.check();
6609
6631
  }
6610
- else if (completedChildSessionID !== undefined && activeSessions.get(completedChildSessionID)?.parallel !== "valid" &&
6611
- returnedMissionWorker) {
6612
- // Keep same-worker recovery identity, but a returned serial Task must not keep a live
6613
- // write reservation. Rebinding the retained manifest reacquires it before any later write.
6614
- await releaseWriteGate(completedChildSessionID);
6615
- }
6616
6632
  }
6617
6633
  },
6618
6634
  "tool.execute.before": async (toolInput, output) => {
@@ -7517,8 +7533,17 @@ export const SortieDogsPlugin = async (input, options) => {
7517
7533
  return { status: "unproven", reservation_id: reservation.reservation_id, reason: "owned-native-lineage-unavailable" };
7518
7534
  }
7519
7535
  const sessionAPI = input.client.session;
7536
+ // V2's session.context is bounded. The aborted parent Task can be days older than
7537
+ // that window; use the owning service's paginated history when the adapter offers it.
7538
+ const historyAPI = sessionAPI;
7539
+ const readHistory = historyAPI.reviewMessages ?? sessionAPI.messages;
7520
7540
  const messages = async (id) => {
7521
- const response = await sessionAPI.messages({ path: { id }, query: { directory: input.directory } });
7541
+ const request = { path: { id }, query: { directory: input.directory } };
7542
+ const response = await readHistory.call(sessionAPI, request).catch(error => {
7543
+ if (readHistory === sessionAPI.messages)
7544
+ throw error;
7545
+ return sessionAPI.messages(request);
7546
+ });
7522
7547
  return isRecord(response) && Array.isArray(response.data) ? response.data.filter(isRecord) : [];
7523
7548
  };
7524
7549
  const children = async (id) => {
@@ -7850,12 +7875,32 @@ export const SortieDogsPlugin = async (input, options) => {
7850
7875
  if (state.goal_id === null || state.budget === null)
7851
7876
  return null;
7852
7877
  const reserved = state.outstanding_reservations.length;
7853
- return { max_units: state.budget.max_units, consumed_units: state.consumed_units,
7878
+ const snapshot = { max_units: state.budget.max_units, consumed_units: state.consumed_units,
7854
7879
  reserved_units: reserved, remaining_units: Math.max(0, state.budget.max_units - state.consumed_units - reserved),
7880
+ settled_time_ms: state.consumed_time_ms, time_limit_ms: state.budget.time_ms,
7855
7881
  settled_cost_usd: state.consumed_cost_usd, cost_limit_usd: state.budget.cost_usd,
7856
7882
  cost_status: state.consumed_cost_usd === null ? "unknown-usage" : reserved > 0 ? "in-flight-not-final" : "settled",
7857
7883
  cost_source: "native-usage-price-table",
7858
7884
  cost_note: "Worker token-price estimates from complete native usage; missing requests remain unknown and are reconciled on later status. Excludes orchestration/review and external campaign spend. Zero settled cost does not mean free execution." };
7885
+ return snapshot;
7886
+ },
7887
+ ...{
7888
+ missionWorkerTerminal: async (childSessionID, writeScopes) => {
7889
+ const active = activeSessions.get(childSessionID), authorization = sessionAuthorizations.get(childSessionID);
7890
+ if ((active?.inFlightCalls.size ?? 0) !== 0 || (active !== undefined && active.released !== true) ||
7891
+ (authorization !== undefined && (!authorization.suspended || authorization.lease !== undefined)))
7892
+ return false;
7893
+ const scopeRoot = await durableScopeRoot(project?.root ?? input.directory);
7894
+ if (scopeRoot === undefined)
7895
+ return false;
7896
+ try {
7897
+ const scope = normalizeWorktreeScope({ read: [], write: [...writeScopes] });
7898
+ return !await new ScopeLeaseRegistry(scopeRoot).hasConflictingLease(scope);
7899
+ }
7900
+ catch {
7901
+ return false;
7902
+ }
7903
+ },
7859
7904
  },
7860
7905
  registerGoalDeclaration: async (root, prompt, missionRevision) => {
7861
7906
  if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
@@ -28,6 +28,10 @@ export function missionProgressReader(directory, root, callID) {
28
28
  return { description: `🐾 ${status}: ${title}`, sortie_progress: {
29
29
  mission_id: mission.id, phase: mission.phase, unit: running ? `${run.runID}/${running.unit.id}` : last?.unit ?? null,
30
30
  title, status, child_session_id: running?.childSessionID ?? null,
31
+ // Stable timestamps, not a fabricated heartbeat. The parent can tell when the
32
+ // run began and when a unit last settled without generating another model turn.
33
+ run_created_at: run?.runID === mission.runID ? run.createdAt : null,
34
+ last_transition_at: last?.at ?? null,
31
35
  completed_units: units.filter(unit => unit.status === "succeeded").length,
32
36
  failed_units: units.filter(unit => unit.status === "failed").length,
33
37
  historical_failed_attempts: mission.progress.filter(unit => unit.status === "failed").length,