@love-moon/ai-sdk 0.17.2 → 0.18.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/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 72324fd: Add a local multi-chat runtime with isolated Chromium workers, persistent profiles,
8
+ per-account concurrency, resumable events, request deduplication, and SDK/CLI access.
9
+ Add experimental Kimi and Claude webpage adapters and optional AI SDK socket transport.
10
+
11
+ Support Kimi Connect JSON envelopes and DeepSeek XHR SSE patches, with verified
12
+ parallel real-account text chats, independent cancellation, and restart recovery.
13
+
14
+ Add DeepSeek --deepthink / --no-deepthink controls and SDK options with persisted
15
+ chat defaults, per-turn overrides, verified UI state, and runtime capability checks.
16
+
17
+ Connect Conductor web-kimi and web-deepseek to managed isolated Chromium workers
18
+ by default. Add conductor chat-web login, account credential bootstrap, automatic
19
+ daemon startup and idle shutdown, stable cross-process resume IDs, and browser
20
+ release when tasks close. Preserve the shared-profile queue for other providers.
21
+ Retain cancelled conversation URLs and report confirmed stops as interruptions.
22
+
23
+ ### Patch Changes
24
+
25
+ - bdbda18: Preserve lightweight conversation identity archives across profile/history GC so
26
+ Conductor can resume the same provider conversation using its existing local ID.
27
+ Archive idle, workerless open chats; persist the completed-turn resume guard; and
28
+ distinguish slow page loading from confirmed authentication failures.
29
+
30
+ Upgrading a CLI while an older managed runtime is running can report
31
+ RUNTIME_UPGRADE_REQUIRED when queuedStartup or durableResume support is missing.
32
+ After active chats finish, run `conductor chat-web runtime stop` and retry;
33
+ the next managed session starts the updated runtime. Alternatively, a runtime
34
+ with no active turns or pinned login windows exits after ten minutes without
35
+ requests. The CLI never terminates running chats automatically.
36
+
37
+ Profile deletion now starts only after the retention checkpoint commits, and
38
+ closed chats awaiting uncertainty review retain their profiles and turn history.
39
+ Accepted prompts refresh idle activity before collection, keeping their parent
40
+ conversation resident even when a client submits directly after a long idle.
41
+
42
+ - 063b97c: Show daemon-host inspection and acknowledgement commands on uncertain chat-web
43
+ errors and REVIEW_REQUIRED, including interrupted sends and timeouts. Keep
44
+ acknowledgement explicit: it does not replay a request, and the user sends the
45
+ next message after checking the provider conversation. The web error message
46
+ contains the commands; acknowledgement currently runs through the host CLI.
47
+
48
+ Compact streamed event history into full text snapshots instead of failing
49
+ after 2000 events. Sequence numbers remain monotonic across compaction and
50
+ restart; replay cursors may cross gaps and recover through a message_snapshot.
51
+ The one-million-character text limit remains enforced.
52
+
53
+ Return CHAT*CLOSED when closing a chat races a headed resume. Strip CONDUCTOR*\*
54
+ identity variables from detached runtime and browser-worker environments while
55
+ preserving browser and proxy configuration.
56
+
57
+ Upgrade note: an already-running shared runtime continues executing its old
58
+ code. After active chats finish, run `conductor chat-web runtime stop` and retry
59
+ to launch the updated runtime. Older runtimes lacking required capabilities
60
+ report RUNTIME_UPGRADE_REQUIRED; unused managed runtimes exit after ten minutes
61
+ without requests or pinned login windows. No running chat is stopped
62
+ automatically during upgrade.
63
+
64
+ - 31b61ea: Bound closed-chat and terminal-turn retention, reclaim browser profiles and orphan
65
+ directories, and keep streaming deltas in memory between durable checkpoints.
66
+ Defer saturated or busy chat startup to the turn queue, validate authentication at
67
+ allocation, and expire queued prompts before crash recovery or delayed submission.
68
+ Keep Gemini on its legacy transport with global runtime sockets and consume the
69
+ Conductor wrapper separator before forwarding a packaged CLI command.
70
+
3
71
  ## 0.17.2
