@love-moon/ai-sdk 0.17.0 → 0.17.2

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,45 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.17.2
4
+
5
+ ### Patch Changes
6
+
7
+ - b124565: Add QueuedChatSession with a shared local browser broker and a FIFO turn queue
8
+ across processes. Each task retains its own conversation tab; idle tasks no
9
+ longer hold exclusive access to the provider profile. The AI SDK uses queued
10
+ sessions when available and reports waiting positions through working status.
11
+
12
+ Cancel pending turns on disconnect, stop/close failed conversation tabs before
13
+ advancing the queue, and release the browser after its last client exits.
14
+ Keep the exclusive ChatSession API available for direct Playwright access.
15
+
16
+ Keep logical sessions usable after an active stop, timeout, or provider failure.
17
+ Restore the original provider conversation for the next explicit user operation;
18
+ require explicit clear when safe restoration is unavailable, and never replay a
19
+ failed prompt. Allow clear after a failed first submission. Correct the Windows
20
+ named-pipe address escaping.
21
+
22
+ - 0a9488e: Add Kimi website text conversations with persistent login, multi-turn context,
23
+ reply progress, new chats and browser diagnostics. Expose the provider through
24
+ ai-sdk's chat-web backend and the web-kimi Conductor configuration alias.
25
+
26
+ Explicit unknown chat-web providers now fail with a clear error instead of
27
+ silently opening ChatGPT. The existing kimi CLI backend is unchanged.
28
+
29
+ ## 0.17.1
30
+
31
+ ### Patch Changes
32
+
33
+ - 7e92c0f: When a task's model quota runs out ("You've hit your session limit · resets
34
+ 1:20am"), fire no longer stops there. It reads the reset time, schedules a
35
+ one-off Conductor scheduled message to continue a minute or few after the
36
+ reset, and posts a single notice. Messages that arrive while waiting are queued
37
+ and handed to the resume turn together instead of each failing with another
38
+ "处理失败". The resume shows up in the task's scheduled-message list, where it
39
+ can be canceled. This also covers a quota hit while Claude works on its own
40
+ between user turns: ai-sdk now reports such a background turn that ends in an
41
+ error result (or a process exit after it) as a failed turn.
42
+
3
43
  ## 0.17.0
4
44
 
5
45
  ### Patch Changes
@@ -1,10 +1,10 @@
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
4
+ * ChatGPT / Gemini / Kimi and ferries conversations through their web
5
5
  * UIs. Choose the underlying chat-web provider via:
6
6
  *
7
- * - `options.chatWebProvider`: "chatgpt" | "gemini" (preferred)
7
+ * - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" (preferred)
8
8
  * - `options.provider`: same surface
9
9
  * - `options.model`: same surface (fallback for ergonomic
10
10
  * `createAiSession("chat-web", { model: "gemini" })`)
@@ -13,10 +13,10 @@
13
13
  *
14
14
  * Lifecycle:
15
15
  * - `boot()` lazily imports `@love-moon/chat-web`, registers its built-in
16
- * providers, and opens a long-lived `ChatSession` (headed by default).
16
+ * providers, and opens a queued conversation tab (headed by default).
17
17
  * - `runTurn(prompt)` calls `session.send(prompt)` and emits a single
18
18
  * `assistant_message` with the model's reply.
19
- * - `close()` tears the Chromium context down.
19
+ * - `close()` releases this task's tab; the broker closes an idle browser.
20
20
  *
21
21
  * Resume: chat-web's "session" is a Chromium browser context, not a
22
22
  * conversation ID — there is no native cross-process resume. We synthesise
