sortie-dogs 0.16.2 → 0.16.3

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.
@@ -3,6 +3,6 @@
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.16.2-direct-validation-registration-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.16.3-strategy-admission-recovery-v1";
7
7
  export declare const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
8
8
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
@@ -3,5 +3,5 @@
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.16.2-direct-validation-registration-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.16.3-strategy-admission-recovery-v1";
7
7
  export const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
@@ -344,7 +344,10 @@ export declare class OperatorRuntime {
344
344
  prepareMission(root: string, raw: unknown, dispatcher?: {
345
345
  sessionID: string;
346
346
  callID: string;
347
- }, supersededRunID?: string, terminalChildren?: readonly string[], replaceRequirements?: boolean, context?: Record<string, unknown>): Promise<OperatorState>;
347
+ }, supersededRunID?: string, terminalChildren?: readonly string[], replaceRequirements?: boolean, context?: Record<string, unknown>, terminalTaskCalls?: {
348
+ runID: string;
349
+ callIDs: readonly string[];
350
+ }): Promise<OperatorState>;
348
351
  /** Stage a settled mission's replacement; rejected preparation never cancels the usable run. */
349
352
  replanMission(root: string, runID: string, raw: unknown, dispatcher?: {
350
353
  sessionID: string;
@@ -380,6 +383,8 @@ export declare class OperatorRuntime {
380
383
  }>;
381
384
  private matchesWorkerTask;
382
385
  matchesRecordedWorkerTask(state: OperatorState, unitID: string, args: unknown): boolean;
386
+ /** Historical comparison only: cancellation revoked the admission generation. */
387
+ matchesCancelledWorkerTask(state: OperatorState, unitID: string, args: unknown): boolean;
383
388
  admitOperator(root: string, callID: string, args: unknown): Promise<OperatorTask>;
384
389
  private admitOperatorOnce;
385
390
  bindOperator(root: string, sessionID: string, prompt: string): Promise<void>;
@@ -766,8 +766,8 @@ export class OperatorRuntime {
766
766
  }
767
767
  }
768
768
  /** Mission authority is supplied only by the owning profile, never by a model-authored plan. */
769
- prepareMission(root, raw, dispatcher, supersededRunID, terminalChildren = [], replaceRequirements = false, context) {
770
- return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID, terminalChildren, replaceRequirements, context }));
769
+ prepareMission(root, raw, dispatcher, supersededRunID, terminalChildren = [], replaceRequirements = false, context, terminalTaskCalls) {
770
+ return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID, terminalChildren, replaceRequirements, context, terminalTaskCalls }));
771
771
  }
772
772
  /** Stage a settled mission's replacement; rejected preparation never cancels the usable run. */
773
773
  replanMission(root, runID, raw, dispatcher, context) {
@@ -946,6 +946,17 @@ export class OperatorRuntime {
946
946
  if (mission?.supersededRunID !== undefined && !superseding)
947
947
  throw new Error("mission-superseded-run-mismatch");
948
948
  const terminalChildren = mission?.terminalChildren ?? [];
949
+ const unclaimedCalls = previous?.units.flatMap(unit => !unit.directExecution && unit.status === "cancelled" &&
950
+ unit.childSessionID === null && unit.callID !== null ? [unit.callID] : []) ?? [];
951
+ // A cancelled admission can precede its child's first prompt. Only the owning
952
+ // profile's exact native Task proof, not null or cancellation alone, closes it.
953
+ if (mission && previous?.phase === "cancelled" && unclaimedCalls.length &&
954
+ (mission?.terminalTaskCalls?.runID !== previous.runID ||
955
+ new Set(mission.terminalTaskCalls.callIDs).size !== unclaimedCalls.length ||
956
+ mission.terminalTaskCalls.callIDs.length !== unclaimedCalls.length ||
957
+ unclaimedCalls.some(call => !mission.terminalTaskCalls.callIDs.includes(call)))) {
958
+ throw new Error("mission-cancelled-run-task-not-terminal");
959
+ }
949
960
  const retainAcceptance = previous !== undefined && cancelledMissionRetainsAcceptance(previous);
950
961
  const predecessorChildren = previous?.units.flatMap(unit => !unit.directExecution && unit.childSessionID !== null
951
962
  ? [unit.childSessionID] : []) ?? [];
@@ -957,7 +968,7 @@ export class OperatorRuntime {
957
968
  unit.repairValidation !== null || (unit.status === "pending" && (unit.callID !== null || unit.childSessionID !== null ||
958
969
  unit.evidence.length > 0 || unit.resultClass !== null)) ||
959
970
  (unit.status === "succeeded" && (unit.childSessionID === null || unit.callID === null || unit.evidence.length === 0 || unit.resultClass !== "acceptance")) ||
960
- (unit.status === "cancelled" && (unit.childSessionID === null || unit.callID === null))))) {
971
+ (unit.status === "cancelled" && unit.callID === null)))) {
961
972
  throw new Error("mission-superseded-run-has-work: reconcile prior workers and evidence before changing acceptance");
962
973
  }
