@code-yeongyu/senpi 2026.8.30-2 → 2026.8.30-3

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 (88) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/dist/core/agent-session.d.ts +2 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +77 -10
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +1 -0
  7. package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts.map +1 -1
  8. package/dist/core/extensions/builtin/compaction/context-pipeline.js +6 -2
  9. package/dist/core/extensions/builtin/compaction/context-pipeline.js.map +1 -1
  10. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  11. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +3 -2
  12. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  13. package/dist/core/extensions/builtin/compaction/idle-retry.d.ts +1 -1
  14. package/dist/core/extensions/builtin/compaction/idle-retry.d.ts.map +1 -1
  15. package/dist/core/extensions/builtin/compaction/idle-retry.js +1 -1
  16. package/dist/core/extensions/builtin/compaction/idle-retry.js.map +1 -1
  17. package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
  18. package/dist/core/extensions/builtin/compaction/index.js +34 -10
  19. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  20. package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts +30 -0
  21. package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts.map +1 -0
  22. package/dist/core/extensions/builtin/compaction/model-usability-budget.js +74 -0
  23. package/dist/core/extensions/builtin/compaction/model-usability-budget.js.map +1 -0
  24. package/dist/core/extensions/builtin/compaction/orchestration.d.ts +3 -13
  25. package/dist/core/extensions/builtin/compaction/orchestration.d.ts.map +1 -1
  26. package/dist/core/extensions/builtin/compaction/orchestration.js +50 -83
  27. package/dist/core/extensions/builtin/compaction/orchestration.js.map +1 -1
  28. package/dist/core/extensions/builtin/compaction/policy.d.ts +8 -1
  29. package/dist/core/extensions/builtin/compaction/policy.d.ts.map +1 -1
  30. package/dist/core/extensions/builtin/compaction/policy.js +12 -3
  31. package/dist/core/extensions/builtin/compaction/policy.js.map +1 -1
  32. package/dist/core/extensions/builtin/compaction/token-budget-reminder.d.ts +8 -2
  33. package/dist/core/extensions/builtin/compaction/token-budget-reminder.d.ts.map +1 -1
  34. package/dist/core/extensions/builtin/compaction/token-budget-reminder.js +12 -5
  35. package/dist/core/extensions/builtin/compaction/token-budget-reminder.js.map +1 -1
  36. package/dist/core/extensions/builtin/compaction/tool-admission.d.ts +12 -21
  37. package/dist/core/extensions/builtin/compaction/tool-admission.d.ts.map +1 -1
  38. package/dist/core/extensions/builtin/compaction/tool-admission.js +27 -63
  39. package/dist/core/extensions/builtin/compaction/tool-admission.js.map +1 -1
  40. package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +11 -0
  41. package/dist/core/extensions/builtin/terminal/runtime-session.d.ts.map +1 -1
  42. package/dist/core/extensions/builtin/terminal/runtime-session.js +18 -2
  43. package/dist/core/extensions/builtin/terminal/runtime-session.js.map +1 -1
  44. package/dist/core/sdk.d.ts.map +1 -1
  45. package/dist/core/sdk.js +5 -0
  46. package/dist/core/sdk.js.map +1 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/modes/index.d.ts +1 -1
  52. package/dist/modes/index.d.ts.map +1 -1
  53. package/dist/modes/index.js +1 -1
  54. package/dist/modes/index.js.map +1 -1
  55. package/dist/modes/interactive/interactive-host-runtime.d.ts +13 -2
  56. package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
  57. package/dist/modes/interactive/interactive-host-runtime.js +317 -72
  58. package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
  59. package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
  60. package/dist/modes/rpc/connection-handler.js +17 -4
  61. package/dist/modes/rpc/connection-handler.js.map +1 -1
  62. package/dist/modes/rpc/rpc-client.d.ts +10 -0
  63. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  64. package/dist/modes/rpc/rpc-client.js +32 -10
  65. package/dist/modes/rpc/rpc-client.js.map +1 -1
  66. package/docs/rpc.md +4 -0
  67. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
  68. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  69. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  70. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  71. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  72. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  73. package/node_modules/@earendil-works/pi-pty/dist/screen-operations.d.ts +42 -0
  74. package/node_modules/@earendil-works/pi-pty/dist/screen-operations.d.ts.map +1 -0
  75. package/node_modules/@earendil-works/pi-pty/dist/screen-operations.js +34 -0
  76. package/node_modules/@earendil-works/pi-pty/dist/screen-operations.js.map +1 -0
  77. package/node_modules/@earendil-works/pi-pty/dist/screen-text.d.ts +8 -0
  78. package/node_modules/@earendil-works/pi-pty/dist/screen-text.d.ts.map +1 -0
  79. package/node_modules/@earendil-works/pi-pty/dist/screen-text.js +51 -0
  80. package/node_modules/@earendil-works/pi-pty/dist/screen-text.js.map +1 -0
  81. package/node_modules/@earendil-works/pi-pty/dist/screen.d.ts +45 -0
  82. package/node_modules/@earendil-works/pi-pty/dist/screen.d.ts.map +1 -1
  83. package/node_modules/@earendil-works/pi-pty/dist/screen.js +178 -55
  84. package/node_modules/@earendil-works/pi-pty/dist/screen.js.map +1 -1
  85. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  86. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  87. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  88. package/package.json +7 -7
