@lelouchhe/webagent 0.9.0 → 0.10.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.
Files changed (47) hide show
  1. package/README.md +39 -14
  2. package/config.toml +7 -27
  3. package/dist/index.html +4 -4
  4. package/dist/js/app.INIQQEGD.js +5 -0
  5. package/dist/js/{chunk.S5LRNRJI.js → chunk.7WADDFJZ.js} +27 -27
  6. package/dist/js/viewer.RHZMFYWJ.js +1 -0
  7. package/dist/login.html +1 -1
  8. package/dist/share-viewer.html +5 -5
  9. package/dist/{styles.00nlhhf3.css → styles.01aj0l37.css} +19 -2
  10. package/dist/sw.js +6 -6
  11. package/lib/attachment-dispatch.js +60 -31
  12. package/lib/attachment-interceptor.js +7 -7
  13. package/lib/attachment-labels.js +1 -1
  14. package/lib/attachments.js +25 -0
  15. package/lib/auth-middleware.js +2 -2
  16. package/lib/auth.js +2 -2
  17. package/lib/bridge.js +109 -83
  18. package/lib/client-registry.js +12 -12
  19. package/lib/config.js +2 -31
  20. package/lib/event-handler.js +143 -90
  21. package/lib/files/routes.js +1 -1
  22. package/lib/mcp/capability.js +74 -0
  23. package/lib/mcp/server.js +148 -0
  24. package/lib/mcp/task-history.js +245 -0
  25. package/lib/mcp/task-host.js +253 -0
  26. package/lib/mcp/tools.js +168 -0
  27. package/lib/mode-bucket.js +1 -1
  28. package/lib/push-service.js +33 -35
  29. package/lib/routes.js +947 -489
  30. package/lib/server.js +64 -16
  31. package/lib/share/routes.js +88 -88
  32. package/lib/shared/task-reference.js +20 -0
  33. package/lib/sse-manager.js +8 -8
  34. package/lib/store.js +941 -314
  35. package/lib/task-collaboration.js +15 -0
  36. package/lib/task-manager.js +1409 -0
  37. package/lib/task-path.js +131 -0
  38. package/lib/{session-state.js → task-state.js} +64 -41
  39. package/lib/task-tree-lock.js +74 -0
  40. package/lib/{sessions-anchor.js → tasks-anchor.js} +8 -7
  41. package/lib/tokens.js +1 -1
  42. package/lib/types.js +2 -2
  43. package/package.json +7 -1
  44. package/dist/js/app.QC7IRDTP.js +0 -5
  45. package/dist/js/viewer.GP5VXAUY.js +0 -1
  46. package/lib/session-manager.js +0 -638
  47. package/lib/title-service.js +0 -95
package/lib/bridge.js CHANGED
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { Writable, Readable } from "node:stream";
3
3
  import { EventEmitter } from "node:events";
4
4
  import * as acp from "@agentclientprotocol/sdk";
5
- import { interruptBashProc } from "./session-manager.js";
5
+ import { interruptBashProc } from "./task-manager.js";
6
6
  import { abbreviateHomePath } from "./home-path.js";
7
7
  import { log } from "./log.js";
8
8
  const blog = log.scope("bridge");
