@gajae-code/coding-agent 0.18.5 → 0.18.7
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/CHANGELOG.md +140 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli-commands.d.ts +10 -0
- package/dist/types/cli-ordinary.d.ts +13 -0
- package/dist/types/cli.d.ts +12 -3
- package/dist/types/commands/contribution-prep.d.ts +0 -3
- package/dist/types/commands/sdk.d.ts +3 -0
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-profile-activation.d.ts +19 -0
- package/dist/types/config/settings.d.ts +4 -0
- package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/edit/modes/replace.d.ts +3 -3
- package/dist/types/edit/streaming.d.ts +2 -2
- package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
- package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
- package/dist/types/sdk/broker/discovery.d.ts +8 -0
- package/dist/types/sdk/broker/ensure.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +4 -2
- package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
- package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +6 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +5 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
- package/dist/types/session/session-retirement.d.ts +47 -0
- package/dist/types/session/session-storage.d.ts +0 -46
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -0
- package/dist/types/tools/ask-contract.d.ts +75 -254
- package/dist/types/tools/bash.d.ts +1 -1
- package/package.json +7 -7
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/scripts/resident-memory-bench.ts +174 -20
- package/src/capability/index.ts +1 -0
- package/src/capability/types.ts +4 -0
- package/src/cli/config-cli.ts +8 -18
- package/src/cli/public-command-entry.ts +5 -0
- package/src/cli/ssh-cli.ts +6 -3
- package/src/cli/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-main.ts +7 -8
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -182
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +171 -21
- package/src/commands/update.ts +15 -1
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-profile-activation.ts +70 -4
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +23 -31
- package/src/config/model-resolver.ts +9 -1
- package/src/config/settings.ts +44 -4
- package/src/coordinator-mcp/event-webhook.ts +19 -14
- package/src/coordinator-mcp/policy.ts +14 -4
- package/src/coordinator-mcp/server.ts +142 -7
- package/src/coordinator-mcp/session-reaper.ts +9 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/discovery/helpers.ts +9 -3
- package/src/discovery/ssh.ts +8 -3
- package/src/edit/modes/replace.ts +6 -6
- package/src/edit/streaming.ts +15 -15
- package/src/exec/bash-shell-supervisor.ts +60 -3
- package/src/gjc-runtime/gc-runtime.ts +2 -1
- package/src/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +724 -370
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/prompts/tools/ask.md +2 -0
- package/src/runtime-mcp/client.ts +13 -4
- package/src/runtime-mcp/elicitation.ts +142 -0
- package/src/sdk/broker/broker.ts +54 -1
- package/src/sdk/broker/daemon-entry.ts +67 -6
- package/src/sdk/broker/discovery.ts +8 -0
- package/src/sdk/broker/ensure.ts +156 -20
- package/src/sdk/broker/lifecycle-ledger.ts +110 -12
- package/src/sdk/broker/lifecycle.ts +334 -348
- package/src/sdk/broker/runtime.ts +5 -1
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/broker/transport.ts +5 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +3 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +15 -12
- package/src/sdk/bus/telegram-daemon.ts +38 -3
- package/src/sdk/cli/diagnostics-cli.ts +247 -0
- package/src/sdk/diagnostics/observe-broker.ts +654 -0
- package/src/sdk/host/control/dispatch.ts +52 -4
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/session-runtime.test.ts +83 -4
- package/src/sdk/host/session-runtime.ts +15 -7
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- package/src/sdk/mcp/server.ts +15 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/router/session-router.ts +26 -3
- package/src/sdk/session.ts +51 -8
- package/src/session/agent-session.ts +169 -43
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +203 -4
- package/src/session/internal/task-artifact-owner-access.ts +229 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
- package/src/session/session-retirement.ts +155 -0
- package/src/session/session-storage.ts +1 -160
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +578 -0
- package/src/session/task-artifact-owner-retirement.ts +475 -0
- package/src/session/task-artifact-owner.ts +289 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
- package/src/setup/hermes-setup.ts +4 -1
- package/src/slash-commands/builtin-registry.ts +8 -5
- package/src/tools/ask-contract.test.ts +76 -1
- package/src/tools/ask-contract.ts +57 -6
- package/src/tools/ask.ts +16 -7
- package/src/tools/bash.ts +38 -29
- package/src/tools/descriptors.test.ts +29 -0
- package/src/tools/tool-catalog.generated.ts +7 -4
- package/src/web/search/index.ts +2 -2
package/src/sdk/broker/ensure.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readBrokerDiscovery,
|
|
15
15
|
readBrokerRestartIntent,
|
|
16
16
|
} from "./discovery";
|
|
17
|
+
import { isProcessIncarnation, observeProcessIncarnation } from "./process-incarnation";
|
|
17
18
|
import {
|
|
18
19
|
isSdkInternalRuntimeImagePresent,
|
|
19
20
|
resolveSdkInternalSpawnCommand,
|
|
@@ -369,6 +370,7 @@ interface BrokerOwner {
|
|
|
369
370
|
stop(): Promise<void>;
|
|
370
371
|
canReuse(discovery: BrokerDiscovery | null): boolean;
|
|
371
372
|
markReady(discovery: BrokerDiscovery): boolean;
|
|
373
|
+
setTarget(pid: number, incarnation: string): void;
|
|
372
374
|
}
|
|
373
375
|
type EnsureInitiator = "discovery" | "fixture-lease";
|
|
374
376
|
type EnsureOutcome =
|
|
@@ -453,25 +455,60 @@ async function reapSpawnedBroker(child: ChildProcess, timing: ReapTiming = DEFAU
|
|
|
453
455
|
throw new Error(`Detached SDK broker (pid ${child.pid}) did not exit after SIGKILL during reap.`);
|
|
454
456
|
}
|
|
455
457
|
|
|
458
|
+
async function reapSpawnedBrokerIdentity(
|
|
459
|
+
pid: number,
|
|
460
|
+
incarnation: string,
|
|
461
|
+
timing: ReapTiming = DEFAULT_REAP_TIMING,
|
|
462
|
+
): Promise<void> {
|
|
463
|
+
const matches = (): boolean => {
|
|
464
|
+
const observed = observeProcessIncarnation(pid);
|
|
465
|
+
return observed.status === "present" && observed.incarnation === incarnation;
|
|
466
|
+
};
|
|
467
|
+
const gone = (): boolean => observeProcessIncarnation(pid).status === "absent";
|
|
468
|
+
const signal = (sig: NodeJS.Signals): void => {
|
|
469
|
+
if (!matches()) return;
|
|
470
|
+
try {
|
|
471
|
+
process.kill(pid, sig);
|
|
472
|
+
} catch {
|
|
473
|
+
// already exited between identity check and signal delivery
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
if (gone() || !matches()) return;
|
|
477
|
+
// A reported trampoline has already detached this broker from the retained
|
|
478
|
+
// child handle, so graceful shutdown cannot be observed or reaped reliably.
|
|
479
|
+
// Force termination keeps discovery-timeout cleanup within its caller's
|
|
480
|
+
// bounded deadline while the identity check still prevents PID reuse.
|
|
481
|
+
signal("SIGKILL");
|
|
482
|
+
await Promise.race([waitForProcessAbsence(pid), sleep(timing.killVerifyMs)]);
|
|
483
|
+
if (!gone()) throw new Error(`Detached SDK broker (pid ${pid}) did not exit after SIGKILL during reap.`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function waitForProcessAbsence(pid: number): Promise<void> {
|
|
487
|
+
while (observeProcessIncarnation(pid).status !== "absent") await Bun.sleep(10);
|
|
488
|
+
}
|
|
489
|
+
|
|
456
490
|
function registerBrokerOwner(
|
|
457
491
|
agentDir: string,
|
|
458
492
|
child: ChildProcess,
|
|
459
493
|
timing: ReapTiming = DEFAULT_REAP_TIMING,
|
|
460
494
|
): BrokerOwner {
|
|
461
|
-
|
|
495
|
+
let targetPid = child.pid;
|
|
496
|
+
let targetIncarnation = child.pid === undefined ? undefined : brokerProcessIncarnation(child.pid);
|
|
462
497
|
let state: "starting" | "ready" | "cleanup-unverified" = "starting";
|
|
463
498
|
const matches = (discovery: BrokerDiscovery | null): boolean =>
|
|
464
499
|
Boolean(
|
|
465
500
|
discovery &&
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
discovery.pid ===
|
|
469
|
-
discovery.incarnation ===
|
|
501
|
+
targetPid !== undefined &&
|
|
502
|
+
targetIncarnation !== undefined &&
|
|
503
|
+
discovery.pid === targetPid &&
|
|
504
|
+
discovery.incarnation === targetIncarnation,
|
|
470
505
|
);
|
|
471
506
|
const owner: BrokerOwner = {
|
|
472
507
|
async stop(): Promise<void> {
|
|
473
508
|
try {
|
|
474
|
-
|
|
509
|
+
if (targetPid !== undefined && targetIncarnation !== undefined && targetPid !== child.pid)
|
|
510
|
+
await reapSpawnedBrokerIdentity(targetPid, targetIncarnation, timing);
|
|
511
|
+
else await reapSpawnedBroker(child, timing);
|
|
475
512
|
} catch (error) {
|
|
476
513
|
state = "cleanup-unverified";
|
|
477
514
|
throw error;
|
|
@@ -486,6 +523,10 @@ function registerBrokerOwner(
|
|
|
486
523
|
state = "ready";
|
|
487
524
|
return true;
|
|
488
525
|
},
|
|
526
|
+
setTarget(pid, incarnation): void {
|
|
527
|
+
targetPid = pid;
|
|
528
|
+
targetIncarnation = incarnation;
|
|
529
|
+
},
|
|
489
530
|
};
|
|
490
531
|
owners.set(agentDir, owner);
|
|
491
532
|
return owner;
|
|
@@ -688,7 +729,11 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
688
729
|
// that finished between our first read and the lock acquisition.
|
|
689
730
|
const discoveredUnderLock = await reconcileBrokerGenerationForStartup(settings, deadline);
|
|
690
731
|
if (discoveredUnderLock) return { kind: "external-discovery", discovery: discoveredUnderLock };
|
|
691
|
-
const command = resolveSdkInternalSpawnCommand(
|
|
732
|
+
const command = resolveSdkInternalSpawnCommand(
|
|
733
|
+
process.platform === "win32" || initiator === "fixture-lease"
|
|
734
|
+
? "broker-internal"
|
|
735
|
+
: "broker-trampoline-internal",
|
|
736
|
+
);
|
|
692
737
|
spawnLog = await openBrokerSpawnLog(settings.agentDir);
|
|
693
738
|
// A stale marker must never be misattributed to this spawn; clear it first.
|
|
694
739
|
await clearBrokerStartupExitRecord(settings.agentDir);
|
|
@@ -696,23 +741,83 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
696
741
|
const childSpawnedAt = Date.now();
|
|
697
742
|
const child = spawn(command.file, [...command.args, "--agent-dir", settings.agentDir], {
|
|
698
743
|
detached: true,
|
|
699
|
-
stdio: ["ignore", "ignore", spawnLog ? spawnLog.handle.fd : "ignore"],
|
|
744
|
+
stdio: ["ignore", process.platform === "win32" ? "ignore" : "pipe", spawnLog ? spawnLog.handle.fd : "ignore"],
|
|
700
745
|
env: brokerSpawnEnvironment(command, settings.env),
|
|
701
746
|
...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
|
|
702
747
|
});
|
|
748
|
+
const isTrampoline = process.platform !== "win32" && initiator !== "fixture-lease";
|
|
703
749
|
let spawnError: Error | undefined;
|
|
750
|
+
let childExited = false;
|
|
751
|
+
let childExitCode: number | null = null;
|
|
752
|
+
let childSignalCode: NodeJS.Signals | null = null;
|
|
753
|
+
let brokerDeathObserved = false;
|
|
704
754
|
child.once("error", error => {
|
|
705
755
|
spawnError = error;
|
|
706
756
|
});
|
|
707
|
-
|
|
757
|
+
child.once("close", (code, signal) => {
|
|
758
|
+
childExited = true;
|
|
759
|
+
childExitCode = code;
|
|
760
|
+
childSignalCode = signal;
|
|
761
|
+
});
|
|
762
|
+
let trampolineReported = !isTrampoline;
|
|
763
|
+
let spawnedBrokerPid = isTrampoline ? undefined : child.pid;
|
|
764
|
+
let spawnedBrokerIncarnation =
|
|
765
|
+
isTrampoline || child.pid === undefined ? undefined : brokerProcessIncarnation(child.pid);
|
|
708
766
|
const owner = registerBrokerOwner(settings.agentDir, child);
|
|
767
|
+
if (isTrampoline && child.stdout) {
|
|
768
|
+
let output = "";
|
|
769
|
+
child.stdout.on("data", chunk => {
|
|
770
|
+
output += Buffer.from(chunk).toString("utf8");
|
|
771
|
+
const newline = output.indexOf("\n");
|
|
772
|
+
if (newline < 0) return;
|
|
773
|
+
const line = output.slice(0, newline).trim();
|
|
774
|
+
const [rawPid, rawIncarnation] = line?.split("\t") ?? [];
|
|
775
|
+
const pid = rawPid ? Number(rawPid) : NaN;
|
|
776
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return;
|
|
777
|
+
if (isProcessIncarnation(rawIncarnation)) {
|
|
778
|
+
const incarnation = rawIncarnation;
|
|
779
|
+
trampolineReported = true;
|
|
780
|
+
spawnedBrokerPid = pid;
|
|
781
|
+
spawnedBrokerIncarnation = incarnation;
|
|
782
|
+
owner.setTarget(pid, incarnation);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
709
786
|
child.unref();
|
|
710
787
|
// The child holds its own duplicate of the descriptor. Failure to close the
|
|
711
788
|
// parent's diagnostic handle must not discard exact ownership of a live child.
|
|
712
789
|
await spawnLog?.handle.close().catch(() => undefined);
|
|
713
790
|
let discoveryError: unknown;
|
|
714
791
|
while (ensureBrokerTiming.now() < deadline) {
|
|
715
|
-
if (
|
|
792
|
+
if (
|
|
793
|
+
spawnError ||
|
|
794
|
+
(isTrampoline && childExited && (!trampolineReported || childExitCode !== 0 || childSignalCode !== null)) ||
|
|
795
|
+
(!isTrampoline && process.platform === "win32" && (child.exitCode !== null || child.signalCode !== null))
|
|
796
|
+
)
|
|
797
|
+
break;
|
|
798
|
+
if (
|
|
799
|
+
isTrampoline &&
|
|
800
|
+
trampolineReported &&
|
|
801
|
+
spawnedBrokerPid !== undefined &&
|
|
802
|
+
spawnedBrokerIncarnation !== undefined
|
|
803
|
+
) {
|
|
804
|
+
const observed = observeProcessIncarnation(spawnedBrokerPid);
|
|
805
|
+
if (observed.status === "present" && observed.incarnation !== spawnedBrokerIncarnation) {
|
|
806
|
+
brokerDeathObserved = true;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
if (observed.status === "absent") {
|
|
810
|
+
brokerDeathObserved = true;
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// The trampoline's stdout line identifies the real broker. Do not read
|
|
815
|
+
// discovery until that line has been parsed, or a marker from the
|
|
816
|
+
// trampoline could be trusted as the broker's own startup evidence.
|
|
817
|
+
if (isTrampoline && !trampolineReported) {
|
|
818
|
+
await ensureBrokerTiming.sleep(50);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
716
821
|
try {
|
|
717
822
|
const discovered = await readBrokerDiscoveryBeforeDeadline(
|
|
718
823
|
settings.agentDir,
|
|
@@ -724,6 +829,20 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
724
829
|
await ensureBrokerTiming.sleep(50);
|
|
725
830
|
continue;
|
|
726
831
|
}
|
|
832
|
+
if (isTrampoline && !childExited) {
|
|
833
|
+
await ensureBrokerTiming.sleep(50);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (isTrampoline && spawnedBrokerPid !== undefined && spawnedBrokerIncarnation !== undefined) {
|
|
837
|
+
const observed = observeProcessIncarnation(spawnedBrokerPid);
|
|
838
|
+
if (
|
|
839
|
+
observed.status === "absent" ||
|
|
840
|
+
(observed.status === "present" && observed.incarnation !== spawnedBrokerIncarnation)
|
|
841
|
+
) {
|
|
842
|
+
brokerDeathObserved = true;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
727
846
|
if (owner.markReady(discovered)) {
|
|
728
847
|
return initiator === "fixture-lease"
|
|
729
848
|
? { kind: "local-started-fixture", discovery: discovered, owner, child }
|
|
@@ -737,8 +856,13 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
737
856
|
}
|
|
738
857
|
await ensureBrokerTiming.sleep(50);
|
|
739
858
|
}
|
|
740
|
-
const exitedBeforeDiscovery =
|
|
741
|
-
|
|
859
|
+
const exitedBeforeDiscovery =
|
|
860
|
+
(isTrampoline &&
|
|
861
|
+
(!trampolineReported ||
|
|
862
|
+
(childExited && (childExitCode !== 0 || childSignalCode !== null)) ||
|
|
863
|
+
brokerDeathObserved)) ||
|
|
864
|
+
(!isTrampoline && process.platform === "win32" && (child.exitCode !== null || child.signalCode !== null));
|
|
865
|
+
if (!isTrampoline && exitedBeforeDiscovery && childExitCode === 0) {
|
|
742
866
|
// A clean exit means another broker won the ownership lock (two ACP
|
|
743
867
|
// processes racing a cold broker state, e.g. a provider probe and an
|
|
744
868
|
// agent launch). The winner may publish its discovery right after our
|
|
@@ -765,16 +889,16 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
765
889
|
const startupExitRecord = await readBrokerStartupExitRecord(settings.agentDir);
|
|
766
890
|
const trustedMarker =
|
|
767
891
|
marker &&
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
marker.pid ===
|
|
771
|
-
marker.incarnation ===
|
|
892
|
+
spawnedBrokerPid !== undefined &&
|
|
893
|
+
spawnedBrokerIncarnation !== undefined &&
|
|
894
|
+
marker.pid === spawnedBrokerPid &&
|
|
895
|
+
marker.incarnation === spawnedBrokerIncarnation
|
|
772
896
|
? marker
|
|
773
897
|
: undefined;
|
|
774
898
|
const trustedStartupExitRecord =
|
|
775
899
|
startupExitRecord &&
|
|
776
|
-
|
|
777
|
-
startupExitRecord.pid ===
|
|
900
|
+
spawnedBrokerPid !== undefined &&
|
|
901
|
+
startupExitRecord.pid === spawnedBrokerPid &&
|
|
778
902
|
startupExitRecord.writtenAt >= childSpawnedAt
|
|
779
903
|
? startupExitRecord
|
|
780
904
|
: undefined;
|
|
@@ -828,8 +952,20 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
828
952
|
? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
|
|
829
953
|
: exitedBeforeDiscovery
|
|
830
954
|
? new BrokerStartupError({
|
|
831
|
-
exitCode:
|
|
832
|
-
|
|
955
|
+
exitCode: isTrampoline
|
|
956
|
+
? (trustedStartupExitRecord?.exitCode ??
|
|
957
|
+
trustedMarker?.exitCode ??
|
|
958
|
+
childExitCode ??
|
|
959
|
+
child.exitCode ??
|
|
960
|
+
null)
|
|
961
|
+
: (childExitCode ?? child.exitCode ?? trustedStartupExitRecord?.exitCode ?? null),
|
|
962
|
+
signal: isTrampoline
|
|
963
|
+
? (trustedStartupExitRecord?.signal ??
|
|
964
|
+
trustedMarker?.signal ??
|
|
965
|
+
childSignalCode ??
|
|
966
|
+
child.signalCode ??
|
|
967
|
+
null)
|
|
968
|
+
: (childSignalCode ?? child.signalCode ?? trustedStartupExitRecord?.signal ?? null),
|
|
833
969
|
reason:
|
|
834
970
|
brokerStartupExitReason(trustedStartupExitRecord) ?? brokerStartupFailureReason(trustedMarker),
|
|
835
971
|
stderrExcerpt: spawnLogTail.length > 0 ? spawnLogTail : undefined,
|
|
@@ -74,6 +74,8 @@ export interface LifecycleDurableEffectsReceipt {
|
|
|
74
74
|
export interface LifecycleLedgerEntry {
|
|
75
75
|
operationKey?: string;
|
|
76
76
|
fingerprint?: string;
|
|
77
|
+
/** True only when a session.close request supplied immutable generation authority. */
|
|
78
|
+
closeAuthorityBound?: boolean;
|
|
77
79
|
version: typeof SDK_STATE_VERSION;
|
|
78
80
|
identity: string;
|
|
79
81
|
requestHash: string;
|
|
@@ -213,6 +215,20 @@ function pendingCleanupSessionId(response: unknown): string | undefined {
|
|
|
213
215
|
return canonicalCleanupSessionId(cleanup?.sessionId) ? cleanup.sessionId : undefined;
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
function retirementCreateIdentity(response: unknown): string | undefined {
|
|
219
|
+
if (!response || typeof response !== "object") return undefined;
|
|
220
|
+
const error = (response as { error?: unknown }).error;
|
|
221
|
+
if (!error || typeof error !== "object") return undefined;
|
|
222
|
+
const cleanup = (error as { cleanup?: unknown }).cleanup;
|
|
223
|
+
if (!cleanup || typeof cleanup !== "object") return undefined;
|
|
224
|
+
const receipt = (cleanup as { uncertainRetirement?: unknown }).uncertainRetirement;
|
|
225
|
+
if (!receipt || typeof receipt !== "object") return undefined;
|
|
226
|
+
const identity = (receipt as { identity?: unknown }).identity;
|
|
227
|
+
if (!identity || typeof identity !== "object") return undefined;
|
|
228
|
+
const createIdentity = (identity as { createIdentity?: unknown }).createIdentity;
|
|
229
|
+
return typeof createIdentity === "string" && createIdentity.length > 0 ? createIdentity : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
216
232
|
function hasValidTerminalDigests(entry: LifecycleLedgerEntry): boolean {
|
|
217
233
|
const response = entry.response as { ok?: unknown; error?: { code?: unknown; cleanup?: unknown } } | undefined;
|
|
218
234
|
const cleanupPendingResponse =
|
|
@@ -646,23 +662,91 @@ export class LifecycleLedger {
|
|
|
646
662
|
await directory.close();
|
|
647
663
|
}
|
|
648
664
|
}
|
|
649
|
-
async #compact(
|
|
665
|
+
async #compact(
|
|
666
|
+
replacement?: LifecycleLedgerEntry,
|
|
667
|
+
incoming?: { readonly bytes: number; readonly rows: number },
|
|
668
|
+
): Promise<boolean> {
|
|
650
669
|
const anchors = new Map<string, LifecycleLedgerEntry>();
|
|
651
670
|
for (const entry of this.#entries) {
|
|
652
671
|
const latest = replacement?.identity === entry.identity ? replacement : this.#byIdentity.get(entry.identity);
|
|
653
672
|
if (entry.state === "accepted" && !anchors.has(entry.identity) && entry.requestHash === latest?.requestHash)
|
|
654
673
|
anchors.set(entry.identity, entry);
|
|
655
674
|
}
|
|
656
|
-
const snapshot: LifecycleLedgerEntry[] = [];
|
|
657
675
|
const compacted = new Map(this.#byIdentity);
|
|
658
676
|
if (replacement) compacted.set(replacement.identity, replacement);
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
677
|
+
const buildSnapshot = (): LifecycleLedgerEntry[] => {
|
|
678
|
+
const next: LifecycleLedgerEntry[] = [];
|
|
679
|
+
for (const [identity, latest] of compacted) {
|
|
680
|
+
const anchor = anchors.get(identity);
|
|
681
|
+
if (!anchor) throw new Error("Lifecycle ledger compaction requires an accepted identity anchor.");
|
|
682
|
+
next.push(replacement?.identity === identity && latest.state === "accepted" ? latest : anchor);
|
|
683
|
+
if (latest.state !== "accepted") next.push(latest);
|
|
684
|
+
}
|
|
685
|
+
return next;
|
|
686
|
+
};
|
|
687
|
+
let snapshot = buildSnapshot();
|
|
688
|
+
let contents = Buffer.from(snapshot.map(entry => `${JSON.stringify(entry)}\n`).join(""));
|
|
689
|
+
const needsEviction = incoming
|
|
690
|
+
? snapshot.length + incoming.rows > this.#limits.maxRows ||
|
|
691
|
+
contents.length + incoming.bytes > this.#limits.maxBytes
|
|
692
|
+
: replacement === undefined
|
|
693
|
+
? snapshot.length >= this.#limits.maxRows || contents.length >= this.#limits.maxBytes
|
|
694
|
+
: snapshot.length > this.#limits.maxRows || contents.length > this.#limits.maxBytes;
|
|
695
|
+
if (needsEviction) {
|
|
696
|
+
const protectedCreateIdentities = new Set<string>();
|
|
697
|
+
for (const entry of compacted.values()) {
|
|
698
|
+
const createIdentity =
|
|
699
|
+
retirementCreateIdentity(entry.response) ?? retirementCreateIdentity(entry.unresolvedCleanupResponse);
|
|
700
|
+
if (createIdentity !== undefined) protectedCreateIdentities.add(createIdentity);
|
|
701
|
+
}
|
|
702
|
+
const evictable = [...compacted.values()]
|
|
703
|
+
.filter(
|
|
704
|
+
latest =>
|
|
705
|
+
latest.identity !== replacement?.identity &&
|
|
706
|
+
terminal(latest.state) &&
|
|
707
|
+
latest.operationKey !== undefined &&
|
|
708
|
+
latest.fingerprint !== undefined &&
|
|
709
|
+
(!latest.operationKey.startsWith("session.close\u0000") || latest.closeAuthorityBound === true) &&
|
|
710
|
+
!protectedCreateIdentities.has(latest.identity) &&
|
|
711
|
+
pendingCleanupSessionId(latest.response) === undefined &&
|
|
712
|
+
pendingCleanupSessionId(latest.unresolvedCleanupResponse) === undefined,
|
|
713
|
+
)
|
|
714
|
+
.sort((left, right) => left.ts - right.ts);
|
|
715
|
+
const rowsByIdentity = new Map<string, number>();
|
|
716
|
+
const bytesByIdentity = new Map<string, number>();
|
|
717
|
+
for (const entry of snapshot) {
|
|
718
|
+
rowsByIdentity.set(entry.identity, (rowsByIdentity.get(entry.identity) ?? 0) + 1);
|
|
719
|
+
bytesByIdentity.set(
|
|
720
|
+
entry.identity,
|
|
721
|
+
(bytesByIdentity.get(entry.identity) ?? 0) + Buffer.byteLength(`${JSON.stringify(entry)}\n`),
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
let remainingRows = snapshot.length;
|
|
725
|
+
let remainingBytes = contents.length;
|
|
726
|
+
const evicted = new Set<string>();
|
|
727
|
+
for (const latest of evictable) {
|
|
728
|
+
if (
|
|
729
|
+
remainingRows <= this.#limits.maxRows / 2 &&
|
|
730
|
+
remainingBytes <= this.#limits.maxBytes / 2 &&
|
|
731
|
+
remainingRows + (incoming?.rows ?? 0) <= this.#limits.maxRows &&
|
|
732
|
+
remainingBytes + (incoming?.bytes ?? 0) <= this.#limits.maxBytes
|
|
733
|
+
)
|
|
734
|
+
break;
|
|
735
|
+
evicted.add(latest.identity);
|
|
736
|
+
remainingRows -= rowsByIdentity.get(latest.identity) ?? 0;
|
|
737
|
+
remainingBytes -= bytesByIdentity.get(latest.identity) ?? 0;
|
|
738
|
+
}
|
|
739
|
+
for (const identity of evicted) compacted.delete(identity);
|
|
740
|
+
snapshot = buildSnapshot();
|
|
741
|
+
contents = Buffer.from(snapshot.map(entry => `${JSON.stringify(entry)}\n`).join(""));
|
|
742
|
+
// Replay is guaranteed only for identities retained in the compacted ledger.
|
|
664
743
|
}
|
|
665
|
-
|
|
744
|
+
if (
|
|
745
|
+
incoming &&
|
|
746
|
+
(snapshot.length + incoming.rows > this.#limits.maxRows ||
|
|
747
|
+
contents.length + incoming.bytes > this.#limits.maxBytes)
|
|
748
|
+
)
|
|
749
|
+
throw new Error("Lifecycle ledger append exceeds configured bounds.");
|
|
666
750
|
if (
|
|
667
751
|
snapshot.length > this.#limits.maxRows ||
|
|
668
752
|
contents.length > this.#limits.maxBytes ||
|
|
@@ -735,7 +819,7 @@ export class LifecycleLedger {
|
|
|
735
819
|
async migrateIdentity(
|
|
736
820
|
from: string,
|
|
737
821
|
to: string,
|
|
738
|
-
metadata: { operationKey: string; fingerprint: string },
|
|
822
|
+
metadata: { operationKey: string; fingerprint: string; closeAuthorityBound?: boolean },
|
|
739
823
|
): Promise<LifecycleLedgerEntry | undefined> {
|
|
740
824
|
return this.#mutate(async () => {
|
|
741
825
|
const existing = this.#byIdentity.get(to);
|
|
@@ -776,9 +860,12 @@ export class LifecycleLedger {
|
|
|
776
860
|
);
|
|
777
861
|
if (line.length - 1 > this.#limits.maxLineBytes)
|
|
778
862
|
throw new Error("Lifecycle ledger row exceeds the maximum byte length.");
|
|
863
|
+
if (line.length > this.#limits.maxBytes) throw new Error("Lifecycle ledger append exceeds configured bounds.");
|
|
779
864
|
let replacementCompacted = false;
|
|
780
865
|
if (this.#rowCount + 1 > this.#limits.maxRows || this.#byteCount + line.length > this.#limits.maxBytes)
|
|
781
|
-
replacementCompacted =
|
|
866
|
+
replacementCompacted = this.#byIdentity.has(entry.identity)
|
|
867
|
+
? await this.#compact(entry)
|
|
868
|
+
: await this.#compact(undefined, { bytes: line.length, rows: 1 });
|
|
782
869
|
if (replacementCompacted) return entry;
|
|
783
870
|
if (this.#rowCount + 1 > this.#limits.maxRows || this.#byteCount + line.length > this.#limits.maxBytes)
|
|
784
871
|
throw new Error("Lifecycle ledger append exceeds configured bounds.");
|
|
@@ -798,7 +885,12 @@ export class LifecycleLedger {
|
|
|
798
885
|
async begin(
|
|
799
886
|
identity: string,
|
|
800
887
|
requestHash: string,
|
|
801
|
-
metadata: {
|
|
888
|
+
metadata: {
|
|
889
|
+
operationKey?: string;
|
|
890
|
+
fingerprint?: string;
|
|
891
|
+
intendedSessionId?: string;
|
|
892
|
+
closeAuthorityBound?: boolean;
|
|
893
|
+
} = {},
|
|
802
894
|
): Promise<BeginResult> {
|
|
803
895
|
return this.#mutate(async () => this.#begin(identity, requestHash, metadata));
|
|
804
896
|
}
|
|
@@ -806,7 +898,12 @@ export class LifecycleLedger {
|
|
|
806
898
|
async #begin(
|
|
807
899
|
identity: string,
|
|
808
900
|
requestHash: string,
|
|
809
|
-
metadata: {
|
|
901
|
+
metadata: {
|
|
902
|
+
operationKey?: string;
|
|
903
|
+
fingerprint?: string;
|
|
904
|
+
intendedSessionId?: string;
|
|
905
|
+
closeAuthorityBound?: boolean;
|
|
906
|
+
},
|
|
810
907
|
): Promise<BeginResult> {
|
|
811
908
|
const prior = this.#byIdentity.get(identity);
|
|
812
909
|
if (!prior)
|
|
@@ -819,6 +916,7 @@ export class LifecycleLedger {
|
|
|
819
916
|
operationKey: metadata.operationKey,
|
|
820
917
|
fingerprint: metadata.fingerprint,
|
|
821
918
|
intendedSessionId: metadata.intendedSessionId,
|
|
919
|
+
closeAuthorityBound: metadata.closeAuthorityBound,
|
|
822
920
|
state: "accepted",
|
|
823
921
|
ts: Date.now(),
|
|
824
922
|
}),
|