sortie-dogs 0.12.19 → 0.12.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/operator-mission.d.ts +70 -0
- package/dist/core/operator-mission.js +45 -2
- package/dist/core/operator-runtime.d.ts +34 -2
- package/dist/core/operator-runtime.js +82 -0
- package/dist/plugin/index.js +64 -19
- package/dist/plugin/mission-progress.js +4 -0
- package/dist/plugin/profiled.js +400 -21
- package/dist/plugin/protected-snapshot.d.ts +3 -0
- package/dist/plugin/protected-snapshot.js +18 -0
- package/dist/plugin/v2.js +1 -1
- package/dist/runtime-mission-assets.js +39 -1
- package/package.json +1 -1
package/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.
|
|
@@ -1402,7 +1697,18 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1402
1697
|
if (!reason?.trim())
|
|
1403
1698
|
throw new Error("mission-replan-reason-required: name the observed correction or write-scope extension");
|
|
1404
1699
|
}
|
|
1405
|
-
|
|
1700
|
+
// A cancelled V2 delegate may leave its Worker Task running after the parent Task aborts.
|
|
1701
|
+
// Reconcile that exact native orphan before checking the cumulative budget or replacing
|
|
1702
|
+
// the run; an unproven orphan remains reserved and the normal terminal check still blocks.
|
|
1703
|
+
const cancelledPredecessor = previous?.phase === "cancelled" &&
|
|
1704
|
+
(mission.supersededRunID === previous.runID ||
|
|
1705
|
+
(mission.supersededRunID === undefined && ["explicit-cancellation", "agent-changed"].includes(previous.decision ?? "") &&
|
|
1706
|
+
previous.units.some(unit => (previous.decision === "agent-changed" || unit.status === "cancelled") && unit.childSessionID !== null)));
|
|
1707
|
+
let budget = await control.currentBudget(root);
|
|
1708
|
+
if (cancelledPredecessor && budget?.reserved_units) {
|
|
1709
|
+
await control.reconcileAbortedOperatorOrphan(root);
|
|
1710
|
+
budget = await control.currentBudget(root);
|
|
1711
|
+
}
|
|
1406
1712
|
if (budget && budget.remaining_units < plan.units.length && !same)
|
|
1407
1713
|
throw new Error(`mission-budget-exhausted: plan needs ${plan.units.length} units; ${budget.remaining_units} remain. ` +
|
|
1408
1714
|
"The current run is retained. Correct the plan within the remaining budget, or report a necessary cumulative extension to Operator.");
|
|
@@ -1413,10 +1719,6 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1413
1719
|
}
|
|
1414
1720
|
// Cancellation marks the durable units before interrupting their native sessions. A cancelled
|
|
1415
1721
|
// status alone therefore cannot prove the old Worker stopped or its reservation settled.
|
|
1416
|
-
const cancelledPredecessor = previous?.phase === "cancelled" &&
|
|
1417
|
-
(mission.supersededRunID === previous.runID ||
|
|
1418
|
-
(mission.supersededRunID === undefined && ["explicit-cancellation", "agent-changed"].includes(previous.decision ?? "") &&
|
|
1419
|
-
previous.units.some(unit => (previous.decision === "agent-changed" || unit.status === "cancelled") && unit.childSessionID !== null)));
|
|
1420
1722
|
const terminalChildren = cancelledPredecessor
|
|
1421
1723
|
? await terminalCancelledMissionChildren(profile, root, previous, budget, {
|
|
1422
1724
|
get: async (id) => {
|
|
@@ -1583,7 +1885,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1583
1885
|
await missions.update(context.sessionID, state => { state.phase = "completed"; });
|
|
1584
1886
|
return result;
|
|
1585
1887
|
} };
|
|
1586
|
-
const ownTools = new Set([startMission, planUnits, expandUnit, reviewMission, submitMission, completeMission,
|
|
1888
|
+
const ownTools = new Set([startMission, planUnits, expandUnit, reviewMission, submitMission, completeMission, skipMissionConsultation,
|
|
1889
|
+
retryMissionUnit, rescueMissionUnit,
|
|
1587
1890
|
prepare, repair, next, status, cancel, complete, resume, resolveContractRepair,
|
|
1588
1891
|
beginProposal, submitProposal, approveProposal, reviseProposal, extendProposalBudget, reopenProposalScope,
|
|
1589
1892
|
reconcileOrphan, reviseApprovedIntent]);
|
|
@@ -1798,17 +2101,36 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1798
2101
|
});
|
|
1799
2102
|
}
|
|
1800
2103
|
textParts[0].text = admitted.prompt;
|
|
2104
|
+
const current = await operators.required(root);
|
|
2105
|
+
const activeUnit = current.units.find(unit => unit.status === "running" && unit.childSessionID === chat.sessionID);
|
|
2106
|
+
if (activeUnit?.terminalRescue)
|
|
2107
|
+
missionRescueSelections.set(chat.sessionID, {
|
|
2108
|
+
attemptID: activeUnit.terminalRescue.attempt_id,
|
|
2109
|
+
model: activeUnit.terminalRescue.selected_model, variant: activeUnit.terminalRescue.selected_variant,
|
|
2110
|
+
});
|
|
1801
2111
|
}
|
|
1802
2112
|
}
|
|
1803
2113
|
const mapped = translate(output, false);
|
|
1804
|
-
|
|
2114
|
+
const mappedChat = translate(chat, false);
|
|
2115
|
+
const rescueSelection = role === "dog-worker" ? missionRescueSelections.get(chat.sessionID) : undefined;
|
|
2116
|
+
if (rescueSelection)
|
|
2117
|
+
mappedChat.missionRescueSelection = { attempt_id: rescueSelection.attemptID,
|
|
2118
|
+
model: rescueSelection.model, variant: rescueSelection.variant };
|
|
2119
|
+
await core["chat.message"]?.(mappedChat, mapped);
|
|
1805
2120
|
Object.assign(output, translate(mapped, true));
|
|
1806
2121
|
if (role === "dog-worker")
|
|
1807
2122
|
for (const part of output.parts) {
|
|
1808
2123
|
if (record(part) && part.type === "text" && typeof part.text === "string")
|
|
1809
2124
|
part.text = workerControlPrompt(part.text, true);
|
|
1810
2125
|
}
|
|
1811
|
-
if (
|
|
2126
|
+
if (rescueSelection) {
|
|
2127
|
+
const split = rescueSelection.model.indexOf("/");
|
|
2128
|
+
if (split <= 0)
|
|
2129
|
+
throw new Error("invalid-mission-terminal-rescue-model");
|
|
2130
|
+
output.message.model = { providerID: rescueSelection.model.slice(0, split), modelID: rescueSelection.model.slice(split + 1),
|
|
2131
|
+
...(rescueSelection.variant === null ? {} : { variant: rescueSelection.variant }) };
|
|
2132
|
+
}
|
|
2133
|
+
else if (role === "dog-worker" && explicitWorkerSelection) {
|
|
1812
2134
|
if (explicitWorkerSelection.model) {
|
|
1813
2135
|
const split = explicitWorkerSelection.model.indexOf("/");
|
|
1814
2136
|
if (split <= 0)
|
|
@@ -1907,6 +2229,11 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1907
2229
|
const mapped = translate(output, false);
|
|
1908
2230
|
await core["tool.execute.before"]?.({ ...request, sessionID: root, agent: "dog-coordinator" }, mapped);
|
|
1909
2231
|
Object.assign(output, translate(mapped, true));
|
|
2232
|
+
if ((consultRole === "dog-advisor" || consultRole === "dog-scout") && typeof args.prompt === "string") {
|
|
2233
|
+
const role = consultRole === "dog-advisor" ? "advisor" : "scout";
|
|
2234
|
+
await missions.recordConsultationDispatch(root, { role, callID: request.callID,
|
|
2235
|
+
...missionConsultationDetails(role, args.prompt) });
|
|
2236
|
+
}
|
|
1910
2237
|
taskOwners.set(request.callID, { root, actor: request.sessionID, operator: false, consultation: consultRole });
|
|
1911
2238
|
return;
|
|
1912
2239
|
}
|
|
@@ -1995,6 +2322,13 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1995
2322
|
throw new Error("operator-worker-task-required");
|
|
1996
2323
|
let expandedWorker;
|
|
1997
2324
|
if (delegated) {
|
|
2325
|
+
const pendingUnit = state.units.find(unit => unit.status !== "succeeded");
|
|
2326
|
+
if (pendingUnit?.terminalRescue) {
|
|
2327
|
+
const requiredModel = pendingUnit.terminalRescue.selected_model;
|
|
2328
|
+
if (args.model !== undefined && args.model !== requiredModel)
|
|
2329
|
+
throw new Error("operator-mission-terminal-rescue-model-mismatch");
|
|
2330
|
+
args.model = requiredModel;
|
|
2331
|
+
}
|
|
1998
2332
|
if (!mission || mission.runID !== state.runID)
|
|
1999
2333
|
await proposals.assertExecutionApproved(root, state.planHash);
|
|
2000
2334
|
if (!mission || mission.runID !== state.runID)
|
|
@@ -2023,6 +2357,20 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2023
2357
|
}
|
|
2024
2358
|
}
|
|
2025
2359
|
}
|
|
2360
|
+
const operationArgs = record(mapped.args) ? mapped.args : {};
|
|
2361
|
+
// A decorated launch would spend on the real operation but leave the native mission observation empty.
|
|
2362
|
+
// Ask the same Worker to correct its shell input before that expensive side effect.
|
|
2363
|
+
if (who.role === "dog-worker" && ["bash", "shell"].includes(request.tool.toLowerCase()) && mission?.execution &&
|
|
2364
|
+
typeof operationArgs.command === "string" &&
|
|
2365
|
+
resolve(input.directory, typeof operationArgs.workdir === "string" ? operationArgs.workdir : ".") === mission.execution.directory) {
|
|
2366
|
+
const actual = normalizeCommand(operationArgs.command);
|
|
2367
|
+
const decorated = mission.execution.commands.some(declared => actual.startsWith(declared) &&
|
|
2368
|
+
/^\s*(?:\||\d?>|&&?|;)/u.test(actual.slice(declared.length)));
|
|
2369
|
+
if (decorated)
|
|
2370
|
+
throw new Error("mission-operation-command-not-observed: run the declared operation command exactly; " +
|
|
2371
|
+
"do not append a pipe, tee, redirection or chained command. Capture its tool output or write a separate result file afterward. " +
|
|
2372
|
+
"No new plan or approval is needed; if the run already started, inspect its existing state instead of starting another one.");
|
|
2373
|
+
}
|
|
2026
2374
|
await core["tool.execute.before"]?.({ ...translate(request, false),
|
|
2027
2375
|
...(delegated ? { sessionID: root, agent: "dog-coordinator" } : {}) }, mapped);
|
|
2028
2376
|
if (who.role === "dog-worker") {
|
|
@@ -2080,6 +2428,27 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2080
2428
|
}
|
|
2081
2429
|
throw error;
|
|
2082
2430
|
}
|
|
2431
|
+
if (delegated && mission?.runID === state.runID) {
|
|
2432
|
+
const admitted = await operators.required(root);
|
|
2433
|
+
const unit = admitted.units.find(candidate => candidate.callID === request.callID);
|
|
2434
|
+
const taskID = unit && /^task_id: (.+)$/m.exec(unit.task.prompt)?.[1];
|
|
2435
|
+
if (unit && taskID) {
|
|
2436
|
+
const previousAttempt = [...(mission.attempts ?? [])].reverse().find(attempt => attempt.runID === admitted.runID && attempt.unitID === unit.unit.id);
|
|
2437
|
+
const kind = unit.terminalRescue ? "astra_rescue"
|
|
2438
|
+
: unit.normalRemediationUsed ? "normal_remediation" : "implementation";
|
|
2439
|
+
await missions.update(root, current => {
|
|
2440
|
+
current.attempts ??= [];
|
|
2441
|
+
if (current.attempts.some(attempt => attempt.callID === request.callID))
|
|
2442
|
+
return;
|
|
2443
|
+
current.attempts.push({ attemptID: unit.terminalRescue?.attempt_id ?? randomUUID(), runID: admitted.runID,
|
|
2444
|
+
unitID: unit.unit.id, taskID, ...(previousAttempt ? { predecessorAttemptID: previousAttempt.attemptID } : { predecessorAttemptID: null }),
|
|
2445
|
+
kind, status: "dispatched", callID: request.callID,
|
|
2446
|
+
...(unit.terminalRescue ? { selectedModel: unit.terminalRescue.selected_model } :
|
|
2447
|
+
typeof args.model === "string" ? { selectedModel: args.model } : {}) });
|
|
2448
|
+
current.attempts = current.attempts.slice(-64);
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2083
2452
|
},
|
|
2084
2453
|
"tool.execute.after": async (request, output) => {
|
|
2085
2454
|
const id = request.sessionID;
|
|
@@ -2124,6 +2493,16 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2124
2493
|
: `\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
2494
|
}
|
|
2126
2495
|
}
|
|
2496
|
+
else if (ownership.consultation === "dog-advisor" || ownership.consultation === "dog-scout") {
|
|
2497
|
+
const child = taskChildSessionID(output);
|
|
2498
|
+
const observed = child ? await observedSessionModel(child) : { outcome: "unknown" };
|
|
2499
|
+
await missions.settleConsultation(ownership.root, request.callID, observed.outcome, {
|
|
2500
|
+
...(child === undefined ? {} : { childSessionID: child }),
|
|
2501
|
+
...(observed.model === undefined ? {} : { observedModel: observed.model }),
|
|
2502
|
+
...(observed.variant === undefined ? {} : { observedVariant: observed.variant }),
|
|
2503
|
+
...(typeof raw === "string" ? { result: raw } : {}),
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2127
2506
|
taskOwners.delete(request.callID);
|
|
2128
2507
|
return;
|
|
2129
2508
|
}
|
|
@@ -5,6 +5,9 @@ export declare function isRuntimeControlPath(path: string): boolean;
|
|
|
5
5
|
/** An existing operation may create its declared outputs. Compare its other inputs during execution;
|
|
6
6
|
* ordinary evidence still pins the full post-operation source and outputs for later acceptance. */
|
|
7
7
|
export declare function operationInputSnapshot(projectRoot: string, binding: Binding): Promise<string | undefined>;
|
|
8
|
+
/** A validation may populate write-only caches. Keep its declared read inputs stable
|
|
9
|
+
* while binding the resulting candidate (including those outputs) after the command. */
|
|
10
|
+
export declare function validationInputSnapshot(projectRoot: string, binding: Binding): Promise<string | undefined>;
|
|
8
11
|
export declare function protectedSnapshot(authorization: {
|
|
9
12
|
manifestPath: string;
|
|
10
13
|
manifestHash: string;
|