@gajae-code/coding-agent 0.15.6 → 0.16.1
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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -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/main.d.ts +1 -0
- 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/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- 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/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -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/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -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/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- 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 +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- 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/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- 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 +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- 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 +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- 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 +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- 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 +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -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 +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- 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 +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- 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/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -3,9 +3,11 @@ 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";
|
|
@@ -190,11 +192,13 @@ export interface SessionRouterClient {
|
|
|
190
192
|
send(frame: Record<string, unknown>): void;
|
|
191
193
|
}
|
|
192
194
|
|
|
195
|
+
/** One frame after the caller's envelope/payload identity correlation. */
|
|
193
196
|
/** One frame after the caller's envelope/payload identity correlation. */
|
|
194
197
|
export interface SessionRouterFrame {
|
|
195
198
|
readonly body: Record<string, unknown>;
|
|
196
199
|
readonly name: string | undefined;
|
|
197
200
|
readonly sessionId: string | undefined;
|
|
201
|
+
readonly revision?: number;
|
|
198
202
|
readonly generation: number | undefined;
|
|
199
203
|
readonly commandId?: string;
|
|
200
204
|
readonly turnId?: string;
|
|
@@ -220,6 +224,8 @@ export interface SessionRouterDeps {
|
|
|
220
224
|
onAttachment?: (attachment: SessionAttachment) => Promise<void> | void;
|
|
221
225
|
/** Called only after the opaque capability becomes externally current. */
|
|
222
226
|
onAttachmentReady?: (attachment: SessionAttachment) => Promise<void> | void;
|
|
227
|
+
/** Called before deferred replay may perform provider-facing mutations. */
|
|
228
|
+
onReplayReady?: () => Promise<void> | void;
|
|
223
229
|
/** Called when the Broker index no longer reports an attached session as live. */
|
|
224
230
|
onSessionRemoved?: (
|
|
225
231
|
attachment: SessionAttachment,
|
|
@@ -258,6 +264,8 @@ export type SessionRouterProviderDeps = Pick<
|
|
|
258
264
|
|
|
259
265
|
export interface SessionRouterOptions {
|
|
260
266
|
agentDir: string;
|
|
267
|
+
/** Limits attachment to exact ids selected by a Broker-scoped operation. */
|
|
268
|
+
sessionIds?: readonly string[];
|
|
261
269
|
deps?: SessionRouterDeps;
|
|
262
270
|
/** Runtime-specific identity validation; Router supplies a conservative fallback. */
|
|
263
271
|
correlateFrame?: SessionRouterFrameCorrelator;
|
|
@@ -327,6 +335,9 @@ type AttachedSession = {
|
|
|
327
335
|
readonly notificationCursor: { generation: number; seq: number };
|
|
328
336
|
published: boolean;
|
|
329
337
|
initializingPublication: boolean;
|
|
338
|
+
replayPending: boolean;
|
|
339
|
+
replayPendingSince?: number;
|
|
340
|
+
replayPendingToken?: object;
|
|
330
341
|
readyTail: Promise<void>;
|
|
331
342
|
readonly publication: { promise: Promise<void>; resolve: () => void; reject: (reason?: unknown) => void };
|
|
332
343
|
dispose: () => void;
|
|
@@ -346,6 +357,7 @@ function isUnansweredAfterDispatch(error: unknown): boolean {
|
|
|
346
357
|
}
|
|
347
358
|
|
|
348
359
|
const REPLAY_BARRIER_LIMIT = 1_024;
|
|
360
|
+
const REPLAY_PENDING_STALL_MS = 10_000;
|
|
349
361
|
const REPLAY_RETRY_ATTEMPTS = 3;
|
|
350
362
|
const REPLAY_RETRY_BACKOFF_MS = 100;
|
|
351
363
|
const DELIVERY_ATTEMPT_LIMIT = 3;
|
|
@@ -425,11 +437,15 @@ function readSequence(value: unknown): number | undefined {
|
|
|
425
437
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
|
|
426
438
|
}
|
|
427
439
|
|
|
440
|
+
function readNonnegativeSequence(value: unknown): number | undefined {
|
|
441
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
428
444
|
async function lstatEndpoint(file: string): Promise<SessionEndpointIdentity | undefined> {
|
|
429
445
|
const stat = await fs.lstat(file).catch(() => undefined);
|
|
430
|
-
if (!stat
|
|
446
|
+
if (!stat?.isFile()) return undefined;
|
|
431
447
|
const identity = await fs.lstat(file, { bigint: true }).catch(() => undefined);
|
|
432
|
-
if (!identity
|
|
448
|
+
if (!identity?.isFile()) return undefined;
|
|
433
449
|
if (
|
|
434
450
|
stat.size !== Number(identity.size) ||
|
|
435
451
|
stat.ino !== Number(identity.ino) ||
|
|
@@ -446,6 +462,10 @@ async function lstatEndpoint(file: string): Promise<SessionEndpointIdentity | un
|
|
|
446
462
|
};
|
|
447
463
|
}
|
|
448
464
|
|
|
465
|
+
function warningAffectsSession(warning: string, sessionId: string): boolean {
|
|
466
|
+
return !warning.startsWith("Session ") || warning.includes(`Session ${sessionId} `);
|
|
467
|
+
}
|
|
468
|
+
|
|
449
469
|
function sameEndpointIdentity(expected: SessionEndpointIdentity, current: SessionEndpointIdentity): boolean {
|
|
450
470
|
return (
|
|
451
471
|
expected.mtimeMs === current.mtimeMs &&
|
|
@@ -461,18 +481,9 @@ function readFrameSequenceClaim(frame: Record<string, unknown>): {
|
|
|
461
481
|
valid: boolean;
|
|
462
482
|
seq?: number;
|
|
463
483
|
} {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
: undefined;
|
|
468
|
-
const claims: unknown[] = [];
|
|
469
|
-
if (Object.hasOwn(frame, "seq")) claims.push(frame.seq);
|
|
470
|
-
if (payload && Object.hasOwn(payload, "seq")) claims.push(payload.seq);
|
|
471
|
-
if (claims.length === 0) return { claimed: false, valid: true };
|
|
472
|
-
const sequences = claims.map(readSequence);
|
|
473
|
-
if (sequences.some(sequence => sequence === undefined)) return { claimed: true, valid: false };
|
|
474
|
-
if (new Set(sequences).size !== 1) return { claimed: true, valid: false };
|
|
475
|
-
return { claimed: true, valid: true, seq: sequences[0] };
|
|
484
|
+
if (!Object.hasOwn(frame, "seq")) return { claimed: false, valid: true };
|
|
485
|
+
const seq = readSequence(frame.seq);
|
|
486
|
+
return seq === undefined ? { claimed: true, valid: false } : { claimed: true, valid: true, seq };
|
|
476
487
|
}
|
|
477
488
|
|
|
478
489
|
function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame | undefined {
|
|
@@ -488,12 +499,9 @@ function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame
|
|
|
488
499
|
const outerSession = frame.sessionId;
|
|
489
500
|
const innerSession = payload?.sessionId;
|
|
490
501
|
const outerGeneration = frame.generation;
|
|
491
|
-
const innerGeneration = payload?.generation;
|
|
492
502
|
if (outerSession !== undefined && innerSession !== undefined && outerSession !== innerSession) return undefined;
|
|
493
|
-
if (outerGeneration !== undefined && innerGeneration !== undefined && outerGeneration !== innerGeneration)
|
|
494
|
-
return undefined;
|
|
495
503
|
const sessionClaim = outerSession !== undefined ? outerSession : innerSession;
|
|
496
|
-
const generationClaim = outerGeneration
|
|
504
|
+
const generationClaim = outerGeneration;
|
|
497
505
|
const sessionId = readSession(sessionClaim);
|
|
498
506
|
const generation = readGeneration(generationClaim);
|
|
499
507
|
if (sessionClaim !== undefined && sessionId === undefined) return undefined;
|
|
@@ -514,10 +522,24 @@ function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame
|
|
|
514
522
|
body,
|
|
515
523
|
name: readName(frame.name) ?? readName(frame.kind) ?? readName(body.type),
|
|
516
524
|
sessionId,
|
|
525
|
+
revision: typeof frame.revision === "number" ? frame.revision : undefined,
|
|
517
526
|
generation,
|
|
518
527
|
commandId,
|
|
519
528
|
turnId,
|
|
520
|
-
seq: readSequence(frame.seq)
|
|
529
|
+
seq: readSequence(frame.seq),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function coordinateFreeRouterFrame(correlated: SessionRouterFrame): SessionRouterFrame {
|
|
534
|
+
return {
|
|
535
|
+
body: correlated.body,
|
|
536
|
+
name: correlated.name,
|
|
537
|
+
sessionId: correlated.sessionId,
|
|
538
|
+
...(correlated.revision === undefined ? {} : { revision: correlated.revision }),
|
|
539
|
+
generation: undefined,
|
|
540
|
+
seq: undefined,
|
|
541
|
+
...(correlated.commandId === undefined ? {} : { commandId: correlated.commandId }),
|
|
542
|
+
...(correlated.turnId === undefined ? {} : { turnId: correlated.turnId }),
|
|
521
543
|
};
|
|
522
544
|
}
|
|
523
545
|
|
|
@@ -565,6 +587,7 @@ type AdoptedSession = {
|
|
|
565
587
|
*/
|
|
566
588
|
export class SessionRouter {
|
|
567
589
|
readonly #agentDir: string;
|
|
590
|
+
readonly #sessionIds: ReadonlySet<string> | undefined;
|
|
568
591
|
readonly #deps: SessionRouterDeps;
|
|
569
592
|
readonly #correlateFrame: SessionRouterFrameCorrelator;
|
|
570
593
|
readonly #index: SessionIndex;
|
|
@@ -603,6 +626,7 @@ export class SessionRouter {
|
|
|
603
626
|
|
|
604
627
|
constructor(options: SessionRouterOptions) {
|
|
605
628
|
this.#agentDir = options.agentDir;
|
|
629
|
+
this.#sessionIds = options.sessionIds === undefined ? undefined : new Set(options.sessionIds);
|
|
606
630
|
this.#deps = options.deps ?? {};
|
|
607
631
|
this.#correlateFrame = options.correlateFrame ?? fallbackCorrelation;
|
|
608
632
|
this.#index = this.#deps.createIndex?.(options.agentDir) ?? new DefaultSessionIndex(options.agentDir);
|
|
@@ -654,7 +678,7 @@ export class SessionRouter {
|
|
|
654
678
|
// STARTUP_ATTACH_BUDGET_MS: those attachments are published with their
|
|
655
679
|
// initial replay delivered. Past that budget the pass continues in the
|
|
656
680
|
// background instead of owning the caller's deadline.
|
|
657
|
-
const initialPass = this.#serialReconcile(runEpoch,
|
|
681
|
+
const initialPass = this.#serialReconcile(runEpoch, true, true);
|
|
658
682
|
// Cancelled as soon as the race settles. Promise.race leaves the loser pending,
|
|
659
683
|
// and a pending Bun.sleep keeps the event loop alive, so a startup that settled
|
|
660
684
|
// in milliseconds still owed the rest of the budget before the process could exit.
|
|
@@ -741,6 +765,7 @@ export class SessionRouter {
|
|
|
741
765
|
const endpointMtimeMs = readEndpointMtime(result.endpointMtimeMs);
|
|
742
766
|
if (
|
|
743
767
|
sessionId !== fallback.sessionId ||
|
|
768
|
+
(this.#sessionIds !== undefined && !this.#sessionIds.has(sessionId ?? "")) ||
|
|
744
769
|
endpointGeneration === undefined ||
|
|
745
770
|
pid === undefined ||
|
|
746
771
|
endpointMtimeMs === undefined ||
|
|
@@ -753,11 +778,11 @@ export class SessionRouter {
|
|
|
753
778
|
"pre_send",
|
|
754
779
|
"Broker lifecycle result omitted an exact session endpoint authority.",
|
|
755
780
|
);
|
|
756
|
-
const
|
|
757
|
-
const stateRoot = path.join(
|
|
781
|
+
const cwd = await canonicalSessionCwd(fallback.cwd);
|
|
782
|
+
const stateRoot = path.join(cwd, ".gjc", "state");
|
|
758
783
|
const indexed: IndexedSession = {
|
|
759
784
|
sessionId,
|
|
760
|
-
locator:
|
|
785
|
+
locator: await resolveSessionLocator(cwd, stateRoot),
|
|
761
786
|
endpointGeneration,
|
|
762
787
|
pid,
|
|
763
788
|
endpointMtimeMs,
|
|
@@ -780,8 +805,9 @@ export class SessionRouter {
|
|
|
780
805
|
if (!attached || !current || !capability)
|
|
781
806
|
throw new SessionRouterError("pre_send", "Broker session endpoint could not be attached.");
|
|
782
807
|
const listing = this.#index.listSessions();
|
|
783
|
-
const indexedCurrent =
|
|
784
|
-
|
|
808
|
+
const indexedCurrent = listing.warnings.some(warning => warningAffectsSession(warning, sessionId))
|
|
809
|
+
? undefined
|
|
810
|
+
: listing.sessions.find(item => item.sessionId === sessionId);
|
|
785
811
|
if (indexedCurrent && sameIndexedAuthority(indexed, indexedCurrent))
|
|
786
812
|
await this.#serialReconcile(this.#runEpoch, true, true);
|
|
787
813
|
return capability;
|
|
@@ -814,8 +840,11 @@ export class SessionRouter {
|
|
|
814
840
|
attached.dispose();
|
|
815
841
|
this.#detachNotification(attached, "removed");
|
|
816
842
|
shutdownTasks.push((async () => await attached.client.close())());
|
|
817
|
-
|
|
818
|
-
|
|
843
|
+
shutdownTasks.push(
|
|
844
|
+
Promise.resolve(this.#deps.onSessionRemoved?.(attached.capability)).catch(error => {
|
|
845
|
+
logger.warn(`SDK provider cleanup failed during router stop: ${String(error)}`);
|
|
846
|
+
throw error;
|
|
847
|
+
}),
|
|
819
848
|
);
|
|
820
849
|
}
|
|
821
850
|
this.#adopted.clear();
|
|
@@ -880,15 +909,19 @@ export class SessionRouter {
|
|
|
880
909
|
onDispatch?: SdkDispatchHandler;
|
|
881
910
|
},
|
|
882
911
|
): Promise<Record<string, unknown>> {
|
|
912
|
+
const matchesExpectedAuthority = (attachment: SessionAttachment): boolean =>
|
|
913
|
+
expectedAttachment === undefined ||
|
|
914
|
+
attachment === expectedAttachment ||
|
|
915
|
+
(expectedAttachment.authorityId !== undefined && attachment.authorityId === expectedAttachment.authorityId);
|
|
883
916
|
const publishing = this.#sessions.get(sessionId);
|
|
884
|
-
if (!
|
|
917
|
+
if (!publishing || !matchesExpectedAuthority(publishing.capability) || !publishing.initializingPublication)
|
|
885
918
|
await this.#serialReconcile(this.#runEpoch, true, true);
|
|
886
919
|
const attached = this.#sessions.get(sessionId);
|
|
887
920
|
if (!attached || !this.#attachmentPublished(attached))
|
|
888
921
|
throw new SessionRouterError("pre_send", "SDK session attachment is unavailable: session not published.");
|
|
889
922
|
if (expectedGeneration !== undefined && expectedGeneration !== attached.generation)
|
|
890
923
|
throw new SessionRouterError("pre_send", "SDK session endpoint changed before command dispatch.");
|
|
891
|
-
if (
|
|
924
|
+
if (!matchesExpectedAuthority(attached.capability))
|
|
892
925
|
throw new SessionRouterError("pre_send", "SDK session attachment changed before command dispatch.");
|
|
893
926
|
if (attached.initializingPublication) {
|
|
894
927
|
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
@@ -936,10 +969,12 @@ export class SessionRouter {
|
|
|
936
969
|
}
|
|
937
970
|
: {}),
|
|
938
971
|
});
|
|
972
|
+
const settled = this.#sessions.get(sessionId);
|
|
939
973
|
if (
|
|
940
|
-
!
|
|
941
|
-
(
|
|
942
|
-
(
|
|
974
|
+
!settled ||
|
|
975
|
+
!this.#attachmentPublished(settled) ||
|
|
976
|
+
(expectedGeneration !== undefined && settled.generation !== expectedGeneration) ||
|
|
977
|
+
!matchesExpectedAuthority(settled.capability)
|
|
943
978
|
)
|
|
944
979
|
throw new SessionRouterError("ambiguous", "SDK session attachment changed while awaiting command response.");
|
|
945
980
|
return response;
|
|
@@ -953,7 +988,7 @@ export class SessionRouter {
|
|
|
953
988
|
try {
|
|
954
989
|
await this.#index.refresh();
|
|
955
990
|
const listing = this.#index.listSessions();
|
|
956
|
-
if (listing.warnings.
|
|
991
|
+
if (listing.warnings.some(warning => warningAffectsSession(warning, sessionId))) return undefined;
|
|
957
992
|
indexed = listing.sessions.find(candidate => candidate.sessionId === sessionId);
|
|
958
993
|
} catch {
|
|
959
994
|
return undefined;
|
|
@@ -1025,7 +1060,7 @@ export class SessionRouter {
|
|
|
1025
1060
|
await this.#index.open();
|
|
1026
1061
|
await this.#index.refresh();
|
|
1027
1062
|
const listing = this.#index.listSessions();
|
|
1028
|
-
if (listing.warnings.
|
|
1063
|
+
if (listing.warnings.some(warning => warningAffectsSession(warning, sessionId)))
|
|
1029
1064
|
throw new SessionActivationError(
|
|
1030
1065
|
"session_not_live",
|
|
1031
1066
|
"Session activation requires an intact session index.",
|
|
@@ -1203,50 +1238,51 @@ export class SessionRouter {
|
|
|
1203
1238
|
return;
|
|
1204
1239
|
}
|
|
1205
1240
|
const indexed = this.#index.listSessions();
|
|
1206
|
-
const live =
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1241
|
+
const live = indexed.sessions.filter(
|
|
1242
|
+
session =>
|
|
1243
|
+
!indexed.warnings.some(warning => warningAffectsSession(warning, session.sessionId)) &&
|
|
1244
|
+
session.live &&
|
|
1245
|
+
isSessionAuthorityEligible(session) &&
|
|
1246
|
+
!session.terminalUncertain &&
|
|
1247
|
+
(this.#sessionIds === undefined || this.#sessionIds.has(session.sessionId)),
|
|
1248
|
+
);
|
|
1212
1249
|
const liveIds = new Set(live.map(session => session.sessionId));
|
|
1213
1250
|
const attachedIds = new Set<string>();
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
this.#adopted.delete(sessionId);
|
|
1244
|
-
attachedIds.add(sessionId);
|
|
1245
|
-
}
|
|
1246
|
-
} catch {
|
|
1251
|
+
for (const [sessionId, adopted] of [...this.#adopted]) {
|
|
1252
|
+
if (indexed.warnings.some(warning => warningAffectsSession(warning, sessionId))) continue;
|
|
1253
|
+
const attached = this.#sessions.get(sessionId);
|
|
1254
|
+
const indexedSession = indexed.sessions.find(session => session.sessionId === sessionId);
|
|
1255
|
+
if (!attached || attached.capability !== adopted.attachment) {
|
|
1256
|
+
this.#adopted.delete(sessionId);
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
const exactIndex =
|
|
1260
|
+
indexedSession?.live === true &&
|
|
1261
|
+
isSessionAuthorityEligible(indexedSession) &&
|
|
1262
|
+
!indexedSession.terminalUncertain &&
|
|
1263
|
+
indexedSession.endpointGeneration === adopted.generation &&
|
|
1264
|
+
indexedSession.pid === adopted.pid &&
|
|
1265
|
+
indexedSession.endpointMtimeMs === adopted.endpointMtimeMs;
|
|
1266
|
+
const endpoint = exactIndex ? await this.#readEndpoint(indexedSession).catch(() => null) : null;
|
|
1267
|
+
if (
|
|
1268
|
+
!exactIndex ||
|
|
1269
|
+
!endpoint ||
|
|
1270
|
+
endpoint.pid !== adopted.pid ||
|
|
1271
|
+
endpoint.url !== attached.endpoint.url ||
|
|
1272
|
+
endpoint.token !== attached.endpoint.token
|
|
1273
|
+
) {
|
|
1274
|
+
this.#adopted.delete(sessionId);
|
|
1275
|
+
await this.#retireAttachment(attached);
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
if (await this.#publishAttachment(attached, true)) {
|
|
1247
1280
|
this.#adopted.delete(sessionId);
|
|
1248
|
-
|
|
1281
|
+
attachedIds.add(sessionId);
|
|
1249
1282
|
}
|
|
1283
|
+
} catch {
|
|
1284
|
+
this.#adopted.delete(sessionId);
|
|
1285
|
+
await this.#retireAttachment(attached);
|
|
1250
1286
|
}
|
|
1251
1287
|
}
|
|
1252
1288
|
let nextAttachment = 0;
|
|
@@ -1361,15 +1397,11 @@ export class SessionRouter {
|
|
|
1361
1397
|
indexed: IndexedSession,
|
|
1362
1398
|
): Promise<{ endpoint: SdkSessionEndpoint; identity: SessionEndpointIdentity } | null> {
|
|
1363
1399
|
if (!isSessionAuthorityEligible(indexed)) return null;
|
|
1364
|
-
const
|
|
1365
|
-
const defaultStateRoot = path.join(
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
// The scope test therefore compares path identity, not spelling: a lexical
|
|
1370
|
-
// match fails for every symlinked cwd (macOS /var -> /private/var,
|
|
1371
|
-
// /home/jun/desk -> /data/Lina-Desk), the adopted attachment is retired on
|
|
1372
|
-
// reconcile, and session/new surfaces "lost exact Router authority".
|
|
1400
|
+
const cwd = indexed.locator.cwd;
|
|
1401
|
+
const defaultStateRoot = path.join(cwd, ".gjc", "state");
|
|
1402
|
+
// Locator cwd is already canonical. `stateRoot` remains the host-provided
|
|
1403
|
+
// authority path, so this comparison resolves equivalent paths only for the
|
|
1404
|
+
// state-root identity boundary; cwd never retains lexical symlink spellings.
|
|
1373
1405
|
const indexedStateRoot = resolveEquivalentPath(indexed.locator.stateRoot);
|
|
1374
1406
|
const scope =
|
|
1375
1407
|
indexedStateRoot === resolveEquivalentPath(defaultStateRoot)
|
|
@@ -1378,9 +1410,9 @@ export class SessionRouter {
|
|
|
1378
1410
|
? "chat"
|
|
1379
1411
|
: undefined;
|
|
1380
1412
|
if (!scope || indexed.endpointMtimeMs === undefined || !Number.isFinite(indexed.endpointMtimeMs)) return null;
|
|
1381
|
-
const endpointPath = path.join(endpointDirectory(
|
|
1413
|
+
const endpointPath = path.join(endpointDirectory(cwd, scope), `${indexed.sessionId}.json`);
|
|
1382
1414
|
const endpointIdentity = await lstatEndpoint(endpointPath);
|
|
1383
|
-
const endpoint = await readSdkSessionEndpoint(
|
|
1415
|
+
const endpoint = await readSdkSessionEndpoint(cwd, indexed.sessionId, scope);
|
|
1384
1416
|
if (!endpoint || endpoint.stale || endpoint.pid !== indexed.pid) return null;
|
|
1385
1417
|
if (!endpointIdentity || endpointIdentity.mtimeMs !== indexed.endpointMtimeMs) return null;
|
|
1386
1418
|
// Identity is proven INSIDE this authority read (#4730 review): sampling it
|
|
@@ -1413,7 +1445,7 @@ export class SessionRouter {
|
|
|
1413
1445
|
return null;
|
|
1414
1446
|
await this.#index.refresh();
|
|
1415
1447
|
const listing = this.#index.listSessions();
|
|
1416
|
-
if (listing.warnings.
|
|
1448
|
+
if (listing.warnings.some(warning => warningAffectsSession(warning, indexed.sessionId))) return null;
|
|
1417
1449
|
const current = listing.sessions.find(session => session.sessionId === indexed.sessionId);
|
|
1418
1450
|
if (!current || !sameIndexedAuthority(indexed, current)) return null;
|
|
1419
1451
|
return { endpoint, identity: provenIdentity };
|
|
@@ -1529,7 +1561,12 @@ export class SessionRouter {
|
|
|
1529
1561
|
existing.endpointMtimeMs === indexed.endpointMtimeMs &&
|
|
1530
1562
|
endpointIdentity !== undefined &&
|
|
1531
1563
|
sameEndpointIdentity(existing.endpointIdentity, endpointIdentity);
|
|
1532
|
-
if (
|
|
1564
|
+
if (
|
|
1565
|
+
existing &&
|
|
1566
|
+
resumable &&
|
|
1567
|
+
!existing.barrier.failed &&
|
|
1568
|
+
(!existing.replayPending || Date.now() - (existing.replayPendingSince ?? Date.now()) < REPLAY_PENDING_STALL_MS)
|
|
1569
|
+
) {
|
|
1533
1570
|
this.#reviveTransport(existing);
|
|
1534
1571
|
return true;
|
|
1535
1572
|
}
|
|
@@ -1691,6 +1728,7 @@ export class SessionRouter {
|
|
|
1691
1728
|
});
|
|
1692
1729
|
attached = {
|
|
1693
1730
|
initializingPublication: false,
|
|
1731
|
+
replayPending: false,
|
|
1694
1732
|
id: crypto.randomUUID(),
|
|
1695
1733
|
sessionId: indexed.sessionId,
|
|
1696
1734
|
endpoint,
|
|
@@ -1828,15 +1866,33 @@ export class SessionRouter {
|
|
|
1828
1866
|
// generation fences are unchanged; replay just runs on the attachment's
|
|
1829
1867
|
// ready tail like the reconnect path (#4527).
|
|
1830
1868
|
if (deferReplay) {
|
|
1869
|
+
const replayToken = {};
|
|
1870
|
+
attached.replayPending = true;
|
|
1871
|
+
attached.replayPendingSince = Date.now();
|
|
1872
|
+
attached.replayPendingToken = replayToken;
|
|
1831
1873
|
attached.readyTail = attached.readyTail
|
|
1832
1874
|
.catch(() => undefined)
|
|
1833
1875
|
.then(async () => {
|
|
1876
|
+
if (!this.#attachmentLive(attached)) return;
|
|
1877
|
+
await this.#deps.onReplayReady?.();
|
|
1834
1878
|
if (!this.#attachmentLive(attached)) return;
|
|
1835
1879
|
if (!(await this.#deliverRecoveredFrames(attached))) return;
|
|
1836
1880
|
await this.#replayAttachment(attached, attached.cursor.seq);
|
|
1881
|
+
})
|
|
1882
|
+
.catch(error => {
|
|
1883
|
+
attached.barrier.failed = true;
|
|
1884
|
+
logger.warn(`SDK deferred replay failed for ${attached.sessionId}: ${String(error)}`);
|
|
1885
|
+
})
|
|
1886
|
+
.finally(() => {
|
|
1887
|
+
if (attached.replayPendingToken === replayToken) {
|
|
1888
|
+
attached.replayPending = false;
|
|
1889
|
+
attached.replayPendingSince = undefined;
|
|
1890
|
+
attached.replayPendingToken = undefined;
|
|
1891
|
+
}
|
|
1837
1892
|
});
|
|
1838
1893
|
return true;
|
|
1839
1894
|
}
|
|
1895
|
+
await this.#deps.onReplayReady?.();
|
|
1840
1896
|
if (!(await this.#deliverRecoveredFrames(attached))) return false;
|
|
1841
1897
|
await this.#replayAttachment(attached, attached.cursor.seq);
|
|
1842
1898
|
return true;
|
|
@@ -1844,6 +1900,10 @@ export class SessionRouter {
|
|
|
1844
1900
|
|
|
1845
1901
|
async #reinitializeAttachment(attached: AttachedSession): Promise<void> {
|
|
1846
1902
|
const previous = attached.readyTail;
|
|
1903
|
+
const replayToken = {};
|
|
1904
|
+
attached.replayPending = true;
|
|
1905
|
+
attached.replayPendingSince = Date.now();
|
|
1906
|
+
attached.replayPendingToken = replayToken;
|
|
1847
1907
|
const current = previous
|
|
1848
1908
|
.catch(() => undefined)
|
|
1849
1909
|
.then(async () => {
|
|
@@ -1868,6 +1928,7 @@ export class SessionRouter {
|
|
|
1868
1928
|
logger.warn(`SDK notification subscription reconnect hook failed locally: ${String(error)}`);
|
|
1869
1929
|
});
|
|
1870
1930
|
await this.#deps.onAttachmentReady?.(attached.capability);
|
|
1931
|
+
await this.#deps.onReplayReady?.();
|
|
1871
1932
|
} catch {
|
|
1872
1933
|
await this.#retireAttachment(attached);
|
|
1873
1934
|
return;
|
|
@@ -1876,8 +1937,14 @@ export class SessionRouter {
|
|
|
1876
1937
|
}
|
|
1877
1938
|
if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
|
|
1878
1939
|
});
|
|
1879
|
-
attached.readyTail = current
|
|
1880
|
-
|
|
1940
|
+
attached.readyTail = current.finally(() => {
|
|
1941
|
+
if (attached.replayPendingToken === replayToken) {
|
|
1942
|
+
attached.replayPending = false;
|
|
1943
|
+
attached.replayPendingSince = undefined;
|
|
1944
|
+
attached.replayPendingToken = undefined;
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
await attached.readyTail;
|
|
1881
1948
|
}
|
|
1882
1949
|
#reviveTransport(attached: AttachedSession): void {
|
|
1883
1950
|
const connect = attached.client.connect?.bind(attached.client);
|
|
@@ -1899,7 +1966,14 @@ export class SessionRouter {
|
|
|
1899
1966
|
*/
|
|
1900
1967
|
#hasUnhealthyAttachment(): boolean {
|
|
1901
1968
|
for (const attached of this.#sessions.values()) {
|
|
1902
|
-
if (
|
|
1969
|
+
if (
|
|
1970
|
+
attached.barrier.failed ||
|
|
1971
|
+
attached.barrier.detached ||
|
|
1972
|
+
!attached.published ||
|
|
1973
|
+
(attached.replayPending &&
|
|
1974
|
+
Date.now() - (attached.replayPendingSince ?? Date.now()) >= REPLAY_PENDING_STALL_MS)
|
|
1975
|
+
)
|
|
1976
|
+
return true;
|
|
1903
1977
|
}
|
|
1904
1978
|
return false;
|
|
1905
1979
|
}
|
|
@@ -2131,9 +2205,9 @@ export class SessionRouter {
|
|
|
2131
2205
|
const correlated = this.#correlateFrame(frame);
|
|
2132
2206
|
if (!correlated) return;
|
|
2133
2207
|
if (correlated.sessionId !== undefined && correlated.sessionId !== attached.sessionId) return;
|
|
2134
|
-
|
|
2135
|
-
this.#dispatchNotificationFrame(attached,
|
|
2136
|
-
void Promise.resolve(this.#deps.onFrame?.(attached.capability,
|
|
2208
|
+
const notificationFrame = coordinateFreeRouterFrame(correlated);
|
|
2209
|
+
this.#dispatchNotificationFrame(attached, notificationFrame);
|
|
2210
|
+
void Promise.resolve(this.#deps.onFrame?.(attached.capability, notificationFrame)).catch(error =>
|
|
2137
2211
|
logger.warn(`SDK provider frame hook failed: ${String(error)}`),
|
|
2138
2212
|
);
|
|
2139
2213
|
}
|
|
@@ -2150,26 +2224,18 @@ export class SessionRouter {
|
|
|
2150
2224
|
const correlated = this.#correlateFrame(frame);
|
|
2151
2225
|
if (!correlated) return;
|
|
2152
2226
|
const sequenceClaim = readFrameSequenceClaim(frame);
|
|
2153
|
-
|
|
2227
|
+
const ringFrame = frame.type === "event";
|
|
2228
|
+
const generationClaim = Object.hasOwn(frame, "generation");
|
|
2229
|
+
const outerGeneration = generationClaim ? readGeneration(frame.generation) : undefined;
|
|
2230
|
+
const partialCoordinate = ringFrame && sequenceClaim.claimed !== generationClaim;
|
|
2231
|
+
if (!sequenceClaim.valid || partialCoordinate || (generationClaim && outerGeneration === undefined)) {
|
|
2154
2232
|
this.#failBarrier(attached, "protocol error: malformed sequence coordinate");
|
|
2155
2233
|
return;
|
|
2156
2234
|
}
|
|
2157
|
-
const
|
|
2158
|
-
if (correlated.seq !== undefined && correlatedSeq === undefined) {
|
|
2159
|
-
this.#failBarrier(attached, "protocol error: malformed correlated sequence coordinate");
|
|
2160
|
-
return;
|
|
2161
|
-
}
|
|
2162
|
-
if (sequenceClaim.claimed && correlatedSeq !== undefined && correlatedSeq !== sequenceClaim.seq) {
|
|
2163
|
-
this.#failBarrier(attached, "protocol error: inconsistent sequence coordinate");
|
|
2164
|
-
return;
|
|
2165
|
-
}
|
|
2166
|
-
const seq = sequenceClaim.seq ?? correlatedSeq;
|
|
2235
|
+
const seq = ringFrame ? sequenceClaim.seq : undefined;
|
|
2167
2236
|
if (correlated.sessionId !== undefined && correlated.sessionId !== attached.sessionId) return;
|
|
2168
|
-
if (
|
|
2169
|
-
|
|
2170
|
-
const ownsSequence =
|
|
2171
|
-
correlated.generation === attached.generation &&
|
|
2172
|
-
(correlated.sessionId === undefined || correlated.sessionId === attached.sessionId);
|
|
2237
|
+
if (outerGeneration !== undefined && outerGeneration !== attached.generation) return;
|
|
2238
|
+
const ownsSequence = seq !== undefined && outerGeneration === attached.generation;
|
|
2173
2239
|
if (seq !== undefined && ownsSequence) {
|
|
2174
2240
|
if (seq <= attached.cursor.seq) return;
|
|
2175
2241
|
const held = attached.barrier.held;
|
|
@@ -2184,16 +2250,19 @@ export class SessionRouter {
|
|
|
2184
2250
|
}
|
|
2185
2251
|
const publicationId =
|
|
2186
2252
|
seq !== undefined && ownsSequence ? `${attached.sessionId}:${attached.generation}:${seq}` : undefined;
|
|
2187
|
-
|
|
2253
|
+
const coordinateFree = coordinateFreeRouterFrame(correlated);
|
|
2254
|
+
const notificationFrame: SessionRouterFrame =
|
|
2255
|
+
publicationId === undefined
|
|
2256
|
+
? coordinateFree
|
|
2257
|
+
: { ...coordinateFree, generation: outerGeneration, publicationId, seq };
|
|
2188
2258
|
try {
|
|
2189
|
-
notificationFrame = publicationId === undefined ? correlated : { ...correlated, publicationId };
|
|
2190
2259
|
this.#dispatchNotificationFrame(attached, notificationFrame);
|
|
2191
2260
|
await this.#deps.onFrame?.(attached.capability, notificationFrame);
|
|
2192
2261
|
} catch (error) {
|
|
2193
2262
|
if (!this.#attachmentLive(attached)) return;
|
|
2194
2263
|
if (seq === undefined || !ownsSequence) throw error;
|
|
2195
2264
|
this.#failDelivery(attached, seq, error);
|
|
2196
|
-
this.#deps.onFrameSettled?.(attached.capability,
|
|
2265
|
+
this.#deps.onFrameSettled?.(attached.capability, notificationFrame);
|
|
2197
2266
|
return;
|
|
2198
2267
|
}
|
|
2199
2268
|
if (!this.#attachmentLive(attached)) return;
|
|
@@ -2285,12 +2354,40 @@ export class SessionRouter {
|
|
|
2285
2354
|
if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
|
|
2286
2355
|
await this.#frameTails.get(attached.id)?.catch(() => undefined);
|
|
2287
2356
|
if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2357
|
+
if (!Array.isArray(replay.events)) {
|
|
2358
|
+
this.#failBarrier(attached, "replay response omitted an events array");
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
if (replay.ok !== true || readGeneration(replay.generation) !== attached.generation) {
|
|
2362
|
+
this.#failBarrier(attached, "replay response failed or crossed generations");
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
const lastSeq = readNonnegativeSequence(replay.lastSeq);
|
|
2366
|
+
if (lastSeq === undefined || lastSeq < sinceSeq) {
|
|
2367
|
+
this.#failBarrier(attached, "replay response omitted a coherent high-watermark");
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
const events = replay.events.filter(
|
|
2371
|
+
(event): event is Record<string, unknown> => !!event && typeof event === "object" && !Array.isArray(event),
|
|
2372
|
+
);
|
|
2373
|
+
if (events.length !== replay.events.length) {
|
|
2374
|
+
this.#failBarrier(attached, "replay response contained a malformed event");
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
let previousSeq = sinceSeq;
|
|
2378
|
+
for (const event of events) {
|
|
2379
|
+
if (event.type !== "event") {
|
|
2380
|
+
this.#failBarrier(attached, "replay response contained a non-event frame");
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const generation = readGeneration(event.generation);
|
|
2384
|
+
const seq = readSequence(event.seq);
|
|
2385
|
+
if (generation !== attached.generation || seq === undefined || seq <= previousSeq || seq > lastSeq) {
|
|
2386
|
+
this.#failBarrier(attached, "replay response contained invalid or unordered coordinates");
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
previousSeq = seq;
|
|
2390
|
+
}
|
|
2294
2391
|
if (replay.gap !== undefined) {
|
|
2295
2392
|
const gap = readReplayGap(replay.gap);
|
|
2296
2393
|
if (!gap) {
|
|
@@ -2308,6 +2405,10 @@ export class SessionRouter {
|
|
|
2308
2405
|
);
|
|
2309
2406
|
return;
|
|
2310
2407
|
}
|
|
2408
|
+
if (gap.toSeq > lastSeq) {
|
|
2409
|
+
this.#failBarrier(attached, "replay gap exceeded the reported high-watermark");
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2311
2412
|
const retained = events
|
|
2312
2413
|
.map(event => readSequence(event.seq))
|
|
2313
2414
|
.find(seq => seq !== undefined && seq <= gap.toSeq);
|