sortie-dogs 0.12.4 → 0.12.6
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/core/failure-swarm-runtime.d.ts +1 -1
- package/dist/core/operator-mission.d.ts +6 -0
- package/dist/core/operator-mission.js +61 -6
- package/dist/core/operator-runtime.d.ts +3 -1
- package/dist/core/operator-runtime.js +32 -8
- package/dist/plugin/index.js +104 -5
- package/dist/plugin/profiled.js +2 -1
- package/package.json +1 -1
|
@@ -67,8 +67,8 @@ export declare class FailureSwarmRuntime {
|
|
|
67
67
|
probe_iterations: number;
|
|
68
68
|
model_attempts: number;
|
|
69
69
|
}>;
|
|
70
|
-
capsule_id: string;
|
|
71
70
|
proposal: string;
|
|
71
|
+
capsule_id: string;
|
|
72
72
|
recovery_kind: Exclude<import("./run-flight-ledger.js").RecoveryKind, "read_only_diagnosis">;
|
|
73
73
|
diagnosis_id: string;
|
|
74
74
|
plan_id: string;
|
|
@@ -22,6 +22,8 @@ export interface OperatorMission {
|
|
|
22
22
|
callID: string | null;
|
|
23
23
|
dispatchOpen: boolean;
|
|
24
24
|
runID: string | null;
|
|
25
|
+
/** A cancelled run replaced by a later real user turn; never reuse its acceptance or evidence. */
|
|
26
|
+
supersededRunID?: string;
|
|
25
27
|
plans: number;
|
|
26
28
|
progress: {
|
|
27
29
|
unit: string;
|
|
@@ -51,6 +53,10 @@ export declare class OperatorMissionRuntime {
|
|
|
51
53
|
constructor(projectRoot: string, profile: RuntimeProfile);
|
|
52
54
|
private file;
|
|
53
55
|
private load;
|
|
56
|
+
/** Recover the predecessor link for missions started before this fix, using one exact archived mission. */
|
|
57
|
+
private loadMission;
|
|
58
|
+
/** Find exactly one archived cancelled mission that owned this run; ambiguity never grants recovery. */
|
|
59
|
+
archivedRun(root: string, runID: string): Promise<OperatorMission | undefined>;
|
|
54
60
|
private save;
|
|
55
61
|
private serial;
|
|
56
62
|
read(root: string): Promise<OperatorMission | undefined>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { normalizeRelativePath } from "./path.js";
|
|
5
5
|
import { parseOperatorPlan } from "./operator-runtime.js";
|
|
@@ -40,6 +40,59 @@ export class OperatorMissionRuntime {
|
|
|
40
40
|
throw error;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/** Recover the predecessor link for missions started before this fix, using one exact archived mission. */
|
|
44
|
+
async loadMission(root) {
|
|
45
|
+
const state = await this.load(this.file(root));
|
|
46
|
+
if (!state || state.supersededRunID !== undefined || state.runID !== null ||
|
|
47
|
+
!["open", "running", "submitted"].includes(state.phase) || state.requests.length === 0)
|
|
48
|
+
return state;
|
|
49
|
+
const directory = join(this.projectRoot, this.profile.stateDirectory, "missions");
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(directory);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT")
|
|
56
|
+
return state;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const prefix = `${digest(root)}.mission-`;
|
|
60
|
+
const candidates = entries.filter(name => name.startsWith(prefix) && /^mission-[a-f0-9-]+\.json$/u.test(name.slice(digest(root).length + 1)));
|
|
61
|
+
const matches = [];
|
|
62
|
+
for (const name of candidates) {
|
|
63
|
+
const previous = await this.load(join(directory, name));
|
|
64
|
+
if (previous?.root === root && previous.phase === "cancelled" && previous.runID !== null &&
|
|
65
|
+
previous.requests[0]?.id !== state.requests[0].id &&
|
|
66
|
+
previous.requests.some(request => request.id === state.requests[0].id))
|
|
67
|
+
matches.push(previous);
|
|
68
|
+
}
|
|
69
|
+
if (matches.length === 1)
|
|
70
|
+
state.supersededRunID = matches[0].runID;
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
/** Find exactly one archived cancelled mission that owned this run; ambiguity never grants recovery. */
|
|
74
|
+
async archivedRun(root, runID) {
|
|
75
|
+
const directory = join(this.projectRoot, this.profile.stateDirectory, "missions");
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = await readdir(directory);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code === "ENOENT")
|
|
82
|
+
return undefined;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
const prefix = `${digest(root)}.mission-`;
|
|
86
|
+
const matches = [];
|
|
87
|
+
for (const name of entries.filter(name => name.startsWith(prefix) &&
|
|
88
|
+
/^mission-[a-f0-9-]+\.json$/u.test(name.slice(digest(root).length + 1)))) {
|
|
89
|
+
const state = await this.load(join(directory, name));
|
|
90
|
+
if (state?.root === root && state.phase === "cancelled" && state.runID === runID &&
|
|
91
|
+
state.coordinator !== null && state.callID !== null)
|
|
92
|
+
matches.push(state);
|
|
93
|
+
}
|
|
94
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
95
|
+
}
|
|
43
96
|
async save(file, value) {
|
|
44
97
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
45
98
|
await mkdir(join(this.projectRoot, this.profile.stateDirectory, "missions"), { recursive: true });
|
|
@@ -64,7 +117,7 @@ export class OperatorMissionRuntime {
|
|
|
64
117
|
}
|
|
65
118
|
async read(root) {
|
|
66
119
|
await this.writes.get(root);
|
|
67
|
-
const state = await this.
|
|
120
|
+
const state = await this.loadMission(root);
|
|
68
121
|
if (state && (state.version !== "0.12" || state.root !== root))
|
|
69
122
|
throw new Error("mission-state-invalid");
|
|
70
123
|
return state;
|
|
@@ -81,7 +134,7 @@ export class OperatorMissionRuntime {
|
|
|
81
134
|
await this.serial(root, async () => {
|
|
82
135
|
// Captured before prompt rewriting, including exact whitespace. Never ask a model to recopy it.
|
|
83
136
|
await this.save(this.file(root, ".request"), request);
|
|
84
|
-
const state = await this.
|
|
137
|
+
const state = await this.loadMission(root);
|
|
85
138
|
if (state && !["completed", "cancelled"].includes(state.phase) && !state.requests.some(item => item.id === request.id)) {
|
|
86
139
|
state.requests.push(request);
|
|
87
140
|
await this.save(this.file(root), state);
|
|
@@ -97,7 +150,7 @@ export class OperatorMissionRuntime {
|
|
|
97
150
|
const request = await this.load(this.file(root, ".request"));
|
|
98
151
|
if (!request)
|
|
99
152
|
throw new Error("mission-original-request-unavailable");
|
|
100
|
-
const previous = await this.
|
|
153
|
+
const previous = await this.loadMission(root);
|
|
101
154
|
if (previous && !["completed", "cancelled"].includes(previous.phase)) {
|
|
102
155
|
if (previous.requirements.some((item, index) => requirements[index] !== item.text)) {
|
|
103
156
|
throw new Error("mission-requirements-preserved: keep the existing ordered requirements and append user additions");
|
|
@@ -110,14 +163,16 @@ export class OperatorMissionRuntime {
|
|
|
110
163
|
await this.save(this.file(root, `.${previous.id}`), previous);
|
|
111
164
|
const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
|
|
112
165
|
requirements: requirements.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "open",
|
|
113
|
-
coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null
|
|
166
|
+
coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
|
|
167
|
+
...(previous?.phase === "cancelled" && previous.runID !== null && request.id !== previous.requests[0]?.id
|
|
168
|
+
? { supersededRunID: previous.runID } : {}) };
|
|
114
169
|
await this.save(this.file(root), state);
|
|
115
170
|
return state;
|
|
116
171
|
});
|
|
117
172
|
}
|
|
118
173
|
update(root, change) {
|
|
119
174
|
return this.serial(root, async () => {
|
|
120
|
-
const state = await this.
|
|
175
|
+
const state = await this.loadMission(root);
|
|
121
176
|
if (!state)
|
|
122
177
|
throw new Error("mission-missing");
|
|
123
178
|
change(state);
|
|
@@ -172,6 +172,8 @@ export interface OperatorState {
|
|
|
172
172
|
readonly sourceRefs: readonly string[];
|
|
173
173
|
readonly createdAt: string;
|
|
174
174
|
readonly parentRunID: string | null;
|
|
175
|
+
/** Archived cancelled run replaced by a later user-authorized mission. */
|
|
176
|
+
readonly supersededRunID?: string;
|
|
175
177
|
readonly priorAcceptedUnits: readonly OperatorAcceptanceAnchor[];
|
|
176
178
|
readonly remediationParent: {
|
|
177
179
|
readonly taskID: string;
|
|
@@ -243,7 +245,7 @@ export declare class OperatorRuntime {
|
|
|
243
245
|
prepareMission(root: string, raw: unknown, dispatcher?: {
|
|
244
246
|
sessionID: string;
|
|
245
247
|
callID: string;
|
|
246
|
-
}): Promise<OperatorState>;
|
|
248
|
+
}, supersededRunID?: string): Promise<OperatorState>;
|
|
247
249
|
retireMissionRun(root: string): Promise<void>;
|
|
248
250
|
private prepareOnce;
|
|
249
251
|
operatorTask(state: OperatorState): OperatorTask;
|
|
@@ -299,7 +299,8 @@ export class OperatorRuntime {
|
|
|
299
299
|
!identifier(state.runID) || !Array.isArray(state.units) || state.units.length > OPERATOR_LIMITS.units ||
|
|
300
300
|
!strings(state.acceptance, true) || !Array.isArray(state.acceptanceProof) || state.acceptanceProof.length !== state.acceptance.length ||
|
|
301
301
|
state.acceptanceProof.some(ids => !strings(ids, true)) || state.acceptanceFingerprint !== acceptanceContinuityFingerprint(state.acceptance) ||
|
|
302
|
-
(state.parentRunID !== null && !identifier(state.parentRunID)) ||
|
|
302
|
+
(state.parentRunID !== null && !identifier(state.parentRunID)) ||
|
|
303
|
+
(state.supersededRunID !== undefined && !identifier(state.supersededRunID)) || !Array.isArray(state.priorAcceptedUnits) ||
|
|
303
304
|
state.priorAcceptedUnits.some(anchor => !record(anchor) || !identifier(anchor.taskID) || typeof anchor.handoffPath !== "string" || !isAbsolute(anchor.handoffPath) ||
|
|
304
305
|
typeof anchor.handoffHash !== "string" || !/^[a-f0-9]{64}$/u.test(anchor.handoffHash)) ||
|
|
305
306
|
(state.remediationParent !== null && (!record(state.remediationParent) || !identifier(state.remediationParent.taskID) ||
|
|
@@ -677,8 +678,8 @@ export class OperatorRuntime {
|
|
|
677
678
|
}
|
|
678
679
|
}
|
|
679
680
|
/** Mission authority is supplied only by the owning profile, never by a model-authored plan. */
|
|
680
|
-
prepareMission(root, raw, dispatcher) {
|
|
681
|
-
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher }));
|
|
681
|
+
prepareMission(root, raw, dispatcher, supersededRunID) {
|
|
682
|
+
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID }));
|
|
682
683
|
}
|
|
683
684
|
retireMissionRun(root) {
|
|
684
685
|
return this.serial(root, async () => {
|
|
@@ -718,18 +719,30 @@ export class OperatorRuntime {
|
|
|
718
719
|
immutableReplacement = { ...immutableReplacement, goal_declaration: declaration };
|
|
719
720
|
}
|
|
720
721
|
const plan = parseOperatorPlan(immutableReplacement);
|
|
722
|
+
const validatedPlanHash = hash(JSON.stringify(plan));
|
|
723
|
+
if (mission?.supersededRunID !== undefined && previous?.supersededRunID === mission.supersededRunID &&
|
|
724
|
+
previous.planHash === validatedPlanHash && previous.phase !== "cancelled")
|
|
725
|
+
return previous;
|
|
726
|
+
const superseding = mission?.supersededRunID !== undefined && previous?.runID === mission.supersededRunID &&
|
|
727
|
+
previous.phase === "cancelled";
|
|
728
|
+
if (mission?.supersededRunID !== undefined && !superseding)
|
|
729
|
+
throw new Error("mission-superseded-run-mismatch");
|
|
730
|
+
if (superseding && previous && (previous.decision !== "explicit-cancellation" || previous.gitLifecycle !== null ||
|
|
731
|
+
previous.repairResidualPaths.length > 0 || previous.priorAcceptedUnits.length > 0 ||
|
|
732
|
+
previous.units.some(unit => unit.childSessionID !== null || unit.status === "succeeded"))) {
|
|
733
|
+
throw new Error("mission-superseded-run-has-work: reconcile prior workers and evidence before changing acceptance");
|
|
734
|
+
}
|
|
721
735
|
if (plan.git_lifecycle !== undefined) {
|
|
722
736
|
await ensureGitManagedStateExcluded(this.projectRoot, this.profile, this.gitPath);
|
|
723
737
|
}
|
|
724
|
-
const planHash = hash(JSON.stringify(plan));
|
|
725
738
|
if (previous && !["completed", "cancelled"].includes(previous.phase)) {
|
|
726
|
-
if (previous.planHash ===
|
|
739
|
+
if (previous.planHash === validatedPlanHash)
|
|
727
740
|
return previous;
|
|
728
741
|
throw new Error("operator-active-contract-immutable");
|
|
729
742
|
}
|
|
730
743
|
// Cancellation stops execution, not the accepted user order. A replacement
|
|
731
744
|
// plan must carry the original ordered criteria; only a completed root clears them.
|
|
732
|
-
const parent = previous?.phase === "cancelled" ? previous : undefined;
|
|
745
|
+
const parent = !superseding && previous?.phase === "cancelled" ? previous : undefined;
|
|
733
746
|
if (parent?.decision === "operator-contract-repair-unavailable-after-cancel") {
|
|
734
747
|
// The diagnosed transients, not the cancellation itself, block a replacement. Once they are
|
|
735
748
|
// gone the same accepted order may proceed; while they remain the refusal names them exactly.
|
|
@@ -878,9 +891,9 @@ export class OperatorRuntime {
|
|
|
878
891
|
childSessionID: null, evidence: [], resultClass: null, repairValidationAttempts: 0, repairValidation: null });
|
|
879
892
|
}
|
|
880
893
|
const gitLifecycle = await this.createGitLifecycle(plan, remediationHead);
|
|
881
|
-
const state = { schema_version: "0.1", profile: this.profile.id, rootSessionID: root, runID, planHash,
|
|
894
|
+
const state = { schema_version: "0.1", profile: this.profile.id, rootSessionID: root, runID, planHash: validatedPlanHash,
|
|
882
895
|
acceptance: plan.acceptance, acceptanceProof: plan.acceptance_proof, acceptanceFingerprint, sourceRefs: plan.source_refs, createdAt: new Date().toISOString(),
|
|
883
|
-
parentRunID: parent?.runID ?? null, priorAcceptedUnits,
|
|
896
|
+
parentRunID: parent?.runID ?? null, ...(superseding ? { supersededRunID: previous.runID } : {}), priorAcceptedUnits,
|
|
884
897
|
remediationParent: remediationTaskID && remediationHead && parent?.gitLifecycle ? {
|
|
885
898
|
taskID: remediationTaskID, committedHead: remediationHead, runID: parent.runID,
|
|
886
899
|
acceptanceFingerprint: parent.acceptanceFingerprint, approvedWriteUnion: parent.gitLifecycle.writeUnion,
|
|
@@ -908,6 +921,17 @@ export class OperatorRuntime {
|
|
|
908
921
|
await handle.close();
|
|
909
922
|
}
|
|
910
923
|
}
|
|
924
|
+
if (superseding && previous) {
|
|
925
|
+
const archive = `${this.file(root)}.${previous.runID}.archive`;
|
|
926
|
+
try {
|
|
927
|
+
await writeFile(archive, JSON.stringify(previous), { flag: "wx", mode: 0o600 });
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
if (error.code !== "EEXIST" ||
|
|
931
|
+
await readFile(archive, "utf8") !== JSON.stringify(previous))
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
911
935
|
await this.save(state);
|
|
912
936
|
return state;
|
|
913
937
|
}
|
package/dist/plugin/index.js
CHANGED
|
@@ -7,12 +7,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import { lstat, mkdir, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import { lstat, mkdir, open, readFile, readdir, readlink, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
11
11
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
12
12
|
import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
13
13
|
import { GOAL_DECLARATION_FORMAT, GOAL_DELIVERY_INTENTS, GOAL_DELIVERY_MODES, GOAL_CRITERION_ENUMS, expandGoalDeclaration } from "../core/goal-declaration-format.js";
|
|
14
14
|
import { ACCEPTANCE_CONTINUITY_AUTHORITY, ACCEPTANCE_CONTINUITY_EXTENSION, ACCEPTANCE_CONTINUITY_SCHEMA_VERSION, acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, } from "../core/acceptance-continuity.js";
|
|
15
15
|
import { resolveGlobalConfigRoot } from "../core/initialize.js";
|
|
16
|
+
import { OperatorMissionRuntime } from "../core/operator-mission.js";
|
|
16
17
|
import { admitLunaFabric } from "../core/luna-fabric-contract.js";
|
|
17
18
|
import { summarizeExperienceEvidence } from "../core/experience-evidence-summary.js";
|
|
18
19
|
import { selectExperienceRoute } from "../core/experience-route-policy.js";
|
|
@@ -234,6 +235,7 @@ async function durableScopeRoot(projectRoot) {
|
|
|
234
235
|
}
|
|
235
236
|
async function protectedScopeDigest(projectRoot, paths, manifestHash) {
|
|
236
237
|
const entries = [];
|
|
238
|
+
const canonicalRoot = await realpath(projectRoot);
|
|
237
239
|
const visit = async (absolute) => {
|
|
238
240
|
const scoped = relative(projectRoot, absolute).replaceAll("\\", "/");
|
|
239
241
|
if (scoped === ".." || scoped.startsWith("../") || isAbsolute(scoped))
|
|
@@ -247,8 +249,22 @@ async function protectedScopeDigest(projectRoot, paths, manifestHash) {
|
|
|
247
249
|
entries.push([scoped, "missing"]);
|
|
248
250
|
return true;
|
|
249
251
|
}
|
|
250
|
-
if (metadata.isSymbolicLink())
|
|
251
|
-
|
|
252
|
+
if (metadata.isSymbolicLink()) {
|
|
253
|
+
// Package managers place executable links under node_modules/.bin. Record
|
|
254
|
+
// both the link and its target bytes, but never follow a link out of the
|
|
255
|
+
// project or recurse through a linked directory (which may form a cycle).
|
|
256
|
+
const target = await realpath(absolute).catch(() => undefined);
|
|
257
|
+
if (target === undefined)
|
|
258
|
+
return false;
|
|
259
|
+
const relativeTarget = relative(canonicalRoot, target);
|
|
260
|
+
if (relativeTarget === ".." || relativeTarget.startsWith("../") || isAbsolute(relativeTarget) ||
|
|
261
|
+
!(await stat(target)).isFile())
|
|
262
|
+
return false;
|
|
263
|
+
const link = await readlink(absolute);
|
|
264
|
+
const contents = await readFile(target);
|
|
265
|
+
entries.push([scoped, `symlink:${link}`, createHash("sha256").update(contents).digest("hex")]);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
252
268
|
if (metadata.isDirectory()) {
|
|
253
269
|
entries.push([scoped, "directory"]);
|
|
254
270
|
const children = await readdir(absolute);
|
|
@@ -1609,6 +1625,82 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1609
1625
|
const data = isRecord(response) ? response.data : undefined;
|
|
1610
1626
|
if (!Array.isArray(data))
|
|
1611
1627
|
return;
|
|
1628
|
+
const rootHistory = data;
|
|
1629
|
+
async function terminalCancelledMissionTask(unitID, reservationID, goalID) {
|
|
1630
|
+
const runID = unitID.replace(/-[1-9][0-9]*$/u, "");
|
|
1631
|
+
if (runID === unitID || !/^operator-[a-f0-9-]+$/u.test(runID) ||
|
|
1632
|
+
input.client?.session?.get === undefined)
|
|
1633
|
+
return undefined;
|
|
1634
|
+
const sessionAPI = input.client.session;
|
|
1635
|
+
const childrenMethod = sessionAPI.children;
|
|
1636
|
+
const getMethod = sessionAPI.get;
|
|
1637
|
+
const mission = await new OperatorMissionRuntime(input.directory, runtimeProfile).archivedRun(root, runID);
|
|
1638
|
+
if (!mission || !rootHistory.some(message => isRecord(message) && isRecord(message.info) &&
|
|
1639
|
+
message.info.sessionID === root && message.info.role === "assistant" && Array.isArray(message.parts) &&
|
|
1640
|
+
message.parts.some(part => isRecord(part) && part.type === "tool" && part.tool === "task" &&
|
|
1641
|
+
part.callID === mission.callID && isRecord(part.state) && part.state.status === "completed" &&
|
|
1642
|
+
isRecord(part.state.input) && typeof part.state.input.prompt === "string" &&
|
|
1643
|
+
part.state.input.prompt.startsWith("SORTIE_MISSION_REF ") && (() => {
|
|
1644
|
+
try {
|
|
1645
|
+
const ref = JSON.parse(part.state.input.prompt.slice("SORTIE_MISSION_REF ".length));
|
|
1646
|
+
return isRecord(ref) && ref.r === root && ref.m === mission.id;
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
})())))
|
|
1652
|
+
return undefined;
|
|
1653
|
+
const coordinatorResponse = await getMethod.call(sessionAPI, { path: { id: mission.coordinator }, query: { directory: input.directory } });
|
|
1654
|
+
const coordinatorInfo = isRecord(coordinatorResponse) ? coordinatorResponse.data : undefined;
|
|
1655
|
+
if (!isRecord(coordinatorInfo) || coordinatorInfo.id !== mission.coordinator ||
|
|
1656
|
+
coordinatorInfo.parentID !== root || coordinatorInfo.agent !== "dogs-coordinator" ||
|
|
1657
|
+
coordinatorInfo.outcome !== "succeeded")
|
|
1658
|
+
return undefined;
|
|
1659
|
+
if (childrenMethod !== undefined) {
|
|
1660
|
+
const childrenResponse = await childrenMethod.call(sessionAPI, { path: { id: root }, query: { directory: input.directory } });
|
|
1661
|
+
const children = isRecord(childrenResponse) && Array.isArray(childrenResponse.data) ? childrenResponse.data : [];
|
|
1662
|
+
if (!children.some(child => isRecord(child) && child.id === mission.coordinator &&
|
|
1663
|
+
child.parentID === root && child.agent === "dogs-coordinator"))
|
|
1664
|
+
return undefined;
|
|
1665
|
+
}
|
|
1666
|
+
const coordinator = mission.coordinator;
|
|
1667
|
+
const historyResponse = await messages.call(sessionAPI, { path: { id: coordinator }, query: { directory: input.directory } });
|
|
1668
|
+
const history = isRecord(historyResponse) && Array.isArray(historyResponse.data) ? historyResponse.data : [];
|
|
1669
|
+
const candidates = [];
|
|
1670
|
+
for (const message of history) {
|
|
1671
|
+
if (!isRecord(message) || !isRecord(message.info) || message.info.role !== "assistant" ||
|
|
1672
|
+
message.info.sessionID !== coordinator || !Array.isArray(message.parts))
|
|
1673
|
+
continue;
|
|
1674
|
+
for (const part of message.parts) {
|
|
1675
|
+
if (!isRecord(part) || part.type !== "tool" || part.tool !== "task" ||
|
|
1676
|
+
!isRecord(part.state) || part.state.status !== "error" || !isRecord(part.state.input) ||
|
|
1677
|
+
typeof part.state.input.prompt !== "string" || !part.state.input.prompt.startsWith("SORTIE_OPERATOR_TASK_REF ") ||
|
|
1678
|
+
typeof part.callID !== "string")
|
|
1679
|
+
continue;
|
|
1680
|
+
try {
|
|
1681
|
+
const ref = JSON.parse(part.state.input.prompt.slice("SORTIE_OPERATOR_TASK_REF ".length));
|
|
1682
|
+
if (isRecord(ref) && ref.r === root && ref.n === runID && ref.t === unitID &&
|
|
1683
|
+
/^[a-f0-9]{64}$/u.test(String(ref.p)) && /^[a-f0-9]{64}$/u.test(String(ref.h)) &&
|
|
1684
|
+
goalFingerprint({ goal_id: goalID, unit_id: unitID, call_id: part.callID }) === reservationID) {
|
|
1685
|
+
const error = isRecord(part.state.error) ? part.state.error : undefined;
|
|
1686
|
+
candidates.push({ callID: part.callID, depthRefused: error?.type === "tool.execution" &&
|
|
1687
|
+
error.message === 'Subagent depth limit reached (1). Increase "experimental.subagent_depth" to allow nested subagents.' });
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
catch { /* malformed native reference cannot prove this reservation */ }
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if (candidates.length !== 1)
|
|
1694
|
+
return undefined;
|
|
1695
|
+
if (childrenMethod !== undefined) {
|
|
1696
|
+
const descendantsResponse = await childrenMethod.call(sessionAPI, { path: { id: coordinator }, query: { directory: input.directory } });
|
|
1697
|
+
if (!isRecord(descendantsResponse) || !Array.isArray(descendantsResponse.data) || descendantsResponse.data.length !== 0)
|
|
1698
|
+
return undefined;
|
|
1699
|
+
}
|
|
1700
|
+
else if (mission.submission?.status !== "blocked" || !candidates[0].depthRefused)
|
|
1701
|
+
return undefined;
|
|
1702
|
+
return candidates[0].callID;
|
|
1703
|
+
}
|
|
1612
1704
|
for (const reservation of pending) {
|
|
1613
1705
|
const matches = [];
|
|
1614
1706
|
for (const message of data) {
|
|
@@ -1686,8 +1778,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1686
1778
|
matches.push({ callID: part.callID, status: String(part.state.status), elapsed });
|
|
1687
1779
|
}
|
|
1688
1780
|
}
|
|
1689
|
-
if (matches.length !== 1)
|
|
1690
|
-
|
|
1781
|
+
if (matches.length !== 1) {
|
|
1782
|
+
const callID = matches.length === 0 ? await terminalCancelledMissionTask(reservation.unit_id, reservation.reservation_id, state.goal_id) : undefined;
|
|
1783
|
+
if (callID === undefined)
|
|
1784
|
+
continue;
|
|
1785
|
+
matches.push({ callID, status: "error", elapsed: null });
|
|
1786
|
+
}
|
|
1691
1787
|
const match = matches[0];
|
|
1692
1788
|
// Reconcile lifecycle accounting only. Lost in-memory validation bindings cannot be
|
|
1693
1789
|
// reconstructed from worker prose and must not manufacture acceptance evidence.
|
|
@@ -7743,6 +7839,9 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
7743
7839
|
currentBudget: async (root) => {
|
|
7744
7840
|
if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
|
|
7745
7841
|
throw new Error("operator-coordinator-required");
|
|
7842
|
+
// Planning must account for terminal native Tasks before reserving new units.
|
|
7843
|
+
// Otherwise a cancelled predecessor keeps a stale unit reservation across missions.
|
|
7844
|
+
await recoverCompletedGoalReservations(root);
|
|
7746
7845
|
const state = await currentGoal(root);
|
|
7747
7846
|
if (state.goal_id === null || state.budget === null)
|
|
7748
7847
|
return null;
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -1139,7 +1139,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1139
1139
|
if (budget && budget.remaining_units < plan.units.length && !same)
|
|
1140
1140
|
throw new Error("mission-budget-exhausted: report the required cumulative extension to Operator");
|
|
1141
1141
|
const state = await operators.prepareMission(root, plan, actor === root ? undefined
|
|
1142
|
-
: { sessionID: actor, callID: mission.callID });
|
|
1142
|
+
: { sessionID: actor, callID: mission.callID }, mission.supersededRunID);
|
|
1143
1143
|
await restorePriorAcceptance(root, state);
|
|
1144
1144
|
await control.registerGoalDeclaration(root, state.units[0].task.prompt, true);
|
|
1145
1145
|
control.enableUnits(root, state.units.filter(unit => unit.status === "pending").length);
|
|
@@ -1149,6 +1149,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1149
1149
|
item.runID = state.runID;
|
|
1150
1150
|
item.phase = "running";
|
|
1151
1151
|
item.submission = null;
|
|
1152
|
+
delete item.supersededRunID;
|
|
1152
1153
|
});
|
|
1153
1154
|
return JSON.stringify(await operators.next(root, actor));
|
|
1154
1155
|
}
|