@@ -10,14 +10,16 @@ export class AgentBridge extends EventEmitter {
10
10
  proc = null;
11
11
  conn = null;
12
12
  permissionResolvers = new Map();
13
- permissionRequestSessions = new Map();
13
+ permissionRequestTasks = new Map();
14
14
  silentSessions = new Set(); // Sessions that don't emit events
15
- silentBuffers = new Map(); // Text buffers for silent sessions
15
+ silentBuffers = new Map(); // Text buffers for silent tasks
16
16
  pendingNewSessions = 0;
17
17
  unboundNewSessionIds = new Set();
18
18
  pendingSessionUpdates = new Map();
19
19
  pendingAborts = new Map();
20
20
  deadReason = null;
21
+ /** Capabilities advertised by the agent at initialize; gates retire calls. */
22
+ sessionCapabilities = null;
21
23
  stderrTail = "";
22
24
  closedProcesses = new WeakSet();
23
25
  agentCmd;
@@ -29,10 +31,10 @@ export class AgentBridge extends EventEmitter {
29
31
  this.agentCmd = agentCmd;
30
32
  this.sessionIds = sessionIds;
31
33
  }
32
- agentSessionId(webSessionId) {
33
- const id = this.sessionIds.getAgentSessionId(webSessionId);
34
+ agentSessionId(taskId) {
35
+ const id = this.sessionIds.getAgentSessionId(taskId);
34
36
  if (!id)
35
- throw new Error(`Session is not available for the current agent: ${webSessionId}`);
37
+ throw new Error(`Task is not available for the current agent: ${taskId}`);
36
38
  return id;
37
39
  }
38
40
  /**
@@ -99,6 +101,7 @@ export class AgentBridge extends EventEmitter {
99
101
  },
100
102
  }));
101
103
  const agentInfo = init.agentInfo;
104
+ this.sessionCapabilities = agentInfo?.sessionCapabilities ?? null;
102
105
  this.emit("event", {
103
106
  type: "connected",
104
107
  agent: {
@@ -115,7 +118,7 @@ export class AgentBridge extends EventEmitter {
115
118
  try {
116
119
  const session = await this.conn.newSession({
117
120
  cwd,
118
- mcpServers: [],
121
+ mcpServers: opts?.mcpServers ?? [],
119
122
  });
120
123
  if (opts?.silent) {
121
124
  this.pendingSessionUpdates.delete(session.sessionId);
@@ -154,26 +157,52 @@ export class AgentBridge extends EventEmitter {
154
157
  this.unboundNewSessionIds.delete(agentSessionId);
155
158
  this.pendingSessionUpdates.delete(agentSessionId);
156
159
  }
157
- async loadSession(sessionId, cwd) {
160
+ /**
161
+ * Explicitly retire an ACP execution whose WebAgent binding has been
162
+ * rotated away or deleted. Best-effort: prefers `session/delete` when the
163
+ * agent advertises it, falls back to `session/close`, and skips silently
164
+ * when the agent supports neither. Failures are logged but never thrown,
165
+ * so retirement can never roll back an already-successful rotation.
166
+ */
167
+ async retireExecution(agentSessionId) {
168
+ if (!this.conn)
169
+ return;
170
+ const params = { sessionId: agentSessionId };
171
+ try {
172
+ if (this.sessionCapabilities?.delete) {
173
+ await this.conn.deleteSession(params);
174
+ }
175
+ else if (this.sessionCapabilities?.close) {
176
+ await this.conn.closeSession(params);
177
+ }
178
+ }
179
+ catch (err) {
180
+ blog.warn("failed to retire retired ACP session", {
181
+ agentSessionId,
182
+ error: err instanceof Error ? err.message : String(err),
183
+ });
184
+ }
185
+ }
186
+ async loadSession(taskId, cwd, mcpServers) {
158
187
  if (!this.conn)
159
188
  throw new Error("Not connected");
160
- const agentSessionId = this.agentSessionId(sessionId);
189
+ const agentSessionId = this.agentSessionId(taskId);
161
190
  let session;
162
191
  try {
163
192
  session = await this.conn.loadSession({
164
193
  sessionId: agentSessionId,
165
194
  cwd,
166
- mcpServers: [],
195
+ mcpServers: mcpServers ?? [],
167
196
  });
168
197
  }
169
198
  catch (err) {
170
199
  // -32002 = Resource not found. Some agents (e.g. claude-agent-acp) don't
171
- // persist sessions across process restarts, so a session in our DB may
200
+ // persist tasks across process restarts, so a session in our DB may
172
201
  // be unknown to the live agent. Translate the JSON-RPC error into a
173
202
  // user-actionable message; routes returns it as 500 / SSE 'error' event.
174
203
  const code = err.code;
175
204
  if (code === -32002) {
176
- throw new Error(`The agent no longer remembers session ${sessionId.slice(0, 8)}… ` +
205
+ throw new Error(`The agent no longer remembers task ${taskId.slice(0, 8)}… ` +
177
206
  `(it may not persist sessions across restarts). Use /new to start a fresh one.`, { cause: err });
178
207
  }
179
208
  throw err;
@@ -181,19 +210,19 @@ export class AgentBridge extends EventEmitter {
181
210
  const configOptions = (session.configOptions ??
182
211
  []);
183
212
  this.emit("event", {
184
- type: "session_created",
185
- sessionId,
213
+ type: "task_created",
214
+ taskId,
186
215
  cwd,
187
216
  cwdDisplay: abbreviateHomePath(cwd),
188
217
  configOptions,
189
218
  });
190
- return { sessionId, configOptions };
219
+ return { taskId, configOptions };
191
220
  }
192
- async setConfigOption(sessionId, configId, value) {
221
+ async setConfigOption(taskId, configId, value) {
193
222
  if (!this.conn)
194
223
  throw new Error("Not connected");
195
224
  const result = await this.conn.setSessionConfigOption({
196
- sessionId: this.agentSessionId(sessionId),
225
+ sessionId: this.agentSessionId(taskId),
197
226
  configId,
198
227
  ...(typeof value === "boolean"
199
228
  ? { type: "boolean", value }
@@ -213,14 +242,14 @@ export class AgentBridge extends EventEmitter {
213
242
  });
214
243
  return result.configOptions;
215
244
  }
216
- async prompt(sessionId, text, attachments,
245
+ async prompt(taskId, text, attachments,
217
246
  /** Turn identity echoed back on this prompt's terminal event, so a
218
247
  * completion that outlives its turn can be told apart from the live one. */
219
248
  promptId) {
220
249
  if (this.deadReason) {
221
250
  this.emit("event", {
222
251
  type: "error",
223
- sessionId,
252
+ taskId,
224
253
  message: this.deadReason,
225
254
  });
226
255
  return;
@@ -231,7 +260,7 @@ export class AgentBridge extends EventEmitter {
231
260
  const abortPromise = new Promise((_, rej) => {
232
261
  abortReject = rej;
233
262
  });
234
- this.pendingAborts.set(sessionId, abortReject);
263
+ this.pendingAborts.set(taskId, abortReject);
235
264
  try {
236
265
  const promptParts = [];
237
266
  if (attachments && attachments.length > 0) {
@@ -242,21 +271,21 @@ export class AgentBridge extends EventEmitter {
242
271
  throw new Error("attachment dispatcher not configured");
243
272
  }
244
273
  for (const ref of attachments) {
245
- const block = await this.attachmentDispatcher.dispatch(sessionId, ref);
246
- promptParts.push(block);
274
+ const blocks = await this.attachmentDispatcher.dispatch(taskId, ref);
275
+ promptParts.push(...blocks);
247
276
  }
248
277
  }
249
278
  promptParts.push({ type: "text", text });
250
279
  const result = (await Promise.race([
251
280
  this.conn.prompt({
252
- sessionId: this.agentSessionId(sessionId),
281
+ sessionId: this.agentSessionId(taskId),
253
282
  prompt: promptParts,
254
283
  }),
255
284
  abortPromise,
256
285
  ]));
257
286
  this.emit("event", {
258
287
  type: "prompt_done",
259
- sessionId,
288
+ taskId,
260
289
  stopReason: result.stopReason ?? "end_turn",
261
290
  ...(promptId ? { promptId } : {}),
262
291
  });
@@ -270,7 +299,7 @@ export class AgentBridge extends EventEmitter {
270
299
  if (/cancel/i.test(message)) {
271
300
  this.emit("event", {
272
301
  type: "prompt_done",
273
- sessionId,
302
+ taskId,
274
303
  stopReason: "cancelled",
275
304
  ...(promptId ? { promptId } : {}),
276
305
  });
@@ -278,23 +307,22 @@ export class AgentBridge extends EventEmitter {
278
307
  }
279
308
  this.emit("event", {
280
309
  type: "error",
281
- sessionId,
310
+ taskId,
282
311
  message,
283
312
  ...(promptId ? { promptId } : {}),
284
313
  });
285
314
  }
286
315
  finally {
287
- this.pendingAborts.delete(sessionId);
316
+ this.pendingAborts.delete(taskId);
288
317
  }
289
318
  }
290
- async cancel(sessionId) {
291
- for (const [requestId, requestSessionId] of this
292
- .permissionRequestSessions) {
293
- if (requestSessionId === sessionId) {
319
+ async cancel(taskId) {
320
+ for (const [requestId, requestTaskId] of this.permissionRequestTasks) {
321
+ if (requestTaskId === taskId) {
294
322
  this.denyPermission(requestId);
295
323
  }
296
324
  }
297
- await this.conn?.cancel({ sessionId: this.agentSessionId(sessionId) });
325
+ await this.conn?.cancel({ sessionId: this.agentSessionId(taskId) });
298
326
  }
299
327
  async cancelAgentSession(agentSessionId) {
300
328
  await this.conn?.cancel({ sessionId: agentSessionId });
@@ -317,10 +345,10 @@ export class AgentBridge extends EventEmitter {
317
345
  blog.error("agent subprocess dead", { reason });
318
346
  const aborts = [...this.pendingAborts.entries()];
319
347
  this.pendingAborts.clear();
320
- for (const [sessionId, abort] of aborts) {
348
+ for (const [taskId, abort] of aborts) {
321
349
  this.emit("event", {
322
350
  type: "error",
323
- sessionId,
351
+ taskId,
324
352
  message: reason,
325
353
  });
326
354
  abort(new Error(reason));
@@ -370,7 +398,7 @@ export class AgentBridge extends EventEmitter {
370
398
  if (resolve) {
371
399
  resolve({ outcome: { outcome: "selected", optionId } });
372
400
  this.permissionResolvers.delete(requestId);
373
- this.permissionRequestSessions.delete(requestId);
401
+ this.permissionRequestTasks.delete(requestId);
374
402
  }
375
403
  }
376
404
  denyPermission(requestId) {
@@ -378,77 +406,75 @@ export class AgentBridge extends EventEmitter {
378
406
  if (resolve) {
379
407
  resolve({ outcome: { outcome: "cancelled" } });
380
408
  this.permissionResolvers.delete(requestId);
381
- this.permissionRequestSessions.delete(requestId);
409
+ this.permissionRequestTasks.delete(requestId);
382
410
  }
383
411
  }
384
412
  /**
385
413
  * Restart the agent subprocess. Cancels all active work, cleans up state,
386
- * shuts down the old process, and starts a new one. Sessions are restored
414
+ * shuts down the old process, and starts a new one. Tasks are restored
387
415
  * lazily via ensureResumed() on next user interaction.
388
416
  */
389
- async restart(sessions, titleService) {
417
+ async restart(tasks) {
390
418
  if (this.reloading)
391
419
  throw new Error("Already reloading");
392
420
  this.reloading = true;
393
- const liveSessionIds = [...sessions.liveSessions];
421
+ const liveTaskIds = [...tasks.liveTasks];
394
422
  this.emit("event", { type: "agent_reloading" });
395
423
  blog.info("reloading agent...");
396
424
  try {
397
425
  // 1. Cancel all active prompts + kill bash procs
398
- for (const sessionId of [...sessions.activePrompts]) {
399
- const proc = sessions.runningBashProcs.get(sessionId);
426
+ for (const taskId of [...tasks.activePrompts]) {
427
+ const proc = tasks.runningBashProcs.get(taskId);
400
428
  if (proc) {
401
429
  interruptBashProc(proc);
402
- sessions.runningBashProcs.delete(sessionId);
430
+ tasks.runningBashProcs.delete(taskId);
403
431
  }
404
432
  try {
405
- await this.cancel(sessionId);
433
+ await this.cancel(taskId);
406
434
  }
407
435
  catch {
408
436
  /* best-effort */
409
437
  }
410
438
  }
411
439
  // 2. Flush buffers to persist partial content
412
- for (const sessionId of liveSessionIds) {
413
- sessions.flushBuffers(sessionId);
440
+ for (const taskId of liveTaskIds) {
441
+ tasks.flushBuffers(taskId);
414
442
  }
415
- // 3. Clean up SessionManager state
416
- sessions.pendingPermissions.clear();
417
- sessions.state.clearPlans();
418
- const busySessionIds = new Set([
419
- ...sessions.activePrompts,
420
- ...sessions.pendingPromptSubmissions.keys(),
443
+ // 3. Clean up TaskManager state
444
+ tasks.pendingPermissions.clear();
445
+ tasks.state.clearPlans();
446
+ const busyTaskIds = new Set([
447
+ ...tasks.activePrompts,
448
+ ...tasks.pendingPromptSubmissions.keys(),
421
449
  ]);
422
- for (const id of busySessionIds) {
423
- sessions.state.patch(id, { runtime: { busy: null } });
450
+ for (const id of busyTaskIds) {
451
+ tasks.state.patch(id, { runtime: { busy: null } });
424
452
  }
425
- for (const submissionId of sessions.pendingPromptSubmissions.values()) {
426
- sessions.cancelledPromptSubmissions.add(submissionId);
453
+ for (const submissionId of tasks.pendingPromptSubmissions.values()) {
454
+ tasks.cancelledPromptSubmissions.add(submissionId);
427
455
  }
428
- sessions.activePrompts.clear();
429
- sessions.pendingPromptSubmissions.clear();
456
+ tasks.activePrompts.clear();
457
+ tasks.pendingPromptSubmissions.clear();
430
458
  // 4. Clean up bridge-side silent session state
431
459
  this.silentSessions.clear();
432
460
  this.silentBuffers.clear();
433
461
  this.unboundNewSessionIds.clear();
434
462
  this.pendingSessionUpdates.clear();
435
- // 5. Invalidate title service session
436
- titleService.invalidate();
437
- // 5. Clear liveSessions so ensureResumed() will re-register on next access
438
- sessions.liveSessions.clear();
463
+ // 5. Clear liveTasks so ensureResumed() will re-register on next access
464
+ tasks.liveTasks.clear();
439
465
  // Also clear the global configOptions cache — a restarted agent may
440
466
  // speak a different schema (e.g. agent upgrade removed a model). The
441
- // next resumeSession will warm it from the user's stored config.
442
- sessions.cachedConfigOptions = [];
467
+ // next resumeTask will warm it from the user's stored config.
468
+ tasks.cachedConfigOptions = [];
443
469
  // 6. Shutdown old process
444
470
  await this.shutdown();
445
471
  // Cancellation is asynchronous: the old agent may emit final chunks
446
472
  // before shutdown completes. Persist that tail and make the terminal
447
473
  // stream state authoritative before starting the replacement process.
448
- for (const sessionId of liveSessionIds) {
449
- sessions.flushBuffers(sessionId);
474
+ for (const taskId of liveTaskIds) {
475
+ tasks.flushBuffers(taskId);
450
476
  }
451
- sessions.state.clearStreaming();
477
+ tasks.state.clearStreaming();
452
478
  // 7. Start new process with retry (exponential backoff, max 3 attempts)
453
479
  let lastError;
454
480
  for (let i = 0; i < 3; i++) {
@@ -484,7 +510,7 @@ export class AgentBridge extends EventEmitter {
484
510
  resolve({ outcome: { outcome: "cancelled" } });
485
511
  }
486
512
  this.permissionResolvers.clear();
487
- this.permissionRequestSessions.clear();
513
+ this.permissionRequestTasks.clear();
488
514
  const proc = this.proc;
489
515
  if (proc && !this.closedProcesses.has(proc)) {
490
516
  await new Promise((resolve) => {
@@ -515,8 +541,8 @@ export class AgentBridge extends EventEmitter {
515
541
  }
516
542
  // --- ACP Client callbacks ---
517
543
  handlePermission(params) {
518
- const webSessionId = this.sessionIds.getWebSessionId(params.sessionId);
519
- if (!webSessionId) {
544
+ const taskId = this.sessionIds.getTaskId(params.sessionId);
545
+ if (!taskId) {
520
546
  return Promise.resolve({ outcome: { outcome: "cancelled" } });
521
547
  }
522
548
  const requestId = crypto.randomUUID();
@@ -529,11 +555,11 @@ export class AgentBridge extends EventEmitter {
529
555
  return new Promise((resolve) => {
530
556
  // Register resolver BEFORE emitting, so synchronous auto-approve can find it
531
557
  this.permissionResolvers.set(requestId, resolve);
532
- this.permissionRequestSessions.set(requestId, webSessionId);
558
+ this.permissionRequestTasks.set(requestId, taskId);
533
559
  this.emit("event", {
534
560
  type: "permission_request",
535
561
  requestId,
536
- sessionId: webSessionId,
562
+ taskId: taskId,
537
563
  title,
538
564
  toolCallId,
539
565
  options: params.options,
@@ -551,8 +577,8 @@ export class AgentBridge extends EventEmitter {
551
577
  this.captureSilentText(agentSessionId, update);
552
578
  return Promise.resolve();
553
579
  }
554
- const sessionId = this.sessionIds.getWebSessionId(agentSessionId);
555
- if (!sessionId) {
580
+ const taskId = this.sessionIds.getTaskId(agentSessionId);
581
+ if (!taskId) {
556
582
  if (this.pendingNewSessions > 0 ||
557
583
  this.unboundNewSessionIds.has(agentSessionId)) {
558
584
  const updates = this.pendingSessionUpdates.get(agentSessionId) ?? [];
@@ -565,7 +591,7 @@ export class AgentBridge extends EventEmitter {
565
591
  });
566
592
  return Promise.resolve();
567
593
  }
568
- const event = this.sessionUpdateToEvent(sessionId, update);
594
+ const event = this.sessionUpdateToEvent(taskId, update);
569
595
  if (event)
570
596
  this.emit("event", event);
571
597
  return Promise.resolve();
@@ -577,21 +603,21 @@ export class AgentBridge extends EventEmitter {
577
603
  this.silentBuffers.set(sessionId, buf);
578
604
  }
579
605
  }
580
- sessionUpdateToEvent(sessionId, update) {
606
+ sessionUpdateToEvent(taskId, update) {
581
607
  // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- only handles events with UI effects
582
608
  switch (update.sessionUpdate) {
583
609
  case "agent_message_chunk":
584
610
  return update.content.type === "text"
585
- ? { type: "message_chunk", sessionId, text: update.content.text }
611
+ ? { type: "message_chunk", taskId, text: update.content.text }
586
612
  : null;
587
613
  case "agent_thought_chunk":
588
614
  return update.content.type === "text"
589
- ? { type: "thought_chunk", sessionId, text: update.content.text }
615
+ ? { type: "thought_chunk", taskId, text: update.content.text }
590
616
  : null;
591
617
  case "tool_call":
592
618
  return {
593
619
  type: "tool_call",
594
- sessionId,
620
+ taskId,
595
621
  id: update.toolCallId,
596
622
  title: update.title,
597
623
  kind: update.kind ?? "unknown",
@@ -600,7 +626,7 @@ export class AgentBridge extends EventEmitter {
600
626
  case "tool_call_update":
601
627
  return {
602
628
  type: "tool_call_update",
603
- sessionId,
629
+ taskId,
604
630
  id: update.toolCallId,
605
631
  status: update.status ?? "",
606
632
  content: (update.content ?? undefined),
@@ -615,11 +641,11 @@ export class AgentBridge extends EventEmitter {
615
641
  : {}),
616
642
  };
617
643
  case "plan":
618
- return { type: "plan", sessionId, entries: update.entries };
644
+ return { type: "plan", taskId, entries: update.entries };
619
645
  case "usage_update":
620
646
  return {
621
647
  type: "usage_update",
622
- sessionId,
648
+ taskId,
623
649
  used: update.used,
624
650
  size: update.size,
625
651
  cost: update.cost,
@@ -627,14 +653,14 @@ export class AgentBridge extends EventEmitter {
627
653
  case "config_option_update":
628
654
  return {
629
655
  type: "config_option_update",
630
- sessionId,
656
+ taskId,
631
657
  configOptions: update
632
658
  .configOptions ?? [],
633
659
  };
634
660
  case "available_commands_update":
635
661
  return {
636
662
  type: "available_commands_update",
637
- sessionId,
663
+ taskId,
638
664
  commands: update.availableCommands.map((command) => ({
639
665
  name: command.name,
640
666
  description: command.description,
@@ -48,7 +48,7 @@ export class ClientRegistry {
48
48
  * - Returns `becameVisibleFor=X` only on first transition into
49
49
  * (visible:true, active:X) — heartbeat refreshes return null so
50
50
  * callers can fire edge-triggered side effects exactly once.
51
- * - Session-switch while visible (active X→Y) restarts the TTL clock
51
+ * - Task-switch while visible (active X→Y) restarts the TTL clock
52
52
  * even when the patch doesn't carry an explicit visible:true.
53
53
  *
54
54
  * No-op on unknown client.
@@ -57,7 +57,7 @@ export class ClientRegistry {
57
57
  const entry = this.clients.get(id);
58
58
  if (!entry)
59
59
  return { becameVisibleFor: null };
60
- const wasVisibleForSession = entry.visible && entry.active != null ? entry.active : null;
60
+ const wasVisibleForTask = entry.visible && entry.active != null ? entry.active : null;
61
61
  if (patch.visible !== undefined) {
62
62
  entry.visible = patch.visible;
63
63
  entry.visibleSince = patch.visible ? this.now() : 0;
@@ -67,37 +67,37 @@ export class ClientRegistry {
67
67
  }
68
68
  const becameVisibleFor = entry.visible &&
69
69
  entry.active != null &&
70
- entry.active !== wasVisibleForSession
70
+ entry.active !== wasVisibleForTask
71
71
  ? entry.active
72
72
  : null;
73
73
  if (becameVisibleFor) {
74
74
  // Any transition into "visible + active=X" restarts TTL — including
75
- // session-switches that arrive without an explicit visible:true.
75
+ // task-switches that arrive without an explicit visible:true.
76
76
  entry.visibleSince = this.now();
77
77
  }
78
78
  entry.lastSeen = this.now();
79
79
  return { becameVisibleFor };
80
80
  }
81
- /** Is this specific client currently visible & viewing `sessionId` & fresh? */
82
- isVisibleForSession(id, sessionId) {
81
+ /** Is this specific client currently visible & viewing `taskId` & fresh? */
82
+ isVisibleForTask(id, taskId) {
83
83
  const entry = this.clients.get(id);
84
84
  if (!entry)
85
85
  return false;
86
86
  if (!entry.visible)
87
87
  return false;
88
- if (entry.active !== sessionId)
88
+ if (entry.active !== taskId)
89
89
  return false;
90
90
  if (this.now() - entry.visibleSince > this.visibilityTtlMs)
91
91
  return false;
92
92
  return true;
93
93
  }
94
- /** Is at least one fresh visible client viewing `sessionId`? */
95
- isSessionVisibleToAnyClient(sessionId) {
94
+ /** Is at least one fresh visible client viewing `taskId`? */
95
+ isTaskVisibleToAnyClient(taskId) {
96
96
  const now = this.now();
97
97
  for (const e of this.clients.values()) {
98
98
  if (!e.visible)
99
99
  continue;
100
- if (e.active !== sessionId)
100
+ if (e.active !== taskId)
101
101
  continue;
102
102
  if (now - e.visibleSince > this.visibilityTtlMs)
103
103
  continue;
@@ -105,7 +105,7 @@ export class ClientRegistry {
105
105
  }
106
106
  return false;
107
107
  }
108
- /** Is this specific client currently fresh-visible (any session)? */
108
+ /** Is this specific client currently fresh-visible (any task)? */
109
109
  isClientVisible(id) {
110
110
  const entry = this.clients.get(id);
111
111
  if (!entry)
@@ -116,7 +116,7 @@ export class ClientRegistry {
116
116
  return false;
117
117
  return true;
118
118
  }
119
- /** Is at least one fresh visible client connected (any session)? */
119
+ /** Is at least one fresh visible client connected (any task)? */
120
120
  hasAnyVisibleClient() {
121
121
  const now = this.now();
122
122
  for (const e of this.clients.values()) {
package/lib/config.js CHANGED
@@ -20,7 +20,7 @@ export const ConfigSchema = z.object({
20
20
  image_upload: z.number().int().positive().default(10_485_760), // 10 MB
21
21
  file_upload: z.number().int().positive().default(52_428_800), // 50 MB — non-image attachments
22
22
  cancel_timeout: z.number().int().nonnegative().default(10_000), // 10s; 0 disables
23
- recent_paths: z.number().int().nonnegative().default(10), // /new menu display limit; 0 = show all
23
+ recent_paths: z.number().int().nonnegative().default(10), // /new + path-picker display limit; 0 = show all
24
24
  recent_paths_ttl: z.number().int().nonnegative().default(30), // days before auto-cleanup; 0 = keep forever
25
25
  })
26
26
  .default({
@@ -40,35 +40,6 @@ export const ConfigSchema = z.object({
40
40
  vapid_subject: "mailto:noreply@example.com",
41
41
  global_visibility_suppression: true,
42
42
  }),
43
- // [title] — title generation sub-session configuration.
44
- //
45
- // `models` is an array of case-insensitive substring patterns. When the
46
- // title sub-session is created, we look at the model list the agent
47
- // reports (ACP `availableModels`) and pick the first model whose id
48
- // matches any pattern in order. Match → call `setConfigOption` with
49
- // that model id; no match → skip the call and inherit the agent's
50
- // default model (`currentModelId`).
51
- //
52
- // Default list targets the cheap/fast tier across major providers:
53
- // - "haiku" → Anthropic (claude-haiku-*)
54
- // - "flash-lite" → Google Gemini (gemini-*-flash-lite) [must precede "flash"]
55
- // - "nano" → OpenAI (gpt-*-nano), Gemini Nano
56
- // - "mini" → OpenAI (gpt-*-mini, 4o-mini), Mistral
57
- // - "flash" → Google Gemini (gemini-*-flash)
58
- // - "lite" → Cohere, generic
59
- //
60
- // Set `models = []` to disable substring matching entirely and always
61
- // inherit the agent's default model. To pin one specific model, pass a
62
- // single-element array: `models = ["claude-haiku-4.5"]`.
63
- title: z
64
- .object({
65
- models: z
66
- .array(z.string())
67
- .default(["haiku", "flash-lite", "nano", "mini", "flash", "lite"]),
68
- })
69
- .default({
70
- models: ["haiku", "flash-lite", "nano", "mini", "flash", "lite"],
71
- }),
72
43
  // [debug] — frontend log level.
73
44
  // level ∈ off | debug | info | warn | error. Default "off".
74
45
  // Users can override per page-load via `?debug=<level>` in the URL,
@@ -86,7 +57,7 @@ export const ConfigSchema = z.object({
86
57
  unprocessed_ttl_days: z.number().int().nonnegative().default(30),
87
58
  })
88
59
  .default({ unprocessed_ttl_days: 30 }),
89
- // [share] — public read-only session share links.
60
+ // [share] — public read-only task share links.
90
61
  // Default: disabled. Dogfood manually flips `enabled = true` after
91
62
  // CF Access bypass + Rate Limiting are configured. See docs/share.md.
92
63
  // enabled — master kill switch; when false, all share routes