@@ -4,8 +4,9 @@ import { executeBashWithOperations } from "../../core/bash-executor.js";
4
4
  import { SessionManager } from "../../core/session-manager.js";
5
5
  import { SettingsManager } from "../../core/settings-manager.js";
6
6
  import { ensureHost } from "../rpc/host-ensure.js";
7
- import { RpcClient } from "../rpc/rpc-client.js";
7
+ import { isTransportGoneError, RpcClient } from "../rpc/rpc-client.js";
8
8
  export const INTERACTIVE_HOST_FALLBACK_WARNING = "Warning: shared interactive host unavailable; continuing locally";
9
+ export const INTERACTIVE_HOST_RECONNECTING_WARNING = "Warning: shared interactive host connection lost; reconnecting";
9
10
  /**
10
11
  * Replace only the transport-facing session operations. The object returned is
11
12
  * deliberately still an AgentSessionRuntime: InteractiveMode and extensions
@@ -17,7 +18,82 @@ export async function createInteractiveHostRuntime(localRuntime, options) {
17
18
  if (!sessionPath)
18
19
  return localRuntime;
19
20
  const startHost = options.ensureHost ?? ((hostOptions) => ensureHost(hostOptions));
20
- const client = new RpcClient({ socketPath: options.socket });
21
+ let remoteSession;
22
+ let remoteRuntime;
23
+ let reconnecting;
24
+ let disconnectQueued = false;
25
+ let disposed = false;
26
+ let fallbackWarned = false;
27
+ let reconnectWarningShown = false;
28
+ const warnReconnect = (cause) => {
29
+ if (reconnectWarningShown)
30
+ return;
31
+ reconnectWarningShown = true;
32
+ options.onWarning?.({
33
+ type: "interactive_host_action_failed",
34
+ message: INTERACTIVE_HOST_RECONNECTING_WARNING,
35
+ cause,
36
+ });
37
+ };
38
+ const warnFallback = (cause) => {
39
+ if (fallbackWarned)
40
+ return;
41
+ fallbackWarned = true;
42
+ options.onWarning?.({ type: "interactive_host_fallback", message: INTERACTIVE_HOST_FALLBACK_WARNING, cause });
43
+ };
44
+ let scheduleReconnect = () => { };
45
+ const client = new RpcClient({
46
+ socketPath: options.socket,
47
+ onDisconnect: () => {
48
+ disconnectQueued = true;
49
+ scheduleReconnect();
50
+ },
51
+ });
52
+ scheduleReconnect = () => {
53
+ if (!remoteSession || reconnecting || disposed || remoteRuntime?.isFallback)
54
+ return;
55
+ remoteRuntime?.enterReconnecting();
56
+ reconnecting = (async () => {
57
+ let cause;
58
+ for (let attempt = 0; attempt < 3; attempt++) {
59
+ if (disposed)
60
+ return;
61
+ disconnectQueued = false;
62
+ try {
63
+ await startHost({ socket: options.socket, agentDir: options.agentDir });
64
+ if (disposed)
65
+ return;
66
+ await client.start();
67
+ if (disposed)
68
+ return;
69
+ await client.openSession({ sessionPath, cwd: localRuntime.cwd });
70
+ if (disposed)
71
+ return;
72
+ await remoteSession.refresh();
73
+ if (disposed)
74
+ return;
75
+ fallbackWarned = false;
76
+ reconnectWarningShown = false;
77
+ remoteRuntime?.enterConnected();
78
+ return;
79
+ }
80
+ catch (error) {
81
+ cause = error;
82
+ await client.stop().catch(() => { });
83
+ if (disposed)
84
+ return;
85
+ }
86
+ }
87
+ if (!disposed) {
88
+ await remoteRuntime?.enterFallback();
89
+ warnFallback(cause);
90
+ }
91
+ })().finally(() => {
92
+ reconnecting = undefined;
93
+ if (disconnectQueued && !disposed && !remoteRuntime?.isFallback)
94
+ scheduleReconnect();
95
+ });
96
+ };
21
97
  try {
22
98
  await startHost({ socket: options.socket, agentDir: options.agentDir });
23
99
  await client.start();
@@ -28,7 +104,12 @@ export async function createInteractiveHostRuntime(localRuntime, options) {
28
104
  modelId: localRuntime.session.model?.id,
29
105
  thinkingLevel: localRuntime.session.thinkingLevel,
30
106
  });
31
- const remoteSession = createRemoteSessionProxy(localRuntime.session, localRuntime.services.agentDir, client, opened.state, options.onWarning);
107
+ remoteSession = createRemoteSessionProxy(localRuntime.session, localRuntime.services.agentDir, client, opened.state, options.onWarning, (cause) => {
108
+ if (remoteRuntime?.isReconnecting)
109
+ warnReconnect(cause);
110
+ else
111
+ warnFallback(cause);
112
+ });
32
113
  if (opened.state.isBashRunning && !opened.attached) {
33
114
  // Only a newly opened session can have an execution orphaned by a
34
115
  // previous connection. An attach is an observer of the same live runtime;
@@ -37,15 +118,25 @@ export async function createInteractiveHostRuntime(localRuntime, options) {
37
118
  }
38
119
  if (opened.attached)
39
120
  await remoteSession.refresh();
40
- return new RemoteInteractiveRuntime(localRuntime, remoteSession, client);
121
+ remoteRuntime = new RemoteInteractiveRuntime(localRuntime, remoteSession, client, {
122
+ onTransportGone: (cause) => {
123
+ if (remoteRuntime?.isReconnecting)
124
+ warnReconnect(cause);
125
+ else
126
+ warnFallback(cause);
127
+ },
128
+ get reconnecting() {
129
+ return reconnecting;
130
+ },
131
+ dispose: () => {
132
+ disposed = true;
133
+ },
134
+ });
135
+ return remoteRuntime;
41
136
  }
42
137
  catch (cause) {
43
138
  await client.stop().catch(() => { });
44
- options.onWarning?.({
45
- type: "interactive_host_fallback",
46
- message: `${INTERACTIVE_HOST_FALLBACK_WARNING}: ${cause instanceof Error ? cause.message : String(cause)}`,
47
- cause,
48
- });
139
+ warnFallback(cause);
49
140
  return localRuntime;
50
141
  }
51
142
  }
@@ -53,21 +144,68 @@ export class RemoteInteractiveRuntime {
53
144
  #local;
54
145
  #remoteSession;
55
146
  #client;
147
+ #lifecycle;
148
+ #onTransportGone;
56
149
  #rebindSession;
57
150
  #beforeSessionInvalidate;
58
- constructor(local, remoteSession, client) {
151
+ #state = "connected";
152
+ #fallbackHandoff;
153
+ get isReconnecting() {
154
+ return this.#state === "reconnecting";
155
+ }
156
+ get isFallback() {
157
+ return this.#state === "fallback";
158
+ }
159
+ constructor(local, remoteSession, client, lifecycle) {
59
160
  this.#local = local;
60
161
  this.#remoteSession = remoteSession;
61
162
  this.#client = client;
163
+ this.#lifecycle = lifecycle;
164
+ this.#onTransportGone = lifecycle?.onTransportGone ?? (() => { });
165
+ }
166
+ async #call(call) {
167
+ try {
168
+ return await call();
169
+ }
170
+ catch (error) {
171
+ if (isTransportGoneError(error)) {
172
+ this.#onTransportGone(error);
173
+ return { cancelled: true };
174
+ }
175
+ throw error;
176
+ }
177
+ }
178
+ enterConnected() {
179
+ if (this.#state !== "disposed" && this.#state !== "fallback")
180
+ this.#state = "connected";
181
+ }
182
+ enterReconnecting() {
183
+ if (this.#state !== "disposed" && this.#state !== "fallback")
184
+ this.#state = "reconnecting";
185
+ }
186
+ async enterFallback() {
187
+ if (this.#state === "disposed" || this.#state === "fallback")
188
+ return;
189
+ this.#state = "fallback";
190
+ this.#beforeSessionInvalidate?.();
191
+ const rebind = this.#rebindSession?.();
192
+ if (rebind) {
193
+ this.#fallbackHandoff = rebind.catch((error) => {
194
+ this.#onTransportGone(error);
195
+ });
196
+ await this.#fallbackHandoff;
197
+ }
62
198
  }
63
199
  get session() {
64
- return this.#remoteSession.session;
200
+ return this.#state === "fallback" || this.#state === "disposed"
201
+ ? this.#local.session
202
+ : this.#remoteSession.session;
65
203
  }
66
204
  get services() {
67
205
  return this.#local.services;
68
206
  }
69
207
  get cwd() {
70
- return this.#remoteSession.session.sessionManager.getCwd();
208
+ return this.session.sessionManager.getCwd();
71
209
  }
72
210
  get diagnostics() {
73
211
  return this.#local.diagnostics;
@@ -80,14 +218,27 @@ export class RemoteInteractiveRuntime {
80
218
  }
81
219
  setBeforeSessionInvalidate(callback) {
82
220
  this.#beforeSessionInvalidate = callback;
221
+ this.#local.setBeforeSessionInvalidate(callback);
83
222
  }
84
223
  setRebindSession(callback) {
85
224
  this.#rebindSession = callback;
225
+ this.#local.setRebindSession(callback);
226
+ if (callback && this.#state === "fallback" && !this.#fallbackHandoff) {
227
+ this.#fallbackHandoff = callback().catch((error) => {
228
+ this.#onTransportGone(error);
229
+ });
230
+ }
86
231
  }
87
232
  setHostUiHandler(callback) {
88
233
  this.#remoteSession.setHostUiHandler(callback);
89
234
  }
90
235
  async dispose() {
236
+ if (this.#state === "disposed")
237
+ return;
238
+ this.#state = "disposed";
239
+ this.#lifecycle?.dispose();
240
+ await this.#lifecycle?.reconnecting?.catch(() => { });
241
+ await this.#fallbackHandoff?.catch(() => { });
91
242
  const errors = [];
92
243
  for (const cleanup of [
93
244
  () => this.#remoteSession.abortLocalBash(),
@@ -106,12 +257,15 @@ export class RemoteInteractiveRuntime {
106
257
  throw errors[0];
107
258
  }
108
259
  async newSession(options) {
260
+ if (this.#state === "fallback")
261
+ return this.#local.newSession(options);
109
262
  // Suppresses for the ENTIRE command, not just the refresh tail: a
110
263
  // multi-session host emits session_replaced while completing it, so the echo
111
264
  // can arrive before the refresh/rebind sequence below starts and race it -
112
265
  // newSession transports setup entries between two refreshes.
113
- return this.#remoteSession.aroundLocalReplacement(async () => {
114
- const result = await this.#client.newSession(options?.parentSession);
266
+ return this.#remoteSession
267
+ .aroundLocalReplacement(async () => {
268
+ const result = await this.#call(() => this.#client.newSession(options?.parentSession));
115
269
  if (!result.cancelled) {
116
270
  this.#beforeSessionInvalidate?.();
117
271
  this.#remoteSession.abortLocalBash();
@@ -128,13 +282,21 @@ export class RemoteInteractiveRuntime {
128
282
  await options.withSession(this.#remoteSession.createReplacedSessionContext());
129
283
  }
130
284
  return result;
285
+ })
286
+ .catch((error) => {
287
+ if (isTransportGoneError(error))
288
+ return { cancelled: true };
289
+ throw error;
131
290
  });
132
291
  }
133
292
  async switchSession(sessionPath, options) {
293
+ if (this.#state === "fallback")
294
+ return this.#local.switchSession(sessionPath, options);
134
295
  // See newSession: the suppression must cover the command itself, since the
135
296
  // host emits session_replaced while completing it.
136
- return this.#remoteSession.aroundLocalReplacement(async () => {
137
- const result = await this.#client.switchSession(sessionPath, options?.cwdOverride === undefined ? undefined : { cwdOverride: options.cwdOverride });
297
+ return this.#remoteSession
298
+ .aroundLocalReplacement(async () => {
299
+ const result = await this.#call(() => this.#client.switchSession(sessionPath, options?.cwdOverride === undefined ? undefined : { cwdOverride: options.cwdOverride }));
138
300
  if (!result.cancelled) {
139
301
  this.#beforeSessionInvalidate?.();
140
302
  this.#remoteSession.abortLocalBash();
@@ -147,12 +309,20 @@ export class RemoteInteractiveRuntime {
147
309
  await options.withSession(this.#remoteSession.createReplacedSessionContext());
148
310
  }
149
311
  return result;
312
+ })
313
+ .catch((error) => {
314
+ if (isTransportGoneError(error))
315
+ return { cancelled: true };
316
+ throw error;
150
317
  });
151
318
  }
152
319
  async fork(entryId, options) {
320
+ if (this.#state === "fallback")
321
+ return this.#local.fork(entryId, options);
153
322
  // See newSession: the suppression must cover the command itself.
154
- return this.#remoteSession.aroundLocalReplacement(async () => {
155
- const result = await this.#client.fork(entryId, options);
323
+ return this.#remoteSession
324
+ .aroundLocalReplacement(async () => {
325
+ const result = await this.#call(() => this.#client.fork(entryId, options));
156
326
  if (!result.cancelled) {
157
327
  this.#beforeSessionInvalidate?.();
158
328
  this.#remoteSession.abortLocalBash();
@@ -161,18 +331,31 @@ export class RemoteInteractiveRuntime {
161
331
  await options.withSession(this.#remoteSession.createReplacedSessionContext());
162
332
  }
163
333
  return { cancelled: result.cancelled, selectedText: result.text };
334
+ })
335
+ .catch((error) => {
336
+ if (isTransportGoneError(error))
337
+ return { cancelled: true };
338
+ throw error;
164
339
  });
165
340
  }
166
341
  async importFromJsonl(inputPath, cwdOverride) {
342
+ if (this.#state === "fallback")
343
+ return this.#local.importFromJsonl(inputPath, cwdOverride);
167
344
  // See newSession: the suppression must cover the command itself.
168
- return this.#remoteSession.aroundLocalReplacement(async () => {
169
- const result = await this.#client.importJsonl(inputPath, cwdOverride);
345
+ return this.#remoteSession
346
+ .aroundLocalReplacement(async () => {
347
+ const result = await this.#call(() => this.#client.importJsonl(inputPath, cwdOverride));
170
348
  if (!result.cancelled) {
171
349
  this.#beforeSessionInvalidate?.();
172
350
  this.#remoteSession.abortLocalBash();
173
351
  await this.#refreshAndRebind();
174
352
  }
175
353
  return result;
354
+ })
355
+ .catch((error) => {
356
+ if (isTransportGoneError(error))
357
+ return { cancelled: true };
358
+ throw error;
176
359
  });
177
360
  }
178
361
  async #refreshAndRebind() {
@@ -181,16 +364,31 @@ export class RemoteInteractiveRuntime {
181
364
  }
182
365
  }
183
366
  /** Exported for direct coverage of the wire-event handling below. */
