@lelouchhe/webagent 0.8.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/lib/server.js CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { loadConfig } from "./config.js";
6
6
  import { setLogLevel, log } from "./log.js";
7
7
  import { AgentBridge } from "./bridge.js";
8
+ import { agentKeyFromCommand } from "./agent-key.js";
8
9
  import { Store } from "./store.js";
9
10
  import { SessionManager } from "./session-manager.js";
10
11
  import { TitleService } from "./title-service.js";
@@ -53,7 +54,7 @@ const PKG_VERSION = (() => {
53
54
  }
54
55
  })();
55
56
  // --- Core dependencies ---
56
- const store = new Store(config.data_dir);
57
+ const store = new Store(config.data_dir, agentKeyFromCommand(preflight.agentCmd));
57
58
  console.log(`[store] using ${config.data_dir}/`);
58
59
  // Pin <data_dir>/sessions realpath at boot so all later anchor checks
59
60
  // (file:// URI construction, permission interceptor) compare against the
@@ -109,28 +110,29 @@ const messageCleanup = startMessageCleanup(store, config.messages.unprocessed_tt
109
110
  });
110
111
  let sharePreviewCleanup = null;
111
112
  // --- HTTP server ---
113
+ const requestHandler = createRequestHandler({
114
+ store,
115
+ sessions,
116
+ sseManager,
117
+ clientRegistry,
118
+ titleService,
119
+ getBridge: () => bridge,
120
+ publicDir: PUBLIC_DIR,
121
+ dataDir: config.data_dir,
122
+ limits: config.limits,
123
+ pushService,
124
+ serverVersion: PKG_VERSION,
125
+ debugLevel: config.debug.level,
126
+ authStore,
127
+ ticketStore,
128
+ attachmentSecret,
129
+ shareConfig: config.share,
130
+ });
112
131
  const server = createServer((req, res) => {
113
- void createRequestHandler({
114
- store,
115
- sessions,
116
- sseManager,
117
- clientRegistry,
118
- titleService,
119
- getBridge: () => bridge,
120
- publicDir: PUBLIC_DIR,
121
- dataDir: config.data_dir,
122
- limits: config.limits,
123
- pushService,
124
- serverVersion: PKG_VERSION,
125
- debugLevel: config.debug.level,
126
- authStore,
127
- ticketStore,
128
- attachmentSecret,
129
- shareConfig: config.share,
130
- })(req, res);
132
+ void requestHandler(req, res);
131
133
  });
