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.
- package/README.md +9 -3
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/worktree-commit-artifact.js +2 -0
- package/dist/plugin/continuation.js +80 -22
- package/dist/plugin/gate.js +96 -2
- package/dist/plugin/index.js +219 -183
- package/dist/runtime-assets.d.ts +8 -8
- package/dist/runtime-assets.js +38 -33
- package/package.json +1 -1
package/dist/plugin/index.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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" ||
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
(
|
|
2838
|
-
|
|
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
|
|
2897
|
-
if (
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
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
|
-
|
|
2925
|
-
|
|
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
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
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
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
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
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
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
|
-
|
|
2960
|
-
|
|
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);
|