@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.
Files changed (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. 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 || !stat.isFile()) return undefined;
446
+ if (!stat?.isFile()) return undefined;
431
447
  const identity = await fs.lstat(file, { bigint: true }).catch(() => undefined);
432
- if (!identity || !identity.isFile()) return undefined;
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
- const payload =
465
- frame.type === "event" && frame.payload && typeof frame.payload === "object" && !Array.isArray(frame.payload)
466
- ? (frame.payload as Record<string, unknown>)
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 !== undefined ? outerGeneration : innerGeneration;
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) ?? readSequence(payload?.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, false, true);
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 repo = path.resolve(fallback.cwd);
757
- const stateRoot = path.join(repo, ".gjc", "state");
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: { repo, stateRoot },
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
- listing.warnings.length === 0 ? listing.sessions.find(item => item.sessionId === sessionId) : undefined;
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
- void Promise.resolve(this.#deps.onSessionRemoved?.(attached.capability)).catch(error =>
818
- logger.warn(`SDK provider cleanup failed during router stop: ${String(error)}`),
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 (!expectedAttachment || publishing?.capability !== expectedAttachment || !publishing.initializingPublication)
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 (expectedAttachment !== undefined && attached.capability !== expectedAttachment)
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
- !this.#attachmentPublished(attached) ||
941
- (expectedGeneration !== undefined && attached.generation !== expectedGeneration) ||
942
- (expectedAttachment !== undefined && attached.capability !== expectedAttachment)
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.length > 0) return undefined;
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.length > 0)
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
- indexed.warnings.length === 0
1208
- ? indexed.sessions.filter(
1209
- session => session.live && isSessionAuthorityEligible(session) && !session.terminalUncertain,
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
- if (indexed.warnings.length === 0) {
1215
- for (const [sessionId, adopted] of [...this.#adopted]) {
1216
- const attached = this.#sessions.get(sessionId);
1217
- const indexedSession = indexed.sessions.find(session => session.sessionId === sessionId);
1218
- if (!attached || attached.capability !== adopted.attachment) {
1219
- this.#adopted.delete(sessionId);
1220
- continue;
1221
- }
1222
- const exactIndex =
1223
- indexedSession?.live === true &&
1224
- isSessionAuthorityEligible(indexedSession) &&
1225
- !indexedSession.terminalUncertain &&
1226
- indexedSession.endpointGeneration === adopted.generation &&
1227
- indexedSession.pid === adopted.pid &&
1228
- indexedSession.endpointMtimeMs === adopted.endpointMtimeMs;
1229
- const endpoint = exactIndex ? await this.#readEndpoint(indexedSession).catch(() => null) : null;
1230
- if (
1231
- !exactIndex ||
1232
- !endpoint ||
1233
- endpoint.pid !== adopted.pid ||
1234
- endpoint.url !== attached.endpoint.url ||
1235
- endpoint.token !== attached.endpoint.token
1236
- ) {
1237
- this.#adopted.delete(sessionId);
1238
- await this.#retireAttachment(attached);
1239
- continue;
1240
- }
1241
- try {
1242
- if (await this.#publishAttachment(attached, true)) {
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
- await this.#retireAttachment(attached);
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 repo = path.resolve(indexed.locator.repo);
1365
- const defaultStateRoot = path.join(repo, ".gjc", "state");
1366
- // The session index stores the lifecycle caller's lexical cwd in `locator.repo`
1367
- // (reconcileReadyScope re-scopes only that field) while `locator.stateRoot` is
1368
- // the host process's physical path, because process.cwd() resolves symlinks.
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(repo, scope), `${indexed.sessionId}.json`);
1413
+ const endpointPath = path.join(endpointDirectory(cwd, scope), `${indexed.sessionId}.json`);
1382
1414
  const endpointIdentity = await lstatEndpoint(endpointPath);
1383
- const endpoint = await readSdkSessionEndpoint(repo, indexed.sessionId, scope);
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.length > 0) return null;
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 (existing && resumable && !existing.barrier.failed) {
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
- await current;
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 (attached.barrier.failed || attached.barrier.detached || !attached.published) return true;
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
- if (correlated.generation !== undefined && correlated.generation !== attached.generation) return;
2135
- this.#dispatchNotificationFrame(attached, correlated);
2136
- void Promise.resolve(this.#deps.onFrame?.(attached.capability, correlated)).catch(error =>
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
- if (!sequenceClaim.valid) {
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 correlatedSeq = correlated.seq === undefined ? undefined : readSequence(correlated.seq);
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 (correlated.generation !== undefined && correlated.generation !== attached.generation) return;
2169
- if (seq !== undefined && correlated.generation === undefined) return;
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
- let notificationFrame: SessionRouterFrame;
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, { ...correlated, seq });
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
- const events = Array.isArray(replay.events)
2289
- ? replay.events.filter(
2290
- (event): event is Record<string, unknown> =>
2291
- !!event && typeof event === "object" && !Array.isArray(event),
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);