@code-yeongyu/senpi 2026.11.2-1 → 2026.11.2-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bundle/chunks/{chunk-AMCDEC5A.js → chunk-B2LRAXQU.js} +2 -2
  3. package/dist/bundle/chunks/{chunk-D22YF34L.js → chunk-CQ3G57HE.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-ZUVGFXCS.js → chunk-FVYWSBX3.js} +3 -3
  5. package/dist/bundle/chunks/{chunk-665UPAKG.js → chunk-H3BTMIH5.js} +6 -6
  6. package/dist/bundle/chunks/{chunk-KEJWNCLN.js → chunk-HUSQY3ID.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-IBV4OVXQ.js → chunk-N6OHYIJR.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-XZR4J6HM.js → chunk-PP52OWBB.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-XVQISKHL.js → chunk-QV2IEA3B.js} +4 -4
  10. package/dist/bundle/chunks/{chunk-2V6BR24J.js → chunk-VEWJSPOZ.js} +1 -1
  11. package/dist/bundle/chunks/{cli-main-REFQSP6S.js → cli-main-6ZI4QO2Z.js} +1 -1
  12. package/dist/bundle/chunks/{host-command-Y3ZT6YLD.js → host-command-O3ALG2O3.js} +1 -1
  13. package/dist/bundle/chunks/{host-lifecycle-VW73K4WA.js → host-lifecycle-SGTVODHB.js} +1 -1
  14. package/dist/bundle/chunks/host-lifecycle.js +5 -5
  15. package/dist/bundle/chunks/{host-session-command-YDEUZBTX.js → host-session-command-4WRR5JSB.js} +1 -1
  16. package/dist/bundle/chunks/{interactive-mode-UZXPNJ4V.js → interactive-mode-TF7R35GK.js} +1 -1
  17. package/dist/bundle/chunks/{main-O5G75MYS.js → main-XLUYSOXP.js} +1 -1
  18. package/dist/bundle/chunks/{multi-session-host-AZKJYA2X.js → multi-session-host-CADYC2BS.js} +4 -3
  19. package/dist/bundle/chunks/{package-manager-cli-2YDUGJNR.js → package-manager-cli-I62KMIWK.js} +1 -1
  20. package/dist/bundle/chunks/{rpc-mode-KZD5HZNI.js → rpc-mode-T5GVMUBQ.js} +1 -1
  21. package/dist/bundle/chunks/{schedule-command-7LSJAINZ.js → schedule-command-YXGRN5CM.js} +1 -1
  22. package/dist/bundle/chunks/{session-control-endpoint-BG2KQ6MJ.js → session-control-endpoint-PREVS5UO.js} +1 -1
  23. package/dist/bundle/chunks/session-worker.js +63 -62
  24. package/dist/bundle/cli.js +2 -2
  25. package/dist/bundle/host-supervisor.js +5 -5
  26. package/dist/bundle/index.js +1 -1
  27. package/dist/bundle/rpc-entry.js +1 -1
  28. package/dist/bundle/runtime-manifest.json +1 -1
  29. package/dist/core/session-recovery-identity.d.ts +12 -1
  30. package/dist/modes/rpc/compact-call-fold.d.ts +3 -1
  31. package/dist/modes/rpc/compact-call-fold.js +17 -6
  32. package/dist/modes/rpc/compact-call-identity.d.ts +13 -0
  33. package/dist/modes/rpc/compact-call-identity.js +30 -0
  34. package/dist/modes/rpc/compact-call-model.d.ts +14 -0
  35. package/dist/modes/rpc/compact-call-model.js +4 -0
  36. package/dist/modes/rpc/compact-call-table.d.ts +9 -0
  37. package/dist/modes/rpc/compact-call-table.js +20 -0
  38. package/dist/modes/rpc/compact-catalog-settled.d.ts +5 -0
  39. package/dist/modes/rpc/compact-catalog-settled.js +9 -2
  40. package/dist/modes/rpc/compact-lane-current.d.ts +2 -0
  41. package/dist/modes/rpc/compact-lane-current.js +4 -0
  42. package/dist/modes/rpc/compact-message-identity.d.ts +74 -0
  43. package/dist/modes/rpc/compact-message-identity.js +101 -0
  44. package/dist/modes/rpc/compact-session-calls.d.ts +5 -4
  45. package/dist/modes/rpc/compact-session-calls.js +13 -8
  46. package/dist/modes/rpc/compact-session-state.d.ts +2 -0
  47. package/dist/modes/rpc/compact-session-state.js +4 -0
  48. package/dist/modes/rpc/compact-settled-messages.d.ts +12 -4
  49. package/dist/modes/rpc/compact-settled-messages.js +18 -10
  50. package/dist/modes/rpc/connection-handler.js +1 -1
  51. package/dist/modes/rpc/connection-question-bridge.d.ts +2 -1
  52. package/dist/modes/rpc/connection-question-bridge.js +3 -1
  53. package/dist/modes/rpc/host-lifecycle-proxy.d.ts +9 -0
  54. package/dist/modes/rpc/host-lifecycle-proxy.js +29 -7
  55. package/dist/modes/rpc/rpc-question-types.d.ts +2 -0
  56. package/dist/modes/rpc/session-acceptance-compact.d.ts +17 -7
  57. package/dist/modes/rpc/session-acceptance-compact.js +20 -8
  58. package/dist/modes/rpc/session-acceptance-lane.d.ts +5 -0
  59. package/dist/modes/rpc/session-acceptance-lane.js +12 -11
  60. package/dist/modes/rpc/session-event-writer-destinations.d.ts +2 -1
  61. package/dist/modes/rpc/session-event-writer-destinations.js +24 -9
  62. package/dist/utils/version-check.js +9 -3
  63. package/docs/engine-socket/fixtures/admission-routes/i-live-identity.json +716 -0
  64. package/docs/engine-socket/fixtures/admission-routes/index.json +3 -1
  65. package/docs/engine-socket/fixtures/admission-routes/j-live-call-identity.json +1020 -0
  66. package/docs/rpc.md +66 -0
  67. package/package.json +6 -6
