@gajae-code/coding-agent 0.16.4 → 0.16.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 +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +33 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +4 -2
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/session-state-lock.ts +51 -8
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +387 -48
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +2 -2
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -382,6 +382,11 @@ export class MCPManager {
|
|
|
382
382
|
pending?.delete(error);
|
|
383
383
|
if (pending?.size === 0) this.#pendingAcquireCleanups.delete(name);
|
|
384
384
|
}
|
|
385
|
+
#pruneSettledPendingAcquireCleanups(name: string): void {
|
|
386
|
+
for (const error of this.#pendingAcquireCleanups.get(name) ?? []) {
|
|
387
|
+
if (!this.#pool.hasPendingAcquireCleanup(error)) this.#removePendingAcquireCleanup(name, error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
385
390
|
|
|
386
391
|
#retainPendingAcquireCleanup(name: string, error: MCPPoolAcquireAbortError): void {
|
|
387
392
|
if (error.cleanupGeneration === undefined || !error.cleanup || !this.#pool.hasPendingAcquireCleanup(error))
|
|
@@ -393,7 +398,11 @@ export class MCPManager {
|
|
|
393
398
|
}
|
|
394
399
|
if (pending.has(error)) return;
|
|
395
400
|
pending.add(error);
|
|
396
|
-
void error.cleanup
|
|
401
|
+
void error.cleanup
|
|
402
|
+
.finally(() => {
|
|
403
|
+
if (!this.#pool.hasPendingAcquireCleanup(error)) this.#removePendingAcquireCleanup(name, error);
|
|
404
|
+
})
|
|
405
|
+
.catch(() => {});
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
#retainConnectionCleanupFailure(name: string, error: unknown): MCPConnectionCleanupFailure | undefined {
|
|
@@ -518,6 +527,7 @@ export class MCPManager {
|
|
|
518
527
|
onRequest: (method, params) => this.#handleServerRequest(method, params),
|
|
519
528
|
onCleanupPending: error => this.#retainPendingAcquireCleanup(name, error),
|
|
520
529
|
});
|
|
530
|
+
this.#pruneSettledPendingAcquireCleanups(name);
|
|
521
531
|
if (trackManagerLease) this.#leaseByConnection.set(lease.connection, lease);
|
|
522
532
|
if (!this.#toolsOnly) lease.updateRoots(this.#getRoots().roots);
|
|
523
533
|
return lease;
|
|
@@ -1040,6 +1050,10 @@ export class MCPManager {
|
|
|
1040
1050
|
const connectionTasks: ConnectionTask[] = [];
|
|
1041
1051
|
|
|
1042
1052
|
for (const [name, config] of Object.entries(configs)) {
|
|
1053
|
+
// Another manager sharing the pool may have retried and completed an
|
|
1054
|
+
// abandoned-acquire cleanup. Retire this manager's observation before
|
|
1055
|
+
// applying its reconnect fence; pool ownership remains authoritative.
|
|
1056
|
+
this.#pruneSettledPendingAcquireCleanups(name);
|
|
1043
1057
|
const pendingAcquire = this.#pendingAcquireCleanups.get(name);
|
|
1044
1058
|
const pendingCleanup = this.#pendingConnectionCleanups.get(name);
|
|
1045
1059
|
if (pendingCleanup || pendingAcquire) {
|
|
@@ -1781,7 +1795,11 @@ export class MCPManager {
|
|
|
1781
1795
|
|
|
1782
1796
|
let closeError: unknown;
|
|
1783
1797
|
try {
|
|
1784
|
-
|
|
1798
|
+
// A transport may ignore its abort signal while the initial connection
|
|
1799
|
+
// is still opening. The pool owns that abandoned flight and closes any
|
|
1800
|
+
// late connection, so disconnect must not wait for the uncooperative
|
|
1801
|
+
// opener. Already-materialized cleanup failures are still retried here.
|
|
1802
|
+
await this.#closePendingConnectionCleanup(name, false);
|
|
1785
1803
|
} catch (error) {
|
|
1786
1804
|
closeError = error;
|
|
1787
1805
|
}
|
|
@@ -462,7 +462,7 @@ describe("MCPConnectionPool", () => {
|
|
|
462
462
|
expect(recoveryTransports).toHaveLength(2);
|
|
463
463
|
});
|
|
464
464
|
|
|
465
|
-
test("manager disconnect
|
|
465
|
+
test("manager disconnect preserves late cleanup failure until pool retry clears every manager fence", async () => {
|
|
466
466
|
const firstFlight = Promise.withResolvers<MCPServerConnection>();
|
|
467
467
|
const firstStarted = Promise.withResolvers<void>();
|
|
468
468
|
const cleanupTransport = new FakeTransport();
|
|
@@ -481,7 +481,7 @@ describe("MCPConnectionPool", () => {
|
|
|
481
481
|
firstStarted.resolve();
|
|
482
482
|
return firstFlight.promise;
|
|
483
483
|
}
|
|
484
|
-
return connection(name, cfg, new
|
|
484
|
+
return connection(name, cfg, new ToolListTransport());
|
|
485
485
|
},
|
|
486
486
|
});
|
|
487
487
|
const manager = new MCPManager(".", null, { pool, sessionId: "disconnect-cleanup" });
|
|
@@ -492,16 +492,19 @@ describe("MCPConnectionPool", () => {
|
|
|
492
492
|
});
|
|
493
493
|
await firstStarted.promise;
|
|
494
494
|
controller.abort(new Error("abort before deferred cleanup failure"));
|
|
495
|
-
await
|
|
495
|
+
const abortedAcquire = await operation.catch(error => error);
|
|
496
|
+
expect(abortedAcquire).toBeInstanceOf(MCPPoolAcquireAbortError);
|
|
497
|
+
if (!(abortedAcquire instanceof MCPPoolAcquireAbortError)) throw new Error("Expected aborted acquisition");
|
|
496
498
|
expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
|
|
497
499
|
|
|
498
|
-
|
|
500
|
+
await manager.disconnectServer("server");
|
|
499
501
|
const fenced = await manager.connectServers({ server: serverConfig }, {});
|
|
500
502
|
expect(fenced.errors.has("server")).toBe(true);
|
|
501
503
|
expect(opens).toBe(1);
|
|
502
504
|
const primaryFailure = new Error("late connection cleanup failed");
|
|
503
505
|
firstFlight.reject(new MCPConnectionCleanupFailure(primaryFailure, cleanupTransport));
|
|
504
|
-
|
|
506
|
+
await expect(abortedAcquire.cleanup).rejects.toBeInstanceOf(MCPConnectionCleanupFailure);
|
|
507
|
+
const disconnectError = await manager.disconnectServer("server").catch(error => error);
|
|
505
508
|
expect(disconnectError).toBeInstanceOf(MCPConnectionCleanupFailure);
|
|
506
509
|
if (!(disconnectError instanceof MCPConnectionCleanupFailure)) {
|
|
507
510
|
throw new Error("Expected retained disconnect cleanup failure");
|
|
@@ -513,14 +516,20 @@ describe("MCPConnectionPool", () => {
|
|
|
513
516
|
expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
|
|
514
517
|
expect(pool.pendingAcquireCleanupCountForTests).toBe(1);
|
|
515
518
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
expect(manager.pendingConnectionCleanupCountForTests).toBe(0);
|
|
519
|
-
expect(pool.pendingAcquireCleanupCountForTests).toBe(0);
|
|
520
|
-
await manager.withPreparedLease("server", serverConfig, async lease => {
|
|
519
|
+
const recoveryManager = new MCPManager(".", null, { pool, sessionId: "disconnect-cleanup" });
|
|
520
|
+
await recoveryManager.withPreparedLease("server", serverConfig, async lease => {
|
|
521
521
|
expect(lease.serverName).toBe("server");
|
|
522
522
|
});
|
|
523
|
-
expect(
|
|
523
|
+
expect(cleanupAttempts).toBe(2);
|
|
524
|
+
expect(pool.pendingAcquireCleanupCountForTests).toBe(0);
|
|
525
|
+
expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
|
|
526
|
+
|
|
527
|
+
const recovered = await manager.connectServers({ server: serverConfig }, {});
|
|
528
|
+
expect(recovered.errors.size).toBe(0);
|
|
529
|
+
expect(recovered.connectedServers).toEqual(["server"]);
|
|
530
|
+
expect(manager.pendingConnectionCleanupCountForTests).toBe(0);
|
|
531
|
+
expect(opens).toBe(3);
|
|
532
|
+
await recoveryManager.disconnectAll();
|
|
524
533
|
await manager.disconnectAll();
|
|
525
534
|
});
|
|
526
535
|
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -41,7 +41,9 @@ import {
|
|
|
41
41
|
executeLifecycle,
|
|
42
42
|
isCanonicalSessionId,
|
|
43
43
|
prepareSpawnChildHostLaunch,
|
|
44
|
+
readSessionLifecycleFailure,
|
|
44
45
|
validateBrokerModelPresetSync,
|
|
46
|
+
writeEffectMarker,
|
|
45
47
|
} from "./lifecycle";
|
|
46
48
|
import {
|
|
47
49
|
type LifecycleDurableEffectsReceipt,
|
|
@@ -724,6 +726,7 @@ function credentialFreeLifecycleResponse(value: unknown): unknown {
|
|
|
724
726
|
type LifecycleReplayEndpoint = {
|
|
725
727
|
endpoint: Record<string, unknown>;
|
|
726
728
|
endpointGeneration: number;
|
|
729
|
+
endpointIncarnation: string;
|
|
727
730
|
pid: number;
|
|
728
731
|
endpointMtimeMs: number;
|
|
729
732
|
};
|
|
@@ -1588,12 +1591,34 @@ export class Broker {
|
|
|
1588
1591
|
).claim;
|
|
1589
1592
|
return spawnFailureError(failure);
|
|
1590
1593
|
}
|
|
1594
|
+
const { pid, processIncarnation: incarnation } = launched.proof;
|
|
1595
|
+
if (pid === undefined || incarnation === undefined) {
|
|
1596
|
+
const failure: SpawnSubstrateFailure = {
|
|
1597
|
+
substrateKind: launched.proof.substrateKind,
|
|
1598
|
+
code: "substrate_proof_failed",
|
|
1599
|
+
message: "session.spawn substrate lacks lifecycle process authority",
|
|
1600
|
+
};
|
|
1601
|
+
await this.#releaseUnownedSubstrate(provider, launchedProof);
|
|
1602
|
+
launchedProof = undefined;
|
|
1603
|
+
current = (
|
|
1604
|
+
await store.persistTransition(lifecycleIdentity, {
|
|
1605
|
+
claimId: current.claimId,
|
|
1606
|
+
from: "substrate_starting",
|
|
1607
|
+
to: "pre_send_rejected",
|
|
1608
|
+
failure,
|
|
1609
|
+
})
|
|
1610
|
+
).claim;
|
|
1611
|
+
return spawnFailureError(failure);
|
|
1612
|
+
}
|
|
1613
|
+
const marker = { pid, incarnation, effectMarker: prep.effectMarker };
|
|
1614
|
+
await writeEffectMarker(prep.stateRoot, prep.childId, marker);
|
|
1591
1615
|
const registration = await this.#spawnPromptLayer.awaitRegistration({
|
|
1592
1616
|
childId: prep.childId,
|
|
1593
1617
|
cwd: prep.cwd,
|
|
1594
1618
|
stateRoot: prep.stateRoot,
|
|
1595
1619
|
});
|
|
1596
1620
|
if (!registration.ok) {
|
|
1621
|
+
const startupFailure = await readSessionLifecycleFailure(prep.stateRoot, prep.childId, marker);
|
|
1597
1622
|
await this.#releaseUnownedSubstrate(provider, launchedProof);
|
|
1598
1623
|
launchedProof = undefined;
|
|
1599
1624
|
current = (
|
|
@@ -1603,7 +1628,11 @@ export class Broker {
|
|
|
1603
1628
|
to: "uncertain",
|
|
1604
1629
|
})
|
|
1605
1630
|
).claim;
|
|
1606
|
-
return error(
|
|
1631
|
+
return error(
|
|
1632
|
+
"terminal_uncertain",
|
|
1633
|
+
"session.spawn child registration is uncertain" +
|
|
1634
|
+
(startupFailure ? ` (${startupFailure.phase}/${startupFailure.reason})` : ""),
|
|
1635
|
+
);
|
|
1607
1636
|
}
|
|
1608
1637
|
pinnedRegistration = registration.registration;
|
|
1609
1638
|
const now = Date.now();
|
|
@@ -2404,26 +2433,35 @@ export class Broker {
|
|
|
2404
2433
|
)
|
|
2405
2434
|
return;
|
|
2406
2435
|
|
|
2407
|
-
//
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2436
|
+
// Give every reclaimed generation a fresh quarantine name. Older brokers
|
|
2437
|
+
// used only the lock inode hash; if that tombstone survived a crash, the
|
|
2438
|
+
// deterministic name collided forever and left the dead canonical lock in
|
|
2439
|
+
// place. The random suffix preserves no-replace rename semantics without
|
|
2440
|
+
// letting retained cleanup debris block takeover.
|
|
2441
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
2442
|
+
const tombstone = path.join(
|
|
2443
|
+
path.dirname(this.#lock),
|
|
2444
|
+
`.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}-${randomBytes(8).toString("hex")}`,
|
|
2445
|
+
);
|
|
2446
|
+
try {
|
|
2447
|
+
await fs.rename(this.#lock, tombstone);
|
|
2448
|
+
return;
|
|
2449
|
+
} catch (e) {
|
|
2450
|
+
const code = (e as NodeJS.ErrnoException).code;
|
|
2451
|
+
if (code === "EEXIST" || code === "ENOTEMPTY") continue;
|
|
2452
|
+
if (["ENOENT", "EISDIR", "ENOTDIR"].includes(code ?? "")) return;
|
|
2453
|
+
if (code === "EPERM") {
|
|
2454
|
+
try {
|
|
2455
|
+
await fs.lstat(tombstone);
|
|
2456
|
+
continue;
|
|
2457
|
+
} catch (statError) {
|
|
2458
|
+
if ((statError as NodeJS.ErrnoException).code !== "ENOENT") throw statError;
|
|
2459
|
+
}
|
|
2423
2460
|
}
|
|
2461
|
+
throw e;
|
|
2424
2462
|
}
|
|
2425
|
-
throw e;
|
|
2426
2463
|
}
|
|
2464
|
+
throw new Error(`Broker lock quarantine namespace is saturated for ${this.#lock}`);
|
|
2427
2465
|
}
|
|
2428
2466
|
async #releaseOwnedLock(): Promise<void> {
|
|
2429
2467
|
try {
|
|
@@ -2549,6 +2587,11 @@ export class Broker {
|
|
|
2549
2587
|
startedAt: now,
|
|
2550
2588
|
heartbeatAt: now,
|
|
2551
2589
|
};
|
|
2590
|
+
// Readiness must not be externally visible until the initial session
|
|
2591
|
+
// checkpoint settles. The bootstrap watchdog owns this pre-publication
|
|
2592
|
+
// interval; publishing first allowed it to kill an endpoint already handed
|
|
2593
|
+
// to callers when a legitimate index-lock wait outlived the fence.
|
|
2594
|
+
await this.#checkpointSessionHeartbeats();
|
|
2552
2595
|
this.#publication = await publishBrokerDiscovery(this.settings.agentDir, this.discovery);
|
|
2553
2596
|
this.#publicationState = "healthy-owned";
|
|
2554
2597
|
this.#publishedAt = process.hrtime.bigint();
|
|
@@ -2560,7 +2603,6 @@ export class Broker {
|
|
|
2560
2603
|
void this.#watchPublication();
|
|
2561
2604
|
void this.#reapSpawnOrphans();
|
|
2562
2605
|
}, cadenceMs);
|
|
2563
|
-
await this.#checkpointSessionHeartbeats();
|
|
2564
2606
|
return this.discovery;
|
|
2565
2607
|
} catch (error) {
|
|
2566
2608
|
await this.#transport?.stop();
|
|
@@ -2862,6 +2904,8 @@ export class Broker {
|
|
|
2862
2904
|
endpointMtimeMs <= 0
|
|
2863
2905
|
)
|
|
2864
2906
|
return error("endpoint_stale", "session endpoint authority is incomplete");
|
|
2907
|
+
const currentIncarnation = endpointIncarnation(record, sessionId);
|
|
2908
|
+
if (!currentIncarnation) return error("endpoint_stale", "session endpoint incarnation is unavailable");
|
|
2865
2909
|
const endpoint = await this.#readEndpoint(record, {});
|
|
2866
2910
|
if (!endpoint.ok) return endpoint;
|
|
2867
2911
|
if (endpoint.result === null || typeof endpoint.result !== "object" || Array.isArray(endpoint.result))
|
|
@@ -2869,6 +2913,7 @@ export class Broker {
|
|
|
2869
2913
|
return {
|
|
2870
2914
|
endpoint: endpoint.result as Record<string, unknown>,
|
|
2871
2915
|
endpointGeneration: record.endpointGeneration,
|
|
2916
|
+
endpointIncarnation: currentIncarnation,
|
|
2872
2917
|
pid: record.pid,
|
|
2873
2918
|
endpointMtimeMs,
|
|
2874
2919
|
};
|
|
@@ -3041,16 +3086,21 @@ export class Broker {
|
|
|
3041
3086
|
if (this.#chains.get(target) === chain) this.#chains.delete(target);
|
|
3042
3087
|
}
|
|
3043
3088
|
}
|
|
3044
|
-
#storeSessionListCursor(cursor: SessionListCursor, replacingToken?: string): string
|
|
3089
|
+
#storeSessionListCursor(cursor: SessionListCursor, replacingToken?: string): string {
|
|
3045
3090
|
const now = Date.now();
|
|
3046
3091
|
for (const [token, stored] of this.#sessionListCursors) {
|
|
3047
3092
|
if (stored.expiresAt <= now) this.#sessionListCursors.delete(token);
|
|
3048
3093
|
}
|
|
3049
|
-
const replacing = replacingToken !== undefined && this.#sessionListCursors.has(replacingToken);
|
|
3050
|
-
if (!replacing && this.#sessionListCursors.size >= SESSION_LIST_MAX_CURSORS)
|
|
3051
|
-
return error("invalid_input", "session.list cursor capacity is exhausted");
|
|
3052
|
-
const token = randomBytes(24).toString("base64url");
|
|
3053
3094
|
if (replacingToken !== undefined) this.#sessionListCursors.delete(replacingToken);
|
|
3095
|
+
// Pagination cursors are a paging convenience, not durable state (#5370).
|
|
3096
|
+
// Evict the oldest cursor when the budget is full so abandoned or partial
|
|
3097
|
+
// paginations degrade gracefully instead of failing unrelated session ops.
|
|
3098
|
+
while (this.#sessionListCursors.size >= SESSION_LIST_MAX_CURSORS) {
|
|
3099
|
+
const oldest = this.#sessionListCursors.keys().next();
|
|
3100
|
+
if (oldest.done) break;
|
|
3101
|
+
this.#sessionListCursors.delete(oldest.value);
|
|
3102
|
+
}
|
|
3103
|
+
const token = randomBytes(24).toString("base64url");
|
|
3054
3104
|
this.#sessionListCursors.set(token, cursor);
|
|
3055
3105
|
return token;
|
|
3056
3106
|
}
|
|
@@ -3121,7 +3171,6 @@ export class Broker {
|
|
|
3121
3171
|
{ ...snapshot, offset, expiresAt: Date.now() + SESSION_LIST_CURSOR_TTL_MS },
|
|
3122
3172
|
typeof cursor === "string" ? cursor : undefined,
|
|
3123
3173
|
);
|
|
3124
|
-
if (isBrokerResponse(continuationCursor)) return continuationCursor;
|
|
3125
3174
|
return {
|
|
3126
3175
|
ok: true,
|
|
3127
3176
|
result: {
|
|
@@ -3326,15 +3375,32 @@ export class Broker {
|
|
|
3326
3375
|
(operation === "session.create" || operation === "session.fork" || operation === "session.resume") &&
|
|
3327
3376
|
typeof (replay.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
|
|
3328
3377
|
) {
|
|
3329
|
-
const
|
|
3330
|
-
|
|
3331
|
-
|
|
3378
|
+
const replayResult = objectRecord(replay.result);
|
|
3379
|
+
const replaySessionId = (replay.result as { sessionId: string }).sessionId;
|
|
3380
|
+
const replayIncarnation =
|
|
3381
|
+
typeof replayResult?.endpointIncarnation === "string" &&
|
|
3382
|
+
/^[a-f0-9]{64}$/.test(replayResult.endpointIncarnation)
|
|
3383
|
+
? replayResult.endpointIncarnation
|
|
3384
|
+
: endpointIncarnation(
|
|
3385
|
+
{
|
|
3386
|
+
endpointGeneration: replayResult?.endpointGeneration as number,
|
|
3387
|
+
endpointMtimeMs: replayResult?.endpointMtimeMs as number,
|
|
3388
|
+
pid: replayResult?.pid as number,
|
|
3389
|
+
},
|
|
3390
|
+
replaySessionId,
|
|
3391
|
+
);
|
|
3392
|
+
if (!replayIncarnation)
|
|
3393
|
+
return error("endpoint_stale", "lifecycle replay lacks original endpoint authority");
|
|
3394
|
+
const refreshed = await this.#readLifecycleReplayEndpoint(replaySessionId);
|
|
3332
3395
|
if (isBrokerResponse(refreshed)) return refreshed;
|
|
3396
|
+
if (refreshed.endpointIncarnation !== replayIncarnation)
|
|
3397
|
+
return error("endpoint_stale", "lifecycle replay target was replaced");
|
|
3333
3398
|
return {
|
|
3334
3399
|
ok: true,
|
|
3335
3400
|
result: {
|
|
3336
3401
|
...(replay.result as Record<string, unknown>),
|
|
3337
3402
|
endpointGeneration: refreshed.endpointGeneration,
|
|
3403
|
+
endpointIncarnation: refreshed.endpointIncarnation,
|
|
3338
3404
|
pid: refreshed.pid,
|
|
3339
3405
|
endpointMtimeMs: refreshed.endpointMtimeMs,
|
|
3340
3406
|
endpoint: refreshed.endpoint,
|
|
@@ -3347,6 +3413,12 @@ export class Broker {
|
|
|
3347
3413
|
const response = outcome.response;
|
|
3348
3414
|
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
3349
3415
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
3416
|
+
...(operation === "session.delete" &&
|
|
3417
|
+
typeof input.sessionId === "string" &&
|
|
3418
|
+
lifecycleResponseState(response) === "terminal_uncertain" &&
|
|
3419
|
+
!pendingCleanupSessionId(response)
|
|
3420
|
+
? { intendedSessionId: input.sessionId }
|
|
3421
|
+
: {}),
|
|
3350
3422
|
response: storedResponse,
|
|
3351
3423
|
responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
|
|
3352
3424
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
@@ -3366,6 +3438,12 @@ export class Broker {
|
|
|
3366
3438
|
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
3367
3439
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
3368
3440
|
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
3441
|
+
...(operation === "session.delete" &&
|
|
3442
|
+
typeof input.sessionId === "string" &&
|
|
3443
|
+
lifecycleResponseState(response) === "terminal_uncertain" &&
|
|
3444
|
+
!pendingCleanupSessionId(response)
|
|
3445
|
+
? { intendedSessionId: input.sessionId }
|
|
3446
|
+
: {}),
|
|
3369
3447
|
response: storedResponse,
|
|
3370
3448
|
responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
|
|
3371
3449
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
@@ -3377,8 +3455,21 @@ export class Broker {
|
|
|
3377
3455
|
const outcome = await executeLifecycle(this, operation, input, identity);
|
|
3378
3456
|
const response = outcome.response;
|
|
3379
3457
|
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
3458
|
+
// Record the refusal's own target session so the fence it may leave
|
|
3459
|
+
// is scoped to that session. Without this, a session.delete refusal
|
|
3460
|
+
// for X persists as an unbound terminal_uncertain row and fences
|
|
3461
|
+
// every later delete for unrelated sessions (#5364). A refusal that
|
|
3462
|
+
// truly cannot name its target fences nothing (see
|
|
3463
|
+
// hasUncertainCleanupForSession).
|
|
3464
|
+
const refusalSessionId =
|
|
3465
|
+
operation === "session.delete" && typeof input.sessionId === "string" ? input.sessionId : undefined;
|
|
3380
3466
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
3381
3467
|
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
3468
|
+
...(refusalSessionId !== undefined &&
|
|
3469
|
+
lifecycleResponseState(response) === "terminal_uncertain" &&
|
|
3470
|
+
!pendingCleanupSessionId(response)
|
|
3471
|
+
? { intendedSessionId: refusalSessionId }
|
|
3472
|
+
: {}),
|
|
3382
3473
|
resultSessionId:
|
|
3383
3474
|
response.ok && typeof (response.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
|
|
3384
3475
|
? (response.result as { sessionId: string }).sessionId
|