4
72
 
5
73
  ### Patch Changes
@@ -1,34 +1,35 @@
1
1
  /**
2
2
  * AI SDK provider that delegates to the chat-web runtime
3
3
  * (`@love-moon/chat-web`), which automates a real Chromium browser against
4
- * ChatGPT / Gemini / Kimi and ferries conversations through their web
5
- * UIs. Choose the underlying chat-web provider via:
4
+ * chatbot web UIs. Kimi and DeepSeek use isolated persistent chat workers
5
+ * with an automatically started local daemon. ChatGPT/Gemini retain the
6
+ * shared-profile queue unless a runtime socket is explicitly configured.
7
+ * Choose the underlying chat-web provider via:
6
8
  *
7
- * - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" (preferred)
9
+ * - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" | "deepseek"
8
10
  * - `options.provider`: same surface
9
11
  * - `options.model`: same surface (fallback for ergonomic
10
12
  * `createAiSession("chat-web", { model: "gemini" })`)
11
13
  *
12
14
  * Defaults to "chatgpt". Aliases: "openai"/"gpt" → chatgpt, "google" → gemini.
13
15
  *
14
- * Lifecycle:
15
- * - `boot()` lazily imports `@love-moon/chat-web`, registers its built-in
16
- * providers, and opens a queued conversation tab (headed by default).
17
- * - `runTurn(prompt)` calls `session.send(prompt)` and emits a single
18
- * `assistant_message` with the model's reply.
19
- * - `close()` releases this task's tab; the broker closes an idle browser.
20
- *
21
- * Resume: chat-web's "session" is a Chromium browser context, not a
22
- * conversation ID — there is no native cross-process resume. We synthesise
23
- * an id so the rest of ai-sdk has something stable to thread on, but
24
- * passing `resumeSessionId` does not reattach to a prior conversation.
16
+ * Login on the daemon host: conductor chat-web login kimi|deepseek.
17
+ * Managed runtime IDs use chat-web-runtime:<id> so restarts restore the same
18
+ * isolated profile and provider conversation. Legacy provider IDs are imported.
19
+ * Set chatWebRuntime: false to explicitly use the shared-profile queue.
20
+ * Explicit chatWebRuntimeSocket clients keep caller-managed lifecycle.
25
21
  */