132
134
  async function initBridge(agentCmd) {
133
- const b = new AgentBridge(agentCmd);
135
+ const b = new AgentBridge(agentCmd, store);
134
136
  b.setAttachmentDispatcher(attachmentDispatcher);
135
137
  const eventHandlerConfig = _buildBridgeEventHandlerConfig({
136
138
  cancelTimeout: config.limits.cancel_timeout,
@@ -6,6 +6,7 @@ import { join } from "node:path";
6
6
  import { MessageNotFoundError } from "./store.js";
7
7
  import { SessionStateManager } from "./session-state.js";
8
8
  import { buildLabelMap } from "./attachment-labels.js";
9
+ import { abbreviateHomePath, expandHomePath } from "./home-path.js";
9
10
  import { log } from "./log.js";
10
11
  const slog = log.scope("session");
11
12
  const IS_WIN = process.platform === "win32";
@@ -89,7 +90,7 @@ export class SessionManager {
89
90
  }
90
91
  /** Create a new session in both bridge and store, inheriting the source session's config. */
91
92
  async createSession(bridge, cwd, inheritFromSessionId, source = "auto", opts) {
92
- const sessionCwd = cwd ?? this.defaultCwd;
93
+ const sessionCwd = expandHomePath(cwd ?? this.defaultCwd);
93
94
  try {
94
95
  const info = await stat(sessionCwd);
95
96
  if (!info.isDirectory())
@@ -109,37 +110,47 @@ export class SessionManager {
109
110
  const sourceSession = inheritFromSessionId
110
111
  ? this.store.getSession(inheritFromSessionId)
111
112
  : null;
112
- const { sessionId, configOptions: createdConfigOptions } = await bridge.newSession(sessionCwd, { silent: opts?.silent });
113
+ const webSessionId = randomUUID();
114
+ const { sessionId: agentSessionId, configOptions: createdConfigOptions } = await bridge.newSession(sessionCwd, { silent: opts?.silent });
113
115
  let configOptions = createdConfigOptions;
114
116
  try {
115
- this.store.createSession(sessionId, sessionCwd, source);
117
+ this.store.createSession(webSessionId, sessionCwd, source, agentSessionId);
116
118
  }
117
119
  catch (err) {
118
120
  slog.warn("ACP session created but local persistence failed", {
119
- sessionId,
121
+ agentSessionId,
120
122
  error: err,
121
123
  });
124
+ bridge.discardUnboundSession?.(agentSessionId);
122
125
  throw err;
123
126
  }
124
- this.liveSessions.add(sessionId);
125
- this.recordConfigOptions(sessionId, createdConfigOptions);
127
+ this.liveSessions.add(webSessionId);
128
+ this.recordConfigOptions(webSessionId, createdConfigOptions);
129
+ bridge.sessionMapped?.(agentSessionId);
126
130
  // Inherit config options from source session
127
131
  if (sourceSession) {
132
+ const thinkingOption = createdConfigOptions.find((option) => "options" in option &&
133
+ (option.id === "reasoning_effort" ||
134
+ option.id === "thought_level" ||
135
+ option.category === "thought_level"));
128
136
  const inherited = [
129
137
  { configId: "model", value: sourceSession.model },
130
- { configId: "reasoning_effort", value: sourceSession.reasoning_effort },
138
+ {
139
+ configId: thinkingOption?.id ?? "reasoning_effort",
140
+ value: sourceSession.reasoning_effort,
141
+ },
131
142
  ];
132
143
  for (const { configId, value } of inherited) {
133
144
  if (!value)
134
145
  continue;
135
146
  try {
136
- const updatedConfigOptions = await bridge.setConfigOption(sessionId, configId, value);
147
+ const updatedConfigOptions = await bridge.setConfigOption(webSessionId, configId, value);
137
148
  if (updatedConfigOptions.length > 0) {
138
149
  configOptions = updatedConfigOptions;
139
- this.recordConfigOptions(sessionId, updatedConfigOptions);
150
+ this.recordConfigOptions(webSessionId, updatedConfigOptions);
140
151
  }
141
152
  else {
142
- this.store.updateSessionConfig(sessionId, configId, value);
153
+ this.store.updateSessionConfig(webSessionId, configId, value);
143
154
  }
144
155
  }
145
156
  catch {
@@ -147,9 +158,9 @@ export class SessionManager {
147
158
  }
148
159
  }
149
160
  }
150
- const session = this.store.getSession(sessionId);
161
+ const session = this.store.getSession(webSessionId);
151
162
  return {
152
- sessionId,
163
+ sessionId: webSessionId,
153
164
  configOptions: session
154
165
  ? this.applyStoredConfig(configOptions, session)
155
166
  : [],
@@ -229,6 +240,7 @@ export class SessionManager {
229
240
  type: "session_created",
230
241
  sessionId,
231
242
  cwd: session.cwd,
243
+ cwdDisplay: abbreviateHomePath(session.cwd),
232
244
  title: session.title,
233
245
  configOptions,
234
246
  };
@@ -252,6 +264,7 @@ export class SessionManager {
252
264
  type: "session_created",
253
265
  sessionId,
254
266
  cwd: session.cwd,
267
+ cwdDisplay: abbreviateHomePath(session.cwd),
255
268
  title: session.title,
256
269
  configOptions,
257
270
  };
@@ -272,33 +285,42 @@ export class SessionManager {
272
285
  * DB row — setConfigOption's currentValue for unrelated keys is the
273
286
  * agent's in-memory default, not the user's preference.
274
287
  *
275
- * Key priority mode > reasoning_effort > model:
288
+ * Key priority mode > thinking aliases > model:
276
289
  * - mode is a small stable enum, rewriting current value is idempotent.
290
+ * - ACP agents use both reasoning_effort and thought_level for thinking.
277
291
  * - model has the highest schema-drift risk (agent upgrades drop values).
278
292
  */
279
293
  async tryWarmCache(bridge, sessionId, session) {
280
294
  if (this.cachedConfigOptions.length > 0)
281
295
  return;
282
- const pick = session.mode
283
- ? { id: "mode", value: session.mode }
284
- : session.reasoning_effort
285
- ? { id: "reasoning_effort", value: session.reasoning_effort }
286
- : session.model
287
- ? { id: "model", value: session.model }
288
- : null;
289
- if (!pick)
296
+ const candidates = [];
297
+ if (session.mode)
298
+ candidates.push({ id: "mode", value: session.mode });
299
+ if (session.reasoning_effort) {
300
+ candidates.push({ id: "reasoning_effort", value: session.reasoning_effort }, { id: "thought_level", value: session.reasoning_effort });
301
+ }
302
+ if (session.model)
303
+ candidates.push({ id: "model", value: session.model });
304
+ if (candidates.length === 0)
290
305
  return;
291
- try {
292
- const opts = await bridge.setConfigOption(sessionId, pick.id, pick.value);
293
- if (opts.length > 0) {
294
- this.cachedConfigOptions = opts;
295
- slog.info("warmed cache on resume", { options: opts.length });
306
+ let lastError = null;
307
+ for (const candidate of candidates) {
308
+ try {
309
+ const opts = await bridge.setConfigOption(sessionId, candidate.id, candidate.value);
310
+ if (opts.length > 0) {
311
+ this.cachedConfigOptions = opts;
312
+ slog.info("warmed cache on resume", { options: opts.length });
313
+ return;
314
+ }
315
+ }
316
+ catch (err) {
317
+ lastError = err;
296
318
  }
297
319
  }
298
- catch (err) {
320
+ if (lastError) {
299
321
  slog.warn("cache warming failed", {
300
322
  sessionId: sessionId.slice(0, 8) + "…",
301
- error: err,
323
+ error: lastError,
302
324
  });
303
325
  }
304
326
  }
@@ -331,9 +353,12 @@ export class SessionManager {
331
353
  model: session.model,
332
354
  mode: session.mode,
333
355
  reasoning_effort: session.reasoning_effort,
356
+ thought_level: session.reasoning_effort,
334
357
  };
335
358
  return configOptions.map((opt) => {
336
- const override = stored[opt.id];
359
+ const override = opt.category === "thought_level"
360
+ ? session.reasoning_effort
361
+ : stored[opt.id];
337
362
  if (override && "options" in opt)
338
363
  return { ...opt, currentValue: override };
339
364
  return opt;
@@ -17,6 +17,7 @@ function defaultState() {
17
17
  pendingPermissions: [],
18
18
  streaming: { assistant: false, thinking: false },
19
19
  plan: null,
20
+ contextUsage: null,
20
21
  },
21
22
  };
22
23
  }
@@ -56,6 +57,14 @@ function plansEqual(a, b) {
56
57
  return false;
57
58
  return a.every((entry, index) => entry.status === b[index].status && entry.content === b[index].content);
58
59
  }
60
+ function contextUsageEqual(a, b) {
61
+ if (a === null || b === null)
62
+ return a === b;
63
+ return (a.used === b.used &&
64
+ a.size === b.size &&
65
+ (a.cost?.amount ?? null) === (b.cost?.amount ?? null) &&
66
+ (a.cost?.currency ?? null) === (b.cost?.currency ?? null));
67
+ }
59
68
  /** True when the patch would change the current runtime state. */
60
69
  function hasRuntimeChanges(current, patch) {
61
70
  if (!patch)
@@ -68,6 +77,9 @@ function hasRuntimeChanges(current, patch) {
68
77
  return true;
69
78
  if ("plan" in patch && !plansEqual(current.plan, patch.plan ?? null))
70
79
  return true;
80
+ if ("contextUsage" in patch &&
81
+ !contextUsageEqual(current.contextUsage, patch.contextUsage ?? null))
82
+ return true;
71
83
  if ("streaming" in patch && patch.streaming) {
72
84
  const s = patch.streaming;
73
85
  if (s.assistant !== undefined &&
@@ -119,6 +131,15 @@ export class SessionStateManager {
119
131
  state.runtime.plan =
120
132
  patch.runtime.plan?.map((entry) => ({ ...entry })) ?? null;
121
133
  }
134
+ if ("contextUsage" in patch.runtime) {
135
+ const usage = patch.runtime.contextUsage;
136
+ state.runtime.contextUsage = usage
137
+ ? {
138
+ ...usage,
139
+ ...(usage.cost ? { cost: { ...usage.cost } } : {}),
140
+ }
141
+ : null;
142
+ }
122
143
  if ("streaming" in patch.runtime && patch.runtime.streaming) {
123
144
  if (patch.runtime.streaming.assistant !== undefined) {
124
145
  state.runtime.streaming.assistant = patch.runtime.streaming.assistant;
@@ -162,6 +183,14 @@ export class SessionStateManager {
162
183
  }
163
184
  }
164
185
  }
186
+ /** Clear context usage for every known session on bridge teardown. */
187
+ clearContextUsage() {
188
+ for (const [sessionId, state] of this.states) {
189
+ if (state.runtime.contextUsage !== null) {
190
+ this.patch(sessionId, { runtime: { contextUsage: null } });
191
+ }
192
+ }
193
+ }
165
194
  /** Clear active stream markers for every known session on bridge teardown. */
166
195
  clearStreaming() {
167
196
  for (const [sessionId, state] of this.states) {
@@ -444,6 +444,10 @@ async function handlePublish(req, res, deps, sessionId) {
444
444
  json(res, HTTP_STATUS.BAD_REQUEST, { error: "token required" });
445
445
  return;
446
446
  }
447
+ if (!deps.store.ownsSession(sessionId)) {
448
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "share not found" });
449
+ return;
450
+ }
447
451
  const row = deps.store.getShareByToken(body.token);
448
452
  if (!row) {
449
453
  json(res, HTTP_STATUS.NOT_FOUND, { error: "share not found" });
@@ -849,6 +853,10 @@ async function handleRevoke(req, res, deps, sessionId) {
849
853
  json(res, HTTP_STATUS.BAD_REQUEST, { error: "token required" });
850
854
  return;
851
855
  }
856
+ if (!deps.store.ownsSession(sessionId)) {
857
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "share not found" });
858
+ return;
859
+ }
852
860
  const row = deps.store.getShareByToken(body.token);
853
861
  if (!row) {
854
862
  // Idempotent DELETE: row already gone (revoked or never existed).
@@ -910,6 +918,10 @@ async function handlePatchLabel(req, res, deps, sessionId) {
910
918
  json(res, HTTP_STATUS.BAD_REQUEST, { error: "token required" });
911
919
  return;
912
920
  }
921
+ if (!deps.store.ownsSession(sessionId)) {
922
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "share not found" });
923
+ return;
924
+ }
913
925
  const row = deps.store.getShareByToken(body.token);
914
926
  if (!row) {
915
927
  json(res, HTTP_STATUS.NOT_FOUND, { error: "share not found" });
package/lib/store.js CHANGED
@@ -9,7 +9,11 @@ export class MessageNotFoundError extends Error {
9
9
  }
10
10
  export class Store {
11
11
  db;
12
- constructor(dataDir) {
12
+ agentKey;
13
+ constructor(dataDir, agentKey) {
14
+ if (!agentKey)
15
+ throw new Error("agentKey is required");
16
+ this.agentKey = agentKey;
13
17
  mkdirSync(dataDir, { recursive: true });
14
18
  this.db = new Database(join(dataDir, "webagent.db"));
15
19
  this.db.pragma("journal_mode = WAL");
@@ -27,6 +31,16 @@ export class Store {
27
31
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
28
32
  last_active_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
29
33
  );
34
+ CREATE TABLE IF NOT EXISTS agent_sessions (
35
+ agent_key TEXT NOT NULL,
36
+ agent_session_id TEXT NOT NULL,
37
+ web_session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
38
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
39
+ PRIMARY KEY (agent_key, agent_session_id)
40
+ );
41
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_sessions_web
42
+ ON agent_sessions(web_session_id)
43
+ WHERE web_session_id IS NOT NULL;
30
44
  CREATE TABLE IF NOT EXISTS events (
31
45
  id INTEGER PRIMARY KEY AUTOINCREMENT,
32
46
  session_id TEXT NOT NULL REFERENCES sessions(id),
@@ -70,6 +84,21 @@ export class Store {
70
84
  if (!colNames.has("deleted_at")) {
71
85
  this.db.exec("ALTER TABLE sessions ADD COLUMN deleted_at INTEGER");
72
86
  }
87
+ // One-time dual-ID migration. Existing WebAgent session IDs were also the
88
+ // ACP agent's IDs, so preserve the public IDs and record that identity
89
+ // mapping under the agent command active during the upgrade.
90
+ this.db
91
+ .prepare(`
92
+ INSERT INTO agent_sessions (
93
+ agent_key, agent_session_id, web_session_id, created_at
94
+ )
95
+ SELECT ?, s.id, s.id, s.created_at
96
+ FROM sessions s
97
+ WHERE NOT EXISTS (
98
+ SELECT 1 FROM agent_sessions a WHERE a.web_session_id = s.id
99
+ )
100
+ `)
101
+ .run(this.agentKey);
73
102
  // messages — pending unbound notifications. POST /api/v1/messages with
74
103
  // `to = "user"` lands here; consumeMessageTx transactionally moves the
75
104
  // content into an existing ACP-backed session's events and deletes the
@@ -247,29 +276,72 @@ export class Store {
247
276
  );
248
277
  `);
249
278
  }
250
- createSession(id, cwd, source = "auto") {
251
- this.db
252
- .prepare("INSERT INTO sessions (id, cwd, source) VALUES (?, ?, ?)")
253
- .run(id, cwd, source);
254
- return this.db
255
- .prepare("SELECT * FROM sessions WHERE id = ?")
256
- .get(id);
279
+ createSession(id, cwd, source = "auto", agentSessionId = id) {
280
+ return this.db.transaction(() => {
281
+ this.db
282
+ .prepare("INSERT INTO sessions (id, cwd, source) VALUES (?, ?, ?)")
283
+ .run(id, cwd, source);
284
+ this.db
285
+ .prepare("INSERT INTO agent_sessions (agent_key, agent_session_id, web_session_id) VALUES (?, ?, ?)")
286
+ .run(this.agentKey, agentSessionId, id);
287
+ return this.db
288
+ .prepare("SELECT * FROM sessions WHERE id = ?")
289
+ .get(id);
290
+ })();
257
291
  }
258
292
  listSessions(opts) {
259
293
  if (opts?.source) {
260
294
  return this.db
261
- .prepare("SELECT * FROM sessions WHERE source = ? AND deleted_at IS NULL ORDER BY COALESCE(last_active_at, created_at) DESC")
262
- .all(opts.source);
295
+ .prepare(`SELECT s.* FROM sessions s
296
+ JOIN agent_sessions a ON a.web_session_id = s.id
297
+ WHERE a.agent_key = ? AND s.source = ? AND s.deleted_at IS NULL
298
+ ORDER BY COALESCE(s.last_active_at, s.created_at) DESC`)
299
+ .all(this.agentKey, opts.source);
263
300
  }
264
301
  return this.db
265
- .prepare("SELECT * FROM sessions WHERE deleted_at IS NULL ORDER BY COALESCE(last_active_at, created_at) DESC")
266
- .all();
302
+ .prepare(`SELECT s.* FROM sessions s
303
+ JOIN agent_sessions a ON a.web_session_id = s.id
304
+ WHERE a.agent_key = ? AND s.deleted_at IS NULL
305
+ ORDER BY COALESCE(s.last_active_at, s.created_at) DESC`)
306
+ .all(this.agentKey);
267
307
  }
268
308
  /** Returns live sessions only. Soft-deleted (tombstone) rows are hidden. */
269
309
  getSession(id) {
270
310
  return this.db
271
- .prepare("SELECT * FROM sessions WHERE id = ? AND deleted_at IS NULL")
272
- .get(id);
311
+ .prepare(`SELECT s.* FROM sessions s
312
+ JOIN agent_sessions a ON a.web_session_id = s.id
313
+ WHERE s.id = ? AND a.agent_key = ? AND s.deleted_at IS NULL`)
314
+ .get(id, this.agentKey);
315
+ }
316
+ registerInternalAgentSession(agentSessionId) {
317
+ this.db
318
+ .prepare("INSERT OR IGNORE INTO agent_sessions (agent_key, agent_session_id, web_session_id) VALUES (?, ?, NULL)")
319
+ .run(this.agentKey, agentSessionId);
320
+ const row = this.db
321
+ .prepare("SELECT * FROM agent_sessions WHERE agent_key = ? AND agent_session_id = ?")
322
+ .get(this.agentKey, agentSessionId);
323
+ if (row.web_session_id) {
324
+ throw new Error("Agent reused a user-visible session ID internally");
325
+ }
326
+ return row;
327
+ }
328
+ getAgentSessionId(webSessionId) {
329
+ return this.db
330
+ .prepare("SELECT agent_session_id FROM agent_sessions WHERE agent_key = ? AND web_session_id = ?")
331
+ .get(this.agentKey, webSessionId)?.agent_session_id;
332
+ }
333
+ getWebSessionId(agentSessionId) {
334
+ return this.db
335
+ .prepare("SELECT web_session_id FROM agent_sessions WHERE agent_key = ? AND agent_session_id = ? AND web_session_id IS NOT NULL")
336
+ .get(this.agentKey, agentSessionId)?.web_session_id;
337
+ }
338
+ getAgentSessionBinding(webSessionId) {
339
+ return this.db
340
+ .prepare("SELECT * FROM agent_sessions WHERE agent_key = ? AND web_session_id = ?")
341
+ .get(this.agentKey, webSessionId);
342
+ }
343
+ ownsSession(webSessionId) {
344
+ return this.getAgentSessionBinding(webSessionId) !== undefined;
273
345
  }
274
346
  /**
275
347
  * Returns a session row even if soft-deleted. Used by the public share
@@ -339,11 +411,14 @@ export class Store {
339
411
  const empties = this.db
340
412
  .prepare(`
341
413
  SELECT s.id FROM sessions s
414
+ JOIN agent_sessions a ON a.web_session_id = s.id
342
415
  LEFT JOIN events e ON e.session_id = s.id
343
416
  WHERE e.id IS NULL
417
+ AND a.agent_key = ?
418
+ AND s.deleted_at IS NULL
344
419
  AND strftime('%s', 'now') - strftime('%s', s.created_at) >= ?
345
420
  `)
346
- .all(minAgeS);
421
+ .all(this.agentKey, minAgeS);
347
422
  if (empties.length === 0)
348
423
  return [];
349
424
  const del = this.db.prepare("DELETE FROM sessions WHERE id = ?");
@@ -367,6 +442,7 @@ export class Store {
367
442
  model: "model",
368
443
  mode: "mode",
369
444
  reasoning_effort: "reasoning_effort",
445
+ thought_level: "reasoning_effort",
370
446
  }[configId];
371
447
  if (!column)
372
448
  return;
@@ -435,14 +511,17 @@ export class Store {
435
511
  .get(sessionId);
436
512
  return row.seq;
437
513
  }
438
- /** Check if the most recent agent turn was interrupted (user_message without a following prompt_done). */
514
+ /** Check if the most recent agent turn lacks a completion or error terminal event. */
439
515
  hasInterruptedTurn(sessionId) {
440
516
  const row = this.db
441
517
  .prepare(`
442
518
  SELECT 1 FROM events
443
519
  WHERE session_id = ? AND type = 'user_message'
444
520
  AND seq > COALESCE(
445
- (SELECT MAX(seq) FROM events WHERE session_id = ? AND type = 'prompt_done'),
521
+ (
522
+ SELECT MAX(seq) FROM events
523
+ WHERE session_id = ? AND type IN ('prompt_done', 'error')
524
+ ),
446
525
  0
447
526
  )
448
527
  LIMIT 1
@@ -771,9 +850,11 @@ export class Store {
771
850
  s.ttl_hours AS ttl_hours,
772
851
  s.last_accessed_at AS last_accessed_at
773
852
  FROM shares s
853
+ JOIN agent_sessions a ON a.web_session_id = s.session_id
774
854
  LEFT JOIN sessions sess ON sess.id = s.session_id
855
+ WHERE a.agent_key = ?
775
856
  ORDER BY s.created_at DESC`)
776
- .all();
857
+ .all(this.agentKey);
777
858
  }
778
859
  /**
779
860
  * One-time write of last_accessed_at (share-plan §4.1 R2 ENG-6a +
@@ -64,7 +64,7 @@ export class TitleService {
64
64
  this.cancelledSourceSessions.add(sessionId);
65
65
  if (!this.titleSessionId || !this.activeSourceSessions.has(sessionId))
66
66
  return;
67
- await bridge.cancel(this.titleSessionId);
67
+ await bridge.cancelAgentSession(this.titleSessionId);
68
68
  }
69
69
  /** Clear the cached title session ID (e.g. after agent reload). */
70
70
  invalidate() {
@@ -76,14 +76,14 @@ export class TitleService {
76
76
  return this.titleSessionId;
77
77
  try {
78
78
  const { sessionId: id, configOptions } = await bridge.newSession(this.defaultCwd, { silent: true });
79
- this.sessions.liveSessions.add(id);
79
+ this.store.registerInternalAgentSession(id);
80
80
  // Pick the cheapest available model by matching id substrings against
81
81
  // the agent's reported availableModels (`configOptions[id=model].options`).
82
82
  // Empty pattern list, no model option, or no match → skip the call and
83
83
  // inherit the agent's default model (`currentModelId`).
84
84
  const picked = pickModelByPatterns(configOptions, this.modelPatterns);
85
85
  if (picked) {
86
- await bridge.setConfigOption(id, "model", picked).catch(() => []);
86
+ await bridge.setAgentConfigOption(id, "model", picked).catch(() => []);
87
87
  }
88
88
  this.titleSessionId = id;
89
89
  return id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lelouchhe/webagent",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A terminal-style web UI for ACP-compatible agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,6 +64,7 @@
64
64
  "@types/web-push": "^3.6.4",
65
65
  "better-sqlite3": "^12.6.2",
66
66
  "busboy": "^1.6.0",
67
+ "diff": "^9.0.0",
67
68
  "dompurify": "^3.4.1",
68
69
  "file-type": "^22.0.1",
69
70
  "highlight.js": "^11.11.1",