@@ -110,24 +110,23 @@ export class CompactSessionCalls {
110
110
  raw: call.raw?.kind === "owned" ? call.raw : raw,
111
111
  });
112
112
  }
113
+ /** Each per-id execution record returns the call it changed (RULING-123), undefined when it changed none. */
113
114
  executionStart(step, start) {
114
115
  const turnId = step.scope.turnId;
115
116
  if (turnId === null)
116
- return;
117
+ return undefined;
117
118
  const started = {
118
119
  startSequence: step.sequence,
119
120
  startedAtMs: Date.now(),
120
121
  resolvedName: start.toolName,
121
122
  };
122
123
  const announced = this.table.oldestById(start.toolCallId, turnId, awaitingStart);
123
- if (announced) {
124
- this.update(step, announced, {
124
+ if (announced)
125
+ return this.update(step, announced, {
125
126
  ...started,
126
127
  phase: "executing",
127
128
  input: { ...announced.input, started: start.args },
128
129
  });
129
- return;
130
- }
131
130
  const existing = this.table.byTurnId(turnId, start.toolCallId);
132
131
  if (existing)
133
132
  this.table.counters.reused++;
@@ -142,6 +141,7 @@ export class CompactSessionCalls {
142
141
  });
143
142
  if (existing)
144
143
  this.table.repoint(turnId, start.toolCallId, created.runtimeItemKey);
144
+ return created;
145
145
  }
146
146
  executionUpdate(step, toolCallId, args, partialResult) {
147
147
  const call = this.resolve(step, toolCallId, executing);
@@ -154,6 +154,7 @@ export class CompactSessionCalls {
154
154
  if (call.input.prepared === null)
155
155
  this.update(step, progressed, { input: { ...call.input, prepared: args } });
156
156
  this.sequence = step.sequence;
157
+ return progressed;
157
158
  }
158
159
  hook(step, toolCallId, record) {
159
160
  const call = this.resolve(step, toolCallId, stillOpen);
@@ -161,8 +162,9 @@ export class CompactSessionCalls {
161
162
  return this.ignore("unknown");
162
163
  if (terminal(call))
163
164
  return this.ignore("late");
164
- this.table.replaceSlot(call, "hook", record);
165
+ const hooked = this.table.replaceSlot(call, "hook", record);
165
166
  this.sequence = step.sequence;
167
+ return hooked;
166
168
  }
167
169
  /** An end closes the oldest call still open under its id (senpi#4278); with none open it may replace an abandoned one. */
168
170
  executionEnd(step, toolCallId, result, isError) {
@@ -172,7 +174,7 @@ export class CompactSessionCalls {
172
174
  if (terminal(call) && call.outcome?.kind !== "abandoned")
173
175
  return this.ignore("late");
174
176
  const ended = this.table.replaceSlot(call, "result", result);
175
- this.update(step, ended, {
177
+ return this.update(step, ended, {
176
178
  phase: TERMINAL_PHASE,
177
179
  outcome: { kind: "result", isError, source: "execution_end" },
178
180
  join: orphaned(this.table, call) || call.join === "carrierless" ? "carrierless" : "pending",
@@ -226,6 +228,7 @@ export class CompactSessionCalls {
226
228
  }
227
229
  ignore(kind) {
228
230
  this.table.counters[kind]++;
231
+ return undefined;
229
232
  }
230
233
  create(step, fields) {
231
234
  if (this.mintedAt !== step.sequence) {
@@ -245,8 +248,10 @@ export class CompactSessionCalls {
245
248
  return call;
246
249
  }
247
250
  update(step, call, patch) {
248
- this.table.put({ ...call, ...patch }, creationOf(call));
251
+ const next = { ...call, ...patch };
252
+ this.table.put(next, creationOf(call));
249
253
  this.sequence = step.sequence;
254
+ return next;
250
255
  }
251
256
  }
252
257
  //# sourceMappingURL=compact-session-calls.js.map
@@ -24,6 +24,8 @@ export declare class CompactSessionState extends CompactActiveMessages {
24
24
  accept(t: CompactTransition, sequence?: number, turnId?: string | null): number;
25
25
  /** A call record at its lane step; `changed` is false when the record is ignored and takes no sequence (Q9). */
26
26
  acceptCall(record: object, sequence: number, turnId: string | null): CallFold;
27
+ /** The outstanding call whose block this is: the call a `toolcall_*` update streams (RULING-123), by the table's index. */
28
+ callOwning(messageId: string, contentIndex: number): import("./compact-call-model.ts").OutstandingCall | undefined;
27
29
  applyCold(t: CompactColdOperation): Promise<import("./compact-cold-operations.ts").CompactColdOutcome>;
28
30
  /** One ordered cold unit per flow: its operations run back to back after acceptance, each with its outcome. */
29
31
  applyColdFlow(flow: CompactColdFlow): Promise<readonly import("./compact-cold-operations.ts").CompactColdOutcome[]>;
@@ -182,6 +182,10 @@ export class CompactSessionState extends CompactActiveMessages {
182
182
  this.sequence = sequence;
183
183
  return fold;
184
184
  }
185
+ /** The outstanding call whose block this is: the call a `toolcall_*` update streams (RULING-123), by the table's index. */
186
+ callOwning(messageId, contentIndex) {
187
+ return this.calls.table.byOwner(messageId, contentIndex);
188
+ }
185
189
  applyCold(t) {
186
190
  return this.cold.run(t, this.sequence);
187
191
  }
@@ -1,6 +1,7 @@
1
1
  import type { RecoveryCarrier } from "../../core/session-recovery-identity.ts";
2
2
  import type { TranscriptTransition } from "../../core/session-transcript-journal.ts";
3
3
  import type { CallProjection } from "./compact-call-model.ts";
4
+ import type { SettledEnd } from "./compact-message-identity.ts";
4
5
  import { type FrozenIndex } from "./compact-content-index.ts";
5
6
  import { CompactContentStore, type CompactContentStoreOptions, type LeafId } from "./compact-content-store.ts";
6
7
  import type { CompactValues, ValueRef } from "./compact-values.ts";
@@ -69,17 +70,24 @@ export declare class CompactSettledMessages {
69
70
  reader(root: SettledRoot): SettledReader;
70
71
  valid(carrier: RecoveryCarrier | undefined): carrier is RecoveryCarrier;
71
72
  /**
72
- * Returns whether this end joined the message to its durable entry: the only end that settles persistence. `call`:
73
+ * Returns the accepted end (undefined when refused): whether it joined the message to its durable entry, the only end
74
+ * that settles persistence, and the message's settled ids (RULING-120): its `messageId` and the end's accepted sequence as
75
+ * `terminalSequence`, which a pending or failed terminal pages as `message-<terminalSequence>` and a joined message
76
+ * pages only through its `join-<n>` member's `messageId` (an already-joined end writes no terminal row). `call`:
73
77
  * the projection of the outstanding call this toolResult settles, stored with the terminal (Q2). The terminal is
74
78
  * serialized from `record`, so a record over the protocol depth limit is refused typed before anything changes
75
79
  * (RULING-59 C); the lane runs that check, and counts the refusal, before it calls this.
76
80
  */
77
- end(record: object, carrier: RecoveryCarrier | undefined, sequence: number, turnId: string | null, call?: CallProjection): boolean;
81
+ end(record: object, carrier: RecoveryCarrier | undefined, sequence: number, turnId: string | null, call?: CallProjection): SettledEnd | undefined;
78
82
  private reject;
79
- fail(carrier: RecoveryCarrier | undefined, errorMessage: string): void;
83
+ /** Marks the message's pending terminal failed; returns the `messageId` that row carries, undefined for none. */
84
+ fail(carrier: RecoveryCarrier | undefined, errorMessage: string): {
85
+ readonly messageId: string;
86
+ } | undefined;
87
+ /** Joins an owned append to its message; returns that message's id, the `messageId` of the join member it made. */
80
88
  append(transition: Extract<TranscriptTransition, {
81
89
  kind: "append";
82
- }>): void;
90
+ }>): string | undefined;
83
91
  private join;
84
92
  /** Ended and joined: its rows stay paged; its end marker folds into its join, its entry stays claimed. */
85
93
  private settle;
@@ -34,7 +34,10 @@ export class CompactSettledMessages {
34
34
  return carrier !== undefined && carrier.epoch === this.epoch;
35
35
  }
36
36
  /**
37
- * Returns whether this end joined the message to its durable entry: the only end that settles persistence. `call`:
37
+ * Returns the accepted end (undefined when refused): whether it joined the message to its durable entry, the only end
38
+ * that settles persistence, and the message's settled ids (RULING-120): its `messageId` and the end's accepted sequence as
39
+ * `terminalSequence`, which a pending or failed terminal pages as `message-<terminalSequence>` and a joined message
40
+ * pages only through its `join-<n>` member's `messageId` (an already-joined end writes no terminal row). `call`:
38
41
  * the projection of the outstanding call this toolResult settles, stored with the terminal (Q2). The terminal is
39
42
  * serialized from `record`, so a record over the protocol depth limit is refused typed before anything changes
40
43
  * (RULING-59 C); the lane runs that check, and counts the refusal, before it calls this.
@@ -43,7 +46,8 @@ export class CompactSettledMessages {
43
46
  assertProtocolDepth(record);
44
47
  if (!this.valid(carrier))
45
48
  return this.reject("unknown_message");
46
- const id = carrier.messageId;
49
+ const identity = { messageId: carrier.messageId, terminalSequence: sequence };
50
+ const id = identity.messageId;
47
51
  if (this.keys.has(`end:${id}`) || this.keys.has(`settled:${id}`))
48
52
  return this.reject("duplicate_end");
49
53
  const owned = this.keys.get(`owner:${id}`);
@@ -58,7 +62,7 @@ export class CompactSettledMessages {
58
62
  }
59
63
  if (this.keys.has(`owner:${id}`)) {
60
64
  this.settle(id);
61
- return joined;
65
+ return { joined, identity };
62
66
  }
63
67
  // The call projection is owned first: a refusal there leaves no payload leaf and no end marker behind.
64
68
  const projection = call ? this.rows.values.own(call) : null;
@@ -80,7 +84,7 @@ export class CompactSettledMessages {
80
84
  kind: "terminal",
81
85
  messageId: id,
82
86
  turnId,
83
- terminalSequence: sequence,
87
+ terminalSequence: identity.terminalSequence,
84
88
  role,
85
89
  runtimeItemKey: call?.runtimeItemKey ?? null,
86
90
  entryId: null,
@@ -95,26 +99,30 @@ export class CompactSettledMessages {
95
99
  this.store.adjust(projection.owner, -1, 0);
96
100
  if (!["user", "assistant", "toolResult", "custom"].includes(role))
97
101
  this.diagnose("unsupported_role");
98
- return false;
102
+ return { joined: false, identity };
99
103
  }
100
104
  reject(reason) {
101
105
  this.diagnose(reason);
102
- return false;
106
+ return undefined;
103
107
  }
108
+ /** Marks the message's pending terminal failed; returns the `messageId` that row carries, undefined for none. */
104
109
  fail(carrier, errorMessage) {
105
110
  if (!this.valid(carrier) || !this.keys.has(`terminal:${carrier.messageId}`))
106
- return this.diagnose("unknown_message");
111
+ return this.reject("unknown_message");
107
112
  this.rows.patch(`terminal:${carrier.messageId}`, { status: "failed", error: { errorMessage } });
113
+ return { messageId: carrier.messageId };
108
114
  }
115
+ /** Joins an owned append to its message; returns that message's id, the `messageId` of the join member it made. */
109
116
  append(transition) {
110
117
  const id = transition.owningMessageId;
111
118
  if (id === undefined)
112
- return;
119
+ return undefined;
113
120
  if (transition.recoveryEpoch !== this.epoch)
114
- return this.diagnose("retired_owner");
121
+ return this.reject("retired_owner");
115
122
  if (this.keys.has(`owner:${id}`) || this.keys.has(`settled:${id}`) || this.keys.has(`entry:${transition.id}`))
116
- return this.diagnose("conflicting_owner");
123
+ return this.reject("conflicting_owner");
117
124
  this.join(id, transition.id);
125
+ return id;
118
126
  }
119
127
  join(messageId, entryId) {
120
128
  const owner = { kind: "owner", messageId, entryId, ordinal: this.joins++ };
@@ -339,7 +339,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
339
339
  const pendingExtensionRequests = new SessionExtensionUiRequests(uiRequestChanged);
340
340
  const grantRequests = new Map();
341
341
  sessionDialogRequests.set(session, pendingExtensionRequests);
342
- const questions = new ConnectionQuestionBridge(output, uiRequestChanged);
342
+ const questions = new ConnectionQuestionBridge(output, uiRequestChanged, () => options.clientInfo?.connectionId());
343
343
  let secretHandshake = false;
344
344
  const secretOwners = new Map();
345
345
  const secretCapable = (connection) => connectionRoles.has(connection, INTERACTIVE_USER_CAPABILITY) &&
@@ -8,7 +8,8 @@ export declare class ConnectionQuestionBridge {
8
8
  private readonly resolved;
9
9
  private readonly output;
10
10
  private readonly changed;
11
- constructor(output: (record: object) => void, changed?: (change?: UiRequestChange) => void);
11
+ private readonly raiser;
12
+ constructor(output: (record: object) => void, changed?: (change?: UiRequestChange) => void, raiser?: () => string | undefined);
12
13
  pendingQuestions(): RpcQuestionUiRequest[];
13
14
  ask(request: QuestionRequest, opts?: Options): Promise<QuestionResponse>;
14
15
  respond(response: RpcExtensionUIResponse, beforeAnswer?: () => void): boolean | "question_incomplete" | "question_already_resolved";
@@ -1,11 +1,12 @@
1
1
  import { settledQuestionStatus, unansweredQuestionIds } from "./extension-ui-response.js";
2
2
  export const sessionQuestionBridges = new WeakMap();
3
3
  export class ConnectionQuestionBridge {
4
- constructor(output, changed) {
4
+ constructor(output, changed, raiser = () => undefined) {
5
5
  this.pending = new Map();
6
6
  this.resolved = new Set();
7
7
  this.output = output;
8
8
  this.changed = changed;
9
+ this.raiser = raiser;
9
10
  }
10
11
  pendingQuestions() {
11
12
  return [...this.pending.values()].map(({ frame }) => ({
@@ -25,6 +26,7 @@ export class ConnectionQuestionBridge {
25
26
  toolCallId: request.requestId,
26
27
  waitForAnswer: request.waitForAnswer,
27
28
  questions: request.questions,
29
+ raisedBy: this.raiser() ?? "agent",
28
30
  timeout,
29
31
  askedAtMs,
30
32
  deadlineAtMs: askedAtMs + timeout,
@@ -42,6 +42,15 @@ export interface PrepareSocketPathHooks {
42
42
  /** Replaces the listener probe, so a test can pin each verdict. */
43
43
  readonly listenerState?: (socketPath: string) => Promise<ListenerState>;
44
44
  }
45
+ /**
46
+ * Frees `socketPath` for this process's bind, removing only a dead socket entry and refusing everything else
47
+ * (#5314). No portable fd-based check exists for a socket entry: `open()` fails on one (ENXIO on Linux,
48
+ * EOPNOTSUPP on macOS), `O_PATH` is Linux-only, and there is no unlink-by-fd. So every read is `lstat`, which
49
+ * never follows a link, and `unlink(2)` removes the name, never a link's target. The entry is re-stamped before
50
+ * the second reachability probe (which must not be steered through a swapped-in link) and again as the last step
51
+ * before the unlink; reachability is re-checked after the decision because a socket bound earlier can start
52
+ * listening without changing identity.
53
+ */
45
54
  export declare function prepareSocketPath(socketPath: string, hooks?: PrepareSocketPathHooks): Promise<void>;
46
55
  export declare function listen(server: Server, socketPath: string, secret?: Uint8Array): Promise<void>;
47
56
  export declare function closeServer(server: Server): Promise<void>;
@@ -3,7 +3,7 @@
3
3
  * adoption of the public name, and the readiness probes of the private hop. Split out of
4
4
  * `host-lifecycle.ts`, which keeps the supervisor's orchestration (senpi#2566).
5
5
  */
6
- import { chmod, mkdir, stat, unlink } from "node:fs/promises";
6
+ import { chmod, lstat, mkdir, unlink } from "node:fs/promises";
7
7
  import { createConnection, createServer } from "node:net";
8
8
  import { dirname } from "node:path";
9
9
  import { proxyWithAdmission } from "./host-admission-proxy.js";
@@ -166,7 +166,7 @@ async function canConnect(socketPath, secret, internalKey, probeMs) {
166
166
  }
167
167
  async function stampOf(socketPath) {
168
168
  try {
169
- const entry = await stat(socketPath, { bigint: true });
169
+ const entry = await lstat(socketPath, { bigint: true });
170
170
  return { dev: entry.dev, ino: entry.ino, ctimeNs: entry.ctimeNs, socket: entry.isSocket() };
171
171
  }
172
172
  catch (cause) {
@@ -176,6 +176,15 @@ async function stampOf(socketPath) {
176
176
  }
177
177
  }
178
178
  const sameStamp = (a, b) => a.dev === b.dev && a.ino === b.ino && a.ctimeNs === b.ctimeNs;
179
+ /**
180
+ * Frees `socketPath` for this process's bind, removing only a dead socket entry and refusing everything else
181
+ * (#5314). No portable fd-based check exists for a socket entry: `open()` fails on one (ENXIO on Linux,
182
+ * EOPNOTSUPP on macOS), `O_PATH` is Linux-only, and there is no unlink-by-fd. So every read is `lstat`, which
183
+ * never follows a link, and `unlink(2)` removes the name, never a link's target. The entry is re-stamped before
184
+ * the second reachability probe (which must not be steered through a swapped-in link) and again as the last step
185
+ * before the unlink; reachability is re-checked after the decision because a socket bound earlier can start
186
+ * listening without changing identity.
187
+ */
179
188
  export async function prepareSocketPath(socketPath, hooks = {}) {
180
189
  if (process.platform === "win32" || socketPath.startsWith("\0"))
181
190
  return;
@@ -192,18 +201,31 @@ export async function prepareSocketPath(socketPath, hooks = {}) {
192
201
  // Linux refuses a connect to a regular file too; never remove what is not a socket.
193
202
  if (!probed.socket)
194
203
  throw inUse("the entry is not a socket");
195
- const state = await (hooks.listenerState ?? socketEntryListenerState)(socketPath);
204
+ const probe = hooks.listenerState ?? socketEntryListenerState;
205
+ const state = await probe(socketPath);
196
206
  if (state.verdict === "gone")
197
207
  return;
198
208
  if (state.verdict === "live")
199
209
  throw inUse(state.evidence);
200
210
  await hooks.afterStaleProbe?.();
201
211
  // Something may have replaced the entry since the probe; unlink only the entry probed dead.
202
- const current = await stampOf(socketPath);
203
- if (current === undefined)
212
+ const unchanged = async () => {
213
+ const current = await stampOf(socketPath);
214
+ if (current === undefined)
215
+ return false;
216
+ if (!sameStamp(probed, current))
217
+ throw inUse("the entry changed after it was probed");
218
+ return true;
219
+ };
220
+ if (!(await unchanged()))
221
+ return;
222
+ const again = await probe(socketPath);
223
+ if (again.verdict === "gone")
224
+ return;
225
+ if (again.verdict === "live")
226
+ throw inUse(again.evidence);
227
+ if (!(await unchanged()))
204
228
  return;
205
- if (!sameStamp(probed, current))
206
- throw inUse("the entry changed after it was probed");
207
229
  try {
208
230
  await unlink(socketPath);
209
231
  }
@@ -23,6 +23,8 @@ export type RpcQuestionUiRequest = {
23
23
  toolCallId: string;
24
24
  waitForAnswer: boolean;
25
25
  questions: RpcQuestionSpec[];
26
+ /** The connection whose command raised the question, or `"agent"` when no connection's command was dispatching. */
27
+ raisedBy?: string;
26
28
  timeout: number;
27
29
  askedAtMs: number;
28
30
  deadlineAtMs: number;
@@ -1,8 +1,9 @@
1
1
  import type { RecoveryCarrier } from "../../core/session-recovery-identity.ts";
2
2
  import type { TranscriptTransition } from "../../core/session-transcript-journal.ts";
3
- import type { CallProjection } from "./compact-call-model.ts";
3
+ import type { CallFold } from "./compact-call-fold.ts";
4
4
  import type { CompactLaneCurrent } from "./compact-lane-current.ts";
5
5
  import type { CanonicalUnit } from "./compact-lane-transitions.ts";
6
+ import type { LiveIdentity } from "./compact-message-identity.ts";
6
7
  import type { CompactSettledMessages } from "./compact-settled-messages.ts";
7
8
  /** The compact components one lane epoch feeds: its settled terminals and its current state. */
8
9
  export interface CompactComponents {
@@ -16,14 +17,23 @@ export interface AcceptedRecord {
16
17
  readonly canonical: CanonicalUnit | undefined;
17
18
  readonly sequence: number;
18
19
  readonly turnId: string | null;
19
- /** The projection of the outstanding call a toolResult terminal settles (RULING-13 Q2), stored with it. */
20
- readonly call?: CallProjection | undefined;
20
+ /**
21
+ * The call fold of a call record: the projection of the outstanding call a toolResult terminal settles (RULING-13 Q2),
22
+ * stored with it, and the call an execution or hook record changed (RULING-123).
23
+ */
24
+ readonly call?: CallFold | undefined;
21
25
  }
22
- /** One accepted transcript transition: its owned join, then the cold flow and persistence it carries. */
23
- export declare function foldCompactTransition({ settled, current }: CompactComponents, transition: TranscriptTransition, transcriptAvailable: boolean, epoch: string): void;
26
+ /**
27
+ * One accepted transcript transition: its owned join, then the cold flow and persistence it carries. Returns the
28
+ * message an append joined, the id its `join-<n>` member records.
29
+ */
30
+ export declare function foldCompactTransition({ settled, current }: CompactComponents, transition: TranscriptTransition, transcriptAvailable: boolean, epoch: string): string | undefined;
24
31
  /**
25
32
  * The compact side of one accepted record: canonical and component folds, its terminal, its persistence. The call
26
- * records fold before the lane commits this sequence (RULING-13 Q9), so they are not here.
33
+ * records fold before the lane commits this sequence (RULING-13 Q9), so they are not here. Returns the identity the
34
+ * settled store recorded for this record's message (RULING-120), or the call a live call record belongs to: the one its
35
+ * fold changed, or for a `toolcall_*` update the call owning the streamed block once it folded (RULING-123); undefined
36
+ * when there is none.
27
37
  */
28
- export declare function foldCompactRecord({ settled, current }: CompactComponents, { record, type, carrier, canonical, sequence, turnId, call }: AcceptedRecord): void;
38
+ export declare function foldCompactRecord({ settled, current }: CompactComponents, { record, type, carrier, canonical, sequence, turnId, call }: AcceptedRecord): LiveIdentity | undefined;
29
39
  //# sourceMappingURL=session-acceptance-compact.d.ts.map
@@ -1,25 +1,37 @@
1
+ import { toolcallIndex } from "./compact-call-identity.js";
2
+ import { callIdentity } from "./compact-call-model.js";
1
3
  import { COMPONENT_RECORDS } from "./compact-current-components.js";
2
- /** One accepted transcript transition: its owned join, then the cold flow and persistence it carries. */
4
+ /**
5
+ * One accepted transcript transition: its owned join, then the cold flow and persistence it carries. Returns the
6
+ * message an append joined, the id its `join-<n>` member records.
7
+ */
3
8
  export function foldCompactTransition({ settled, current }, transition, transcriptAvailable, epoch) {
4
- if (transition.kind === "append" && transcriptAvailable)
5
- settled.append(transition);
9
+ const owner = transition.kind === "append" && transcriptAvailable ? settled.append(transition) : undefined;
6
10
  current.transcript(transition, settled, epoch);
11
+ return owner;
7
12
  }
8
13
  /**
9
14
  * The compact side of one accepted record: canonical and component folds, its terminal, its persistence. The call
10
- * records fold before the lane commits this sequence (RULING-13 Q9), so they are not here.
15
+ * records fold before the lane commits this sequence (RULING-13 Q9), so they are not here. Returns the identity the
16
+ * settled store recorded for this record's message (RULING-120), or the call a live call record belongs to: the one its
17
+ * fold changed, or for a `toolcall_*` update the call owning the streamed block once it folded (RULING-123); undefined
18
+ * when there is none.
11
19
  */
12
20
  export function foldCompactRecord({ settled, current }, { record, type, carrier, canonical, sequence, turnId, call }) {
13
21
  if (canonical)
14
22
  current.fold(canonical, sequence, turnId);
15
23
  if (COMPONENT_RECORDS.has(type))
16
24
  current.foldComponent(record, sequence);
17
- const joined = type === "message_end" && settled.end(record, carrier, sequence, turnId, call);
18
- if (type === "transcript_write_failed")
19
- settled.fail(carrier, String(Reflect.get(record, "errorMessage")));
25
+ const ended = type === "message_end" ? settled.end(record, carrier, sequence, turnId, call?.projection) : undefined;
26
+ const failed = type === "transcript_write_failed"
27
+ ? settled.fail(carrier, String(Reflect.get(record, "errorMessage")))
28
+ : undefined;
20
29
  // A failed write, or a terminal whose committed entry the lane accepted, settles its compact persistence too.
21
- const status = type === "transcript_write_failed" ? "failed" : joined ? "appended" : undefined;
30
+ const status = type === "transcript_write_failed" ? "failed" : ended?.joined ? "appended" : undefined;
22
31
  if (status && settled.valid(carrier))
23
32
  current.status(carrier.messageId, status);
33
+ const block = toolcallIndex(record);
34
+ const streamed = block !== undefined && carrier ? callIdentity(current.callOwning(carrier.messageId, block)) : undefined;
35
+ return ended?.identity ?? failed ?? call?.identity ?? streamed;
24
36
  }
25
37
  //# sourceMappingURL=session-acceptance-compact.js.map
@@ -2,6 +2,7 @@ import type { RecoveryCarrier } from "../../core/session-recovery-identity.ts";
2
2
  import type { TranscriptTransition } from "../../core/session-transcript-journal.ts";
3
3
  import { type TranscriptRoot } from "./accepted-transcript.ts";
4
4
  import type { CanonicalUnit } from "./compact-lane-transitions.ts";
5
+ import { MessageIdentities } from "./compact-message-identity.ts";
5
6
  import { type SettledDiagnostic, type SettledRoot } from "./compact-settled-messages.ts";
6
7
  import type { FrozenCompactState } from "./compact-state-view.ts";
7
8
  import { type CompactContentStoreOptions } from "./compact-content-store.ts";
@@ -59,6 +60,8 @@ export declare class SessionAcceptanceLane extends AcceptedRootReads {
59
60
  private turnId;
60
61
  private correlation;
61
62
  private refusedCorrelations;
63
+ /** RULING-120 (#5331): the settled ids of the record being delivered, read at `acceptedSequence`. */
64
+ readonly identities: MessageIdentities;
62
65
  private retiredAs;
63
66
  private storageRetired;
64
67
  private nonce;
@@ -119,6 +122,8 @@ export declare class SessionAcceptanceLane extends AcceptedRootReads {
119
122
  allocatedIndexNodes: number;
120
123
  residentPages: number;
121
124
  peakResidentPages: number;
125
+ ownerJoinsEvicted: number;
126
+ ownerJoinsOverflowed: number;
122
127
  residentSettledKeys: number;
123
128
  refusedRecords: number;
124
129
  transcript: import("./compact-transfer-staging.ts").StagingStatus;
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { AcceptedTranscript } from "./accepted-transcript.js";
5
+ import { MessageIdentities } from "./compact-message-identity.js";
5
6
  import { CompactSettledMessages } from "./compact-settled-messages.js";
6
7
  import { CompactContentStore } from "./compact-content-store.js";
7
8
  import { TransferStaging } from "./compact-transfer-staging.js";
@@ -26,6 +27,8 @@ export class SessionAcceptanceLane extends AcceptedRootReads {
26
27
  this.turnId = null;
27
28
  this.correlation = null;
28
29
  this.refusedCorrelations = 0;
30
+ /** RULING-120 (#5331): the settled ids of the record being delivered, read at `acceptedSequence`. */
31
+ this.identities = new MessageIdentities();
29
32
  this.storageRetired = false;
30
33
  this.nonce = randomUUID();
31
34
  this.cleanup = Promise.resolve();
@@ -67,6 +70,7 @@ export class SessionAcceptanceLane extends AcceptedRootReads {
67
70
  return;
68
71
  if (this.settled)
69
72
  this.retireSettled();
73
+ this.identities.reset();
70
74
  this.nonce = nonce;
71
75
  if (input.epochSignal)
72
76
  Buffer.from(nonce, "ascii").copy(Buffer.from(input.epochSignal));
@@ -98,7 +102,8 @@ export class SessionAcceptanceLane extends AcceptedRootReads {
98
102
  for (const transition of transitions) {
99
103
  this.sequence += 1;
100
104
  this.transcript.apply(transition);
101
- foldCompactTransition(this.compact(), transition, this.transcript.available, this.nonce);
105
+ const owner = foldCompactTransition(this.compact(), transition, this.transcript.available, this.nonce);
106
+ this.identities.joined(transition, owner);
102
107
  }
103
108
  }
104
109
  /**
@@ -109,6 +114,7 @@ export class SessionAcceptanceLane extends AcceptedRootReads {
109
114
  */
110
115
  accept(record, carrier, canonical, canonicalRefused = false) {
111
116
  const type = field(record, "type");
117
+ this.identities.clear();
112
118
  if (this.retired || this.epoch === "" || typeof type !== "string" || !SEQUENCED.has(type))
113
119
  return undefined;
114
120
  this.beforeAccept?.(record);
@@ -132,16 +138,9 @@ export class SessionAcceptanceLane extends AcceptedRootReads {
132
138
  const messageId = type.startsWith("message_") && this.settled.valid(carrier) ? carrier.messageId : null;
133
139
  if (type === "message_start")
134
140
  this.activeMessageId = messageId;
135
- if (folds)
136
- foldCompactRecord(this.compact(), {
137
- record,
138
- type,
139
- carrier,
140
- canonical,
141
- sequence: this.sequence,
142
- turnId,
143
- call: call?.projection,
144
- });
141
+ const unit = { record, type, carrier, canonical, sequence: this.sequence, turnId, call };
142
+ // An unfolded record is still delivered, so its `entry_appended` takes its entry's join (RULING-122).
143
+ this.identities.hold(type, this.sequence, record, folds ? foldCompactRecord(this.compact(), unit) : undefined);
145
144
  if (type === "message_end" && this.activeMessageId === messageId)
146
145
  this.activeMessageId = null;
147
146
  return {
@@ -203,6 +202,7 @@ export class SessionAcceptanceLane extends AcceptedRootReads {
203
202
  return {
204
203
  ...this.settled.storageStatus(),
205
204
  refusedRecords: this.current.refusedRecords,
205
+ ...this.identities.status(),
206
206
  transcript: this.staging.status(),
207
207
  };
208
208
  }
@@ -227,6 +227,7 @@ export class SessionAcceptanceLane extends AcceptedRootReads {
227
227
  */
228
228
  retire(reason, keep = new Set()) {
229
229
  this.retiredAs ??= reason;
230
+ this.identities.reset();
230
231
  this.generations.close();
231
232
  this.transcript = new AcceptedTranscript(this.staging);
232
233
  for (const root of this.live)
@@ -20,7 +20,8 @@ export declare abstract class SessionEventWriterDestinations extends SessionEven
20
20
  * holds this lane-owned session, becomes an `eventLease` envelope at the record's accepted sequence, its content
21
21
  * staged from the line another destination built or else streamed from the record (never one legacy line); every
22
22
  * other destination gets the legacy line. Past the peer's lease bound, or when the record cannot be staged, the peer
23
- * gets a typed refusal and its connection closes (fail closed, like a plain peer's overflow).
23
+ * gets a typed refusal and its connection closes (fail closed, like a plain peer's overflow). A stamped copy is
24
+ * built once per delivery (`copies`), so every compact peer leases the same staged text.
24
25
  */
25
26
  private eventLine;
26
27
  }