sortie-dogs 0.5.15 → 0.5.17
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
CHANGED
|
@@ -20,7 +20,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
20
20
|
|
|
21
21
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
22
22
|
|
|
23
|
-
Release: [v0.5.
|
|
23
|
+
Release: [v0.5.17](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.5.17)
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
@@ -13,6 +13,7 @@ const MAX_OUTPUT = 1024 * 1024;
|
|
|
13
13
|
const MAX_TIMEOUT = 10 * 60_000;
|
|
14
14
|
const GIT_TIMEOUT = 30_000;
|
|
15
15
|
const EXIT_GRACE = 500;
|
|
16
|
+
let systemdUnitSequence = 0;
|
|
16
17
|
const KILL_WAIT = 2_000;
|
|
17
18
|
const WINDOWS_WRAPPER_GRACE = 15_000;
|
|
18
19
|
const LINUX_WRAPPER_GRACE = 10_000;
|
|
@@ -483,9 +484,163 @@ async function terminateTree(child, closed) {
|
|
|
483
484
|
return false;
|
|
484
485
|
};
|
|
485
486
|
if (!(await groupGone())) {
|
|
486
|
-
|
|
487
|
+
try {
|
|
488
|
+
process.kill(-child.pid, "SIGKILL");
|
|
489
|
+
}
|
|
490
|
+
catch { /* Already closed. */ }
|
|
491
|
+
if (!(await groupGone())) {
|
|
492
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Process-tree termination could not be confirmed.");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async function linuxSystemdRun() {
|
|
498
|
+
for (const candidate of ["/usr/bin/systemd-run", "/bin/systemd-run"]) {
|
|
499
|
+
const canonical = await realpath(candidate).catch(() => undefined);
|
|
500
|
+
if (canonical === undefined || !canonical.startsWith("/usr/bin/") && !canonical.startsWith("/bin/"))
|
|
501
|
+
continue;
|
|
502
|
+
const info = await lstat(canonical).catch(() => undefined);
|
|
503
|
+
if (info !== undefined && info.isFile() && !info.isSymbolicLink() && info.uid === 0 && (info.mode & 0o111) !== 0)
|
|
504
|
+
return canonical;
|
|
505
|
+
}
|
|
506
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation containment setup failed.");
|
|
507
|
+
}
|
|
508
|
+
async function linuxSystemctl() {
|
|
509
|
+
for (const candidate of ["/usr/bin/systemctl", "/bin/systemctl"]) {
|
|
510
|
+
const canonical = await realpath(candidate).catch(() => undefined);
|
|
511
|
+
if (canonical === undefined || !canonical.startsWith("/usr/bin/") && !canonical.startsWith("/bin/"))
|
|
512
|
+
continue;
|
|
513
|
+
const info = await lstat(canonical).catch(() => undefined);
|
|
514
|
+
if (info !== undefined && info.isFile() && !info.isSymbolicLink() && info.uid === 0 && (info.mode & 0o111) !== 0)
|
|
515
|
+
return canonical;
|
|
516
|
+
}
|
|
517
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation containment setup failed.");
|
|
518
|
+
}
|
|
519
|
+
async function linuxEnvironmentExecutable() {
|
|
520
|
+
for (const candidate of ["/usr/bin/env", "/bin/env"]) {
|
|
521
|
+
const canonical = await realpath(candidate).catch(() => undefined);
|
|
522
|
+
if (canonical === undefined || !canonical.startsWith("/usr/bin/") && !canonical.startsWith("/bin/"))
|
|
523
|
+
continue;
|
|
524
|
+
const info = await lstat(canonical).catch(() => undefined);
|
|
525
|
+
if (info !== undefined && info.isFile() && !info.isSymbolicLink() && info.uid === 0 && (info.mode & 0o111) !== 0)
|
|
526
|
+
return canonical;
|
|
527
|
+
}
|
|
528
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation containment setup failed.");
|
|
529
|
+
}
|
|
530
|
+
async function linuxShell() {
|
|
531
|
+
for (const candidate of ["/bin/sh", "/usr/bin/sh"]) {
|
|
532
|
+
const canonical = await realpath(candidate).catch(() => undefined);
|
|
533
|
+
if (canonical === undefined || !canonical.startsWith("/usr/bin/") && !canonical.startsWith("/bin/"))
|
|
534
|
+
continue;
|
|
535
|
+
const info = await lstat(canonical).catch(() => undefined);
|
|
536
|
+
if (info !== undefined && info.isFile() && !info.isSymbolicLink() && info.uid === 0 && (info.mode & 0o111) !== 0)
|
|
537
|
+
return canonical;
|
|
538
|
+
}
|
|
539
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation containment setup failed.");
|
|
540
|
+
}
|
|
541
|
+
async function stopSystemdUnit(unit, environment) {
|
|
542
|
+
const systemctl = await linuxSystemctl();
|
|
543
|
+
const invoke = async (args) => {
|
|
544
|
+
const child = spawn(systemctl, args, { env: environment, shell: false, windowsHide: true, stdio: "ignore" });
|
|
545
|
+
const closed = new Promise((done, reject) => {
|
|
546
|
+
child.once("error", reject);
|
|
547
|
+
child.once("close", (code) => done({ code }));
|
|
548
|
+
});
|
|
549
|
+
if (!(await waitForClose(closed, KILL_WAIT + 1_500))) {
|
|
550
|
+
child.kill("SIGKILL");
|
|
551
|
+
if (!(await waitForClose(closed, KILL_WAIT))) {
|
|
552
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation containment shutdown failed.");
|
|
553
|
+
}
|
|
487
554
|
}
|
|
555
|
+
return (await closed).code;
|
|
556
|
+
};
|
|
557
|
+
await invoke(["--user", "stop", unit]).catch(() => undefined);
|
|
558
|
+
const activeState = async () => {
|
|
559
|
+
const code = await invoke(["--user", "is-active", "--quiet", unit]);
|
|
560
|
+
if (code === 0)
|
|
561
|
+
return true;
|
|
562
|
+
if (code === 3 || code === 4)
|
|
563
|
+
return false;
|
|
564
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation containment shutdown failed.");
|
|
565
|
+
};
|
|
566
|
+
const deadline = Date.now() + KILL_WAIT;
|
|
567
|
+
while (Date.now() < deadline) {
|
|
568
|
+
if (await activeState()) {
|
|
569
|
+
await invoke(["--user", "stop", unit]).catch(() => undefined);
|
|
570
|
+
}
|
|
571
|
+
await new Promise((done) => setTimeout(done, 25));
|
|
572
|
+
}
|
|
573
|
+
if (await activeState()) {
|
|
574
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation containment shutdown failed.");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function runLinuxSystemd(executable, args, cwd, timeout) {
|
|
578
|
+
const systemdRun = await linuxSystemdRun();
|
|
579
|
+
const [environmentExecutable, shell] = await Promise.all([linuxEnvironmentExecutable(), linuxShell()]);
|
|
580
|
+
const uid = process.getuid?.();
|
|
581
|
+
if (uid === undefined)
|
|
582
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation containment setup failed.");
|
|
583
|
+
const runtimeDirectory = `/run/user/${uid}`;
|
|
584
|
+
const environment = {
|
|
585
|
+
...cleanEnvironment(),
|
|
586
|
+
XDG_RUNTIME_DIR: runtimeDirectory,
|
|
587
|
+
DBUS_SESSION_BUS_ADDRESS: `unix:path=${runtimeDirectory}/bus`,
|
|
588
|
+
};
|
|
589
|
+
systemdUnitSequence = (systemdUnitSequence + 1) % Number.MAX_SAFE_INTEGER;
|
|
590
|
+
const unit = `sortie-dogs-${process.pid}-${Date.now()}-${systemdUnitSequence}`;
|
|
591
|
+
const validationEnvironment = Object.entries(cleanEnvironment()).map(([key, value]) => `${key}=${value}`);
|
|
592
|
+
const child = spawn(systemdRun, [
|
|
593
|
+
"--user", "--wait", "--collect", "--quiet", `--unit=${unit}`,
|
|
594
|
+
"--property=KillMode=control-group", `--property=RuntimeMaxSec=${timeout}ms`,
|
|
595
|
+
"--property=TimeoutStopSec=1s", "--working-directory", cwd, "--",
|
|
596
|
+
environmentExecutable, "-i", ...validationEnvironment, shell, "-c",
|
|
597
|
+
'"$@"; code=$?; if [ "$code" -eq 0 ]; then exit 0; else exit 239; fi',
|
|
598
|
+
"sortie-validation", executable, ...args,
|
|
599
|
+
], {
|
|
600
|
+
cwd, env: environment, shell: false, windowsHide: true, detached: true, stdio: ["ignore", "pipe", "pipe"],
|
|
601
|
+
});
|
|
602
|
+
const chunks = [];
|
|
603
|
+
let outputBytes = 0;
|
|
604
|
+
let overflow = false;
|
|
605
|
+
const collect = (chunk) => {
|
|
606
|
+
outputBytes += chunk.byteLength;
|
|
607
|
+
if (outputBytes <= MAX_OUTPUT)
|
|
608
|
+
chunks.push(chunk);
|
|
609
|
+
else
|
|
610
|
+
overflow = true;
|
|
611
|
+
};
|
|
612
|
+
child.stdout.on("data", collect);
|
|
613
|
+
child.stderr.on("data", (chunk) => {
|
|
614
|
+
outputBytes += chunk.byteLength;
|
|
615
|
+
if (outputBytes > MAX_OUTPUT)
|
|
616
|
+
overflow = true;
|
|
617
|
+
});
|
|
618
|
+
const closed = new Promise((done, reject) => {
|
|
619
|
+
child.once("error", reject);
|
|
620
|
+
child.once("close", (code) => done({ code }));
|
|
621
|
+
});
|
|
622
|
+
let timer;
|
|
623
|
+
const bounded = await Promise.race([
|
|
624
|
+
closed.then((result) => ({ kind: "closed", result })),
|
|
625
|
+
new Promise((done) => {
|
|
626
|
+
timer = setTimeout(() => done({ kind: "timeout" }), timeout + LINUX_WRAPPER_GRACE);
|
|
627
|
+
}),
|
|
628
|
+
]).catch(async () => {
|
|
629
|
+
await terminateTree(child, closed).catch(() => undefined);
|
|
630
|
+
await stopSystemdUnit(unit, environment);
|
|
631
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation executable failed.");
|
|
632
|
+
});
|
|
633
|
+
if (timer !== undefined)
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
if (bounded.kind === "timeout" || overflow) {
|
|
636
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
637
|
+
await terminateTree(child, closed);
|
|
638
|
+
await stopSystemdUnit(unit, environment);
|
|
639
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Validation exceeded its resource bound.");
|
|
488
640
|
}
|
|
641
|
+
await stopSystemdUnit(unit, environment);
|
|
642
|
+
const code = bounded.result.code === 0 ? 0 : bounded.result.code === 1 ? 238 : 239;
|
|
643
|
+
return { code, stdout: Buffer.concat(chunks) };
|
|
489
644
|
}
|
|
490
645
|
async function runBounded(executable, args, cwd, timeout, kind) {
|
|
491
646
|
const windowsWrapper = kind === "validation" && process.platform === "win32";
|
|
@@ -557,6 +712,8 @@ async function runBounded(executable, args, cwd, timeout, kind) {
|
|
|
557
712
|
}
|
|
558
713
|
const code = linuxWrapper && result.code !== 0 && ![238, 239, 240, 241].includes(result.code ?? -1)
|
|
559
714
|
? 240 : result.code ?? -1;
|
|
715
|
+
if (linuxWrapper && code === 240)
|
|
716
|
+
return await runLinuxSystemd(executable, args, cwd, timeout);
|
|
560
717
|
return { code, stdout: Buffer.concat(chunks) };
|
|
561
718
|
}
|
|
562
719
|
catch (error) {
|
|
@@ -865,7 +865,9 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
865
865
|
}
|
|
866
866
|
else if (nonTerminalProgress(state.latestCoordinatorReport) ||
|
|
867
867
|
(state.stepRecoveryActive && !terminalCheckpoint(state.latestCoordinatorReport))) {
|
|
868
|
-
|
|
868
|
+
if (input.allowStepRecoveryFallback !== false) {
|
|
869
|
+
scheduleStepRecovery(input.sessionID, state, state.latestCoordinatorReport);
|
|
870
|
+
}
|
|
869
871
|
}
|
|
870
872
|
// The resumed coordinator completed a turn, so no late event from its prior compaction can
|
|
871
873
|
// compete with the next host-managed compaction.
|
package/dist/plugin/index.js
CHANGED
|
@@ -919,6 +919,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
919
919
|
return loaded?.gate === undefined && manifestAbsent;
|
|
920
920
|
}
|
|
921
921
|
const inspected = new Map();
|
|
922
|
+
const inspectionOperations = new Map();
|
|
922
923
|
const sessionAuthorizations = new Map();
|
|
923
924
|
const bindingPins = new Map();
|
|
924
925
|
const bindingOperations = new Set();
|
|
@@ -1383,8 +1384,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1383
1384
|
if (JSON.stringify(running.artifact.validation.command) !== JSON.stringify(requestedCommand)) {
|
|
1384
1385
|
return deny("artifact-replay");
|
|
1385
1386
|
}
|
|
1386
|
-
await (await getParallelCoordinator()).acceptArtifact(binding.ownerRoot, binding.completionCallID, sessionID, binding.descriptor, running.artifact);
|
|
1387
1387
|
await removeParallelControlFiles(binding.descriptor);
|
|
1388
|
+
await (await getParallelCoordinator()).acceptArtifact(binding.ownerRoot, binding.completionCallID, sessionID, binding.descriptor, running.artifact);
|
|
1388
1389
|
parallelArtifacts.set(sessionID, { requestFingerprint: request.fingerprint, artifact: running.artifact });
|
|
1389
1390
|
pruneParallelChildMap(parallelArtifacts);
|
|
1390
1391
|
return JSON.stringify({ status: "created", replay: true, artifact: boundedParallelArtifact(running.artifact) });
|
|
@@ -1397,8 +1398,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1397
1398
|
};
|
|
1398
1399
|
const recovered = await recoverWorktreeCommitArtifact(produceRequest);
|
|
1399
1400
|
const artifact = recovered ?? await produceWorktreeCommitArtifact(produceRequest);
|
|
1400
|
-
await (await getParallelCoordinator()).acceptArtifact(binding.ownerRoot, binding.completionCallID, sessionID, binding.descriptor, artifact);
|
|
1401
1401
|
await removeParallelControlFiles(binding.descriptor);
|
|
1402
|
+
await (await getParallelCoordinator()).acceptArtifact(binding.ownerRoot, binding.completionCallID, sessionID, binding.descriptor, artifact);
|
|
1402
1403
|
parallelArtifacts.set(sessionID, { requestFingerprint: request.fingerprint, artifact });
|
|
1403
1404
|
pruneParallelChildMap(parallelArtifacts);
|
|
1404
1405
|
return JSON.stringify({ status: "created", ...(recovered === undefined ? {} : { replay: true }),
|
|
@@ -1564,13 +1565,14 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1564
1565
|
authorization.lease = undefined;
|
|
1565
1566
|
}
|
|
1566
1567
|
}
|
|
1567
|
-
async function completeContinuationText(sessionID, text) {
|
|
1568
|
+
async function completeContinuationText(sessionID, text, allowStepRecoveryFallback = true) {
|
|
1568
1569
|
const continuationText = fastLane.manualCompactionForbidden(sessionID)
|
|
1569
1570
|
? text.replaceAll(ROLLOVER_MARKER, "").replaceAll(CONTINUATION_MARKER, "").trimEnd()
|
|
1570
1571
|
: text;
|
|
1571
1572
|
await continuation.textComplete({
|
|
1572
1573
|
sessionID,
|
|
1573
1574
|
allowCheckpointContinuation: fastLane.backlogContinuationAllowed(sessionID),
|
|
1575
|
+
allowStepRecoveryFallback,
|
|
1574
1576
|
}, { text: continuationText });
|
|
1575
1577
|
if (fastLane.backlogContinuationAllowed(sessionID) && continuation.blocksTool(sessionID)) {
|
|
1576
1578
|
fastLane.continuationQueued(sessionID);
|
|
@@ -1952,6 +1954,11 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1952
1954
|
return deny("binding-replay");
|
|
1953
1955
|
}
|
|
1954
1956
|
const existingAuthorization = sessionAuthorizations.get(sessionID);
|
|
1957
|
+
const pendingInspections = [...inspectionOperations.entries()]
|
|
1958
|
+
.filter(([key]) => key.startsWith(`${sessionID}\u0000`))
|
|
1959
|
+
.map(([, operation]) => operation);
|
|
1960
|
+
if (pendingInspections.length > 0)
|
|
1961
|
+
await Promise.allSettled(pendingInspections);
|
|
1955
1962
|
pruneInspections(now);
|
|
1956
1963
|
const inspectedEntry = [...inspected.entries()].find(([key, entry]) => key.startsWith(`${sessionID}\u0000`) &&
|
|
1957
1964
|
entry.ownerSessionID === sessionID &&
|
|
@@ -2400,7 +2407,17 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2400
2407
|
const path = input.args.filePath;
|
|
2401
2408
|
if (typeof path !== "string" || path.length === 0)
|
|
2402
2409
|
return;
|
|
2403
|
-
|
|
2410
|
+
const absolutePath = resolve(path);
|
|
2411
|
+
const key = `${input.sessionID}\u0000${absolutePath}`;
|
|
2412
|
+
const operation = inspect(path, input.sessionID).then(() => undefined);
|
|
2413
|
+
inspectionOperations.set(key, operation);
|
|
2414
|
+
try {
|
|
2415
|
+
await operation;
|
|
2416
|
+
}
|
|
2417
|
+
finally {
|
|
2418
|
+
if (inspectionOperations.get(key) === operation)
|
|
2419
|
+
inspectionOperations.delete(key);
|
|
2420
|
+
}
|
|
2404
2421
|
}
|
|
2405
2422
|
async function invalidateEditedHandoff(path) {
|
|
2406
2423
|
await ensureLoaded();
|
|
@@ -2481,7 +2498,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2481
2498
|
return undefined;
|
|
2482
2499
|
}
|
|
2483
2500
|
}
|
|
2484
|
-
async function
|
|
2501
|
+
async function hostSessionRecoveryHistory(sessionID) {
|
|
2485
2502
|
const messages = input.client?.session?.messages;
|
|
2486
2503
|
if (messages === undefined)
|
|
2487
2504
|
return undefined;
|
|
@@ -2493,55 +2510,40 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2493
2510
|
const payload = isRecord(response) && "data" in response ? response.data : response;
|
|
2494
2511
|
if (!Array.isArray(payload))
|
|
2495
2512
|
return undefined;
|
|
2513
|
+
let persistedTurn;
|
|
2514
|
+
let hasForeignUserTurn = false;
|
|
2496
2515
|
for (let index = payload.length - 1; index >= 0; index -= 1) {
|
|
2497
2516
|
const message = payload[index];
|
|
2498
|
-
if ((message
|
|
2499
|
-
continue;
|
|
2500
|
-
const agent = message.info?.agent ?? message.agent;
|
|
2501
|
-
if (typeof agent !== "string")
|
|
2517
|
+
if (!isRecord(message))
|
|
2502
2518
|
return undefined;
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
catch {
|
|
2511
|
-
return undefined;
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
async function hostSessionForeignUserAgent(sessionID) {
|
|
2515
|
-
const messages = input.client?.session?.messages;
|
|
2516
|
-
if (messages === undefined)
|
|
2517
|
-
return undefined;
|
|
2518
|
-
try {
|
|
2519
|
-
const response = await messages.call(input.client.session, {
|
|
2520
|
-
path: { id: sessionID },
|
|
2521
|
-
query: { directory: input.directory },
|
|
2522
|
-
});
|
|
2523
|
-
const payload = isRecord(response) && "data" in response ? response.data : response;
|
|
2524
|
-
if (!Array.isArray(payload))
|
|
2525
|
-
return undefined;
|
|
2526
|
-
for (const message of payload) {
|
|
2527
|
-
if ((message.info?.role ?? message.role) !== "user")
|
|
2519
|
+
if (message.info !== undefined && !isRecord(message.info))
|
|
2520
|
+
return undefined;
|
|
2521
|
+
const info = isRecord(message.info) ? message.info : undefined;
|
|
2522
|
+
const role = info?.role ?? message.role;
|
|
2523
|
+
if (role !== "user" && role !== "assistant")
|
|
2524
|
+
return undefined;
|
|
2525
|
+
if (role !== "user")
|
|
2528
2526
|
continue;
|
|
2529
|
-
const agent =
|
|
2527
|
+
const agent = info?.agent ?? message.agent;
|
|
2530
2528
|
if (typeof agent !== "string")
|
|
2531
2529
|
return undefined;
|
|
2532
2530
|
if (agent !== COORDINATOR_AGENT)
|
|
2533
|
-
|
|
2531
|
+
hasForeignUserTurn = true;
|
|
2532
|
+
if (persistedTurn === undefined) {
|
|
2533
|
+
if (message.parts !== undefined && !Array.isArray(message.parts))
|
|
2534
|
+
return undefined;
|
|
2535
|
+
persistedTurn = {
|
|
2536
|
+
agent,
|
|
2537
|
+
synthetic: Array.isArray(message.parts) && message.parts.some((part) => isRecord(part) && part.synthetic === true),
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2534
2540
|
}
|
|
2535
|
-
return
|
|
2541
|
+
return { hasForeignUserTurn, persistedTurn };
|
|
2536
2542
|
}
|
|
2537
2543
|
catch {
|
|
2538
2544
|
return undefined;
|
|
2539
2545
|
}
|
|
2540
2546
|
}
|
|
2541
|
-
async function hostSessionHasForeignUserTurn(sessionID) {
|
|
2542
|
-
const agent = await hostSessionForeignUserAgent(sessionID);
|
|
2543
|
-
return typeof agent === "string" ? true : agent;
|
|
2544
|
-
}
|
|
2545
2547
|
async function assistantMessageText(sessionID, messageID, expectedAgent, partID) {
|
|
2546
2548
|
const messages = input.client?.session?.messages;
|
|
2547
2549
|
if (messages === undefined)
|
|
@@ -2599,10 +2601,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2599
2601
|
return false;
|
|
2600
2602
|
if (identity.agent !== undefined && identity.agent !== COORDINATOR_AGENT)
|
|
2601
2603
|
return false;
|
|
2602
|
-
|
|
2604
|
+
const history = await hostSessionRecoveryHistory(sessionID);
|
|
2605
|
+
if (history === undefined || history.hasForeignUserTurn)
|
|
2606
|
+
return false;
|
|
2607
|
+
const persistedTurn = history.persistedTurn;
|
|
2608
|
+
if (persistedTurn === undefined && identity.agent !== COORDINATOR_AGENT)
|
|
2603
2609
|
return false;
|
|
2604
|
-
|
|
2605
|
-
if (persistedTurn?.agent !== COORDINATOR_AGENT)
|
|
2610
|
+
if (persistedTurn !== undefined && persistedTurn.agent !== COORDINATOR_AGENT)
|
|
2606
2611
|
return false;
|
|
2607
2612
|
await rememberCoordinatorRoot(sessionID);
|
|
2608
2613
|
releaseSessionEnforcement(sessionID);
|
|
@@ -2898,7 +2903,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2898
2903
|
.replaceAll(CONTINUATION_MARKER, "")
|
|
2899
2904
|
.trimEnd();
|
|
2900
2905
|
}
|
|
2901
|
-
await completeContinuationText(textInput.sessionID, textOutput.text);
|
|
2906
|
+
await completeContinuationText(textInput.sessionID, textOutput.text, false);
|
|
2902
2907
|
},
|
|
2903
2908
|
"experimental.session.compacting": async (compactInput, compactOutput) => {
|
|
2904
2909
|
await continuation.sessionCompacting(compactInput, compactOutput);
|
|
@@ -3101,6 +3106,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3101
3106
|
*/
|
|
3102
3107
|
"tool.execute.after": async (toolInput, output) => {
|
|
3103
3108
|
const completedChildSessionID = toolInput.tool === "task" ? taskChildSessionID(output) : undefined;
|
|
3109
|
+
const handoffInspection = inspectSuccessfulRead(toolInput);
|
|
3104
3110
|
try {
|
|
3105
3111
|
if (bootstrapRequired && toolInput.tool === "sortie_check_contract" && toolInput.sessionID !== undefined &&
|
|
3106
3112
|
isCoordinatorSession(toolInput.sessionID) && successfulBootstrapContractCheck(output)) {
|
|
@@ -3120,7 +3126,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3120
3126
|
}
|
|
3121
3127
|
}
|
|
3122
3128
|
}
|
|
3123
|
-
await
|
|
3129
|
+
await handoffInspection;
|
|
3124
3130
|
let parallel = parallelCalls.get(toolInput.callID ?? "");
|
|
3125
3131
|
if (parallel === undefined && toolInput.tool === "task" && toolInput.sessionID !== undefined &&
|
|
3126
3132
|
toolInput.callID !== undefined &&
|
|
@@ -3517,14 +3523,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3517
3523
|
completedCoordinatorParts.add(eventPart.id);
|
|
3518
3524
|
const text = await eventAssistantMessageText(eventSessionID, eventPart.messageID, COORDINATOR_AGENT, eventPart.id);
|
|
3519
3525
|
if (text !== undefined && text === eventPart.text.trim()) {
|
|
3520
|
-
completedCoordinatorMessages.add(eventPart.messageID);
|
|
3521
3526
|
while (completedCoordinatorParts.size > ACTIVE_SESSION_CACHE.maximum) {
|
|
3522
3527
|
completedCoordinatorParts.delete(completedCoordinatorParts.values().next().value);
|
|
3523
3528
|
}
|
|
3524
3529
|
while (completedCoordinatorMessages.size > ACTIVE_SESSION_CACHE.maximum) {
|
|
3525
3530
|
completedCoordinatorMessages.delete(completedCoordinatorMessages.values().next().value);
|
|
3526
3531
|
}
|
|
3527
|
-
await completeContinuationText(eventSessionID, text);
|
|
3532
|
+
await completeContinuationText(eventSessionID, text, false);
|
|
3528
3533
|
return;
|
|
3529
3534
|
}
|
|
3530
3535
|
completedCoordinatorParts.delete(eventPart.id);
|