@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.
- package/CHANGELOG.md +51 -1
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +77 -10
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +1 -0
- package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/context-pipeline.js +6 -2
- package/dist/core/extensions/builtin/compaction/context-pipeline.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +3 -2
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/idle-retry.d.ts +1 -1
- package/dist/core/extensions/builtin/compaction/idle-retry.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/idle-retry.js +1 -1
- package/dist/core/extensions/builtin/compaction/idle-retry.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/index.js +34 -10
- package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts +30 -0
- package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts.map +1 -0
- package/dist/core/extensions/builtin/compaction/model-usability-budget.js +74 -0
- package/dist/core/extensions/builtin/compaction/model-usability-budget.js.map +1 -0
- package/dist/core/extensions/builtin/compaction/orchestration.d.ts +3 -13
- package/dist/core/extensions/builtin/compaction/orchestration.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/orchestration.js +50 -83
- package/dist/core/extensions/builtin/compaction/orchestration.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/policy.d.ts +8 -1
- package/dist/core/extensions/builtin/compaction/policy.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/policy.js +12 -3
- package/dist/core/extensions/builtin/compaction/policy.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/token-budget-reminder.d.ts +8 -2
- package/dist/core/extensions/builtin/compaction/token-budget-reminder.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/token-budget-reminder.js +12 -5
- package/dist/core/extensions/builtin/compaction/token-budget-reminder.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/tool-admission.d.ts +12 -21
- package/dist/core/extensions/builtin/compaction/tool-admission.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/tool-admission.js +27 -63
- package/dist/core/extensions/builtin/compaction/tool-admission.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +11 -0
- package/dist/core/extensions/builtin/terminal/runtime-session.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/runtime-session.js +18 -2
- package/dist/core/extensions/builtin/terminal/runtime-session.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +5 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/interactive-host-runtime.d.ts +13 -2
- package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-host-runtime.js +317 -72
- package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
- package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
- package/dist/modes/rpc/connection-handler.js +17 -4
- package/dist/modes/rpc/connection-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +10 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +32 -10
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/docs/rpc.md +4 -0
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/dist/screen-operations.d.ts +42 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen-operations.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen-operations.js +34 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen-operations.js.map +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen-text.d.ts +8 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen-text.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen-text.js +51 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen-text.js.map +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen.d.ts +45 -0
- package/node_modules/@earendil-works/pi-pty/dist/screen.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/screen.js +178 -55
- package/node_modules/@earendil-works/pi-pty/dist/screen.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
|
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
|
|
114
|
-
|
|
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
|
|
137
|
-
|
|
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
|
|
155
|
-
|
|
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
|
|
169
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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) =>
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
|
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
|
|
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))
|
|
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
|
},
|