@gajae-code/coding-agent 0.15.5 → 0.15.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +2 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +96 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +15 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -7
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +40 -5
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/config/autorouting-tier-map.ts +32 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +51 -9
- package/src/config/model-registry.ts +1021 -122
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/cli/session-cli.ts +12 -1
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +371 -37
- package/src/sdk/host/session-runtime.ts +364 -52
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +228 -96
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +678 -103
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +5 -4
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +772 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
} from "../broker/session-index";
|
|
11
11
|
import { cancellableSleep, lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
12
12
|
import { SdkClient, SdkClientError, type SdkDispatchContext, type SdkDispatchHandler } from "../client/client";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
endpointDirectory,
|
|
15
|
+
readSdkBrokerDiscovery,
|
|
16
|
+
readSdkSessionEndpoint,
|
|
17
|
+
type SdkSessionEndpoint,
|
|
18
|
+
} from "../client/discovery";
|
|
14
19
|
import {
|
|
15
20
|
type ActivatedPreparedSession,
|
|
16
21
|
type PreparedSessionActivationClient,
|
|
@@ -19,6 +24,14 @@ import {
|
|
|
19
24
|
} from "../session-activation";
|
|
20
25
|
import { ACP_SESSION_RECONNECT, SESSION_REQUEST_TIMEOUT_MS } from "../session-reconnect";
|
|
21
26
|
|
|
27
|
+
export interface SessionEndpointIdentity {
|
|
28
|
+
readonly mtimeMs: number;
|
|
29
|
+
readonly mtimeNs: bigint;
|
|
30
|
+
readonly ctimeNs: bigint;
|
|
31
|
+
readonly size: bigint;
|
|
32
|
+
readonly ino: bigint;
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* Exact identity of one attached SDK session endpoint. Providers persist it next to
|
|
24
37
|
* their conversation state and re-prove it before every resume, so it must be derived
|
|
@@ -32,11 +45,22 @@ export function sessionAttachmentAuthorityId(input: {
|
|
|
32
45
|
endpointMtimeMs: number | undefined;
|
|
33
46
|
url: string;
|
|
34
47
|
token: string;
|
|
48
|
+
/** Proven no-follow endpoint identity, when the caller has one. */
|
|
49
|
+
endpointIdentity?: SessionEndpointIdentity;
|
|
35
50
|
}): string {
|
|
36
51
|
const endpointAuthorityDigest = crypto
|
|
37
52
|
.createHash("sha256")
|
|
38
53
|
.update(JSON.stringify({ url: input.url, token: input.token }))
|
|
39
54
|
.digest("hex");
|
|
55
|
+
const endpointIdentity = input.endpointIdentity
|
|
56
|
+
? {
|
|
57
|
+
mtimeMs: input.endpointIdentity.mtimeMs,
|
|
58
|
+
mtimeNs: input.endpointIdentity.mtimeNs.toString(),
|
|
59
|
+
ctimeNs: input.endpointIdentity.ctimeNs.toString(),
|
|
60
|
+
size: input.endpointIdentity.size.toString(),
|
|
61
|
+
ino: input.endpointIdentity.ino.toString(),
|
|
62
|
+
}
|
|
63
|
+
: undefined;
|
|
40
64
|
return crypto
|
|
41
65
|
.createHash("sha256")
|
|
42
66
|
.update(
|
|
@@ -46,6 +70,7 @@ export function sessionAttachmentAuthorityId(input: {
|
|
|
46
70
|
pid: input.pid,
|
|
47
71
|
endpointMtimeMs: input.endpointMtimeMs,
|
|
48
72
|
endpointAuthorityDigest,
|
|
73
|
+
endpointIdentity,
|
|
49
74
|
}),
|
|
50
75
|
)
|
|
51
76
|
.digest("hex");
|
|
@@ -287,6 +312,8 @@ type AttachedSession = {
|
|
|
287
312
|
readonly generation: number;
|
|
288
313
|
readonly pid: number;
|
|
289
314
|
readonly endpointMtimeMs: number;
|
|
315
|
+
/** No-follow endpoint identity proven before this attachment became current. */
|
|
316
|
+
readonly endpointIdentity: SessionEndpointIdentity;
|
|
290
317
|
readonly runEpoch: number;
|
|
291
318
|
readonly client: SessionRouterClient;
|
|
292
319
|
readonly indexed: IndexedSession;
|
|
@@ -398,6 +425,56 @@ function readSequence(value: unknown): number | undefined {
|
|
|
398
425
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
|
|
399
426
|
}
|
|
400
427
|
|
|
428
|
+
async function lstatEndpoint(file: string): Promise<SessionEndpointIdentity | undefined> {
|
|
429
|
+
const stat = await fs.lstat(file).catch(() => undefined);
|
|
430
|
+
if (!stat || !stat.isFile()) return undefined;
|
|
431
|
+
const identity = await fs.lstat(file, { bigint: true }).catch(() => undefined);
|
|
432
|
+
if (!identity || !identity.isFile()) return undefined;
|
|
433
|
+
if (
|
|
434
|
+
stat.size !== Number(identity.size) ||
|
|
435
|
+
stat.ino !== Number(identity.ino) ||
|
|
436
|
+
Math.abs(stat.mtimeMs - Number(identity.mtimeNs) / 1_000_000) > 0.0005 ||
|
|
437
|
+
Math.abs(stat.ctimeMs - Number(identity.ctimeNs) / 1_000_000) > 0.0005
|
|
438
|
+
)
|
|
439
|
+
return undefined;
|
|
440
|
+
return {
|
|
441
|
+
mtimeMs: stat.mtimeMs,
|
|
442
|
+
mtimeNs: identity.mtimeNs,
|
|
443
|
+
ctimeNs: identity.ctimeNs,
|
|
444
|
+
size: identity.size,
|
|
445
|
+
ino: identity.ino,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function sameEndpointIdentity(expected: SessionEndpointIdentity, current: SessionEndpointIdentity): boolean {
|
|
450
|
+
return (
|
|
451
|
+
expected.mtimeMs === current.mtimeMs &&
|
|
452
|
+
expected.mtimeNs === current.mtimeNs &&
|
|
453
|
+
expected.ctimeNs === current.ctimeNs &&
|
|
454
|
+
expected.size === current.size &&
|
|
455
|
+
expected.ino === current.ino
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function readFrameSequenceClaim(frame: Record<string, unknown>): {
|
|
460
|
+
claimed: boolean;
|
|
461
|
+
valid: boolean;
|
|
462
|
+
seq?: number;
|
|
463
|
+
} {
|
|
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] };
|
|
476
|
+
}
|
|
477
|
+
|
|
401
478
|
function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame | undefined {
|
|
402
479
|
const payload =
|
|
403
480
|
frame.type === "event" && frame.payload && typeof frame.payload === "object" && !Array.isArray(frame.payload)
|
|
@@ -503,16 +580,11 @@ export class SessionRouter {
|
|
|
503
580
|
{ generation: number; frames: Array<{ seq: number; frame: Record<string, unknown> }> }
|
|
504
581
|
>();
|
|
505
582
|
readonly #reviving = new Set<string>();
|
|
506
|
-
/**
|
|
507
|
-
* Endpoint-file inode captured when each attachment was published (#4730
|
|
508
|
-
* review). Maintenance renewal compares against this so a rename-replace that
|
|
509
|
-
* preserves size, mtime and body cannot keep a superseded endpoint authorized.
|
|
510
|
-
*/
|
|
511
|
-
readonly #endpointInodes = new Map<string, bigint>();
|
|
512
583
|
readonly #notificationReceipts = new Map<string, NotificationCleanupReceipt>();
|
|
513
584
|
#stopTimer: (() => void) | undefined;
|
|
514
585
|
#reconcileTail: Promise<void> = Promise.resolve();
|
|
515
586
|
#reconcilePending: { readonly runEpoch: number; force: boolean } | undefined;
|
|
587
|
+
#attachmentTails = new Map<string, Promise<boolean>>();
|
|
516
588
|
/**
|
|
517
589
|
* Set when a live indexed session failed to attach; keeps the idle gate
|
|
518
590
|
* from parking retries until the 30s sweep (#4689 review).
|
|
@@ -524,6 +596,8 @@ export class SessionRouter {
|
|
|
524
596
|
#lastReconciledIndexSeq = -1;
|
|
525
597
|
#ready = false;
|
|
526
598
|
#started = false;
|
|
599
|
+
#startPromise: Promise<void> | undefined;
|
|
600
|
+
#stopPromise: Promise<void> | undefined;
|
|
527
601
|
#stopController = new AbortController();
|
|
528
602
|
#runEpoch = 0;
|
|
529
603
|
|
|
@@ -545,14 +619,27 @@ export class SessionRouter {
|
|
|
545
619
|
|
|
546
620
|
/** Starts reconciliation and the index watcher. */
|
|
547
621
|
async start(): Promise<void> {
|
|
622
|
+
if (this.#stopPromise) await this.#stopPromise;
|
|
623
|
+
if (this.#startPromise) return await this.#startPromise;
|
|
548
624
|
if (this.#started) return;
|
|
549
625
|
this.#started = true;
|
|
626
|
+
const startup = this.#startImpl();
|
|
627
|
+
this.#startPromise = startup;
|
|
628
|
+
try {
|
|
629
|
+
await startup;
|
|
630
|
+
} finally {
|
|
631
|
+
if (this.#startPromise === startup) this.#startPromise = undefined;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async #startImpl(): Promise<void> {
|
|
550
636
|
const runEpoch = ++this.#runEpoch;
|
|
551
637
|
if (this.#stopController.signal.aborted) {
|
|
552
638
|
this.#stopController = new AbortController();
|
|
553
639
|
this.#reconcileTail = Promise.resolve();
|
|
554
640
|
this.#reconcilePending = undefined;
|
|
555
641
|
this.#frameTails.clear();
|
|
642
|
+
this.#attachmentTails.clear();
|
|
556
643
|
// A restart must re-run the full body on the first tick: reset the
|
|
557
644
|
// idle-gate markers so stale state cannot carry across run epochs.
|
|
558
645
|
this.#lastReconcileSweepAt = 0;
|
|
@@ -701,6 +788,20 @@ export class SessionRouter {
|
|
|
701
788
|
}
|
|
702
789
|
|
|
703
790
|
async stop(): Promise<void> {
|
|
791
|
+
if (this.#stopPromise) return await this.#stopPromise;
|
|
792
|
+
const stopping = this.#stopImpl();
|
|
793
|
+
this.#stopPromise = stopping;
|
|
794
|
+
try {
|
|
795
|
+
await stopping;
|
|
796
|
+
} finally {
|
|
797
|
+
if (this.#stopPromise === stopping) this.#stopPromise = undefined;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async #stopImpl(): Promise<void> {
|
|
802
|
+
// Detach the in-flight startup promise before invalidating its epoch. A
|
|
803
|
+
// later start must not return a promise owned by the stopped epoch.
|
|
804
|
+
this.#startPromise = undefined;
|
|
704
805
|
if (this.#stopTimer) this.#stopTimer();
|
|
705
806
|
this.#stopTimer = undefined;
|
|
706
807
|
this.#started = false;
|
|
@@ -720,7 +821,7 @@ export class SessionRouter {
|
|
|
720
821
|
this.#adopted.clear();
|
|
721
822
|
// Provider notification work is detached and bounded independently; core
|
|
722
823
|
// shutdown waits only for Router reconciliation and client close.
|
|
723
|
-
const pending = Promise.allSettled([this.#reconcileTail, ...shutdownTasks]);
|
|
824
|
+
const pending = Promise.allSettled([this.#reconcileTail, ...this.#attachmentTails.values(), ...shutdownTasks]);
|
|
724
825
|
// Cancelled once the race settles, so a completed shutdown does not hold the
|
|
725
826
|
// event loop open for the remainder of the timeout.
|
|
726
827
|
const shutdownCutoff = new AbortController();
|
|
@@ -790,14 +891,15 @@ export class SessionRouter {
|
|
|
790
891
|
if (expectedAttachment !== undefined && attached.capability !== expectedAttachment)
|
|
791
892
|
throw new SessionRouterError("pre_send", "SDK session attachment changed before command dispatch.");
|
|
792
893
|
if (attached.initializingPublication) {
|
|
793
|
-
const
|
|
894
|
+
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
794
895
|
if (
|
|
795
|
-
!
|
|
796
|
-
endpoint.url !== attached.endpoint.url ||
|
|
797
|
-
endpoint.token !== attached.endpoint.token ||
|
|
798
|
-
endpoint.pid !== attached.pid
|
|
896
|
+
!proven ||
|
|
897
|
+
proven.endpoint.url !== attached.endpoint.url ||
|
|
898
|
+
proven.endpoint.token !== attached.endpoint.token ||
|
|
899
|
+
proven.endpoint.pid !== attached.pid ||
|
|
900
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
799
901
|
) {
|
|
800
|
-
await this.#retireAttachment(attached,
|
|
902
|
+
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
801
903
|
throw new SessionRouterError("pre_send", "SDK session attachment changed during publication.");
|
|
802
904
|
}
|
|
803
905
|
}
|
|
@@ -865,8 +967,15 @@ export class SessionRouter {
|
|
|
865
967
|
)
|
|
866
968
|
return undefined;
|
|
867
969
|
if (!Number.isSafeInteger(indexed.pid) || indexed.pid <= 0) return undefined;
|
|
868
|
-
const
|
|
869
|
-
if (
|
|
970
|
+
const proven = await this.#readProvenEndpoint(indexed).catch(() => null);
|
|
971
|
+
if (
|
|
972
|
+
!proven ||
|
|
973
|
+
proven.endpoint.stale === true ||
|
|
974
|
+
proven.endpoint.pid !== indexed.pid ||
|
|
975
|
+
!proven.endpoint.token ||
|
|
976
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
977
|
+
)
|
|
978
|
+
return undefined;
|
|
870
979
|
if (this.#sessions.get(sessionId) !== attached || !this.#attachmentPublished(attached)) return undefined;
|
|
871
980
|
return { sessionId, endpointGeneration: attached.generation };
|
|
872
981
|
}
|
|
@@ -945,8 +1054,16 @@ export class SessionRouter {
|
|
|
945
1054
|
"session_not_live",
|
|
946
1055
|
"Session activation requires an exact live session endpoint.",
|
|
947
1056
|
);
|
|
948
|
-
const
|
|
949
|
-
|
|
1057
|
+
const provenEndpoint = await this.#readProvenEndpoint(indexed).catch(() => null);
|
|
1058
|
+
const endpoint = provenEndpoint?.endpoint;
|
|
1059
|
+
if (
|
|
1060
|
+
!provenEndpoint ||
|
|
1061
|
+
!endpoint ||
|
|
1062
|
+
endpoint.stale === true ||
|
|
1063
|
+
!endpoint.url ||
|
|
1064
|
+
!endpoint.token ||
|
|
1065
|
+
endpoint.pid !== indexed.pid
|
|
1066
|
+
)
|
|
950
1067
|
throw new SessionActivationError(
|
|
951
1068
|
"session_not_live",
|
|
952
1069
|
"Session activation requires a readable session discovery endpoint.",
|
|
@@ -966,12 +1083,15 @@ export class SessionRouter {
|
|
|
966
1083
|
throw new SessionActivationError("activation_unavailable", "The session endpoint could not be reached.");
|
|
967
1084
|
}
|
|
968
1085
|
try {
|
|
969
|
-
const
|
|
1086
|
+
const currentProvenEndpoint = await this.#readProvenEndpoint(indexed).catch(() => null);
|
|
1087
|
+
const currentEndpoint = currentProvenEndpoint?.endpoint;
|
|
970
1088
|
if (
|
|
1089
|
+
!currentProvenEndpoint ||
|
|
971
1090
|
!currentEndpoint ||
|
|
972
1091
|
currentEndpoint.url !== endpoint.url ||
|
|
973
1092
|
currentEndpoint.token !== endpoint.token ||
|
|
974
|
-
currentEndpoint.pid !== endpoint.pid
|
|
1093
|
+
currentEndpoint.pid !== endpoint.pid ||
|
|
1094
|
+
!sameEndpointIdentity(provenEndpoint.identity, currentProvenEndpoint.identity)
|
|
975
1095
|
)
|
|
976
1096
|
throw new SessionActivationError(
|
|
977
1097
|
"session_not_live",
|
|
@@ -1200,15 +1320,9 @@ export class SessionRouter {
|
|
|
1200
1320
|
// attachment was PUBLISHED against is therefore the identity, and the
|
|
1201
1321
|
// generation is compared so a re-registration reusing the same url/token/pid
|
|
1202
1322
|
// cannot keep the old attachment authorized.
|
|
1203
|
-
const
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1206
|
-
const inoBefore = await fs
|
|
1207
|
-
.stat(attached.endpoint.path, { bigint: true })
|
|
1208
|
-
.then(value => value.ino)
|
|
1209
|
-
.catch(() => undefined);
|
|
1210
|
-
if (!statBefore || inoBefore === undefined || inoBefore !== publishedIno) return false;
|
|
1211
|
-
if (statBefore.mtimeMs !== indexed.endpointMtimeMs) return false;
|
|
1323
|
+
const identityBefore = await lstatEndpoint(attached.endpoint.path);
|
|
1324
|
+
if (!identityBefore || !sameEndpointIdentity(attached.endpointIdentity, identityBefore)) return false;
|
|
1325
|
+
if (identityBefore.mtimeMs !== indexed.endpointMtimeMs) return false;
|
|
1212
1326
|
let raw: Record<string, unknown>;
|
|
1213
1327
|
try {
|
|
1214
1328
|
const parsed = JSON.parse(await Bun.file(attached.endpoint.path).text());
|
|
@@ -1229,26 +1343,23 @@ export class SessionRouter {
|
|
|
1229
1343
|
// comes from the indexed row this attachment was published against.
|
|
1230
1344
|
if (indexed.endpointGeneration !== attached.generation) return false;
|
|
1231
1345
|
if (raw.endpointGeneration !== undefined && raw.endpointGeneration !== attached.generation) return false;
|
|
1232
|
-
const
|
|
1233
|
-
const inoAfter = await fs
|
|
1234
|
-
.stat(attached.endpoint.path, { bigint: true })
|
|
1235
|
-
.then(value => value.ino)
|
|
1236
|
-
.catch(() => undefined);
|
|
1346
|
+
const identityAfter = await lstatEndpoint(attached.endpoint.path);
|
|
1237
1347
|
return (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
inoAfter === publishedIno
|
|
1348
|
+
identityAfter !== undefined &&
|
|
1349
|
+
identityAfter.mtimeMs === indexed.endpointMtimeMs &&
|
|
1350
|
+
sameEndpointIdentity(attached.endpointIdentity, identityAfter)
|
|
1242
1351
|
);
|
|
1243
1352
|
}
|
|
1244
1353
|
|
|
1245
1354
|
/**
|
|
1246
|
-
* Returns the endpoint together with the
|
|
1355
|
+
* Returns the endpoint together with the full identity this read PROVED (#4730
|
|
1247
1356
|
* review). The identity travels with the value instead of living in a side
|
|
1248
1357
|
* map, so every consumer decision compares the same proof and the two cannot
|
|
1249
1358
|
* drift apart.
|
|
1250
1359
|
*/
|
|
1251
|
-
async #readProvenEndpoint(
|
|
1360
|
+
async #readProvenEndpoint(
|
|
1361
|
+
indexed: IndexedSession,
|
|
1362
|
+
): Promise<{ endpoint: SdkSessionEndpoint; identity: SessionEndpointIdentity } | null> {
|
|
1252
1363
|
if (!isSessionAuthorityEligible(indexed)) return null;
|
|
1253
1364
|
const repo = path.resolve(indexed.locator.repo);
|
|
1254
1365
|
const defaultStateRoot = path.join(repo, ".gjc", "state");
|
|
@@ -1267,18 +1378,15 @@ export class SessionRouter {
|
|
|
1267
1378
|
? "chat"
|
|
1268
1379
|
: undefined;
|
|
1269
1380
|
if (!scope || indexed.endpointMtimeMs === undefined || !Number.isFinite(indexed.endpointMtimeMs)) return null;
|
|
1381
|
+
const endpointPath = path.join(endpointDirectory(repo, scope), `${indexed.sessionId}.json`);
|
|
1382
|
+
const endpointIdentity = await lstatEndpoint(endpointPath);
|
|
1270
1383
|
const endpoint = await readSdkSessionEndpoint(repo, indexed.sessionId, scope);
|
|
1271
1384
|
if (!endpoint || endpoint.stale || endpoint.pid !== indexed.pid) return null;
|
|
1272
|
-
|
|
1273
|
-
const endpointIno = await fs
|
|
1274
|
-
.stat(endpoint.path, { bigint: true })
|
|
1275
|
-
.then(value => value.ino)
|
|
1276
|
-
.catch(() => undefined);
|
|
1277
|
-
if (!endpointStat || endpointIno === undefined || endpointStat.mtimeMs !== indexed.endpointMtimeMs) return null;
|
|
1385
|
+
if (!endpointIdentity || endpointIdentity.mtimeMs !== indexed.endpointMtimeMs) return null;
|
|
1278
1386
|
// Identity is proven INSIDE this authority read (#4730 review): sampling it
|
|
1279
1387
|
// afterwards would let an identical rename between the read and the sample
|
|
1280
1388
|
// install the replacement's inode as the trusted baseline.
|
|
1281
|
-
const
|
|
1389
|
+
const provenIdentity = endpointIdentity;
|
|
1282
1390
|
let raw: Record<string, unknown>;
|
|
1283
1391
|
try {
|
|
1284
1392
|
const parsed = JSON.parse(await Bun.file(endpoint.path).text());
|
|
@@ -1296,16 +1404,11 @@ export class SessionRouter {
|
|
|
1296
1404
|
)
|
|
1297
1405
|
return null;
|
|
1298
1406
|
// Fail CLOSED on stat error and on any identity change across the body read.
|
|
1299
|
-
const
|
|
1300
|
-
const inoAfterRead = await fs
|
|
1301
|
-
.stat(endpoint.path, { bigint: true })
|
|
1302
|
-
.then(value => value.ino)
|
|
1303
|
-
.catch(() => undefined);
|
|
1407
|
+
const endpointIdentityAfterRead = await lstatEndpoint(endpoint.path);
|
|
1304
1408
|
if (
|
|
1305
|
-
!
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
inoAfterRead !== provenIno
|
|
1409
|
+
!endpointIdentityAfterRead ||
|
|
1410
|
+
endpointIdentityAfterRead.mtimeMs !== indexed.endpointMtimeMs ||
|
|
1411
|
+
!sameEndpointIdentity(endpointIdentity, endpointIdentityAfterRead)
|
|
1309
1412
|
)
|
|
1310
1413
|
return null;
|
|
1311
1414
|
await this.#index.refresh();
|
|
@@ -1313,7 +1416,7 @@ export class SessionRouter {
|
|
|
1313
1416
|
if (listing.warnings.length > 0) return null;
|
|
1314
1417
|
const current = listing.sessions.find(session => session.sessionId === indexed.sessionId);
|
|
1315
1418
|
if (!current || !sameIndexedAuthority(indexed, current)) return null;
|
|
1316
|
-
return { endpoint,
|
|
1419
|
+
return { endpoint, identity: provenIdentity };
|
|
1317
1420
|
}
|
|
1318
1421
|
|
|
1319
1422
|
/** Endpoint-only view for callers that do not make an authority decision. */
|
|
@@ -1375,6 +1478,28 @@ export class SessionRouter {
|
|
|
1375
1478
|
skipReplay = false,
|
|
1376
1479
|
deferPublication = false,
|
|
1377
1480
|
deferReplay = false,
|
|
1481
|
+
): Promise<boolean> {
|
|
1482
|
+
const previous = this.#attachmentTails.get(indexed.sessionId) ?? Promise.resolve(true);
|
|
1483
|
+
const task = previous
|
|
1484
|
+
.catch(() => false)
|
|
1485
|
+
.then(() =>
|
|
1486
|
+
this.#attachUnserialized(indexed, runEpoch, resolvedEndpoint, skipReplay, deferPublication, deferReplay),
|
|
1487
|
+
);
|
|
1488
|
+
this.#attachmentTails.set(indexed.sessionId, task);
|
|
1489
|
+
try {
|
|
1490
|
+
return await task;
|
|
1491
|
+
} finally {
|
|
1492
|
+
if (this.#attachmentTails.get(indexed.sessionId) === task) this.#attachmentTails.delete(indexed.sessionId);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async #attachUnserialized(
|
|
1497
|
+
indexed: IndexedSession,
|
|
1498
|
+
runEpoch: number,
|
|
1499
|
+
resolvedEndpoint?: SdkSessionEndpoint,
|
|
1500
|
+
skipReplay = false,
|
|
1501
|
+
deferPublication = false,
|
|
1502
|
+
deferReplay = false,
|
|
1378
1503
|
): Promise<boolean> {
|
|
1379
1504
|
const retirementVersion = this.#retirementVersions.get(indexed.sessionId) ?? 0;
|
|
1380
1505
|
const retirement = this.#retirements.get(indexed.sessionId);
|
|
@@ -1386,18 +1511,14 @@ export class SessionRouter {
|
|
|
1386
1511
|
// authority read of its own, so it samples once here.
|
|
1387
1512
|
const proven = resolvedEndpoint === undefined ? await this.#readProvenEndpoint(indexed) : null;
|
|
1388
1513
|
const endpoint = resolvedEndpoint ?? proven?.endpoint ?? null;
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
provenIno = await fs
|
|
1392
|
-
.stat(resolvedEndpoint.path, { bigint: true })
|
|
1393
|
-
.then(value => value.ino)
|
|
1394
|
-
.catch(() => undefined);
|
|
1514
|
+
const endpointIdentity =
|
|
1515
|
+
resolvedEndpoint === undefined ? proven?.identity : await lstatEndpoint(resolvedEndpoint.path);
|
|
1395
1516
|
const retirementAfterValidation = this.#retirements.get(indexed.sessionId);
|
|
1396
1517
|
if (retirementAfterValidation) await retirementAfterValidation;
|
|
1397
1518
|
if ((this.#retirementVersions.get(indexed.sessionId) ?? 0) !== retirementVersion)
|
|
1398
|
-
return await this.#
|
|
1519
|
+
return await this.#attachUnserialized(indexed, runEpoch, undefined, skipReplay, deferPublication, deferReplay);
|
|
1399
1520
|
if (!this.#running(runEpoch)) return false;
|
|
1400
|
-
if (!endpoint) return false;
|
|
1521
|
+
if (!endpoint || !endpointIdentity) return false;
|
|
1401
1522
|
const existing = this.#sessions.get(indexed.sessionId);
|
|
1402
1523
|
const resumable =
|
|
1403
1524
|
existing !== undefined &&
|
|
@@ -1405,7 +1526,9 @@ export class SessionRouter {
|
|
|
1405
1526
|
existing.endpoint.token === endpoint.token &&
|
|
1406
1527
|
existing.generation === indexed.endpointGeneration &&
|
|
1407
1528
|
existing.pid === indexed.pid &&
|
|
1408
|
-
existing.endpointMtimeMs === indexed.endpointMtimeMs
|
|
1529
|
+
existing.endpointMtimeMs === indexed.endpointMtimeMs &&
|
|
1530
|
+
endpointIdentity !== undefined &&
|
|
1531
|
+
sameEndpointIdentity(existing.endpointIdentity, endpointIdentity);
|
|
1409
1532
|
if (existing && resumable && !existing.barrier.failed) {
|
|
1410
1533
|
this.#reviveTransport(existing);
|
|
1411
1534
|
return true;
|
|
@@ -1434,7 +1557,9 @@ export class SessionRouter {
|
|
|
1434
1557
|
(existing.endpoint.url !== endpoint.url ||
|
|
1435
1558
|
existing.endpoint.token !== endpoint.token ||
|
|
1436
1559
|
existing.pid !== indexed.pid ||
|
|
1437
|
-
existing.endpointMtimeMs !== indexed.endpointMtimeMs
|
|
1560
|
+
existing.endpointMtimeMs !== indexed.endpointMtimeMs ||
|
|
1561
|
+
endpointIdentity === undefined ||
|
|
1562
|
+
!sameEndpointIdentity(existing.endpointIdentity, endpointIdentity))
|
|
1438
1563
|
? "replaced_same_generation"
|
|
1439
1564
|
: "replaced",
|
|
1440
1565
|
),
|
|
@@ -1455,7 +1580,7 @@ export class SessionRouter {
|
|
|
1455
1580
|
await client.close().catch(() => undefined);
|
|
1456
1581
|
const currentRetirement = this.#retirements.get(indexed.sessionId);
|
|
1457
1582
|
if (currentRetirement) await currentRetirement;
|
|
1458
|
-
return await this.#
|
|
1583
|
+
return await this.#attachUnserialized(indexed, runEpoch, undefined, skipReplay, deferPublication, deferReplay);
|
|
1459
1584
|
}
|
|
1460
1585
|
let attached: AttachedSession | undefined;
|
|
1461
1586
|
const barrier: ReplayBarrier = { held: undefined, detached: false, failed: false };
|
|
@@ -1469,6 +1594,7 @@ export class SessionRouter {
|
|
|
1469
1594
|
endpointMtimeMs: indexed.endpointMtimeMs,
|
|
1470
1595
|
url: endpoint.url,
|
|
1471
1596
|
token: endpoint.token,
|
|
1597
|
+
endpointIdentity,
|
|
1472
1598
|
}),
|
|
1473
1599
|
sessionId: indexed.sessionId,
|
|
1474
1600
|
generation: indexed.endpointGeneration,
|
|
@@ -1480,14 +1606,15 @@ export class SessionRouter {
|
|
|
1480
1606
|
if (!attached || !this.#attachmentPublished(attached))
|
|
1481
1607
|
throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
|
|
1482
1608
|
if (attached.initializingPublication) {
|
|
1483
|
-
const
|
|
1609
|
+
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
1484
1610
|
if (
|
|
1485
|
-
!
|
|
1486
|
-
endpoint.url !== attached.endpoint.url ||
|
|
1487
|
-
endpoint.token !== attached.endpoint.token ||
|
|
1488
|
-
endpoint.pid !== attached.pid
|
|
1611
|
+
!proven ||
|
|
1612
|
+
proven.endpoint.url !== attached.endpoint.url ||
|
|
1613
|
+
proven.endpoint.token !== attached.endpoint.token ||
|
|
1614
|
+
proven.endpoint.pid !== attached.pid ||
|
|
1615
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
1489
1616
|
) {
|
|
1490
|
-
await this.#retireAttachment(attached,
|
|
1617
|
+
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
1491
1618
|
throw new SessionRouterError("pre_send", "SDK session attachment changed during publication.");
|
|
1492
1619
|
}
|
|
1493
1620
|
} else await this.#serialReconcile(runEpoch, true, true);
|
|
@@ -1569,6 +1696,7 @@ export class SessionRouter {
|
|
|
1569
1696
|
endpoint,
|
|
1570
1697
|
pid: indexed.pid,
|
|
1571
1698
|
endpointMtimeMs: indexed.endpointMtimeMs,
|
|
1699
|
+
endpointIdentity,
|
|
1572
1700
|
generation: indexed.endpointGeneration,
|
|
1573
1701
|
runEpoch,
|
|
1574
1702
|
client,
|
|
@@ -1592,10 +1720,6 @@ export class SessionRouter {
|
|
|
1592
1720
|
new SessionRouterError("pre_send", "SDK session publication was detached before completion."),
|
|
1593
1721
|
);
|
|
1594
1722
|
barrier.held = undefined;
|
|
1595
|
-
// Every teardown path (retirement, replacement, stop, failed
|
|
1596
|
-
// publication) routes through dispose(), so the published-inode entry
|
|
1597
|
-
// is dropped here rather than at one call site (#4730 review).
|
|
1598
|
-
if (attached) this.#endpointInodes.delete(attached.id);
|
|
1599
1723
|
},
|
|
1600
1724
|
};
|
|
1601
1725
|
// Re-check at publication (#4730 review): the endpoint must STILL be the
|
|
@@ -1609,14 +1733,9 @@ export class SessionRouter {
|
|
|
1609
1733
|
await client.close().catch(() => undefined);
|
|
1610
1734
|
return false;
|
|
1611
1735
|
};
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
.stat(endpoint.path, { bigint: true })
|
|
1615
|
-
.then(value => value.ino)
|
|
1616
|
-
.catch(() => undefined);
|
|
1617
|
-
if (publishIno === undefined || publishIno !== provenIno) return await rollback();
|
|
1736
|
+
const publishIdentity = await lstatEndpoint(endpoint.path);
|
|
1737
|
+
if (!publishIdentity || !sameEndpointIdentity(endpointIdentity, publishIdentity)) return await rollback();
|
|
1618
1738
|
this.#sessions.set(indexed.sessionId, attached);
|
|
1619
|
-
this.#endpointInodes.set(attached.id, provenIno);
|
|
1620
1739
|
if (deferPublication)
|
|
1621
1740
|
this.#adopted.set(indexed.sessionId, {
|
|
1622
1741
|
generation: indexed.endpointGeneration,
|
|
@@ -1656,19 +1775,17 @@ export class SessionRouter {
|
|
|
1656
1775
|
if (!this.#attachmentLive(attached)) return false;
|
|
1657
1776
|
const proven = await this.#readProvenEndpoint(attached.indexed).catch(() => null);
|
|
1658
1777
|
if (!this.#attachmentLive(attached)) return false;
|
|
1659
|
-
// The commit point compares the PROVEN
|
|
1778
|
+
// The commit point compares the PROVEN endpoint identity alongside url/token/pid
|
|
1660
1779
|
// (#4730 review): an identical-byte rename during the pre-publication
|
|
1661
1780
|
// hooks matches every other field, so identity is what rejects it. The
|
|
1662
1781
|
// value compared here is the one the authority read proved, not a later
|
|
1663
1782
|
// observation, so the proof and its consumer cannot drift apart.
|
|
1664
|
-
const publishedIno = this.#endpointInodes.get(attached.id);
|
|
1665
1783
|
if (
|
|
1666
1784
|
!proven ||
|
|
1667
1785
|
proven.endpoint.url !== attached.endpoint.url ||
|
|
1668
1786
|
proven.endpoint.token !== attached.endpoint.token ||
|
|
1669
1787
|
proven.endpoint.pid !== attached.pid ||
|
|
1670
|
-
|
|
1671
|
-
proven.ino !== publishedIno
|
|
1788
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
1672
1789
|
) {
|
|
1673
1790
|
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
1674
1791
|
return false;
|
|
@@ -1731,14 +1848,15 @@ export class SessionRouter {
|
|
|
1731
1848
|
.catch(() => undefined)
|
|
1732
1849
|
.then(async () => {
|
|
1733
1850
|
if (!this.#attachmentLive(attached)) return;
|
|
1734
|
-
const
|
|
1851
|
+
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
1735
1852
|
if (
|
|
1736
|
-
!
|
|
1737
|
-
endpoint.url !== attached.endpoint.url ||
|
|
1738
|
-
endpoint.token !== attached.endpoint.token ||
|
|
1739
|
-
endpoint.pid !== attached.pid
|
|
1853
|
+
!proven ||
|
|
1854
|
+
proven.endpoint.url !== attached.endpoint.url ||
|
|
1855
|
+
proven.endpoint.token !== attached.endpoint.token ||
|
|
1856
|
+
proven.endpoint.pid !== attached.pid ||
|
|
1857
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
1740
1858
|
) {
|
|
1741
|
-
await this.#retireAttachment(attached,
|
|
1859
|
+
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
1742
1860
|
return;
|
|
1743
1861
|
}
|
|
1744
1862
|
attached.initializingPublication = true;
|
|
@@ -2031,7 +2149,21 @@ export class SessionRouter {
|
|
|
2031
2149
|
}
|
|
2032
2150
|
const correlated = this.#correlateFrame(frame);
|
|
2033
2151
|
if (!correlated) return;
|
|
2034
|
-
const
|
|
2152
|
+
const sequenceClaim = readFrameSequenceClaim(frame);
|
|
2153
|
+
if (!sequenceClaim.valid) {
|
|
2154
|
+
this.#failBarrier(attached, "protocol error: malformed sequence coordinate");
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
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;
|
|
2035
2167
|
if (correlated.sessionId !== undefined && correlated.sessionId !== attached.sessionId) return;
|
|
2036
2168
|
if (correlated.generation !== undefined && correlated.generation !== attached.generation) return;
|
|
2037
2169
|
if (seq !== undefined && correlated.generation === undefined) return;
|