@lelouchhe/webagent 0.7.0 → 0.9.0
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/README.md +4 -1
- package/dist/index.html +31 -6
- package/dist/js/app.QC7IRDTP.js +5 -0
- package/dist/js/chunk.3CLGCUHW.js +1 -0
- package/dist/js/chunk.AOTG3PL7.js +20 -0
- package/dist/js/{chunk.3CCHZCG7.js → chunk.S5LRNRJI.js} +42 -41
- package/dist/js/{login.2WA6DTGM.js → login.WMURU4NI.js} +1 -1
- package/dist/js/viewer.GP5VXAUY.js +1 -0
- package/dist/login.html +2 -2
- package/dist/share-viewer.html +4 -4
- package/dist/{styles.01ky3cex.css → styles.00nlhhf3.css} +261 -6
- package/dist/sw.js +19 -9
- package/lib/agent-key.js +6 -0
- package/lib/attachments.js +44 -7
- package/lib/auth-middleware.js +9 -2
- package/lib/bridge.js +164 -33
- package/lib/event-handler.js +91 -18
- package/lib/files/limits.js +15 -0
- package/lib/files/paths.js +155 -0
- package/lib/files/routes.js +232 -0
- package/lib/home-path.js +35 -0
- package/lib/http-status.js +1 -0
- package/lib/message-cleanup.js +5 -4
- package/lib/routes.js +316 -72
- package/lib/server.js +26 -23
- package/lib/session-manager.js +217 -44
- package/lib/session-state.js +99 -5
- package/lib/share/routes.js +12 -0
- package/lib/sse-manager.js +31 -7
- package/lib/store.js +124 -41
- package/lib/title-service.js +3 -3
- package/package.json +2 -1
- package/dist/js/app.KFQAFHA3.js +0 -4
- package/dist/js/chunk.UMQMOGWO.js +0 -1
- package/dist/js/viewer.T32M5AZZ.js +0 -1
package/lib/bridge.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Writable, Readable } from "node:stream";
|
|
|
3
3
|
import { EventEmitter } from "node:events";
|
|
4
4
|
import * as acp from "@agentclientprotocol/sdk";
|
|
5
5
|
import { interruptBashProc } from "./session-manager.js";
|
|
6
|
+
import { abbreviateHomePath } from "./home-path.js";
|
|
6
7
|
import { log } from "./log.js";
|
|
7
8
|
const blog = log.scope("bridge");
|
|
8
9
|
export class AgentBridge extends EventEmitter {
|
|
@@ -12,15 +13,27 @@ export class AgentBridge extends EventEmitter {
|
|
|
12
13
|
permissionRequestSessions = new Map();
|
|
13
14
|
silentSessions = new Set(); // Sessions that don't emit events
|
|
14
15
|
silentBuffers = new Map(); // Text buffers for silent sessions
|
|
16
|
+
pendingNewSessions = 0;
|
|
17
|
+
unboundNewSessionIds = new Set();
|
|
18
|
+
pendingSessionUpdates = new Map();
|
|
15
19
|
pendingAborts = new Map();
|
|
16
20
|
deadReason = null;
|
|
17
21
|
stderrTail = "";
|
|
22
|
+
closedProcesses = new WeakSet();
|
|
18
23
|
agentCmd;
|
|
24
|
+
sessionIds;
|
|
19
25
|
reloading = false;
|
|
20
26
|
attachmentDispatcher = null;
|
|
21
|
-
constructor(agentCmd) {
|
|
27
|
+
constructor(agentCmd, sessionIds) {
|
|
22
28
|
super();
|
|
23
29
|
this.agentCmd = agentCmd;
|
|
30
|
+
this.sessionIds = sessionIds;
|
|
31
|
+
}
|
|
32
|
+
agentSessionId(webSessionId) {
|
|
33
|
+
const id = this.sessionIds.getAgentSessionId(webSessionId);
|
|
34
|
+
if (!id)
|
|
35
|
+
throw new Error(`Session is not available for the current agent: ${webSessionId}`);
|
|
36
|
+
return id;
|
|
24
37
|
}
|
|
25
38
|
/**
|
|
26
39
|
* Inject the dispatcher used to translate client attachment refs into
|
|
@@ -60,6 +73,9 @@ export class AgentBridge extends EventEmitter {
|
|
|
60
73
|
`\nCheck '${this.agentCmd}' is properly configured (e.g. authenticated).`;
|
|
61
74
|
this.markAgentDead(reason);
|
|
62
75
|
});
|
|
76
|
+
proc.once("close", () => {
|
|
77
|
+
this.closedProcesses.add(proc);
|
|
78
|
+
});
|
|
63
79
|
proc.on("error", (err) => {
|
|
64
80
|
if (this.reloading)
|
|
65
81
|
return;
|
|
@@ -95,29 +111,57 @@ export class AgentBridge extends EventEmitter {
|
|
|
95
111
|
async newSession(cwd, opts) {
|
|
96
112
|
if (!this.conn)
|
|
97
113
|
throw new Error("Not connected");
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
const configOptions = (session.configOptions ??
|
|
103
|
-
[]);
|
|
104
|
-
if (!opts?.silent) {
|
|
105
|
-
this.emit("event", {
|
|
106
|
-
type: "session_created",
|
|
107
|
-
sessionId: session.sessionId,
|
|
114
|
+
this.pendingNewSessions++;
|
|
115
|
+
try {
|
|
116
|
+
const session = await this.conn.newSession({
|
|
108
117
|
cwd,
|
|
109
|
-
|
|
118
|
+
mcpServers: [],
|
|
110
119
|
});
|
|
120
|
+
if (opts?.silent) {
|
|
121
|
+
this.pendingSessionUpdates.delete(session.sessionId);
|
|
122
|
+
this.silentSessions.add(session.sessionId);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.unboundNewSessionIds.add(session.sessionId);
|
|
126
|
+
}
|
|
127
|
+
const configOptions = (session.configOptions ??
|
|
128
|
+
[]);
|
|
129
|
+
return { sessionId: session.sessionId, configOptions };
|
|
111
130
|
}
|
|
112
|
-
|
|
131
|
+
finally {
|
|
132
|
+
this.pendingNewSessions--;
|
|
133
|
+
if (this.pendingNewSessions === 0) {
|
|
134
|
+
for (const sessionId of this.pendingSessionUpdates.keys()) {
|
|
135
|
+
if (!this.unboundNewSessionIds.has(sessionId)) {
|
|
136
|
+
this.pendingSessionUpdates.delete(sessionId);
|
|
137
|
+
blog.warn("discarded update for unrelated unmapped ACP session", {
|
|
138
|
+
sessionId,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
sessionMapped(agentSessionId) {
|
|
146
|
+
this.unboundNewSessionIds.delete(agentSessionId);
|
|
147
|
+
const updates = this.pendingSessionUpdates.get(agentSessionId) ?? [];
|
|
148
|
+
this.pendingSessionUpdates.delete(agentSessionId);
|
|
149
|
+
for (const update of updates) {
|
|
150
|
+
void this.handleSessionUpdate({ sessionId: agentSessionId, update });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
discardUnboundSession(agentSessionId) {
|
|
154
|
+
this.unboundNewSessionIds.delete(agentSessionId);
|
|
155
|
+
this.pendingSessionUpdates.delete(agentSessionId);
|
|
113
156
|
}
|
|
114
157
|
async loadSession(sessionId, cwd) {
|
|
115
158
|
if (!this.conn)
|
|
116
159
|
throw new Error("Not connected");
|
|
160
|
+
const agentSessionId = this.agentSessionId(sessionId);
|
|
117
161
|
let session;
|
|
118
162
|
try {
|
|
119
163
|
session = await this.conn.loadSession({
|
|
120
|
-
sessionId,
|
|
164
|
+
sessionId: agentSessionId,
|
|
121
165
|
cwd,
|
|
122
166
|
mcpServers: [],
|
|
123
167
|
});
|
|
@@ -140,6 +184,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
140
184
|
type: "session_created",
|
|
141
185
|
sessionId,
|
|
142
186
|
cwd,
|
|
187
|
+
cwdDisplay: abbreviateHomePath(cwd),
|
|
143
188
|
configOptions,
|
|
144
189
|
});
|
|
145
190
|
return { sessionId, configOptions };
|
|
@@ -148,7 +193,19 @@ export class AgentBridge extends EventEmitter {
|
|
|
148
193
|
if (!this.conn)
|
|
149
194
|
throw new Error("Not connected");
|
|
150
195
|
const result = await this.conn.setSessionConfigOption({
|
|
151
|
-
sessionId,
|
|
196
|
+
sessionId: this.agentSessionId(sessionId),
|
|
197
|
+
configId,
|
|
198
|
+
...(typeof value === "boolean"
|
|
199
|
+
? { type: "boolean", value }
|
|
200
|
+
: { value }),
|
|
201
|
+
});
|
|
202
|
+
return result.configOptions;
|
|
203
|
+
}
|
|
204
|
+
async setAgentConfigOption(agentSessionId, configId, value) {
|
|
205
|
+
if (!this.conn)
|
|
206
|
+
throw new Error("Not connected");
|
|
207
|
+
const result = await this.conn.setSessionConfigOption({
|
|
208
|
+
sessionId: agentSessionId,
|
|
152
209
|
configId,
|
|
153
210
|
...(typeof value === "boolean"
|
|
154
211
|
? { type: "boolean", value }
|
|
@@ -156,7 +213,10 @@ export class AgentBridge extends EventEmitter {
|
|
|
156
213
|
});
|
|
157
214
|
return result.configOptions;
|
|
158
215
|
}
|
|
159
|
-
async prompt(sessionId, text, attachments
|
|
216
|
+
async prompt(sessionId, text, attachments,
|
|
217
|
+
/** Turn identity echoed back on this prompt's terminal event, so a
|
|
218
|
+
* completion that outlives its turn can be told apart from the live one. */
|
|
219
|
+
promptId) {
|
|
160
220
|
if (this.deadReason) {
|
|
161
221
|
this.emit("event", {
|
|
162
222
|
type: "error",
|
|
@@ -189,7 +249,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
189
249
|
promptParts.push({ type: "text", text });
|
|
190
250
|
const result = (await Promise.race([
|
|
191
251
|
this.conn.prompt({
|
|
192
|
-
sessionId,
|
|
252
|
+
sessionId: this.agentSessionId(sessionId),
|
|
193
253
|
prompt: promptParts,
|
|
194
254
|
}),
|
|
195
255
|
abortPromise,
|
|
@@ -198,6 +258,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
198
258
|
type: "prompt_done",
|
|
199
259
|
sessionId,
|
|
200
260
|
stopReason: result.stopReason ?? "end_turn",
|
|
261
|
+
...(promptId ? { promptId } : {}),
|
|
201
262
|
});
|
|
202
263
|
}
|
|
203
264
|
catch (err) {
|
|
@@ -211,6 +272,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
211
272
|
type: "prompt_done",
|
|
212
273
|
sessionId,
|
|
213
274
|
stopReason: "cancelled",
|
|
275
|
+
...(promptId ? { promptId } : {}),
|
|
214
276
|
});
|
|
215
277
|
return;
|
|
216
278
|
}
|
|
@@ -218,6 +280,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
218
280
|
type: "error",
|
|
219
281
|
sessionId,
|
|
220
282
|
message,
|
|
283
|
+
...(promptId ? { promptId } : {}),
|
|
221
284
|
});
|
|
222
285
|
}
|
|
223
286
|
finally {
|
|
@@ -231,7 +294,10 @@ export class AgentBridge extends EventEmitter {
|
|
|
231
294
|
this.denyPermission(requestId);
|
|
232
295
|
}
|
|
233
296
|
}
|
|
234
|
-
await this.conn?.cancel({ sessionId });
|
|
297
|
+
await this.conn?.cancel({ sessionId: this.agentSessionId(sessionId) });
|
|
298
|
+
}
|
|
299
|
+
async cancelAgentSession(agentSessionId) {
|
|
300
|
+
await this.conn?.cancel({ sessionId: agentSessionId });
|
|
235
301
|
}
|
|
236
302
|
/**
|
|
237
303
|
* Mark the agent subprocess as dead. Rejects in-flight prompts and emits
|
|
@@ -324,6 +390,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
324
390
|
if (this.reloading)
|
|
325
391
|
throw new Error("Already reloading");
|
|
326
392
|
this.reloading = true;
|
|
393
|
+
const liveSessionIds = [...sessions.liveSessions];
|
|
327
394
|
this.emit("event", { type: "agent_reloading" });
|
|
328
395
|
blog.info("reloading agent...");
|
|
329
396
|
try {
|
|
@@ -342,18 +409,29 @@ export class AgentBridge extends EventEmitter {
|
|
|
342
409
|
}
|
|
343
410
|
}
|
|
344
411
|
// 2. Flush buffers to persist partial content
|
|
345
|
-
for (const sessionId of
|
|
412
|
+
for (const sessionId of liveSessionIds) {
|
|
346
413
|
sessions.flushBuffers(sessionId);
|
|
347
414
|
}
|
|
348
415
|
// 3. Clean up SessionManager state
|
|
349
416
|
sessions.pendingPermissions.clear();
|
|
350
|
-
|
|
417
|
+
sessions.state.clearPlans();
|
|
418
|
+
const busySessionIds = new Set([
|
|
419
|
+
...sessions.activePrompts,
|
|
420
|
+
...sessions.pendingPromptSubmissions.keys(),
|
|
421
|
+
]);
|
|
422
|
+
for (const id of busySessionIds) {
|
|
351
423
|
sessions.state.patch(id, { runtime: { busy: null } });
|
|
352
424
|
}
|
|
425
|
+
for (const submissionId of sessions.pendingPromptSubmissions.values()) {
|
|
426
|
+
sessions.cancelledPromptSubmissions.add(submissionId);
|
|
427
|
+
}
|
|
353
428
|
sessions.activePrompts.clear();
|
|
429
|
+
sessions.pendingPromptSubmissions.clear();
|
|
354
430
|
// 4. Clean up bridge-side silent session state
|
|
355
431
|
this.silentSessions.clear();
|
|
356
432
|
this.silentBuffers.clear();
|
|
433
|
+
this.unboundNewSessionIds.clear();
|
|
434
|
+
this.pendingSessionUpdates.clear();
|
|
357
435
|
// 5. Invalidate title service session
|
|
358
436
|
titleService.invalidate();
|
|
359
437
|
// 5. Clear liveSessions so ensureResumed() will re-register on next access
|
|
@@ -364,6 +442,13 @@ export class AgentBridge extends EventEmitter {
|
|
|
364
442
|
sessions.cachedConfigOptions = [];
|
|
365
443
|
// 6. Shutdown old process
|
|
366
444
|
await this.shutdown();
|
|
445
|
+
// Cancellation is asynchronous: the old agent may emit final chunks
|
|
446
|
+
// before shutdown completes. Persist that tail and make the terminal
|
|
447
|
+
// stream state authoritative before starting the replacement process.
|
|
448
|
+
for (const sessionId of liveSessionIds) {
|
|
449
|
+
sessions.flushBuffers(sessionId);
|
|
450
|
+
}
|
|
451
|
+
sessions.state.clearStreaming();
|
|
367
452
|
// 7. Start new process with retry (exponential backoff, max 3 attempts)
|
|
368
453
|
let lastError;
|
|
369
454
|
for (let i = 0; i < 3; i++) {
|
|
@@ -400,17 +485,28 @@ export class AgentBridge extends EventEmitter {
|
|
|
400
485
|
}
|
|
401
486
|
this.permissionResolvers.clear();
|
|
402
487
|
this.permissionRequestSessions.clear();
|
|
403
|
-
|
|
404
|
-
|
|
488
|
+
const proc = this.proc;
|
|
489
|
+
if (proc && !this.closedProcesses.has(proc)) {
|
|
405
490
|
await new Promise((resolve) => {
|
|
406
|
-
|
|
407
|
-
|
|
491
|
+
let settled = false;
|
|
492
|
+
const finish = () => {
|
|
493
|
+
if (settled)
|
|
494
|
+
return;
|
|
495
|
+
settled = true;
|
|
496
|
+
this.closedProcesses.add(proc);
|
|
497
|
+
clearTimeout(killTimer);
|
|
498
|
+
clearTimeout(drainTimer);
|
|
499
|
+
proc.off("close", finish);
|
|
408
500
|
resolve();
|
|
501
|
+
};
|
|
502
|
+
const killTimer = setTimeout(() => {
|
|
503
|
+
proc.kill(process.platform === "win32" ? undefined : "SIGKILL");
|
|
409
504
|
}, 5000);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
505
|
+
// `close` follows `exit` after stdio has drained. Keep a bounded
|
|
506
|
+
// fallback for pathological child-process implementations that never
|
|
507
|
+
// deliver close even after SIGKILL.
|
|
508
|
+
const drainTimer = setTimeout(finish, 6000);
|
|
509
|
+
proc.once("close", finish);
|
|
414
510
|
proc.kill();
|
|
415
511
|
});
|
|
416
512
|
}
|
|
@@ -419,6 +515,10 @@ export class AgentBridge extends EventEmitter {
|
|
|
419
515
|
}
|
|
420
516
|
// --- ACP Client callbacks ---
|
|
421
517
|
handlePermission(params) {
|
|
518
|
+
const webSessionId = this.sessionIds.getWebSessionId(params.sessionId);
|
|
519
|
+
if (!webSessionId) {
|
|
520
|
+
return Promise.resolve({ outcome: { outcome: "cancelled" } });
|
|
521
|
+
}
|
|
422
522
|
const requestId = crypto.randomUUID();
|
|
423
523
|
const toolCall = params.toolCall;
|
|
424
524
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- toolCall may be undefined in practice
|
|
@@ -429,11 +529,11 @@ export class AgentBridge extends EventEmitter {
|
|
|
429
529
|
return new Promise((resolve) => {
|
|
430
530
|
// Register resolver BEFORE emitting, so synchronous auto-approve can find it
|
|
431
531
|
this.permissionResolvers.set(requestId, resolve);
|
|
432
|
-
this.permissionRequestSessions.set(requestId,
|
|
532
|
+
this.permissionRequestSessions.set(requestId, webSessionId);
|
|
433
533
|
this.emit("event", {
|
|
434
534
|
type: "permission_request",
|
|
435
535
|
requestId,
|
|
436
|
-
sessionId:
|
|
536
|
+
sessionId: webSessionId,
|
|
437
537
|
title,
|
|
438
538
|
toolCallId,
|
|
439
539
|
options: params.options,
|
|
@@ -446,9 +546,23 @@ export class AgentBridge extends EventEmitter {
|
|
|
446
546
|
}
|
|
447
547
|
handleSessionUpdate(params) {
|
|
448
548
|
const update = params.update;
|
|
449
|
-
const
|
|
450
|
-
if (this.silentSessions.has(
|
|
451
|
-
this.captureSilentText(
|
|
549
|
+
const agentSessionId = params.sessionId;
|
|
550
|
+
if (this.silentSessions.has(agentSessionId)) {
|
|
551
|
+
this.captureSilentText(agentSessionId, update);
|
|
552
|
+
return Promise.resolve();
|
|
553
|
+
}
|
|
554
|
+
const sessionId = this.sessionIds.getWebSessionId(agentSessionId);
|
|
555
|
+
if (!sessionId) {
|
|
556
|
+
if (this.pendingNewSessions > 0 ||
|
|
557
|
+
this.unboundNewSessionIds.has(agentSessionId)) {
|
|
558
|
+
const updates = this.pendingSessionUpdates.get(agentSessionId) ?? [];
|
|
559
|
+
updates.push(update);
|
|
560
|
+
this.pendingSessionUpdates.set(agentSessionId, updates);
|
|
561
|
+
return Promise.resolve();
|
|
562
|
+
}
|
|
563
|
+
blog.warn("ignored event for unmapped ACP session", {
|
|
564
|
+
sessionId: agentSessionId,
|
|
565
|
+
});
|
|
452
566
|
return Promise.resolve();
|
|
453
567
|
}
|
|
454
568
|
const event = this.sessionUpdateToEvent(sessionId, update);
|
|
@@ -490,9 +604,26 @@ export class AgentBridge extends EventEmitter {
|
|
|
490
604
|
id: update.toolCallId,
|
|
491
605
|
status: update.status ?? "",
|
|
492
606
|
content: (update.content ?? undefined),
|
|
607
|
+
...(typeof update.title === "string" ? { title: update.title } : {}),
|
|
608
|
+
...(typeof update.kind === "string" ? { kind: update.kind } : {}),
|
|
609
|
+
...(update.rawInput ? { rawInput: update.rawInput } : {}),
|
|
610
|
+
...(Object.hasOwn(update, "rawOutput")
|
|
611
|
+
? { rawOutput: update.rawOutput }
|
|
612
|
+
: {}),
|
|
613
|
+
...(Array.isArray(update.locations)
|
|
614
|
+
? { locations: update.locations }
|
|
615
|
+
: {}),
|
|
493
616
|
};
|
|
494
617
|
case "plan":
|
|
495
618
|
return { type: "plan", sessionId, entries: update.entries };
|
|
619
|
+
case "usage_update":
|
|
620
|
+
return {
|
|
621
|
+
type: "usage_update",
|
|
622
|
+
sessionId,
|
|
623
|
+
used: update.used,
|
|
624
|
+
size: update.size,
|
|
625
|
+
cost: update.cost,
|
|
626
|
+
};
|
|
496
627
|
case "config_option_update":
|
|
497
628
|
return {
|
|
498
629
|
type: "config_option_update",
|
package/lib/event-handler.js
CHANGED
|
@@ -3,6 +3,7 @@ import { log } from "./log.js";
|
|
|
3
3
|
import { isAutopilotMode } from "./mode-bucket.js";
|
|
4
4
|
const ailog = log.scope("attachment-interceptor");
|
|
5
5
|
const plog = log.scope("push");
|
|
6
|
+
const clog = log.scope("cancel");
|
|
6
7
|
function handleConnected(event, sessions, config) {
|
|
7
8
|
event.cancelTimeout = config.cancelTimeout;
|
|
8
9
|
event.recentPathsLimit = config.recentPathsLimit;
|
|
@@ -10,14 +11,8 @@ function handleConnected(event, sessions, config) {
|
|
|
10
11
|
if (event.agent)
|
|
11
12
|
sessions.agentInfo = event.agent;
|
|
12
13
|
}
|
|
13
|
-
function handleConfigLikeEvent(event, sessions
|
|
14
|
-
|
|
15
|
-
sessions.cachedConfigOptions = event.configOptions;
|
|
16
|
-
for (const opt of event.configOptions) {
|
|
17
|
-
if (typeof opt.currentValue === "string") {
|
|
18
|
-
store.updateSessionConfig(event.sessionId, opt.id, opt.currentValue);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
14
|
+
function handleConfigLikeEvent(event, sessions) {
|
|
15
|
+
sessions.recordConfigOptions(event.sessionId, event.configOptions);
|
|
21
16
|
}
|
|
22
17
|
function handleMessageChunk(event, sessions) {
|
|
23
18
|
sessions.flushThinkingBuffer(event.sessionId);
|
|
@@ -45,10 +40,27 @@ function handleToolCall(event, sessions, store) {
|
|
|
45
40
|
rawInput: event.rawInput,
|
|
46
41
|
}, { from_ref: "agent" });
|
|
47
42
|
}
|
|
43
|
+
function handleUsageUpdate(event, sessions) {
|
|
44
|
+
sessions.state.patch(event.sessionId, {
|
|
45
|
+
runtime: {
|
|
46
|
+
contextUsage: {
|
|
47
|
+
used: event.used,
|
|
48
|
+
size: event.size,
|
|
49
|
+
...(event.cost !== undefined ? { cost: event.cost } : {}),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
48
54
|
function handlePlan(event, sessions, store) {
|
|
49
55
|
sessions.flushBuffers(event.sessionId);
|
|
50
56
|
sessions.state.patch(event.sessionId, {
|
|
51
|
-
runtime: {
|
|
57
|
+
runtime: {
|
|
58
|
+
streaming: { assistant: false, thinking: false },
|
|
59
|
+
plan: event.entries.length > 0 &&
|
|
60
|
+
!event.entries.every((entry) => entry.status === "completed")
|
|
61
|
+
? event.entries
|
|
62
|
+
: null,
|
|
63
|
+
},
|
|
52
64
|
});
|
|
53
65
|
store.saveEvent(event.sessionId, event.type, { entries: event.entries }, { from_ref: "agent" });
|
|
54
66
|
}
|
|
@@ -144,18 +156,61 @@ function handlePermissionRequest(event, sessions, store, bridge, sseManager, con
|
|
|
144
156
|
return false;
|
|
145
157
|
}
|
|
146
158
|
function handlePromptDone(event, sessions, store) {
|
|
147
|
-
sessions.
|
|
148
|
-
|
|
159
|
+
const cancelStatus = sessions.state.getState(event.sessionId).runtime.busy?.cancelStatus ?? null;
|
|
160
|
+
if (cancelStatus !== null) {
|
|
161
|
+
clog.info("agent completed after request", {
|
|
162
|
+
sessionId: event.sessionId.slice(0, 8),
|
|
163
|
+
requestedStatus: cancelStatus,
|
|
164
|
+
stopReason: event.stopReason,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// The tail this turn buffered must always land, even when the turn has
|
|
168
|
+
// already been superseded — it is the only copy of that text.
|
|
169
|
+
const isCurrent = sessions.isCurrentPrompt(event.sessionId, event.promptId);
|
|
170
|
+
if (isCurrent) {
|
|
171
|
+
sessions.activePrompts.delete(event.sessionId);
|
|
172
|
+
sessions.syncBusy(event.sessionId);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
clog.info("completion from a superseded turn", {
|
|
176
|
+
sessionId: event.sessionId.slice(0, 8),
|
|
177
|
+
promptId: event.promptId,
|
|
178
|
+
stopReason: event.stopReason,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
149
181
|
sessions.flushBuffers(event.sessionId);
|
|
150
182
|
sessions.state.patch(event.sessionId, {
|
|
151
183
|
runtime: { streaming: { assistant: false, thinking: false } },
|
|
152
184
|
});
|
|
153
|
-
store.saveEvent(event.sessionId, event.type, {
|
|
185
|
+
store.saveEvent(event.sessionId, event.type, {
|
|
186
|
+
stopReason: event.stopReason,
|
|
187
|
+
// Replay must be able to make the same judgement the live path does.
|
|
188
|
+
...(event.promptId ? { promptId: event.promptId } : {}),
|
|
189
|
+
}, { from_ref: "agent" });
|
|
154
190
|
}
|
|
155
|
-
function handleError(event, sessions) {
|
|
191
|
+
function handleError(event, sessions, store) {
|
|
156
192
|
if (event.sessionId) {
|
|
157
|
-
|
|
158
|
-
|
|
193
|
+
// Same attribution as a completion: a superseded turn failing late must
|
|
194
|
+
// not end the turn that replaced it. The buffered tail still flushes.
|
|
195
|
+
if (sessions.isCurrentPrompt(event.sessionId, event.promptId)) {
|
|
196
|
+
sessions.activePrompts.delete(event.sessionId);
|
|
197
|
+
sessions.syncBusy(event.sessionId);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
clog.info("failure from a superseded turn", {
|
|
201
|
+
sessionId: event.sessionId.slice(0, 8),
|
|
202
|
+
promptId: event.promptId,
|
|
203
|
+
message: event.message,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
sessions.flushBuffers(event.sessionId);
|
|
207
|
+
sessions.state.patch(event.sessionId, {
|
|
208
|
+
runtime: { streaming: { assistant: false, thinking: false } },
|
|
209
|
+
});
|
|
210
|
+
store.saveEvent(event.sessionId, event.type, {
|
|
211
|
+
message: event.message,
|
|
212
|
+
...(event.promptId ? { promptId: event.promptId } : {}),
|
|
213
|
+
}, { from_ref: "agent" });
|
|
159
214
|
}
|
|
160
215
|
}
|
|
161
216
|
function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager) {
|
|
@@ -166,7 +221,7 @@ function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager)
|
|
|
166
221
|
return false;
|
|
167
222
|
case "session_created":
|
|
168
223
|
case "config_option_update":
|
|
169
|
-
handleConfigLikeEvent(event, sessions
|
|
224
|
+
handleConfigLikeEvent(event, sessions);
|
|
170
225
|
return false;
|
|
171
226
|
case "message_chunk":
|
|
172
227
|
handleMessageChunk(event, sessions);
|
|
@@ -178,7 +233,15 @@ function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager)
|
|
|
178
233
|
handleToolCall(event, sessions, store);
|
|
179
234
|
return false;
|
|
180
235
|
case "tool_call_update":
|
|
181
|
-
store.saveEvent(event.sessionId, event.type, {
|
|
236
|
+
store.saveEvent(event.sessionId, event.type, {
|
|
237
|
+
id: event.id,
|
|
238
|
+
status: event.status,
|
|
239
|
+
content: event.content,
|
|
240
|
+
title: event.title,
|
|
241
|
+
kind: event.kind,
|
|
242
|
+
rawInput: event.rawInput,
|
|
243
|
+
locations: event.locations,
|
|
244
|
+
}, { from_ref: "agent" });
|
|
182
245
|
return false;
|
|
183
246
|
case "plan":
|
|
184
247
|
handlePlan(event, sessions, store);
|
|
@@ -189,7 +252,7 @@ function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager)
|
|
|
189
252
|
handlePromptDone(event, sessions, store);
|
|
190
253
|
return false;
|
|
191
254
|
case "error":
|
|
192
|
-
handleError(event, sessions);
|
|
255
|
+
handleError(event, sessions, store);
|
|
193
256
|
return false;
|
|
194
257
|
}
|
|
195
258
|
return false;
|
|
@@ -213,6 +276,10 @@ function maybePushNotify(event, pushService) {
|
|
|
213
276
|
});
|
|
214
277
|
}
|
|
215
278
|
export function handleAgentEvent(event, sessions, store, bridge, config, sseManager, pushService, _clientRegistry) {
|
|
279
|
+
if (event.type === "usage_update") {
|
|
280
|
+
handleUsageUpdate(event, sessions);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
216
283
|
if (event.type === "available_commands_update") {
|
|
217
284
|
const snapshot = sessions.updateAgentCommands(event.sessionId, event.commands);
|
|
218
285
|
if (sessions.restoringSessions.has(event.sessionId))
|
|
@@ -221,6 +288,12 @@ export function handleAgentEvent(event, sessions, store, bridge, config, sseMana
|
|
|
221
288
|
return;
|
|
222
289
|
}
|
|
223
290
|
if (event.type === "agent_reloading" || event.type === "agent_disconnected") {
|
|
291
|
+
for (const sessionId of sessions.liveSessions) {
|
|
292
|
+
sessions.flushBuffers(sessionId);
|
|
293
|
+
}
|
|
294
|
+
sessions.state.clearStreaming();
|
|
295
|
+
sessions.state.clearPlans();
|
|
296
|
+
sessions.state.clearContextUsage();
|
|
224
297
|
for (const snapshot of sessions.clearAgentCommands()) {
|
|
225
298
|
sseManager.broadcast({
|
|
226
299
|
type: "available_commands_update",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File viewer size limits.
|
|
3
|
+
*
|
|
4
|
+
* Kept as plain exported constants for now so the routes have a single,
|
|
5
|
+
* testable knob; wiring these into `[limits]` config is a later milestone.
|
|
6
|
+
* Values stay in the same order of magnitude as attachment limits and keep
|
|
7
|
+
* rendered previews bounded. Files outside preview limits stream as downloads
|
|
8
|
+
* instead of being buffered in server or browser memory.
|
|
9
|
+
*/
|
|
10
|
+
/** Directory listings cap — beyond this the response is truncated + flagged. */
|
|
11
|
+
export const MAX_LIST_ITEMS = 2000;
|
|
12
|
+
/** Text/Markdown/code preview + highlight cap; larger files download. */
|
|
13
|
+
export const MAX_TEXT_PREVIEW_BYTES = 1024 * 1024; // 1 MiB
|
|
14
|
+
/** Image render cap — larger images stream as downloads. */
|
|
15
|
+
export const MAX_IMAGE_BYTES = 20 * 1024 * 1024; // 20 MB
|