@gajae-code/coding-agent 0.16.0 → 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 +41 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -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/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -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/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -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/rows.d.ts +8 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +1 -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 +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -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 +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -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/index.d.ts +6 -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 +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- 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.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +154 -28
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +5 -5
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- 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 +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- 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/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -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 +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +111 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- 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/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +33 -21
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +2 -0
- 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 +463 -8
- package/src/sdk/host/session-runtime.ts +383 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +152 -59
- package/src/sdk/session.ts +60 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- 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 +156 -33
- 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/index.ts +6 -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
|
@@ -34,7 +34,7 @@ const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
|
|
|
34
34
|
"thinking.cycle": "cycleThinkingLevel",
|
|
35
35
|
"queue.steering_mode.set": "setQueueMode",
|
|
36
36
|
"queue.follow_up_mode.set": "setQueueMode",
|
|
37
|
-
"queue.
|
|
37
|
+
"queue.tool_interrupt_policy.set": "setQueueMode",
|
|
38
38
|
"todo.replace": "sdkControl",
|
|
39
39
|
"permission_mode.set": "sdkControl",
|
|
40
40
|
"skill.invoke": "invokeSkill",
|
|
@@ -62,8 +62,8 @@ const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
|
|
|
62
62
|
// Resource queries (`artifact.read`, `runtime.jobs.list`) remain dispatchable when their
|
|
63
63
|
// backing session resource is absent so their handlers can return `resource_gone`.
|
|
64
64
|
// `session.checkpoint` (Q30) is likewise unconditionally installed: it is an SDK-native
|
|
65
|
-
// replay authority
|
|
66
|
-
//
|
|
65
|
+
// replay authority whose handler returns explicit `unavailable` when the host cannot
|
|
66
|
+
// provide one atomic transcript/ring/idle capture, so it must never be hidden behind a binding gate.
|
|
67
67
|
const QUERY_BINDINGS: Readonly<Record<string, string | undefined>> = {
|
|
68
68
|
"skill.list/state": "getSkillState",
|
|
69
69
|
"config.list/get": "getConfigItems",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseModelString } from "../config/model-resolver";
|
|
2
|
-
import type
|
|
2
|
+
import { type AgentSession, isSessionDisposalIncompleteError } from "../session/agent-session";
|
|
3
3
|
import { type CreateAgentSessionOptions, createAgentSession } from "./session";
|
|
4
4
|
import {
|
|
5
5
|
lifecycleMcpStartupTimeoutOption,
|
|
@@ -106,7 +106,12 @@ export async function createLifecycleAgentSession(
|
|
|
106
106
|
const expected = parseModelString(modelId);
|
|
107
107
|
const expectedSelector = expected ? `${expected.provider}/${expected.id}` : modelId;
|
|
108
108
|
if (!activeSelector || activeSelector.toLowerCase() !== expectedSelector.toLowerCase()) {
|
|
109
|
-
|
|
109
|
+
try {
|
|
110
|
+
await result.session.dispose();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (!isSessionDisposalIncompleteError(error)) throw error;
|
|
113
|
+
await result.session.awaitDisposeCompletion();
|
|
114
|
+
}
|
|
110
115
|
throw new Error(
|
|
111
116
|
`Model "${modelId}" not found. Use --list-models to see available models.${
|
|
112
117
|
activeSelector ? ` Session resolved ${activeSelector} instead.` : ""
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
promptDeadlineAt,
|
|
8
8
|
recordAttributableProgress,
|
|
9
9
|
} from "./prompt-deadline-lease";
|
|
10
|
+
import type { SdkPromptTerminalOutcome } from "./prompt-status";
|
|
10
11
|
import type { TurnResultContent } from "./turn-result";
|
|
11
12
|
|
|
12
13
|
const MAX_EXPIRY_RETRIES = 5;
|
|
@@ -26,7 +27,7 @@ export type PromptDeadlineOutcome = {
|
|
|
26
27
|
export interface PromptTerminalTransitionEvidence {
|
|
27
28
|
content?: TurnResultContent;
|
|
28
29
|
hasActivity?: boolean;
|
|
29
|
-
outcome?:
|
|
30
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function leaseKey(correlation: InvocationCorrelation): string {
|
|
@@ -346,7 +346,7 @@
|
|
|
346
346
|
"sourceFile": "packages/coding-agent/src/sdk/protocol/operation-registry.ts",
|
|
347
347
|
"sourceKind": "registry",
|
|
348
348
|
"decision": "include",
|
|
349
|
-
"sdkId": "queue.
|
|
349
|
+
"sdkId": "queue.tool_interrupt_policy.set",
|
|
350
350
|
"adapterMappings": {
|
|
351
351
|
"telegram": "generic_safe",
|
|
352
352
|
"discord": "generic_safe",
|
|
@@ -2315,6 +2315,28 @@
|
|
|
2315
2315
|
"testIds": "not_applicable"
|
|
2316
2316
|
}
|
|
2317
2317
|
},
|
|
2318
|
+
{
|
|
2319
|
+
"sourceId": "slash_command:star",
|
|
2320
|
+
"sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
|
|
2321
|
+
"sourceKind": "slash_command",
|
|
2322
|
+
"decision": "exclude",
|
|
2323
|
+
"rationale": "local transcript-discovery metadata command; no SDK operation counterpart or runtime/retention authority",
|
|
2324
|
+
"exclusionMetadata": {
|
|
2325
|
+
"adapterMappings": "not_applicable",
|
|
2326
|
+
"testIds": "not_applicable"
|
|
2327
|
+
}
|
|
2328
|
+
},
|
|
2329
|
+
{
|
|
2330
|
+
"sourceId": "slash_command:unstar",
|
|
2331
|
+
"sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
|
|
2332
|
+
"sourceKind": "slash_command",
|
|
2333
|
+
"decision": "exclude",
|
|
2334
|
+
"rationale": "local transcript-discovery metadata command; no SDK operation counterpart or runtime/retention authority",
|
|
2335
|
+
"exclusionMetadata": {
|
|
2336
|
+
"adapterMappings": "not_applicable",
|
|
2337
|
+
"testIds": "not_applicable"
|
|
2338
|
+
}
|
|
2339
|
+
},
|
|
2318
2340
|
{
|
|
2319
2341
|
"sourceId": "slash_command:resume",
|
|
2320
2342
|
"sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
|
|
@@ -3038,6 +3060,17 @@
|
|
|
3038
3060
|
"testIds": "not_applicable"
|
|
3039
3061
|
}
|
|
3040
3062
|
},
|
|
3063
|
+
{
|
|
3064
|
+
"sourceId": "agent_session:onJobFold",
|
|
3065
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
3066
|
+
"sourceKind": "agent_session",
|
|
3067
|
+
"decision": "exclude",
|
|
3068
|
+
"rationale": "internal fold-event plumbing feeding the bash_folded event frame, not a control seam",
|
|
3069
|
+
"exclusionMetadata": {
|
|
3070
|
+
"adapterMappings": "not_applicable",
|
|
3071
|
+
"testIds": "not_applicable"
|
|
3072
|
+
}
|
|
3073
|
+
},
|
|
3041
3074
|
{
|
|
3042
3075
|
"sourceId": "agent_session:requestForegroundBashBackground",
|
|
3043
3076
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
@@ -3049,6 +3082,24 @@
|
|
|
3049
3082
|
"testIds": "not_applicable"
|
|
3050
3083
|
}
|
|
3051
3084
|
},
|
|
3085
|
+
{
|
|
3086
|
+
"sourceId": "agent_session:requestForegroundBashBackgroundOutcome",
|
|
3087
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
3088
|
+
"sourceKind": "agent_session",
|
|
3089
|
+
"decision": "include",
|
|
3090
|
+
"sdkId": "bash.background",
|
|
3091
|
+
"adapterMappings": {
|
|
3092
|
+
"telegram": "prohibited",
|
|
3093
|
+
"discord": "prohibited",
|
|
3094
|
+
"slack": "prohibited",
|
|
3095
|
+
"mcp": "generic_safe",
|
|
3096
|
+
"acp": "generic_safe",
|
|
3097
|
+
"daemonCli": "generic_safe"
|
|
3098
|
+
},
|
|
3099
|
+
"testIds": [
|
|
3100
|
+
"packages/coding-agent/test/sdk-operation-inventory.test.ts"
|
|
3101
|
+
]
|
|
3102
|
+
},
|
|
3052
3103
|
{
|
|
3053
3104
|
"sourceId": "agent_session:getAllToolNames",
|
|
3054
3105
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
@@ -4732,11 +4783,11 @@
|
|
|
4732
4783
|
]
|
|
4733
4784
|
},
|
|
4734
4785
|
{
|
|
4735
|
-
"sourceId": "agent_session:
|
|
4786
|
+
"sourceId": "agent_session:setToolInterruptPolicy",
|
|
4736
4787
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
4737
4788
|
"sourceKind": "agent_session",
|
|
4738
4789
|
"decision": "include",
|
|
4739
|
-
"sdkId": "queue.
|
|
4790
|
+
"sdkId": "queue.tool_interrupt_policy.set",
|
|
4740
4791
|
"adapterMappings": {
|
|
4741
4792
|
"telegram": "generic_safe",
|
|
4742
4793
|
"discord": "generic_safe",
|
|
@@ -91,7 +91,7 @@ const controls = [
|
|
|
91
91
|
["permission_mode.set", "Set permission mode."],
|
|
92
92
|
["queue.steering_mode.set", "Set steering queue mode."],
|
|
93
93
|
["queue.follow_up_mode.set", "Set follow-up queue mode."],
|
|
94
|
-
["queue.
|
|
94
|
+
["queue.tool_interrupt_policy.set", "Set whether a steer aborts or finishes in-flight tools."],
|
|
95
95
|
["compaction.run", "Run context compaction."],
|
|
96
96
|
["compaction.auto.set", "Set automatic compaction."],
|
|
97
97
|
["retry.auto.set", "Set automatic retry."],
|
package/src/sdk/receipt-state.ts
CHANGED
|
@@ -25,6 +25,16 @@ export function receiptStateForTerminal(source: ReceiptSource): Extract<ReceiptS
|
|
|
25
25
|
return reportableReceipt(source) ? "present" : "missing";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Terminal receipt evidence is monotonic: present > missing > unknown. */
|
|
29
|
+
export function reduceReceiptState(
|
|
30
|
+
existing: Exclude<ReceiptState, "absent"> | undefined,
|
|
31
|
+
incoming: Exclude<ReceiptState, "absent"> | undefined,
|
|
32
|
+
): Exclude<ReceiptState, "absent"> | undefined {
|
|
33
|
+
if (existing === "present" || incoming === "present") return "present";
|
|
34
|
+
if (existing === "missing" || incoming === "missing") return "missing";
|
|
35
|
+
return existing ?? incoming;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
export function reduceTerminalReceiptState(input: TerminalReceiptInput): TerminalReceiptState {
|
|
29
39
|
switch (input.execution) {
|
|
30
40
|
case "accepted":
|
|
@@ -192,11 +192,13 @@ export interface SessionRouterClient {
|
|
|
192
192
|
send(frame: Record<string, unknown>): void;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/** One frame after the caller's envelope/payload identity correlation. */
|
|
195
196
|
/** One frame after the caller's envelope/payload identity correlation. */
|
|
196
197
|
export interface SessionRouterFrame {
|
|
197
198
|
readonly body: Record<string, unknown>;
|
|
198
199
|
readonly name: string | undefined;
|
|
199
200
|
readonly sessionId: string | undefined;
|
|
201
|
+
readonly revision?: number;
|
|
200
202
|
readonly generation: number | undefined;
|
|
201
203
|
readonly commandId?: string;
|
|
202
204
|
readonly turnId?: string;
|
|
@@ -222,6 +224,8 @@ export interface SessionRouterDeps {
|
|
|
222
224
|
onAttachment?: (attachment: SessionAttachment) => Promise<void> | void;
|
|
223
225
|
/** Called only after the opaque capability becomes externally current. */
|
|
224
226
|
onAttachmentReady?: (attachment: SessionAttachment) => Promise<void> | void;
|
|
227
|
+
/** Called before deferred replay may perform provider-facing mutations. */
|
|
228
|
+
onReplayReady?: () => Promise<void> | void;
|
|
225
229
|
/** Called when the Broker index no longer reports an attached session as live. */
|
|
226
230
|
onSessionRemoved?: (
|
|
227
231
|
attachment: SessionAttachment,
|
|
@@ -331,6 +335,9 @@ type AttachedSession = {
|
|
|
331
335
|
readonly notificationCursor: { generation: number; seq: number };
|
|
332
336
|
published: boolean;
|
|
333
337
|
initializingPublication: boolean;
|
|
338
|
+
replayPending: boolean;
|
|
339
|
+
replayPendingSince?: number;
|
|
340
|
+
replayPendingToken?: object;
|
|
334
341
|
readyTail: Promise<void>;
|
|
335
342
|
readonly publication: { promise: Promise<void>; resolve: () => void; reject: (reason?: unknown) => void };
|
|
336
343
|
dispose: () => void;
|
|
@@ -350,6 +357,7 @@ function isUnansweredAfterDispatch(error: unknown): boolean {
|
|
|
350
357
|
}
|
|
351
358
|
|
|
352
359
|
const REPLAY_BARRIER_LIMIT = 1_024;
|
|
360
|
+
const REPLAY_PENDING_STALL_MS = 10_000;
|
|
353
361
|
const REPLAY_RETRY_ATTEMPTS = 3;
|
|
354
362
|
const REPLAY_RETRY_BACKOFF_MS = 100;
|
|
355
363
|
const DELIVERY_ATTEMPT_LIMIT = 3;
|
|
@@ -429,11 +437,15 @@ function readSequence(value: unknown): number | undefined {
|
|
|
429
437
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
|
|
430
438
|
}
|
|
431
439
|
|
|
440
|
+
function readNonnegativeSequence(value: unknown): number | undefined {
|
|
441
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
432
444
|
async function lstatEndpoint(file: string): Promise<SessionEndpointIdentity | undefined> {
|
|
433
445
|
const stat = await fs.lstat(file).catch(() => undefined);
|
|
434
|
-
if (!stat
|
|
446
|
+
if (!stat?.isFile()) return undefined;
|
|
435
447
|
const identity = await fs.lstat(file, { bigint: true }).catch(() => undefined);
|
|
436
|
-
if (!identity
|
|
448
|
+
if (!identity?.isFile()) return undefined;
|
|
437
449
|
if (
|
|
438
450
|
stat.size !== Number(identity.size) ||
|
|
439
451
|
stat.ino !== Number(identity.ino) ||
|
|
@@ -469,18 +481,9 @@ function readFrameSequenceClaim(frame: Record<string, unknown>): {
|
|
|
469
481
|
valid: boolean;
|
|
470
482
|
seq?: number;
|
|
471
483
|
} {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
: undefined;
|
|
476
|
-
const claims: unknown[] = [];
|
|
477
|
-
if (Object.hasOwn(frame, "seq")) claims.push(frame.seq);
|
|
478
|
-
if (payload && Object.hasOwn(payload, "seq")) claims.push(payload.seq);
|
|
479
|
-
if (claims.length === 0) return { claimed: false, valid: true };
|
|
480
|
-
const sequences = claims.map(readSequence);
|
|
481
|
-
if (sequences.some(sequence => sequence === undefined)) return { claimed: true, valid: false };
|
|
482
|
-
if (new Set(sequences).size !== 1) return { claimed: true, valid: false };
|
|
483
|
-
return { claimed: true, valid: true, seq: sequences[0] };
|
|
484
|
+
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 };
|
|
484
487
|
}
|
|
485
488
|
|
|
486
489
|
function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame | undefined {
|
|
@@ -496,12 +499,9 @@ function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame
|
|
|
496
499
|
const outerSession = frame.sessionId;
|
|
497
500
|
const innerSession = payload?.sessionId;
|
|
498
501
|
const outerGeneration = frame.generation;
|
|
499
|
-
const innerGeneration = payload?.generation;
|
|
500
502
|
if (outerSession !== undefined && innerSession !== undefined && outerSession !== innerSession) return undefined;
|
|
501
|
-
if (outerGeneration !== undefined && innerGeneration !== undefined && outerGeneration !== innerGeneration)
|
|
502
|
-
return undefined;
|
|
503
503
|
const sessionClaim = outerSession !== undefined ? outerSession : innerSession;
|
|
504
|
-
const generationClaim = outerGeneration
|
|
504
|
+
const generationClaim = outerGeneration;
|
|
505
505
|
const sessionId = readSession(sessionClaim);
|
|
506
506
|
const generation = readGeneration(generationClaim);
|
|
507
507
|
if (sessionClaim !== undefined && sessionId === undefined) return undefined;
|
|
@@ -522,10 +522,24 @@ function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame
|
|
|
522
522
|
body,
|
|
523
523
|
name: readName(frame.name) ?? readName(frame.kind) ?? readName(body.type),
|
|
524
524
|
sessionId,
|
|
525
|
+
revision: typeof frame.revision === "number" ? frame.revision : undefined,
|
|
525
526
|
generation,
|
|
526
527
|
commandId,
|
|
527
528
|
turnId,
|
|
528
|
-
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 }),
|
|
529
543
|
};
|
|
530
544
|
}
|
|
531
545
|
|
|
@@ -664,7 +678,7 @@ export class SessionRouter {
|
|
|
664
678
|
// STARTUP_ATTACH_BUDGET_MS: those attachments are published with their
|
|
665
679
|
// initial replay delivered. Past that budget the pass continues in the
|
|
666
680
|
// background instead of owning the caller's deadline.
|
|
667
|
-
const initialPass = this.#serialReconcile(runEpoch,
|
|
681
|
+
const initialPass = this.#serialReconcile(runEpoch, true, true);
|
|
668
682
|
// Cancelled as soon as the race settles. Promise.race leaves the loser pending,
|
|
669
683
|
// and a pending Bun.sleep keeps the event loop alive, so a startup that settled
|
|
670
684
|
// in milliseconds still owed the rest of the budget before the process could exit.
|
|
@@ -751,6 +765,7 @@ export class SessionRouter {
|
|
|
751
765
|
const endpointMtimeMs = readEndpointMtime(result.endpointMtimeMs);
|
|
752
766
|
if (
|
|
753
767
|
sessionId !== fallback.sessionId ||
|
|
768
|
+
(this.#sessionIds !== undefined && !this.#sessionIds.has(sessionId ?? "")) ||
|
|
754
769
|
endpointGeneration === undefined ||
|
|
755
770
|
pid === undefined ||
|
|
756
771
|
endpointMtimeMs === undefined ||
|
|
@@ -825,8 +840,11 @@ export class SessionRouter {
|
|
|
825
840
|
attached.dispose();
|
|
826
841
|
this.#detachNotification(attached, "removed");
|
|
827
842
|
shutdownTasks.push((async () => await attached.client.close())());
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
}),
|
|
830
848
|
);
|
|
831
849
|
}
|
|
832
850
|
this.#adopted.clear();
|
|
@@ -891,15 +909,19 @@ export class SessionRouter {
|
|
|
891
909
|
onDispatch?: SdkDispatchHandler;
|
|
892
910
|
},
|
|
893
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);
|
|
894
916
|
const publishing = this.#sessions.get(sessionId);
|
|
895
|
-
if (!
|
|
917
|
+
if (!publishing || !matchesExpectedAuthority(publishing.capability) || !publishing.initializingPublication)
|
|
896
918
|
await this.#serialReconcile(this.#runEpoch, true, true);
|
|
897
919
|
const attached = this.#sessions.get(sessionId);
|
|
898
920
|
if (!attached || !this.#attachmentPublished(attached))
|
|
899
921
|
throw new SessionRouterError("pre_send", "SDK session attachment is unavailable: session not published.");
|
|
900
922
|
if (expectedGeneration !== undefined && expectedGeneration !== attached.generation)
|
|
901
923
|
throw new SessionRouterError("pre_send", "SDK session endpoint changed before command dispatch.");
|
|
902
|
-
if (
|
|
924
|
+
if (!matchesExpectedAuthority(attached.capability))
|
|
903
925
|
throw new SessionRouterError("pre_send", "SDK session attachment changed before command dispatch.");
|
|
904
926
|
if (attached.initializingPublication) {
|
|
905
927
|
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
@@ -947,10 +969,12 @@ export class SessionRouter {
|
|
|
947
969
|
}
|
|
948
970
|
: {}),
|
|
949
971
|
});
|
|
972
|
+
const settled = this.#sessions.get(sessionId);
|
|
950
973
|
if (
|
|
951
|
-
!
|
|
952
|
-
(
|
|
953
|
-
(
|
|
974
|
+
!settled ||
|
|
975
|
+
!this.#attachmentPublished(settled) ||
|
|
976
|
+
(expectedGeneration !== undefined && settled.generation !== expectedGeneration) ||
|
|
977
|
+
!matchesExpectedAuthority(settled.capability)
|
|
954
978
|
)
|
|
955
979
|
throw new SessionRouterError("ambiguous", "SDK session attachment changed while awaiting command response.");
|
|
956
980
|
return response;
|
|
@@ -1537,7 +1561,12 @@ export class SessionRouter {
|
|
|
1537
1561
|
existing.endpointMtimeMs === indexed.endpointMtimeMs &&
|
|
1538
1562
|
endpointIdentity !== undefined &&
|
|
1539
1563
|
sameEndpointIdentity(existing.endpointIdentity, endpointIdentity);
|
|
1540
|
-
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
|
+
) {
|
|
1541
1570
|
this.#reviveTransport(existing);
|
|
1542
1571
|
return true;
|
|
1543
1572
|
}
|
|
@@ -1699,6 +1728,7 @@ export class SessionRouter {
|
|
|
1699
1728
|
});
|
|
1700
1729
|
attached = {
|
|
1701
1730
|
initializingPublication: false,
|
|
1731
|
+
replayPending: false,
|
|
1702
1732
|
id: crypto.randomUUID(),
|
|
1703
1733
|
sessionId: indexed.sessionId,
|
|
1704
1734
|
endpoint,
|
|
@@ -1836,15 +1866,33 @@ export class SessionRouter {
|
|
|
1836
1866
|
// generation fences are unchanged; replay just runs on the attachment's
|
|
1837
1867
|
// ready tail like the reconnect path (#4527).
|
|
1838
1868
|
if (deferReplay) {
|
|
1869
|
+
const replayToken = {};
|
|
1870
|
+
attached.replayPending = true;
|
|
1871
|
+
attached.replayPendingSince = Date.now();
|
|
1872
|
+
attached.replayPendingToken = replayToken;
|
|
1839
1873
|
attached.readyTail = attached.readyTail
|
|
1840
1874
|
.catch(() => undefined)
|
|
1841
1875
|
.then(async () => {
|
|
1876
|
+
if (!this.#attachmentLive(attached)) return;
|
|
1877
|
+
await this.#deps.onReplayReady?.();
|
|
1842
1878
|
if (!this.#attachmentLive(attached)) return;
|
|
1843
1879
|
if (!(await this.#deliverRecoveredFrames(attached))) return;
|
|
1844
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
|
+
}
|
|
1845
1892
|
});
|
|
1846
1893
|
return true;
|
|
1847
1894
|
}
|
|
1895
|
+
await this.#deps.onReplayReady?.();
|
|
1848
1896
|
if (!(await this.#deliverRecoveredFrames(attached))) return false;
|
|
1849
1897
|
await this.#replayAttachment(attached, attached.cursor.seq);
|
|
1850
1898
|
return true;
|
|
@@ -1852,6 +1900,10 @@ export class SessionRouter {
|
|
|
1852
1900
|
|
|
1853
1901
|
async #reinitializeAttachment(attached: AttachedSession): Promise<void> {
|
|
1854
1902
|
const previous = attached.readyTail;
|
|
1903
|
+
const replayToken = {};
|
|
1904
|
+
attached.replayPending = true;
|
|
1905
|
+
attached.replayPendingSince = Date.now();
|
|
1906
|
+
attached.replayPendingToken = replayToken;
|
|
1855
1907
|
const current = previous
|
|
1856
1908
|
.catch(() => undefined)
|
|
1857
1909
|
.then(async () => {
|
|
@@ -1876,6 +1928,7 @@ export class SessionRouter {
|
|
|
1876
1928
|
logger.warn(`SDK notification subscription reconnect hook failed locally: ${String(error)}`);
|
|
1877
1929
|
});
|
|
1878
1930
|
await this.#deps.onAttachmentReady?.(attached.capability);
|
|
1931
|
+
await this.#deps.onReplayReady?.();
|
|
1879
1932
|
} catch {
|
|
1880
1933
|
await this.#retireAttachment(attached);
|
|
1881
1934
|
return;
|
|
@@ -1884,8 +1937,14 @@ export class SessionRouter {
|
|
|
1884
1937
|
}
|
|
1885
1938
|
if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
|
|
1886
1939
|
});
|
|
1887
|
-
attached.readyTail = current
|
|
1888
|
-
|
|
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;
|
|
1889
1948
|
}
|
|
1890
1949
|
#reviveTransport(attached: AttachedSession): void {
|
|
1891
1950
|
const connect = attached.client.connect?.bind(attached.client);
|
|
@@ -1907,7 +1966,14 @@ export class SessionRouter {
|
|
|
1907
1966
|
*/
|
|
1908
1967
|
#hasUnhealthyAttachment(): boolean {
|
|
1909
1968
|
for (const attached of this.#sessions.values()) {
|
|
1910
|
-
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;
|
|
1911
1977
|
}
|
|
1912
1978
|
return false;
|
|
1913
1979
|
}
|
|
@@ -2139,9 +2205,9 @@ export class SessionRouter {
|
|
|
2139
2205
|
const correlated = this.#correlateFrame(frame);
|
|
2140
2206
|
if (!correlated) return;
|
|
2141
2207
|
if (correlated.sessionId !== undefined && correlated.sessionId !== attached.sessionId) return;
|
|
2142
|
-
|
|
2143
|
-
this.#dispatchNotificationFrame(attached,
|
|
2144
|
-
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 =>
|
|
2145
2211
|
logger.warn(`SDK provider frame hook failed: ${String(error)}`),
|
|
2146
2212
|
);
|
|
2147
2213
|
}
|
|
@@ -2158,26 +2224,18 @@ export class SessionRouter {
|
|
|
2158
2224
|
const correlated = this.#correlateFrame(frame);
|
|
2159
2225
|
if (!correlated) return;
|
|
2160
2226
|
const sequenceClaim = readFrameSequenceClaim(frame);
|
|
2161
|
-
|
|
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)) {
|
|
2162
2232
|
this.#failBarrier(attached, "protocol error: malformed sequence coordinate");
|
|
2163
2233
|
return;
|
|
2164
2234
|
}
|
|
2165
|
-
const
|
|
2166
|
-
if (correlated.seq !== undefined && correlatedSeq === undefined) {
|
|
2167
|
-
this.#failBarrier(attached, "protocol error: malformed correlated sequence coordinate");
|
|
2168
|
-
return;
|
|
2169
|
-
}
|
|
2170
|
-
if (sequenceClaim.claimed && correlatedSeq !== undefined && correlatedSeq !== sequenceClaim.seq) {
|
|
2171
|
-
this.#failBarrier(attached, "protocol error: inconsistent sequence coordinate");
|
|
2172
|
-
return;
|
|
2173
|
-
}
|
|
2174
|
-
const seq = sequenceClaim.seq ?? correlatedSeq;
|
|
2235
|
+
const seq = ringFrame ? sequenceClaim.seq : undefined;
|
|
2175
2236
|
if (correlated.sessionId !== undefined && correlated.sessionId !== attached.sessionId) return;
|
|
2176
|
-
if (
|
|
2177
|
-
|
|
2178
|
-
const ownsSequence =
|
|
2179
|
-
correlated.generation === attached.generation &&
|
|
2180
|
-
(correlated.sessionId === undefined || correlated.sessionId === attached.sessionId);
|
|
2237
|
+
if (outerGeneration !== undefined && outerGeneration !== attached.generation) return;
|
|
2238
|
+
const ownsSequence = seq !== undefined && outerGeneration === attached.generation;
|
|
2181
2239
|
if (seq !== undefined && ownsSequence) {
|
|
2182
2240
|
if (seq <= attached.cursor.seq) return;
|
|
2183
2241
|
const held = attached.barrier.held;
|
|
@@ -2192,16 +2250,19 @@ export class SessionRouter {
|
|
|
2192
2250
|
}
|
|
2193
2251
|
const publicationId =
|
|
2194
2252
|
seq !== undefined && ownsSequence ? `${attached.sessionId}:${attached.generation}:${seq}` : undefined;
|
|
2195
|
-
|
|
2253
|
+
const coordinateFree = coordinateFreeRouterFrame(correlated);
|
|
2254
|
+
const notificationFrame: SessionRouterFrame =
|
|
2255
|
+
publicationId === undefined
|
|
2256
|
+
? coordinateFree
|
|
2257
|
+
: { ...coordinateFree, generation: outerGeneration, publicationId, seq };
|
|
2196
2258
|
try {
|
|
2197
|
-
notificationFrame = publicationId === undefined ? correlated : { ...correlated, publicationId };
|
|
2198
2259
|
this.#dispatchNotificationFrame(attached, notificationFrame);
|
|
2199
2260
|
await this.#deps.onFrame?.(attached.capability, notificationFrame);
|
|
2200
2261
|
} catch (error) {
|
|
2201
2262
|
if (!this.#attachmentLive(attached)) return;
|
|
2202
2263
|
if (seq === undefined || !ownsSequence) throw error;
|
|
2203
2264
|
this.#failDelivery(attached, seq, error);
|
|
2204
|
-
this.#deps.onFrameSettled?.(attached.capability,
|
|
2265
|
+
this.#deps.onFrameSettled?.(attached.capability, notificationFrame);
|
|
2205
2266
|
return;
|
|
2206
2267
|
}
|
|
2207
2268
|
if (!this.#attachmentLive(attached)) return;
|
|
@@ -2293,12 +2354,40 @@ export class SessionRouter {
|
|
|
2293
2354
|
if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
|
|
2294
2355
|
await this.#frameTails.get(attached.id)?.catch(() => undefined);
|
|
2295
2356
|
if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
+
}
|
|
2302
2391
|
if (replay.gap !== undefined) {
|
|
2303
2392
|
const gap = readReplayGap(replay.gap);
|
|
2304
2393
|
if (!gap) {
|
|
@@ -2316,6 +2405,10 @@ export class SessionRouter {
|
|
|
2316
2405
|
);
|
|
2317
2406
|
return;
|
|
2318
2407
|
}
|
|
2408
|
+
if (gap.toSeq > lastSeq) {
|
|
2409
|
+
this.#failBarrier(attached, "replay gap exceeded the reported high-watermark");
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2319
2412
|
const retained = events
|
|
2320
2413
|
.map(event => readSequence(event.seq))
|
|
2321
2414
|
.find(seq => seq !== undefined && seq <= gap.toSeq);
|