@@ -50,6 +50,7 @@ export class ChatWebSession extends EventEmitter<[never]> {
50
50
  closed: boolean;
51
51
  currentTurn: {
52
52
  aborted: boolean;
53
+ controller: AbortController;
53
54
  } | null;
54
55
  currentTurnStatus: {
55
56
  source: string;
@@ -65,6 +66,7 @@ export class ChatWebSession extends EventEmitter<[never]> {
65
66
  session_file_path: undefined;
66
67
  updated_at: string;
67
68
  } | null;
69
+ turnAttempted: boolean;
68
70
  sessionMessageHandler: any;
69
71
  workingStatusHandler: any;
70
72
  activeReplyTarget: string;
@@ -4,7 +4,7 @@ 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"]);
8
8
  const DEFAULT_CHAT_WEB_PROVIDER = "chatgpt";
9
9
  const DEFAULT_TURN_TIMEOUT_MS = 5 * 60 * 1000;
10
10
  function normalizeChatWebProvider(value) {
@@ -22,9 +22,14 @@ function resolveChatWebProvider(options = {}) {
22
22
  const candidates = [options.chatWebProvider, options.provider, options.model];
23
23
  for (const candidate of candidates) {
24
24
  const normalized = normalizeChatWebProvider(candidate);
25
- if (SUPPORTED_CHAT_WEB_PROVIDERS.has(normalized)) {
26
- return normalized;
25
+ if (!normalized)
26
+ 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
+ error.reason = "unknown_chat_web_provider";
30
+ throw error;
27
31
  }
32
+ return normalized;
28
33
  }
29
34
  return DEFAULT_CHAT_WEB_PROVIDER;
30
35
  }
@@ -93,10 +98,10 @@ function isPlaywrightMissingError(error) {
93
98
  /**
94
99
  * AI SDK provider that delegates to the chat-web runtime
95
100
  * (`@love-moon/chat-web`), which automates a real Chromium browser against
96
- * ChatGPT / Gemini / DeepSeek and ferries conversations through their web
101
+ * ChatGPT / Gemini / Kimi and ferries conversations through their web
97
102
  * UIs. Choose the underlying chat-web provider via:
98
103
  *
99
- * - `options.chatWebProvider`: "chatgpt" | "gemini" (preferred)
104
+ * - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" (preferred)
100
105
  * - `options.provider`: same surface
101
106
  * - `options.model`: same surface (fallback for ergonomic
102
107
  * `createAiSession("chat-web", { model: "gemini" })`)
@@ -105,10 +110,10 @@ function isPlaywrightMissingError(error) {
105
110
  *
106
111
  * Lifecycle:
107
112
  * - `boot()` lazily imports `@love-moon/chat-web`, registers its built-in
108
- * providers, and opens a long-lived `ChatSession` (headed by default).
113
+ * providers, and opens a queued conversation tab (headed by default).
109
114
  * - `runTurn(prompt)` calls `session.send(prompt)` and emits a single
110
115
  * `assistant_message` with the model's reply.
111
- * - `close()` tears the Chromium context down.
116
+ * - `close()` releases this task's tab; the broker closes an idle browser.
112
117
  *
113
118
  * Resume: chat-web's "session" is a Chromium browser context, not a
114
119
  * conversation ID — there is no native cross-process resume. We synthesise
@@ -176,6 +181,7 @@ export class ChatWebSession extends EventEmitter {
176
181
  this.closed = false;
177
182
  this.currentTurn = null;
178
183
  this.currentTurnStatus = null;
184
+ this.turnAttempted = false;
179
185
  this.sessionMessageHandler = null;
180
186
  this.workingStatusHandler = null;
181
187
  this.activeReplyTarget = "";
@@ -316,7 +322,10 @@ export class ChatWebSession extends EventEmitter {
316
322
  if (typeof mod.registerBuiltinProviders === "function") {
317
323
  mod.registerBuiltinProviders();
318
324
  }
319
- if (typeof mod.ChatSession?.open !== "function") {
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;
328
+ if (typeof Session?.open !== "function") {
320
329
  throw new Error("Loaded @love-moon/chat-web is missing ChatSession.open");
321
330
  }
322
331
  // Forward optional browser-binary overrides to chat-web. The env vars
@@ -328,7 +337,7 @@ export class ChatWebSession extends EventEmitter {
328
337
  launch.channel = this.browserChannel;
329
338
  if (this.browserExecutablePath)
330
339
  launch.executablePath = this.browserExecutablePath;
331
- this.chatSession = await mod.ChatSession.open(this.chatWebProvider, {
340
+ this.chatSession = await Session.open(this.chatWebProvider, {
332
341
  headless: this.headless,
333
342
  // IMPORTANT: chat-web's logger contract is `{ error, warn, info, debug }`;
334
343
  // ai-sdk's normalised logger is `{ log }`. Passing the ai-sdk logger
@@ -376,7 +385,7 @@ export class ChatWebSession extends EventEmitter {
376
385
  }
377
386
  // Nothing has been said yet — fire boots the browser at announce time, so
378
387
  // "booted" alone does not mean there is a conversation to walk away from.
379
- if (!this.providerConversationId && this.history.length === 0) {
388
+ if (!this.providerConversationId && this.history.length === 0 && !this.turnAttempted) {
380
389
  return { clear: { status: "noop" }, usage: null, metadata: {} };
381
390
  }
382
391
  await this.boot();
@@ -394,6 +403,7 @@ export class ChatWebSession extends EventEmitter {
394
403
  providerUrl: undefined,
395
404
  };
396
405
  this.history = [];
406
+ this.turnAttempted = false;
397
407
  this.emit("session", this.getSessionInfo());
398
408
  // No id to report: the provider mints one on the next reply.
399
409
  return { clear: { status: "cleared" }, usage: null, metadata: {} };
@@ -438,6 +448,8 @@ export class ChatWebSession extends EventEmitter {
438
448
  switch (this.chatWebProvider) {
439
449
  case "chatgpt":
440
450
  return `https://chatgpt.com/c/${this.providerConversationId}`;
451
+ case "kimi":
452
+ return `https://www.kimi.com/chat/${encodeURIComponent(this.providerConversationId)}`;
441
453
  case "gemini":
442
454
  // chat-web's `gemini` provider targets AI Studio
443
455
  // (aistudio.google.com/prompts/new_chat) — that's the free web
@@ -476,7 +488,7 @@ export class ChatWebSession extends EventEmitter {
476
488
  error.reason = "turn_already_running";
477
489
  throw error;
478
490
  }
479
- this.currentTurn = { aborted: false };
491
+ this.currentTurn = { aborted: false, controller: new AbortController() };
480
492
  await this.emitWorkingStatus({
481
493
  phase: "turn_started",
482
494
  reply_in_progress: true,
@@ -486,8 +498,19 @@ export class ChatWebSession extends EventEmitter {
486
498
  await this.boot();
487
499
  if (this.closeRequested)
488
500
  throw this.createSessionClosedError();
501
+ this.turnAttempted = true;
489
502
  const result = await this.chatSession.send(prompt, {
490
503
  timeoutMs: this.turnTimeoutMs,
504
+ signal: this.currentTurn.controller.signal,
505
+ onQueue: (position) => {
506
+ void this.emitWorkingStatus({
507
+ phase: position > 0 ? "queued" : "turn_started",
508
+ reply_in_progress: true,
509
+ status_line: position > 0
510
+ ? `chat-web (${this.chatWebProvider}) queued: ${position} ahead`
511
+ : `chat-web (${this.chatWebProvider}) is working`,
512
+ }, onProgress);
513
+ },
491
514
  onProgress: (text) => {
492
515
  // chat-web's onProgress fires while streaming text grows; we
493
516
  // forward those as working_status updates with a short preview.
@@ -567,12 +590,11 @@ export class ChatWebSession extends EventEmitter {
567
590
  }
568
591
  }
569
592
  async interruptCurrentTurn() {
570
- // chat-web's ChatSession doesn't expose a turn abort — the underlying
571
- // Chromium tab is still busy with the model. We mark the turn as
572
- // aborted so the next status emission is "interrupted", but the
573
- // assistant_message may still arrive once the model finishes.
574
593
  if (this.currentTurn) {
575
594
  this.currentTurn.aborted = true;
595
+ // The shared broker removes a pending turn, or stops/closes the active
596
+ // task's tab before moving to the next task. No queued prompt is sent.
597
+ this.currentTurn.controller.abort();
576
598
  }
577
599
  }
578
600
  async close() {
@@ -553,6 +553,8 @@ export class ClaudeAgentSdkSession extends EventEmitter {
553
553
  status_done_line: payload?.status_done_line,
554
554
  reply_preview: payload?.reply_preview,
555
555
  thread_id: this.sessionId || undefined,
556
+ // A turn claude ran on its own, between user turns (see dispatchLiveMessage).
557
+ ...(payload?.background === true ? { background: true } : {}),
556
558
  };
557
559
  this.updateCurrentTurnStatus(normalized);
558
560
  const snapshot = this.getCurrentTurnStatus();
@@ -1123,6 +1125,17 @@ export class ClaudeAgentSdkSession extends EventEmitter {
1123
1125
  if (backgroundTurn) {
1124
1126
  await this.finishBackgroundTurn(backgroundTurn, null, failure);
1125
1127
  }
1128
+ else if (failure && !this.closeRequested && !this.currentTurn) {
1129
+ // The process died after a background turn's result, e.g. an error
1130
+ // result (quota hit) makes claude exit right after reporting it. No turn
1131
+ // is left to own the failure, so report it as a background one.
1132
+ await this.emitWorkingStatus({
1133
+ phase: "turn_failed",
1134
+ status_done_line: failure?.message || String(failure),
1135
+ reply_in_progress: false,
1136
+ background: true,
1137
+ });
1138
+ }
1126
1139
  }
1127
1140
  /**
1128
1141
  * Routes the long-lived query's output. A result ends the pending turn when it
@@ -1155,9 +1168,13 @@ export class ClaudeAgentSdkSession extends EventEmitter {
1155
1168
  await this.finishBackgroundTurn(target, message);
1156
1169
  }
1157
1170
  async finishBackgroundTurn(backgroundTurn, resultMessage, failure = null) {
1158
- const failed = Boolean(failure) || (resultMessage && resultMessage.subtype !== "success");
1171
+ // An error result (e.g. "You've hit your session limit") can carry subtype
1172
+ // "success"; `is_error` is what marks it.
1173
+ const errorResult = Boolean(resultMessage && (resultMessage.subtype !== "success" || resultMessage.is_error === true));
1174
+ const failed = Boolean(failure) || errorResult;
1159
1175
  const text = normalizeText(resultMessage?.result);
1160
- if (!failed && text && !backgroundTurn.emittedAssistantMessage) {
1176
+ // An error result's text still reaches the chat, as it always has.
1177
+ if (!failure && resultMessage?.subtype === "success" && text && !backgroundTurn.emittedAssistantMessage) {
1161
1178
  await this.emitAssistantMessage(text);
1162
1179
  }
1163
1180
  // A pending user turn owns the working status.
@@ -1167,9 +1184,11 @@ export class ClaudeAgentSdkSession extends EventEmitter {
1167
1184
  await this.emitTerminalWorkingStatus(backgroundTurn, failed
1168
1185
  ? {
1169
1186
  phase: "turn_failed",
1170
- status_done_line: failure?.message || extractResultErrorMessage(resultMessage),
1187
+ status_done_line: failure?.message ||
1188
+ (resultMessage?.is_error === true && text ? text : extractResultErrorMessage(resultMessage)),
1189
+ background: true,
1171
1190
  }
1172
- : { phase: "turn_completed", status_done_line: "claude finished" });
1191
+ : { phase: "turn_completed", status_done_line: "claude finished", background: true });
1173
1192
  }
1174
1193
  async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
1175
1194
  if (this.closeRequested) {
@@ -1262,8 +1281,11 @@ export class ClaudeAgentSdkSession extends EventEmitter {
1262
1281
  reason: "missing_result",
1263
1282
  });
1264
1283
  }
1265
- if (resultMessage.subtype !== "success") {
1266
- const errorMessage = extractResultErrorMessage(resultMessage);
1284
+ // An error result (e.g. "You've hit your session limit") can carry subtype
1285
+ // "success"; `is_error` is what marks it.
1286
+ if (resultMessage.subtype !== "success" || resultMessage.is_error === true) {
1287
+ const errorText = resultMessage.is_error === true ? normalizeText(resultMessage.result) : "";
1288
+ const errorMessage = errorText || extractResultErrorMessage(resultMessage);
1267
1289
  this.maybeEmitAuthRequired(errorMessage, errorMessage);
1268
1290
  await this.emitTerminalWorkingStatus(currentTurn, {
1269
1291
  phase: "turn_failed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
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.0"
56
+ "@love-moon/chat-web": "^0.17.2"
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": "2799bc0"
63
+ "gitCommitId": "1671ee1"
64
64
  }