@gajae-code/coding-agent 0.18.1 → 0.18.6
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 +161 -0
- package/dist/types/cli/stats-cli.d.ts +0 -5
- 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/autorouting-tier-map.d.ts +29 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -0
- package/dist/types/config/model-registry.d.ts +16 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -3
- package/dist/types/edit/streaming.d.ts +2 -2
- package/dist/types/exa/mcp-client.d.ts +1 -5
- package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
- package/dist/types/hashline/parser.d.ts +28 -5
- package/dist/types/modes/acp/acp-agent.d.ts +6 -1
- 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 +6 -1
- package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
- package/dist/types/sdk/broker/runtime.d.ts +21 -1
- package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
- package/dist/types/sdk/bus/index.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +1 -0
- 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/session-runtime.d.ts +7 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +5 -1
- package/dist/types/tools/ask-contract.d.ts +75 -254
- package/dist/types/tools/bash.d.ts +1 -0
- package/package.json +7 -7
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/src/cli/public-command-entry.ts +5 -0
- package/src/cli/ssh-cli.ts +6 -3
- package/src/cli/stats-cli.ts +4 -36
- package/src/cli/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-main.ts +4 -8
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -186
- package/src/commands/commit.ts +2 -2
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +60 -2
- package/src/commands/update.ts +15 -1
- package/src/config/autorouting-tier-map.ts +20 -2
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-preset-registry.ts +17 -2
- package/src/config/model-profile-activation.ts +73 -5
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +29 -29
- package/src/config/model-registry.ts +45 -17
- package/src/config/model-resolver.ts +9 -1
- 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/edit/modes/replace.ts +6 -6
- package/src/edit/streaming.ts +15 -15
- package/src/exa/mcp-client.ts +41 -8
- package/src/exec/bash-shell-guardian.ts +184 -34
- package/src/exec/bash-shell-supervisor.ts +60 -3
- package/src/hashline/execute.ts +51 -3
- package/src/hashline/parser.ts +63 -16
- package/src/internal-urls/docs-index.generated.ts +29 -28
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +688 -371
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/controllers/ssh-command-controller.ts +3 -1
- package/src/prompts/tools/ask.md +2 -0
- package/src/sdk/broker/broker.ts +46 -0
- 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 +108 -34
- package/src/sdk/broker/recovery-backoff.ts +135 -0
- package/src/sdk/broker/runtime.ts +88 -1
- package/src/sdk/broker/startup-budget.ts +6 -1
- package/src/sdk/broker/transport.ts +5 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +305 -19
- 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 +34 -1
- 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/session-runtime.test.ts +40 -2
- package/src/sdk/host/session-runtime.ts +112 -8
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- package/src/sdk/prompt-watchdog.ts +17 -4
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/router/session-router.ts +7 -1
- package/src/sdk/session.ts +37 -8
- package/src/session/agent-session.ts +128 -41
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +119 -3
- 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 +77 -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/dist/types/cli/grep-cli.d.ts +0 -17
- package/dist/types/cli/shell-cli.d.ts +0 -8
- package/dist/types/commands/grep.d.ts +0 -42
- package/dist/types/commands/shell.d.ts +0 -21
- package/src/cli/grep-cli.ts +0 -160
- package/src/cli/shell-cli.ts +0 -176
- package/src/commands/grep.ts +0 -48
- package/src/commands/shell.ts +0 -29
|
@@ -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
|
}),
|
|
@@ -97,6 +97,7 @@ import {
|
|
|
97
97
|
} from "./session-index";
|
|
98
98
|
import {
|
|
99
99
|
cancellableSleep,
|
|
100
|
+
DEFAULT_BROKER_PRESPAWN_PREPARATION_TIMEOUT_MS,
|
|
100
101
|
DEFAULT_READINESS_TIMEOUT_MS,
|
|
101
102
|
deriveLifecycleOuterDeadlines,
|
|
102
103
|
isValidReadinessTimeoutMs,
|
|
@@ -517,6 +518,7 @@ type LifecycleCommandResolver = () => LifecycleCommand;
|
|
|
517
518
|
const lifecycleCommandResolversForTest = new WeakMap<Broker, LifecycleCommandResolver>();
|
|
518
519
|
const lifecycleCleanupHooksForTest = new WeakMap<Broker, () => void>();
|
|
519
520
|
const startupAdmittedInputs = new WeakSet<Input>();
|
|
521
|
+
const startupBrokerDerivedAdmissions = new WeakMap<Input, { admittedAt: number; preSpawnDeadlineAt: number }>();
|
|
520
522
|
const startupLaunchInputs = new WeakMap<Input, SessionLaunch>();
|
|
521
523
|
type EnsureLaunchWorktreeForTest = (
|
|
522
524
|
plan: GjcLaunchWorktreePlan,
|
|
@@ -627,7 +629,10 @@ export function hasValidLifecycleDeadlines(value: LifecycleDeadlines, now = Date
|
|
|
627
629
|
type Input = Record<string, unknown>;
|
|
628
630
|
// The admitted launch deadline must survive the response phase: executeLifecycle
|
|
629
631
|
// receives the caller's original input after startup admission has expanded it.
|
|
630
|
-
type LifecycleEffectIntentWithDeadline = LifecycleEffectIntent & {
|
|
632
|
+
type LifecycleEffectIntentWithDeadline = LifecycleEffectIntent & {
|
|
633
|
+
lifecycleCleanupDeadlineAt?: number;
|
|
634
|
+
admissionCleanupDeadlineAt?: number;
|
|
635
|
+
};
|
|
631
636
|
export const isCanonicalSessionId = (value: string): boolean => /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
|
|
632
637
|
const defaultStateRoot = (cwd: string) => path.join(path.resolve(cwd), ".gjc", "state");
|
|
633
638
|
const hasDefaultStateRoot = (cwd: string, root: string) => path.resolve(root) === defaultStateRoot(cwd);
|
|
@@ -6414,6 +6419,7 @@ async function executeLifecycleResponse(
|
|
|
6414
6419
|
const timing = lifecycleTiming(broker);
|
|
6415
6420
|
const admissionGranted = startupAdmittedInputs.has(input);
|
|
6416
6421
|
if (!admissionGranted) {
|
|
6422
|
+
const startupReceivedAt = timing.now();
|
|
6417
6423
|
const suppliedDeadlineFields = [
|
|
6418
6424
|
input.receivedAt,
|
|
6419
6425
|
input.requestedReadinessTimeoutMs,
|
|
@@ -6441,15 +6447,26 @@ async function executeLifecycleResponse(
|
|
|
6441
6447
|
);
|
|
6442
6448
|
}
|
|
6443
6449
|
const admitted = await broker.runStartup(queueWaitMs, timing, async admittedAt => {
|
|
6444
|
-
const
|
|
6445
|
-
|
|
6446
|
-
|
|
6450
|
+
const callerSuppliedDeadlines = suppliedDeadlineFields.some(value => value !== undefined);
|
|
6451
|
+
let admittedInput: Input;
|
|
6452
|
+
if (launch.worktreePlan) admittedInput = { ...input, admittedAt, requestedReadinessTimeoutMs };
|
|
6453
|
+
else if (callerSuppliedDeadlines) admittedInput = { ...input };
|
|
6454
|
+
else admittedInput = { ...input, ...deriveLifecycleDeadlines(admittedAt, requestedReadinessTimeoutMs) };
|
|
6447
6455
|
startupAdmittedInputs.add(admittedInput);
|
|
6456
|
+
if (!launch.worktreePlan && !callerSuppliedDeadlines)
|
|
6457
|
+
startupBrokerDerivedAdmissions.set(admittedInput, {
|
|
6458
|
+
admittedAt,
|
|
6459
|
+
preSpawnDeadlineAt: Math.min(
|
|
6460
|
+
admittedAt + DEFAULT_BROKER_PRESPAWN_PREPARATION_TIMEOUT_MS,
|
|
6461
|
+
startupReceivedAt + queueWaitMs,
|
|
6462
|
+
),
|
|
6463
|
+
});
|
|
6448
6464
|
startupLaunchInputs.set(admittedInput, launch);
|
|
6449
6465
|
try {
|
|
6450
6466
|
return await executeLifecycleResponse(broker, operation, admittedInput, identity, cleanup);
|
|
6451
6467
|
} finally {
|
|
6452
6468
|
startupLaunchInputs.delete(admittedInput);
|
|
6469
|
+
startupBrokerDerivedAdmissions.delete(admittedInput);
|
|
6453
6470
|
startupAdmittedInputs.delete(admittedInput);
|
|
6454
6471
|
}
|
|
6455
6472
|
});
|
|
@@ -6477,6 +6494,8 @@ async function executeLifecycleResponse(
|
|
|
6477
6494
|
let readinessDeadline: number;
|
|
6478
6495
|
let terminationStartDeadline: number;
|
|
6479
6496
|
let outerCleanupDeadlineAt: number | undefined;
|
|
6497
|
+
const brokerAdmission = startupBrokerDerivedAdmissions.get(input);
|
|
6498
|
+
const preSpawnDeadlineAt = brokerAdmission?.preSpawnDeadlineAt;
|
|
6480
6499
|
if (launch.worktreePlan) {
|
|
6481
6500
|
const prepTimeouts = readPreparationTimeouts(input);
|
|
6482
6501
|
if (!prepTimeouts.ok) return fail("invalid_input", PREPARATION_TIMEOUT_INVALID_MESSAGE);
|
|
@@ -6507,6 +6526,10 @@ async function executeLifecycleResponse(
|
|
|
6507
6526
|
lifecycleDeadline = deadlines.lifecycleCleanupDeadlineAt;
|
|
6508
6527
|
readinessDeadline = deadlines.semanticReadyDeadlineAt;
|
|
6509
6528
|
terminationStartDeadline = deadlines.terminationStartDeadlineAt;
|
|
6529
|
+
if (preSpawnDeadlineAt !== undefined) {
|
|
6530
|
+
outerCleanupDeadlineAt = preSpawnDeadlineAt + deadlines.requestedReadinessTimeoutMs;
|
|
6531
|
+
lifecycleDeadline = outerCleanupDeadlineAt;
|
|
6532
|
+
}
|
|
6510
6533
|
}
|
|
6511
6534
|
|
|
6512
6535
|
if (!hasProcessIncarnationAuthority())
|
|
@@ -6530,6 +6553,9 @@ async function executeLifecycleResponse(
|
|
|
6530
6553
|
childOwnershipEstablished: false,
|
|
6531
6554
|
lifecycleCleanupDeadlineAt:
|
|
6532
6555
|
outerCleanupDeadlineAt ?? (childDeadlines as LifecycleDeadlines).lifecycleCleanupDeadlineAt,
|
|
6556
|
+
...(brokerAdmission
|
|
6557
|
+
? { admissionCleanupDeadlineAt: (childDeadlines as LifecycleDeadlines).lifecycleCleanupDeadlineAt }
|
|
6558
|
+
: {}),
|
|
6533
6559
|
...(plannedWorktreeIntent ? { worktree: plannedWorktreeIntent } : {}),
|
|
6534
6560
|
};
|
|
6535
6561
|
|
|
@@ -6631,7 +6657,22 @@ async function executeLifecycleResponse(
|
|
|
6631
6657
|
}
|
|
6632
6658
|
return mapPreparationFailure(error);
|
|
6633
6659
|
}
|
|
6634
|
-
if (
|
|
6660
|
+
if (preSpawnDeadlineAt !== undefined && brokerAdmission !== undefined) {
|
|
6661
|
+
const prepFinishedAt = timing.now();
|
|
6662
|
+
if (prepFinishedAt >= preSpawnDeadlineAt)
|
|
6663
|
+
return fail(
|
|
6664
|
+
"readiness_timeout",
|
|
6665
|
+
`Broker pre-spawn preparation exceeded its ${Math.max(0, preSpawnDeadlineAt - brokerAdmission.admittedAt)} ms allowance after ${Math.max(0, prepFinishedAt - brokerAdmission.admittedAt)} ms.`,
|
|
6666
|
+
);
|
|
6667
|
+
// Only broker-derived tuples restart here; exact caller deadlines remain unchanged.
|
|
6668
|
+
childDeadlines = deriveLifecycleDeadlines(
|
|
6669
|
+
prepFinishedAt,
|
|
6670
|
+
(childDeadlines as LifecycleDeadlines).requestedReadinessTimeoutMs,
|
|
6671
|
+
);
|
|
6672
|
+
readinessDeadline = childDeadlines.semanticReadyDeadlineAt;
|
|
6673
|
+
terminationStartDeadline = childDeadlines.terminationStartDeadlineAt;
|
|
6674
|
+
lifecycleDeadline = childDeadlines.lifecycleCleanupDeadlineAt;
|
|
6675
|
+
} else if (!launch.worktreePlan && timing.now() >= readinessDeadline)
|
|
6635
6676
|
return fail(
|
|
6636
6677
|
"readiness_timeout",
|
|
6637
6678
|
"Lifecycle preparation exhausted the semantic readiness deadline before spawning.",
|
|
@@ -7991,6 +8032,26 @@ export interface LifecycleExecutionOutcome {
|
|
|
7991
8032
|
deferredArtifactCleanup?: () => Promise<void>;
|
|
7992
8033
|
}
|
|
7993
8034
|
|
|
8035
|
+
export function classifyLateAdmissionSpawnFailure(
|
|
8036
|
+
response: BrokerResponse,
|
|
8037
|
+
effectIntent: LifecycleEffectIntentWithDeadline | undefined,
|
|
8038
|
+
now: number,
|
|
8039
|
+
messageSource: BrokerResponse,
|
|
8040
|
+
): BrokerResponse {
|
|
8041
|
+
if (
|
|
8042
|
+
response.ok ||
|
|
8043
|
+
response.error.code !== "spawn_failed" ||
|
|
8044
|
+
effectIntent?.childOwnershipEstablished !== true ||
|
|
8045
|
+
effectIntent.admissionCleanupDeadlineAt === undefined ||
|
|
8046
|
+
now < effectIntent.admissionCleanupDeadlineAt
|
|
8047
|
+
)
|
|
8048
|
+
return response;
|
|
8049
|
+
return {
|
|
8050
|
+
...response,
|
|
8051
|
+
error: { code: "terminal_uncertain", message: terminalUncertainStartupMessage(messageSource) },
|
|
8052
|
+
};
|
|
8053
|
+
}
|
|
8054
|
+
|
|
7994
8055
|
/** Returns the response together with every durable lifecycle fact needed for truthful replay. */
|
|
7995
8056
|
export async function executeLifecycle(
|
|
7996
8057
|
broker: Broker,
|
|
@@ -7999,6 +8060,7 @@ export async function executeLifecycle(
|
|
|
7999
8060
|
identity: string,
|
|
8000
8061
|
cleanup?: CleanupEvidence,
|
|
8001
8062
|
): Promise<LifecycleExecutionOutcome> {
|
|
8063
|
+
const timing = lifecycleTiming(broker);
|
|
8002
8064
|
let proofBudget = lifecycleProofBudgetFromInput(broker, input);
|
|
8003
8065
|
if (!proofBudget)
|
|
8004
8066
|
proofBudget = lifecycleProofBudgetFromEffectIntent(broker, broker.ledger.get(identity)?.effectIntent);
|
|
@@ -8052,16 +8114,25 @@ export async function executeLifecycle(
|
|
|
8052
8114
|
const binding = validateLifecycleDeleteMetadataBinding(broker, operation, input, identity, cleanup);
|
|
8053
8115
|
if (binding) return { response: binding };
|
|
8054
8116
|
}
|
|
8117
|
+
const reconciled = await reconcileLifecycleCleanup(
|
|
8118
|
+
broker,
|
|
8119
|
+
identity,
|
|
8120
|
+
cleanup,
|
|
8121
|
+
operation === "session.delete"
|
|
8122
|
+
? { ok: true, result: { sessionId: cleanup.sessionId } }
|
|
8123
|
+
: fail("spawn_failed", "No ready SDK endpoint remains available."),
|
|
8124
|
+
proofBudget,
|
|
8125
|
+
);
|
|
8055
8126
|
return {
|
|
8056
|
-
response:
|
|
8057
|
-
broker,
|
|
8058
|
-
identity,
|
|
8059
|
-
cleanup,
|
|
8127
|
+
response:
|
|
8060
8128
|
operation === "session.delete"
|
|
8061
|
-
?
|
|
8062
|
-
:
|
|
8063
|
-
|
|
8064
|
-
|
|
8129
|
+
? reconciled
|
|
8130
|
+
: classifyLateAdmissionSpawnFailure(
|
|
8131
|
+
reconciled,
|
|
8132
|
+
broker.ledger.get(identity)?.effectIntent,
|
|
8133
|
+
timing.now(),
|
|
8134
|
+
reconciled,
|
|
8135
|
+
),
|
|
8065
8136
|
};
|
|
8066
8137
|
}
|
|
8067
8138
|
const response = await executeLifecycleResponse(broker, operation, input, identity, cleanup);
|
|
@@ -8089,27 +8160,28 @@ export async function executeLifecycle(
|
|
|
8089
8160
|
expected,
|
|
8090
8161
|
evidence,
|
|
8091
8162
|
proofBudget,
|
|
8092
|
-
!response.ok
|
|
8163
|
+
!response.ok ? response.error.code === "ready_then_exited" : false,
|
|
8093
8164
|
);
|
|
8094
|
-
const startupFailure: LifecycleStartupFailureReceipt | undefined =
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8165
|
+
const startupFailure: LifecycleStartupFailureReceipt | undefined =
|
|
8166
|
+
evidence && !response.ok
|
|
8167
|
+
? {
|
|
8168
|
+
artifactDigest: evidence.digest,
|
|
8169
|
+
message: evidence.artifact.message,
|
|
8170
|
+
phase: evidence.artifact.phase,
|
|
8171
|
+
reason: evidence.artifact.reason,
|
|
8172
|
+
...(evidence.artifact.code === undefined
|
|
8173
|
+
? {}
|
|
8174
|
+
: { code: evidence.artifact.code, details: evidence.artifact.details }),
|
|
8175
|
+
rollback: {
|
|
8176
|
+
endpointGeneration: evidence.artifact.rollback.endpointGeneration,
|
|
8177
|
+
fenced: evidence.artifact.rollback.fenced,
|
|
8178
|
+
runtimeRemoved: evidence.artifact.rollback.runtimeRemoved,
|
|
8179
|
+
hostStopped: evidence.artifact.rollback.hostStopped,
|
|
8180
|
+
brokerRegistrationReleased: evidence.artifact.rollback.brokerRegistrationReleased,
|
|
8181
|
+
},
|
|
8182
|
+
...(cleanupProof ? { cleanupProof } : {}),
|
|
8183
|
+
}
|
|
8184
|
+
: undefined;
|
|
8113
8185
|
const durableEffectsBody: Omit<LifecycleDurableEffectsReceipt, "digest"> = {
|
|
8114
8186
|
...(priorDurableEffects?.worktree ? { worktree: priorDurableEffects.worktree } : {}),
|
|
8115
8187
|
...(evidence?.artifact.transcript
|
|
@@ -8228,8 +8300,10 @@ export async function executeLifecycle(
|
|
|
8228
8300
|
...(startupFailure ? { startupFailure } : {}),
|
|
8229
8301
|
}
|
|
8230
8302
|
: response;
|
|
8303
|
+
// The extended proof window may reconcile artifacts after the original
|
|
8304
|
+
// admission deadline, but cannot retroactively prove a timely spawn failure.
|
|
8231
8305
|
return {
|
|
8232
|
-
response: terminalResponse,
|
|
8306
|
+
response: classifyLateAdmissionSpawnFailure(terminalResponse, entry?.effectIntent, timing.now(), response),
|
|
8233
8307
|
...(durableEffects ? { durableEffects } : {}),
|
|
8234
8308
|
...(startupFailure ? { startupFailure } : {}),
|
|
8235
8309
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded exponential backoff tracker for broker recovery failures per agent dir.
|
|
3
|
+
* Tracks repeated recovery failures and implements exponential backoff with a cap
|
|
4
|
+
* to prevent repeated spawns from destabilizing the system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Configuration for recovery backoff behavior. */
|
|
8
|
+
export interface RecoveryBackoffConfig {
|
|
9
|
+
/** Initial backoff delay in milliseconds (default: 1000ms). */
|
|
10
|
+
initialDelayMs: number;
|
|
11
|
+
/** Maximum backoff delay in milliseconds (default: 30000ms = 30s). */
|
|
12
|
+
maxDelayMs: number;
|
|
13
|
+
/** Backoff multiplier applied each retry (default: 2). */
|
|
14
|
+
multiplier: number;
|
|
15
|
+
/** Maximum number of recovery attempts per session (default: 5). */
|
|
16
|
+
maxAttempts: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** State for a single agent dir's recovery backoff. */
|
|
20
|
+
export interface RecoveryBackoffState {
|
|
21
|
+
/** Timestamp of the last recovery failure. */
|
|
22
|
+
lastFailureAt: number;
|
|
23
|
+
/** Current backoff delay in milliseconds. */
|
|
24
|
+
currentDelayMs: number;
|
|
25
|
+
/** Number of consecutive failures. */
|
|
26
|
+
failureCount: number;
|
|
27
|
+
/** Whether recovery has been permanently stopped due to max attempts. */
|
|
28
|
+
stopped: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Session broker recovery ticks every 30 s (`SESSION_BROKER_RECOVERY_INTERVAL_MS`), so the
|
|
33
|
+
* first retry waits two ticks and the delay doubles up to 30 minutes: a broker that keeps
|
|
34
|
+
* failing costs about two spawn attempts per hour at steady state instead of one per tick.
|
|
35
|
+
* Recovery never stops for good on failures alone -- a broker outage must still heal once
|
|
36
|
+
* the broker comes back. Only a replaced runtime image ends recovery (session-runtime.ts).
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULTS: RecoveryBackoffConfig = {
|
|
39
|
+
initialDelayMs: 60_000,
|
|
40
|
+
maxDelayMs: 30 * 60_000,
|
|
41
|
+
multiplier: 2,
|
|
42
|
+
maxAttempts: Number.POSITIVE_INFINITY,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Manages per-agent-dir recovery backoff state. */
|
|
46
|
+
export class RecoveryBackoffTracker {
|
|
47
|
+
#state = new Map<string, RecoveryBackoffState>();
|
|
48
|
+
#config: RecoveryBackoffConfig;
|
|
49
|
+
#clock: { now(): number } = { now: Date.now };
|
|
50
|
+
|
|
51
|
+
constructor(config: Partial<RecoveryBackoffConfig> = {}) {
|
|
52
|
+
this.#config = { ...DEFAULTS, ...config };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Test hook: inject a custom clock. */
|
|
56
|
+
setClockForTest(clock: { now(): number }): void {
|
|
57
|
+
this.#clock = clock;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if recovery should proceed now, considering backoff.
|
|
62
|
+
* Returns true if sufficient time has elapsed since the last failure.
|
|
63
|
+
*/
|
|
64
|
+
canAttemptRecovery(agentDir: string): boolean {
|
|
65
|
+
const state = this.#state.get(agentDir);
|
|
66
|
+
if (!state) return true;
|
|
67
|
+
if (state.stopped) return false;
|
|
68
|
+
|
|
69
|
+
const now = this.#clock.now();
|
|
70
|
+
return now >= state.lastFailureAt + state.currentDelayMs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Record a recovery failure and update backoff state.
|
|
75
|
+
* Returns true if recovery should continue to be attempted, false if max attempts reached.
|
|
76
|
+
*/
|
|
77
|
+
recordFailure(agentDir: string): boolean {
|
|
78
|
+
let state = this.#state.get(agentDir);
|
|
79
|
+
if (!state) {
|
|
80
|
+
state = {
|
|
81
|
+
lastFailureAt: this.#clock.now(),
|
|
82
|
+
currentDelayMs: this.#config.initialDelayMs,
|
|
83
|
+
failureCount: 1,
|
|
84
|
+
stopped: false,
|
|
85
|
+
};
|
|
86
|
+
this.#state.set(agentDir, state);
|
|
87
|
+
} else {
|
|
88
|
+
state.lastFailureAt = this.#clock.now();
|
|
89
|
+
state.failureCount += 1;
|
|
90
|
+
|
|
91
|
+
// Apply exponential backoff
|
|
92
|
+
state.currentDelayMs = Math.min(state.currentDelayMs * this.#config.multiplier, this.#config.maxDelayMs);
|
|
93
|
+
|
|
94
|
+
// Stop if we've hit max attempts
|
|
95
|
+
if (state.failureCount >= this.#config.maxAttempts) {
|
|
96
|
+
state.stopped = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return !state.stopped;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Record a successful recovery and reset backoff state.
|
|
105
|
+
*/
|
|
106
|
+
recordSuccess(agentDir: string): void {
|
|
107
|
+
this.#state.delete(agentDir);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the current backoff state for an agent dir (for testing/diagnostics).
|
|
112
|
+
*/
|
|
113
|
+
getState(agentDir: string): RecoveryBackoffState | undefined {
|
|
114
|
+
return this.#state.get(agentDir);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get milliseconds until the next recovery attempt can proceed.
|
|
119
|
+
*/
|
|
120
|
+
getBackoffWaitMs(agentDir: string): number {
|
|
121
|
+
const state = this.#state.get(agentDir);
|
|
122
|
+
if (!state) return 0;
|
|
123
|
+
|
|
124
|
+
const now = this.#clock.now();
|
|
125
|
+
const nextAttemptAt = state.lastFailureAt + state.currentDelayMs;
|
|
126
|
+
return Math.max(0, nextAttemptAt - now);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reset all backoff state (for testing).
|
|
131
|
+
*/
|
|
132
|
+
resetForTest(): void {
|
|
133
|
+
this.#state.clear();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -6,7 +6,11 @@ import { currentExecutablePath } from "@gajae-code/natives";
|
|
|
6
6
|
|
|
7
7
|
import internalSourceMarker from "./internal-source-marker-2178.txt" with { type: "file" };
|
|
8
8
|
|
|
9
|
-
export type SdkInternalAction =
|
|
9
|
+
export type SdkInternalAction =
|
|
10
|
+
| "broker-internal"
|
|
11
|
+
| "broker-trampoline-internal"
|
|
12
|
+
| "session-host-internal"
|
|
13
|
+
| "stderr-drain-internal";
|
|
10
14
|
|
|
11
15
|
export type SdkInternalSpawnCommand =
|
|
12
16
|
| {
|
|
@@ -271,6 +275,15 @@ export function sdkInternalRuntimeImage(): string | undefined {
|
|
|
271
275
|
return publishedRuntimeImage({});
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
/** Identity of a runtime image: enough to detect replacement at the same path. */
|
|
279
|
+
export interface SdkInternalRuntimeImageIdentity {
|
|
280
|
+
path: string;
|
|
281
|
+
dev: number;
|
|
282
|
+
ino: number;
|
|
283
|
+
mtimeMs: number;
|
|
284
|
+
size: number;
|
|
285
|
+
}
|
|
286
|
+
|
|
274
287
|
/** A stat that has not answered by here is inconclusive, never proof of absence. */
|
|
275
288
|
const RUNTIME_IMAGE_PROBE_TIMEOUT_MS = 1_000;
|
|
276
289
|
|
|
@@ -308,6 +321,80 @@ export async function isSdkInternalRuntimeImagePresent(file: string): Promise<bo
|
|
|
308
321
|
}
|
|
309
322
|
}
|
|
310
323
|
|
|
324
|
+
const RUNTIME_IMAGE_PROBE_TIMED_OUT = Symbol("runtime-image-probe-timed-out");
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* `fsp.stat` bounded by the runtime-image probe timeout. A metadata request on a
|
|
328
|
+
* stalled mount can never settle; the timeout sentinel lets callers treat that as
|
|
329
|
+
* inconclusive instead of pending forever.
|
|
330
|
+
*/
|
|
331
|
+
async function statRuntimeImageBounded(file: string): Promise<fs.Stats | typeof RUNTIME_IMAGE_PROBE_TIMED_OUT> {
|
|
332
|
+
const timedOut = Promise.withResolvers<typeof RUNTIME_IMAGE_PROBE_TIMED_OUT>();
|
|
333
|
+
const timer: NodeJS.Timeout = setTimeout(
|
|
334
|
+
() => timedOut.resolve(RUNTIME_IMAGE_PROBE_TIMED_OUT),
|
|
335
|
+
RUNTIME_IMAGE_PROBE_TIMEOUT_MS,
|
|
336
|
+
);
|
|
337
|
+
try {
|
|
338
|
+
return await Promise.race([fsp.stat(file), timedOut.promise]);
|
|
339
|
+
} finally {
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Capture the runtime image identity (dev, ino, mtimeMs, size) at startup.
|
|
346
|
+
* Used to detect if the binary at the same path has been replaced.
|
|
347
|
+
*/
|
|
348
|
+
export async function captureRuntimeImageIdentity(file: string): Promise<SdkInternalRuntimeImageIdentity | undefined> {
|
|
349
|
+
try {
|
|
350
|
+
const resolved = path.resolve(file);
|
|
351
|
+
const stats = await statRuntimeImageBounded(resolved);
|
|
352
|
+
// A timed-out probe leaves the identity unknown, like any other failure.
|
|
353
|
+
if (stats === RUNTIME_IMAGE_PROBE_TIMED_OUT || !stats.isFile()) return undefined;
|
|
354
|
+
return {
|
|
355
|
+
path: resolved,
|
|
356
|
+
dev: stats.dev,
|
|
357
|
+
ino: stats.ino,
|
|
358
|
+
mtimeMs: stats.mtimeMs,
|
|
359
|
+
size: stats.size,
|
|
360
|
+
};
|
|
361
|
+
} catch {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Check if the runtime image has been replaced since startup.
|
|
368
|
+
* Returns `true` if the path is gone, its dev/ino differ from the startup identity,
|
|
369
|
+
* or its size or modification time changed (an in-place rewrite keeps the inode).
|
|
370
|
+
* Returns `false` (inconclusive) if the file exists and matches, or on any error.
|
|
371
|
+
*/
|
|
372
|
+
export async function isSdkInternalRuntimeImageReplaced(
|
|
373
|
+
startupIdentity: SdkInternalRuntimeImageIdentity | undefined,
|
|
374
|
+
): Promise<boolean> {
|
|
375
|
+
if (startupIdentity === undefined) return false;
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const stats = await statRuntimeImageBounded(startupIdentity.path);
|
|
379
|
+
// A timed-out probe is inconclusive, not proof of replacement.
|
|
380
|
+
if (stats === RUNTIME_IMAGE_PROBE_TIMED_OUT) return false;
|
|
381
|
+
if (!stats.isFile()) return false; // Not a file, but not proven gone
|
|
382
|
+
// dev/ino catch a replacement at the same path; size and mtime catch an
|
|
383
|
+
// in-place rewrite that keeps the inode (mtime also covers same-size bytes).
|
|
384
|
+
return (
|
|
385
|
+
stats.dev !== startupIdentity.dev ||
|
|
386
|
+
stats.ino !== startupIdentity.ino ||
|
|
387
|
+
stats.size !== startupIdentity.size ||
|
|
388
|
+
stats.mtimeMs !== startupIdentity.mtimeMs
|
|
389
|
+
);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
// Only ENOENT/ENOTDIR prove absence
|
|
392
|
+
if (isProvenRuntimeImageAbsence(error)) return true;
|
|
393
|
+
// Inconclusive on other errors
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
311
398
|
/** Resolve the production descriptor from the statically imported marker and current Bun runtime evidence. */
|
|
312
399
|
export function resolveSdkInternalSpawnCommand(action: SdkInternalAction): SdkInternalSpawnCommand {
|
|
313
400
|
return resolveSdkInternalSpawnCommandWithEvidence(action, {});
|