sortie-dogs 0.12.18 → 0.12.20
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/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/contract-limits.d.ts +1 -1
- package/dist/core/contract-limits.js +1 -1
- package/dist/core/operator-mission.d.ts +71 -0
- package/dist/core/operator-mission.js +61 -11
- package/dist/core/operator-runtime.d.ts +34 -2
- package/dist/core/operator-runtime.js +82 -0
- package/dist/plugin/index.js +43 -12
- package/dist/plugin/mission-progress.js +4 -0
- package/dist/plugin/mission-review.js +72 -15
- package/dist/plugin/profiled.js +390 -18
- package/dist/plugin/v2.js +1 -1
- package/dist/runtime-assets-v010.js +4 -5
- package/dist/runtime-assets.d.ts +1 -1
- package/dist/runtime-assets.js +2 -2
- package/dist/runtime-mission-assets.d.ts +2 -0
- package/dist/runtime-mission-assets.js +58 -11
- package/package.json +1 -1
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.20-mission-recovery-rescue-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.20-mission-recovery-rescue-v1";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Common text bounds shared by handoff, manifest and goal evidence validation. */
|
|
2
|
-
export const CONTRACT_TEXT_LIMITS = Object.freeze({ title: 160, objective: 2000, statement: 1000, command:
|
|
2
|
+
export const CONTRACT_TEXT_LIMITS = Object.freeze({ title: 160, objective: 2000, statement: 1000, command: 8192, path: 512 });
|
|
@@ -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. */
|
|
@@ -130,8 +186,23 @@ export declare class OperatorMissionRuntime {
|
|
|
130
186
|
start(root: string, requirements: unknown, replaceRequirements?: boolean, options?: {
|
|
131
187
|
kind?: OperatorMission["kind"];
|
|
132
188
|
context?: MissionContext[];
|
|
189
|
+
cancelledRunID?: string;
|
|
133
190
|
}): Promise<OperatorMission>;
|
|
134
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>;
|
|
135
206
|
/** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
|
|
136
207
|
carryForward(root: string, missionID: string, acceptance: readonly string[], supersededRunID?: string): Promise<OperatorMission>;
|
|
137
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 {
|
|
@@ -240,16 +241,19 @@ export class OperatorMissionRuntime {
|
|
|
240
241
|
}
|
|
241
242
|
if (previous)
|
|
242
243
|
await this.save(this.file(root, `.${previous.id}`), previous);
|
|
244
|
+
// A cancelled standalone/legacy run has no mission-owned runID. Only an explicit
|
|
245
|
+
// replacement may link it; normal continuation must not silently inherit its goal.
|
|
246
|
+
const predecessor = previous?.runID ?? previous?.supersededRunID ??
|
|
247
|
+
(replaceRequirements ? options.cancelledRunID : undefined);
|
|
243
248
|
const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
|
|
244
249
|
kind: options.kind ?? "implementation",
|
|
245
250
|
context: (options.context ?? []).filter(item => item.id !== request.id),
|
|
246
251
|
requirements: requirements.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "open",
|
|
247
252
|
...(replaceRequirements ? { requirementsReplaced: true } : {}),
|
|
248
253
|
coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
|
|
249
|
-
...(previous
|
|
250
|
-
(previous
|
|
251
|
-
|
|
252
|
-
? { supersededRunID: (previous.runID ?? previous.supersededRunID) } : {}) };
|
|
254
|
+
...((previous === undefined || previous.phase === "cancelled") &&
|
|
255
|
+
(previous?.runID === null || previous === undefined || request.id !== previous.requests[0]?.id) && predecessor
|
|
256
|
+
? { supersededRunID: predecessor } : {}) };
|
|
253
257
|
await this.save(this.file(root), state);
|
|
254
258
|
return state;
|
|
255
259
|
});
|
|
@@ -264,6 +268,41 @@ export class OperatorMissionRuntime {
|
|
|
264
268
|
return state;
|
|
265
269
|
});
|
|
266
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
|
+
}
|
|
267
306
|
/** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
|
|
268
307
|
carryForward(root, missionID, acceptance, supersededRunID) {
|
|
269
308
|
return this.update(root, state => {
|
|
@@ -356,17 +395,20 @@ export function missionPlan(mission, raw) {
|
|
|
356
395
|
throw new Error(`mission-unit-${index + 1}: ${field} must be paths`);
|
|
357
396
|
return [...new Set(entries.map(item => normalizeExecutionScope(item)))];
|
|
358
397
|
};
|
|
359
|
-
//
|
|
360
|
-
const ids = value.requirement_ids ?? (mission.requirements.length === 1 ?
|
|
398
|
+
// A sole unit owns the whole request. This schedules work; it does not prove acceptance.
|
|
399
|
+
const ids = value.requirement_ids ?? (raw.length === 1 || mission.requirements.length === 1 ? mission.requirements.map(item => item.id) : []);
|
|
361
400
|
if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id => mission.requirements.some(item => item.id === id))) {
|
|
362
|
-
throw new Error(`mission-unit-${index + 1}: requirement_ids must name the related R IDs;
|
|
401
|
+
throw new Error(`mission-unit-${index + 1}: requirement_ids must name the related R IDs; splitting multiple requirements across units needs explicit coverage`);
|
|
363
402
|
}
|
|
364
403
|
if (!Array.isArray(value.validation) || value.validation.length === 0 ||
|
|
365
404
|
!value.validation.every(command => typeof command === "string" && command.trim())) {
|
|
366
405
|
throw new Error(`mission-unit-${index + 1}: validation must contain exact commands; final command proves the unit`);
|
|
367
406
|
}
|
|
407
|
+
const validation = value.validation.map(missionValidationCommand);
|
|
408
|
+
// Retain last occurrences so removing a redundant check preserves the final proof command.
|
|
368
409
|
return { id: `unit-${index + 1}`, title: line("title"), objective: line("objective"), read: paths("read"), write: paths("write"),
|
|
369
|
-
validation:
|
|
410
|
+
validation: validation.filter((command, i) => validation.lastIndexOf(command) === i),
|
|
411
|
+
acceptance_indices: [...new Set(ids.map(id => mission.requirements.findIndex(item => item.id === id)))] };
|
|
370
412
|
});
|
|
371
413
|
// The serial engine counts new proof milestones. Units sharing the same final suite are one
|
|
372
414
|
// milestone, so coalesce them instead of making the model repair a bookkeeping rejection.
|
|
@@ -412,7 +454,8 @@ export function missionPacket(mission, run) {
|
|
|
412
454
|
completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
|
|
413
455
|
note: "Historical results and spend are retained; they do not complete the current requirements." } } : {}),
|
|
414
456
|
requirements: mission.requirements, original_request_refs: mission.requests.map(item => `user:${item.id}`),
|
|
415
|
-
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 } : {}),
|
|
416
459
|
operation: { kind: mission.kind ?? "implementation", status: missionExecutionStatus(mission),
|
|
417
460
|
...(mission.execution ? { ...mission.execution } : {}) },
|
|
418
461
|
execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
|
|
@@ -432,13 +475,19 @@ export function missionPacket(mission, run) {
|
|
|
432
475
|
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence,
|
|
433
476
|
handoff_path: unit.handoffPath, operation_manifest_path: unit.manifestPath,
|
|
434
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 } } : {}),
|
|
435
480
|
...(unit.dispatchDenial ? { dispatch_denial: unit.dispatchDenial } : {}) })) } : {}),
|
|
436
481
|
next_action: mission.phase === "completed" ? "Mission completed. Report the accepted result and retained review gaps; no further dispatch or completion call is needed."
|
|
437
482
|
: mission.phase === "submitted" && mission.submission?.status === "ready"
|
|
438
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."
|
|
439
484
|
: run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
|
|
440
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."
|
|
441
|
-
:
|
|
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.")
|
|
442
491
|
: run?.phase === "awaiting-acceptance" && !operationComplete
|
|
443
492
|
? `Requested operation is ${operationStatus}. Continue the actual operation or report its blocker; auxiliary checks and review disposition cannot complete it.`
|
|
444
493
|
: run?.phase === "awaiting-acceptance" ? (currentReview && mission.review?.verdict === "evidence-gaps" && !reviewAccepted
|
|
@@ -462,7 +511,8 @@ export function missionReviewTraces(mission, raw) {
|
|
|
462
511
|
throw new Error("mission-review-traces: supply concise implementation/test traces");
|
|
463
512
|
}
|
|
464
513
|
const grouped = raw.map(text => ({ text,
|
|
465
|
-
ids:
|
|
514
|
+
ids: [...text.matchAll(/(?:^|[.;]\s+|[\r\n])\s*((?:R\d+\s*[/,]?\s*)+):/gu)]
|
|
515
|
+
.flatMap(match => match[1].match(/R\d+/gu) ?? []) }));
|
|
466
516
|
if (grouped.every(item => item.ids.length === 0) && raw.length === mission.requirements.length)
|
|
467
517
|
return raw;
|
|
468
518
|
const unknown = grouped.flatMap(item => item.ids).filter(id => !mission.requirements.some(requirement => requirement.id === id));
|
|
@@ -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
|
@@ -6259,14 +6259,24 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6259
6259
|
let terminalRescueTarget;
|
|
6260
6260
|
if (selectedAgent === SERIAL_WORKER_AGENT) {
|
|
6261
6261
|
const text = explicitTaskText(output);
|
|
6262
|
-
const
|
|
6262
|
+
const rescueAttemptID = text === undefined ? undefined : handoffValue(handoffEntries(text), ["terminal_rescue_attempt"]);
|
|
6263
|
+
const rescueRequested = rescueAttemptID !== undefined;
|
|
6264
|
+
const missionSelection = chatInput.missionRescueSelection;
|
|
6265
|
+
const missionRescueTarget = runtimeProfile.id === "v010" && rescueRequested && isRecord(missionSelection) &&
|
|
6266
|
+
missionSelection.attempt_id === rescueAttemptID && typeof missionSelection.model === "string" &&
|
|
6267
|
+
missionSelection.model.includes("/") &&
|
|
6268
|
+
(missionSelection.variant === null || typeof missionSelection.variant === "string")
|
|
6269
|
+
? { model: missionSelection.model,
|
|
6270
|
+
...(missionSelection.variant === null ? {} : { variant: missionSelection.variant }) }
|
|
6271
|
+
: undefined;
|
|
6263
6272
|
if (text !== undefined && !rescueRequested)
|
|
6264
6273
|
terminalRescueHandoffs.delete(chatInput.sessionID);
|
|
6265
6274
|
const suppliedPath = !rescueRequested ? undefined : handoffValue(handoffEntries(text), ["handoff_path"]);
|
|
6266
6275
|
const path = suppliedPath === undefined ? terminalRescueHandoffs.get(chatInput.sessionID) : unquoteValue(suppliedPath);
|
|
6267
6276
|
if (path !== undefined) {
|
|
6268
|
-
const identity = await inspect(path, undefined, { report: true,
|
|
6269
|
-
|
|
6277
|
+
const identity = await inspect(path, undefined, { report: true,
|
|
6278
|
+
...(missionRescueTarget === undefined ? { rescueSessionID: chatInput.sessionID } : {}) });
|
|
6279
|
+
terminalRescueTarget = missionRescueTarget ?? identity?.terminalRescueTarget;
|
|
6270
6280
|
if (rescueRequested && terminalRescueTarget === undefined)
|
|
6271
6281
|
throw new Error("rescue-binding-unavailable");
|
|
6272
6282
|
if (terminalRescueTarget !== undefined) {
|
|
@@ -6586,10 +6596,11 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6586
6596
|
diagnosis.accepting = false;
|
|
6587
6597
|
diagnosisCalls.delete(toolInput.callID ?? "");
|
|
6588
6598
|
}
|
|
6589
|
-
const returnedMissionWorker = completedChildSessionID !== undefined &&
|
|
6590
|
-
await input.runtimeBridge?.allowsInvestigativeShell?.(completedChildSessionID);
|
|
6591
6599
|
parallelCalls.delete(toolInput.callID ?? "");
|
|
6592
6600
|
activeSessions.get(toolInput.sessionID ?? "")?.inFlightCalls.delete(toolInput.callID ?? "");
|
|
6601
|
+
// Snapshot Mission ownership before settling a failed validation changes its durable phase to awaiting-decision.
|
|
6602
|
+
const returnedMissionWorker = completedChildSessionID !== undefined &&
|
|
6603
|
+
await input.runtimeBridge?.allowsInvestigativeShell?.(completedChildSessionID);
|
|
6593
6604
|
if (coordinatorTaskFinished && diagnosis === undefined) {
|
|
6594
6605
|
if (toolInput.callID !== undefined) {
|
|
6595
6606
|
await settleGoalDispatch(toolInput.callID, output).catch((error) => {
|
|
@@ -6603,16 +6614,16 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6603
6614
|
if (repair?.callID === toolInput.callID)
|
|
6604
6615
|
operatorContractRepairResumes.delete(toolInput.sessionID);
|
|
6605
6616
|
}
|
|
6617
|
+
if (completedChildSessionID !== undefined && activeSessions.get(completedChildSessionID)?.parallel !== "valid" &&
|
|
6618
|
+
returnedMissionWorker) {
|
|
6619
|
+
// Mission failures also leave the run awaiting-decision before this return hook completes. Release the
|
|
6620
|
+
// serial writer before settlement/correction can observe terminal ownership, then evict as usual.
|
|
6621
|
+
await releaseWriteGate(completedChildSessionID);
|
|
6622
|
+
}
|
|
6606
6623
|
if (completedChildSessionID !== undefined && !recoverableWorkerChildren.has(completedChildSessionID)) {
|
|
6607
6624
|
evictSession(completedChildSessionID);
|
|
6608
6625
|
void childLifecycles.get(completedChildSessionID)?.check();
|
|
6609
6626
|
}
|
|
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
6627
|
}
|
|
6617
6628
|
},
|
|
6618
6629
|
"tool.execute.before": async (toolInput, output) => {
|
|
@@ -7850,12 +7861,32 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
7850
7861
|
if (state.goal_id === null || state.budget === null)
|
|
7851
7862
|
return null;
|
|
7852
7863
|
const reserved = state.outstanding_reservations.length;
|
|
7853
|
-
|
|
7864
|
+
const snapshot = { max_units: state.budget.max_units, consumed_units: state.consumed_units,
|
|
7854
7865
|
reserved_units: reserved, remaining_units: Math.max(0, state.budget.max_units - state.consumed_units - reserved),
|
|
7866
|
+
settled_time_ms: state.consumed_time_ms, time_limit_ms: state.budget.time_ms,
|
|
7855
7867
|
settled_cost_usd: state.consumed_cost_usd, cost_limit_usd: state.budget.cost_usd,
|
|
7856
7868
|
cost_status: state.consumed_cost_usd === null ? "unknown-usage" : reserved > 0 ? "in-flight-not-final" : "settled",
|
|
7857
7869
|
cost_source: "native-usage-price-table",
|
|
7858
7870
|
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." };
|
|
7871
|
+
return snapshot;
|
|
7872
|
+
},
|
|
7873
|
+
...{
|
|
7874
|
+
missionWorkerTerminal: async (childSessionID, writeScopes) => {
|
|
7875
|
+
const active = activeSessions.get(childSessionID), authorization = sessionAuthorizations.get(childSessionID);
|
|
7876
|
+
if ((active?.inFlightCalls.size ?? 0) !== 0 || (active !== undefined && active.released !== true) ||
|
|
7877
|
+
(authorization !== undefined && (!authorization.suspended || authorization.lease !== undefined)))
|
|
7878
|
+
return false;
|
|
7879
|
+
const scopeRoot = await durableScopeRoot(project?.root ?? input.directory);
|
|
7880
|
+
if (scopeRoot === undefined)
|
|
7881
|
+
return false;
|
|
7882
|
+
try {
|
|
7883
|
+
const scope = normalizeWorktreeScope({ read: [], write: [...writeScopes] });
|
|
7884
|
+
return !await new ScopeLeaseRegistry(scopeRoot).hasConflictingLease(scope);
|
|
7885
|
+
}
|
|
7886
|
+
catch {
|
|
7887
|
+
return false;
|
|
7888
|
+
}
|
|
7889
|
+
},
|
|
7859
7890
|
},
|
|
7860
7891
|
registerGoalDeclaration: async (root, prompt, missionRevision) => {
|
|
7861
7892
|
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,
|