@love-moon/ai-sdk 0.2.30 → 0.2.32

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.
@@ -156,6 +156,9 @@ export class ClaudeAgentSdkSession extends EventEmitter {
156
156
  this.currentTurn = null;
157
157
  this.lastResult = null;
158
158
  this.rateLimitInfo = null;
159
+ this.currentTurnStatus = null;
160
+ this.currentTurnActivityAt = 0;
161
+ this.now = typeof options.now === "function" ? options.now : () => Date.now();
159
162
  this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
160
163
  this.sdkModulePromise = null;
161
164
  const envConfig = loadEnvConfig(options.configFile);
@@ -179,10 +182,14 @@ export class ClaudeAgentSdkSession extends EventEmitter {
179
182
  return this.sessionId;
180
183
  }
181
184
  get threadOptions() {
182
- const model = typeof this.options.model === "string" && this.options.model.trim()
183
- ? this.options.model.trim()
184
- : this.backend;
185
- return { model };
185
+ const model = this.sessionInfo?.model ||
186
+ (typeof this.options.model === "string" && this.options.model.trim()
187
+ ? this.options.model.trim()
188
+ : this.backend);
189
+ return {
190
+ model,
191
+ modelProvider: this.sessionInfo?.modelProvider || undefined,
192
+ };
186
193
  }
187
194
  getSnapshot() {
188
195
  return {
@@ -199,11 +206,15 @@ export class ClaudeAgentSdkSession extends EventEmitter {
199
206
  command: `claude --resume ${this.sessionId}`,
200
207
  }
201
208
  : null,
209
+ currentTurnStatus: this.getCurrentTurnStatus(),
202
210
  };
203
211
  }
204
212
  getSessionInfo() {
205
213
  return this.sessionInfo ? { ...this.sessionInfo } : null;
206
214
  }
215
+ getCurrentTurnStatus() {
216
+ return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
217
+ }
207
218
  async ensureSessionInfo() {
208
219
  return this.getSessionInfo();
209
220
  }
@@ -244,6 +255,27 @@ export class ClaudeAgentSdkSession extends EventEmitter {
244
255
  getCurrentReplyTarget() {
245
256
  return this.activeReplyTarget || this.lastReplyTarget || undefined;
246
257
  }
258
+ touchTurnActivity() {
259
+ this.currentTurnActivityAt = this.now();
260
+ }
261
+ updateCurrentTurnStatus(payload) {
262
+ const updatedAtMs = this.now();
263
+ this.currentTurnActivityAt = updatedAtMs;
264
+ this.currentTurnStatus = {
265
+ ...payload,
266
+ updated_at: new Date(updatedAtMs).toISOString(),
267
+ };
268
+ }
269
+ markTurnStartedStatus() {
270
+ this.updateCurrentTurnStatus({
271
+ source: CLAUDE_PROVIDER_VARIANT,
272
+ reply_in_progress: true,
273
+ replyTo: this.getCurrentReplyTarget(),
274
+ phase: "turn_started",
275
+ status_line: "claude is working",
276
+ thread_id: this.sessionId || undefined,
277
+ });
278
+ }
247
279
  async emitWorkingStatus(payload, onProgress = null) {
248
280
  const normalized = {
249
281
  source: CLAUDE_PROVIDER_VARIANT,
@@ -256,15 +288,18 @@ export class ClaudeAgentSdkSession extends EventEmitter {
256
288
  reply_preview: payload?.reply_preview,
257
289
  thread_id: this.sessionId || undefined,
258
290
  };
291
+ this.updateCurrentTurnStatus(normalized);
292
+ const snapshot = this.getCurrentTurnStatus();
259
293
  if (typeof onProgress === "function") {
260
- onProgress(normalized);
294
+ onProgress(snapshot);
261
295
  }
262
296
  if (typeof this.workingStatusHandler === "function") {
263
- await this.workingStatusHandler(normalized);
297
+ await this.workingStatusHandler(snapshot);
264
298
  }
265
- this.emit("working_status", normalized);
299
+ this.emit("working_status", snapshot);
266
300
  }
267
301
  async emitAssistantMessage(text) {
302
+ this.touchTurnActivity();
268
303
  const payload = {
269
304
  text,
270
305
  preserveWhitespace: true,
@@ -338,8 +373,27 @@ export class ClaudeAgentSdkSession extends EventEmitter {
338
373
  };
339
374
  }
340
375
  let timer = null;
341
- const promise = new Promise((_, reject) => {
376
+ let settled = false;
377
+ const schedule = (reject) => {
378
+ const now = this.now();
379
+ const lastActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
380
+ ? this.currentTurnActivityAt
381
+ : now;
382
+ const elapsedMs = Math.max(0, now - lastActivityAt);
383
+ const waitMs = Math.max(1, this.turnDeadlineMs - elapsedMs);
342
384
  timer = setTimeout(() => {
385
+ if (settled) {
386
+ return;
387
+ }
388
+ const activityNow = this.now();
389
+ const latestActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
390
+ ? this.currentTurnActivityAt
391
+ : activityNow;
392
+ if (activityNow - latestActivityAt < this.turnDeadlineMs) {
393
+ schedule(reject);
394
+ return;
395
+ }
396
+ settled = true;
343
397
  try {
344
398
  onTimeout?.();
345
399
  }
@@ -347,14 +401,18 @@ export class ClaudeAgentSdkSession extends EventEmitter {
347
401
  // best effort
348
402
  }
349
403
  reject(this.createTurnTimeoutError(this.turnDeadlineMs));
350
- }, this.turnDeadlineMs);
351
- if (typeof timer.unref === "function") {
404
+ }, waitMs);
405
+ if (typeof timer?.unref === "function") {
352
406
  timer.unref();
353
407
  }
408
+ };
409
+ const promise = new Promise((_, reject) => {
410
+ schedule(reject);
354
411
  });
355
412
  return {
356
413
  promise,
357
414
  cleanup: () => {
415
+ settled = true;
358
416
  if (timer) {
359
417
  clearTimeout(timer);
360
418
  }
@@ -414,10 +472,19 @@ export class ClaudeAgentSdkSession extends EventEmitter {
414
472
  const changed = this.sessionId !== normalizedSessionId;
415
473
  this.sessionId = normalizedSessionId;
416
474
  this.manualResumeReady = true;
475
+ const modelUsage = this.lastResult?.modelUsage && typeof this.lastResult.modelUsage === "object"
476
+ ? this.lastResult.modelUsage
477
+ : null;
478
+ const resolvedModel = typeof modelUsage?.model === "string" && modelUsage.model.trim()
479
+ ? modelUsage.model.trim()
480
+ : typeof this.options.model === "string" && this.options.model.trim()
481
+ ? this.options.model.trim()
482
+ : undefined;
417
483
  this.sessionInfo = {
418
484
  ...(this.sessionInfo || {}),
419
485
  backend: this.backend,
420
486
  sessionId: normalizedSessionId,
487
+ model: resolvedModel,
421
488
  };
422
489
  if (changed) {
423
490
  this.trace(`session ready id=${normalizedSessionId}`);
@@ -668,6 +735,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
668
735
  terminalWorkingStatusEmitted: false,
669
736
  };
670
737
  this.currentTurn = currentTurn;
738
+ this.markTurnStartedStatus();
671
739
  const closeGuard = this.createCloseGuard(() => {
672
740
  abortController.abort();
673
741
  currentTurn.query?.close?.();
@@ -21,6 +21,9 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
21
21
  nativeSessionId: string;
22
22
  rateLimits: any;
23
23
  tokenUsage: any;
24
+ currentTurnStatus: any;
25
+ currentTurnActivityAt: number;
26
+ now: any;
24
27
  turnDeadlineMs: any;
25
28
  currentTurn: {
26
29
  turnId: string;
@@ -37,7 +40,8 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
37
40
  trace(message: any): void;
38
41
  get threadId(): any;
39
42
  get threadOptions(): {
40
- model: string;
43
+ model: any;
44
+ modelProvider: any;
41
45
  };
42
46
  getSnapshot(): {
43
47
  backend: string;
@@ -51,9 +55,11 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
51
55
  ready: boolean;
52
56
  command: string;
53
57
  } | null;
58
+ currentTurnStatus: any;
54
59
  pid: number | undefined;
55
60
  };
56
61
  getSessionInfo(): any;
62
+ getCurrentTurnStatus(): any;
57
63
  ensureSessionInfo(): Promise<any>;
58
64
  getSessionUsageSummary(): Promise<{
59
65
  sessionId: any;
@@ -72,9 +78,13 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
72
78
  setWorkingStatusHandler(handler: any): void;
73
79
  setSessionReplyTarget(replyTo: any): void;
74
80
  getCurrentReplyTarget(): string | undefined;
81
+ touchTurnActivity(): void;
82
+ updateCurrentTurnStatus(payload: any): void;
83
+ markTurnStartedStatus(): void;
84
+ failPendingTurnStart(error: any): Promise<void>;
75
85
  boot(): Promise<void>;
76
86
  bootInternal(): Promise<void>;
77
- applyThreadInfo(thread: any, { resumeReady }?: {
87
+ applyThreadInfo(payload: any, { resumeReady }?: {
78
88
  resumeReady?: boolean | undefined;
79
89
  }): void;
80
90
  applySessionConfigured(params: any): void;
@@ -186,6 +186,9 @@ export class CodexAppServerSession extends EventEmitter {
186
186
  this.nativeSessionId = "";
187
187
  this.rateLimits = null;
188
188
  this.tokenUsage = null;
189
+ this.currentTurnStatus = null;
190
+ this.currentTurnActivityAt = 0;
191
+ this.now = typeof options.now === "function" ? options.now : () => Date.now();
189
192
  this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
190
193
  this.currentTurn = null;
191
194
  this.bootPromise = null;
@@ -224,7 +227,10 @@ export class CodexAppServerSession extends EventEmitter {
224
227
  return this.sessionId;
225
228
  }
226
229
  get threadOptions() {
227
- return { model: this.backend };
230
+ return {
231
+ model: this.sessionInfo?.model || this.backend,
232
+ modelProvider: this.sessionInfo?.modelProvider || undefined,
233
+ };
228
234
  }
229
235
  getSnapshot() {
230
236
  return {
@@ -241,12 +247,16 @@ export class CodexAppServerSession extends EventEmitter {
241
247
  command: `codex resume ${this.sessionId}`,
242
248
  }
243
249
  : null,
250
+ currentTurnStatus: this.getCurrentTurnStatus(),
244
251
  pid: this.transport.pid || undefined,
245
252
  };
246
253
  }
247
254
  getSessionInfo() {
248
255
  return this.sessionInfo ? { ...this.sessionInfo } : null;
249
256
  }
257
+ getCurrentTurnStatus() {
258
+ return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
259
+ }
250
260
  async ensureSessionInfo() {
251
261
  await this.boot();
252
262
  return this.getSessionInfo();
@@ -288,6 +298,37 @@ export class CodexAppServerSession extends EventEmitter {
288
298
  getCurrentReplyTarget() {
289
299
  return this.activeReplyTarget || this.lastReplyTarget || undefined;
290
300
  }
301
+ touchTurnActivity() {
302
+ this.currentTurnActivityAt = this.now();
303
+ }
304
+ updateCurrentTurnStatus(payload) {
305
+ const updatedAtMs = this.now();
306
+ this.currentTurnActivityAt = updatedAtMs;
307
+ this.currentTurnStatus = {
308
+ ...payload,
309
+ updated_at: new Date(updatedAtMs).toISOString(),
310
+ };
311
+ }
312
+ markTurnStartedStatus() {
313
+ this.updateCurrentTurnStatus({
314
+ source: "codex-app-server",
315
+ reply_in_progress: true,
316
+ replyTo: this.getCurrentReplyTarget(),
317
+ phase: "turn_started",
318
+ status_line: "codex is working",
319
+ thread_id: this.sessionId || undefined,
320
+ });
321
+ }
322
+ async failPendingTurnStart(error) {
323
+ if (this.closeRequested || error?.reason === "session_closed") {
324
+ return;
325
+ }
326
+ await this.emitWorkingStatus({
327
+ phase: "turn_failed",
328
+ reply_in_progress: false,
329
+ status_done_line: error instanceof Error ? error.message : String(error),
330
+ });
331
+ }
291
332
  async boot() {
292
333
  if (this.booted) {
293
334
  return;
@@ -326,9 +367,10 @@ export class CodexAppServerSession extends EventEmitter {
326
367
  else {
327
368
  result = await this.transport.request("thread/start", params);
328
369
  }
329
- this.applyThreadInfo(result?.thread, { resumeReady: Boolean(this.resumeSessionId) });
370
+ this.applyThreadInfo(result, { resumeReady: Boolean(this.resumeSessionId) });
330
371
  }
331
- applyThreadInfo(thread, { resumeReady = false } = {}) {
372
+ applyThreadInfo(payload, { resumeReady = false } = {}) {
373
+ const thread = payload?.thread && typeof payload.thread === "object" ? payload.thread : payload;
332
374
  const threadId = typeof thread?.id === "string" ? thread.id.trim() : "";
333
375
  const threadPath = typeof thread?.path === "string" ? thread.path.trim() : "";
334
376
  if (!threadId) {
@@ -336,15 +378,30 @@ export class CodexAppServerSession extends EventEmitter {
336
378
  }
337
379
  this.sessionId = threadId;
338
380
  this.threadPath = threadPath;
381
+ const resolvedModel = typeof payload?.model === "string" && payload.model.trim()
382
+ ? payload.model.trim()
383
+ : this.sessionInfo?.model || undefined;
384
+ const resolvedModelProvider = typeof payload?.modelProvider === "string" && payload.modelProvider.trim()
385
+ ? payload.modelProvider.trim()
386
+ : typeof thread?.modelProvider === "string" && thread.modelProvider.trim()
387
+ ? thread.modelProvider.trim()
388
+ : this.sessionInfo?.modelProvider || undefined;
389
+ const reasoningEffort = typeof payload?.reasoningEffort === "string" && payload.reasoningEffort.trim()
390
+ ? payload.reasoningEffort.trim()
391
+ : this.sessionInfo?.reasoningEffort || undefined;
339
392
  this.sessionInfo = {
393
+ ...(this.sessionInfo || {}),
340
394
  backend: this.backend,
341
395
  sessionId: threadId,
342
396
  sessionFilePath: threadPath || undefined,
397
+ model: resolvedModel,
398
+ modelProvider: resolvedModelProvider,
399
+ reasoningEffort,
343
400
  };
344
401
  if (resumeReady) {
345
402
  this.manualResumeReady = true;
346
403
  }
347
- this.trace(`thread ready id=${threadId} path="${sanitizeForLog(threadPath, 180)}"`);
404
+ this.trace(`thread ready id=${threadId} path="${sanitizeForLog(threadPath, 180)}" model="${sanitizeForLog(resolvedModel || this.backend, 80)}" provider="${sanitizeForLog(resolvedModelProvider || this.backend, 80)}"`);
348
405
  this.emit("session", this.getSessionInfo());
349
406
  }
350
407
  applySessionConfigured(params) {
@@ -387,12 +444,15 @@ export class CodexAppServerSession extends EventEmitter {
387
444
  }
388
445
  async emitWorkingStatus(payload) {
389
446
  const normalized = this.normalizeWorkingStatusPayload(payload);
447
+ this.updateCurrentTurnStatus(normalized);
448
+ const snapshot = this.getCurrentTurnStatus();
390
449
  if (typeof this.workingStatusHandler === "function") {
391
- await this.workingStatusHandler(normalized);
450
+ await this.workingStatusHandler(snapshot);
392
451
  }
393
- this.emit("working_status", normalized);
452
+ this.emit("working_status", snapshot);
394
453
  }
395
454
  async emitAssistantMessage(text) {
455
+ this.touchTurnActivity();
396
456
  const payload = {
397
457
  text,
398
458
  preserveWhitespace: true,
@@ -470,17 +530,40 @@ export class CodexAppServerSession extends EventEmitter {
470
530
  };
471
531
  }
472
532
  let timer = null;
473
- const promise = new Promise((_, reject) => {
533
+ let settled = false;
534
+ const schedule = (reject) => {
535
+ const now = this.now();
536
+ const lastActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
537
+ ? this.currentTurnActivityAt
538
+ : now;
539
+ const elapsedMs = Math.max(0, now - lastActivityAt);
540
+ const waitMs = Math.max(1, this.turnDeadlineMs - elapsedMs);
474
541
  timer = setTimeout(() => {
542
+ if (settled) {
543
+ return;
544
+ }
545
+ const activityNow = this.now();
546
+ const latestActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
547
+ ? this.currentTurnActivityAt
548
+ : activityNow;
549
+ if (activityNow - latestActivityAt < this.turnDeadlineMs) {
550
+ schedule(reject);
551
+ return;
552
+ }
553
+ settled = true;
475
554
  reject(this.createTurnTimeoutError(this.turnDeadlineMs));
476
- }, this.turnDeadlineMs);
477
- if (typeof timer.unref === "function") {
555
+ }, waitMs);
556
+ if (typeof timer?.unref === "function") {
478
557
  timer.unref();
479
558
  }
559
+ };
560
+ const promise = new Promise((_, reject) => {
561
+ schedule(reject);
480
562
  });
481
563
  return {
482
564
  promise,
483
565
  cleanup: () => {
566
+ settled = true;
484
567
  if (timer) {
485
568
  clearTimeout(timer);
486
569
  }
@@ -571,7 +654,7 @@ export class CodexAppServerSession extends EventEmitter {
571
654
  }
572
655
  switch (method) {
573
656
  case "thread/started":
574
- this.applyThreadInfo(params?.thread, { resumeReady: Boolean(this.resumeSessionId) });
657
+ this.applyThreadInfo(params, { resumeReady: Boolean(this.resumeSessionId) });
575
658
  return;
576
659
  case "sessionConfigured":
577
660
  case "session_configured":
@@ -784,7 +867,6 @@ export class CodexAppServerSession extends EventEmitter {
784
867
  if (this.closeRequested) {
785
868
  throw this.createSessionClosedError();
786
869
  }
787
- await this.boot();
788
870
  const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
789
871
  if (!effectivePrompt) {
790
872
  return {
@@ -799,6 +881,14 @@ export class CodexAppServerSession extends EventEmitter {
799
881
  reason: "turn_already_running",
800
882
  });
801
883
  }
884
+ this.markTurnStartedStatus();
885
+ try {
886
+ await this.boot();
887
+ }
888
+ catch (error) {
889
+ await this.failPendingTurnStart(error);
890
+ throw error;
891
+ }
802
892
  this.history.push({ role: "user", content: promptText });
803
893
  const closeGuard = this.createCloseGuard();
804
894
  const turnTimeoutGuard = this.createTurnTimeoutGuard();
@@ -860,6 +950,13 @@ export class CodexAppServerSession extends EventEmitter {
860
950
  if (error?.reason === "turn_timeout") {
861
951
  await this.interruptCurrentTurn();
862
952
  }
953
+ if (!this.closeRequested && error?.reason !== "session_closed") {
954
+ await this.emitWorkingStatus({
955
+ phase: "turn_failed",
956
+ reply_in_progress: false,
957
+ status_done_line: error instanceof Error ? error.message : String(error),
958
+ });
959
+ }
863
960
  this.maybeEmitAuthRequired(error);
864
961
  throw error;
865
962
  }
@@ -32,6 +32,8 @@ export class KimiCliSession extends EventEmitter<[never]> {
32
32
  } | null;
33
33
  lastTokenUsage: any;
34
34
  lastContextUsagePercent: number | undefined;
35
+ currentTurnStatus: any;
36
+ currentTurnActivityAt: number;
35
37
  turnDeadlineMs: any;
36
38
  workingStatusDedupeMs: any;
37
39
  workingStatusThrottleMs: any;
@@ -77,12 +79,14 @@ export class KimiCliSession extends EventEmitter<[never]> {
77
79
  ready: boolean;
78
80
  command: any;
79
81
  } | null;
82
+ currentTurnStatus: any;
80
83
  pid: any;
81
84
  };
82
85
  getSessionInfo(): {
83
86
  backend: string;
84
87
  sessionId: any;
85
88
  } | null;
89
+ getCurrentTurnStatus(): any;
86
90
  ensureSessionInfo(): Promise<{
87
91
  backend: string;
88
92
  sessionId: any;
@@ -104,6 +108,10 @@ export class KimiCliSession extends EventEmitter<[never]> {
104
108
  setWorkingStatusHandler(handler: any): void;
105
109
  setSessionReplyTarget(replyTo: any): void;
106
110
  getCurrentReplyTarget(): string | undefined;
111
+ touchTurnActivity(): void;
112
+ updateCurrentTurnStatus(payload: any): void;
113
+ markTurnStartedStatus(): void;
114
+ failPendingTurnStart(error: any, onProgress?: null): Promise<void>;
107
115
  buildWorkingStatusFingerprint(payload: any): string;
108
116
  shouldSuppressWorkingStatus(payload: any): boolean;
109
117
  recordWorkingStatusEmission(payload: any): void;
@@ -134,6 +134,8 @@ export class KimiCliSession extends EventEmitter {
134
134
  this.currentTurn = null;
135
135
  this.lastTokenUsage = null;
136
136
  this.lastContextUsagePercent = undefined;
137
+ this.currentTurnStatus = null;
138
+ this.currentTurnActivityAt = 0;
137
139
  this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
138
140
  this.workingStatusDedupeMs = getBoundedEnvInt("CONDUCTOR_KIMI_STATUS_DEDUPE_MS", DEFAULT_STATUS_DEDUPE_MS, 0, MAX_STATUS_TIMING_MS);
139
141
  this.workingStatusThrottleMs = getBoundedEnvInt("CONDUCTOR_KIMI_STATUS_THROTTLE_MS", DEFAULT_STATUS_THROTTLE_MS, 0, MAX_STATUS_TIMING_MS);
@@ -216,12 +218,16 @@ export class KimiCliSession extends EventEmitter {
216
218
  command: this.buildManualResumeCommand(),
217
219
  }
218
220
  : null,
221
+ currentTurnStatus: this.getCurrentTurnStatus(),
219
222
  pid: this.transport.pid || undefined,
220
223
  };
221
224
  }
222
225
  getSessionInfo() {
223
226
  return this.sessionInfo ? { ...this.sessionInfo } : null;
224
227
  }
228
+ getCurrentTurnStatus() {
229
+ return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
230
+ }
225
231
  async ensureSessionInfo() {
226
232
  await this.boot();
227
233
  return this.getSessionInfo();
@@ -261,6 +267,39 @@ export class KimiCliSession extends EventEmitter {
261
267
  getCurrentReplyTarget() {
262
268
  return this.activeReplyTarget || this.lastReplyTarget || undefined;
263
269
  }
270
+ touchTurnActivity() {
271
+ this.currentTurnActivityAt = this.now();
272
+ }
273
+ updateCurrentTurnStatus(payload) {
274
+ const updatedAtMs = this.now();
275
+ this.currentTurnActivityAt = updatedAtMs;
276
+ this.currentTurnStatus = {
277
+ ...payload,
278
+ updated_at: new Date(updatedAtMs).toISOString(),
279
+ };
280
+ }
281
+ markTurnStartedStatus() {
282
+ this.updateCurrentTurnStatus({
283
+ source: KIMI_PROVIDER_VARIANT,
284
+ reply_in_progress: true,
285
+ replyTo: this.getCurrentReplyTarget(),
286
+ phase: "turn_started",
287
+ status_line: statusLineForPhase("turn_started"),
288
+ thread_id: this.sessionId || undefined,
289
+ session_id: this.sessionId || undefined,
290
+ session_file_path: undefined,
291
+ });
292
+ }
293
+ async failPendingTurnStart(error, onProgress = null) {
294
+ if (this.closeRequested || error?.reason === "session_closed") {
295
+ return;
296
+ }
297
+ await this.emitWorkingStatus({
298
+ phase: "turn_failed",
299
+ reply_in_progress: false,
300
+ status_done_line: error instanceof Error ? error.message : String(error),
301
+ }, onProgress);
302
+ }
264
303
  buildWorkingStatusFingerprint(payload) {
265
304
  return JSON.stringify({
266
305
  reply_in_progress: Boolean(payload?.reply_in_progress),
@@ -338,18 +377,22 @@ export class KimiCliSession extends EventEmitter {
338
377
  session_file_path: undefined,
339
378
  };
340
379
  if (this.shouldSuppressWorkingStatus(normalized)) {
380
+ this.updateCurrentTurnStatus(normalized);
341
381
  return;
342
382
  }
383
+ this.updateCurrentTurnStatus(normalized);
384
+ const snapshot = this.getCurrentTurnStatus();
343
385
  this.recordWorkingStatusEmission(normalized);
344
386
  if (typeof onProgress === "function") {
345
- onProgress(normalized);
387
+ onProgress(snapshot);
346
388
  }
347
389
  if (typeof this.workingStatusHandler === "function") {
348
- await this.workingStatusHandler(normalized);
390
+ await this.workingStatusHandler(snapshot);
349
391
  }
350
- this.emit("working_status", normalized);
392
+ this.emit("working_status", snapshot);
351
393
  }
352
394
  async emitAssistantMessage(text) {
395
+ this.touchTurnActivity();
353
396
  const payload = {
354
397
  text,
355
398
  preserveWhitespace: true,
@@ -423,8 +466,27 @@ export class KimiCliSession extends EventEmitter {
423
466
  };
424
467
  }
425
468
  let timer = null;
426
- const promise = new Promise((_, reject) => {
469
+ let settled = false;
470
+ const schedule = (reject) => {
471
+ const now = this.now();
472
+ const lastActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
473
+ ? this.currentTurnActivityAt
474
+ : now;
475
+ const elapsedMs = Math.max(0, now - lastActivityAt);
476
+ const waitMs = Math.max(1, this.turnDeadlineMs - elapsedMs);
427
477
  timer = setTimeout(() => {
478
+ if (settled) {
479
+ return;
480
+ }
481
+ const activityNow = this.now();
482
+ const latestActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
483
+ ? this.currentTurnActivityAt
484
+ : activityNow;
485
+ if (activityNow - latestActivityAt < this.turnDeadlineMs) {
486
+ schedule(reject);
487
+ return;
488
+ }
489
+ settled = true;
428
490
  try {
429
491
  onTimeout?.();
430
492
  }
@@ -432,14 +494,18 @@ export class KimiCliSession extends EventEmitter {
432
494
  // best effort
433
495
  }
434
496
  reject(this.createTurnTimeoutError(this.turnDeadlineMs));
435
- }, this.turnDeadlineMs);
436
- if (typeof timer.unref === "function") {
497
+ }, waitMs);
498
+ if (typeof timer?.unref === "function") {
437
499
  timer.unref();
438
500
  }
501
+ };
502
+ const promise = new Promise((_, reject) => {
503
+ schedule(reject);
439
504
  });
440
505
  return {
441
506
  promise,
442
507
  cleanup: () => {
508
+ settled = true;
443
509
  if (timer) {
444
510
  clearTimeout(timer);
445
511
  }
@@ -763,12 +829,19 @@ export class KimiCliSession extends EventEmitter {
763
829
  if (!effectivePrompt) {
764
830
  return buildEmptyTurnResult();
765
831
  }
766
- await this.boot();
767
832
  if (this.currentTurn) {
768
833
  throw createTurnError("Kimi turn already running", {
769
834
  reason: "turn_already_running",
770
835
  });
771
836
  }
837
+ this.markTurnStartedStatus();
838
+ try {
839
+ await this.boot();
840
+ }
841
+ catch (error) {
842
+ await this.failPendingTurnStart(error, onProgress);
843
+ throw error;
844
+ }
772
845
  this.history.push({ role: "user", content: promptText });
773
846
  const currentTurn = {
774
847
  fullText: "",
@@ -46,6 +46,9 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
46
46
  };
47
47
  } | null;
48
48
  lastAssistantInfo: any;
49
+ currentTurnStatus: any;
50
+ currentTurnActivityAt: number;
51
+ now: any;
49
52
  turnDeadlineMs: any;
50
53
  client: any;
51
54
  sdkModulePromise: Promise<any> | Promise<typeof import("@opencode-ai/sdk/v2/client")> | null;
@@ -60,6 +63,7 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
60
63
  get threadId(): any;
61
64
  get threadOptions(): {
62
65
  model: any;
66
+ modelProvider: any;
63
67
  };
64
68
  getSnapshot(): {
65
69
  backend: string;
@@ -73,12 +77,14 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
73
77
  useSessionFileReplyStream: boolean;
74
78
  resumeReady: boolean;
75
79
  manualResume: null;
80
+ currentTurnStatus: any;
76
81
  pid: any;
77
82
  };
78
83
  getSessionInfo(): {
79
84
  backend: string;
80
85
  sessionId: any;
81
86
  } | null;
87
+ getCurrentTurnStatus(): any;
82
88
  ensureSessionInfo(): Promise<{
83
89
  backend: string;
84
90
  sessionId: any;
@@ -105,6 +111,10 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
105
111
  setWorkingStatusHandler(handler: any): void;
106
112
  setSessionReplyTarget(replyTo: any): void;
107
113
  getCurrentReplyTarget(): string | undefined;
114
+ touchTurnActivity(): void;
115
+ updateCurrentTurnStatus(payload: any): void;
116
+ markTurnStartedStatus(): void;
117
+ failPendingTurnStart(error: any, onProgress?: null): Promise<void>;
108
118
  emitWorkingStatus(payload: any, onProgress?: null): Promise<void>;
109
119
  emitAssistantMessage(text: any): Promise<void>;
110
120
  emitTerminalWorkingStatus(currentTurn: any, payload: any, onProgress?: null): Promise<void>;