963
974
  if (plan.git_lifecycle !== undefined) {
@@ -1197,9 +1208,11 @@ export class OperatorRuntime {
1197
1208
  await handle.close();
1198
1209
  }
1199
1210
  }
1200
- if ((superseding || replanning || previous?.phase === "completed") && previous) {
1211
+ if ((superseding || replanning || previous?.phase === "completed" ||
1212
+ previous?.phase === "cancelled" && unclaimedCalls.length > 0 && mission?.terminalTaskCalls !== undefined) && previous) {
1201
1213
  // A crash before the state switch may leave a checkpoint while the old run later advances.
1202
1214
  // Sequence-qualified replan snapshots keep that history without blocking a subsequent correction.
1215
+ // Same-request recovery also retains the original unclaimed cancellation, not just its costs.
1203
1216
  const archive = replanning || previous.phase === "completed" ? this.replanArchivePath(previous)
1204
1217
  : `${this.file(root)}.${previous.runID}.archive`;
1205
1218
  try {
@@ -1370,6 +1383,28 @@ export class OperatorRuntime {
1370
1383
  : args;
1371
1384
  return unit !== undefined && this.matchesWorkerTask(state, unit, normalized);
1372
1385
  }
1386
+ /** Historical comparison only: cancellation revoked the admission generation. */
1387
+ matchesCancelledWorkerTask(state, unitID, args) {
1388
+ const unit = state.units.find(item => item.unit.id === unitID);
1389
+ if (state.phase !== "cancelled" || !unit || unit.status !== "cancelled" || unit.childSessionID !== null ||
1390
+ !record(args) || typeof args.prompt !== "string")
1391
+ return false;
1392
+ if (args.prompt === unit.task.prompt)
1393
+ return this.matchesRecordedWorkerTask(state, unitID, args);
1394
+ if (!args.prompt.startsWith(`${OPERATOR_TASK_REFERENCE} `))
1395
+ return false;
1396
+ let reference;
1397
+ try {
1398
+ reference = JSON.parse(args.prompt.slice(OPERATOR_TASK_REFERENCE.length + 1));
1399
+ }
1400
+ catch {
1401
+ return false;
1402
+ }
1403
+ if (!record(reference) || !Number.isSafeInteger(reference.g) || reference.g < 1 ||
1404
+ reference.g >= state.generation)
1405
+ return false;
1406
+ return this.matchesRecordedWorkerTask({ ...state, generation: reference.g }, unitID, args);
1407
+ }
1373
1408
  admitOperator(root, callID, args) {
1374
1409
  return this.serial(root, () => this.admitOperatorOnce(root, callID, args));
1375
1410
  }
@@ -1251,6 +1251,9 @@ export function createProfiledPlugin(profile, assetVersion) {
1251
1251
  return packet;
1252
1252
  }
1253
1253
  function missionDispatchPacket(mission, run) {
1254
+ const strategyRecovery = " If a method stalled or proposed avoidable extra work, Operator: compare the original goal, actual result and concrete unmet work before choosing to continue, change or stop that method. " +
1255
+ `For a concrete adjustment, use ${profile.toolPrefix}plan_units with proposed units, reason and handoff_context, then dispatch its returned same-Coordinator continuation. ` +
1256
+ "Preserve required checks through lawful registration/replanning and reuse current evidence. Otherwise continue normally; no extra approval or logging-only step.";
1254
1257
  const packet = { ...missionPacket(mission, run), project_root: input.directory,
1255
1258
  coordinator_dispatch: mission.dispatchOpen ? "active" : ["completed", "cancelled"].includes(mission.phase) ? "terminal" : "resumable" };
1256
1259
  if (mission.phase === "submitted" && mission.submission?.status === "blocked" &&
@@ -1284,7 +1287,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1284
1287
  if (mission.runID === run?.runID && run?.units.some(unit => unit.status === "running")) {
1285
1288
  if (mission.coordinator && !mission.dispatchOpen && !["completed", "cancelled"].includes(mission.phase))
1286
1289
  return { ...packet, task: missions.task(mission),
1287
- next_action: "Resume this existing Coordinator Task with its task_id to continue the retained unit. Do not dispatch a replacement or wait for background work that has already ended." };
1290
+ next_action: "Resume this existing Coordinator Task with its task_id to continue the retained unit. Do not dispatch a replacement or wait for background work that has already ended." + strategyRecovery };
1288
1291
  return { ...packet, next_action: "The current unit is active. Continue its owning session; do not dispatch a replacement Worker or Coordinator. Native background completion will return to the parent." };
1289
1292
  }
1290
1293
  if (mission.coordinator === null && mission.runID === run?.runID && !mission.dispatchOpen &&
@@ -1330,7 +1333,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1330
1333
  next_action: (run?.units.some(unit => unit.dispatchDenial) ? `${packet.next_action}\n` : "") +
1331
1334
  (mission.coordinator === null
1332
1335
  ? "Fast-lane needs correction or coordination: dispatch this exact Coordinator Task with the existing Worker changes, checks and concrete remaining work. Keep the same mission, requirements and cumulative budget; do not replace an active Worker."
1333
- : "The previous Coordinator Task is finished. Dispatch this exact Task to continue the same mission and Coordinator session; keep the original requirements and cumulative budget.") };
1336
+ : "The previous Coordinator Task is finished. Dispatch this exact Task to continue the same mission and Coordinator session; keep the original requirements and cumulative budget." + strategyRecovery) };
1334
1337
  }
1335
1338
  return packet;
1336
1339
  }
