sortie-dogs 0.12.19 → 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/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 +43 -12
- package/dist/plugin/mission-progress.js +4 -0
- package/dist/plugin/profiled.js +374 -16
- package/dist/plugin/v2.js +1 -1
- package/dist/runtime-mission-assets.js +28 -1
- 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";
|
|
@@ -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
|
@@ -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,
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -18,7 +18,10 @@ import { publishMissionProgress } from "./mission-progress.js";
|
|
|
18
18
|
import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionReviewBaseline, missionReviewSource } from "./mission-review.js";
|
|
19
19
|
import { missionLocations, missionLocationPacket } from "./mission-location.js";
|
|
20
20
|
import { SOURCE_REVIEW_RISK_TAGS } from "../core/consultation.js";
|
|
21
|
-
import { createHash } from "node:crypto";
|
|
21
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
22
|
+
import { proposeTerminalRescue } from "../core/terminal-rescue-policy.js";
|
|
23
|
+
import { DEFAULT_TERMINAL_RESCUE_MODEL } from "./terminal-rescue-host.js";
|
|
24
|
+
import { readHostModels } from "./model-routing-hook.js";
|
|
22
25
|
const SERIAL_CAPABILITIES = new Set([
|
|
23
26
|
"sortie_bind_write_gate", "sortie_release_write_gate", "sortie_check_contract",
|
|
24
27
|
"sortie_compact_and_continue", "sortie_enable_backlog_drain", "sortie_reflection",
|
|
@@ -84,6 +87,17 @@ export function previewModelCatalog(routes = PREVIEW_ROUTES, base = BUILT_IN_MOD
|
|
|
84
87
|
}
|
|
85
88
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
86
89
|
const payload = (value) => record(value) && "data" in value ? value.data : value;
|
|
90
|
+
function missionConsultationDetails(role, prompt) {
|
|
91
|
+
const marker = role === "advisor" ? /^strategy_trigger:\s*(\S+)\s*$/mu : /^missing_evidence_code:\s*(\S+)\s*$/mu;
|
|
92
|
+
const match = marker.exec(prompt);
|
|
93
|
+
const lines = prompt.split(/\r?\n/u);
|
|
94
|
+
const start = match === null ? 0 : lines.findIndex(line => marker.test(line)) + 1;
|
|
95
|
+
const question = lines.slice(Math.max(0, start)).map(line => line.trim()).find(line => line.length > 0 &&
|
|
96
|
+
!/^(?:project_root|known_paths|missing_evidence_code|strategy_trigger|role|task_id|acceptance|validation)\s*:/u.test(line));
|
|
97
|
+
const summary = (question ?? prompt.trim().split(/\r?\n/u).find(line => line.trim()) ?? "consultation requested").slice(0, 1000);
|
|
98
|
+
return { reason: summary, ...(question === undefined ? {} : { question: question.slice(0, 1000) }),
|
|
99
|
+
...(match === null ? {} : { trigger: match[1].slice(0, 128) }) };
|
|
100
|
+
}
|
|
87
101
|
/** Prove native V2 termination of the old dispatch, including earlier units and consultations. */
|
|
88
102
|
export async function terminalCancelledMissionChildren(profile, root, previous, budget, host) {
|
|
89
103
|
if (!budget || budget.reserved_units !== 0)
|
|
@@ -153,6 +167,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
153
167
|
const selected = new Map();
|
|
154
168
|
const operatorParents = new Map();
|
|
155
169
|
const taskOwners = new Map();
|
|
170
|
+
const missionRescueSelections = new Map();
|
|
156
171
|
const operatorTurnLifecycle = new Map();
|
|
157
172
|
const historicalTurnMessages = new Map();
|
|
158
173
|
const dispatchTransitions = new Map();
|
|
@@ -189,6 +204,19 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
189
204
|
const result = payload(await session(typeof nativeSession?.reviewMessages === "function" ? "reviewMessages" : "messages", { path: { id }, query: { directory: input.directory } }));
|
|
190
205
|
return Array.isArray(result) ? result.filter(record) : [];
|
|
191
206
|
}
|
|
207
|
+
async function observedSessionModel(id) {
|
|
208
|
+
const info = payload(await session("get", { path: { id }, query: { directory: input.directory } }).catch(() => undefined));
|
|
209
|
+
if (!record(info))
|
|
210
|
+
return { outcome: "unknown" };
|
|
211
|
+
const model = record(info.model) ? info.model : info;
|
|
212
|
+
const provider = typeof model.providerID === "string" ? model.providerID : info.providerID;
|
|
213
|
+
const modelID = typeof model.modelID === "string" ? model.modelID : typeof model.id === "string" ? model.id : info.modelID;
|
|
214
|
+
const variant = typeof model.variant === "string" ? model.variant : typeof info.variant === "string" ? info.variant : undefined;
|
|
215
|
+
const outcome = ["succeeded", "completed"].includes(String(info.outcome)) ? "completed"
|
|
216
|
+
: ["failed", "interrupted", "cancelled"].includes(String(info.outcome)) ? "failed" : "unknown";
|
|
217
|
+
return { ...(typeof provider === "string" && typeof modelID === "string" ? { model: `${provider}/${modelID}` } : {}),
|
|
218
|
+
...(variant === undefined ? {} : { variant }), outcome };
|
|
219
|
+
}
|
|
192
220
|
async function identity(id) {
|
|
193
221
|
const info = payload(await session("get", { path: { id }, query: { directory: input.directory } }));
|
|
194
222
|
const parent = record(info) && typeof info.parentID === "string" ? info.parentID : undefined;
|
|
@@ -466,8 +494,52 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
466
494
|
const unit = run.units.find(unit => unit.callID === result.callID);
|
|
467
495
|
if (!unit)
|
|
468
496
|
return;
|
|
497
|
+
const recordedAttempt = [...(mission.attempts ?? [])].reverse().find(attempt => attempt.callID === result.callID);
|
|
498
|
+
const observed = result.childSessionID ? await observedSessionModel(result.childSessionID) : { outcome: "unknown" };
|
|
499
|
+
const currentCandidate = result.resultClass === "acceptance" && result.failure?.outcome === "fail"
|
|
500
|
+
? await missionReviewSource(input.directory, run, [], mission.reviewBaseline, mission.reviewScope).then(value => value.fingerprint)
|
|
501
|
+
: undefined;
|
|
502
|
+
const attemptFailure = result.disposition === "cancelled"
|
|
503
|
+
? { category: "cancellation", code: "native-task-cancelled" }
|
|
504
|
+
: result.resultClass === "acceptance" && result.failure?.outcome === "fail" && observed.outcome === "completed"
|
|
505
|
+
? { category: "implementation",
|
|
506
|
+
code: `validation-failed:${result.failure.exitCode ?? "unknown"}` }
|
|
507
|
+
: result.resultClass === "process-defect"
|
|
508
|
+
? { category: "contract", code: result.resultClass }
|
|
509
|
+
: { category: "unknown", code: result.resultClass };
|
|
469
510
|
const progress = { unit: `${run.runID}/${unit.unit.id}`, title: unit.unit.title, status: unit.status, at: new Date().toISOString() };
|
|
470
|
-
await missions.update(result.rootSessionID, state => {
|
|
511
|
+
await missions.update(result.rootSessionID, state => {
|
|
512
|
+
state.progress.push(progress);
|
|
513
|
+
const attempt = recordedAttempt && state.attempts?.find(item => item.attemptID === recordedAttempt.attemptID);
|
|
514
|
+
if (attempt) {
|
|
515
|
+
attempt.status = result.disposition;
|
|
516
|
+
attempt.resultClass = result.resultClass;
|
|
517
|
+
attempt.nativeOutcome = observed.outcome;
|
|
518
|
+
if (result.childSessionID)
|
|
519
|
+
attempt.childSessionID = result.childSessionID;
|
|
520
|
+
if (observed.model)
|
|
521
|
+
attempt.observedModel = observed.model;
|
|
522
|
+
if (observed.variant)
|
|
523
|
+
attempt.observedVariant = observed.variant;
|
|
524
|
+
if (result.disposition !== "succeeded")
|
|
525
|
+
attempt.failure = attemptFailure;
|
|
526
|
+
else
|
|
527
|
+
delete attempt.failure;
|
|
528
|
+
if (currentCandidate)
|
|
529
|
+
attempt.candidateID = currentCandidate;
|
|
530
|
+
}
|
|
531
|
+
if (state.rescue && state.rescue.attemptID === unit.terminalRescue?.attempt_id) {
|
|
532
|
+
state.rescue.status = result.disposition === "succeeded" ? "succeeded"
|
|
533
|
+
: result.disposition === "cancelled" ? "cancelled" : "failed";
|
|
534
|
+
state.rescue.outcome = result.disposition;
|
|
535
|
+
if (observed.model)
|
|
536
|
+
state.rescue.observedModel = observed.model;
|
|
537
|
+
if (observed.variant)
|
|
538
|
+
state.rescue.observedVariant = observed.variant;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
if (result.childSessionID)
|
|
542
|
+
missionRescueSelections.delete(result.childSessionID);
|
|
471
543
|
await publishMissionProgress(result.rootSessionID, { description: `🐾 ${unit.status === "succeeded" ? "✅" : "🔧"} ${unit.unit.title}`,
|
|
472
544
|
sortie_progress: progress });
|
|
473
545
|
},
|
|
@@ -507,6 +579,12 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
507
579
|
next_action: "Use a known Coordinator's native location to resume. Absence here does not prove absence in other locations." },
|
|
508
580
|
} : {}) };
|
|
509
581
|
}
|
|
582
|
+
function sourceReconciliationRequired(mission, previous) {
|
|
583
|
+
return previous?.phase === "cancelled" && !["completed", "cancelled"].includes(mission.phase) &&
|
|
584
|
+
mission.runID === null && mission.supersededRunID === undefined &&
|
|
585
|
+
(previous.sourceRefs[0] !== `user:${mission.requests[0]?.id}` ||
|
|
586
|
+
!previous.sourceRefs.every(ref => mission.requests.some(request => ref === `user:${request.id}`)));
|
|
587
|
+
}
|
|
510
588
|
async function missionAuthority(id) {
|
|
511
589
|
const root = await rootFor(id);
|
|
512
590
|
if (!root)
|
|
@@ -516,11 +594,54 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
516
594
|
throw new Error("mission-controller-required");
|
|
517
595
|
return { root, mission };
|
|
518
596
|
}
|
|
597
|
+
async function missionWorkerTerminalProof(root, mission, run, attempt) {
|
|
598
|
+
if (!attempt.callID || !attempt.childSessionID || !mission.coordinator ||
|
|
599
|
+
!control) {
|
|
600
|
+
return { status: "non_rescue", reason: "terminal_not_reconciled" };
|
|
601
|
+
}
|
|
602
|
+
if (attempt.runID !== run.runID || attempt.unitID !== run.units.find(unit => unit.unit.id === attempt.unitID)?.unit.id) {
|
|
603
|
+
return { status: "non_rescue", reason: "terminal_identity_conflict" };
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
await control.assertActiveGoal(root, await operators.completionGoalFingerprint(run));
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return { status: "non_rescue", reason: "goal_not_active" };
|
|
610
|
+
}
|
|
611
|
+
const budget = await control.currentBudget(root);
|
|
612
|
+
if (!budget || budget.reserved_units !== 0)
|
|
613
|
+
return { status: "non_rescue", reason: "goal_reservation_unsettled" };
|
|
614
|
+
const child = attempt.childSessionID;
|
|
615
|
+
const who = await identity(child);
|
|
616
|
+
if (who.role !== "dog-worker" || who.parent !== mission.coordinator) {
|
|
617
|
+
return { status: "non_rescue", reason: "terminal_identity_conflict" };
|
|
618
|
+
}
|
|
619
|
+
const info = payload(await session("get", { path: { id: child }, query: { directory: input.directory } }).catch(() => undefined));
|
|
620
|
+
const outcome = record(info) ? String(info.outcome) : "unknown";
|
|
621
|
+
if (!record(info) || info.id !== child || info.parentID !== mission.coordinator ||
|
|
622
|
+
!["succeeded", "completed"].includes(outcome)) {
|
|
623
|
+
return { status: "non_rescue", reason: "terminal_not_reconciled" };
|
|
624
|
+
}
|
|
625
|
+
const descendants = payload(await session("children", { path: { id: child }, query: { directory: input.directory } }).catch(() => undefined));
|
|
626
|
+
if (!Array.isArray(descendants) || descendants.length !== 0) {
|
|
627
|
+
return { status: "non_rescue", reason: "terminal_not_reconciled" };
|
|
628
|
+
}
|
|
629
|
+
const terminalObserver = control?.missionWorkerTerminal;
|
|
630
|
+
if (!terminalObserver) {
|
|
631
|
+
return { status: "non_rescue", reason: "terminal_not_reconciled" };
|
|
632
|
+
}
|
|
633
|
+
if (!await terminalObserver(child, run.units.find(unit => unit.unit.id === attempt.unitID).unit.write)) {
|
|
634
|
+
return { status: "non_rescue", reason: "writer_not_released" };
|
|
635
|
+
}
|
|
636
|
+
return { status: "ready", child };
|
|
637
|
+
}
|
|
519
638
|
async function reconcileMissionDispatch(root) {
|
|
520
639
|
const mission = await missions.read(root);
|
|
521
|
-
if (!mission?.dispatchOpen || !mission.callID || ["cancelled", "completed"].includes(mission.phase)
|
|
522
|
-
taskOwners.has(mission.callID))
|
|
640
|
+
if (!mission?.dispatchOpen || !mission.callID || ["cancelled", "completed"].includes(mission.phase))
|
|
523
641
|
return mission;
|
|
642
|
+
// A native Task can be interrupted without an execute.after/event callback. Its in-memory
|
|
643
|
+
// owner then survives even though the host has already persisted a terminal tool part.
|
|
644
|
+
// The native record, not that cache, decides whether this exact dispatch can be resumed.
|
|
524
645
|
const history = await messages(root);
|
|
525
646
|
const finished = history.flatMap(message => Array.isArray(message.parts) ? message.parts : []).filter(part => record(part) && part.type === "tool" && part.tool === "task" && part.callID === mission.callID &&
|
|
526
647
|
record(part.state) && ["completed", "error"].includes(String(part.state.status)) &&
|
|
@@ -535,11 +656,20 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
535
656
|
if (who.parent !== root || who.role !== "dog-operator")
|
|
536
657
|
return mission;
|
|
537
658
|
}
|
|
538
|
-
|
|
659
|
+
const reconciled = await missions.reconcileFinishedDispatch(root, mission.id, mission.callID);
|
|
660
|
+
taskOwners.delete(mission.callID);
|
|
661
|
+
return reconciled;
|
|
539
662
|
}
|
|
540
663
|
function missionDispatchPacket(mission, run) {
|
|
541
664
|
const packet = { ...missionPacket(mission, run), project_root: input.directory,
|
|
542
665
|
coordinator_dispatch: mission.dispatchOpen ? "active" : ["completed", "cancelled"].includes(mission.phase) ? "terminal" : "resumable" };
|
|
666
|
+
if (sourceReconciliationRequired(mission, run))
|
|
667
|
+
return { ...packet, status: "mission-source-reconciliation-required",
|
|
668
|
+
next_action: mission.dispatchOpen ? "The Coordinator Task is still active. Do not redispatch; wait for its native completion and reconcile via operator_status."
|
|
669
|
+
: `The cancelled run's source_refs cannot prove this mission is the same goal. Do not dispatch or repeat plan_units. ` +
|
|
670
|
+
`If these saved requirements reflect the user's changed or narrowed scope, Operator: call ${profile.toolPrefix}start_mission ` +
|
|
671
|
+
`with intent=replace and the exact requirements array shown here. The host repairs this mission in place, retains spend, ` +
|
|
672
|
+
`and verifies old children before preparing a Worker. Otherwise obtain the user's scope decision.` };
|
|
543
673
|
if (!mission.dispatchOpen && (["open", "running"].includes(mission.phase) ||
|
|
544
674
|
(mission.phase === "submitted" && mission.submission?.status !== "ready"))) {
|
|
545
675
|
return { ...packet, task: missions.task(mission),
|
|
@@ -560,8 +690,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
560
690
|
}
|
|
561
691
|
// A different original user request needs the explicit, host-proven supersession path.
|
|
562
692
|
// Only the same source can retain a cancelled run's acceptance as a prefix.
|
|
563
|
-
if (previous
|
|
564
|
-
!previous.sourceRefs.every(ref => mission.requests.some(request => ref === `user:${request.id}`))) {
|
|
693
|
+
if (sourceReconciliationRequired(mission, previous)) {
|
|
565
694
|
throw new Error("mission-cancelled-source-unproven: the cancelled run's source_refs do not prove this is the same request. " +
|
|
566
695
|
"If the user changed or narrowed the goal, start_mission with intent=replace and the complete current requirements; " +
|
|
567
696
|
"otherwise preserve the earlier accepted criteria and establish the original source before continuing.");
|
|
@@ -838,6 +967,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
838
967
|
const activeMission = await reconcileMissionDispatch(root);
|
|
839
968
|
if (activeMission && !["cancelled", "completed"].includes(activeMission.phase) && activeMission.runID === null && context.sessionID !== root) {
|
|
840
969
|
const budget = await control.currentBudget(root);
|
|
970
|
+
const prior = await operators.read(root);
|
|
971
|
+
if (sourceReconciliationRequired(activeMission, prior))
|
|
972
|
+
return JSON.stringify({ ...missionDispatchPacket(activeMission, prior), budget });
|
|
841
973
|
return JSON.stringify({ ...missionDispatchPacket(activeMission, await operators.read(root)), budget,
|
|
842
974
|
next_action: "This mission has no unit plan yet. Call plan_units with the next useful work under the current requirements; prior run results are historical." });
|
|
843
975
|
}
|
|
@@ -1339,9 +1471,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1339
1471
|
return JSON.stringify({ ...proposalIdentity(committed), execution: JSON.parse(preparedTask(prepared.state)) });
|
|
1340
1472
|
});
|
|
1341
1473
|
} };
|
|
1342
|
-
const startMission = `${profile.toolPrefix}start_mission`, planUnits = `${profile.toolPrefix}plan_units`, reviewMission = `${profile.toolPrefix}review_mission`, submitMission = `${profile.toolPrefix}submit_mission`, completeMission = `${profile.toolPrefix}complete_mission`, expandUnit = `${profile.toolPrefix}expand_unit`;
|
|
1474
|
+
const startMission = `${profile.toolPrefix}start_mission`, planUnits = `${profile.toolPrefix}plan_units`, reviewMission = `${profile.toolPrefix}review_mission`, submitMission = `${profile.toolPrefix}submit_mission`, completeMission = `${profile.toolPrefix}complete_mission`, expandUnit = `${profile.toolPrefix}expand_unit`, skipMissionConsultation = `${profile.toolPrefix}skip_mission_consultation`;
|
|
1343
1475
|
const stringList = { type: "array", items: { type: "string" } };
|
|
1344
|
-
tools[startMission] = { description: "Operator: save the current user requirements. Use intent=replace when the user changes an existing request (version, parallelism, target): host cancels the previous run and archives its requirements/results while retaining spend. Supply the complete current requirements, retaining constraints the user has not changed. For separate work in a different location use intent=new. Dispatch the Coordinator immediately, or plan_units for a known single-unit procedure; item count and runtime alone do not require a Coordinator.",
|
|
1476
|
+
tools[startMission] = { description: "Operator: save the current user requirements. Use intent=replace when the user changes an existing request (version, parallelism, target): host cancels the previous run and archives its requirements/results while retaining spend. If status reports mission-source-reconciliation-required and the saved requirements reflect that changed scope, call intent=replace with those exact requirements: the host repairs this mission in place and keeps its Coordinator. Supply the complete current requirements, retaining constraints the user has not changed. For separate work in a different location use intent=new. Dispatch the Coordinator immediately, or plan_units for a known single-unit procedure; item count and runtime alone do not require a Coordinator.",
|
|
1345
1477
|
args: { requirements: { ...stringList, minItems: 1, maxItems: 64 },
|
|
1346
1478
|
kind: { type: "string", enum: ["implementation", "operation"], description: "Use operation for running an existing benchmark, command or procedure. The host records its actual execution separately from setup and checks.", "x-sortie-optional": true },
|
|
1347
1479
|
intent: { type: "string", enum: ["", "continue", "new", "replace"], "x-sortie-optional": true } }, execute: async (args, context) => {
|
|
@@ -1350,6 +1482,27 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1350
1482
|
throw new Error("mission-intent-invalid");
|
|
1351
1483
|
return serializeDispatchTransition(context.sessionID, async () => {
|
|
1352
1484
|
const existing = await reconcileMissionDispatch(context.sessionID);
|
|
1485
|
+
const prior = await operators.read(context.sessionID);
|
|
1486
|
+
const requirements = args.requirements;
|
|
1487
|
+
if (args.intent === "replace" && existing && !existing.dispatchOpen && existing.runID === null &&
|
|
1488
|
+
existing.phase !== "cancelled" && existing.phase !== "completed" &&
|
|
1489
|
+
(existing.phase !== "submitted" || existing.submission?.status === "blocked") &&
|
|
1490
|
+
sourceReconciliationRequired(existing, prior) && Array.isArray(requirements) &&
|
|
1491
|
+
requirements.length === existing.requirements.length &&
|
|
1492
|
+
existing.requirements.every((item, index) => item.text === requirements[index])) {
|
|
1493
|
+
const repaired = await missions.update(context.sessionID, state => {
|
|
1494
|
+
if (state.id !== existing.id || state.runID !== null || state.dispatchOpen)
|
|
1495
|
+
throw new Error("mission-source-reconciliation-stale");
|
|
1496
|
+
state.supersededRunID = prior.runID;
|
|
1497
|
+
state.requirementsReplaced = true;
|
|
1498
|
+
if (state.phase === "submitted") {
|
|
1499
|
+
state.phase = "running";
|
|
1500
|
+
state.submission = null;
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
return JSON.stringify({ ...locationObservation(context.sessionID), ...missionDispatchPacket(repaired, prior),
|
|
1504
|
+
budget: await control.currentBudget(context.sessionID) });
|
|
1505
|
+
}
|
|
1353
1506
|
if (args.intent === "replace" && existing && !["completed", "cancelled"].includes(existing.phase)) {
|
|
1354
1507
|
await stop(context.sessionID, "explicit-cancellation", false);
|
|
1355
1508
|
}
|
|
@@ -1358,12 +1511,16 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1358
1511
|
if (relocated)
|
|
1359
1512
|
return JSON.stringify({ ...relocated, budget: await control.currentBudget(context.sessionID) });
|
|
1360
1513
|
}
|
|
1361
|
-
const
|
|
1362
|
-
let mission = await
|
|
1514
|
+
const cancelled = await operators.read(context.sessionID);
|
|
1515
|
+
let mission = await missions.start(context.sessionID, requirements, args.intent === "replace" || args.intent === "new", {
|
|
1363
1516
|
kind: args.kind === "operation" ? "operation" : "implementation",
|
|
1364
1517
|
context: missionConversationContext(await messages(context.sessionID)),
|
|
1365
|
-
...(args.intent === "replace" &&
|
|
1366
|
-
})
|
|
1518
|
+
...(args.intent === "replace" && cancelled?.phase === "cancelled" ? { cancelledRunID: cancelled.runID } : {}),
|
|
1519
|
+
});
|
|
1520
|
+
if (sourceReconciliationRequired(mission, cancelled))
|
|
1521
|
+
return JSON.stringify({ ...locationObservation(context.sessionID),
|
|
1522
|
+
...missionDispatchPacket(mission, cancelled), budget: await control.currentBudget(context.sessionID) });
|
|
1523
|
+
mission = await retainCancelledMissionAcceptance(context.sessionID, mission, cancelled);
|
|
1367
1524
|
if (!mission.reviewBaseline) {
|
|
1368
1525
|
const baseline = await missionReviewBaseline(input.directory);
|
|
1369
1526
|
if (baseline)
|
|
@@ -1375,8 +1532,146 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1375
1532
|
next_action: "Nontrivial: dispatch task now. Simple single-unit work with known scope/check: call plan_units directly. Do not create a proposal or ask for plan approval." }) });
|
|
1376
1533
|
});
|
|
1377
1534
|
} };
|
|
1535
|
+
tools[skipMissionConsultation] = { description: "Coordinator: durably record why a concrete optional Advisor/Scout consultation is unnecessary. This is observation only: it adds no approval, consultation requirement, or dispatch gate.",
|
|
1536
|
+
args: { role: { type: "string", enum: ["advisor", "scout"] }, reason: stringSchema }, execute: async (args, context) => {
|
|
1537
|
+
const { root, mission } = await missionAuthority(context.sessionID);
|
|
1538
|
+
if (context.sessionID !== mission.coordinator || (await identity(context.sessionID)).role !== "dog-operator") {
|
|
1539
|
+
throw new Error("mission-consultation-coordinator-required");
|
|
1540
|
+
}
|
|
1541
|
+
const consultation = await missions.recordConsultationSkip(root, args.role, args.reason);
|
|
1542
|
+
return JSON.stringify({ status: "recorded", consultation: consultation.consultations?.at(-1) });
|
|
1543
|
+
} };
|
|
1544
|
+
const retryMissionUnit = `${profile.toolPrefix}retry_mission_unit`, rescueMissionUnit = `${profile.toolPrefix}rescue_mission_unit`;
|
|
1545
|
+
tools[retryMissionUnit] = { description: "Coordinator: after one host-classified implementation validation failure, dispatch exactly one same-scope ordinary Mission Worker remediation. Native child termination, released reservation/writer, and remaining cumulative unit budget are required; this does not change acceptance or scope.",
|
|
1546
|
+
args: { unit_id: stringSchema }, execute: async (args, context) => {
|
|
1547
|
+
const { root, mission } = await missionAuthority(context.sessionID);
|
|
1548
|
+
if (context.sessionID !== mission.coordinator || (await identity(context.sessionID)).role !== "dog-operator") {
|
|
1549
|
+
throw new Error("mission-remediation-coordinator-required");
|
|
1550
|
+
}
|
|
1551
|
+
const run = await operators.required(root), unitID = String(args.unit_id);
|
|
1552
|
+
const unit = run.units.find(item => item.unit.id === unitID);
|
|
1553
|
+
const attempt = [...(mission.attempts ?? [])].reverse().find(item => item.runID === run.runID && item.unitID === unitID);
|
|
1554
|
+
if (!unit || unit.status !== "failed" || unit.resultClass !== "acceptance" || unit.failure?.outcome !== "fail" ||
|
|
1555
|
+
unit.normalRemediationUsed || !attempt || attempt.kind !== "implementation" || attempt.status !== "failed" ||
|
|
1556
|
+
attempt.failure?.category !== "implementation")
|
|
1557
|
+
throw new Error("operator-mission-normal-remediation-unavailable");
|
|
1558
|
+
const terminal = await missionWorkerTerminalProof(root, mission, run, attempt);
|
|
1559
|
+
if (terminal.status !== "ready") {
|
|
1560
|
+
const packet = missionPacket(mission, run);
|
|
1561
|
+
return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason: terminal.reason });
|
|
1562
|
+
}
|
|
1563
|
+
const budget = await control.currentBudget(root);
|
|
1564
|
+
if (!budget || budget.remaining_units < 1) {
|
|
1565
|
+
const packet = missionPacket(mission, run);
|
|
1566
|
+
return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason: "budget_exhausted", budget });
|
|
1567
|
+
}
|
|
1568
|
+
const prepared = await operators.prepareMissionNormalRemediation(root, run.runID, unitID);
|
|
1569
|
+
control.enableUnits(root, 1);
|
|
1570
|
+
const next = await operators.next(root, context.sessionID);
|
|
1571
|
+
return JSON.stringify({ status: "normal_remediation_prepared", run_id: prepared.runID,
|
|
1572
|
+
unit_id: unitID, task: record(next) ? next.task : undefined, budget: await control.currentBudget(root) });
|
|
1573
|
+
} };
|
|
1574
|
+
tools[rescueMissionUnit] = { description: "Coordinator: only after the same unit's one ordinary remediation failed the same declared validation, evaluate one optional Astra Rescue. The host requires the current Mission failure class, reconciled native terminal, exact current candidate/acceptance/scope/validation, discovered model availability, one-use and cumulative budget; the returned Worker must still pass ordinary validation, independent review, and root acceptance. This is not the legacy sortie_execute_terminal_rescue tool.",
|
|
1575
|
+
args: { unit_id: stringSchema }, execute: async (args, context) => {
|
|
1576
|
+
const { root, mission } = await missionAuthority(context.sessionID);
|
|
1577
|
+
if (context.sessionID !== mission.coordinator || (await identity(context.sessionID)).role !== "dog-operator") {
|
|
1578
|
+
throw new Error("mission-rescue-coordinator-required");
|
|
1579
|
+
}
|
|
1580
|
+
const run = await operators.required(root), unitID = String(args.unit_id);
|
|
1581
|
+
const unit = run.units.find(item => item.unit.id === unitID);
|
|
1582
|
+
const attempt = [...(mission.attempts ?? [])].reverse().find(item => item.runID === run.runID && item.unitID === unitID);
|
|
1583
|
+
const nonRescue = async (reason) => {
|
|
1584
|
+
await missions.update(root, state => {
|
|
1585
|
+
state.rescue = { attemptID: attempt?.attemptID ?? randomUUID(),
|
|
1586
|
+
runID: run.runID, unitID, status: "non_rescue", reason };
|
|
1587
|
+
});
|
|
1588
|
+
const packet = missionPacket(await missions.required(root), await operators.read(root));
|
|
1589
|
+
return JSON.stringify({ ...packet, run_status: packet.status, status: "non_rescue", reason });
|
|
1590
|
+
};
|
|
1591
|
+
if (!unit || unit.status !== "failed" || unit.resultClass !== "acceptance" || unit.failure?.outcome !== "fail" ||
|
|
1592
|
+
unit.normalRemediationUsed !== true || unit.terminalRescue || !attempt || attempt.kind !== "normal_remediation" ||
|
|
1593
|
+
attempt.status !== "failed" || attempt.resultClass !== "acceptance" || attempt.failure?.category !== "implementation") {
|
|
1594
|
+
return nonRescue("normal_remediation_not_exhausted");
|
|
1595
|
+
}
|
|
1596
|
+
const terminal = await missionWorkerTerminalProof(root, mission, run, attempt);
|
|
1597
|
+
if (terminal.status !== "ready")
|
|
1598
|
+
return nonRescue(terminal.reason);
|
|
1599
|
+
const candidate = await missionReviewSource(input.directory, run, [], mission.reviewBaseline, mission.reviewScope);
|
|
1600
|
+
if (!attempt.candidateID || attempt.candidateID !== candidate.fingerprint)
|
|
1601
|
+
return nonRescue("accepted_candidate_changed");
|
|
1602
|
+
const budget = await control.currentBudget(root);
|
|
1603
|
+
if (!budget || budget.reserved_units !== 0)
|
|
1604
|
+
return nonRescue("budget_exhausted");
|
|
1605
|
+
const budgetDetails = budget;
|
|
1606
|
+
const hasTimeLimit = typeof budgetDetails.time_limit_ms === "number";
|
|
1607
|
+
const timeLimit = hasTimeLimit ? budgetDetails.time_limit_ms : Number.MAX_SAFE_INTEGER;
|
|
1608
|
+
const consumedTime = hasTimeLimit ? budgetDetails.settled_time_ms : 0;
|
|
1609
|
+
const hasCostLimit = typeof budgetDetails.cost_limit_usd === "number";
|
|
1610
|
+
const costLimit = hasCostLimit ? budgetDetails.cost_limit_usd : Number.MAX_VALUE;
|
|
1611
|
+
const consumedCost = hasCostLimit ? budgetDetails.settled_cost_usd : (budgetDetails.settled_cost_usd ?? 0);
|
|
1612
|
+
if (!Number.isSafeInteger(timeLimit) || timeLimit < 0 || !Number.isSafeInteger(consumedTime) || consumedTime < 0 ||
|
|
1613
|
+
!Number.isFinite(costLimit) || costLimit < 0 || typeof consumedCost !== "number" || !Number.isFinite(consumedCost) || consumedCost < 0) {
|
|
1614
|
+
return nonRescue("budget_exhausted");
|
|
1615
|
+
}
|
|
1616
|
+
if ((hasTimeLimit && consumedTime >= timeLimit) || (hasCostLimit && consumedCost >= costLimit)) {
|
|
1617
|
+
return nonRescue("budget_exhausted");
|
|
1618
|
+
}
|
|
1619
|
+
const nativeModels = await readHostModels(input.client);
|
|
1620
|
+
const target = nativeModels?.has(DEFAULT_TERMINAL_RESCUE_MODEL)
|
|
1621
|
+
? { model: DEFAULT_TERMINAL_RESCUE_MODEL, variant: null } : null;
|
|
1622
|
+
const terminalIdentity = { run_id: run.runID, unit_id: unitID, attempt_id: attempt.attemptID,
|
|
1623
|
+
predecessor_attempt_id: attempt.predecessorAttemptID ?? null, candidate_id: candidate.fingerprint,
|
|
1624
|
+
route_id: `${profile.id}:mission:${unitID}`, child_id: terminal.child, call_id: attempt.callID };
|
|
1625
|
+
const priorFailure = attempt.failure;
|
|
1626
|
+
const policy = proposeTerminalRescue({ prior_attempt: { identity: terminalIdentity, role: "implementation", recovery_kind: "normal_remediation",
|
|
1627
|
+
disposition: "failed", failure: priorFailure ? { category: priorFailure.category, code: priorFailure.code } : null },
|
|
1628
|
+
terminal_reconciliation: { current: terminalIdentity, observation: { identity: terminalIdentity, disposition: "failed" }, evidence: {
|
|
1629
|
+
terminal: "satisfied", tools_quiescent: "satisfied", artifact_window_closed: "satisfied", writer_released: "satisfied",
|
|
1630
|
+
gate_released: "satisfied", lease_released: "satisfied", worktree_released: "satisfied"
|
|
1631
|
+
} },
|
|
1632
|
+
accepted_base: { candidate_id: candidate.fingerprint, contract_id: run.acceptanceFingerprint, scope: [...unit.unit.write],
|
|
1633
|
+
acceptance: [...run.acceptance], validation: [...unit.unit.validation] }, available_target: target,
|
|
1634
|
+
explicit_override: (attempt.selectedModel !== undefined && attempt.selectedModel !== PREVIEW_WORKER_ROUTE.model) ||
|
|
1635
|
+
(attempt.observedModel !== undefined && attempt.observedModel !== PREVIEW_WORKER_ROUTE.model) ||
|
|
1636
|
+
(attempt.observedVariant !== undefined && attempt.observedVariant !== PREVIEW_WORKER_ROUTE.variant),
|
|
1637
|
+
unit_rescue_count: (mission.attempts ?? []).filter(item => item.runID === run.runID && item.unitID === unitID && item.kind === "astra_rescue").length,
|
|
1638
|
+
active_run_rescue_count: (mission.attempts ?? []).filter(item => item.runID === run.runID && item.kind === "astra_rescue").length,
|
|
1639
|
+
budget_limits: { counters: { recovery_actions: budget.max_units, probe_iterations: 0, model_attempts: budget.max_units },
|
|
1640
|
+
resources: { time_ms: timeLimit, cost_usd: costLimit } },
|
|
1641
|
+
budget_consumed: { counters: { recovery_actions: budget.consumed_units + budget.reserved_units, probe_iterations: 0,
|
|
1642
|
+
model_attempts: budget.consumed_units + budget.reserved_units },
|
|
1643
|
+
resources: { time_ms: consumedTime, cost_usd: consumedCost } },
|
|
1644
|
+
budget_request: { counters: { recovery_actions: 1, probe_iterations: 0, model_attempts: 1 },
|
|
1645
|
+
// Current Mission charges native time/cost through its cumulative goal ledger at normal dispatch settlement;
|
|
1646
|
+
// this policy reservation consumes one existing unit, not an invented parallel resource allowance.
|
|
1647
|
+
resources: { time_ms: 0, cost_usd: 0 } } });
|
|
1648
|
+
if (policy.status !== "proposed")
|
|
1649
|
+
return nonRescue(policy.reason);
|
|
1650
|
+
const rescueAttemptID = randomUUID();
|
|
1651
|
+
const prepared = await operators.prepareMissionTerminalRescue(root, run.runID, unitID, {
|
|
1652
|
+
attempt_id: rescueAttemptID, selected_model: policy.proposal.target.model,
|
|
1653
|
+
selected_variant: policy.proposal.target.variant,
|
|
1654
|
+
accepted_base: { candidate_id: policy.proposal.accepted_base_candidate_id, contract_id: policy.proposal.contract_id,
|
|
1655
|
+
scope: policy.proposal.scope, acceptance: policy.proposal.acceptance, validation: policy.proposal.validation },
|
|
1656
|
+
});
|
|
1657
|
+
control.enableUnits(root, 1);
|
|
1658
|
+
await missions.update(root, state => {
|
|
1659
|
+
state.rescue = { attemptID: rescueAttemptID, runID: run.runID, unitID,
|
|
1660
|
+
selectedModel: policy.proposal.target.model, selectedVariant: policy.proposal.target.variant, status: "prepared" };
|
|
1661
|
+
});
|
|
1662
|
+
const next = await operators.next(root, context.sessionID);
|
|
1663
|
+
return JSON.stringify({ status: "prepared", rescue: { attempt_id: rescueAttemptID, selected_model: policy.proposal.target.model,
|
|
1664
|
+
selected_variant: policy.proposal.target.variant, accepted_base_candidate_id: policy.proposal.accepted_base_candidate_id,
|
|
1665
|
+
obligations: policy.proposal.obligations }, run_id: prepared.runID, unit_id: unitID,
|
|
1666
|
+
task: record(next) ? next.task : undefined, budget: await control.currentBudget(root) });
|
|
1667
|
+
} };
|
|
1378
1668
|
async function declareMissionUnits(root, actor, mission, raw, reason, execution) {
|
|
1379
1669
|
const previous = await operators.read(root);
|
|
1670
|
+
if (sourceReconciliationRequired(mission, previous))
|
|
1671
|
+
return JSON.stringify({ ...missionDispatchPacket(mission, previous),
|
|
1672
|
+
next_action: `Coordinator: do not repeat plan_units. Call ${submitMission} with status=blocked and report the saved ` +
|
|
1673
|
+
`requirements to Operator. Operator can relink this mission in place via ${startMission} intent=replace ` +
|
|
1674
|
+
`when they match the user's changed scope; no Worker can start before that decision.` });
|
|
1380
1675
|
mission = await retainCancelledMissionAcceptance(root, mission, previous);
|
|
1381
1676
|
let operation = mission.execution;
|
|
1382
1677
|
// Models can serialize an optional operation field as an empty object for a normal edit.
|
|
@@ -1583,7 +1878,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1583
1878
|
await missions.update(context.sessionID, state => { state.phase = "completed"; });
|
|
1584
1879
|
return result;
|
|
1585
1880
|
} };
|
|
1586
|
-
const ownTools = new Set([startMission, planUnits, expandUnit, reviewMission, submitMission, completeMission,
|
|
1881
|
+
const ownTools = new Set([startMission, planUnits, expandUnit, reviewMission, submitMission, completeMission, skipMissionConsultation,
|
|
1882
|
+
retryMissionUnit, rescueMissionUnit,
|
|
1587
1883
|
prepare, repair, next, status, cancel, complete, resume, resolveContractRepair,
|
|
1588
1884
|
beginProposal, submitProposal, approveProposal, reviseProposal, extendProposalBudget, reopenProposalScope,
|
|
1589
1885
|
reconcileOrphan, reviseApprovedIntent]);
|
|
@@ -1798,17 +2094,36 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1798
2094
|
});
|
|
1799
2095
|
}
|
|
1800
2096
|
textParts[0].text = admitted.prompt;
|
|
2097
|
+
const current = await operators.required(root);
|
|
2098
|
+
const activeUnit = current.units.find(unit => unit.status === "running" && unit.childSessionID === chat.sessionID);
|
|
2099
|
+
if (activeUnit?.terminalRescue)
|
|
2100
|
+
missionRescueSelections.set(chat.sessionID, {
|
|
2101
|
+
attemptID: activeUnit.terminalRescue.attempt_id,
|
|
2102
|
+
model: activeUnit.terminalRescue.selected_model, variant: activeUnit.terminalRescue.selected_variant,
|
|
2103
|
+
});
|
|
1801
2104
|
}
|
|
1802
2105
|
}
|
|
1803
2106
|
const mapped = translate(output, false);
|
|
1804
|
-
|
|
2107
|
+
const mappedChat = translate(chat, false);
|
|
2108
|
+
const rescueSelection = role === "dog-worker" ? missionRescueSelections.get(chat.sessionID) : undefined;
|
|
2109
|
+
if (rescueSelection)
|
|
2110
|
+
mappedChat.missionRescueSelection = { attempt_id: rescueSelection.attemptID,
|
|
2111
|
+
model: rescueSelection.model, variant: rescueSelection.variant };
|
|
2112
|
+
await core["chat.message"]?.(mappedChat, mapped);
|
|
1805
2113
|
Object.assign(output, translate(mapped, true));
|
|
1806
2114
|
if (role === "dog-worker")
|
|
1807
2115
|
for (const part of output.parts) {
|
|
1808
2116
|
if (record(part) && part.type === "text" && typeof part.text === "string")
|
|
1809
2117
|
part.text = workerControlPrompt(part.text, true);
|
|
1810
2118
|
}
|
|
1811
|
-
if (
|
|
2119
|
+
if (rescueSelection) {
|
|
2120
|
+
const split = rescueSelection.model.indexOf("/");
|
|
2121
|
+
if (split <= 0)
|
|
2122
|
+
throw new Error("invalid-mission-terminal-rescue-model");
|
|
2123
|
+
output.message.model = { providerID: rescueSelection.model.slice(0, split), modelID: rescueSelection.model.slice(split + 1),
|
|
2124
|
+
...(rescueSelection.variant === null ? {} : { variant: rescueSelection.variant }) };
|
|
2125
|
+
}
|
|
2126
|
+
else if (role === "dog-worker" && explicitWorkerSelection) {
|
|
1812
2127
|
if (explicitWorkerSelection.model) {
|
|
1813
2128
|
const split = explicitWorkerSelection.model.indexOf("/");
|
|
1814
2129
|
if (split <= 0)
|
|
@@ -1907,6 +2222,11 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1907
2222
|
const mapped = translate(output, false);
|
|
1908
2223
|
await core["tool.execute.before"]?.({ ...request, sessionID: root, agent: "dog-coordinator" }, mapped);
|
|
1909
2224
|
Object.assign(output, translate(mapped, true));
|
|
2225
|
+
if ((consultRole === "dog-advisor" || consultRole === "dog-scout") && typeof args.prompt === "string") {
|
|
2226
|
+
const role = consultRole === "dog-advisor" ? "advisor" : "scout";
|
|
2227
|
+
await missions.recordConsultationDispatch(root, { role, callID: request.callID,
|
|
2228
|
+
...missionConsultationDetails(role, args.prompt) });
|
|
2229
|
+
}
|
|
1910
2230
|
taskOwners.set(request.callID, { root, actor: request.sessionID, operator: false, consultation: consultRole });
|
|
1911
2231
|
return;
|
|
1912
2232
|
}
|
|
@@ -1995,6 +2315,13 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1995
2315
|
throw new Error("operator-worker-task-required");
|
|
1996
2316
|
let expandedWorker;
|
|
1997
2317
|
if (delegated) {
|
|
2318
|
+
const pendingUnit = state.units.find(unit => unit.status !== "succeeded");
|
|
2319
|
+
if (pendingUnit?.terminalRescue) {
|
|
2320
|
+
const requiredModel = pendingUnit.terminalRescue.selected_model;
|
|
2321
|
+
if (args.model !== undefined && args.model !== requiredModel)
|
|
2322
|
+
throw new Error("operator-mission-terminal-rescue-model-mismatch");
|
|
2323
|
+
args.model = requiredModel;
|
|
2324
|
+
}
|
|
1998
2325
|
if (!mission || mission.runID !== state.runID)
|
|
1999
2326
|
await proposals.assertExecutionApproved(root, state.planHash);
|
|
2000
2327
|
if (!mission || mission.runID !== state.runID)
|
|
@@ -2080,6 +2407,27 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2080
2407
|
}
|
|
2081
2408
|
throw error;
|
|
2082
2409
|
}
|
|
2410
|
+
if (delegated && mission?.runID === state.runID) {
|
|
2411
|
+
const admitted = await operators.required(root);
|
|
2412
|
+
const unit = admitted.units.find(candidate => candidate.callID === request.callID);
|
|
2413
|
+
const taskID = unit && /^task_id: (.+)$/m.exec(unit.task.prompt)?.[1];
|
|
2414
|
+
if (unit && taskID) {
|
|
2415
|
+
const previousAttempt = [...(mission.attempts ?? [])].reverse().find(attempt => attempt.runID === admitted.runID && attempt.unitID === unit.unit.id);
|
|
2416
|
+
const kind = unit.terminalRescue ? "astra_rescue"
|
|
2417
|
+
: unit.normalRemediationUsed ? "normal_remediation" : "implementation";
|
|
2418
|
+
await missions.update(root, current => {
|
|
2419
|
+
current.attempts ??= [];
|
|
2420
|
+
if (current.attempts.some(attempt => attempt.callID === request.callID))
|
|
2421
|
+
return;
|
|
2422
|
+
current.attempts.push({ attemptID: unit.terminalRescue?.attempt_id ?? randomUUID(), runID: admitted.runID,
|
|
2423
|
+
unitID: unit.unit.id, taskID, ...(previousAttempt ? { predecessorAttemptID: previousAttempt.attemptID } : { predecessorAttemptID: null }),
|
|
2424
|
+
kind, status: "dispatched", callID: request.callID,
|
|
2425
|
+
...(unit.terminalRescue ? { selectedModel: unit.terminalRescue.selected_model } :
|
|
2426
|
+
typeof args.model === "string" ? { selectedModel: args.model } : {}) });
|
|
2427
|
+
current.attempts = current.attempts.slice(-64);
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2083
2431
|
},
|
|
2084
2432
|
"tool.execute.after": async (request, output) => {
|
|
2085
2433
|
const id = request.sessionID;
|
|
@@ -2124,6 +2472,16 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2124
2472
|
: `\n\nHOST: evidence-gap review ${count}/${MISSION_EVIDENCE_GAP_REVIEW_LIMIT}. Supply focused original-file excerpts through review_mission evidence and traces. Do not create an evidence-copying Worker or re-implement.`;
|
|
2125
2473
|
}
|
|
2126
2474
|
}
|
|
2475
|
+
else if (ownership.consultation === "dog-advisor" || ownership.consultation === "dog-scout") {
|
|
2476
|
+
const child = taskChildSessionID(output);
|
|
2477
|
+
const observed = child ? await observedSessionModel(child) : { outcome: "unknown" };
|
|
2478
|
+
await missions.settleConsultation(ownership.root, request.callID, observed.outcome, {
|
|
2479
|
+
...(child === undefined ? {} : { childSessionID: child }),
|
|
2480
|
+
...(observed.model === undefined ? {} : { observedModel: observed.model }),
|
|
2481
|
+
...(observed.variant === undefined ? {} : { observedVariant: observed.variant }),
|
|
2482
|
+
...(typeof raw === "string" ? { result: raw } : {}),
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2127
2485
|
taskOwners.delete(request.callID);
|
|
2128
2486
|
return;
|
|
2129
2487
|
}
|
package/dist/plugin/v2.js
CHANGED
|
@@ -492,7 +492,7 @@ async function registerV2Hooks(context, hooks) {
|
|
|
492
492
|
if (record(event.tools)) {
|
|
493
493
|
const visible = {
|
|
494
494
|
"dog-operator": ["start_mission", "plan_units", "operator_next", "operator_status", "expand_unit", "review_mission", "complete_mission", "cancel_operator", "reflection"],
|
|
495
|
-
"dogs-coordinator": ["plan_units", "operator_next", "operator_status", "expand_unit", "review_mission", "submit_mission"],
|
|
495
|
+
"dogs-coordinator": ["plan_units", "operator_next", "operator_status", "expand_unit", "review_mission", "submit_mission", "skip_mission_consultation", "retry_mission_unit", "rescue_mission_unit"],
|
|
496
496
|
"dog-worker-v010": ["bind_write_gate", "release_write_gate", "operator_status"],
|
|
497
497
|
"dog-luna-worker-v010": ["bind_write_gate", "release_write_gate", "operator_status"],
|
|
498
498
|
"dog-reviewer-v010": [], "dog-scout-v010": [], "dog-advisor-v010": [],
|
|
@@ -56,6 +56,13 @@ quality threshold and explicit model/budget choice. Follow AGENTS.md and use the
|
|
|
56
56
|
When the user replaces a version, target, parallelism or other requirement, call start_mission with
|
|
57
57
|
intent: "replace" and the complete current requirements. The host cancels/archives the old run and
|
|
58
58
|
retains spend/results; superseded instructions are history, not additional obligations.
|
|
59
|
+
If status/start_mission reports mission-source-reconciliation-required, do not dispatch its Task
|
|
60
|
+
or declare the user's work impossible. Compare the saved requirements with the user's current
|
|
61
|
+
scope. When they reflect an already requested narrowing/change, call start_mission with intent:
|
|
62
|
+
"replace" and the exact saved requirements. The host links the cancelled predecessor to the
|
|
63
|
+
SAME mission before any Worker starts, retains its Coordinator and cumulative spend, and checks
|
|
64
|
+
old children before preparing a Worker. This is not permission to discard unchanged acceptance:
|
|
65
|
+
if the scope is uncertain, ask the user which requirements remain instead of inferring a replacement.
|
|
59
66
|
2. Dispatch the returned ${profileAgent(profile, "dog-operator")} task immediately. It owns investigation,
|
|
60
67
|
unit boundaries, Worker/Scout/Advisor/independent Reviewer calls, write-scope extensions and corrections
|
|
61
68
|
within the original request and cumulative budget. Do not investigate or approve each unit at the root.
|
|
@@ -135,7 +142,7 @@ permission:
|
|
|
135
142
|
${profileAgent(profile, "dog-advisor")}: allow
|
|
136
143
|
tools:
|
|
137
144
|
"sortie_*": false
|
|
138
|
-
${controls(profile, ["plan_units", "operator_next", "operator_status", "expand_unit", "review_mission", "submit_mission"])}
|
|
145
|
+
${controls(profile, ["plan_units", "operator_next", "operator_status", "expand_unit", "review_mission", "submit_mission", "skip_mission_consultation", "retry_mission_unit", "rescue_mission_unit"])}
|
|
139
146
|
---
|
|
140
147
|
# ${profileAgent(profile, "dog-operator")}
|
|
141
148
|
|
|
@@ -190,6 +197,10 @@ ${profileAgent(profile, "dog-worker")} task verbatim, in foreground. V2 maps sub
|
|
|
190
197
|
task_id to sessionID. Do not insert model overrides unless the user explicitly selected them.
|
|
191
198
|
|
|
192
199
|
After each Worker returns, use its actual report and host evidence. Continue pending units with operator_next.
|
|
200
|
+
If plan_units returns mission-source-reconciliation-required, do not retry the same plan. The
|
|
201
|
+
root-only Operator must reconcile the prior cancelled run. Submit status=blocked with the saved
|
|
202
|
+
requirements and exact host diagnostic, then return; do not declare a user-only decision when
|
|
203
|
+
the current request already narrowed the old scope.
|
|
193
204
|
For a needed write-scope addition within the original request, call expand_unit with unit_id, paths and
|
|
194
205
|
reason; the host returns a replacement contract without Operator approval. For a changed approach, formal
|
|
195
206
|
check or reviewer finding, call plan_units with the corrected units and a short observed reason. Replanning
|
|
@@ -213,6 +224,22 @@ the header, correct and redispatch this same consultation once before proceeding
|
|
|
213
224
|
do not investigate runtime policy or bounce the question to Operator. Use your existing evidence
|
|
214
225
|
and ask a bounded question in the user's language; do not send generic exploratory delegations.
|
|
215
226
|
If the user explicitly requested Advisor input before a decision, do not treat it as optional.
|
|
227
|
+
Actual Advisor/Scout dispatches are recorded in operator_status with their trigger/code, bounded question,
|
|
228
|
+
observed model and native outcome. When you considered a concrete decision or missing fact but existing
|
|
229
|
+
evidence makes consultation unnecessary, record the role and concise skip reason with
|
|
230
|
+
${profile.toolPrefix}skip_mission_consultation. Record only meaningful considered skips, not a generic
|
|
231
|
+
"not needed" for each unit. This is observation only: it neither requires consultation nor adds approval.
|
|
232
|
+
|
|
233
|
+
After a Mission Worker returns a host-classified failed declared validation (not a Task launch error, contract
|
|
234
|
+
defect, cancellation, or unknown outcome), you may call ${profile.toolPrefix}retry_mission_unit once for that unit.
|
|
235
|
+
It reuses the exact scope, acceptance and validation under the ordinary cumulative budget. If that same normal
|
|
236
|
+
remediation then fails the same declared validation and native termination/writer release are confirmed, you may
|
|
237
|
+
call ${profile.toolPrefix}rescue_mission_unit once. The host records the Astra model actually selected, or a
|
|
238
|
+
specific non_rescue reason; do not expose or substitute the legacy sortie_execute_terminal_rescue capability.
|
|
239
|
+
Rescue is still a normal current-Mission Worker dispatch: its declared validation must pass, then the existing
|
|
240
|
+
independent review, final evidence check and root complete_mission acceptance remain mandatory. A Worker return
|
|
241
|
+
or rescue dispatch alone is never success. On non_rescue, continue the ordinary correction/replan within the same
|
|
242
|
+
requirements and remaining budget; never bypass a failure class or create another run to reset spend.
|
|
216
243
|
|
|
217
244
|
After formal validation, call review_mission with risk_tags and one concise implementation/test trace per
|
|
218
245
|
requirement. For changed failure behavior, connect the operation and concrete input to the contract-derived
|