@gajae-code/coding-agent 0.15.5 → 0.16.0
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 +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -3,14 +3,21 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
5
5
|
import {
|
|
6
|
+
canonicalSessionCwd,
|
|
6
7
|
SessionIndex as DefaultSessionIndex,
|
|
7
8
|
type IndexedSession,
|
|
8
9
|
isSessionAuthorityEligible,
|
|
10
|
+
resolveSessionLocator,
|
|
9
11
|
type SessionIndex,
|
|
10
12
|
} from "../broker/session-index";
|
|
11
13
|
import { cancellableSleep, lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
12
14
|
import { SdkClient, SdkClientError, type SdkDispatchContext, type SdkDispatchHandler } from "../client/client";
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
endpointDirectory,
|
|
17
|
+
readSdkBrokerDiscovery,
|
|
18
|
+
readSdkSessionEndpoint,
|
|
19
|
+
type SdkSessionEndpoint,
|
|
20
|
+
} from "../client/discovery";
|
|
14
21
|
import {
|
|
15
22
|
type ActivatedPreparedSession,
|
|
16
23
|
type PreparedSessionActivationClient,
|
|
@@ -19,6 +26,14 @@ import {
|
|
|
19
26
|
} from "../session-activation";
|
|
20
27
|
import { ACP_SESSION_RECONNECT, SESSION_REQUEST_TIMEOUT_MS } from "../session-reconnect";
|
|
21
28
|
|
|
29
|
+
export interface SessionEndpointIdentity {
|
|
30
|
+
readonly mtimeMs: number;
|
|
31
|
+
readonly mtimeNs: bigint;
|
|
32
|
+
readonly ctimeNs: bigint;
|
|
33
|
+
readonly size: bigint;
|
|
34
|
+
readonly ino: bigint;
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
/**
|
|
23
38
|
* Exact identity of one attached SDK session endpoint. Providers persist it next to
|
|
24
39
|
* their conversation state and re-prove it before every resume, so it must be derived
|
|
@@ -32,11 +47,22 @@ export function sessionAttachmentAuthorityId(input: {
|
|
|
32
47
|
endpointMtimeMs: number | undefined;
|
|
33
48
|
url: string;
|
|
34
49
|
token: string;
|
|
50
|
+
/** Proven no-follow endpoint identity, when the caller has one. */
|
|
51
|
+
endpointIdentity?: SessionEndpointIdentity;
|
|
35
52
|
}): string {
|
|
36
53
|
const endpointAuthorityDigest = crypto
|
|
37
54
|
.createHash("sha256")
|
|
38
55
|
.update(JSON.stringify({ url: input.url, token: input.token }))
|
|
39
56
|
.digest("hex");
|
|
57
|
+
const endpointIdentity = input.endpointIdentity
|
|
58
|
+
? {
|
|
59
|
+
mtimeMs: input.endpointIdentity.mtimeMs,
|
|
60
|
+
mtimeNs: input.endpointIdentity.mtimeNs.toString(),
|
|
61
|
+
ctimeNs: input.endpointIdentity.ctimeNs.toString(),
|
|
62
|
+
size: input.endpointIdentity.size.toString(),
|
|
63
|
+
ino: input.endpointIdentity.ino.toString(),
|
|
64
|
+
}
|
|
65
|
+
: undefined;
|
|
40
66
|
return crypto
|
|
41
67
|
.createHash("sha256")
|
|
42
68
|
.update(
|
|
@@ -46,6 +72,7 @@ export function sessionAttachmentAuthorityId(input: {
|
|
|
46
72
|
pid: input.pid,
|
|
47
73
|
endpointMtimeMs: input.endpointMtimeMs,
|
|
48
74
|
endpointAuthorityDigest,
|
|
75
|
+
endpointIdentity,
|
|
49
76
|
}),
|
|
50
77
|
)
|
|
51
78
|
.digest("hex");
|
|
@@ -233,6 +260,8 @@ export type SessionRouterProviderDeps = Pick<
|
|
|
233
260
|
|
|
234
261
|
export interface SessionRouterOptions {
|
|
235
262
|
agentDir: string;
|
|
263
|
+
/** Limits attachment to exact ids selected by a Broker-scoped operation. */
|
|
264
|
+
sessionIds?: readonly string[];
|
|
236
265
|
deps?: SessionRouterDeps;
|
|
237
266
|
/** Runtime-specific identity validation; Router supplies a conservative fallback. */
|
|
238
267
|
correlateFrame?: SessionRouterFrameCorrelator;
|
|
@@ -287,6 +316,8 @@ type AttachedSession = {
|
|
|
287
316
|
readonly generation: number;
|
|
288
317
|
readonly pid: number;
|
|
289
318
|
readonly endpointMtimeMs: number;
|
|
319
|
+
/** No-follow endpoint identity proven before this attachment became current. */
|
|
320
|
+
readonly endpointIdentity: SessionEndpointIdentity;
|
|
290
321
|
readonly runEpoch: number;
|
|
291
322
|
readonly client: SessionRouterClient;
|
|
292
323
|
readonly indexed: IndexedSession;
|
|
@@ -398,6 +429,60 @@ function readSequence(value: unknown): number | undefined {
|
|
|
398
429
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
|
|
399
430
|
}
|
|
400
431
|
|
|
432
|
+
async function lstatEndpoint(file: string): Promise<SessionEndpointIdentity | undefined> {
|
|
433
|
+
const stat = await fs.lstat(file).catch(() => undefined);
|
|
434
|
+
if (!stat || !stat.isFile()) return undefined;
|
|
435
|
+
const identity = await fs.lstat(file, { bigint: true }).catch(() => undefined);
|
|
436
|
+
if (!identity || !identity.isFile()) return undefined;
|
|
437
|
+
if (
|
|
438
|
+
stat.size !== Number(identity.size) ||
|
|
439
|
+
stat.ino !== Number(identity.ino) ||
|
|
440
|
+
Math.abs(stat.mtimeMs - Number(identity.mtimeNs) / 1_000_000) > 0.0005 ||
|
|
441
|
+
Math.abs(stat.ctimeMs - Number(identity.ctimeNs) / 1_000_000) > 0.0005
|
|
442
|
+
)
|
|
443
|
+
return undefined;
|
|
444
|
+
return {
|
|
445
|
+
mtimeMs: stat.mtimeMs,
|
|
446
|
+
mtimeNs: identity.mtimeNs,
|
|
447
|
+
ctimeNs: identity.ctimeNs,
|
|
448
|
+
size: identity.size,
|
|
449
|
+
ino: identity.ino,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function warningAffectsSession(warning: string, sessionId: string): boolean {
|
|
454
|
+
return !warning.startsWith("Session ") || warning.includes(`Session ${sessionId} `);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function sameEndpointIdentity(expected: SessionEndpointIdentity, current: SessionEndpointIdentity): boolean {
|
|
458
|
+
return (
|
|
459
|
+
expected.mtimeMs === current.mtimeMs &&
|
|
460
|
+
expected.mtimeNs === current.mtimeNs &&
|
|
461
|
+
expected.ctimeNs === current.ctimeNs &&
|
|
462
|
+
expected.size === current.size &&
|
|
463
|
+
expected.ino === current.ino
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function readFrameSequenceClaim(frame: Record<string, unknown>): {
|
|
468
|
+
claimed: boolean;
|
|
469
|
+
valid: boolean;
|
|
470
|
+
seq?: number;
|
|
471
|
+
} {
|
|
472
|
+
const payload =
|
|
473
|
+
frame.type === "event" && frame.payload && typeof frame.payload === "object" && !Array.isArray(frame.payload)
|
|
474
|
+
? (frame.payload as Record<string, unknown>)
|
|
475
|
+
: undefined;
|
|
476
|
+
const claims: unknown[] = [];
|
|
477
|
+
if (Object.hasOwn(frame, "seq")) claims.push(frame.seq);
|
|
478
|
+
if (payload && Object.hasOwn(payload, "seq")) claims.push(payload.seq);
|
|
479
|
+
if (claims.length === 0) return { claimed: false, valid: true };
|
|
480
|
+
const sequences = claims.map(readSequence);
|
|
481
|
+
if (sequences.some(sequence => sequence === undefined)) return { claimed: true, valid: false };
|
|
482
|
+
if (new Set(sequences).size !== 1) return { claimed: true, valid: false };
|
|
483
|
+
return { claimed: true, valid: true, seq: sequences[0] };
|
|
484
|
+
}
|
|
485
|
+
|
|
401
486
|
function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame | undefined {
|
|
402
487
|
const payload =
|
|
403
488
|
frame.type === "event" && frame.payload && typeof frame.payload === "object" && !Array.isArray(frame.payload)
|
|
@@ -488,6 +573,7 @@ type AdoptedSession = {
|
|
|
488
573
|
*/
|
|
489
574
|
export class SessionRouter {
|
|
490
575
|
readonly #agentDir: string;
|
|
576
|
+
readonly #sessionIds: ReadonlySet<string> | undefined;
|
|
491
577
|
readonly #deps: SessionRouterDeps;
|
|
492
578
|
readonly #correlateFrame: SessionRouterFrameCorrelator;
|
|
493
579
|
readonly #index: SessionIndex;
|
|
@@ -503,16 +589,11 @@ export class SessionRouter {
|
|
|
503
589
|
{ generation: number; frames: Array<{ seq: number; frame: Record<string, unknown> }> }
|
|
504
590
|
>();
|
|
505
591
|
readonly #reviving = new Set<string>();
|
|
506
|
-
/**
|
|
507
|
-
* Endpoint-file inode captured when each attachment was published (#4730
|
|
508
|
-
* review). Maintenance renewal compares against this so a rename-replace that
|
|
509
|
-
* preserves size, mtime and body cannot keep a superseded endpoint authorized.
|
|
510
|
-
*/
|
|
511
|
-
readonly #endpointInodes = new Map<string, bigint>();
|
|
512
592
|
readonly #notificationReceipts = new Map<string, NotificationCleanupReceipt>();
|
|
513
593
|
#stopTimer: (() => void) | undefined;
|
|
514
594
|
#reconcileTail: Promise<void> = Promise.resolve();
|
|
515
595
|
#reconcilePending: { readonly runEpoch: number; force: boolean } | undefined;
|
|
596
|
+
#attachmentTails = new Map<string, Promise<boolean>>();
|
|
516
597
|
/**
|
|
517
598
|
* Set when a live indexed session failed to attach; keeps the idle gate
|
|
518
599
|
* from parking retries until the 30s sweep (#4689 review).
|
|
@@ -524,11 +605,14 @@ export class SessionRouter {
|
|
|
524
605
|
#lastReconciledIndexSeq = -1;
|
|
525
606
|
#ready = false;
|
|
526
607
|
#started = false;
|
|
608
|
+
#startPromise: Promise<void> | undefined;
|
|
609
|
+
#stopPromise: Promise<void> | undefined;
|
|
527
610
|
#stopController = new AbortController();
|
|
528
611
|
#runEpoch = 0;
|
|
529
612
|
|
|
530
613
|
constructor(options: SessionRouterOptions) {
|
|
531
614
|
this.#agentDir = options.agentDir;
|
|
615
|
+
this.#sessionIds = options.sessionIds === undefined ? undefined : new Set(options.sessionIds);
|
|
532
616
|
this.#deps = options.deps ?? {};
|
|
533
617
|
this.#correlateFrame = options.correlateFrame ?? fallbackCorrelation;
|
|
534
618
|
this.#index = this.#deps.createIndex?.(options.agentDir) ?? new DefaultSessionIndex(options.agentDir);
|
|
@@ -545,14 +629,27 @@ export class SessionRouter {
|
|
|
545
629
|
|
|
546
630
|
/** Starts reconciliation and the index watcher. */
|
|
547
631
|
async start(): Promise<void> {
|
|
632
|
+
if (this.#stopPromise) await this.#stopPromise;
|
|
633
|
+
if (this.#startPromise) return await this.#startPromise;
|
|
548
634
|
if (this.#started) return;
|
|
549
635
|
this.#started = true;
|
|
636
|
+
const startup = this.#startImpl();
|
|
637
|
+
this.#startPromise = startup;
|
|
638
|
+
try {
|
|
639
|
+
await startup;
|
|
640
|
+
} finally {
|
|
641
|
+
if (this.#startPromise === startup) this.#startPromise = undefined;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async #startImpl(): Promise<void> {
|
|
550
646
|
const runEpoch = ++this.#runEpoch;
|
|
551
647
|
if (this.#stopController.signal.aborted) {
|
|
552
648
|
this.#stopController = new AbortController();
|
|
553
649
|
this.#reconcileTail = Promise.resolve();
|
|
554
650
|
this.#reconcilePending = undefined;
|
|
555
651
|
this.#frameTails.clear();
|
|
652
|
+
this.#attachmentTails.clear();
|
|
556
653
|
// A restart must re-run the full body on the first tick: reset the
|
|
557
654
|
// idle-gate markers so stale state cannot carry across run epochs.
|
|
558
655
|
this.#lastReconcileSweepAt = 0;
|
|
@@ -666,11 +763,11 @@ export class SessionRouter {
|
|
|
666
763
|
"pre_send",
|
|
667
764
|
"Broker lifecycle result omitted an exact session endpoint authority.",
|
|
668
765
|
);
|
|
669
|
-
const
|
|
670
|
-
const stateRoot = path.join(
|
|
766
|
+
const cwd = await canonicalSessionCwd(fallback.cwd);
|
|
767
|
+
const stateRoot = path.join(cwd, ".gjc", "state");
|
|
671
768
|
const indexed: IndexedSession = {
|
|
672
769
|
sessionId,
|
|
673
|
-
locator:
|
|
770
|
+
locator: await resolveSessionLocator(cwd, stateRoot),
|
|
674
771
|
endpointGeneration,
|
|
675
772
|
pid,
|
|
676
773
|
endpointMtimeMs,
|
|
@@ -693,14 +790,29 @@ export class SessionRouter {
|
|
|
693
790
|
if (!attached || !current || !capability)
|
|
694
791
|
throw new SessionRouterError("pre_send", "Broker session endpoint could not be attached.");
|
|
695
792
|
const listing = this.#index.listSessions();
|
|
696
|
-
const indexedCurrent =
|
|
697
|
-
|
|
793
|
+
const indexedCurrent = listing.warnings.some(warning => warningAffectsSession(warning, sessionId))
|
|
794
|
+
? undefined
|
|
795
|
+
: listing.sessions.find(item => item.sessionId === sessionId);
|
|
698
796
|
if (indexedCurrent && sameIndexedAuthority(indexed, indexedCurrent))
|
|
699
797
|
await this.#serialReconcile(this.#runEpoch, true, true);
|
|
700
798
|
return capability;
|
|
701
799
|
}
|
|
702
800
|
|
|
703
801
|
async stop(): Promise<void> {
|
|
802
|
+
if (this.#stopPromise) return await this.#stopPromise;
|
|
803
|
+
const stopping = this.#stopImpl();
|
|
804
|
+
this.#stopPromise = stopping;
|
|
805
|
+
try {
|
|
806
|
+
await stopping;
|
|
807
|
+
} finally {
|
|
808
|
+
if (this.#stopPromise === stopping) this.#stopPromise = undefined;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async #stopImpl(): Promise<void> {
|
|
813
|
+
// Detach the in-flight startup promise before invalidating its epoch. A
|
|
814
|
+
// later start must not return a promise owned by the stopped epoch.
|
|
815
|
+
this.#startPromise = undefined;
|
|
704
816
|
if (this.#stopTimer) this.#stopTimer();
|
|
705
817
|
this.#stopTimer = undefined;
|
|
706
818
|
this.#started = false;
|
|
@@ -720,7 +832,7 @@ export class SessionRouter {
|
|
|
720
832
|
this.#adopted.clear();
|
|
721
833
|
// Provider notification work is detached and bounded independently; core
|
|
722
834
|
// shutdown waits only for Router reconciliation and client close.
|
|
723
|
-
const pending = Promise.allSettled([this.#reconcileTail, ...shutdownTasks]);
|
|
835
|
+
const pending = Promise.allSettled([this.#reconcileTail, ...this.#attachmentTails.values(), ...shutdownTasks]);
|
|
724
836
|
// Cancelled once the race settles, so a completed shutdown does not hold the
|
|
725
837
|
// event loop open for the remainder of the timeout.
|
|
726
838
|
const shutdownCutoff = new AbortController();
|
|
@@ -790,14 +902,15 @@ export class SessionRouter {
|
|
|
790
902
|
if (expectedAttachment !== undefined && attached.capability !== expectedAttachment)
|
|
791
903
|
throw new SessionRouterError("pre_send", "SDK session attachment changed before command dispatch.");
|
|
792
904
|
if (attached.initializingPublication) {
|
|
793
|
-
const
|
|
905
|
+
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
794
906
|
if (
|
|
795
|
-
!
|
|
796
|
-
endpoint.url !== attached.endpoint.url ||
|
|
797
|
-
endpoint.token !== attached.endpoint.token ||
|
|
798
|
-
endpoint.pid !== attached.pid
|
|
907
|
+
!proven ||
|
|
908
|
+
proven.endpoint.url !== attached.endpoint.url ||
|
|
909
|
+
proven.endpoint.token !== attached.endpoint.token ||
|
|
910
|
+
proven.endpoint.pid !== attached.pid ||
|
|
911
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
799
912
|
) {
|
|
800
|
-
await this.#retireAttachment(attached,
|
|
913
|
+
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
801
914
|
throw new SessionRouterError("pre_send", "SDK session attachment changed during publication.");
|
|
802
915
|
}
|
|
803
916
|
}
|
|
@@ -851,7 +964,7 @@ export class SessionRouter {
|
|
|
851
964
|
try {
|
|
852
965
|
await this.#index.refresh();
|
|
853
966
|
const listing = this.#index.listSessions();
|
|
854
|
-
if (listing.warnings.
|
|
967
|
+
if (listing.warnings.some(warning => warningAffectsSession(warning, sessionId))) return undefined;
|
|
855
968
|
indexed = listing.sessions.find(candidate => candidate.sessionId === sessionId);
|
|
856
969
|
} catch {
|
|
857
970
|
return undefined;
|
|
@@ -865,8 +978,15 @@ export class SessionRouter {
|
|
|
865
978
|
)
|
|
866
979
|
return undefined;
|
|
867
980
|
if (!Number.isSafeInteger(indexed.pid) || indexed.pid <= 0) return undefined;
|
|
868
|
-
const
|
|
869
|
-
if (
|
|
981
|
+
const proven = await this.#readProvenEndpoint(indexed).catch(() => null);
|
|
982
|
+
if (
|
|
983
|
+
!proven ||
|
|
984
|
+
proven.endpoint.stale === true ||
|
|
985
|
+
proven.endpoint.pid !== indexed.pid ||
|
|
986
|
+
!proven.endpoint.token ||
|
|
987
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
988
|
+
)
|
|
989
|
+
return undefined;
|
|
870
990
|
if (this.#sessions.get(sessionId) !== attached || !this.#attachmentPublished(attached)) return undefined;
|
|
871
991
|
return { sessionId, endpointGeneration: attached.generation };
|
|
872
992
|
}
|
|
@@ -916,7 +1036,7 @@ export class SessionRouter {
|
|
|
916
1036
|
await this.#index.open();
|
|
917
1037
|
await this.#index.refresh();
|
|
918
1038
|
const listing = this.#index.listSessions();
|
|
919
|
-
if (listing.warnings.
|
|
1039
|
+
if (listing.warnings.some(warning => warningAffectsSession(warning, sessionId)))
|
|
920
1040
|
throw new SessionActivationError(
|
|
921
1041
|
"session_not_live",
|
|
922
1042
|
"Session activation requires an intact session index.",
|
|
@@ -945,8 +1065,16 @@ export class SessionRouter {
|
|
|
945
1065
|
"session_not_live",
|
|
946
1066
|
"Session activation requires an exact live session endpoint.",
|
|
947
1067
|
);
|
|
948
|
-
const
|
|
949
|
-
|
|
1068
|
+
const provenEndpoint = await this.#readProvenEndpoint(indexed).catch(() => null);
|
|
1069
|
+
const endpoint = provenEndpoint?.endpoint;
|
|
1070
|
+
if (
|
|
1071
|
+
!provenEndpoint ||
|
|
1072
|
+
!endpoint ||
|
|
1073
|
+
endpoint.stale === true ||
|
|
1074
|
+
!endpoint.url ||
|
|
1075
|
+
!endpoint.token ||
|
|
1076
|
+
endpoint.pid !== indexed.pid
|
|
1077
|
+
)
|
|
950
1078
|
throw new SessionActivationError(
|
|
951
1079
|
"session_not_live",
|
|
952
1080
|
"Session activation requires a readable session discovery endpoint.",
|
|
@@ -966,12 +1094,15 @@ export class SessionRouter {
|
|
|
966
1094
|
throw new SessionActivationError("activation_unavailable", "The session endpoint could not be reached.");
|
|
967
1095
|
}
|
|
968
1096
|
try {
|
|
969
|
-
const
|
|
1097
|
+
const currentProvenEndpoint = await this.#readProvenEndpoint(indexed).catch(() => null);
|
|
1098
|
+
const currentEndpoint = currentProvenEndpoint?.endpoint;
|
|
970
1099
|
if (
|
|
1100
|
+
!currentProvenEndpoint ||
|
|
971
1101
|
!currentEndpoint ||
|
|
972
1102
|
currentEndpoint.url !== endpoint.url ||
|
|
973
1103
|
currentEndpoint.token !== endpoint.token ||
|
|
974
|
-
currentEndpoint.pid !== endpoint.pid
|
|
1104
|
+
currentEndpoint.pid !== endpoint.pid ||
|
|
1105
|
+
!sameEndpointIdentity(provenEndpoint.identity, currentProvenEndpoint.identity)
|
|
975
1106
|
)
|
|
976
1107
|
throw new SessionActivationError(
|
|
977
1108
|
"session_not_live",
|
|
@@ -1083,50 +1214,51 @@ export class SessionRouter {
|
|
|
1083
1214
|
return;
|
|
1084
1215
|
}
|
|
1085
1216
|
const indexed = this.#index.listSessions();
|
|
1086
|
-
const live =
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1217
|
+
const live = indexed.sessions.filter(
|
|
1218
|
+
session =>
|
|
1219
|
+
!indexed.warnings.some(warning => warningAffectsSession(warning, session.sessionId)) &&
|
|
1220
|
+
session.live &&
|
|
1221
|
+
isSessionAuthorityEligible(session) &&
|
|
1222
|
+
!session.terminalUncertain &&
|
|
1223
|
+
(this.#sessionIds === undefined || this.#sessionIds.has(session.sessionId)),
|
|
1224
|
+
);
|
|
1092
1225
|
const liveIds = new Set(live.map(session => session.sessionId));
|
|
1093
1226
|
const attachedIds = new Set<string>();
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
this.#adopted.delete(sessionId);
|
|
1124
|
-
attachedIds.add(sessionId);
|
|
1125
|
-
}
|
|
1126
|
-
} catch {
|
|
1227
|
+
for (const [sessionId, adopted] of [...this.#adopted]) {
|
|
1228
|
+
if (indexed.warnings.some(warning => warningAffectsSession(warning, sessionId))) continue;
|
|
1229
|
+
const attached = this.#sessions.get(sessionId);
|
|
1230
|
+
const indexedSession = indexed.sessions.find(session => session.sessionId === sessionId);
|
|
1231
|
+
if (!attached || attached.capability !== adopted.attachment) {
|
|
1232
|
+
this.#adopted.delete(sessionId);
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
const exactIndex =
|
|
1236
|
+
indexedSession?.live === true &&
|
|
1237
|
+
isSessionAuthorityEligible(indexedSession) &&
|
|
1238
|
+
!indexedSession.terminalUncertain &&
|
|
1239
|
+
indexedSession.endpointGeneration === adopted.generation &&
|
|
1240
|
+
indexedSession.pid === adopted.pid &&
|
|
1241
|
+
indexedSession.endpointMtimeMs === adopted.endpointMtimeMs;
|
|
1242
|
+
const endpoint = exactIndex ? await this.#readEndpoint(indexedSession).catch(() => null) : null;
|
|
1243
|
+
if (
|
|
1244
|
+
!exactIndex ||
|
|
1245
|
+
!endpoint ||
|
|
1246
|
+
endpoint.pid !== adopted.pid ||
|
|
1247
|
+
endpoint.url !== attached.endpoint.url ||
|
|
1248
|
+
endpoint.token !== attached.endpoint.token
|
|
1249
|
+
) {
|
|
1250
|
+
this.#adopted.delete(sessionId);
|
|
1251
|
+
await this.#retireAttachment(attached);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
if (await this.#publishAttachment(attached, true)) {
|
|
1127
1256
|
this.#adopted.delete(sessionId);
|
|
1128
|
-
|
|
1257
|
+
attachedIds.add(sessionId);
|
|
1129
1258
|
}
|
|
1259
|
+
} catch {
|
|
1260
|
+
this.#adopted.delete(sessionId);
|
|
1261
|
+
await this.#retireAttachment(attached);
|
|
1130
1262
|
}
|
|
1131
1263
|
}
|
|
1132
1264
|
let nextAttachment = 0;
|
|
@@ -1200,15 +1332,9 @@ export class SessionRouter {
|
|
|
1200
1332
|
// attachment was PUBLISHED against is therefore the identity, and the
|
|
1201
1333
|
// generation is compared so a re-registration reusing the same url/token/pid
|
|
1202
1334
|
// cannot keep the old attachment authorized.
|
|
1203
|
-
const
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1206
|
-
const inoBefore = await fs
|
|
1207
|
-
.stat(attached.endpoint.path, { bigint: true })
|
|
1208
|
-
.then(value => value.ino)
|
|
1209
|
-
.catch(() => undefined);
|
|
1210
|
-
if (!statBefore || inoBefore === undefined || inoBefore !== publishedIno) return false;
|
|
1211
|
-
if (statBefore.mtimeMs !== indexed.endpointMtimeMs) return false;
|
|
1335
|
+
const identityBefore = await lstatEndpoint(attached.endpoint.path);
|
|
1336
|
+
if (!identityBefore || !sameEndpointIdentity(attached.endpointIdentity, identityBefore)) return false;
|
|
1337
|
+
if (identityBefore.mtimeMs !== indexed.endpointMtimeMs) return false;
|
|
1212
1338
|
let raw: Record<string, unknown>;
|
|
1213
1339
|
try {
|
|
1214
1340
|
const parsed = JSON.parse(await Bun.file(attached.endpoint.path).text());
|
|
@@ -1229,36 +1355,29 @@ export class SessionRouter {
|
|
|
1229
1355
|
// comes from the indexed row this attachment was published against.
|
|
1230
1356
|
if (indexed.endpointGeneration !== attached.generation) return false;
|
|
1231
1357
|
if (raw.endpointGeneration !== undefined && raw.endpointGeneration !== attached.generation) return false;
|
|
1232
|
-
const
|
|
1233
|
-
const inoAfter = await fs
|
|
1234
|
-
.stat(attached.endpoint.path, { bigint: true })
|
|
1235
|
-
.then(value => value.ino)
|
|
1236
|
-
.catch(() => undefined);
|
|
1358
|
+
const identityAfter = await lstatEndpoint(attached.endpoint.path);
|
|
1237
1359
|
return (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
inoAfter === publishedIno
|
|
1360
|
+
identityAfter !== undefined &&
|
|
1361
|
+
identityAfter.mtimeMs === indexed.endpointMtimeMs &&
|
|
1362
|
+
sameEndpointIdentity(attached.endpointIdentity, identityAfter)
|
|
1242
1363
|
);
|
|
1243
1364
|
}
|
|
1244
1365
|
|
|
1245
1366
|
/**
|
|
1246
|
-
* Returns the endpoint together with the
|
|
1367
|
+
* Returns the endpoint together with the full identity this read PROVED (#4730
|
|
1247
1368
|
* review). The identity travels with the value instead of living in a side
|
|
1248
1369
|
* map, so every consumer decision compares the same proof and the two cannot
|
|
1249
1370
|
* drift apart.
|
|
1250
1371
|
*/
|
|
1251
|
-
async #readProvenEndpoint(
|
|
1372
|
+
async #readProvenEndpoint(
|
|
1373
|
+
indexed: IndexedSession,
|
|
1374
|
+
): Promise<{ endpoint: SdkSessionEndpoint; identity: SessionEndpointIdentity } | null> {
|
|
1252
1375
|
if (!isSessionAuthorityEligible(indexed)) return null;
|
|
1253
|
-
const
|
|
1254
|
-
const defaultStateRoot = path.join(
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1257
|
-
//
|
|
1258
|
-
// The scope test therefore compares path identity, not spelling: a lexical
|
|
1259
|
-
// match fails for every symlinked cwd (macOS /var -> /private/var,
|
|
1260
|
-
// /home/jun/desk -> /data/Lina-Desk), the adopted attachment is retired on
|
|
1261
|
-
// reconcile, and session/new surfaces "lost exact Router authority".
|
|
1376
|
+
const cwd = indexed.locator.cwd;
|
|
1377
|
+
const defaultStateRoot = path.join(cwd, ".gjc", "state");
|
|
1378
|
+
// Locator cwd is already canonical. `stateRoot` remains the host-provided
|
|
1379
|
+
// authority path, so this comparison resolves equivalent paths only for the
|
|
1380
|
+
// state-root identity boundary; cwd never retains lexical symlink spellings.
|
|
1262
1381
|
const indexedStateRoot = resolveEquivalentPath(indexed.locator.stateRoot);
|
|
1263
1382
|
const scope =
|
|
1264
1383
|
indexedStateRoot === resolveEquivalentPath(defaultStateRoot)
|
|
@@ -1267,18 +1386,15 @@ export class SessionRouter {
|
|
|
1267
1386
|
? "chat"
|
|
1268
1387
|
: undefined;
|
|
1269
1388
|
if (!scope || indexed.endpointMtimeMs === undefined || !Number.isFinite(indexed.endpointMtimeMs)) return null;
|
|
1270
|
-
const
|
|
1389
|
+
const endpointPath = path.join(endpointDirectory(cwd, scope), `${indexed.sessionId}.json`);
|
|
1390
|
+
const endpointIdentity = await lstatEndpoint(endpointPath);
|
|
1391
|
+
const endpoint = await readSdkSessionEndpoint(cwd, indexed.sessionId, scope);
|
|
1271
1392
|
if (!endpoint || endpoint.stale || endpoint.pid !== indexed.pid) return null;
|
|
1272
|
-
|
|
1273
|
-
const endpointIno = await fs
|
|
1274
|
-
.stat(endpoint.path, { bigint: true })
|
|
1275
|
-
.then(value => value.ino)
|
|
1276
|
-
.catch(() => undefined);
|
|
1277
|
-
if (!endpointStat || endpointIno === undefined || endpointStat.mtimeMs !== indexed.endpointMtimeMs) return null;
|
|
1393
|
+
if (!endpointIdentity || endpointIdentity.mtimeMs !== indexed.endpointMtimeMs) return null;
|
|
1278
1394
|
// Identity is proven INSIDE this authority read (#4730 review): sampling it
|
|
1279
1395
|
// afterwards would let an identical rename between the read and the sample
|
|
1280
1396
|
// install the replacement's inode as the trusted baseline.
|
|
1281
|
-
const
|
|
1397
|
+
const provenIdentity = endpointIdentity;
|
|
1282
1398
|
let raw: Record<string, unknown>;
|
|
1283
1399
|
try {
|
|
1284
1400
|
const parsed = JSON.parse(await Bun.file(endpoint.path).text());
|
|
@@ -1296,24 +1412,19 @@ export class SessionRouter {
|
|
|
1296
1412
|
)
|
|
1297
1413
|
return null;
|
|
1298
1414
|
// Fail CLOSED on stat error and on any identity change across the body read.
|
|
1299
|
-
const
|
|
1300
|
-
const inoAfterRead = await fs
|
|
1301
|
-
.stat(endpoint.path, { bigint: true })
|
|
1302
|
-
.then(value => value.ino)
|
|
1303
|
-
.catch(() => undefined);
|
|
1415
|
+
const endpointIdentityAfterRead = await lstatEndpoint(endpoint.path);
|
|
1304
1416
|
if (
|
|
1305
|
-
!
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
inoAfterRead !== provenIno
|
|
1417
|
+
!endpointIdentityAfterRead ||
|
|
1418
|
+
endpointIdentityAfterRead.mtimeMs !== indexed.endpointMtimeMs ||
|
|
1419
|
+
!sameEndpointIdentity(endpointIdentity, endpointIdentityAfterRead)
|
|
1309
1420
|
)
|
|
1310
1421
|
return null;
|
|
1311
1422
|
await this.#index.refresh();
|
|
1312
1423
|
const listing = this.#index.listSessions();
|
|
1313
|
-
if (listing.warnings.
|
|
1424
|
+
if (listing.warnings.some(warning => warningAffectsSession(warning, indexed.sessionId))) return null;
|
|
1314
1425
|
const current = listing.sessions.find(session => session.sessionId === indexed.sessionId);
|
|
1315
1426
|
if (!current || !sameIndexedAuthority(indexed, current)) return null;
|
|
1316
|
-
return { endpoint,
|
|
1427
|
+
return { endpoint, identity: provenIdentity };
|
|
1317
1428
|
}
|
|
1318
1429
|
|
|
1319
1430
|
/** Endpoint-only view for callers that do not make an authority decision. */
|
|
@@ -1375,6 +1486,28 @@ export class SessionRouter {
|
|
|
1375
1486
|
skipReplay = false,
|
|
1376
1487
|
deferPublication = false,
|
|
1377
1488
|
deferReplay = false,
|
|
1489
|
+
): Promise<boolean> {
|
|
1490
|
+
const previous = this.#attachmentTails.get(indexed.sessionId) ?? Promise.resolve(true);
|
|
1491
|
+
const task = previous
|
|
1492
|
+
.catch(() => false)
|
|
1493
|
+
.then(() =>
|
|
1494
|
+
this.#attachUnserialized(indexed, runEpoch, resolvedEndpoint, skipReplay, deferPublication, deferReplay),
|
|
1495
|
+
);
|
|
1496
|
+
this.#attachmentTails.set(indexed.sessionId, task);
|
|
1497
|
+
try {
|
|
1498
|
+
return await task;
|
|
1499
|
+
} finally {
|
|
1500
|
+
if (this.#attachmentTails.get(indexed.sessionId) === task) this.#attachmentTails.delete(indexed.sessionId);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async #attachUnserialized(
|
|
1505
|
+
indexed: IndexedSession,
|
|
1506
|
+
runEpoch: number,
|
|
1507
|
+
resolvedEndpoint?: SdkSessionEndpoint,
|
|
1508
|
+
skipReplay = false,
|
|
1509
|
+
deferPublication = false,
|
|
1510
|
+
deferReplay = false,
|
|
1378
1511
|
): Promise<boolean> {
|
|
1379
1512
|
const retirementVersion = this.#retirementVersions.get(indexed.sessionId) ?? 0;
|
|
1380
1513
|
const retirement = this.#retirements.get(indexed.sessionId);
|
|
@@ -1386,18 +1519,14 @@ export class SessionRouter {
|
|
|
1386
1519
|
// authority read of its own, so it samples once here.
|
|
1387
1520
|
const proven = resolvedEndpoint === undefined ? await this.#readProvenEndpoint(indexed) : null;
|
|
1388
1521
|
const endpoint = resolvedEndpoint ?? proven?.endpoint ?? null;
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
provenIno = await fs
|
|
1392
|
-
.stat(resolvedEndpoint.path, { bigint: true })
|
|
1393
|
-
.then(value => value.ino)
|
|
1394
|
-
.catch(() => undefined);
|
|
1522
|
+
const endpointIdentity =
|
|
1523
|
+
resolvedEndpoint === undefined ? proven?.identity : await lstatEndpoint(resolvedEndpoint.path);
|
|
1395
1524
|
const retirementAfterValidation = this.#retirements.get(indexed.sessionId);
|
|
1396
1525
|
if (retirementAfterValidation) await retirementAfterValidation;
|
|
1397
1526
|
if ((this.#retirementVersions.get(indexed.sessionId) ?? 0) !== retirementVersion)
|
|
1398
|
-
return await this.#
|
|
1527
|
+
return await this.#attachUnserialized(indexed, runEpoch, undefined, skipReplay, deferPublication, deferReplay);
|
|
1399
1528
|
if (!this.#running(runEpoch)) return false;
|
|
1400
|
-
if (!endpoint) return false;
|
|
1529
|
+
if (!endpoint || !endpointIdentity) return false;
|
|
1401
1530
|
const existing = this.#sessions.get(indexed.sessionId);
|
|
1402
1531
|
const resumable =
|
|
1403
1532
|
existing !== undefined &&
|
|
@@ -1405,7 +1534,9 @@ export class SessionRouter {
|
|
|
1405
1534
|
existing.endpoint.token === endpoint.token &&
|
|
1406
1535
|
existing.generation === indexed.endpointGeneration &&
|
|
1407
1536
|
existing.pid === indexed.pid &&
|
|
1408
|
-
existing.endpointMtimeMs === indexed.endpointMtimeMs
|
|
1537
|
+
existing.endpointMtimeMs === indexed.endpointMtimeMs &&
|
|
1538
|
+
endpointIdentity !== undefined &&
|
|
1539
|
+
sameEndpointIdentity(existing.endpointIdentity, endpointIdentity);
|
|
1409
1540
|
if (existing && resumable && !existing.barrier.failed) {
|
|
1410
1541
|
this.#reviveTransport(existing);
|
|
1411
1542
|
return true;
|
|
@@ -1434,7 +1565,9 @@ export class SessionRouter {
|
|
|
1434
1565
|
(existing.endpoint.url !== endpoint.url ||
|
|
1435
1566
|
existing.endpoint.token !== endpoint.token ||
|
|
1436
1567
|
existing.pid !== indexed.pid ||
|
|
1437
|
-
existing.endpointMtimeMs !== indexed.endpointMtimeMs
|
|
1568
|
+
existing.endpointMtimeMs !== indexed.endpointMtimeMs ||
|
|
1569
|
+
endpointIdentity === undefined ||
|
|
1570
|
+
!sameEndpointIdentity(existing.endpointIdentity, endpointIdentity))
|
|
1438
1571
|
? "replaced_same_generation"
|
|
1439
1572
|
: "replaced",
|
|
1440
1573
|
),
|
|
@@ -1455,7 +1588,7 @@ export class SessionRouter {
|
|
|
1455
1588
|
await client.close().catch(() => undefined);
|
|
1456
1589
|
const currentRetirement = this.#retirements.get(indexed.sessionId);
|
|
1457
1590
|
if (currentRetirement) await currentRetirement;
|
|
1458
|
-
return await this.#
|
|
1591
|
+
return await this.#attachUnserialized(indexed, runEpoch, undefined, skipReplay, deferPublication, deferReplay);
|
|
1459
1592
|
}
|
|
1460
1593
|
let attached: AttachedSession | undefined;
|
|
1461
1594
|
const barrier: ReplayBarrier = { held: undefined, detached: false, failed: false };
|
|
@@ -1469,6 +1602,7 @@ export class SessionRouter {
|
|
|
1469
1602
|
endpointMtimeMs: indexed.endpointMtimeMs,
|
|
1470
1603
|
url: endpoint.url,
|
|
1471
1604
|
token: endpoint.token,
|
|
1605
|
+
endpointIdentity,
|
|
1472
1606
|
}),
|
|
1473
1607
|
sessionId: indexed.sessionId,
|
|
1474
1608
|
generation: indexed.endpointGeneration,
|
|
@@ -1480,14 +1614,15 @@ export class SessionRouter {
|
|
|
1480
1614
|
if (!attached || !this.#attachmentPublished(attached))
|
|
1481
1615
|
throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
|
|
1482
1616
|
if (attached.initializingPublication) {
|
|
1483
|
-
const
|
|
1617
|
+
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
1484
1618
|
if (
|
|
1485
|
-
!
|
|
1486
|
-
endpoint.url !== attached.endpoint.url ||
|
|
1487
|
-
endpoint.token !== attached.endpoint.token ||
|
|
1488
|
-
endpoint.pid !== attached.pid
|
|
1619
|
+
!proven ||
|
|
1620
|
+
proven.endpoint.url !== attached.endpoint.url ||
|
|
1621
|
+
proven.endpoint.token !== attached.endpoint.token ||
|
|
1622
|
+
proven.endpoint.pid !== attached.pid ||
|
|
1623
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
1489
1624
|
) {
|
|
1490
|
-
await this.#retireAttachment(attached,
|
|
1625
|
+
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
1491
1626
|
throw new SessionRouterError("pre_send", "SDK session attachment changed during publication.");
|
|
1492
1627
|
}
|
|
1493
1628
|
} else await this.#serialReconcile(runEpoch, true, true);
|
|
@@ -1569,6 +1704,7 @@ export class SessionRouter {
|
|
|
1569
1704
|
endpoint,
|
|
1570
1705
|
pid: indexed.pid,
|
|
1571
1706
|
endpointMtimeMs: indexed.endpointMtimeMs,
|
|
1707
|
+
endpointIdentity,
|
|
1572
1708
|
generation: indexed.endpointGeneration,
|
|
1573
1709
|
runEpoch,
|
|
1574
1710
|
client,
|
|
@@ -1592,10 +1728,6 @@ export class SessionRouter {
|
|
|
1592
1728
|
new SessionRouterError("pre_send", "SDK session publication was detached before completion."),
|
|
1593
1729
|
);
|
|
1594
1730
|
barrier.held = undefined;
|
|
1595
|
-
// Every teardown path (retirement, replacement, stop, failed
|
|
1596
|
-
// publication) routes through dispose(), so the published-inode entry
|
|
1597
|
-
// is dropped here rather than at one call site (#4730 review).
|
|
1598
|
-
if (attached) this.#endpointInodes.delete(attached.id);
|
|
1599
1731
|
},
|
|
1600
1732
|
};
|
|
1601
1733
|
// Re-check at publication (#4730 review): the endpoint must STILL be the
|
|
@@ -1609,14 +1741,9 @@ export class SessionRouter {
|
|
|
1609
1741
|
await client.close().catch(() => undefined);
|
|
1610
1742
|
return false;
|
|
1611
1743
|
};
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
.stat(endpoint.path, { bigint: true })
|
|
1615
|
-
.then(value => value.ino)
|
|
1616
|
-
.catch(() => undefined);
|
|
1617
|
-
if (publishIno === undefined || publishIno !== provenIno) return await rollback();
|
|
1744
|
+
const publishIdentity = await lstatEndpoint(endpoint.path);
|
|
1745
|
+
if (!publishIdentity || !sameEndpointIdentity(endpointIdentity, publishIdentity)) return await rollback();
|
|
1618
1746
|
this.#sessions.set(indexed.sessionId, attached);
|
|
1619
|
-
this.#endpointInodes.set(attached.id, provenIno);
|
|
1620
1747
|
if (deferPublication)
|
|
1621
1748
|
this.#adopted.set(indexed.sessionId, {
|
|
1622
1749
|
generation: indexed.endpointGeneration,
|
|
@@ -1656,19 +1783,17 @@ export class SessionRouter {
|
|
|
1656
1783
|
if (!this.#attachmentLive(attached)) return false;
|
|
1657
1784
|
const proven = await this.#readProvenEndpoint(attached.indexed).catch(() => null);
|
|
1658
1785
|
if (!this.#attachmentLive(attached)) return false;
|
|
1659
|
-
// The commit point compares the PROVEN
|
|
1786
|
+
// The commit point compares the PROVEN endpoint identity alongside url/token/pid
|
|
1660
1787
|
// (#4730 review): an identical-byte rename during the pre-publication
|
|
1661
1788
|
// hooks matches every other field, so identity is what rejects it. The
|
|
1662
1789
|
// value compared here is the one the authority read proved, not a later
|
|
1663
1790
|
// observation, so the proof and its consumer cannot drift apart.
|
|
1664
|
-
const publishedIno = this.#endpointInodes.get(attached.id);
|
|
1665
1791
|
if (
|
|
1666
1792
|
!proven ||
|
|
1667
1793
|
proven.endpoint.url !== attached.endpoint.url ||
|
|
1668
1794
|
proven.endpoint.token !== attached.endpoint.token ||
|
|
1669
1795
|
proven.endpoint.pid !== attached.pid ||
|
|
1670
|
-
|
|
1671
|
-
proven.ino !== publishedIno
|
|
1796
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
1672
1797
|
) {
|
|
1673
1798
|
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
1674
1799
|
return false;
|
|
@@ -1731,14 +1856,15 @@ export class SessionRouter {
|
|
|
1731
1856
|
.catch(() => undefined)
|
|
1732
1857
|
.then(async () => {
|
|
1733
1858
|
if (!this.#attachmentLive(attached)) return;
|
|
1734
|
-
const
|
|
1859
|
+
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
1735
1860
|
if (
|
|
1736
|
-
!
|
|
1737
|
-
endpoint.url !== attached.endpoint.url ||
|
|
1738
|
-
endpoint.token !== attached.endpoint.token ||
|
|
1739
|
-
endpoint.pid !== attached.pid
|
|
1861
|
+
!proven ||
|
|
1862
|
+
proven.endpoint.url !== attached.endpoint.url ||
|
|
1863
|
+
proven.endpoint.token !== attached.endpoint.token ||
|
|
1864
|
+
proven.endpoint.pid !== attached.pid ||
|
|
1865
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
1740
1866
|
) {
|
|
1741
|
-
await this.#retireAttachment(attached,
|
|
1867
|
+
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
1742
1868
|
return;
|
|
1743
1869
|
}
|
|
1744
1870
|
attached.initializingPublication = true;
|
|
@@ -2031,7 +2157,21 @@ export class SessionRouter {
|
|
|
2031
2157
|
}
|
|
2032
2158
|
const correlated = this.#correlateFrame(frame);
|
|
2033
2159
|
if (!correlated) return;
|
|
2034
|
-
const
|
|
2160
|
+
const sequenceClaim = readFrameSequenceClaim(frame);
|
|
2161
|
+
if (!sequenceClaim.valid) {
|
|
2162
|
+
this.#failBarrier(attached, "protocol error: malformed sequence coordinate");
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
const correlatedSeq = correlated.seq === undefined ? undefined : readSequence(correlated.seq);
|
|
2166
|
+
if (correlated.seq !== undefined && correlatedSeq === undefined) {
|
|
2167
|
+
this.#failBarrier(attached, "protocol error: malformed correlated sequence coordinate");
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
if (sequenceClaim.claimed && correlatedSeq !== undefined && correlatedSeq !== sequenceClaim.seq) {
|
|
2171
|
+
this.#failBarrier(attached, "protocol error: inconsistent sequence coordinate");
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const seq = sequenceClaim.seq ?? correlatedSeq;
|
|
2035
2175
|
if (correlated.sessionId !== undefined && correlated.sessionId !== attached.sessionId) return;
|
|
2036
2176
|
if (correlated.generation !== undefined && correlated.generation !== attached.generation) return;
|
|
2037
2177
|
if (seq !== undefined && correlated.generation === undefined) return;
|