@love-moon/ai-sdk 0.17.1 → 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,99 @@
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
+
71
+ ## 0.17.2
72
+
73
+ ### Patch Changes
74
+
75
+ - b124565: Add QueuedChatSession with a shared local browser broker and a FIFO turn queue
76
+ across processes. Each task retains its own conversation tab; idle tasks no
77
+ longer hold exclusive access to the provider profile. The AI SDK uses queued
78
+ sessions when available and reports waiting positions through working status.
79
+
80
+ Cancel pending turns on disconnect, stop/close failed conversation tabs before
81
+ advancing the queue, and release the browser after its last client exits.
82
+ Keep the exclusive ChatSession API available for direct Playwright access.
83
+
84
+ Keep logical sessions usable after an active stop, timeout, or provider failure.
85
+ Restore the original provider conversation for the next explicit user operation;
86
+ require explicit clear when safe restoration is unavailable, and never replay a
87
+ failed prompt. Allow clear after a failed first submission. Correct the Windows
88
+ named-pipe address escaping.
89
+
90
+ - 0a9488e: Add Kimi website text conversations with persistent login, multi-turn context,
91
+ reply progress, new chats and browser diagnostics. Expose the provider through
92
+ ai-sdk's chat-web backend and the web-kimi Conductor configuration alias.
93
+
94
+ Explicit unknown chat-web providers now fail with a clear error instead of
95
+ silently opening ChatGPT. The existing kimi CLI backend is unchanged.
96
+
3
97
  ## 0.17.1
4
98
 
5
99
  ### 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 / DeepSeek 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" (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 long-lived `ChatSession` (headed by default).