26
22
  export class ChatWebSession extends EventEmitter<[never]> {
27
23
  constructor(backend: any, options?: {});
28
24
  backend: string;
29
25
  options: {};
30
26
  logger: any;
27
+ runtimeSocket: any;
31
28
  chatWebProvider: string;
29
+ runtimeMode: boolean;
30
+ managedRuntime: boolean;
31
+ runtimeConversationId: any;
32
+ runtimeProviderConversationId: any;
32
33
  headless: boolean;
33
34
  browserChannel: any;
34
35
  browserExecutablePath: any;
@@ -163,6 +164,8 @@ export class ChatWebSession extends EventEmitter<[never]> {
163
164
  manualResume: null;
164
165
  }>;
165
166
  getChatWebModule(): Promise<any>;
167
+ loginCommand(): string;
168
+ reviewInstructions(): string;
166
169
  boot(): Promise<void>;
167
170
  bootInternal(): Promise<void>;
168
171
  /**
@@ -4,7 +4,8 @@ import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
4
4
  import { assertMediaCapabilities, resolveTurnMedia } from "../media-input.js";
5
5
  import { normalizeContextFiles } from "../context-files.js";
6
6
  import { emitLog, normalizeLogger } from "../shared.js";
7
- const SUPPORTED_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "gemini", "kimi"]);
7
+ const SUPPORTED_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "gemini", "kimi", "deepseek"]);
8
+ const RUNTIME_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "deepseek", "kimi", "claude"]);
8
9
  const DEFAULT_CHAT_WEB_PROVIDER = "chatgpt";
9
10
  const DEFAULT_TURN_TIMEOUT_MS = 5 * 60 * 1000;
10
11
  function normalizeChatWebProvider(value) {
@@ -19,13 +20,14 @@ function normalizeChatWebProvider(value) {
19
20
  return raw;
20
21
  }
21
22
  function resolveChatWebProvider(options = {}) {
22
- const candidates = [options.chatWebProvider, options.provider, options.model];
23
- for (const candidate of candidates) {
23
+ const supported = options.chatWebRuntimeSocket || process.env.CHAT_WEB_RUNTIME_SOCKET
24
+ ? new Set([...SUPPORTED_CHAT_WEB_PROVIDERS, ...RUNTIME_CHAT_WEB_PROVIDERS]) : SUPPORTED_CHAT_WEB_PROVIDERS;
25
+ for (const candidate of [options.chatWebProvider, options.provider, options.model]) {
24
26
  const normalized = normalizeChatWebProvider(candidate);
25
27
  if (!normalized)
26
28
  continue;
27
- if (!SUPPORTED_CHAT_WEB_PROVIDERS.has(normalized)) {
28
- const error = new Error(`Unknown chat-web provider "${normalized}". Supported: ${[...SUPPORTED_CHAT_WEB_PROVIDERS].join(", ")}`);
29
+ if (!supported.has(normalized)) {
30
+ const error = new Error(`Unknown chat-web provider "${normalized}". Supported: ${[...supported].join(", ")}`);
29
31
  error.reason = "unknown_chat_web_provider";
30
32
  throw error;
31
33
  }
@@ -33,6 +35,10 @@ function resolveChatWebProvider(options = {}) {
33
35
  }
34
36
  return DEFAULT_CHAT_WEB_PROVIDER;
35
37
  }
38
+ function quoteRuntimeArgument(value) {
39
+ const text = String(value);
40
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(text) ? text : "'" + text.replaceAll("'", "'\\''") + "'";
41
+ }
36
42
  function extractErrorMessage(error) {
37
43
  if (typeof error?.message === "string" && error.message.trim()) {
38
44
  return error.message.trim();
@@ -98,27 +104,23 @@ function isPlaywrightMissingError(error) {
98
104
  /**
99
105
  * AI SDK provider that delegates to the chat-web runtime
100
106
  * (`@love-moon/chat-web`), which automates a real Chromium browser against
101
- * ChatGPT / Gemini / Kimi and ferries conversations through their web
102
- * UIs. Choose the underlying chat-web provider via:
107
+ * chatbot web UIs. Kimi and DeepSeek use isolated persistent chat workers
108
+ * with an automatically started local daemon. ChatGPT/Gemini retain the
109
+ * shared-profile queue unless a runtime socket is explicitly configured.
110
+ * Choose the underlying chat-web provider via:
103
111
  *
104
- * - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" (preferred)
112
+ * - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" | "deepseek"
105
113
  * - `options.provider`: same surface
106
114
  * - `options.model`: same surface (fallback for ergonomic
107
115
  * `createAiSession("chat-web", { model: "gemini" })`)
108
116
  *
109
117
  * Defaults to "chatgpt". Aliases: "openai"/"gpt" → chatgpt, "google" → gemini.
110
118
  *
111
- * Lifecycle:
112
- * - `boot()` lazily imports `@love-moon/chat-web`, registers its built-in
113
- * providers, and opens a queued conversation tab (headed by default).
114
- * - `runTurn(prompt)` calls `session.send(prompt)` and emits a single
115
- * `assistant_message` with the model's reply.
116
- * - `close()` releases this task's tab; the broker closes an idle browser.
117
- *
118
- * Resume: chat-web's "session" is a Chromium browser context, not a
119
- * conversation ID — there is no native cross-process resume. We synthesise
120
- * an id so the rest of ai-sdk has something stable to thread on, but
121
- * passing `resumeSessionId` does not reattach to a prior conversation.
119
+ * Login on the daemon host: conductor chat-web login kimi|deepseek.
120
+ * Managed runtime IDs use chat-web-runtime:<id> so restarts restore the same
121
+ * isolated profile and provider conversation. Legacy provider IDs are imported.
122
+ * Set chatWebRuntime: false to explicitly use the shared-profile queue.
123
+ * Explicit chatWebRuntimeSocket clients keep caller-managed lifecycle.
122
124
  */
123
125
  export class ChatWebSession extends EventEmitter {
124
126
  constructor(backend, options = {}) {
@@ -126,17 +128,25 @@ export class ChatWebSession extends EventEmitter {
126
128
  this.backend = "chat-web";
127
129
  this.options = options;
128
130
  this.logger = normalizeLogger(options.logger);
131
+ this.runtimeSocket = options.chatWebRuntimeSocket || process.env.CHAT_WEB_RUNTIME_SOCKET || "";
129
132
  this.chatWebProvider = resolveChatWebProvider(options);
130
- // Match chat-web SDK's own default (headed). Per chat-web/core/browser.ts
131
- // resolveHeadless(), headed mode is the documented safe default for
132
- // anti-bot heuristics — ChatGPT/AI Studio routinely serve unauthenticated
133
- // or challenge pages to chrome-headless-shell even when profile cookies
134
- // are valid, which masquerades as "not logged in" downstream. The old
135
- // `options.headless !== false` defaulted to true and effectively neutered
136
- // chat-web's anti-bot stance whenever the caller (daemon, serve-ai)
137
- // didn't explicitly pass a value. Users who actually want headless must
138
- // now opt in with an explicit `headless: true`.
139
- this.headless = options.headless === true;
133
+ if (!RUNTIME_CHAT_WEB_PROVIDERS.has(this.chatWebProvider))
134
+ this.runtimeSocket = "";
135
+ this.runtimeMode = Boolean(this.runtimeSocket) ||
136
+ (["kimi", "deepseek"].includes(this.chatWebProvider) && options.chatWebRuntime !== false);
137
+ this.managedRuntime = this.runtimeMode && !this.runtimeSocket;
138
+ const resumed = typeof options.resumeSessionId === "string" ? options.resumeSessionId : "";
139
+ const prefix = "chat-web-runtime:";
140
+ this.runtimeConversationId = options.chatWebConversationId ||
141
+ (this.managedRuntime ? (resumed.startsWith(prefix) ? resumed.slice(prefix.length) : undefined) : this.runtimeMode ? resumed : undefined);
142
+ this.runtimeProviderConversationId = this.managedRuntime && resumed && !resumed.startsWith(prefix) && !options.chatWebConversationId ? resumed : undefined;
143
+ if (options.chatWebDeepThink !== undefined &&
144
+ (typeof options.chatWebDeepThink !== "boolean" || this.chatWebProvider !== "deepseek" || !this.runtimeMode)) {
145
+ throw new Error("chatWebDeepThink requires a boolean and the DeepSeek chat runtime");
146
+ }
147
+ // Kimi requires headed Chromium with the tested login flow. DeepSeek's
148
+ // managed workers support headless; explicit options override either default.
149
+ this.headless = this.runtimeMode && this.chatWebProvider === "deepseek" ? options.headless !== false : options.headless === true;
140
150
  // Optional: use a specific Chromium-family binary (system Chrome /
141
151
  // Edge / explicit path) instead of Playwright's bundled
142
152
  // `chrome-headless-shell`. Useful when the user's network treats
@@ -213,12 +223,12 @@ export class ChatWebSession extends EventEmitter {
213
223
  sessionId: this.sessionId,
214
224
  sessionInfo: this.getSessionInfo(),
215
225
  useSessionFileReplyStream: this.usesSessionFileReplyStream(),
216
- resumeReady: Boolean(this.providerConversationId),
226
+ resumeReady: Boolean(this.runtimeMode ? this.runtimeConversationId : this.providerConversationId),
217
227
  manualResume: this.providerConversationUrl()
218
228
  ? { ready: true, command: this.providerConversationUrl() }
219
229
  : null,
220
230
  currentTurnStatus: this.getCurrentTurnStatus(),
221
- capabilities: { clear: true, media: PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT] },
231
+ capabilities: { clear: !this.runtimeMode, media: PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT] },
222
232
  chatWebProvider: this.chatWebProvider,
223
233
  providerConversationId: this.providerConversationId,
224
234
  providerUrl: this.providerConversationUrl(),
@@ -235,7 +245,7 @@ export class ChatWebSession extends EventEmitter {
235
245
  //
236
246
  // We expose `sessionIdDeferred: true` so the fire-side announce can
237
247
  // hold off until the real id arrives, then re-announce.
238
- const hasRealId = Boolean(this.providerConversationId);
248
+ const hasRealId = Boolean(this.runtimeMode ? this.runtimeConversationId : this.providerConversationId);
239
249
  return {
240
250
  ...this.sessionInfo,
241
251
  sessionIdDeferred: !hasRealId,
@@ -300,6 +310,21 @@ export class ChatWebSession extends EventEmitter {
300
310
  throw enriched;
301
311
  }
302
312
  }
313
+ loginCommand() {
314
+ if (!this.runtimeMode)
315
+ return `chat-web login ${this.chatWebProvider}`;
316
+ if (this.managedRuntime && (!this.options.chatWebAccountId || this.options.chatWebAccountId === "default")) {
317
+ return `conductor chat-web login ${this.chatWebProvider}`;
318
+ }
319
+ return `conductor chat-web chat login ${this.runtimeConversationId} --socket ${this.runtimeSocket}`;
320
+ }
321
+ reviewInstructions() {
322
+ if (!this.runtimeConversationId || !this.runtimeSocket)
323
+ return "";
324
+ const id = quoteRuntimeArgument(this.runtimeConversationId);
325
+ const socket = quoteRuntimeArgument(this.runtimeSocket);
326
+ return ` The previous webpage request has an uncertain outcome. On the daemon host, inspect the same conversation with: conductor chat-web chat login ${id} --socket ${socket}. After checking its result, acknowledge with: conductor chat-web chat resume ${id} --acknowledge --socket ${socket}. Then resend only the message you still want to send; acknowledgement does not replay the previous request.`;
327
+ }
303
328
  async boot() {
304
329
  if (this.booted)
305
330
  return;
@@ -310,6 +335,13 @@ export class ChatWebSession extends EventEmitter {
310
335
  await this.bootPromise;
311
336
  this.booted = true;
312
337
  }
338
+ catch (error) {
339
+ // A failed login must release this task's browser so a fresh account
340
+ // snapshot can be imported when the user retries the same task.
341
+ await this.chatSession?.close().catch(() => undefined);
342
+ this.chatSession = null;
343
+ throw error;
344
+ }
313
345
  finally {
314
346
  this.bootPromise = null;
315
347
  }
@@ -322,11 +354,9 @@ export class ChatWebSession extends EventEmitter {
322
354
  if (typeof mod.registerBuiltinProviders === "function") {
323
355
  mod.registerBuiltinProviders();
324
356
  }
325
- // New runtimes share a browser and schedule complete turns across workers.
326
- // Keep the existing session surface for older optional chat-web versions.
327
- const Session = mod.QueuedChatSession ?? mod.ChatSession;
357
+ const Session = this.runtimeMode ? mod.RuntimeChatSession : (mod.QueuedChatSession ?? mod.ChatSession);
328
358
  if (typeof Session?.open !== "function") {
329
- throw new Error("Loaded @love-moon/chat-web is missing ChatSession.open");
359
+ throw new Error(`Loaded @love-moon/chat-web is missing ${this.runtimeMode ? "RuntimeChatSession" : "ChatSession"}.open; upgrade chat-web`);
330
360
  }
331
361
  // Forward optional browser-binary overrides to chat-web. The env vars
332
362
  // CHAT_WEB_BROWSER_CHANNEL / CHAT_WEB_BROWSER_EXECUTABLE are also
@@ -338,6 +368,7 @@ export class ChatWebSession extends EventEmitter {
338
368
  if (this.browserExecutablePath)
339
369
  launch.executablePath = this.browserExecutablePath;
340
370
  this.chatSession = await Session.open(this.chatWebProvider, {
371
+ ...(this.runtimeMode ? { socketPath: this.managedRuntime ? undefined : this.runtimeSocket || undefined, conversationId: this.runtimeConversationId, providerConversationId: this.runtimeProviderConversationId, accountId: this.options.chatWebAccountId, deepThink: this.options.chatWebDeepThink, releaseOnClose: this.managedRuntime } : {}),
341
372
  headless: this.headless,
342
373
  // IMPORTANT: chat-web's logger contract is `{ error, warn, info, debug }`;
343
374
  // ai-sdk's normalised logger is `{ log }`. Passing the ai-sdk logger
@@ -351,9 +382,23 @@ export class ChatWebSession extends EventEmitter {
351
382
  this.chatSession = null;
352
383
  throw this.createSessionClosedError();
353
384
  }
354
- const loggedIn = await this.chatSession.isLoggedIn().catch(() => false);
355
- if (!loggedIn) {
356
- const error = new Error(`chat-web provider "${this.chatWebProvider}" is not logged in. Run: chat-web login ${this.chatWebProvider}`);
385
+ if (this.runtimeMode) {
386
+ this.runtimeConversationId = this.chatSession.id;
387
+ this.runtimeSocket = this.chatSession.client?.socketPath || this.runtimeSocket;
388
+ this.sessionId = this.managedRuntime ? "chat-web-runtime:" + this.runtimeConversationId : this.runtimeConversationId;
389
+ if (this.chatSession.providerConversationId)
390
+ this.providerConversationId = this.chatSession.providerConversationId;
391
+ this.sessionInfo = { ...this.sessionInfo, sessionId: this.sessionId };
392
+ this.emit("session", this.getSessionInfo());
393
+ }
394
+ const readiness = this.runtimeMode && typeof this.chatSession.prepare === "function"
395
+ ? await this.chatSession.prepare()
396
+ : { ready: this.runtimeMode ? await this.chatSession.isLoggedIn() : await this.chatSession.isLoggedIn().catch(() => false), authRequired: true };
397
+ if (!readiness.ready && !readiness.queued) {
398
+ if (readiness.authRequired !== true) {
399
+ throw Object.assign(new Error("Chat page did not become ready; wait for it to finish loading and retry"), { code: "PAGE_NOT_READY" });
400
+ }
401
+ const error = new Error(`Chat requires login. On this daemon host run: ${this.loginCommand()}`);
357
402
  error.reason = "not_logged_in";
358
403
  this.emit("auth_required", {
359
404
  reason: "login_required",
@@ -375,6 +420,11 @@ export class ChatWebSession extends EventEmitter {
375
420
  * for exactly this window.
376
421
  */
377
422
  async runClear() {
423
+ if (this.runtimeMode) {
424
+ const error = new Error("Create a new runtime conversation to clear an isolated chat");
425
+ error.reason = "unsupported_clear";
426
+ throw error;
427
+ }
378
428
  if (this.closeRequested) {
379
429
  throw this.createSessionClosedError();
380
430
  }
@@ -425,11 +475,12 @@ export class ChatWebSession extends EventEmitter {
425
475
  if (normalized === this.providerConversationId)
426
476
  return;
427
477
  this.providerConversationId = normalized;
428
- this.sessionId = normalized;
478
+ if (!this.runtimeMode)
479
+ this.sessionId = normalized;
429
480
  this.sessionInfo = {
430
481
  ...(this.sessionInfo || {}),
431
482
  backend: this.backend,
432
- sessionId: normalized,
483
+ sessionId: this.sessionId,
433
484
  model: this.chatWebProvider,
434
485
  modelProvider: "chat-web",
435
486
  providerConversationId: normalized,
@@ -448,8 +499,12 @@ export class ChatWebSession extends EventEmitter {
448
499
  switch (this.chatWebProvider) {
449
500
  case "chatgpt":
450
501
  return `https://chatgpt.com/c/${this.providerConversationId}`;
502
+ case "deepseek":
503
+ return `https://chat.deepseek.com/a/chat/s/${this.providerConversationId}`;
451
504
  case "kimi":
452
505
  return `https://www.kimi.com/chat/${encodeURIComponent(this.providerConversationId)}`;
506
+ case "claude":
507
+ return `https://claude.ai/chat/${this.providerConversationId}`;
453
508
  case "gemini":
454
509
  // chat-web's `gemini` provider targets AI Studio
455
510
  // (aistudio.google.com/prompts/new_chat) — that's the free web
@@ -498,10 +553,20 @@ export class ChatWebSession extends EventEmitter {
498
553
  await this.boot();
499
554
  if (this.closeRequested)
500
555
  throw this.createSessionClosedError();
556
+ if (this.currentTurn.aborted)
557
+ throw new DOMException("Aborted", "AbortError");
501
558
  this.turnAttempted = true;
502
559
  const result = await this.chatSession.send(prompt, {
503
560
  timeoutMs: this.turnTimeoutMs,
504
561
  signal: this.currentTurn.controller.signal,
562
+ onStatus: (phase) => {
563
+ if (phase === "queued" || phase === "starting")
564
+ void this.emitWorkingStatus({
565
+ phase: phase === "queued" ? "queued" : "turn_started",
566
+ reply_in_progress: true,
567
+ status_line: phase === "queued" ? `chat-web (${this.chatWebProvider}) waiting for a browser worker` : `chat-web (${this.chatWebProvider}) is working`,
568
+ }, onProgress);
569
+ },
505
570
  onQueue: (position) => {
506
571
  void this.emitWorkingStatus({
507
572
  phase: position > 0 ? "queued" : "turn_started",
@@ -566,6 +631,17 @@ export class ChatWebSession extends EventEmitter {
566
631
  catch (error) {
567
632
  const message = extractErrorMessage(error);
568
633
  const code = typeof error?.code === "string" ? error.code : "";
634
+ if (this.runtimeMode && (code === "AUTH_REQUIRED" || /Provider HTTP 401/.test(message))) {
635
+ const login = this.loginCommand();
636
+ error.message = `Browser login needs renewal. On this daemon host run: ${login}.`;
637
+ error.reason = "not_logged_in";
638
+ }
639
+ if (this.runtimeMode && (code === "REVIEW_REQUIRED" || error?.turnStatus === "uncertain")) {
640
+ error.message = (error.message || message) + this.reviewInstructions();
641
+ }
642
+ if (this.runtimeMode && (code === "AUTH_REQUIRED" || /Provider HTTP 401/.test(message))) {
643
+ this.emit("auth_required", { reason: "login_required", message: error.message, provider: this.chatWebProvider });
644
+ }
569
645
  // Surface chat-web's typed "needs API key" / "permission denied"
570
646
  // errors as auth_required so the UI / daemon can route them
571
647
  // through the same flow as ChatGPT's "not logged in" — they're
@@ -580,7 +656,7 @@ export class ChatWebSession extends EventEmitter {
580
656
  }
581
657
  await this.emitTerminalWorkingStatus({
582
658
  phase: this.currentTurn?.aborted ? "turn_interrupted" : "turn_failed",
583
- status_done_line: message,
659
+ status_done_line: error.message || message,
584
660
  }, onProgress);
585
661
  throw error;
586
662
  }
@@ -590,12 +666,18 @@ export class ChatWebSession extends EventEmitter {
590
666
  }
591
667
  }
592
668
  async interruptCurrentTurn() {
669
+ // Direct chat-web ChatSession doesn't expose a turn abort — the underlying
670
+ // Chromium tab is still busy with the model. We mark the turn as
671
+ // aborted so the next status emission is "interrupted", but the
672
+ // assistant_message may still arrive once the model finishes.
593
673
  if (this.currentTurn) {
594
674
  this.currentTurn.aborted = true;
595
675
  // The shared broker removes a pending turn, or stops/closes the active
596
676
  // task's tab before moving to the next task. No queued prompt is sent.
597
677
  this.currentTurn.controller.abort();
598
678
  }
679
+ if (this.runtimeMode)
680
+ await this.chatSession?.interruptCurrentTurn?.();
599
681
  }
600
682
  async close() {
601
683
  if (this.closed)
@@ -1,9 +1,7 @@
1
1
  /**
2
- * chat-web's "session" is a Chromium browser context, not a conversation id.
3
- * There is no native resume primitive: closing and reopening produces a
4
- * fresh ChatSession with a new in-memory chat. We satisfy ai-sdk's resume
5
- * contract by surfacing a synthetic session id and a "best effort" context,
6
- * but downstream callers should treat resume here as a no-op.
2
+ * Managed Kimi/DeepSeek sessions use a stable chat-web-runtime:<id> token.
3
+ * RuntimeChatSession restores the saved isolated profile and provider URL.
4
+ * Other chat-web modes retain their existing provider-specific resume behavior.
7
5
  */
8
6
  export function buildCliArgs(sessionId: any): string[];
9
7
  export function findSessionPath(): Promise<null>;
@@ -1,11 +1,9 @@
1
1
  import { buildResumeContext, normalizeSessionId } from "./shared.js";
2
2
  export const BACKEND = "chat-web";
3
3
  /**
4
- * chat-web's "session" is a Chromium browser context, not a conversation id.
5
- * There is no native resume primitive: closing and reopening produces a
6
- * fresh ChatSession with a new in-memory chat. We satisfy ai-sdk's resume
7
- * contract by surfacing a synthetic session id and a "best effort" context,
8
- * but downstream callers should treat resume here as a no-op.
4
+ * Managed Kimi/DeepSeek sessions use a stable chat-web-runtime:<id> token.
5
+ * RuntimeChatSession restores the saved isolated profile and provider URL.
6
+ * Other chat-web modes retain their existing provider-specific resume behavior.
9
7
  */
10
8
  export function buildCliArgs(sessionId) {
11
9
  // chat-web has no CLI to pass these args to (it's an in-process SDK), but
@@ -38,7 +36,9 @@ export async function resolveResumeContext(sessionId, options = {}) {
38
36
  cwd,
39
37
  cwdSource: "process_cwd",
40
38
  extraDebug: {
41
- note: "chat-web does not support cross-process conversation resume; a fresh ChatSession will be opened.",
39
+ note: normalizedSessionId.startsWith("chat-web-runtime:")
40
+ ? "The isolated chat runtime restores this saved conversation; a missing runtime id is an error."
41
+ : "The selected chat-web transport resolves this provider conversation id when opening.",
42
42
  },
43
43
  });
44
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.17.2",
3
+ "version": "0.18.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -53,12 +53,12 @@
53
53
  "zod": "^4.1.5"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@love-moon/chat-web": "^0.17.2"
56
+ "@love-moon/chat-web": "^0.18.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^22.10.2",
60
60
  "tsx": "^4.20.6",
61
61
  "typescript": "^5.6.3"
62
62
  },
63
- "gitCommitId": "1671ee1"
63
+ "gitCommitId": "825d90a"
64
64
  }