@gajae-code/coding-agent 0.16.4 → 0.16.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 +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +33 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +4 -2
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/session-state-lock.ts +51 -8
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +387 -48
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +2 -2
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -427,14 +427,12 @@ export class SessionSdkSessionRuntime {
|
|
|
427
427
|
* asked for. Delivery failure is ignored: a disconnected consumer must never
|
|
428
428
|
* disturb the turn producing the content.
|
|
429
429
|
*/
|
|
430
|
-
sendFrameTo(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
// A dead connection is reaped by the transport's own close handling.
|
|
437
|
-
}
|
|
430
|
+
sendFrameTo(connectionId: string, frame: SdkFrame): void {
|
|
431
|
+
try {
|
|
432
|
+
const result = this.transport.sendFrame(connectionId, frame);
|
|
433
|
+
if (result instanceof Promise) result.catch(() => undefined);
|
|
434
|
+
} catch {
|
|
435
|
+
// A dead connection is reaped by the transport's own close handling.
|
|
438
436
|
}
|
|
439
437
|
}
|
|
440
438
|
|
|
@@ -491,13 +489,13 @@ export class SessionSdkSessionRuntime {
|
|
|
491
489
|
}
|
|
492
490
|
}
|
|
493
491
|
|
|
494
|
-
async stop(): Promise<void> {
|
|
492
|
+
async stop(options: { allowLockContention?: boolean } = {}): Promise<void> {
|
|
495
493
|
this.#connectionDisposer?.();
|
|
496
494
|
this.#capabilitiesDisposer?.();
|
|
497
495
|
this.#malformedDisposer?.();
|
|
498
496
|
let hostError: unknown;
|
|
499
497
|
try {
|
|
500
|
-
await this.host.stop();
|
|
498
|
+
await this.host.stop(options);
|
|
501
499
|
} catch (error) {
|
|
502
500
|
hostError = error;
|
|
503
501
|
} finally {
|
|
@@ -2628,6 +2626,19 @@ function createControlSurface(
|
|
|
2628
2626
|
// so their recovery owner is this bounded in-place retry loop: the same
|
|
2629
2627
|
// order, retried, until both writes land or the budget is spent.
|
|
2630
2628
|
const attemptRejectionTerminalization = async (remaining: number): Promise<void> => {
|
|
2629
|
+
// AgentSession can reject cleanup after its durable terminal was published.
|
|
2630
|
+
// That rejection is diagnostic, not another outcome for this correlation.
|
|
2631
|
+
const record = reconciliation.lookup(kind, correlation) as { status?: string };
|
|
2632
|
+
if (record.status === "terminal_ok" || record.status === "failed") {
|
|
2633
|
+
logger.warn("SDK submission cleanup failed after terminal publication", {
|
|
2634
|
+
kind,
|
|
2635
|
+
commandId: correlation.commandId,
|
|
2636
|
+
turnId: correlation.turnId,
|
|
2637
|
+
error: sanitizePromptFailure(error),
|
|
2638
|
+
});
|
|
2639
|
+
retirePendingOwner?.(kind, correlation, "always");
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2631
2642
|
try {
|
|
2632
2643
|
await reconciliation.noteTransition(kind, correlation, { type: "agent_failed", error });
|
|
2633
2644
|
} catch {
|
|
@@ -4037,6 +4048,22 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4037
4048
|
correlation: InvocationCorrelation;
|
|
4038
4049
|
connectionId: string | undefined;
|
|
4039
4050
|
}>;
|
|
4051
|
+
/**
|
|
4052
|
+
* Content frames observed before this batch's correlated agent_start
|
|
4053
|
+
* reached the wire. The synchronous session subscription can see a
|
|
4054
|
+
* message_update before emitLifecycle finishes awaiting durable start
|
|
4055
|
+
* persistence; holding them here keeps the producer's start/content
|
|
4056
|
+
* boundary for every SDK consumer. Each entry carries the recipients
|
|
4057
|
+
* that owned the run when the event was produced: an owner attached
|
|
4058
|
+
* later must not receive content from before its attachment.
|
|
4059
|
+
*/
|
|
4060
|
+
startPublished: boolean;
|
|
4061
|
+
/** Set once the hold bound dropped content for this batch; gates the single warning. */
|
|
4062
|
+
heldContentDropped: boolean;
|
|
4063
|
+
heldContent: Array<{
|
|
4064
|
+
event: AgentSessionEvent;
|
|
4065
|
+
recipients: Array<{ correlation: InvocationCorrelation; connectionId: string | undefined }>;
|
|
4066
|
+
}>;
|
|
4040
4067
|
}>;
|
|
4041
4068
|
disposeGate?: () => void;
|
|
4042
4069
|
lifecycleActive: boolean;
|
|
@@ -4220,6 +4247,56 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4220
4247
|
owner.lifecycleTasks.add(task);
|
|
4221
4248
|
return task;
|
|
4222
4249
|
};
|
|
4250
|
+
/**
|
|
4251
|
+
* Publish one content frame per owning invocation, each carrying its own
|
|
4252
|
+
* correlation, so a shared run lets every submitter attribute the content to
|
|
4253
|
+
* its own prompt. Content is best-effort and bypasses the lifecycle replay
|
|
4254
|
+
* ring; the turn producing it is authoritative.
|
|
4255
|
+
*/
|
|
4256
|
+
const publishContentFrames = (
|
|
4257
|
+
current: RuntimeState,
|
|
4258
|
+
event: AgentSessionEvent,
|
|
4259
|
+
invocations: ReadonlyArray<{ correlation: InvocationCorrelation; connectionId: string | undefined }>,
|
|
4260
|
+
): void => {
|
|
4261
|
+
try {
|
|
4262
|
+
const payload = toAgentWireEventPayload(event);
|
|
4263
|
+
for (const invocation of invocations) {
|
|
4264
|
+
if (invocation.connectionId === undefined) continue;
|
|
4265
|
+
current.runtime.sendFrameTo(invocation.connectionId, {
|
|
4266
|
+
type: "event",
|
|
4267
|
+
kind: event.type,
|
|
4268
|
+
payload,
|
|
4269
|
+
...invocation.correlation,
|
|
4270
|
+
});
|
|
4271
|
+
}
|
|
4272
|
+
} catch {
|
|
4273
|
+
// Streamed content is best-effort; the turn producing it is authoritative.
|
|
4274
|
+
}
|
|
4275
|
+
};
|
|
4276
|
+
/**
|
|
4277
|
+
* Upper bound on content events held for one batch while its correlated
|
|
4278
|
+
* agent_start is still being durably recorded. A start write normally settles
|
|
4279
|
+
* in milliseconds, so this is only reached when persistence is wedged. Past
|
|
4280
|
+
* the bound the OLDEST held content is dropped (with one warning per batch):
|
|
4281
|
+
* content is never published before its start, so a wedged write degrades
|
|
4282
|
+
* to a gap in the stream, not an inverted start/content boundary, and it
|
|
4283
|
+
* can never accumulate an entire response in memory. The turn's terminal
|
|
4284
|
+
* result remains authoritative for anything a consumer did not see.
|
|
4285
|
+
*/
|
|
4286
|
+
const MAX_HELD_CONTENT_EVENTS = 256;
|
|
4287
|
+
/**
|
|
4288
|
+
* Release content that arrived before the batch's correlated agent_start
|
|
4289
|
+
* reached the wire. Called once per batch, after the start frames are
|
|
4290
|
+
* published (or after the start publication path gave up), so SDK consumers
|
|
4291
|
+
* observe start before any content, in producer order.
|
|
4292
|
+
*/
|
|
4293
|
+
const releaseHeldContent = (current: RuntimeState, batch: LifecycleBatch): void => {
|
|
4294
|
+
if (batch.startPublished) return;
|
|
4295
|
+
batch.startPublished = true;
|
|
4296
|
+
const held = batch.heldContent;
|
|
4297
|
+
batch.heldContent = [];
|
|
4298
|
+
for (const { event, recipients } of held) publishContentFrames(current, event, recipients);
|
|
4299
|
+
};
|
|
4223
4300
|
const emitLifecycle = async (
|
|
4224
4301
|
type: "agent_start" | "agent_end" | "agent_failed",
|
|
4225
4302
|
ctx: ExtensionContext,
|
|
@@ -4231,6 +4308,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4231
4308
|
terminalOutcome?: InvocationOutcome,
|
|
4232
4309
|
stopReason?: AgentEndEvent["stopReason"],
|
|
4233
4310
|
startToken?: string,
|
|
4311
|
+
startTokens?: string[],
|
|
4234
4312
|
): Promise<void> => {
|
|
4235
4313
|
const current = lifecycleOwner?.state ?? active;
|
|
4236
4314
|
if (!current) return;
|
|
@@ -4305,10 +4383,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4305
4383
|
// turn (queued-while-streaming submissions never push), so a mid-prompt
|
|
4306
4384
|
// continuation agent_start with an empty queue leaves the current owner
|
|
4307
4385
|
// untouched (review thread P1).
|
|
4386
|
+
const cohort = new Set(startTokens);
|
|
4387
|
+
if (startToken) cohort.add(startToken);
|
|
4308
4388
|
const matchingPending =
|
|
4309
|
-
|
|
4310
|
-
? current.pending.filter(entry => entry.sdkRunToken === startToken)
|
|
4311
|
-
: current.pending.slice();
|
|
4389
|
+
cohort.size > 0 ? current.pending.filter(entry => cohort.has(entry.sdkRunToken)) : current.pending.slice();
|
|
4312
4390
|
const matchingKeys = new Set(matchingPending.map(entry => entry.sdkRunToken));
|
|
4313
4391
|
const pendingSnapshot = current.pending.splice(0);
|
|
4314
4392
|
const drained = pendingSnapshot
|
|
@@ -4324,14 +4402,16 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4324
4402
|
epoch: current.lifecycleEpoch,
|
|
4325
4403
|
invocations: drained,
|
|
4326
4404
|
attachedInvocations: [],
|
|
4405
|
+
startPublished: false,
|
|
4406
|
+
heldContentDropped: false,
|
|
4407
|
+
heldContent: [],
|
|
4327
4408
|
};
|
|
4328
4409
|
current.openLifecycleBatches.push(batch);
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
lifecycleRunOwners.set(startToken, {
|
|
4410
|
+
for (const entry of drained) {
|
|
4411
|
+
lifecycleRunOwners.set(entry.sdkRunToken, {
|
|
4332
4412
|
state: current,
|
|
4333
4413
|
batch,
|
|
4334
|
-
|
|
4414
|
+
correlationKey: lifecycleCorrelationKey(entry.correlation),
|
|
4335
4415
|
});
|
|
4336
4416
|
}
|
|
4337
4417
|
adoptLifecycleBatch(drained);
|
|
@@ -4599,6 +4679,27 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4599
4679
|
observed = false;
|
|
4600
4680
|
}
|
|
4601
4681
|
}
|
|
4682
|
+
} else if (type === "agent_start" && transitions.length > 0) {
|
|
4683
|
+
for (const invocation of transitions) {
|
|
4684
|
+
try {
|
|
4685
|
+
current.runtime.emitEvent({ type, sessionId, ...invocation.correlation });
|
|
4686
|
+
} catch {
|
|
4687
|
+
observed = false;
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
// The start frames are on the wire (or definitively failed); content
|
|
4691
|
+
// that the synchronous session subscription observed meanwhile must
|
|
4692
|
+
// follow them, never precede them.
|
|
4693
|
+
const started = current.openLifecycleBatches.find(candidate =>
|
|
4694
|
+
candidate.invocations.some(entry =>
|
|
4695
|
+
transitions.some(
|
|
4696
|
+
({ correlation }) =>
|
|
4697
|
+
correlation.commandId === entry.correlation.commandId &&
|
|
4698
|
+
correlation.turnId === entry.correlation.turnId,
|
|
4699
|
+
),
|
|
4700
|
+
),
|
|
4701
|
+
);
|
|
4702
|
+
if (started) releaseHeldContent(current, started);
|
|
4602
4703
|
} else if (type === "agent_end" && transitions.length > 0) {
|
|
4603
4704
|
// An invocation-owned terminal must carry the identity it was accepted
|
|
4604
4705
|
// under and the normalized outcome. A client that submitted this turn
|
|
@@ -4621,9 +4722,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4621
4722
|
}
|
|
4622
4723
|
}
|
|
4623
4724
|
} else {
|
|
4624
|
-
//
|
|
4625
|
-
// uncorrelated frame: `agent_start` and a host-originated run (autonomous
|
|
4626
|
-
// continuation, cron, monitor) that owns no invocation.
|
|
4725
|
+
// Host-originated runs without an invocation keep an uncorrelated frame.
|
|
4627
4726
|
try {
|
|
4628
4727
|
current.runtime.emitEvent({ type, sessionId });
|
|
4629
4728
|
} catch {
|
|
@@ -4704,6 +4803,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4704
4803
|
undefined,
|
|
4705
4804
|
undefined,
|
|
4706
4805
|
typeof event.sdkRunToken === "string" ? event.sdkRunToken : undefined,
|
|
4806
|
+
event.sdkRunTokens,
|
|
4707
4807
|
),
|
|
4708
4808
|
owner,
|
|
4709
4809
|
);
|
|
@@ -4880,7 +4980,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4880
4980
|
"tool_execution_update",
|
|
4881
4981
|
"tool_execution_end",
|
|
4882
4982
|
]);
|
|
4883
|
-
const streamTurnEvent = (event:
|
|
4983
|
+
const streamTurnEvent = (event: AgentSessionEvent | null | undefined, ctx: ExtensionContext): void => {
|
|
4884
4984
|
const current = lifecycleStateForContext(ctx, "agent_start");
|
|
4885
4985
|
const activeInvocation = current?.activeInvocation;
|
|
4886
4986
|
if (!current) return;
|
|
@@ -4899,42 +4999,37 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4899
4999
|
? (current.attachedInvocations ?? [])
|
|
4900
5000
|
: [];
|
|
4901
5001
|
if (invocations.length === 0) return;
|
|
4902
|
-
//
|
|
4903
|
-
// throw out of them: a frame the wire producer does not recognize is dropped.
|
|
5002
|
+
// Content bypasses the lifecycle replay ring and must never interrupt its producer.
|
|
4904
5003
|
if (!event || typeof event.type !== "string" || !STREAMED_TURN_EVENT_TYPES.has(event.type)) return;
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
5004
|
+
// emitLifecycle("agent_start") awaits durable persistence before it
|
|
5005
|
+
// publishes the correlated start frame, while this subscription is
|
|
5006
|
+
// synchronous. Hold content until that start is on the wire so no consumer
|
|
5007
|
+
// sees turn content before the turn it belongs to.
|
|
5008
|
+
if (batch && !batch.startPublished) {
|
|
5009
|
+
batch.heldContent.push({ event: event as AgentSessionEvent, recipients: invocations });
|
|
5010
|
+
if (batch.heldContent.length > MAX_HELD_CONTENT_EVENTS) {
|
|
5011
|
+
if (!batch.heldContentDropped) {
|
|
5012
|
+
batch.heldContentDropped = true;
|
|
5013
|
+
logger.warn(
|
|
5014
|
+
"sdk: agent_start persistence still pending; dropping oldest held turn content to bound memory",
|
|
5015
|
+
{
|
|
5016
|
+
epoch: batch.epoch,
|
|
5017
|
+
bound: MAX_HELD_CONTENT_EVENTS,
|
|
5018
|
+
invocations: batch.invocations.length,
|
|
5019
|
+
},
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
5022
|
+
batch.heldContent.splice(0, batch.heldContent.length - MAX_HELD_CONTENT_EVENTS);
|
|
4917
5023
|
}
|
|
4918
|
-
|
|
4919
|
-
// Streamed content is best-effort; the turn producing it is authoritative.
|
|
5024
|
+
return;
|
|
4920
5025
|
}
|
|
5026
|
+
publishContentFrames(current, event as AgentSessionEvent, invocations);
|
|
4921
5027
|
};
|
|
4922
|
-
api.on("tool_execution_start", async (
|
|
5028
|
+
api.on("tool_execution_start", async (_event, ctx) => {
|
|
4923
5029
|
renewAttributableProgress("tool_execution_start", ctx);
|
|
4924
|
-
streamTurnEvent(event, ctx);
|
|
4925
5030
|
});
|
|
4926
|
-
api.on("
|
|
4927
|
-
streamTurnEvent(event, ctx);
|
|
4928
|
-
});
|
|
4929
|
-
api.on("tool_execution_end", async (event, ctx) => {
|
|
5031
|
+
api.on("tool_execution_end", async (_event, ctx) => {
|
|
4930
5032
|
renewAttributableProgress("tool_execution_end", ctx);
|
|
4931
|
-
streamTurnEvent(event, ctx);
|
|
4932
|
-
});
|
|
4933
|
-
api.on("message_update", async (event, ctx) => {
|
|
4934
|
-
streamTurnEvent(event, ctx);
|
|
4935
|
-
});
|
|
4936
|
-
api.on("message_end", async (event, ctx) => {
|
|
4937
|
-
streamTurnEvent(event, ctx);
|
|
4938
5033
|
});
|
|
4939
5034
|
const errorCode = (error: unknown): string | undefined =>
|
|
4940
5035
|
typeof error === "object" &&
|
|
@@ -5003,6 +5098,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
5003
5098
|
correlation: InvocationCorrelation;
|
|
5004
5099
|
connectionId: string | undefined;
|
|
5005
5100
|
}>;
|
|
5101
|
+
startPublished: boolean;
|
|
5102
|
+
heldContentDropped: boolean;
|
|
5103
|
+
heldContent: Array<{
|
|
5104
|
+
event: AgentSessionEvent;
|
|
5105
|
+
recipients: Array<{ correlation: InvocationCorrelation; connectionId: string | undefined }>;
|
|
5106
|
+
}>;
|
|
5006
5107
|
}> = [];
|
|
5007
5108
|
const configRevision = { current: 0 };
|
|
5008
5109
|
let acceptingGateResolutions = true;
|
|
@@ -5598,9 +5699,15 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
5598
5699
|
});
|
|
5599
5700
|
}
|
|
5600
5701
|
});
|
|
5702
|
+
// Extension message_update hooks are queued while final/tool hooks are not.
|
|
5703
|
+
// Use only the synchronous session boundary so those paths cannot reorder or duplicate content.
|
|
5704
|
+
const disposeStream = ctx.onSessionEvent?.(event => {
|
|
5705
|
+
if (STREAMED_TURN_EVENT_TYPES.has(event.type)) streamTurnEvent(event, ctx);
|
|
5706
|
+
});
|
|
5601
5707
|
const disposeGate = (): void => {
|
|
5602
5708
|
disposeWorkflowGate?.();
|
|
5603
5709
|
disposeFold?.();
|
|
5710
|
+
disposeStream?.();
|
|
5604
5711
|
};
|
|
5605
5712
|
let publishedEndpointUrl: string | undefined;
|
|
5606
5713
|
let brokerRegistered = false;
|
|
@@ -5811,7 +5918,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
5811
5918
|
retiredLifecycleOwnerTimers.set(current, timer);
|
|
5812
5919
|
} else current.deadlineManager.clearAll();
|
|
5813
5920
|
current.disposeGate?.();
|
|
5814
|
-
await current.runtime.stop();
|
|
5921
|
+
await current.runtime.stop({ allowLockContention: cancelSkillRecovery });
|
|
5815
5922
|
} catch (error) {
|
|
5816
5923
|
// Keep the immutable owner available for a retry when transport teardown
|
|
5817
5924
|
// fails after quiescing. Clearing `active` before stop prevents a second
|
|
@@ -200,6 +200,7 @@ export interface SessionLifecycleSessionResult {
|
|
|
200
200
|
readonly sessionId: string;
|
|
201
201
|
readonly cwd?: string;
|
|
202
202
|
readonly endpointGeneration?: number;
|
|
203
|
+
readonly endpointIncarnation?: string;
|
|
203
204
|
readonly reused?: boolean;
|
|
204
205
|
readonly note?: string;
|
|
205
206
|
}
|
|
@@ -511,6 +512,7 @@ function sessionResult(value: unknown, expectedSessionId?: string): SessionLifec
|
|
|
511
512
|
sessionId: string;
|
|
512
513
|
cwd?: string;
|
|
513
514
|
endpointGeneration?: number;
|
|
515
|
+
endpointIncarnation?: string;
|
|
514
516
|
reused?: boolean;
|
|
515
517
|
note?: string;
|
|
516
518
|
} = { sessionId };
|
|
@@ -518,6 +520,8 @@ function sessionResult(value: unknown, expectedSessionId?: string): SessionLifec
|
|
|
518
520
|
const endpointGeneration = record.endpointGeneration;
|
|
519
521
|
if (typeof endpointGeneration === "number" && Number.isSafeInteger(endpointGeneration) && endpointGeneration > 0)
|
|
520
522
|
result.endpointGeneration = endpointGeneration;
|
|
523
|
+
if (typeof record.endpointIncarnation === "string" && /^[a-f0-9]{64}$/.test(record.endpointIncarnation))
|
|
524
|
+
result.endpointIncarnation = record.endpointIncarnation;
|
|
521
525
|
if (typeof record.reused === "boolean") result.reused = record.reused;
|
|
522
526
|
if (typeof record.note === "string") result.note = record.note;
|
|
523
527
|
return result;
|
|
@@ -2940,6 +2940,17 @@
|
|
|
2940
2940
|
"testIds": "not_applicable"
|
|
2941
2941
|
}
|
|
2942
2942
|
},
|
|
2943
|
+
{
|
|
2944
|
+
"sourceId": "agent_session:awaitCoordinatorRuntimeStatePersistenceForTests",
|
|
2945
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
2946
|
+
"sourceKind": "agent_session",
|
|
2947
|
+
"decision": "exclude",
|
|
2948
|
+
"rationale": "test-only coordinator persistence drain barrier, not a user-facing SDK control seam",
|
|
2949
|
+
"exclusionMetadata": {
|
|
2950
|
+
"adapterMappings": "not_applicable",
|
|
2951
|
+
"testIds": "not_applicable"
|
|
2952
|
+
}
|
|
2953
|
+
},
|
|
2943
2954
|
{
|
|
2944
2955
|
"sourceId": "agent_session:queueCoordinatorRuntimeStatePersistForTests",
|
|
2945
2956
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
@@ -2,6 +2,7 @@ import * as crypto from "node:crypto";
|
|
|
2
2
|
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
|
+
import { matchesIndexedEndpointFile } from "../broker/endpoint-authority";
|
|
5
6
|
import {
|
|
6
7
|
canonicalSessionCwd,
|
|
7
8
|
SessionIndex as DefaultSessionIndex,
|
|
@@ -11,7 +12,13 @@ import {
|
|
|
11
12
|
type SessionIndex,
|
|
12
13
|
} from "../broker/session-index";
|
|
13
14
|
import { cancellableSleep, lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
SdkClient,
|
|
17
|
+
SdkClientError,
|
|
18
|
+
type SdkDispatchContext,
|
|
19
|
+
type SdkDispatchHandler,
|
|
20
|
+
SdkPreparedDispatchError,
|
|
21
|
+
} from "../client/client";
|
|
15
22
|
import {
|
|
16
23
|
endpointDirectory,
|
|
17
24
|
readSdkBrokerDiscovery,
|
|
@@ -27,6 +34,7 @@ import {
|
|
|
27
34
|
import { ACP_SESSION_RECONNECT, SESSION_REQUEST_TIMEOUT_MS } from "../session-reconnect";
|
|
28
35
|
|
|
29
36
|
export interface SessionEndpointIdentity {
|
|
37
|
+
readonly dev: bigint;
|
|
30
38
|
readonly mtimeMs: number;
|
|
31
39
|
readonly mtimeNs: bigint;
|
|
32
40
|
readonly ctimeNs: bigint;
|
|
@@ -56,6 +64,7 @@ export function sessionAttachmentAuthorityId(input: {
|
|
|
56
64
|
.digest("hex");
|
|
57
65
|
const endpointIdentity = input.endpointIdentity
|
|
58
66
|
? {
|
|
67
|
+
dev: input.endpointIdentity.dev.toString(),
|
|
59
68
|
mtimeMs: input.endpointIdentity.mtimeMs,
|
|
60
69
|
mtimeNs: input.endpointIdentity.mtimeNs.toString(),
|
|
61
70
|
ctimeNs: input.endpointIdentity.ctimeNs.toString(),
|
|
@@ -86,7 +95,13 @@ export interface SessionAttachment {
|
|
|
86
95
|
readonly connectionId?: string;
|
|
87
96
|
readonly generation: number;
|
|
88
97
|
isCurrent(): boolean;
|
|
89
|
-
send(
|
|
98
|
+
send(
|
|
99
|
+
frame: Record<string, unknown>,
|
|
100
|
+
options?: {
|
|
101
|
+
beforeDispatch?: () => void;
|
|
102
|
+
dispatchFence?: (dispatch: () => Promise<void>) => Promise<void>;
|
|
103
|
+
},
|
|
104
|
+
): unknown;
|
|
90
105
|
/**
|
|
91
106
|
* Idempotent provider-lease heartbeat send that skips the pre-send authority
|
|
92
107
|
* reconcile (#4689).
|
|
@@ -179,6 +194,7 @@ export interface SessionRouterClient {
|
|
|
179
194
|
frame: Record<string, unknown>,
|
|
180
195
|
options?: {
|
|
181
196
|
timeoutMs?: number;
|
|
197
|
+
connectedOnly?: boolean;
|
|
182
198
|
/** Synchronous pre-send observer; a throw aborts the dispatch before the wire. */
|
|
183
199
|
beforeDispatch?: (context: SdkDispatchContext) => void;
|
|
184
200
|
/** Synchronous post-send boundary observer for transport-close-aware consumers. */
|
|
@@ -456,6 +472,7 @@ async function lstatEndpoint(file: string): Promise<SessionEndpointIdentity | un
|
|
|
456
472
|
)
|
|
457
473
|
return undefined;
|
|
458
474
|
return {
|
|
475
|
+
dev: identity.dev,
|
|
459
476
|
mtimeMs: stat.mtimeMs,
|
|
460
477
|
mtimeNs: identity.mtimeNs,
|
|
461
478
|
ctimeNs: identity.ctimeNs,
|
|
@@ -470,6 +487,7 @@ function warningAffectsSession(warning: string, sessionId: string): boolean {
|
|
|
470
487
|
|
|
471
488
|
function sameEndpointIdentity(expected: SessionEndpointIdentity, current: SessionEndpointIdentity): boolean {
|
|
472
489
|
return (
|
|
490
|
+
expected.dev === current.dev &&
|
|
473
491
|
expected.mtimeMs === current.mtimeMs &&
|
|
474
492
|
expected.mtimeNs === current.mtimeNs &&
|
|
475
493
|
expected.ctimeNs === current.ctimeNs &&
|
|
@@ -909,6 +927,7 @@ export class SessionRouter {
|
|
|
909
927
|
timeoutMs?: number;
|
|
910
928
|
beforeDispatch?: (context: SdkDispatchContext) => void;
|
|
911
929
|
onDispatch?: SdkDispatchHandler;
|
|
930
|
+
dispatchFence?: (dispatch: () => Promise<Record<string, unknown>>) => Promise<Record<string, unknown>>;
|
|
912
931
|
},
|
|
913
932
|
): Promise<Record<string, unknown>> {
|
|
914
933
|
const matchesExpectedAuthority = (attachment: SessionAttachment): boolean =>
|
|
@@ -946,31 +965,34 @@ export class SessionRouter {
|
|
|
946
965
|
// token: the wire frame alone carries credentials, and the observer
|
|
947
966
|
// context is a deep-frozen, token-redacted copy (#4640 review).
|
|
948
967
|
const wireFrame = this.#prepareFrame(attached, frame);
|
|
949
|
-
const { beforeDispatch, onDispatch, ...requestOptions } = options ?? {};
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
:
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
968
|
+
const { beforeDispatch, onDispatch, dispatchFence, ...requestOptions } = options ?? {};
|
|
969
|
+
try {
|
|
970
|
+
await attached.client.connect?.();
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (error instanceof SdkClientError) throw new SdkPreparedDispatchError(error);
|
|
973
|
+
throw error;
|
|
974
|
+
}
|
|
975
|
+
if (!this.#attachmentPublished(attached))
|
|
976
|
+
throw new SessionRouterError("pre_send", "SDK session attachment changed during transport preparation.");
|
|
977
|
+
const dispatch = () =>
|
|
978
|
+
attached.client.request(wireFrame, {
|
|
979
|
+
...requestOptions,
|
|
980
|
+
connectedOnly: true,
|
|
981
|
+
timeoutMs: requestOptions.timeoutMs ?? SESSION_REQUEST_TIMEOUT_MS,
|
|
982
|
+
...(beforeDispatch
|
|
983
|
+
? {
|
|
984
|
+
beforeDispatch: (context: SdkDispatchContext) =>
|
|
985
|
+
beforeDispatch({ ...context, frame: redactDispatchFrame(context.frame) }),
|
|
986
|
+
}
|
|
987
|
+
: {}),
|
|
988
|
+
...(onDispatch
|
|
989
|
+
? {
|
|
990
|
+
onDispatch: (context: SdkDispatchContext) =>
|
|
991
|
+
onDispatch({ ...context, frame: redactDispatchFrame(context.frame) }),
|
|
992
|
+
}
|
|
993
|
+
: {}),
|
|
994
|
+
});
|
|
995
|
+
const response = await (dispatchFence ? dispatchFence(dispatch) : dispatch());
|
|
974
996
|
const settled = this.#sessions.get(sessionId);
|
|
975
997
|
if (
|
|
976
998
|
!settled ||
|
|
@@ -1141,7 +1163,12 @@ export class SessionRouter {
|
|
|
1141
1163
|
}
|
|
1142
1164
|
|
|
1143
1165
|
/** Lists saved sessions through Router-owned Broker discovery without exposing credentials or mutation authority. */
|
|
1144
|
-
async listBrokerSessions(
|
|
1166
|
+
async listBrokerSessions(
|
|
1167
|
+
input: Record<string, unknown>,
|
|
1168
|
+
idempotencyKey: string,
|
|
1169
|
+
options?: { beforeDispatch?: (context: SdkDispatchContext) => void },
|
|
1170
|
+
dispatchFence?: (dispatch: () => Promise<Record<string, unknown>>) => Promise<Record<string, unknown>>,
|
|
1171
|
+
): Promise<Record<string, unknown>> {
|
|
1145
1172
|
const operation = "session.list";
|
|
1146
1173
|
const discovery = await readSdkBrokerDiscovery(this.#agentDir);
|
|
1147
1174
|
if (!discovery) throw new SessionRouterError("pre_send", "SDK broker discovery is unavailable.");
|
|
@@ -1155,10 +1182,18 @@ export class SessionRouter {
|
|
|
1155
1182
|
}
|
|
1156
1183
|
try {
|
|
1157
1184
|
const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1185
|
+
try {
|
|
1186
|
+
await client.connect?.();
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
if (error instanceof SdkClientError) throw new SdkPreparedDispatchError(error);
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
1191
|
+
const dispatch = () =>
|
|
1192
|
+
client.request(
|
|
1193
|
+
{ type: "broker_request", operation, input, idempotencyKey },
|
|
1194
|
+
{ ...(timeoutMs === undefined ? {} : { timeoutMs }), ...options, connectedOnly: true },
|
|
1195
|
+
);
|
|
1196
|
+
return await (dispatchFence ? dispatchFence(dispatch) : dispatch());
|
|
1162
1197
|
} finally {
|
|
1163
1198
|
await client.close().catch(error => {
|
|
1164
1199
|
logger.warn(`SDK Broker session.list transport cleanup failed (${String(error)}).`);
|
|
@@ -1360,7 +1395,7 @@ export class SessionRouter {
|
|
|
1360
1395
|
// cannot keep the old attachment authorized.
|
|
1361
1396
|
const identityBefore = await lstatEndpoint(attached.endpoint.path);
|
|
1362
1397
|
if (!identityBefore || !sameEndpointIdentity(attached.endpointIdentity, identityBefore)) return false;
|
|
1363
|
-
if (identityBefore
|
|
1398
|
+
if (!matchesIndexedEndpointFile(identityBefore, indexed)) return false;
|
|
1364
1399
|
let raw: Record<string, unknown>;
|
|
1365
1400
|
try {
|
|
1366
1401
|
const parsed = JSON.parse(await Bun.file(attached.endpoint.path).text());
|
|
@@ -1384,7 +1419,7 @@ export class SessionRouter {
|
|
|
1384
1419
|
const identityAfter = await lstatEndpoint(attached.endpoint.path);
|
|
1385
1420
|
return (
|
|
1386
1421
|
identityAfter !== undefined &&
|
|
1387
|
-
identityAfter
|
|
1422
|
+
matchesIndexedEndpointFile(identityAfter, indexed) &&
|
|
1388
1423
|
sameEndpointIdentity(attached.endpointIdentity, identityAfter)
|
|
1389
1424
|
);
|
|
1390
1425
|
}
|
|
@@ -1416,7 +1451,7 @@ export class SessionRouter {
|
|
|
1416
1451
|
const endpointIdentity = await lstatEndpoint(endpointPath);
|
|
1417
1452
|
const endpoint = await readSdkSessionEndpoint(cwd, indexed.sessionId, scope);
|
|
1418
1453
|
if (!endpoint || endpoint.stale || endpoint.pid !== indexed.pid) return null;
|
|
1419
|
-
if (!endpointIdentity || endpointIdentity
|
|
1454
|
+
if (!endpointIdentity || !matchesIndexedEndpointFile(endpointIdentity, indexed)) return null;
|
|
1420
1455
|
// Identity is proven INSIDE this authority read (#4730 review): sampling it
|
|
1421
1456
|
// afterwards would let an identical rename between the read and the sample
|
|
1422
1457
|
// install the replacement's inode as the trusted baseline.
|
|
@@ -1441,7 +1476,7 @@ export class SessionRouter {
|
|
|
1441
1476
|
const endpointIdentityAfterRead = await lstatEndpoint(endpoint.path);
|
|
1442
1477
|
if (
|
|
1443
1478
|
!endpointIdentityAfterRead ||
|
|
1444
|
-
endpointIdentityAfterRead
|
|
1479
|
+
!matchesIndexedEndpointFile(endpointIdentityAfterRead, indexed) ||
|
|
1445
1480
|
!sameEndpointIdentity(endpointIdentity, endpointIdentityAfterRead)
|
|
1446
1481
|
)
|
|
1447
1482
|
return null;
|
|
@@ -1641,7 +1676,13 @@ export class SessionRouter {
|
|
|
1641
1676
|
return attached?.client.connectionId;
|
|
1642
1677
|
},
|
|
1643
1678
|
isCurrent: () => attached !== undefined && this.#attachmentPublished(attached),
|
|
1644
|
-
send: async (
|
|
1679
|
+
send: async (
|
|
1680
|
+
frame: Record<string, unknown>,
|
|
1681
|
+
options?: {
|
|
1682
|
+
beforeDispatch?: () => void;
|
|
1683
|
+
dispatchFence?: (dispatch: () => Promise<void>) => Promise<void>;
|
|
1684
|
+
},
|
|
1685
|
+
) => {
|
|
1645
1686
|
if (!attached || !this.#attachmentPublished(attached))
|
|
1646
1687
|
throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
|
|
1647
1688
|
if (attached.initializingPublication) {
|
|
@@ -1659,7 +1700,17 @@ export class SessionRouter {
|
|
|
1659
1700
|
} else await this.#serialReconcile(runEpoch, true, true);
|
|
1660
1701
|
if (!attached || !this.#attachmentPublished(attached))
|
|
1661
1702
|
throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
|
|
1662
|
-
attached
|
|
1703
|
+
const dispatchAttachment = attached;
|
|
1704
|
+
const dispatch = async () => {
|
|
1705
|
+
options?.beforeDispatch?.();
|
|
1706
|
+
try {
|
|
1707
|
+
dispatchAttachment.client.send(this.#prepareFrame(dispatchAttachment, frame));
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
if (error instanceof SdkClientError) throw new SdkPreparedDispatchError(error);
|
|
1710
|
+
throw error;
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
await (options?.dispatchFence ? options.dispatchFence(dispatch) : dispatch());
|
|
1663
1714
|
},
|
|
1664
1715
|
/**
|
|
1665
1716
|
* Provider-lease heartbeats skip the pre-send authority reconcile
|