184
- export function createRemoteSessionProxy(local, agentDir, client, initialState, onWarning) {
367
+ export function createRemoteSessionProxy(local, agentDir, client, initialState, onWarning, onTransportGone) {
185
368
  // Fire-and-forget setters keep the sync AgentSession signature, but their RPC
186
369
  // failures must not vanish: the matching *_changed wire event confirms success,
187
370
  // and a rejection here is the only signal of failure.
188
371
  const reportActionFailure = (action) => (error) => {
189
- onWarning?.({
190
- type: "interactive_host_action_failed",
191
- message: `Warning: shared interactive host ${action} failed: ${error instanceof Error ? error.message : String(error)}`,
192
- cause: error,
193
- });
372
+ const transportGone = isTransportGoneError(error);
373
+ if (transportGone)
374
+ onTransportGone?.(error);
375
+ else
376
+ onWarning?.({
377
+ type: "interactive_host_action_failed",
378
+ message: `Warning: shared interactive host ${action} failed: ${error instanceof Error ? error.message : String(error)}`,
379
+ cause: error,
380
+ });
381
+ };
382
+ const transportCall = async (action, call, cancelledValue) => {
383
+ try {
384
+ return await call();
385
+ }
386
+ catch (error) {
387
+ if (!isTransportGoneError(error))
388
+ throw error;
389
+ reportActionFailure(action)(error);
390
+ return cancelledValue;
391
+ }
194
392
  };
195
393
  let state = { ...initialState };
196
394
  const waitForBashCallbacks = async (promises, allowAbandonment) => {
@@ -262,7 +460,7 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
262
460
  const remoteSessionManager = new Proxy({}, {
263
461
  get(_target, property, _receiver) {
264
462
  if (property === "appendLabelChange") {
265
- return (entryId, label) => void client.setLabel(entryId, label);
463
+ return (entryId, label) => void client.setLabel(entryId, label).catch(reportActionFailure("appendLabelChange"));
266
464
  }
267
465
  if (property === "getCwd")
268
466
  return () => state.cwd;
@@ -283,7 +481,9 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
283
481
  if (wireEvent.type === "extension_ui_request") {
284
482
  const request = wireEvent;
285
483
  if (hostUiHandler)
286
- void Promise.resolve(hostUiHandler(request)).then((response) => response && client.sendExtensionUIResponse(response));
484
+ void Promise.resolve(hostUiHandler(request))
485
+ .then((response) => response && client.sendExtensionUIResponse(response))
486
+ .catch(reportActionFailure("host UI response"));
287
487
  else
288
488
  pendingUiRequests.push(request);
289
489
  return;
@@ -441,7 +641,13 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
441
641
  listener(event);
442
642
  });
443
643
  const performRefresh = async () => {
444
- const nextState = await client.getState();
644
+ // Captured before the first await: entries that arrive via `entry_appended`
645
+ // while this refresh is in flight are newer than the snapshot it reconciles
646
+ // against, and must survive the rebuild below.
647
+ const idsAtRefreshStart = new Set(sessionManager.getEntries().map((entry) => entry.id));
648
+ const nextState = await transportCall("refresh", () => client.getState());
649
+ if (!nextState)
650
+ return;
445
651
  state = { ...stateFromRpc(nextState) };
446
652
  nextQueuedInputOrder = Math.max(0, ...nextState.ordered.map((item) => item.enqueueOrder));
447
653
  let messages;
@@ -460,14 +666,40 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
460
666
  // deferred creating the file for a setup-only session.
461
667
  sessionManager = SessionManager.open(nextState.sessionFile, undefined, nextState.cwd);
462
668
  }
463
- if (nextState.entries?.length && !sessionManager.getEntries().length) {
464
- for (const entry of nextState.entries)
465
- sessionManager.appendEntry(entry);
669
+ // The host ships its complete entry list while the session file is still
670
+ // deferred (no assistant message yet, so nothing has been written), which
671
+ // makes that list the only authoritative view of the session. Reconcile the
672
+ // mirror to it exactly: `entry_appended` notifications that cross a
673
+ // concurrent refresh land on whichever manager is current, so the mirror
674
+ // can hold an arbitrary partial set rather than a prefix, and a snapshot
675
+ // taken mid-bind can simply be short. Backfilling only an empty mirror
676
+ // wedged it at whatever partial set it happened to hold.
677
+ const authoritative = nextState.entries;
678
+ if (authoritative?.length) {
679
+ const mirror = sessionManager.getEntries();
680
+ const matches = mirror.length === authoritative.length &&
681
+ mirror.every((entry, index) => entry.id === authoritative[index]?.id);
682
+ if (!matches) {
683
+ const rebuilt = SessionManager.open(nextState.sessionFile, undefined, nextState.cwd);
684
+ for (const entry of authoritative)
685
+ rebuilt.appendEntry(entry);
686
+ // A notification that crossed this refresh refers to an entry the
687
+ // snapshot predates, so it is newer than the snapshot; dropping it
688
+ // would strand the entry until an unrelated refresh happened to
689
+ // run. Keep it, in arrival order, after the authoritative list.
690
+ const authoritativeIds = new Set(authoritative.map((entry) => entry.id));
691
+ for (const entry of mirror) {
692
+ if (!idsAtRefreshStart.has(entry.id) && !authoritativeIds.has(entry.id)) {
693
+ rebuilt.appendEntry(entry);
694
+ }
695
+ }
696
+ sessionManager = rebuilt;
697
+ }
466
698
  }
467
699
  messages = sessionManager.buildSessionContext().messages;
468
700
  }
469
701
  else {
470
- messages = await client.getMessages();
702
+ messages = await transportCall("refresh", () => client.getMessages(), []);
471
703
  }
472
704
  local.agent.state.messages.splice(0, local.agent.state.messages.length, ...structuredClone(messages));
473
705
  streamingAssistant = undefined;
@@ -485,23 +717,32 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
485
717
  const session = new Proxy(local, {
486
718
  get(target, property, receiver) {
487
719
  if (property === "prompt")
488
- return (message, options) => client.prompt(message, {
489
- ...(options?.images ? { images: options.images } : {}),
490
- ...(options?.streamingBehavior ? { streamingBehavior: options.streamingBehavior } : {}),
491
- ...(options?.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}),
492
- ...(options?.sessionTitlePrompt !== undefined
493
- ? { sessionTitlePrompt: options.sessionTitlePrompt }
494
- : {}),
495
- ...(options?.expandPromptTemplates !== undefined
496
- ? { expandPromptTemplates: options.expandPromptTemplates }
497
- : {}),
498
- ...(options?.promptDisposition ? { promptDisposition: options.promptDisposition } : {}),
499
- ...(options?.preflightResult ? { preflightResult: options.preflightResult } : {}),
500
- });
720
+ return async (message, options) => {
721
+ try {
722
+ await client.prompt(message, {
723
+ ...(options?.images ? { images: options.images } : {}),
724
+ ...(options?.streamingBehavior ? { streamingBehavior: options.streamingBehavior } : {}),
725
+ ...(options?.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}),
726
+ ...(options?.sessionTitlePrompt !== undefined
727
+ ? { sessionTitlePrompt: options.sessionTitlePrompt }
728
+ : {}),
729
+ ...(options?.expandPromptTemplates !== undefined
730
+ ? { expandPromptTemplates: options.expandPromptTemplates }
731
+ : {}),
732
+ ...(options?.promptDisposition ? { promptDisposition: options.promptDisposition } : {}),
733
+ ...(options?.preflightResult ? { preflightResult: options.preflightResult } : {}),
734
+ });
735
+ }
736
+ catch (error) {
737
+ if (!isTransportGoneError(error))
738
+ throw error;
739
+ reportActionFailure("prompt")(error);
740
+ }
741
+ };
501
742
  if (property === "abort")
502
743
  return () => client.abort();
503
744
  if (property === "abortCompaction")
504
- return () => void client.abortCompaction();
745
+ return () => void client.abortCompaction().catch(reportActionFailure("abortCompaction"));
505
746
  if (property === "steer")
506
747
  return (message, images, recovery) => {
507
748
  const enqueueOrder = recovery?.enqueueOrder ?? Math.max(0, ...state.ordered.map((item) => item.enqueueOrder)) + 1;
@@ -511,7 +752,7 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
511
752
  ordered: [...state.ordered, { text: message, mode: "steer", enqueueOrder }],
512
753
  pendingMessageCount: state.pendingMessageCount + 1,
513
754
  };
514
- return client.steer(message, images, { ...recovery, enqueueOrder });
755
+ return transportCall("steer", () => client.steer(message, images, { ...recovery, enqueueOrder }));
515
756
  };
516
757
  if (property === "followUp")
517
758
  return (message, images, recovery) => {
@@ -522,7 +763,7 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
522
763
  ordered: [...state.ordered, { text: message, mode: "followUp", enqueueOrder }],
523
764
  pendingMessageCount: state.pendingMessageCount + 1,
524
765
  };
525
- return client.followUp(message, images, { ...recovery, enqueueOrder });
766
+ return transportCall("followUp", () => client.followUp(message, images, { ...recovery, enqueueOrder }));
526
767
  };
527
768
  if (property === "waitForIdle")
528
769
  return () => client.waitForIdle();
@@ -530,8 +771,8 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
530
771
  return () => target.getLastAssistantText();
531
772
  if (property === "setModel" || property === "setSessionModel")
532
773
  return async (model) => {
533
- const next = await client.setModel(model.provider, model.id);
534
- return { systemPromptName: next.systemPromptName, model: next };
774
+ const next = await transportCall("setModel", () => client.setModel(model.provider, model.id), undefined);
775
+ return next ? { systemPromptName: next.systemPromptName, model: next } : undefined;
535
776
  };
536
777
  if (property === "setSessionThinkingLevel")
537
778
  return (level) => void client
@@ -556,19 +797,19 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
556
797
  void client.setScopedModels(models).catch(reportActionFailure("setScopedModels"));
557
798
  };
558
799
  if (property === "cycleModel")
559
- return (direction) => client.cycleModel(direction);
800
+ return (direction) => transportCall("cycleModel", () => client.cycleModel(direction), undefined);
560
801
  if (property === "setThinkingLevel")
561
802
  return (level) => void client.setThinkingLevel(level).catch(reportActionFailure("setThinkingLevel"));
562
803
  if (property === "cycleThinkingLevel")
563
- return () => client.cycleThinkingLevel().then((result) => result?.level);
804
+ return () => transportCall("cycleThinkingLevel", () => client.cycleThinkingLevel(), undefined).then((result) => result?.level);
564
805
  if (property === "getAvailableThinkingLevels")
565
- return () => client.getAvailableThinkingLevels();
806
+ return () => transportCall("getAvailableThinkingLevels", () => client.getAvailableThinkingLevels(), []);
566
807
  if (property === "setSteeringMode")
567
808
  return (mode) => void client.setSteeringMode(mode).catch(reportActionFailure("setSteeringMode"));
568
809
  if (property === "setFollowUpMode")
569
810
  return (mode) => void client.setFollowUpMode(mode).catch(reportActionFailure("setFollowUpMode"));
570
811
  if (property === "compact")
571
- return (instructions) => client.compact(instructions);
812
+ return (instructions) => transportCall("compact", () => client.compact(instructions));
572
813
  if (property === "setAutoCompactionEnabled")
573
814
  return (enabled) => void client.setAutoCompaction(enabled).catch(reportActionFailure("setAutoCompaction"));
574
815
  if (property === "executeBash")
@@ -584,7 +825,7 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
584
825
  try {
585
826
  const result = await executeBashWithOperations(resolvedCommand, state.cwd, options.operations, { onChunk, signal: abortController.signal });
586
827
  if (state.sessionId === sessionAtStart) {
587
- await client.recordBashResult(command, result, options.excludeFromContext);
828
+ await transportCall("recordBashResult", () => client.recordBashResult(command, result, options.excludeFromContext));
588
829
  }
589
830
  return result;
590
831
  }
@@ -603,11 +844,11 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
603
844
  };
604
845
  bashExecutions.set(executionId, execution);
605
846
  try {
606
- const result = await client.bash(command, {
847
+ const result = await transportCall("bash", () => client.bash(command, {
607
848
  excludeFromContext: options?.excludeFromContext,
608
849
  executionId,
609
850
  operations: options?.operations,
610
- });
851
+ }), { output: "", exitCode: undefined, cancelled: true, truncated: false });
611
852
  const callbacksSettled = await waitForBashCallbacks(execution.promises, false);
612
853
  if (!callbacksSettled) {
613
854
  if (result.fullOutputPath)
@@ -635,20 +876,22 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
635
876
  if (property === "getContextUsage")
636
877
  return () => state.contextUsage;
637
878
  if (property === "getSessionStats")
638
- return () => client.getSessionStats();
879
+ return () => transportCall("getSessionStats", () => client.getSessionStats(), local.getSessionStats());
639
880
  if (property === "exportToHtml")
640
- return (outputPath, options) => client.exportHtml(outputPath, options?.themeName).then((result) => result.path);
881
+ return (outputPath, options) => transportCall("exportToHtml", () => client.exportHtml(outputPath, options?.themeName)).then((result) => result?.path);
641
882
  if (property === "setSessionName")
642
883
  return (name) => client.setSessionName(name).catch(reportActionFailure("setSessionName"));
643
884
  if (property === "navigateTree")
644
885
  return async (targetId, options) => {
645
- const result = await client.navigateTree(targetId, options);
886
+ const result = await transportCall("navigateTree", () => client.navigateTree(targetId, options), {
887
+ cancelled: true,
888
+ });
646
889
  if (!result.cancelled)
647
890
  await refresh();
648
891
  return result;
649
892
  };
650
893
  if (property === "getUserMessagesForForking")
651
- return () => client.getForkMessages();
894
+ return () => transportCall("getUserMessagesForForking", () => client.getForkMessages(), []);
652
895
  if (property === "subscribe")
653
896
  return (listener) => {
654
897
  listeners.add(listener);
@@ -694,11 +937,11 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
694
937
  if (property === "isBashRunning")
695
938
  return state.isBashRunning;
696
939
  if (property === "reload")
697
- return (_options) => client.reload();
940
+ return (_options) => transportCall("reload", () => client.reload(), { cancelled: true });
698
941
  if (property === "checkReloadVeto")
699
- return () => client.checkReloadVeto();
942
+ return () => transportCall("checkReloadVeto", () => client.checkReloadVeto(), { cancelled: true });
700
943
  if (property === "exportToJsonl")
701
- return (outputPath) => client.exportJsonl(outputPath).then((result) => result.path);
944
+ return (outputPath) => transportCall("exportToJsonl", () => client.exportJsonl(outputPath)).then((result) => result?.path);
702
945
  // The footer and other renderers read session.state.*; surface the
703
946
  // host-authoritative fields there too, not only via the direct getters.
704
947
  if (property === "state") {
@@ -750,7 +993,9 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
750
993
  hostUiHandler = callback;
751
994
  if (callback)
752
995
  for (const request of pendingUiRequests.splice(0))
753
- void Promise.resolve(callback(request)).then((response) => response && client.sendExtensionUIResponse(response));
996
+ void Promise.resolve(callback(request))
997
+ .then((response) => response && client.sendExtensionUIResponse(response))
998
+ .catch(reportActionFailure("host UI response"));
754
999
  },
755
1000
  refresh,
756
1001
  aroundLocalReplacement: async (body) => {
@@ -767,26 +1012,26 @@ export function createRemoteSessionProxy(local, agentDir, client, initialState,
767
1012
  const context = local.createReplacedSessionContext();
768
1013
  Object.defineProperty(context, "cwd", { value: state.cwd });
769
1014
  Object.defineProperty(context, "sessionManager", { value: remoteSessionManager });
770
- context.sendMessage = (message, options) => client.sendCustomMessage(message, {
1015
+ context.sendMessage = (message, options) => transportCall("sendMessage", () => client.sendCustomMessage(message, {
771
1016
  triggerTurn: options?.triggerTurn,
772
1017
  deliverAs: options?.deliverAs,
773
- });
1018
+ }));
774
1019
  context.sendUserMessage = (content, options) => {
775
1020
  if (typeof content === "string")
776
- return client.prompt(content, {
1021
+ return transportCall("sendUserMessage", () => client.prompt(content, {
777
1022
  streamingBehavior: options?.deliverAs,
778
1023
  expandPromptTemplates: options?.expandPromptTemplates,
779
- });
1024
+ }));
780
1025
  const text = content
781
1026
  .filter((part) => part.type === "text")
782
1027
  .map((part) => part.text)
783
1028
  .join("\n");
784
1029
  const images = content.filter((part) => part.type === "image");
785
- return client.prompt(text, {
1030
+ return transportCall("sendUserMessage", () => client.prompt(text, {
786
1031
  images,
787
1032
  streamingBehavior: options?.deliverAs,
788
1033
  expandPromptTemplates: options?.expandPromptTemplates,
789
- });
1034
+ }));
790
1035
  };
791
1036
  return context;
792
1037
  },