@love-moon/ai-sdk 0.7.6 → 0.8.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,9 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.8.0
4
+
5
+ ## 0.7.7
6
+
3
7
  ## 0.7.6
4
8
 
5
9
  ### Patch Changes
@@ -139,6 +139,17 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
139
139
  thread_id: any;
140
140
  };
141
141
  emitWorkingStatus(payload: any): Promise<void>;
142
+ /**
143
+ * Idempotent terminal working-status clear (see BUG-R6-01 lesson). The
144
+ * "reply_in_progress:false" signal that unsticks the composer must be emitted
145
+ * from a reliable, always-arriving path, and multiple paths emitting it must
146
+ * be a safe no-op. A one-time guard on the turn object ensures we clear
147
+ * exactly once regardless of how many code paths reach here.
148
+ */
149
+ emitTurnCompletedStatus(currentTurn: any, { phase, status_done_line }?: {
150
+ phase?: string | undefined;
151
+ status_done_line?: string | undefined;
152
+ }): Promise<void>;
142
153
  emitAssistantMessage(text: any): Promise<void>;
143
154
  finalizeActiveAssistantMessage(currentTurn?: {
144
155
  turnId: string;
@@ -489,6 +489,26 @@ export class CodexAppServerSession extends EventEmitter {
489
489
  }
490
490
  this.emit("working_status", snapshot);
491
491
  }
492
+ /**
493
+ * Idempotent terminal working-status clear (see BUG-R6-01 lesson). The
494
+ * "reply_in_progress:false" signal that unsticks the composer must be emitted
495
+ * from a reliable, always-arriving path, and multiple paths emitting it must
496
+ * be a safe no-op. A one-time guard on the turn object ensures we clear
497
+ * exactly once regardless of how many code paths reach here.
498
+ */
499
+ async emitTurnCompletedStatus(currentTurn, { phase = "turn_completed", status_done_line = "codex finished" } = {}) {
500
+ if (currentTurn && currentTurn.terminalStatusEmitted) {
501
+ return;
502
+ }
503
+ if (currentTurn) {
504
+ currentTurn.terminalStatusEmitted = true;
505
+ }
506
+ await this.emitWorkingStatus({
507
+ phase,
508
+ reply_in_progress: false,
509
+ status_done_line,
510
+ });
511
+ }
492
512
  async emitAssistantMessage(text) {
493
513
  this.touchTurnActivity();
494
514
  const payload = {
@@ -869,7 +889,15 @@ export class CodexAppServerSession extends EventEmitter {
869
889
  return;
870
890
  }
871
891
  case "turn/completed": {
872
- const currentTurn = this.ensureCurrentTurn(params);
892
+ let currentTurn = this.ensureCurrentTurn(params);
893
+ if (!currentTurn && this.currentTurn && !this.currentTurn.goalMode) {
894
+ // Defense-in-depth (BUG-R6-01 class): a non-goal session has exactly
895
+ // one active turn. If turn matching fails (missing `turn/started`, a
896
+ // turnId mismatch, or an id carried on a different field), we must NOT
897
+ // strand the terminal clear here — otherwise the composer stays on
898
+ // "codex composing reply" until the 12-min idle deadline or a reload.
899
+ currentTurn = this.currentTurn;
900
+ }
873
901
  if (!currentTurn) {
874
902
  return;
875
903
  }
@@ -887,9 +915,8 @@ export class CodexAppServerSession extends EventEmitter {
887
915
  turnStatus: status,
888
916
  turnError,
889
917
  });
890
- await this.emitWorkingStatus({
918
+ await this.emitTurnCompletedStatus(currentTurn, {
891
919
  phase: "turn_failed",
892
- reply_in_progress: false,
893
920
  status_done_line: `codex failed (${status})`,
894
921
  });
895
922
  if (currentTurn.goalMode) {
@@ -927,11 +954,7 @@ export class CodexAppServerSession extends EventEmitter {
927
954
  currentTurn.activeAssistantMessageText = "";
928
955
  return;
929
956
  }
930
- await this.emitWorkingStatus({
931
- phase: "turn_completed",
932
- reply_in_progress: false,
933
- status_done_line: "codex finished",
934
- });
957
+ await this.emitTurnCompletedStatus(currentTurn);
935
958
  currentTurn.resolve({
936
959
  turn,
937
960
  usage: this.tokenUsage ? { ...this.tokenUsage } : null,
@@ -1075,6 +1098,10 @@ export class CodexAppServerSession extends EventEmitter {
1075
1098
  await this.finalizeActiveAssistantMessage(currentTurn, {
1076
1099
  messageId: currentTurn.activeAssistantMessageId,
1077
1100
  });
1101
+ // Belt-and-suspenders terminal clear. Idempotent (guarded on the turn),
1102
+ // so when the wire `turn/completed` already cleared this is a no-op; it
1103
+ // only fires if the turn resolved without the wire clear reaching here.
1104
+ await this.emitTurnCompletedStatus(currentTurn);
1078
1105
  if (currentTurn.fullText) {
1079
1106
  this.history.push({ role: "assistant", content: currentTurn.fullText });
1080
1107
  }
@@ -22,6 +22,9 @@ export class KimiCliSession extends EventEmitter<[never]> {
22
22
  currentTurn: {
23
23
  fullText: string;
24
24
  bufferedAssistantText: string;
25
+ thinkText: string;
26
+ thinkStatusLine: string;
27
+ thinkStatusLineAt: number;
25
28
  items: never[];
26
29
  toolCalls: Map<any, any>;
27
30
  onProgress: null;
@@ -136,6 +139,8 @@ export class KimiCliSession extends EventEmitter<[never]> {
136
139
  bootInternal(): Promise<void>;
137
140
  updateUsageFromStatus(payload: any): void;
138
141
  appendAssistantText(currentTurn: any, text: any): void;
142
+ appendThinkText(currentTurn: any, text: any): void;
143
+ thinkStatusLine(currentTurn: any): any;
139
144
  finalizeAssistantMessage(currentTurn: any): Promise<boolean>;
140
145
  maybeEmitAuthRequired(error: any): void;
141
146
  failUnexpectedInteractiveRequest(id: any, requestType: any, payload: any): Promise<void>;
@@ -11,6 +11,8 @@ const DEFAULT_STATUS_THROTTLE_MS = 450;
11
11
  const DEFAULT_REASONING_STATUS_THROTTLE_MS = 2500;
12
12
  const DEFAULT_STATUS_PREVIEW_DELTA_CHARS = 24;
13
13
  const MAX_STATUS_TIMING_MS = 10 * 1000;
14
+ const THINK_STATUS_LINE_MAX_CHARS = 120;
15
+ const THINK_TAIL_BUFFER_CHARS = 400;
14
16
  function waitForever() {
15
17
  return new Promise(() => { });
16
18
  }
@@ -603,6 +605,29 @@ export class KimiCliSession extends EventEmitter {
603
605
  }
604
606
  currentTurn.bufferedAssistantText += normalized;
605
607
  }
608
+ appendThinkText(currentTurn, text) {
609
+ const normalized = normalizeText(text);
610
+ if (!normalized) {
611
+ return;
612
+ }
613
+ currentTurn.thinkText = `${currentTurn.thinkText}${normalized}`.slice(-THINK_TAIL_BUFFER_CHARS);
614
+ }
615
+ thinkStatusLine(currentTurn) {
616
+ const now = this.now();
617
+ if (currentTurn.thinkStatusLine &&
618
+ now - currentTurn.thinkStatusLineAt < this.reasoningStatusThrottleMs) {
619
+ return currentTurn.thinkStatusLine;
620
+ }
621
+ const sanitized = sanitizeSummary(currentTurn.thinkText, THINK_TAIL_BUFFER_CHARS);
622
+ const snippet = sanitized.length > THINK_STATUS_LINE_MAX_CHARS
623
+ ? `…${sanitized.slice(-THINK_STATUS_LINE_MAX_CHARS)}`
624
+ : sanitized;
625
+ if (snippet && snippet !== currentTurn.thinkStatusLine) {
626
+ currentTurn.thinkStatusLine = snippet;
627
+ currentTurn.thinkStatusLineAt = now;
628
+ }
629
+ return currentTurn.thinkStatusLine || statusLineForPhase("reasoning");
630
+ }
606
631
  async finalizeAssistantMessage(currentTurn) {
607
632
  if (!currentTurn) {
608
633
  return false;
@@ -707,8 +732,23 @@ export class KimiCliSession extends EventEmitter {
707
732
  return;
708
733
  case "TurnEnd":
709
734
  currentTurn.seenTurnEnd = true;
735
+ if (currentTurn.bufferedAssistantText) {
736
+ await this.finalizeAssistantMessage(currentTurn);
737
+ }
738
+ // Clear reply_in_progress as soon as the wire reports the turn ended,
739
+ // decoupled from the `prompt` JSON-RPC promise. Without this, a delayed
740
+ // or dropped prompt RPC response leaves the web UI pinned on
741
+ // "Kimi is writing the reply" until reload. emitTerminalWorkingStatus is
742
+ // idempotent, so the later runTurn call becomes a harmless no-op.
743
+ await this.emitTerminalWorkingStatus(currentTurn, {
744
+ phase: "turn_completed",
745
+ status_done_line: "Kimi finished",
746
+ }, currentTurn.onProgress);
710
747
  return;
711
748
  case "StepBegin":
749
+ if (currentTurn.bufferedAssistantText) {
750
+ await this.finalizeAssistantMessage(currentTurn);
751
+ }
712
752
  await this.emitWorkingStatus({
713
753
  phase: "reasoning",
714
754
  reply_in_progress: true,
@@ -734,10 +774,11 @@ export class KimiCliSession extends EventEmitter {
734
774
  return;
735
775
  case "ContentPart":
736
776
  if (normalizedPayload.type === "think") {
777
+ this.appendThinkText(currentTurn, normalizedPayload.think);
737
778
  await this.emitWorkingStatus({
738
779
  phase: "reasoning",
739
780
  reply_in_progress: true,
740
- status_line: statusLineForPhase("reasoning"),
781
+ status_line: this.thinkStatusLine(currentTurn),
741
782
  }, currentTurn.onProgress);
742
783
  return;
743
784
  }
@@ -860,6 +901,9 @@ export class KimiCliSession extends EventEmitter {
860
901
  const currentTurn = {
861
902
  fullText: "",
862
903
  bufferedAssistantText: "",
904
+ thinkText: "",
905
+ thinkStatusLine: "",
906
+ thinkStatusLineAt: 0,
863
907
  items: [],
864
908
  toolCalls: new Map(),
865
909
  onProgress,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -32,12 +32,12 @@
32
32
  "zod": "^4.1.5"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@love-moon/chat-web": "^0.7.6"
35
+ "@love-moon/chat-web": "^0.8.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.10.2",
39
39
  "tsx": "^4.20.6",
40
40
  "typescript": "^5.6.3"
41
41
  },
42
- "gitCommitId": "c939df2"
42
+ "gitCommitId": "04d4f62"
43
43
  }