17
- * - `runTurn(prompt)` calls `session.send(prompt)` and emits a single
18
- * `assistant_message` with the model's reply.
19
- * - `close()` tears the Chromium context down.
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;
@@ -50,6 +51,7 @@ export class ChatWebSession extends EventEmitter<[never]> {
50
51
  closed: boolean;
51
52
  currentTurn: {
52
53
  aborted: boolean;
54
+ controller: AbortController;
53
55
  } | null;
54
56
  currentTurnStatus: {
55
57
  source: string;
@@ -65,6 +67,7 @@ export class ChatWebSession extends EventEmitter<[never]> {
65
67
  session_file_path: undefined;
66
68
  updated_at: string;
67
69
  } | null;
70
+ turnAttempted: boolean;
68
71
  sessionMessageHandler: any;
69
72
  workingStatusHandler: any;
70
73
  activeReplyTarget: string;
@@ -161,6 +164,8 @@ export class ChatWebSession extends EventEmitter<[never]> {
161
164
  manualResume: null;
162
165
  }>;
163
166
  getChatWebModule(): Promise<any>;
167
+ loginCommand(): string;
168
+ reviewInstructions(): string;
164
169
  boot(): Promise<void>;
165
170
  bootInternal(): Promise<void>;
166
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"]);
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,15 +20,25 @@ 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
- if (SUPPORTED_CHAT_WEB_PROVIDERS.has(normalized)) {
26
- return normalized;
27
+ if (!normalized)
28
+ continue;
29
+ if (!supported.has(normalized)) {
30
+ const error = new Error(`Unknown chat-web provider "${normalized}". Supported: ${[...supported].join(", ")}`);
31
+ error.reason = "unknown_chat_web_provider";
32
+ throw error;
27
33
  }
34
+ return normalized;
28
35
  }
29
36
  return DEFAULT_CHAT_WEB_PROVIDER;
30
37
  }
38
+ function quoteRuntimeArgument(value) {
39
+ const text = String(value);
40
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(text) ? text : "'" + text.replaceAll("'", "'\\''") + "'";
41
+ }
31
42
  function extractErrorMessage(error) {
32
43
  if (typeof error?.message === "string" && error.message.trim()) {
33
44
  return error.message.trim();
@@ -93,27 +104,23 @@ function isPlaywrightMissingError(error) {
93
104
  /**
94
105
  * AI SDK provider that delegates to the chat-web runtime
95
106
  * (`@love-moon/chat-web`), which automates a real Chromium browser against
96
- * ChatGPT / Gemini / DeepSeek and ferries conversations through their web
97
- * 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:
98
111
  *
99
- * - `options.chatWebProvider`: "chatgpt" | "gemini" (preferred)
112
+ * - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" | "deepseek"
100
113
  * - `options.provider`: same surface
101
114
  * - `options.model`: same surface (fallback for ergonomic
102
115
  * `createAiSession("chat-web", { model: "gemini" })`)
103
116
  *
104
117
  * Defaults to "chatgpt". Aliases: "openai"/"gpt" → chatgpt, "google" → gemini.
105
118
  *
106
- * Lifecycle:
107
- * - `boot()` lazily imports `@love-moon/chat-web`, registers its built-in
108
- * providers, and opens a long-lived `ChatSession` (headed by default).
109
- * - `runTurn(prompt)` calls `session.send(prompt)` and emits a single
110
- * `assistant_message` with the model's reply.
111
- * - `close()` tears the Chromium context down.
112
- *
113
- * Resume: chat-web's "session" is a Chromium browser context, not a
114
- * conversation ID — there is no native cross-process resume. We synthesise
115
- * an id so the rest of ai-sdk has something stable to thread on, but
116
- * 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.
117
124
  */
118
125
  export class ChatWebSession extends EventEmitter {
119
126
  constructor(backend, options = {}) {
@@ -121,17 +128,25 @@ export class ChatWebSession extends EventEmitter {
121
128
  this.backend = "chat-web";
122
129
  this.options = options;
123
130
  this.logger = normalizeLogger(options.logger);
131
+ this.runtimeSocket = options.chatWebRuntimeSocket || process.env.CHAT_WEB_RUNTIME_SOCKET || "";
124
132
  this.chatWebProvider = resolveChatWebProvider(options);
125
- // Match chat-web SDK's own default (headed). Per chat-web/core/browser.ts
126
- // resolveHeadless(), headed mode is the documented safe default for
127
- // anti-bot heuristics — ChatGPT/AI Studio routinely serve unauthenticated
128
- // or challenge pages to chrome-headless-shell even when profile cookies
129
- // are valid, which masquerades as "not logged in" downstream. The old
130
- // `options.headless !== false` defaulted to true and effectively neutered
131
- // chat-web's anti-bot stance whenever the caller (daemon, serve-ai)
132
- // didn't explicitly pass a value. Users who actually want headless must
133
- // now opt in with an explicit `headless: true`.
134
- 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;
135
150
  // Optional: use a specific Chromium-family binary (system Chrome /
136
151
  // Edge / explicit path) instead of Playwright's bundled
137
152
  // `chrome-headless-shell`. Useful when the user's network treats
@@ -176,6 +191,7 @@ export class ChatWebSession extends EventEmitter {
176
191
  this.closed = false;
177
192
  this.currentTurn = null;
178
193
  this.currentTurnStatus = null;
194
+ this.turnAttempted = false;
179
195
  this.sessionMessageHandler = null;
180
196
  this.workingStatusHandler = null;
181
197
  this.activeReplyTarget = "";
@@ -207,12 +223,12 @@ export class ChatWebSession extends EventEmitter {
207
223
  sessionId: this.sessionId,
208
224
  sessionInfo: this.getSessionInfo(),
209
225
  useSessionFileReplyStream: this.usesSessionFileReplyStream(),
210
- resumeReady: Boolean(this.providerConversationId),
226
+ resumeReady: Boolean(this.runtimeMode ? this.runtimeConversationId : this.providerConversationId),
211
227
  manualResume: this.providerConversationUrl()
212
228
  ? { ready: true, command: this.providerConversationUrl() }
213
229
  : null,
214
230
  currentTurnStatus: this.getCurrentTurnStatus(),
215
- capabilities: { clear: true, media: PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT] },
231
+ capabilities: { clear: !this.runtimeMode, media: PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT] },
216
232
  chatWebProvider: this.chatWebProvider,
217
233
  providerConversationId: this.providerConversationId,
218
234
  providerUrl: this.providerConversationUrl(),
@@ -229,7 +245,7 @@ export class ChatWebSession extends EventEmitter {
229
245
  //
230
246
  // We expose `sessionIdDeferred: true` so the fire-side announce can
231
247
  // hold off until the real id arrives, then re-announce.
232
- const hasRealId = Boolean(this.providerConversationId);
248
+ const hasRealId = Boolean(this.runtimeMode ? this.runtimeConversationId : this.providerConversationId);
233
249
  return {
234
250
  ...this.sessionInfo,
235
251
  sessionIdDeferred: !hasRealId,
@@ -294,6 +310,21 @@ export class ChatWebSession extends EventEmitter {
294
310
  throw enriched;
295
311
  }
296
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
+ }
297
328
  async boot() {
298
329
  if (this.booted)
299
330
  return;
@@ -304,6 +335,13 @@ export class ChatWebSession extends EventEmitter {
304
335
  await this.bootPromise;
305
336
  this.booted = true;
306
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
+ }
307
345
  finally {
308
346
  this.bootPromise = null;
309
347
  }
@@ -316,8 +354,9 @@ export class ChatWebSession extends EventEmitter {
316
354
  if (typeof mod.registerBuiltinProviders === "function") {
317
355
  mod.registerBuiltinProviders();
318
356
  }
319
- if (typeof mod.ChatSession?.open !== "function") {
320
- throw new Error("Loaded @love-moon/chat-web is missing ChatSession.open");
357
+ const Session = this.runtimeMode ? mod.RuntimeChatSession : (mod.QueuedChatSession ?? mod.ChatSession);
358
+ if (typeof Session?.open !== "function") {
359
+ throw new Error(`Loaded @love-moon/chat-web is missing ${this.runtimeMode ? "RuntimeChatSession" : "ChatSession"}.open; upgrade chat-web`);
321
360
  }
322
361
  // Forward optional browser-binary overrides to chat-web. The env vars
323
362
  // CHAT_WEB_BROWSER_CHANNEL / CHAT_WEB_BROWSER_EXECUTABLE are also
@@ -328,7 +367,8 @@ export class ChatWebSession extends EventEmitter {
328
367
  launch.channel = this.browserChannel;
329
368
  if (this.browserExecutablePath)
330
369
  launch.executablePath = this.browserExecutablePath;
331
- this.chatSession = await mod.ChatSession.open(this.chatWebProvider, {
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 } : {}),
332
372
  headless: this.headless,
333
373
  // IMPORTANT: chat-web's logger contract is `{ error, warn, info, debug }`;
334
374
  // ai-sdk's normalised logger is `{ log }`. Passing the ai-sdk logger
@@ -342,9 +382,23 @@ export class ChatWebSession extends EventEmitter {
342
382
  this.chatSession = null;
343
383
  throw this.createSessionClosedError();
344
384
  }
345
- const loggedIn = await this.chatSession.isLoggedIn().catch(() => false);
346
- if (!loggedIn) {
347
- 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()}`);
348
402
  error.reason = "not_logged_in";
349
403
  this.emit("auth_required", {
350
404
  reason: "login_required",
@@ -366,6 +420,11 @@ export class ChatWebSession extends EventEmitter {
366
420
  * for exactly this window.
367
421
  */
368
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
+ }
369
428
  if (this.closeRequested) {
370
429
  throw this.createSessionClosedError();
371
430
  }
@@ -376,7 +435,7 @@ export class ChatWebSession extends EventEmitter {
376
435
  }
377
436
  // Nothing has been said yet — fire boots the browser at announce time, so
378
437
  // "booted" alone does not mean there is a conversation to walk away from.
379
- if (!this.providerConversationId && this.history.length === 0) {
438
+ if (!this.providerConversationId && this.history.length === 0 && !this.turnAttempted) {
380
439
  return { clear: { status: "noop" }, usage: null, metadata: {} };
381
440
  }
382
441
  await this.boot();
@@ -394,6 +453,7 @@ export class ChatWebSession extends EventEmitter {
394
453
  providerUrl: undefined,
395
454
  };
396
455
  this.history = [];
456
+ this.turnAttempted = false;
397
457
  this.emit("session", this.getSessionInfo());
398
458
  // No id to report: the provider mints one on the next reply.
399
459
  return { clear: { status: "cleared" }, usage: null, metadata: {} };
@@ -415,11 +475,12 @@ export class ChatWebSession extends EventEmitter {
415
475
  if (normalized === this.providerConversationId)
416
476
  return;
417
477
  this.providerConversationId = normalized;
418
- this.sessionId = normalized;
478
+ if (!this.runtimeMode)
479
+ this.sessionId = normalized;
419
480
  this.sessionInfo = {
420
481
  ...(this.sessionInfo || {}),
421
482
  backend: this.backend,
422
- sessionId: normalized,
483
+ sessionId: this.sessionId,
423
484
  model: this.chatWebProvider,
424
485
  modelProvider: "chat-web",
425
486
  providerConversationId: normalized,
@@ -438,6 +499,12 @@ export class ChatWebSession extends EventEmitter {
438
499
  switch (this.chatWebProvider) {
439
500
  case "chatgpt":
440
501
  return `https://chatgpt.com/c/${this.providerConversationId}`;
502
+ case "deepseek":
503
+ return `https://chat.deepseek.com/a/chat/s/${this.providerConversationId}`;
504
+ case "kimi":
505
+ return `https://www.kimi.com/chat/${encodeURIComponent(this.providerConversationId)}`;
506
+ case "claude":
507
+ return `https://claude.ai/chat/${this.providerConversationId}`;
441
508
  case "gemini":
442
509
  // chat-web's `gemini` provider targets AI Studio
443
510
  // (aistudio.google.com/prompts/new_chat) — that's the free web
@@ -476,7 +543,7 @@ export class ChatWebSession extends EventEmitter {
476
543
  error.reason = "turn_already_running";
477
544
  throw error;
478
545
  }
479
- this.currentTurn = { aborted: false };
546
+ this.currentTurn = { aborted: false, controller: new AbortController() };
480
547
  await this.emitWorkingStatus({
481
548
  phase: "turn_started",
482
549
  reply_in_progress: true,
@@ -486,8 +553,29 @@ export class ChatWebSession extends EventEmitter {
486
553
  await this.boot();
487
554
  if (this.closeRequested)
488
555
  throw this.createSessionClosedError();
556
+ if (this.currentTurn.aborted)
557
+ throw new DOMException("Aborted", "AbortError");
558
+ this.turnAttempted = true;
489
559
  const result = await this.chatSession.send(prompt, {
490
560
  timeoutMs: this.turnTimeoutMs,
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
+ },
570
+ onQueue: (position) => {
571
+ void this.emitWorkingStatus({
572
+ phase: position > 0 ? "queued" : "turn_started",
573
+ reply_in_progress: true,
574
+ status_line: position > 0
575
+ ? `chat-web (${this.chatWebProvider}) queued: ${position} ahead`
576
+ : `chat-web (${this.chatWebProvider}) is working`,
577
+ }, onProgress);
578
+ },
491
579
  onProgress: (text) => {
492
580
  // chat-web's onProgress fires while streaming text grows; we
493
581
  // forward those as working_status updates with a short preview.
@@ -543,6 +631,17 @@ export class ChatWebSession extends EventEmitter {
543
631
  catch (error) {
544
632
  const message = extractErrorMessage(error);
545
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
+ }
546
645
  // Surface chat-web's typed "needs API key" / "permission denied"
547
646
  // errors as auth_required so the UI / daemon can route them
548
647
  // through the same flow as ChatGPT's "not logged in" — they're
@@ -557,7 +656,7 @@ export class ChatWebSession extends EventEmitter {
557
656
  }
558
657
  await this.emitTerminalWorkingStatus({
559
658
  phase: this.currentTurn?.aborted ? "turn_interrupted" : "turn_failed",
560
- status_done_line: message,
659
+ status_done_line: error.message || message,
561
660
  }, onProgress);
562
661
  throw error;
563
662
  }
@@ -567,13 +666,18 @@ export class ChatWebSession extends EventEmitter {
567
666
  }
568
667
  }
569
668
  async interruptCurrentTurn() {
570
- // chat-web's ChatSession doesn't expose a turn abort — the underlying
669
+ // Direct chat-web ChatSession doesn't expose a turn abort — the underlying
571
670
  // Chromium tab is still busy with the model. We mark the turn as
572
671
  // aborted so the next status emission is "interrupted", but the
573
672
  // assistant_message may still arrive once the model finishes.
574
673
  if (this.currentTurn) {
575
674
  this.currentTurn.aborted = true;
675
+ // The shared broker removes a pending turn, or stops/closes the active
676
+ // task's tab before moving to the next task. No queued prompt is sent.
677
+ this.currentTurn.controller.abort();
576
678
  }
679
+ if (this.runtimeMode)
680
+ await this.chatSession?.interruptCurrentTurn?.();
577
681
  }
578
682
  async close() {
579
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.1",
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.1"
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": "e3dbbad"
63
+ "gitCommitId": "825d90a"
64
64
  }