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 +7 -0
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/operator-mission.d.ts +70 -0
- package/dist/core/operator-mission.js +45 -2
- package/dist/core/operator-runtime.d.ts +34 -2
- package/dist/core/operator-runtime.js +82 -0
- package/dist/plugin/index.js +64 -19
- package/dist/plugin/mission-progress.js +4 -0
- package/dist/plugin/profiled.js +400 -21
- package/dist/plugin/protected-snapshot.d.ts +3 -0
- package/dist/plugin/protected-snapshot.js +18 -0
- package/dist/plugin/v2.js +1 -1
- package/dist/runtime-mission-assets.js +39 -1
- package/package.json +1 -1
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
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -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.
|
|
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;
|
package/dist/asset-version.js
CHANGED
|
@@ -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.
|
|
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
|
-
:
|
|
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
|
-
|
|
109
|
+
task: OperatorTask;
|
|
110
110
|
readonly handoffPath: string;
|
|
111
111
|
readonly manifestPath: string;
|
|
112
|
-
hashes:
|
|
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,
|
package/dist/plugin/index.js
CHANGED
|
@@ -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
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
const fresh = refreshed !== undefined && (execution.operationInputs
|
|
1519
|
-
?
|
|
1520
|
-
:
|
|
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
|
|
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,
|
|
6269
|
-
|
|
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
|
|
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
|
-
|
|
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,
|