@@ -1559,6 +1562,58 @@ export function createProfiledPlugin(profile, assetVersion) {
1559
1562
  await operators.recordStoppedChild(root, run.runID, child);
1560
1563
  }
1561
1564
  }
1565
+ async function cancelledUnclaimedTaskCalls(root, run) {
1566
+ const units = run.units.filter(unit => !unit.directExecution && unit.status === "cancelled" &&
1567
+ unit.childSessionID === null && unit.callID !== null);
1568
+ if (!units.length)
1569
+ return undefined;
1570
+ const owner = run.operatorSessionID ?? root;
1571
+ const deny = () => new Error("mission-cancelled-run-task-not-terminal");
1572
+ // The bounded chat context may omit the old Task. Use the existing native
1573
+ // history reader, and never substitute a model's stop report for a tool terminal.
1574
+ const history = await reviewMessages(owner, undefined, true);
1575
+ for (const unit of units) {
1576
+ const parts = history.flatMap(message => record(message.info) && message.info.role === "assistant" &&
1577
+ message.info.sessionID === owner && Array.isArray(message.parts) ? message.parts : []).filter(part => record(part) && part.type === "tool" && part.tool === "task" && part.callID === unit.callID);
1578
+ const part = parts.length === 1 && record(parts[0]) ? parts[0] : undefined;
1579
+ const state = part && record(part.state) ? part.state : undefined;
1580
+ if (!part || part.sessionID !== undefined && part.sessionID !== owner || !state ||
1581
+ !["completed", "error"].includes(String(state.status)) ||
1582
+ record(state.metadata) && ["running", "pending", "unknown"].includes(String(state.metadata.status)) ||
1583
+ !record(state.time) || typeof state.time.end !== "number" || !Number.isFinite(state.time.end) ||
1584
+ !operators.matchesCancelledWorkerTask(run, unit.unit.id, state.input) || taskChildSessionID(state) !== undefined)
1585
+ throw deny();
1586
+ }
1587
+ // A parent Task terminal does not prove that an unclaimed child never started.
1588
+ // Reuse the owning service's active snapshot and child lineage before allowing
1589
+ // this formerly unrepresentable cancellation into prepareMission.
1590
+ const active = payload(await session("active", {}));
1591
+ if (!record(active) || !Object.values(active).every(value => record(value) &&
1592
+ ["running", "retry"].includes(String(value.type))) || owner !== root && Object.hasOwn(active, owner))
1593
+ throw deny();
1594
+ const children = payload(await session("children", { path: { id: owner }, query: { directory: input.directory } }));
1595
+ if (!Array.isArray(children) || children.some(child => !record(child) || typeof child.id !== "string") ||
1596
+ new Set(children.map(child => child.id)).size !== children.length)
1597
+ throw deny();
1598
+ for (const child of children) {
1599
+ const info = payload(await session("get", { path: { id: child.id }, query: { directory: input.directory } }));
1600
+ if (!record(info) || info.id !== child.id || info.parentID !== owner ||
1601
+ typeof info.agent !== "string" || !info.agent.trim())
1602
+ throw deny();
1603
+ const role = canonicalAgent(profile, info.agent);
1604
+ // Root Fast-lane Tasks must not claim unrelated profile or native agents.
1605
+ if (owner === root && role !== "dog-worker" && role !== "dog-luna-worker")
1606
+ continue;
1607
+ if (!role)
1608
+ throw deny();
1609
+ const descendants = payload(await session("children", { path: { id: child.id }, query: { directory: input.directory } }));
1610
+ if (Object.hasOwn(active, child.id) || !Array.isArray(descendants) || descendants.length ||
1611
+ !["succeeded", "failed", "interrupted"].includes(String(info.outcome)) &&
1612
+ !(info.outcome === undefined && run.stoppedChildren?.includes(child.id)))
1613
+ throw deny();
1614
+ }
1615
+ return { runID: run.runID, callIDs: units.map(unit => unit.callID) };
1616
+ }
1562
1617
  async function stop(root, reason, retireRoot = true) {
1563
1618
  const directActor = (await operators.read(root))?.units.find(unit => unit.status === "running" && unit.directExecution)?.directExecution?.actor;
1564
1619
  // Stopping owned execution does not write its artifacts. Its existing reservation
@@ -3031,8 +3086,24 @@ export function createProfiledPlugin(profile, assetVersion) {
3031
3086
  // packet can already contain one even while its Reviewer is still pending.
3032
3087
  const { task: _dispatchTask, ...packet } = missionDispatchPacket(mission, previous);
3033
3088
  const reviewPending = mission.runID === previous?.runID && mission.review?.verdict === "pending";
3034
- const active = mission.dispatchOpen || reviewPending || mission.runID === previous?.runID &&
3035
- previous?.units.some(unit => unit.status === "running");
3089
+ const running = mission.runID === previous?.runID ? previous.units.filter(unit => unit.status === "running") : [];
3090
+ let retainedDirectOwnerEnded = false;
3091
+ if (!mission.dispatchOpen && !reviewPending && mission.coordinator && running.length &&
3092
+ running.every(unit => unit.directExecution?.actor === mission.coordinator && !unit.reviewerCorrection)) {
3093
+ // This only forwards a proposal to the SAME owner. Native session termination
3094
+ // does not settle its retained unit or authorize a new writer/contract.
3095
+ const native = payload(await session("get", { path: { id: mission.coordinator } }).catch(() => undefined));
3096
+ if (record(native) && native.id === mission.coordinator && native.parentID === root &&
3097
+ canonicalAgent(profile, native.agent) === "dog-operator" &&
3098
+ ["succeeded", "failed", "interrupted"].includes(String(native.outcome))) {
3099
+ try {
3100
+ await assertDirectPlanningQuiescent(mission.coordinator);
3101
+ retainedDirectOwnerEnded = true;
3102
+ }
3103
+ catch { /* A live/unproven native tool still owns this work; do not offer another Task. */ }
3104
+ }
3105
+ }
3106
+ const active = mission.dispatchOpen || reviewPending || running.length > 0 && !retainedDirectOwnerEnded;
3036
3107
  const continuation = !active ? missions.task(mission) : undefined;
3037
3108
  // Existing Coordinator resumes accept a concrete prompt. Do not use the opaque
3038
3109
  // initial reference here: claim() would expand it from unchanged Mission state
@@ -3108,7 +3179,8 @@ export function createProfiledPlugin(profile, assetVersion) {
3108
3179
  const cancelledPredecessor = previous?.phase === "cancelled" &&
3109
3180
  (mission.supersededRunID === previous.runID ||
3110
3181
  (mission.supersededRunID === undefined && ["explicit-cancellation", "agent-changed"].includes(previous.decision ?? "") &&
3111
- previous.units.some(unit => (previous.decision === "agent-changed" || unit.status === "cancelled") && unit.childSessionID !== null)));
3182
+ previous.units.some(unit => (previous?.decision === "agent-changed" || unit.status === "cancelled") &&
3183
+ (unit.childSessionID !== null || !unit.directExecution && unit.callID !== null))));
3112
3184
  let budget = await control.currentBudget(root);
3113
3185
  if (cancelledPredecessor && budget?.reserved_units) {
3114
3186
  await control.reconcileAbortedOperatorOrphan(root);
@@ -3119,6 +3191,7 @@ export function createProfiledPlugin(profile, assetVersion) {
3119
3191
  // Reconcile it even when reservation recovery has nothing to do. Never resume it,
3120
3192
  // interrupt live work here, or manufacture its native terminal/validation records.
3121
3193
  await stopCancelledChildren(root, previous, true);
3194
+ previous = await operators.required(root);
3122
3195
  }
3123
3196
  if (budget && budget.remaining_units < plan.units.length && !same)
3124
3197
  throw new Error(`mission-budget-exhausted: plan needs ${plan.units.length} units; ${budget.remaining_units} remain. ` +
@@ -3156,8 +3229,9 @@ export function createProfiledPlugin(profile, assetVersion) {
3156
3229
  ...(previous ? { retained_validation: missionReviewValidation(previous, replanning || previous.phase === "completed"
3157
3230
  ? operators.replanArchivePath(previous) : cancelledPredecessor ? `${operators.statePath(root)}.${previous.runID}.archive` : operators.statePath(root)),
3158
3231
  prior_run_id: previous.runID } : {}) };
3232
+ const terminalTaskCalls = cancelledPredecessor && previous ? await cancelledUnclaimedTaskCalls(root, previous) : undefined;
3159
3233
  const state = replanning && previous ? await operators.replanMission(root, previous.runID, plan, dispatcher, context)
3160
- : await operators.prepareMission(root, plan, dispatcher, mission.supersededRunID, terminalChildren, mission.requirementsReplaced, context);
3234
+ : await operators.prepareMission(root, plan, dispatcher, mission.supersededRunID, terminalChildren, mission.requirementsReplaced, context, terminalTaskCalls);
3161
3235
  // prepareMission retains an identical prepared run after either registration stage
3162
3236
  // fails. Verify its pinned controls, then idempotently register the same declaration;
3163
3237
  // only the durable Mission binding below makes it eligible for admission.
@@ -163,6 +163,18 @@ ${controls(profile, ["start_mission", "plan_units", "start_direct_unit", "finish
163
163
  You are Operator, the user-facing strategic authority. Preserve every current requirement, prohibition,
164
164
  quality threshold and explicit model/budget choice. Follow AGENTS.md and use the user's language.
165
165
 
166
+ Own strategy, not just the relay of a stopped author's reply. At a method stall or proposed extra step,
167
+ compare the user's goal, actual result and concrete unmet work. Continue planned useful work without
168
+ intervention; suppress unnecessary expansion, and choose only the recovery needed for a real obstacle.
169
+ Separate a required outcome/check from an AI-selected method. Do not promote your own checker, evidence
170
+ format or procedure into an immutable user requirement. An AI-selected method already registered as
171
+ formal validation still needs lawful recipe correction or replanning; never silently omit it or invent PASS.
172
+ When a change of method is needed, give the existing author the concrete alternative, rationale and
173
+ retained obligations, not merely "resume" or the same failing instruction. Use plan_units with proposed
174
+ units, reason and handoff_context to obtain its concrete Coordinator continuation when available.
175
+ Do not restart a release, cancel live work or take over implementation just to exercise strategic authority.
176
+ Ordinary implementation decisions remain with the author; no per-Task approval or decision-record ritual.
177
+
166
178
  ${MISSION_DEPENDENCY_WORKFLOW}
167
179
 
168
180
  1. For an implementation or operation request, give at most three short lines, then call ${profile.toolPrefix}start_mission
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
4
  "description": "Adaptive execution harness for OpenCode and Codex: goal retention, validation, review, and evidence-backed handoff",
5
5
  "keywords": [
6
6
  "opencode",