sortie-dogs 0.5.10 → 0.5.13

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.
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { lstat, open, readFile, realpath, rm, stat } from "node:fs/promises";
2
+ import { lstat, mkdir, open, readFile, realpath, rm, stat } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
5
5
  import { normalizeRelativePath, RelativePathError } from "../core/path.js";
@@ -8,6 +8,7 @@ import { produceWorktreeCommitArtifact, recoverWorktreeCommitArtifact, resolveVa
8
8
  import { normalizeWorktreeScope } from "../core/worktree-scope.js";
9
9
  import { ParallelDispatchCoordinator, ParallelDispatchError, } from "../core/worktree-parallel-dispatch.js";
10
10
  import { IntegrationQueueError, WorktreeIntegrationQueue } from "../core/worktree-integration-queue.js";
11
+ import { WorktreeLifecycleError } from "../core/worktree-lifecycle.js";
11
12
  import { validateManifest } from "../core/validate-manifest.js";
12
13
  import { safeSchemaPointer, validateHandoffSchema, validateOperationManifestSchema, } from "../core/validate-schema.js";
13
14
  import { DEFAULT_PLUGIN_OPTIONS, resolvePluginConfiguration, resolvePluginConfigurationSourcesWithGlobal, } from "./config.js";
@@ -32,6 +33,8 @@ const SORTIE_TRIGGER = /^\/sortie(?:\s|$)/;
32
33
  const TASK_ROLES = new Set(["implementation", "remediation", "blocker-resolution"]);
33
34
  const GIT_POINTER_LIMIT = 4096;
34
35
  const PARALLEL_OUTCOME_MARKER = "SORTIE_PARALLEL_OUTCOME";
36
+ const CANONICAL_CONTRACT_DIRECTORY = ".sortie-dogs/contracts";
37
+ const CANONICAL_CONTRACT_HANDOFF = `${CANONICAL_CONTRACT_DIRECTORY}/handoff.json`;
35
38
  export const PARALLEL_COMMIT_ARTIFACT_CAPABILITY = "sortie_create_parallel_commit_artifact";
36
39
  const CONTRACT_DEFECTS = { limit: 8, pointerCharacters: 120 };
37
40
  function contractPointer(pointer) {
@@ -273,7 +276,7 @@ function readEnvironmentConfig() {
273
276
  }
274
277
  function loadConfigured(config, handoffBase, client) {
275
278
  const handoffPaths = config.handoffPaths.map((path) => resolve(handoffBase, path));
276
- const handoffRelativePaths = config.handoffPaths.flatMap((path) => {
279
+ const handoffRelativePaths = [CANONICAL_CONTRACT_HANDOFF, ...config.handoffPaths].flatMap((path) => {
277
280
  try {
278
281
  return [normalizeRelativePath(path)];
279
282
  }
@@ -558,37 +561,6 @@ function taskContractText(text) {
558
561
  }
559
562
  return text;
560
563
  }
561
- function taskValidationCommand(text) {
562
- const values = taskInlineValues(text, ["validation"]);
563
- if (values.length === 1) {
564
- const value = values[0];
565
- const start = /(?:^|[{,])\s*command\s*:\s*/iu.exec(value);
566
- if (start !== null) {
567
- const remainder = value.slice(start.index + start[0].length);
568
- const end = remainder.search(/,\s*diagnostics\s*:/iu);
569
- if (end >= 0)
570
- return unquoteValue(remainder.slice(0, end).trim());
571
- }
572
- }
573
- const lines = text.split(/\r?\n/u);
574
- const header = lines.findIndex((line) => /^\s*validation\s*:\s*$/iu.test(line));
575
- if (header < 0)
576
- return undefined;
577
- const baseIndent = /^\s*/u.exec(lines[header])[0].replaceAll("\t", " ").length;
578
- for (let index = header + 1; index < lines.length; index += 1) {
579
- const line = lines[index];
580
- if (line.trim().length === 0)
581
- continue;
582
- const indent = /^\s*/u.exec(line)[0].replaceAll("\t", " ").length;
583
- if (indent <= baseIndent)
584
- break;
585
- const match = HANDOFF_ENTRY.exec(line);
586
- if (match !== null && (match[2] ?? match[3]).toLowerCase() === "command") {
587
- return unquoteValue(unwrapMarkdownValue(match[4]));
588
- }
589
- }
590
- return undefined;
591
- }
592
564
  function isBlockTaskHandoff(text) {
593
565
  const inline = (keys) => taskInlineValues(text, keys);
594
566
  const present = (keys) => inline(keys).length === 1 || taskBlockHasContent(text, keys);
@@ -872,7 +844,6 @@ export const SortieDogsPlugin = async (input, options) => {
872
844
  (sessionID) => isCoordinatorSession(sessionID)
873
845
  ? { agent: COORDINATOR_AGENT, parentID: undefined }
874
846
  : undefined);
875
- const normalizedCoordinatorDrifts = new Set();
876
847
  const completedCoordinatorMessages = new Set();
877
848
  const completedCoordinatorParts = new Set();
878
849
  async function ensureLoaded() {
@@ -955,6 +926,7 @@ export const SortieDogsPlugin = async (input, options) => {
955
926
  const coordinatorRoots = new Map();
956
927
  const bootstrapIdleWarnings = new Set();
957
928
  const coordinatorTaskCalls = new Map();
929
+ const chatTransitions = new Map();
958
930
  const reflectionOwnedRoots = new Set();
959
931
  const reflectionClosingRoots = new Set();
960
932
  const reflectionInFlight = new Map();
@@ -962,6 +934,7 @@ export const SortieDogsPlugin = async (input, options) => {
962
934
  const bindingDenials = new Map();
963
935
  const expiredSessions = new Set();
964
936
  const sessionParents = new Map();
937
+ const knownChildSessions = new Set();
965
938
  const sessionRoots = new Map();
966
939
  const sessionTaskIDs = new Map();
967
940
  const recoverableWorkerChildren = new Set();
@@ -1024,12 +997,23 @@ export const SortieDogsPlugin = async (input, options) => {
1024
997
  let manifests = 0;
1025
998
  let handoffs = 0;
1026
999
  for (const absolute of absolutes) {
1027
- if (!await project.contains(absolute) || dirname(absolute) !== project.root)
1000
+ const relativePath = relative(project.root, absolute).replaceAll("\\", "/");
1001
+ const canonical = relativePath.startsWith(`${CANONICAL_CONTRACT_DIRECTORY}/`) &&
1002
+ relativePath.split("/").length === 3;
1003
+ if (!await project.contains(absolute) || (!canonical && dirname(absolute) !== project.root))
1028
1004
  return false;
1029
1005
  const name = basename(absolute);
1030
- if (name === "operation-manifest.json" || name.endsWith(".operation-manifest.json"))
1006
+ if (canonical) {
1007
+ if (/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.operation-manifest\.json$/u.test(name))
1008
+ manifests += 1;
1009
+ else if (/^handoff\.[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.json$/u.test(name))
1010
+ handoffs += 1;
1011
+ else
1012
+ return false;
1013
+ }
1014
+ else if (name === "operation-manifest.json" || name.endsWith(".operation-manifest.json"))
1031
1015
  manifests += 1;
1032
- else if (name === "handoff.json" || (/^handoff[.-].+\.json$/u.test(name)))
1016
+ else if (name === "handoff.json" || /^handoff[.-].+\.json$/u.test(name))
1033
1017
  handoffs += 1;
1034
1018
  else
1035
1019
  return false;
@@ -1184,16 +1168,44 @@ export const SortieDogsPlugin = async (input, options) => {
1184
1168
  }
1185
1169
  function parallelControlPaths(descriptor) {
1186
1170
  return {
1187
- handoff_path: join(descriptor.managed_path, `handoff.${descriptor.task_id}.json`),
1188
- operation_manifest: join(descriptor.managed_path, `${descriptor.task_id}.operation-manifest.json`),
1171
+ handoff_path: join(descriptor.managed_path, CANONICAL_CONTRACT_DIRECTORY, `handoff.${descriptor.task_id}.json`),
1172
+ operation_manifest: join(descriptor.managed_path, CANONICAL_CONTRACT_DIRECTORY, `${descriptor.task_id}.operation-manifest.json`),
1189
1173
  };
1190
1174
  }
1175
+ async function ensureParallelContractDirectory(canonicalRoot) {
1176
+ let parent = canonicalRoot;
1177
+ for (const segment of [".sortie-dogs", "contracts"]) {
1178
+ const candidate = join(parent, segment);
1179
+ let info = await lstat(candidate).catch((error) => {
1180
+ if (isRecord(error) && error.code === "ENOENT")
1181
+ return undefined;
1182
+ throw error;
1183
+ });
1184
+ if (info === undefined) {
1185
+ await mkdir(candidate, { mode: 0o700 }).catch((error) => {
1186
+ if (!isRecord(error) || error.code !== "EEXIST")
1187
+ throw error;
1188
+ });
1189
+ info = await lstat(candidate);
1190
+ }
1191
+ if (!info.isDirectory() || info.isSymbolicLink() || !samePath(await realpath(candidate), candidate)) {
1192
+ throw new ParallelDispatchError("lifecycle-failed", "Parallel contract directory is not a safe managed-worktree directory.");
1193
+ }
1194
+ parent = candidate;
1195
+ }
1196
+ return parent;
1197
+ }
1191
1198
  async function createParallelControlFiles(descriptor, validationCommands) {
1192
1199
  const canonicalRoot = await realpath(descriptor.managed_path);
1193
1200
  if (!samePath(canonicalRoot, descriptor.managed_path)) {
1194
1201
  throw new ParallelDispatchError("lifecycle-failed", "Managed worktree identity changed before contract creation.");
1195
1202
  }
1196
1203
  const paths = parallelControlPaths(descriptor);
1204
+ const contractDirectory = await ensureParallelContractDirectory(canonicalRoot);
1205
+ const expectedContractDirectory = resolve(canonicalRoot, CANONICAL_CONTRACT_DIRECTORY);
1206
+ if (!samePath(contractDirectory, expectedContractDirectory)) {
1207
+ throw new ParallelDispatchError("lifecycle-failed", "Parallel contract directory escapes the managed worktree.");
1208
+ }
1197
1209
  const manifestValue = {
1198
1210
  version: "0.1.0",
1199
1211
  task_id: descriptor.task_id,
@@ -1207,7 +1219,7 @@ export const SortieDogsPlugin = async (input, options) => {
1207
1219
  id: descriptor.task_id,
1208
1220
  created_at: new Date().toISOString(),
1209
1221
  ext: { "sortie-dogs/write-gate": {
1210
- operation_manifest: basename(paths.operation_manifest),
1222
+ operation_manifest: relative(descriptor.managed_path, paths.operation_manifest).replaceAll("\\", "/"),
1211
1223
  project_root: descriptor.managed_path,
1212
1224
  } },
1213
1225
  task: {
@@ -1279,6 +1291,13 @@ export const SortieDogsPlugin = async (input, options) => {
1279
1291
  const validationCommands = loaded?.manifest?.validation ?? [];
1280
1292
  await Promise.all(snapshot.ready.map((descriptor) => createParallelControlFiles(descriptor, validationCommands)));
1281
1293
  }
1294
+ async function restoreActiveParallelControls(snapshot) {
1295
+ await ensureLoaded();
1296
+ const validationCommands = loaded?.manifest?.validation ?? [];
1297
+ await Promise.all(snapshot.tasks
1298
+ .filter(({ phase }) => phase === "pending" || phase === "reserved" || phase === "running")
1299
+ .map(({ descriptor }) => createParallelControlFiles(descriptor, validationCommands)));
1300
+ }
1282
1301
  function boundedParallelSnapshot(snapshot) {
1283
1302
  return {
1284
1303
  run_id: snapshot.run_id,
@@ -1465,10 +1484,18 @@ export const SortieDogsPlugin = async (input, options) => {
1465
1484
  return JSON.stringify({ status: "denied", reason: "coordinator-root-required" });
1466
1485
  const coordinator = await getParallelCoordinator();
1467
1486
  const active = await coordinator.snapshot(ownerRoot, runID || undefined);
1468
- const snapshot = await coordinator.cancel(ownerRoot, runID || undefined);
1469
- if (active !== undefined && snapshot !== undefined) {
1487
+ if (active !== undefined) {
1470
1488
  await Promise.all(active.tasks.filter(({ phase }) => phase === "pending" || phase === "reserved")
1471
- .map(({ descriptor }) => removeParallelControlFiles(descriptor).catch(() => undefined)));
1489
+ .map(({ descriptor }) => removeParallelControlFiles(descriptor)));
1490
+ }
1491
+ let snapshot;
1492
+ try {
1493
+ snapshot = await coordinator.cancel(ownerRoot, runID || undefined);
1494
+ }
1495
+ catch (error) {
1496
+ if (active !== undefined)
1497
+ await ensureParallelReadyControls(active);
1498
+ throw error;
1472
1499
  }
1473
1500
  return snapshot === undefined
1474
1501
  ? JSON.stringify({ status: "absent" })
@@ -1481,6 +1508,54 @@ export const SortieDogsPlugin = async (input, options) => {
1481
1508
  });
1482
1509
  }
1483
1510
  }
1511
+ async function retireParallelWorkflow(sessionID) {
1512
+ if (parallelCoordinator === undefined) {
1513
+ project ??= await createProjectPaths(resolveProjectRoot(input));
1514
+ const gitMarker = await lstat(join(project.root, ".git")).catch((error) => {
1515
+ if (isRecord(error) && error.code === "ENOENT")
1516
+ return undefined;
1517
+ throw error;
1518
+ });
1519
+ if (gitMarker === undefined)
1520
+ return;
1521
+ }
1522
+ let coordinator = parallelCoordinator;
1523
+ if (coordinator === undefined) {
1524
+ try {
1525
+ coordinator = await getParallelCoordinator();
1526
+ }
1527
+ catch (error) {
1528
+ if (error instanceof WorktreeLifecycleError && error.code === "invalid-repository" &&
1529
+ error.message === "Repository root is not the primary checkout.")
1530
+ return;
1531
+ throw error;
1532
+ }
1533
+ }
1534
+ const active = await coordinator.snapshot(sessionID);
1535
+ if (active === undefined || active.archived)
1536
+ return;
1537
+ await Promise.all(active.tasks.map(({ descriptor }) => removeParallelControlFiles(descriptor)));
1538
+ try {
1539
+ await coordinator.cancel(sessionID, active.run_id);
1540
+ }
1541
+ catch (error) {
1542
+ await restoreActiveParallelControls(active);
1543
+ throw error;
1544
+ }
1545
+ await coordinator.reconcile(sessionID, new Set(), active.run_id);
1546
+ }
1547
+ async function serializeChatTransition(sessionID, operation) {
1548
+ const previous = chatTransitions.get(sessionID) ?? Promise.resolve();
1549
+ const current = previous.catch(() => undefined).then(operation);
1550
+ chatTransitions.set(sessionID, current);
1551
+ try {
1552
+ await current;
1553
+ }
1554
+ finally {
1555
+ if (chatTransitions.get(sessionID) === current)
1556
+ chatTransitions.delete(sessionID);
1557
+ }
1558
+ }
1484
1559
  function abandonSessionLease(sessionID) {
1485
1560
  const authorization = sessionAuthorizations.get(sessionID);
1486
1561
  if (authorization?.lease !== undefined) {
@@ -1489,10 +1564,13 @@ export const SortieDogsPlugin = async (input, options) => {
1489
1564
  }
1490
1565
  }
1491
1566
  async function completeContinuationText(sessionID, text) {
1567
+ const continuationText = fastLane.manualCompactionForbidden(sessionID)
1568
+ ? text.replaceAll(ROLLOVER_MARKER, "").replaceAll(CONTINUATION_MARKER, "").trimEnd()
1569
+ : text;
1492
1570
  await continuation.textComplete({
1493
1571
  sessionID,
1494
1572
  allowCheckpointContinuation: fastLane.backlogContinuationAllowed(sessionID),
1495
- }, { text });
1573
+ }, { text: continuationText });
1496
1574
  if (fastLane.backlogContinuationAllowed(sessionID) && continuation.blocksTool(sessionID)) {
1497
1575
  fastLane.continuationQueued(sessionID);
1498
1576
  }
@@ -2363,6 +2441,11 @@ export const SortieDogsPlugin = async (input, options) => {
2363
2441
  return await lineageProject.contains(root) ? rootSessionID : undefined;
2364
2442
  }
2365
2443
  function rememberParent(sessionID, parentID) {
2444
+ knownChildSessions.delete(sessionID);
2445
+ knownChildSessions.add(sessionID);
2446
+ while (knownChildSessions.size > ACTIVE_SESSION_CACHE.maximum) {
2447
+ knownChildSessions.delete(knownChildSessions.values().next().value);
2448
+ }
2366
2449
  sessionParents.delete(sessionID);
2367
2450
  sessionParents.set(sessionID, parentID);
2368
2451
  }
@@ -2807,7 +2890,6 @@ export const SortieDogsPlugin = async (input, options) => {
2807
2890
  .replaceAll(ROLLOVER_MARKER, "")
2808
2891
  .replaceAll(CONTINUATION_MARKER, "")
2809
2892
  .trimEnd();
2810
- return;
2811
2893
  }
2812
2894
  await completeContinuationText(textInput.sessionID, textOutput.text);
2813
2895
  },
@@ -2818,147 +2900,110 @@ export const SortieDogsPlugin = async (input, options) => {
2818
2900
  await continuation.compactionAutoContinue(autoInput, autoOutput);
2819
2901
  },
2820
2902
  "chat.message": async (chatInput, output) => {
2821
- const parentID = chatParentID(chatInput);
2822
- const synthetic = output.parts.some((part) => isRecord(part) && part.synthetic === true);
2823
- if (parentID !== undefined)
2824
- rememberParent(chatInput.sessionID, parentID);
2825
- const coordinatorRoot = isCoordinatorSession(chatInput.sessionID);
2826
- const requestedCoordinator = chatInput.agent === COORDINATOR_AGENT || output.message.agent === COORDINATOR_AGENT;
2827
- if (requestedCoordinator && !coordinatorRoot) {
2828
- const explicitChild = parentID !== undefined || sessionParents.has(chatInput.sessionID);
2829
- const identity = input.client?.session?.get === undefined
2830
- ? undefined
2831
- : await hostSessionIdentity(chatInput.sessionID);
2832
- const foreignRoot = identity?.agent !== undefined && identity.agent !== COORDINATOR_AGENT;
2833
- const foreignHistoryAgent = await hostSessionForeignUserAgent(chatInput.sessionID);
2834
- const foreignHistory = typeof foreignHistoryAgent === "string" ? true : foreignHistoryAgent;
2835
- const unverifiableRoot = (input.client?.session?.get !== undefined && (identity === undefined ||
2836
- (identity.agent === undefined && input.client?.session?.messages === undefined))) ||
2837
- (input.client?.session?.messages !== undefined && foreignHistory === undefined);
2838
- const currentAgent = !explicitChild && identity?.parentPresent === false
2839
- ? typeof foreignHistoryAgent === "string"
2840
- ? foreignHistoryAgent
2841
- : foreignRoot
2842
- ? identity.agent
2843
- : undefined
2844
- : undefined;
2845
- if (currentAgent !== undefined) {
2846
- const noticePart = output.parts.find((part) => isRecord(part) && part.type === "text");
2847
- if (!isRecord(noticePart)) {
2848
- throw new Error("SORTIE_FRESH_SESSION_REQUIRED: /sortie requires a fresh root dog-coordinator session; it cannot convert another agent or promote a child");
2903
+ await serializeChatTransition(chatInput.sessionID, async () => {
2904
+ const parentID = chatParentID(chatInput);
2905
+ const synthetic = output.parts.some((part) => isRecord(part) && part.synthetic === true);
2906
+ if (parentID !== undefined)
2907
+ rememberParent(chatInput.sessionID, parentID);
2908
+ const coordinatorRoot = isCoordinatorSession(chatInput.sessionID);
2909
+ const selectedAgent = chatInput.agent ?? output.message.agent;
2910
+ if (chatInput.agent !== undefined && output.message.agent !== chatInput.agent) {
2911
+ output.message.agent = chatInput.agent;
2912
+ }
2913
+ const requestedCoordinator = selectedAgent === COORDINATOR_AGENT;
2914
+ if (requestedCoordinator && !coordinatorRoot) {
2915
+ const explicitChild = parentID !== undefined || knownChildSessions.has(chatInput.sessionID);
2916
+ const identity = input.client?.session?.get === undefined
2917
+ ? undefined
2918
+ : await hostSessionIdentity(chatInput.sessionID);
2919
+ if (explicitChild || identity?.parentPresent === true) {
2920
+ throw new Error("SORTIE_FRESH_SESSION_REQUIRED: /sortie cannot promote a child session to a root dog-coordinator");
2849
2921
  }
2850
- chatInput.agent = currentAgent;
2851
- output.message.agent = currentAgent;
2852
- noticePart.text = "SORTIE_FRESH_SESSION_REQUIRED. Do not perform the requested task or use tools. " +
2853
- `Explain only that /sortie must start in a new session because this session belongs to ${currentAgent}.`;
2854
- output.parts.splice(0, output.parts.length, noticePart);
2855
- await input.client?.tui?.showToast?.({ body: {
2856
- title: "Sortie-dogs",
2857
- message: `/sortie requires a new session. This session remains ${currentAgent}.`,
2858
- variant: "warning",
2859
- duration: 8000,
2860
- } }).catch(() => undefined);
2861
- return;
2862
- }
2863
- if (explicitChild || identity?.parentPresent === true ||
2864
- foreignHistory || unverifiableRoot) {
2865
- throw new Error("SORTIE_FRESH_SESSION_REQUIRED: /sortie requires a fresh root dog-coordinator session; it cannot convert another agent or promote a child");
2866
- }
2867
- }
2868
- const normalizeCoordinatorDrift = coordinatorRoot && !requestedCoordinator;
2869
- if (normalizeCoordinatorDrift) {
2870
- /*
2871
- * OpenCode can switch both agent and model when the user only intends to hand the root from
2872
- * Terra to Sol. Letting that agent drift stand silently discards the coordinator prompt and
2873
- * every in-memory continuation guard. Keep the selected model, but make the established root
2874
- * role sticky; an actual workflow switch belongs in a new session.
2875
- */
2876
- chatInput.agent = COORDINATOR_AGENT;
2877
- output.message.agent = COORDINATOR_AGENT;
2878
- if (!normalizedCoordinatorDrifts.has(chatInput.sessionID)) {
2879
- normalizedCoordinatorDrifts.add(chatInput.sessionID);
2880
- console.warn(`[sortie-coordinator] normalized agent drift for ${chatInput.sessionID}; selected model preserved`);
2881
- }
2882
- }
2883
- const coordinatorOrigin = parentID === undefined && (requestedCoordinator || normalizeCoordinatorDrift);
2884
- if (coordinatorOrigin) {
2885
- fastLane.beginTurn(chatInput.sessionID, synthetic);
2886
- releaseSessionEnforcement(chatInput.sessionID);
2887
- await rememberCoordinatorRoot(chatInput.sessionID);
2888
- }
2889
- else {
2890
- if (!synthetic && isCoordinatorSession(chatInput.sessionID)) {
2891
2922
  fastLane.forget(chatInput.sessionID);
2892
2923
  abortCoordinatorTasks(chatInput.sessionID);
2893
2924
  continuation.forgetSession(chatInput.sessionID);
2925
+ await retireParallelWorkflow(chatInput.sessionID);
2894
2926
  evictSession(chatInput.sessionID);
2895
2927
  }
2896
- const taskText = explicitTaskText(output);
2897
- if (taskText !== undefined) {
2898
- const taskID = handoffValue(handoffEntries(taskText), ["task_id"]);
2899
- if (taskID !== undefined)
2900
- sessionTaskIDs.set(chatInput.sessionID, unquoteValue(taskID));
2901
- }
2902
- let inheritedRoot = taskText === undefined
2903
- ? undefined
2904
- : await inheritedTaskRoot(chatInput.sessionID, taskText);
2905
- if (taskText !== undefined && inheritedRoot === undefined) {
2906
- await recoverCoordinatorLineage(chatInput.sessionID);
2907
- inheritedRoot = await inheritedTaskRoot(chatInput.sessionID, taskText);
2908
- }
2909
- if (inheritedRoot !== undefined) {
2910
- sessionRoots.set(chatInput.sessionID, inheritedRoot);
2911
- activateSession(chatInput.sessionID, parallelTaskMode(taskText));
2912
- const descriptor = parallelDescriptor(taskText);
2913
- const recordedAgent = chatInput.agent ?? output.message.agent;
2914
- const matchingCall = descriptor === undefined ? undefined : [...parallelCalls.values()].find((call) => call.ownerRoot === inheritedRoot && sameParallelDescriptor(call.descriptor, descriptor));
2915
- if (descriptor !== undefined && recordedAgent === "dog-worker" && matchingCall !== undefined &&
2916
- samePath(taskProjectRoot(taskText) ?? "", descriptor.managed_path) && sessionParents.get(chatInput.sessionID) === inheritedRoot) {
2917
- parallelChildBindings.set(chatInput.sessionID, {
2918
- ownerRoot: inheritedRoot,
2919
- descriptor,
2920
- completionCallID: matchingCall.completionCallID,
2921
- });
2922
- pruneParallelChildMap(parallelChildBindings);
2928
+ const coordinatorOrigin = parentID === undefined && requestedCoordinator;
2929
+ if (coordinatorOrigin) {
2930
+ fastLane.beginTurn(chatInput.sessionID, synthetic);
2931
+ releaseSessionEnforcement(chatInput.sessionID);
2932
+ await rememberCoordinatorRoot(chatInput.sessionID);
2933
+ }
2934
+ else {
2935
+ if (!synthetic && isCoordinatorSession(chatInput.sessionID)) {
2936
+ fastLane.forget(chatInput.sessionID);
2937
+ abortCoordinatorTasks(chatInput.sessionID);
2938
+ continuation.forgetSession(chatInput.sessionID);
2939
+ await retireParallelWorkflow(chatInput.sessionID);
2940
+ evictSession(chatInput.sessionID);
2923
2941
  }
2924
- else {
2925
- parallelChildBindings.delete(chatInput.sessionID);
2942
+ const taskText = explicitTaskText(output);
2943
+ if (taskText !== undefined) {
2944
+ const taskID = handoffValue(handoffEntries(taskText), ["task_id"]);
2945
+ if (taskID !== undefined)
2946
+ sessionTaskIDs.set(chatInput.sessionID, unquoteValue(taskID));
2926
2947
  }
2948
+ let inheritedRoot = taskText === undefined
2949
+ ? undefined
2950
+ : await inheritedTaskRoot(chatInput.sessionID, taskText);
2951
+ if (taskText !== undefined && inheritedRoot === undefined) {
2952
+ await recoverCoordinatorLineage(chatInput.sessionID);
2953
+ inheritedRoot = await inheritedTaskRoot(chatInput.sessionID, taskText);
2954
+ }
2955
+ if (inheritedRoot !== undefined) {
2956
+ sessionRoots.set(chatInput.sessionID, inheritedRoot);
2957
+ activateSession(chatInput.sessionID, parallelTaskMode(taskText));
2958
+ const descriptor = parallelDescriptor(taskText);
2959
+ const recordedAgent = chatInput.agent ?? output.message.agent;
2960
+ const matchingCall = descriptor === undefined ? undefined : [...parallelCalls.values()].find((call) => call.ownerRoot === inheritedRoot && sameParallelDescriptor(call.descriptor, descriptor));
2961
+ if (descriptor !== undefined && recordedAgent === "dog-worker" && matchingCall !== undefined &&
2962
+ samePath(taskProjectRoot(taskText) ?? "", descriptor.managed_path) && sessionParents.get(chatInput.sessionID) === inheritedRoot) {
2963
+ parallelChildBindings.set(chatInput.sessionID, {
2964
+ ownerRoot: inheritedRoot,
2965
+ descriptor,
2966
+ completionCallID: matchingCall.completionCallID,
2967
+ });
2968
+ pruneParallelChildMap(parallelChildBindings);
2969
+ }
2970
+ else {
2971
+ parallelChildBindings.delete(chatInput.sessionID);
2972
+ }
2973
+ }
2974
+ else if (activatesSession(chatInput, output)) {
2975
+ activateSession(chatInput.sessionID, taskText === undefined ? "none" : parallelTaskMode(taskText));
2976
+ }
2977
+ touchActiveSession(chatInput.sessionID);
2927
2978
  }
2928
- else if (activatesSession(chatInput, output)) {
2929
- activateSession(chatInput.sessionID, taskText === undefined ? "none" : parallelTaskMode(taskText));
2930
- }
2931
- touchActiveSession(chatInput.sessionID);
2932
- }
2933
- /*
2934
- * Role routing is a dispatch policy, not a write-gate concern. Consultation and evidence roles
2935
- * never activate the write gate, so gating routing on session activation left every one of
2936
- * them silently inheriting the caller's model instead of its own configured route.
2937
- */
2938
- await ensureLoaded();
2939
- const consultationFallbackRetry = await reserveConsultationFallbackRetry(chatInput, output);
2940
- try {
2941
- const routed = normalizeCoordinatorDrift
2942
- ? false
2943
- : await loaded?.modelRoutingHook?.(chatInput, output, {
2979
+ /*
2980
+ * Role routing is a dispatch policy, not a write-gate concern. Consultation and evidence roles
2981
+ * never activate the write gate, so gating routing on session activation left every one of
2982
+ * them silently inheriting the caller's model instead of its own configured route.
2983
+ */
2984
+ await ensureLoaded();
2985
+ const consultationFallbackRetry = await reserveConsultationFallbackRetry(chatInput, output);
2986
+ try {
2987
+ const routed = await loaded?.modelRoutingHook?.(chatInput, output, {
2944
2988
  skipPreferred: consultationFallbackRetry !== undefined,
2945
2989
  });
2946
- if (consultationFallbackRetry !== undefined && routed === true) {
2947
- consultationRetries.set(consultationFallbackRetry.key, {
2948
- phase: "consumed",
2949
- retryChildSessionID: consultationFallbackRetry.childSessionID,
2950
- });
2990
+ if (consultationFallbackRetry !== undefined && routed === true) {
2991
+ consultationRetries.set(consultationFallbackRetry.key, {
2992
+ phase: "consumed",
2993
+ retryChildSessionID: consultationFallbackRetry.childSessionID,
2994
+ });
2995
+ }
2951
2996
  }
2952
- }
2953
- finally {
2954
- if (consultationFallbackRetry !== undefined &&
2955
- consultationRetries.get(consultationFallbackRetry.key)?.phase === "routing") {
2956
- consultationRetries.set(consultationFallbackRetry.key, { phase: "pending" });
2997
+ finally {
2998
+ if (consultationFallbackRetry !== undefined &&
2999
+ consultationRetries.get(consultationFallbackRetry.key)?.phase === "routing") {
3000
+ consultationRetries.set(consultationFallbackRetry.key, { phase: "pending" });
3001
+ }
2957
3002
  }
2958
- }
2959
- if (coordinatorOrigin) {
2960
- continuation.observeModel(chatInput.sessionID, output.message.model, synthetic);
2961
- }
3003
+ if (coordinatorOrigin) {
3004
+ continuation.observeModel(chatInput.sessionID, output.message.model, synthetic);
3005
+ }
3006
+ });
2962
3007
  },
2963
3008
  "experimental.chat.system.transform": async (transformInput, transformOutput) => {
2964
3009
  if (fastLane.terminalInstructionRequired(transformInput.sessionID)) {
@@ -3306,13 +3351,6 @@ export const SortieDogsPlugin = async (input, options) => {
3306
3351
  defects: [contractDefect("contract", "/", "dispatch_identity_mismatch")],
3307
3352
  });
3308
3353
  }
3309
- const validationCommand = taskValidationCommand(contractPrompt);
3310
- if (validationCommand !== undefined &&
3311
- !identity.validationCommands.has(normalizeCommand(validationCommand))) {
3312
- throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
3313
- defects: [contractDefect("contract", "/validation/command", "dispatch_validation_manifest_mismatch")],
3314
- });
3315
- }
3316
3354
  }
3317
3355
  }
3318
3356
  if (reservedParallelDescriptor !== undefined) {
@@ -3453,7 +3491,6 @@ export const SortieDogsPlugin = async (input, options) => {
3453
3491
  return;
3454
3492
  const eventPartTime = isRecord(eventPart?.time) ? eventPart.time : undefined;
3455
3493
  if (event.type === "message.part.updated" && isCoordinatorSession(eventSessionID) &&
3456
- !fastLane.manualCompactionForbidden(eventSessionID) &&
3457
3494
  eventPart?.type === "text" && typeof eventPart.text === "string" && eventPart.text.trim().length > 0 &&
3458
3495
  typeof eventPartTime?.end === "number" && typeof eventPart.id === "string" &&
3459
3496
  typeof eventPart.messageID === "string" &&
@@ -3489,7 +3526,6 @@ export const SortieDogsPlugin = async (input, options) => {
3489
3526
  return;
3490
3527
  }
3491
3528
  if (event.type === "message.updated" && isCoordinatorSession(eventSessionID) &&
3492
- !fastLane.manualCompactionForbidden(eventSessionID) &&
3493
3529
  info?.role === "assistant" && info.agent === COORDINATOR_AGENT && isRecord(info.time) &&
3494
3530
  typeof info.time.completed === "number" && typeof info.id === "string" &&
3495
3531
  !completedCoordinatorMessages.has(info.id)) {
@@ -3519,7 +3555,6 @@ export const SortieDogsPlugin = async (input, options) => {
3519
3555
  return;
3520
3556
  }
3521
3557
  if (event.type === "session.deleted") {
3522
- normalizedCoordinatorDrifts.delete(eventSessionID);
3523
3558
  fastLane.forget(eventSessionID);
3524
3559
  abortCoordinatorTasks(eventSessionID);
3525
3560
  if (reflectionStore !== undefined && reflectionConfiguration?.layers.run && reflectionOwnedRoots.has(eventSessionID)) {
@@ -3556,6 +3591,7 @@ export const SortieDogsPlugin = async (input, options) => {
3556
3591
  }
3557
3592
  }
3558
3593
  evictSession(eventSessionID);
3594
+ knownChildSessions.delete(eventSessionID);
3559
3595
  for (const role of [REVIEWER_AGENT, ADVISOR_AGENT]) {
3560
3596
  const key = consultationRetryKey(eventSessionID, role);
3561
3597
  consultationRetries.delete(key);