devez-vibe 1.2.6 → 1.2.8

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.
@@ -435,25 +435,25 @@ async function createSession(params, resumeId) {
435
435
  turnSequence: 1,
436
436
  itemSequence: 1,
437
437
  streamBlocks: new Map(),
438
- tools: new Map(),
439
- tasks: new Map(),
440
- subagents: new Map(),
441
- knownSubagents: new Map(),
442
- lastContextUsage: null,
438
+ tools: new Map(),
439
+ tasks: new Map(),
440
+ subagents: new Map(),
441
+ knownSubagents: new Map(),
442
+ lastContextUsage: null,
443
443
  lastContextWindow: 0,
444
444
  };
445
445
  const agentQuery = await startAgentQuery(queue, makeOptions(params, id, resumeId));
446
446
  session.query = agentQuery;
447
447
  sessions.set(id, session);
448
- const consumer = consume(session).catch((error) => {
448
+ const consumer = consume(session).catch((error) => {
449
449
  notify("error", {
450
450
  threadId: id,
451
451
  provider: "Claude",
452
452
  error: { message: error instanceof Error ? error.message : String(error) },
453
- willRetry: false,
454
- });
455
- clearSubagents(session);
456
- if (session.turn) finishTurn(session, error);
453
+ willRetry: false,
454
+ });
455
+ clearSubagents(session);
456
+ if (session.turn) finishTurn(session, error);
457
457
  });
458
458
  session.consumer = consumer;
459
459
  let initialization;
@@ -501,84 +501,123 @@ function emitDelta(session, method, itemId, delta) {
501
501
  notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
502
502
  }
503
503
 
504
- // Windows rounds larger timer delays up to the next scheduler slice. Ten
505
- // milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
506
- const SMOOTH_TEXT_INTERVAL_MS = 10;
507
- const SMOOTH_TEXT_TARGET_FRAMES = 8;
508
- const SMOOTH_TEXT_MAX_GRAPHEMES = 24;
509
- const graphemeSegmenter = typeof Intl.Segmenter === "function"
510
- ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
511
- : null;
512
-
513
- function splitGraphemes(text) {
514
- return graphemeSegmenter
515
- ? Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment)
516
- : Array.from(text);
504
+ function isKoreanPrompt(input) {
505
+ const prompt = (Array.isArray(input) ? input : [])
506
+ .filter((item) => item?.type === "text")
507
+ .map((item) => String(item.text || ""))
508
+ .join("\n");
509
+ return /[\uac00-\ud7a3]/.test(prompt);
517
510
  }
518
511
 
519
- // Claude can deliver a whole phrase in one SDK event. Drain roughly one visual
520
- // frame's share at a time, catching up quickly when a large backlog arrives.
521
- function takeSmoothTextChunk(text) {
522
- const graphemes = splitGraphemes(text);
523
- const size = Math.min(
524
- SMOOTH_TEXT_MAX_GRAPHEMES,
525
- Math.max(1, Math.ceil(graphemes.length / SMOOTH_TEXT_TARGET_FRAMES)),
526
- );
527
- return {
528
- chunk: graphemes.slice(0, size).join(""),
529
- rest: graphemes.slice(size).join(""),
530
- };
512
+ function openingNotice(input) {
513
+ return isKoreanPrompt(input)
514
+ ? "요청 내용을 확인하고 필요한 작업을 진행하겠습니다."
515
+ : "I’ll review the request and proceed with the necessary work.";
531
516
  }
532
517
 
533
- class SmoothTextStream {
534
- constructor(emit, intervalMs = SMOOTH_TEXT_INTERVAL_MS) {
535
- this.emit = emit;
536
- this.intervalMs = intervalMs;
537
- this.pending = "";
538
- this.timer = null;
539
- this.waiters = [];
540
- }
541
-
542
- push(text) {
543
- this.pending += text;
544
- if (this.timer == null) this.drain();
545
- }
546
-
547
- drain() {
548
- if (!this.pending) {
549
- this.timer = null;
550
- for (const resolve of this.waiters.splice(0)) resolve();
551
- return;
552
- }
553
- const { chunk, rest } = takeSmoothTextChunk(this.pending);
554
- this.pending = rest;
555
- this.emit(chunk);
556
- if (this.pending) {
557
- this.timer = setTimeout(() => this.drain(), this.intervalMs);
558
- } else {
559
- this.timer = null;
560
- for (const resolve of this.waiters.splice(0)) resolve();
561
- }
562
- }
563
-
564
- finish() {
565
- if (!this.pending && this.timer == null) return Promise.resolve();
566
- return new Promise((resolve) => this.waiters.push(resolve));
567
- }
568
-
569
- flush() {
570
- if (this.timer != null) clearTimeout(this.timer);
571
- this.timer = null;
572
- if (this.pending) this.emit(this.pending);
573
- this.pending = "";
574
- for (const resolve of this.waiters.splice(0)) resolve();
518
+ function normalizeProgressText(turn, text) {
519
+ const value = String(text || "");
520
+ const trimmed = value.trim();
521
+ if (turn?.koreanRequest
522
+ && trimmed.length <= 160
523
+ && !/[\uac00-\ud7a3]/.test(trimmed)
524
+ && /^Now\b[^\r\n]*[.!?]?$/i.test(trimmed)) {
525
+ return "다음 부분을 이어서 확인하겠습니다.";
575
526
  }
527
+ return value;
576
528
  }
577
529
 
578
- function flushSmoothStreams(session) {
579
- for (const block of session.streamBlocks.values()) block.smooth?.flush();
530
+ // Claude can answer with a tool_use as its first and only content block even
531
+ // when the prompt asks for an opening update. Keep the visible contract stable
532
+ // without duplicating a real model-written update.
533
+ function emitOpeningNotice(session) {
534
+ if (!session.turn || session.turn.openingNoticeEmitted) return;
535
+ const text = session.turn.openingNotice;
536
+ const id = nextItemId(session, "opening");
537
+ const item = { id, type: "agentMessage", text, provider: "Claude" };
538
+ emitItem(session, "started", item);
539
+ emitItem(session, "completed", item);
540
+ session.turn.openingNoticeEmitted = true;
580
541
  }
581
542
 
543
+ // Windows rounds larger timer delays up to the next scheduler slice. Ten
544
+ // milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
545
+ const SMOOTH_TEXT_INTERVAL_MS = 10;
546
+ const SMOOTH_TEXT_TARGET_FRAMES = 8;
547
+ const SMOOTH_TEXT_MAX_GRAPHEMES = 24;
548
+ const graphemeSegmenter = typeof Intl.Segmenter === "function"
549
+ ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
550
+ : null;
551
+
552
+ function splitGraphemes(text) {
553
+ return graphemeSegmenter
554
+ ? Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment)
555
+ : Array.from(text);
556
+ }
557
+
558
+ // Claude can deliver a whole phrase in one SDK event. Drain roughly one visual
559
+ // frame's share at a time, catching up quickly when a large backlog arrives.
560
+ function takeSmoothTextChunk(text) {
561
+ const graphemes = splitGraphemes(text);
562
+ const size = Math.min(
563
+ SMOOTH_TEXT_MAX_GRAPHEMES,
564
+ Math.max(1, Math.ceil(graphemes.length / SMOOTH_TEXT_TARGET_FRAMES)),
565
+ );
566
+ return {
567
+ chunk: graphemes.slice(0, size).join(""),
568
+ rest: graphemes.slice(size).join(""),
569
+ };
570
+ }
571
+
572
+ class SmoothTextStream {
573
+ constructor(emit, intervalMs = SMOOTH_TEXT_INTERVAL_MS) {
574
+ this.emit = emit;
575
+ this.intervalMs = intervalMs;
576
+ this.pending = "";
577
+ this.timer = null;
578
+ this.waiters = [];
579
+ }
580
+
581
+ push(text) {
582
+ this.pending += text;
583
+ if (this.timer == null) this.drain();
584
+ }
585
+
586
+ drain() {
587
+ if (!this.pending) {
588
+ this.timer = null;
589
+ for (const resolve of this.waiters.splice(0)) resolve();
590
+ return;
591
+ }
592
+ const { chunk, rest } = takeSmoothTextChunk(this.pending);
593
+ this.pending = rest;
594
+ this.emit(chunk);
595
+ if (this.pending) {
596
+ this.timer = setTimeout(() => this.drain(), this.intervalMs);
597
+ } else {
598
+ this.timer = null;
599
+ for (const resolve of this.waiters.splice(0)) resolve();
600
+ }
601
+ }
602
+
603
+ finish() {
604
+ if (!this.pending && this.timer == null) return Promise.resolve();
605
+ return new Promise((resolve) => this.waiters.push(resolve));
606
+ }
607
+
608
+ flush() {
609
+ if (this.timer != null) clearTimeout(this.timer);
610
+ this.timer = null;
611
+ if (this.pending) this.emit(this.pending);
612
+ this.pending = "";
613
+ for (const resolve of this.waiters.splice(0)) resolve();
614
+ }
615
+ }
616
+
617
+ function flushSmoothStreams(session) {
618
+ for (const block of session.streamBlocks.values()) block.smooth?.flush();
619
+ }
620
+
582
621
  function tokenBreakdown(usage) {
583
622
  if (!usage) return null;
584
623
  const input = Number(usage.input_tokens ?? usage.inputTokens ?? 0);
@@ -625,44 +664,74 @@ function historyTokenUsage(messages, models, model) {
625
664
  };
626
665
  }
627
666
 
628
- async function processStreamEvent(session, message) {
629
- if (!session.turn || message.parent_tool_use_id) return;
630
- const event = message.event || {};
631
- if (event.type === "message_start") {
632
- flushSmoothStreams(session);
633
- session.streamBlocks.clear();
634
- }
667
+ async function processStreamEvent(session, message) {
668
+ if (!session.turn || message.parent_tool_use_id) return;
669
+ const event = message.event || {};
670
+ if (event.type === "message_start") {
671
+ flushSmoothStreams(session);
672
+ session.streamBlocks.clear();
673
+ }
635
674
  if (event.type === "content_block_start") {
636
- const block = event.content_block || {};
637
- if (block.type !== "text" && block.type !== "thinking") return;
638
- const id = nextItemId(session, block.type);
675
+ const block = event.content_block || {};
676
+ if (block.type !== "text" && block.type !== "thinking") return;
677
+ const id = nextItemId(session, block.type);
639
678
  const item = block.type === "text"
640
679
  ? { id, type: "agentMessage", text: "", provider: "Claude" }
641
680
  : { id, type: "reasoning", summary: [] };
642
- const smooth = block.type === "text"
643
- ? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
644
- : null;
645
- session.streamBlocks.set(event.index, { id, type: block.type, text: "", smooth });
646
- emitItem(session, "started", item);
647
- return;
648
- }
649
- if (event.type === "content_block_delta") {
681
+ const smooth = block.type === "text"
682
+ ? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
683
+ : null;
684
+ session.streamBlocks.set(event.index, {
685
+ id,
686
+ type: block.type,
687
+ text: "",
688
+ smooth,
689
+ languagePending: block.type === "text" && session.turn.koreanRequest ? "" : null,
690
+ holdEnglishProgress: false,
691
+ });
692
+ emitItem(session, "started", item);
693
+ return;
694
+ }
695
+ if (event.type === "content_block_delta") {
650
696
  const current = session.streamBlocks.get(event.index);
651
697
  if (!current) return;
652
698
  const delta = event.delta?.text || event.delta?.thinking || "";
653
699
  if (!delta) return;
654
700
  current.text += delta;
655
- if (current.smooth) current.smooth.push(delta);
656
- else emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
701
+ if (current.type === "text") session.turn.sawVisibleText = true;
702
+ if (!current.smooth) {
703
+ emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
704
+ return;
705
+ }
706
+ if (current.languagePending != null) {
707
+ current.languagePending += delta;
708
+ const probe = current.languagePending.trimStart();
709
+ const lower = probe.toLowerCase();
710
+ if (!current.holdEnglishProgress && "now".startsWith(lower)) return;
711
+ if (/^now(?:\s|$)/i.test(probe)) {
712
+ current.holdEnglishProgress = true;
713
+ return;
714
+ }
715
+ current.smooth.push(current.languagePending);
716
+ current.languagePending = null;
717
+ return;
718
+ }
719
+ current.smooth.push(delta);
657
720
  return;
658
721
  }
659
- if (event.type === "content_block_stop") {
722
+ if (event.type === "content_block_stop") {
660
723
  const current = session.streamBlocks.get(event.index);
661
724
  if (!current) return;
725
+ if (current.languagePending != null) {
726
+ const visible = normalizeProgressText(session.turn, current.languagePending);
727
+ current.text = visible;
728
+ current.smooth?.push(visible);
729
+ current.languagePending = null;
730
+ }
662
731
  await current.smooth?.finish();
663
- const item = current.type === "text"
664
- ? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
665
- : { id: current.id, type: "reasoning", summary: [current.text] };
732
+ const item = current.type === "text"
733
+ ? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
734
+ : { id: current.id, type: "reasoning", summary: [current.text] };
666
735
  emitItem(session, "completed", item);
667
736
  session.streamBlocks.delete(event.index);
668
737
  }
@@ -684,22 +753,29 @@ function processAssistant(session, message) {
684
753
  message.message?.model,
685
754
  session.model,
686
755
  );
687
- const content = Array.isArray(message.message?.content) ? message.message.content : [];
688
- for (const block of content) {
689
- if (block.type === "tool_use") processToolUse(session, block);
690
- }
691
- if (!session.streamBlocks.size && !session.turn.sawStreamText) {
692
- for (const block of content) {
693
- if (block.type !== "text" && block.type !== "thinking") continue;
694
- const id = nextItemId(session, block.type);
695
- const item = block.type === "text"
696
- ? { id, type: "agentMessage", text: block.text || "", provider: "Claude" }
697
- : { id, type: "reasoning", summary: [block.thinking || ""] };
698
- emitItem(session, "started", item);
699
- emitItem(session, "completed", item);
700
- }
701
- }
702
- }
756
+ const content = Array.isArray(message.message?.content) ? message.message.content : [];
757
+ const hasToolUse = content.some((block) => block.type === "tool_use");
758
+ const hasVisibleText = session.turn.sawVisibleText || content.some(
759
+ (block) => block.type === "text" && String(block.text || "").trim(),
760
+ );
761
+ // Without partial SDK events, replay completed text before tool items so the
762
+ // visible order still matches the assistant content order.
763
+ if (!session.streamBlocks.size && !session.turn.sawStreamText) {
764
+ for (const block of content) {
765
+ if (block.type !== "text" && block.type !== "thinking") continue;
766
+ const id = nextItemId(session, block.type);
767
+ const item = block.type === "text"
768
+ ? { id, type: "agentMessage", text: normalizeProgressText(session.turn, block.text), provider: "Claude" }
769
+ : { id, type: "reasoning", summary: [block.thinking || ""] };
770
+ emitItem(session, "started", item);
771
+ emitItem(session, "completed", item);
772
+ }
773
+ }
774
+ if (hasToolUse && !hasVisibleText) emitOpeningNotice(session);
775
+ for (const block of content) {
776
+ if (block.type === "tool_use") processToolUse(session, block);
777
+ }
778
+ }
703
779
 
704
780
  function processToolUse(session, block) {
705
781
  const name = block.name || "Tool";
@@ -751,52 +827,89 @@ function fileChanges(name, input) {
751
827
  return [{ path, kind: { type: name === "Write" ? "add" : "update" }, diff: `@@ -0,0 +1 @@\n${additions}` }];
752
828
  }
753
829
 
754
- function numberedTaskIndex(subject) {
755
- const match = String(subject || "").trim().match(/^(\d+)[.)]\s+/);
756
- return match ? Number(match[1]) : null;
757
- }
758
-
759
- // Claude의 Task id는 세션 전체에서 누적된다. 제목 번호가 다시 1부터
760
- // 시작할 때만 새 계획이며, 3·4번만 갱신되는 턴은 기존 1~6번을 지킨다.
761
- function prepareTaskPlanForCreate(tasks, subject) {
762
- if (numberedTaskIndex(subject) === 1) tasks.clear();
763
- }
764
-
765
- function applyTaskUpdate(tasks, input, turnId) {
766
- const task = tasks.get(String(input.taskId));
767
- if (!task) return false;
768
- if (input.subject) task.subject = input.subject;
769
- if (input.status) task.status = input.status;
770
- task.turnId = turnId;
771
- return true;
772
- }
773
-
774
- // TaskList에는 예전 계획까지 함께 들어올 수 있다. 마지막으로 번호가 1부터
775
- // 시작한 묶음만 현재 계획으로 삼되, 번호가 없는 목록은 손실 없이 그대로 둔다.
776
- function latestTaskPlan(tasks) {
777
- const entries = [...tasks.entries()];
778
- let start = 0;
779
- for (let index = 0; index < entries.length; index++) {
780
- if (numberedTaskIndex(entries[index][1].subject) === 1) start = index;
781
- }
782
- return new Map(entries.slice(start));
783
- }
830
+ function numberedTaskIndex(subject) {
831
+ const match = String(subject || "").trim().match(/^(\d+)[.)]\s+/);
832
+ return match ? Number(match[1]) : null;
833
+ }
834
+
835
+ // Claude의 Task id는 세션 전체에서 누적된다. 제목 번호가 다시 1부터
836
+ // 시작할 때만 새 계획이며, 3·4번만 갱신되는 턴은 기존 1~6번을 지킨다.
837
+ function prepareTaskPlanForCreate(tasks, subject) {
838
+ if (numberedTaskIndex(subject) === 1) tasks.clear();
839
+ }
840
+
841
+ function applyTaskUpdate(tasks, input, turnId, onIntermediate) {
842
+ const task = tasks.get(String(input.taskId));
843
+ if (!task) return false;
844
+ if (input.subject) task.subject = input.subject;
845
+ const status = input.status;
846
+ if (status === "in_progress" || status === "completed") {
847
+ const entries = [...tasks.values()];
848
+ const targetIndex = entries.indexOf(task);
849
+
850
+ // Claude occasionally closes a later pending task at the end of a turn
851
+ // without ever starting it. Keep the visible plan truthful and sequential:
852
+ // every skipped predecessor and the target itself pass through in_progress.
853
+ for (let index = 0; index < targetIndex; index++) {
854
+ const previous = entries[index];
855
+ if (previous.status === "completed") continue;
856
+ if (previous.status !== "in_progress") {
857
+ previous.status = "in_progress";
858
+ previous.turnId = turnId;
859
+ onIntermediate?.();
860
+ }
861
+ previous.status = "completed";
862
+ previous.turnId = turnId;
863
+ onIntermediate?.();
864
+ }
865
+
866
+ for (let index = 0; index < entries.length; index++) {
867
+ const other = entries[index];
868
+ if (other === task || other.status !== "in_progress") continue;
869
+ other.status = index < targetIndex ? "completed" : "pending";
870
+ other.turnId = turnId;
871
+ onIntermediate?.();
872
+ }
873
+
874
+ if (status === "completed" && task.status !== "in_progress" && task.status !== "completed") {
875
+ task.status = "in_progress";
876
+ task.turnId = turnId;
877
+ onIntermediate?.();
878
+ }
879
+ task.status = status;
880
+ } else if (status) {
881
+ task.status = status;
882
+ }
883
+ task.turnId = turnId;
884
+ return true;
885
+ }
886
+
887
+ // TaskList에는 예전 계획까지 함께 들어올 수 있다. 마지막으로 번호가 1부터
888
+ // 시작한 묶음만 현재 계획으로 삼되, 번호가 없는 목록은 손실 없이 그대로 둔다.
889
+ function latestTaskPlan(tasks) {
890
+ const entries = [...tasks.entries()];
891
+ let start = 0;
892
+ for (let index = 0; index < entries.length; index++) {
893
+ if (numberedTaskIndex(entries[index][1].subject) === 1) start = index;
894
+ }
895
+ return new Map(entries.slice(start));
896
+ }
784
897
 
785
898
  function updatePlanFromToolUse(session, name, toolUseId, input) {
786
- const turnId = session.turn?.id;
787
- if (name === "TaskCreate") {
788
- prepareTaskPlanForCreate(session.tasks, input.subject);
789
- session.tasks.set(`pending:${toolUseId}`, {
790
- id: `pending:${toolUseId}`,
791
- subject: input.subject || input.description || "작업",
899
+ const turnId = session.turn?.id;
900
+ if (name === "TaskCreate") {
901
+ prepareTaskPlanForCreate(session.tasks, input.subject);
902
+ session.tasks.set(`pending:${toolUseId}`, {
903
+ id: `pending:${toolUseId}`,
904
+ subject: input.subject || input.description || "작업",
792
905
  status: "pending",
793
- turnId,
794
- });
795
- } else if (name === "TaskUpdate") {
796
- applyTaskUpdate(session.tasks, input, turnId);
797
- }
798
- emitPlan(session);
799
- }
906
+ turnId,
907
+ });
908
+ } else if (name === "TaskUpdate") {
909
+ applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
910
+ }
911
+ emitPlan(session);
912
+ }
800
913
 
801
914
  function updatePlanFromToolResult(session, pending, message) {
802
915
  const value = message.tool_use_result;
@@ -824,10 +937,10 @@ function updatePlanFromToolResult(session, pending, message) {
824
937
  turnId: previous.get(id)?.turnId ?? turnId,
825
938
  });
826
939
  }
827
- session.tasks = latestTaskPlan(session.tasks);
828
- emitPlan(session);
829
- }
830
- }
940
+ session.tasks = latestTaskPlan(session.tasks);
941
+ emitPlan(session);
942
+ }
943
+ }
831
944
 
832
945
  function taskCreatedResult(structured, content) {
833
946
  const created = structured?.task || structured;
@@ -868,78 +981,78 @@ function numberedTaskSubject(subject, index) {
868
981
  // 그 ID로 묶어 두면 지금 어떤 에이전트가 무슨 도구를 돌리는지 그대로 복원할 수 있다.
869
982
  const SUBAGENT_TOOLS = ["Agent", "Task"];
870
983
 
871
- function startSubagent(session, block) {
872
- const input = block.input || {};
873
- session.subagents.set(block.id, {
874
- id: block.id,
875
- taskId: "",
876
- background: false,
877
- name: firstLine(input.subagent_type || input.agentType || "agent", 40),
984
+ function startSubagent(session, block) {
985
+ const input = block.input || {};
986
+ session.subagents.set(block.id, {
987
+ id: block.id,
988
+ taskId: "",
989
+ background: false,
990
+ name: firstLine(input.subagent_type || input.agentType || "agent", 40),
878
991
  description: firstLine(input.description || input.prompt || "", 120),
879
992
  tool: "",
880
993
  startedAt: Date.now(),
881
994
  });
882
- emitSubagents(session);
883
- }
884
-
885
- function isBackgroundSubagentResult(result) {
886
- return result?.isAsync === true || result?.status === "async_launched";
887
- }
888
-
889
- // Claude Code treats an async Agent result as a launch receipt. The agent remains
890
- // live until its later task-notification names the same task or tool-use id.
891
- function keepBackgroundSubagent(session, toolUseId, result) {
892
- if (!isBackgroundSubagentResult(result)) return false;
893
- const running = session.subagents.get(toolUseId);
894
- if (!running) return false;
895
- running.background = true;
896
- running.taskId = firstLine(result?.agentId || result?.taskId || "", 80);
897
- if (running.taskId) {
898
- session.knownSubagents.set(running.taskId, {
899
- name: running.name,
900
- description: running.description,
901
- });
902
- }
903
- return true;
904
- }
905
-
906
- // SendMessage can wake a completed agent from its transcript. Its new parent
907
- // tool-use id owns this run, while the stable agent id lets later notifications
908
- // and further resumes recover the original label.
909
- function resumeBackgroundSubagent(session, toolUseId, pending, result) {
910
- const taskId = firstLine(result?.resumedAgentId || "", 80);
911
- if (!taskId) return false;
912
- const existing = [...session.subagents.values()].find((agent) => agent.taskId === taskId);
913
- if (existing) return true;
914
- const known = session.knownSubagents.get(taskId);
915
- const input = pending?.input || {};
916
- const running = {
917
- id: toolUseId,
918
- taskId,
919
- background: true,
920
- name: known?.name || "agent",
921
- description: known?.description || firstLine(input.summary || input.message || "", 120),
922
- tool: "",
923
- startedAt: Date.now(),
924
- };
925
- session.subagents.set(toolUseId, running);
926
- session.knownSubagents.set(taskId, {
927
- name: running.name,
928
- description: running.description,
929
- });
930
- emitSubagents(session);
931
- return true;
932
- }
933
-
934
- function findSubagent(session, id) {
935
- return session.subagents.get(id)
936
- || [...session.subagents.values()].find((agent) => agent.taskId === id);
937
- }
995
+ emitSubagents(session);
996
+ }
997
+
998
+ function isBackgroundSubagentResult(result) {
999
+ return result?.isAsync === true || result?.status === "async_launched";
1000
+ }
1001
+
1002
+ // Claude Code treats an async Agent result as a launch receipt. The agent remains
1003
+ // live until its later task-notification names the same task or tool-use id.
1004
+ function keepBackgroundSubagent(session, toolUseId, result) {
1005
+ if (!isBackgroundSubagentResult(result)) return false;
1006
+ const running = session.subagents.get(toolUseId);
1007
+ if (!running) return false;
1008
+ running.background = true;
1009
+ running.taskId = firstLine(result?.agentId || result?.taskId || "", 80);
1010
+ if (running.taskId) {
1011
+ session.knownSubagents.set(running.taskId, {
1012
+ name: running.name,
1013
+ description: running.description,
1014
+ });
1015
+ }
1016
+ return true;
1017
+ }
1018
+
1019
+ // SendMessage can wake a completed agent from its transcript. Its new parent
1020
+ // tool-use id owns this run, while the stable agent id lets later notifications
1021
+ // and further resumes recover the original label.
1022
+ function resumeBackgroundSubagent(session, toolUseId, pending, result) {
1023
+ const taskId = firstLine(result?.resumedAgentId || "", 80);
1024
+ if (!taskId) return false;
1025
+ const existing = [...session.subagents.values()].find((agent) => agent.taskId === taskId);
1026
+ if (existing) return true;
1027
+ const known = session.knownSubagents.get(taskId);
1028
+ const input = pending?.input || {};
1029
+ const running = {
1030
+ id: toolUseId,
1031
+ taskId,
1032
+ background: true,
1033
+ name: known?.name || "agent",
1034
+ description: known?.description || firstLine(input.summary || input.message || "", 120),
1035
+ tool: "",
1036
+ startedAt: Date.now(),
1037
+ };
1038
+ session.subagents.set(toolUseId, running);
1039
+ session.knownSubagents.set(taskId, {
1040
+ name: running.name,
1041
+ description: running.description,
1042
+ });
1043
+ emitSubagents(session);
1044
+ return true;
1045
+ }
1046
+
1047
+ function findSubagent(session, id) {
1048
+ return session.subagents.get(id)
1049
+ || [...session.subagents.values()].find((agent) => agent.taskId === id);
1050
+ }
938
1051
 
939
1052
  // 서브에이전트가 실제로 무엇을 했는지는 자식 메시지에만 남는다. 열람용 기록은 여기서
940
1053
  // 한 줄씩 흘려보내고, 목록 행에 쓸 현재 도구만 따로 갱신한다.
941
- function recordSubagentMessage(session, message) {
942
- const running = findSubagent(session, message.parent_tool_use_id);
1054
+ function recordSubagentMessage(session, message) {
1055
+ const running = findSubagent(session, message.parent_tool_use_id);
943
1056
  if (!running) return;
944
1057
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
945
1058
  let toolChanged = false;
@@ -960,8 +1073,8 @@ function recordSubagentMessage(session, message) {
960
1073
  if (toolChanged) emitSubagents(session);
961
1074
  }
962
1075
 
963
- function recordSubagentResult(session, message) {
964
- const running = findSubagent(session, message.parent_tool_use_id);
1076
+ function recordSubagentResult(session, message) {
1077
+ const running = findSubagent(session, message.parent_tool_use_id);
965
1078
  if (!running) return;
966
1079
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
967
1080
  for (const block of content) {
@@ -997,32 +1110,32 @@ function subagentToolLabel(block) {
997
1110
  return text ? `${name}(${text})` : name;
998
1111
  }
999
1112
 
1000
- function finishSubagent(session, toolUseId) {
1001
- if (!session.subagents.delete(toolUseId)) return;
1002
- emitSubagents(session);
1003
- }
1004
-
1005
- function finishSubagentTask(session, taskId) {
1006
- if (!taskId) return false;
1007
- const entry = [...session.subagents.entries()].find(([, agent]) => agent.taskId === taskId);
1008
- if (!entry) return false;
1009
- session.subagents.delete(entry[0]);
1010
- emitSubagents(session);
1011
- return true;
1012
- }
1013
-
1014
- function clearForegroundSubagents(session) {
1015
- let changed = false;
1016
- for (const [id, agent] of session.subagents) {
1017
- if (agent.background) continue;
1018
- session.subagents.delete(id);
1019
- changed = true;
1020
- }
1021
- if (changed) emitSubagents(session);
1022
- }
1023
-
1024
- function clearSubagents(session) {
1025
- if (!session.subagents.size) return;
1113
+ function finishSubagent(session, toolUseId) {
1114
+ if (!session.subagents.delete(toolUseId)) return;
1115
+ emitSubagents(session);
1116
+ }
1117
+
1118
+ function finishSubagentTask(session, taskId) {
1119
+ if (!taskId) return false;
1120
+ const entry = [...session.subagents.entries()].find(([, agent]) => agent.taskId === taskId);
1121
+ if (!entry) return false;
1122
+ session.subagents.delete(entry[0]);
1123
+ emitSubagents(session);
1124
+ return true;
1125
+ }
1126
+
1127
+ function clearForegroundSubagents(session) {
1128
+ let changed = false;
1129
+ for (const [id, agent] of session.subagents) {
1130
+ if (agent.background) continue;
1131
+ session.subagents.delete(id);
1132
+ changed = true;
1133
+ }
1134
+ if (changed) emitSubagents(session);
1135
+ }
1136
+
1137
+ function clearSubagents(session) {
1138
+ if (!session.subagents.size) return;
1026
1139
  session.subagents.clear();
1027
1140
  emitSubagents(session);
1028
1141
  }
@@ -1042,87 +1155,87 @@ function emitSubagents(session) {
1042
1155
  tool: agent.tool,
1043
1156
  elapsedMs: Date.now() - agent.startedAt,
1044
1157
  })),
1045
- });
1046
- }
1047
-
1048
- function messageTextParts(message) {
1049
- const content = message.message?.content;
1050
- if (typeof content === "string") return [content];
1051
- if (!Array.isArray(content)) return [];
1052
- return content
1053
- .filter((block) => block?.type === "text" && typeof block.text === "string")
1054
- .map((block) => block.text);
1055
- }
1056
-
1057
- function notificationTag(body, name) {
1058
- const match = body.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
1059
- return match?.[1]?.trim() || "";
1060
- }
1061
-
1062
- function taskNotifications(message) {
1063
- if (message.origin?.kind !== "task-notification"
1064
- && !messageTextParts(message).some((text) => text.includes("<task-notification>"))) {
1065
- return [];
1066
- }
1067
- const notifications = [];
1068
- for (const text of messageTextParts(message)) {
1069
- for (const match of text.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)) {
1070
- const body = match[1];
1071
- notifications.push({
1072
- taskId: notificationTag(body, "task-id"),
1073
- toolUseId: notificationTag(body, "tool-use-id"),
1074
- status: notificationTag(body, "status"),
1075
- summary: notificationTag(body, "summary"),
1076
- });
1077
- }
1078
- }
1079
- return notifications;
1080
- }
1081
-
1082
- function finishNotifiedSubagents(session, notifications) {
1083
- for (const notification of notifications) {
1084
- const byToolUse = notification.toolUseId
1085
- ? session.subagents.get(notification.toolUseId)
1086
- : null;
1087
- const running = byToolUse || (notification.taskId
1088
- ? [...session.subagents.values()].find((agent) => agent.taskId === notification.taskId)
1089
- : null);
1090
- if (!running) continue;
1091
- emitSubagentLine(session, running.id, {
1092
- kind: notification.status === "completed" ? "result" : "error",
1093
- text: notification.summary || notification.status || "완료됨",
1094
- });
1095
- session.subagents.delete(running.id);
1096
- emitSubagents(session);
1097
- }
1098
- }
1099
-
1100
- function processUser(session, message) {
1158
+ });
1159
+ }
1160
+
1161
+ function messageTextParts(message) {
1162
+ const content = message.message?.content;
1163
+ if (typeof content === "string") return [content];
1164
+ if (!Array.isArray(content)) return [];
1165
+ return content
1166
+ .filter((block) => block?.type === "text" && typeof block.text === "string")
1167
+ .map((block) => block.text);
1168
+ }
1169
+
1170
+ function notificationTag(body, name) {
1171
+ const match = body.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
1172
+ return match?.[1]?.trim() || "";
1173
+ }
1174
+
1175
+ function taskNotifications(message) {
1176
+ if (message.origin?.kind !== "task-notification"
1177
+ && !messageTextParts(message).some((text) => text.includes("<task-notification>"))) {
1178
+ return [];
1179
+ }
1180
+ const notifications = [];
1181
+ for (const text of messageTextParts(message)) {
1182
+ for (const match of text.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)) {
1183
+ const body = match[1];
1184
+ notifications.push({
1185
+ taskId: notificationTag(body, "task-id"),
1186
+ toolUseId: notificationTag(body, "tool-use-id"),
1187
+ status: notificationTag(body, "status"),
1188
+ summary: notificationTag(body, "summary"),
1189
+ });
1190
+ }
1191
+ }
1192
+ return notifications;
1193
+ }
1194
+
1195
+ function finishNotifiedSubagents(session, notifications) {
1196
+ for (const notification of notifications) {
1197
+ const byToolUse = notification.toolUseId
1198
+ ? session.subagents.get(notification.toolUseId)
1199
+ : null;
1200
+ const running = byToolUse || (notification.taskId
1201
+ ? [...session.subagents.values()].find((agent) => agent.taskId === notification.taskId)
1202
+ : null);
1203
+ if (!running) continue;
1204
+ emitSubagentLine(session, running.id, {
1205
+ kind: notification.status === "completed" ? "result" : "error",
1206
+ text: notification.summary || notification.status || "완료됨",
1207
+ });
1208
+ session.subagents.delete(running.id);
1209
+ emitSubagents(session);
1210
+ }
1211
+ }
1212
+
1213
+ function processUser(session, message) {
1101
1214
  // 자식 tool_result의 tool_use_id는 부모 세션의 것과 다른 공간이므로, 부모 흐름에
1102
1215
  // 섞이기 전에 서브에이전트 기록으로 보낸다.
1103
1216
  if (message.parent_tool_use_id) {
1104
- recordSubagentResult(session, message);
1105
- return;
1106
- }
1107
- const notifications = taskNotifications(message);
1108
- if (notifications.length) {
1109
- if (!session.turn) beginTurn(session);
1110
- finishNotifiedSubagents(session, notifications);
1111
- return;
1112
- }
1113
- const content = Array.isArray(message.message?.content) ? message.message.content : [];
1114
- for (const block of content) {
1115
- if (block.type !== "tool_result") continue;
1116
- const pending = session.tools.get(block.tool_use_id);
1117
- const staysInBackground = SUBAGENT_TOOLS.includes(pending?.name)
1118
- && keepBackgroundSubagent(session, block.tool_use_id, message.tool_use_result);
1119
- if (!staysInBackground) finishSubagent(session, block.tool_use_id);
1120
- if (pending?.name === "SendMessage") {
1121
- resumeBackgroundSubagent(session, block.tool_use_id, pending, message.tool_use_result);
1122
- } else if (pending?.name === "TaskStop" && message.tool_use_result?.success !== false) {
1123
- finishSubagentTask(session, firstLine(pending.input?.task_id || "", 80));
1124
- }
1125
- if (!pending) continue;
1217
+ recordSubagentResult(session, message);
1218
+ return;
1219
+ }
1220
+ const notifications = taskNotifications(message);
1221
+ if (notifications.length) {
1222
+ if (!session.turn) beginTurn(session);
1223
+ finishNotifiedSubagents(session, notifications);
1224
+ return;
1225
+ }
1226
+ const content = Array.isArray(message.message?.content) ? message.message.content : [];
1227
+ for (const block of content) {
1228
+ if (block.type !== "tool_result") continue;
1229
+ const pending = session.tools.get(block.tool_use_id);
1230
+ const staysInBackground = SUBAGENT_TOOLS.includes(pending?.name)
1231
+ && keepBackgroundSubagent(session, block.tool_use_id, message.tool_use_result);
1232
+ if (!staysInBackground) finishSubagent(session, block.tool_use_id);
1233
+ if (pending?.name === "SendMessage") {
1234
+ resumeBackgroundSubagent(session, block.tool_use_id, pending, message.tool_use_result);
1235
+ } else if (pending?.name === "TaskStop" && message.tool_use_result?.success !== false) {
1236
+ finishSubagentTask(session, firstLine(pending.input?.task_id || "", 80));
1237
+ }
1238
+ if (!pending) continue;
1126
1239
  pending.toolUseId = block.tool_use_id;
1127
1240
  if (pending.suppressed) {
1128
1241
  updatePlanFromToolResult(session, pending, message);
@@ -1209,10 +1322,10 @@ async function runPendingPrompt(session) {
1209
1322
  }
1210
1323
  }
1211
1324
 
1212
- function finishTurn(session, error, durationMs) {
1213
- if (!session.turn) return;
1214
- flushSmoothStreams(session);
1215
- clearForegroundSubagents(session);
1325
+ function finishTurn(session, error, durationMs) {
1326
+ if (!session.turn) return;
1327
+ flushSmoothStreams(session);
1328
+ clearForegroundSubagents(session);
1216
1329
  const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
1217
1330
  if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
1218
1331
  if (durationMs != null) turn.durationMs = durationMs;
@@ -1228,7 +1341,7 @@ async function consume(session) {
1228
1341
  if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
1229
1342
  if (session.turn) session.turn.sawStreamText = true;
1230
1343
  }
1231
- await processStreamEvent(session, message);
1344
+ await processStreamEvent(session, message);
1232
1345
  } else if (message.type === "assistant") processAssistant(session, message);
1233
1346
  else if (message.type === "user") processUser(session, message);
1234
1347
  else if (message.type === "result") await processResult(session, message);
@@ -1309,28 +1422,36 @@ async function runPrompt(session, params) {
1309
1422
  if (effort) {
1310
1423
  await session.query.applyFlagSettings({ effortLevel: effort });
1311
1424
  }
1312
- session.effort = effort;
1313
- await applyPermissionMode(session, params.permissionMode);
1314
- const turnId = beginTurn(session);
1315
- session.queue.push({
1316
- type: "user",
1317
- message: { role: "user", content: await inputContent(params.input, params.handoffContext) },
1425
+ session.effort = effort;
1426
+ await applyPermissionMode(session, params.permissionMode);
1427
+ const content = await inputContent(params.input, params.handoffContext);
1428
+ const turnId = beginTurn(session, params.input);
1429
+ session.queue.push({
1430
+ type: "user",
1431
+ message: { role: "user", content },
1318
1432
  parent_tool_use_id: null,
1319
1433
  session_id: id,
1320
1434
  origin: { kind: "human" },
1321
1435
  });
1322
- return { turn: { id: turnId } };
1323
- }
1324
-
1325
- // A background task notification is an internal user message that starts its
1326
- // own Claude response even though the host did not submit a new prompt.
1327
- function beginTurn(session) {
1436
+ return { turn: { id: turnId } };
1437
+ }
1438
+
1439
+ // A background task notification is an internal user message that starts its
1440
+ // own Claude response even though the host did not submit a new prompt.
1441
+ function beginTurn(session, input = []) {
1328
1442
  const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
1329
- session.turn = { id: turnId, sawStreamText: false };
1330
- session.lastContextUsage = null;
1331
- notify("turn/started", { threadId: session.id, turn: { id: turnId } });
1332
- return turnId;
1333
- }
1443
+ session.turn = {
1444
+ id: turnId,
1445
+ sawStreamText: false,
1446
+ sawVisibleText: false,
1447
+ koreanRequest: isKoreanPrompt(input),
1448
+ openingNotice: openingNotice(input),
1449
+ openingNoticeEmitted: false,
1450
+ };
1451
+ session.lastContextUsage = null;
1452
+ notify("turn/started", { threadId: session.id, turn: { id: turnId } });
1453
+ return turnId;
1454
+ }
1334
1455
 
1335
1456
  function contentBlocks(message) {
1336
1457
  const content = message?.content;
@@ -1355,7 +1476,7 @@ function isInternalHistoryText(message, text) {
1355
1476
  ].includes(tag);
1356
1477
  }
1357
1478
 
1358
- function historyState(messages) {
1479
+ function historyState(messages) {
1359
1480
  const turns = [];
1360
1481
  let turn = null;
1361
1482
  const tools = new Map();
@@ -1391,14 +1512,14 @@ function historyState(messages) {
1391
1512
  if (block.type === "text") turn.items.push({ id: `${message.uuid}-text`, type: "agentMessage", text: block.text || "", provider: "Claude" });
1392
1513
  else if (block.type === "thinking") turn.items.push({ id: `${message.uuid}-thinking`, type: "reasoning", summary: [block.thinking || ""] });
1393
1514
  else if (block.type === "tool_use") {
1394
- const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
1395
- tools.set(block.id, pending);
1396
- if (block.name === "TaskCreate") {
1397
- prepareTaskPlanForCreate(tasks, block.input?.subject);
1398
- tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "작업", status: "pending", turnId: turn.id });
1399
- } else if (block.name === "TaskUpdate") {
1400
- applyTaskUpdate(tasks, block.input || {}, turn.id);
1401
- } else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
1515
+ const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
1516
+ tools.set(block.id, pending);
1517
+ if (block.name === "TaskCreate") {
1518
+ prepareTaskPlanForCreate(tasks, block.input?.subject);
1519
+ tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "작업", status: "pending", turnId: turn.id });
1520
+ } else if (block.name === "TaskUpdate") {
1521
+ applyTaskUpdate(tasks, block.input || {}, turn.id);
1522
+ } else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
1402
1523
  }
1403
1524
  }
1404
1525
  } else if (message.type === "user") {
@@ -1415,12 +1536,12 @@ function historyState(messages) {
1415
1536
  tasks.set(temporary.id, temporary);
1416
1537
  }
1417
1538
  } else if (pending.name === "TaskList" && Array.isArray(message.tool_use_result?.tasks)) {
1418
- const known = new Map(tasks);
1419
- tasks.clear();
1420
- for (const task of message.tool_use_result.tasks) tasks.set(String(task.id), { id: String(task.id), subject: task.subject || "작업", status: task.status || "pending", turnId: known.get(String(task.id))?.turnId ?? turn.id });
1421
- const current = latestTaskPlan(tasks);
1422
- tasks.clear();
1423
- for (const [id, task] of current) tasks.set(id, task);
1539
+ const known = new Map(tasks);
1540
+ tasks.clear();
1541
+ for (const task of message.tool_use_result.tasks) tasks.set(String(task.id), { id: String(task.id), subject: task.subject || "작업", status: task.status || "pending", turnId: known.get(String(task.id))?.turnId ?? turn.id });
1542
+ const current = latestTaskPlan(tasks);
1543
+ tasks.clear();
1544
+ for (const [id, task] of current) tasks.set(id, task);
1424
1545
  } else if (pending.item) {
1425
1546
  const output = toolOutput(block.content, message.tool_use_result);
1426
1547
  Object.assign(pending.item, pending.item.type === "commandExecution"
@@ -1436,17 +1557,17 @@ function historyState(messages) {
1436
1557
  const text = [...tasks.values()].map((task, index) => `${task.status === "completed" ? "✓" : task.status === "in_progress" ? "▸" : "□"} ${numberedTaskSubject(task.subject, index)}`).join("\n");
1437
1558
  turn.items.push({ id: "claude-plan-latest", type: "plan", text });
1438
1559
  }
1439
- return {
1440
- tasks,
1441
- turns: turns
1442
- .filter((candidate) => !candidate.synthetic)
1443
- .map(({ synthetic: _, ...candidate }) => candidate),
1444
- };
1445
- }
1446
-
1447
- function historyTurns(messages) {
1448
- return historyState(messages).turns;
1449
- }
1560
+ return {
1561
+ tasks,
1562
+ turns: turns
1563
+ .filter((candidate) => !candidate.synthetic)
1564
+ .map(({ synthetic: _, ...candidate }) => candidate),
1565
+ };
1566
+ }
1567
+
1568
+ function historyTurns(messages) {
1569
+ return historyState(messages).turns;
1570
+ }
1450
1571
 
1451
1572
  async function dispatch(method, params = {}) {
1452
1573
  if (method === "model/list") return loadModelCatalog(params);
@@ -1471,11 +1592,11 @@ async function dispatch(method, params = {}) {
1471
1592
  }
1472
1593
  if (method === "session/resume") {
1473
1594
  const id = liveSessionId(params.sessionId);
1474
- const existing = sessions.get(id);
1475
- if (existing) {
1476
- const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
1477
- if (!existing.tasks.size) existing.tasks = historyState(messages).tasks;
1478
- return {
1595
+ const existing = sessions.get(id);
1596
+ if (existing) {
1597
+ const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
1598
+ if (!existing.tasks.size) existing.tasks = historyState(messages).tasks;
1599
+ return {
1479
1600
  id,
1480
1601
  thread: { id, turns: [] },
1481
1602
  initialTurnsPage: { data: [], nextCursor: null },
@@ -1499,11 +1620,11 @@ async function dispatch(method, params = {}) {
1499
1620
  model: params.model || lastModel || params.fallbackModel,
1500
1621
  effort: params.effort || params.fallbackEffort,
1501
1622
  }, id);
1502
- const tokenUsage = historyTokenUsage(messages, session.models, session.model);
1503
- // Seed the live session so the next turn keeps reporting a full context.
1504
- session.lastContextUsage = tokenUsage?.last || null;
1505
- session.lastContextWindow = tokenUsage?.modelContextWindow || 0;
1506
- session.tasks = historyState(messages).tasks;
1623
+ const tokenUsage = historyTokenUsage(messages, session.models, session.model);
1624
+ // Seed the live session so the next turn keeps reporting a full context.
1625
+ session.lastContextUsage = tokenUsage?.last || null;
1626
+ session.lastContextWindow = tokenUsage?.modelContextWindow || 0;
1627
+ session.tasks = historyState(messages).tasks;
1507
1628
  return {
1508
1629
  id,
1509
1630
  thread: { id, turns: [] },
@@ -1605,7 +1726,7 @@ async function dispatch(method, params = {}) {
1605
1726
  throw new Error(`지원하지 않는 Claude 브리지 메서드: ${method}`);
1606
1727
  }
1607
1728
 
1608
- async function runSelfTest() {
1729
+ async function runSelfTest() {
1609
1730
  const user = (uuid, text) => ({
1610
1731
  type: "user",
1611
1732
  uuid,
@@ -1636,70 +1757,89 @@ async function runSelfTest() {
1636
1757
  ["say hello", "claude:claude-sonnet-5"],
1637
1758
  ["hay zzz", "claude:claude-haiku-4-5-20251001"],
1638
1759
  ];
1639
- if (turns.length !== expected.length
1640
- || prompts.some((prompt, index) => prompt?.content?.[0]?.text !== expected[index][0]
1641
- || prompt.model !== expected[index][1])) {
1642
- throw new Error(`Claude history self-test failed: ${JSON.stringify(turns)}`);
1643
- }
1644
- const taskUse = (uuid, id, name, input) => ({
1645
- type: "assistant",
1646
- uuid,
1647
- message: {
1648
- role: "assistant",
1649
- model: "claude-opus-5",
1650
- content: [{ type: "tool_use", id, name, input }],
1651
- },
1652
- });
1653
- const taskResult = (uuid, id, toolUseResult, content) => ({
1654
- type: "user",
1655
- uuid,
1656
- message: { role: "user", content: [{ type: "tool_result", tool_use_id: id, content }] },
1657
- tool_use_result: toolUseResult,
1658
- });
1659
- const taskMessages = [user("plan", "작업을 진행해")];
1660
- for (let index = 1; index <= 6; index++) {
1661
- const id = String(24 + index);
1662
- const toolId = `create-${id}`;
1663
- const subject = `${index}. 작업 ${index}`;
1664
- taskMessages.push(
1665
- taskUse(`create-use-${id}`, toolId, "TaskCreate", { subject }),
1666
- taskResult(`create-result-${id}`, toolId, { task: { id, subject } }, `Task #${id} created successfully: ${subject}`),
1667
- );
1668
- }
1669
- taskMessages.push(
1670
- taskUse("update-25-start", "update-25-start", "TaskUpdate", { taskId: "25", status: "in_progress" }),
1671
- taskUse("update-25-done", "update-25-done", "TaskUpdate", { taskId: "25", status: "completed" }),
1672
- taskUse("update-26-start", "update-26-start", "TaskUpdate", { taskId: "26", status: "in_progress" }),
1673
- );
1674
- const restoredTasks = historyState(taskMessages).tasks;
1675
- if ([...restoredTasks.keys()].join(",") !== "25,26,27,28,29,30"
1676
- || restoredTasks.get("25")?.status !== "completed"
1677
- || restoredTasks.get("26")?.status !== "in_progress"
1678
- || [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
1679
- throw new Error(`Claude task resume self-test failed: ${JSON.stringify([...restoredTasks])}`);
1680
- }
1681
- applyTaskUpdate(restoredTasks, { taskId: "26", status: "completed" }, "resumed-turn");
1682
- applyTaskUpdate(restoredTasks, { taskId: "27", status: "in_progress" }, "resumed-turn");
1683
- if (restoredTasks.size !== 6
1684
- || restoredTasks.get("25")?.status !== "completed"
1685
- || restoredTasks.get("26")?.status !== "completed"
1686
- || restoredTasks.get("27")?.status !== "in_progress"
1687
- || [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
1688
- throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
1689
- }
1690
- const mixedPlans = new Map([
1691
- ["old-1", { subject: "1. 이전 작업", status: "completed" }],
1692
- ["old-2", { subject: "2. 이전 검증", status: "completed" }],
1693
- ...[...restoredTasks],
1694
- ]);
1695
- if ([...latestTaskPlan(mixedPlans).keys()].join(",") !== "25,26,27,28,29,30") {
1696
- throw new Error(`Claude latest task plan self-test failed: ${JSON.stringify([...mixedPlans])}`);
1697
- }
1698
- prepareTaskPlanForCreate(restoredTasks, "7. 추가 작업");
1699
- if (restoredTasks.size !== 6) throw new Error("Claude appended task unexpectedly reset the plan");
1700
- prepareTaskPlanForCreate(restoredTasks, "1. 새 작업");
1701
- if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
1702
- const usage = tokenBreakdown({
1760
+ if (turns.length !== expected.length
1761
+ || prompts.some((prompt, index) => prompt?.content?.[0]?.text !== expected[index][0]
1762
+ || prompt.model !== expected[index][1])) {
1763
+ throw new Error(`Claude history self-test failed: ${JSON.stringify(turns)}`);
1764
+ }
1765
+ const taskUse = (uuid, id, name, input) => ({
1766
+ type: "assistant",
1767
+ uuid,
1768
+ message: {
1769
+ role: "assistant",
1770
+ model: "claude-opus-5",
1771
+ content: [{ type: "tool_use", id, name, input }],
1772
+ },
1773
+ });
1774
+ const taskResult = (uuid, id, toolUseResult, content) => ({
1775
+ type: "user",
1776
+ uuid,
1777
+ message: { role: "user", content: [{ type: "tool_result", tool_use_id: id, content }] },
1778
+ tool_use_result: toolUseResult,
1779
+ });
1780
+ const taskMessages = [user("plan", "작업을 진행해")];
1781
+ for (let index = 1; index <= 6; index++) {
1782
+ const id = String(24 + index);
1783
+ const toolId = `create-${id}`;
1784
+ const subject = `${index}. 작업 ${index}`;
1785
+ taskMessages.push(
1786
+ taskUse(`create-use-${id}`, toolId, "TaskCreate", { subject }),
1787
+ taskResult(`create-result-${id}`, toolId, { task: { id, subject } }, `Task #${id} created successfully: ${subject}`),
1788
+ );
1789
+ }
1790
+ taskMessages.push(
1791
+ taskUse("update-25-start", "update-25-start", "TaskUpdate", { taskId: "25", status: "in_progress" }),
1792
+ taskUse("update-25-done", "update-25-done", "TaskUpdate", { taskId: "25", status: "completed" }),
1793
+ taskUse("update-26-start", "update-26-start", "TaskUpdate", { taskId: "26", status: "in_progress" }),
1794
+ );
1795
+ const restoredTasks = historyState(taskMessages).tasks;
1796
+ if ([...restoredTasks.keys()].join(",") !== "25,26,27,28,29,30"
1797
+ || restoredTasks.get("25")?.status !== "completed"
1798
+ || restoredTasks.get("26")?.status !== "in_progress"
1799
+ || [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
1800
+ throw new Error(`Claude task resume self-test failed: ${JSON.stringify([...restoredTasks])}`);
1801
+ }
1802
+ applyTaskUpdate(restoredTasks, { taskId: "26", status: "completed" }, "resumed-turn");
1803
+ applyTaskUpdate(restoredTasks, { taskId: "27", status: "in_progress" }, "resumed-turn");
1804
+ if (restoredTasks.size !== 6
1805
+ || restoredTasks.get("25")?.status !== "completed"
1806
+ || restoredTasks.get("26")?.status !== "completed"
1807
+ || restoredTasks.get("27")?.status !== "in_progress"
1808
+ || [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
1809
+ throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
1810
+ }
1811
+ const skippedTasks = new Map([
1812
+ ["1", { id: "1", subject: "1. 조사", status: "pending" }],
1813
+ ["2", { id: "2", subject: "2. 분석", status: "pending" }],
1814
+ ["3", { id: "3", subject: "3. 검증", status: "pending" }],
1815
+ ]);
1816
+ const transitions = [];
1817
+ const snapshot = () => transitions.push([...skippedTasks.values()].map((task) => task.status).join(","));
1818
+ applyTaskUpdate(skippedTasks, { taskId: "3", status: "completed" }, "turn", snapshot);
1819
+ snapshot();
1820
+ if (transitions.join("|") !== [
1821
+ "in_progress,pending,pending",
1822
+ "completed,pending,pending",
1823
+ "completed,in_progress,pending",
1824
+ "completed,completed,pending",
1825
+ "completed,completed,in_progress",
1826
+ "completed,completed,completed",
1827
+ ].join("|")) {
1828
+ throw new Error(`Claude skipped task transition self-test failed: ${transitions.join("|")}`);
1829
+ }
1830
+ const mixedPlans = new Map([
1831
+ ["old-1", { subject: "1. 이전 작업", status: "completed" }],
1832
+ ["old-2", { subject: "2. 이전 검증", status: "completed" }],
1833
+ ...[...restoredTasks],
1834
+ ]);
1835
+ if ([...latestTaskPlan(mixedPlans).keys()].join(",") !== "25,26,27,28,29,30") {
1836
+ throw new Error(`Claude latest task plan self-test failed: ${JSON.stringify([...mixedPlans])}`);
1837
+ }
1838
+ prepareTaskPlanForCreate(restoredTasks, "7. 추가 작업");
1839
+ if (restoredTasks.size !== 6) throw new Error("Claude appended task unexpectedly reset the plan");
1840
+ prepareTaskPlanForCreate(restoredTasks, "1. 새 작업");
1841
+ if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
1842
+ const usage = tokenBreakdown({
1703
1843
  input_tokens: 2,
1704
1844
  cache_read_input_tokens: 68_000,
1705
1845
  cache_creation_input_tokens: 500,
@@ -1713,148 +1853,226 @@ async function runSelfTest() {
1713
1853
  catalogEntry({ value: "sonnet", resolvedModel: "claude-sonnet-5" }, "").contextWindow,
1714
1854
  catalogEntry({ value: "haiku", resolvedModel: "x", contextWindow: 300_000 }, "").contextWindow,
1715
1855
  ];
1716
- if (windows.join(",") !== "1000000,200000,300000") {
1717
- throw new Error(`Claude context window self-test failed: ${windows.join(",")}`);
1718
- }
1719
- const notification = taskNotifications({
1720
- origin: { kind: "task-notification" },
1721
- message: {
1722
- content: `<task-notification>
1723
- <task-id>agent-1</task-id>
1724
- <tool-use-id>toolu_1</tool-use-id>
1725
- <status>completed</status>
1726
- <summary>Agent "Explore" finished</summary>
1727
- <result>done</result>
1728
- </task-notification>`,
1729
- },
1730
- });
1731
- if (notification.length !== 1
1732
- || notification[0].taskId !== "agent-1"
1733
- || notification[0].toolUseId !== "toolu_1"
1734
- || notification[0].status !== "completed"
1735
- || notification[0].summary !== 'Agent "Explore" finished') {
1736
- throw new Error(`Claude task notification self-test failed: ${JSON.stringify(notification)}`);
1856
+ if (windows.join(",") !== "1000000,200000,300000") {
1857
+ throw new Error(`Claude context window self-test failed: ${windows.join(",")}`);
1858
+ }
1859
+ const notification = taskNotifications({
1860
+ origin: { kind: "task-notification" },
1861
+ message: {
1862
+ content: `<task-notification>
1863
+ <task-id>agent-1</task-id>
1864
+ <tool-use-id>toolu_1</tool-use-id>
1865
+ <status>completed</status>
1866
+ <summary>Agent "Explore" finished</summary>
1867
+ <result>done</result>
1868
+ </task-notification>`,
1869
+ },
1870
+ });
1871
+ if (notification.length !== 1
1872
+ || notification[0].taskId !== "agent-1"
1873
+ || notification[0].toolUseId !== "toolu_1"
1874
+ || notification[0].status !== "completed"
1875
+ || notification[0].summary !== 'Agent "Explore" finished') {
1876
+ throw new Error(`Claude task notification self-test failed: ${JSON.stringify(notification)}`);
1877
+ }
1878
+ const lifecycleSession = {
1879
+ id: "self-test-session",
1880
+ turn: { id: "parent-turn", sawStreamText: false },
1881
+ turnSequence: 1,
1882
+ streamBlocks: new Map(),
1883
+ tools: new Map([[
1884
+ "toolu_1",
1885
+ {
1886
+ name: "Agent",
1887
+ input: { subagent_type: "Explore", description: "Inspect files" },
1888
+ item: { id: "toolu_1", type: "collabAgentToolCall", tool: { name: "Agent", arguments: {} } },
1889
+ },
1890
+ ]]),
1891
+ subagents: new Map([[
1892
+ "toolu_1",
1893
+ {
1894
+ id: "toolu_1",
1895
+ taskId: "",
1896
+ background: false,
1897
+ name: "Explore",
1898
+ description: "Inspect files",
1899
+ tool: "",
1900
+ startedAt: Date.now(),
1901
+ },
1902
+ ]]),
1903
+ knownSubagents: new Map(),
1904
+ lastContextUsage: null,
1905
+ };
1906
+ const captured = [];
1907
+ const stdoutWrite = process.stdout.write;
1908
+ process.stdout.write = (chunk) => {
1909
+ captured.push(String(chunk));
1910
+ return true;
1911
+ };
1912
+ try {
1913
+ processUser(lifecycleSession, {
1914
+ message: { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "launched" }] },
1915
+ tool_use_result: { isAsync: true, status: "async_launched", agentId: "agent-1" },
1916
+ });
1917
+ finishTurn(lifecycleSession, null, 1);
1918
+ if (!lifecycleSession.subagents.has("toolu_1") || lifecycleSession.turn !== null) {
1919
+ throw new Error("Claude background subagent did not survive its parent turn");
1920
+ }
1921
+ processUser(lifecycleSession, {
1922
+ origin: { kind: "task-notification" },
1923
+ message: { content: notification[0] && `<task-notification>
1924
+ <task-id>agent-1</task-id><tool-use-id>toolu_1</tool-use-id>
1925
+ <status>completed</status><summary>Agent finished</summary>
1926
+ </task-notification>` },
1927
+ });
1928
+ if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
1929
+ throw new Error("Claude task notification did not finish the agent in an automatic turn");
1930
+ }
1931
+ finishTurn(lifecycleSession, null, 1);
1932
+
1933
+ beginTurn(lifecycleSession);
1934
+ lifecycleSession.tools.set("toolu_2", {
1935
+ name: "SendMessage",
1936
+ input: { to: "agent-1", summary: "Continue inspection" },
1937
+ item: { id: "toolu_2", type: "dynamicToolCall", tool: "SendMessage", arguments: {} },
1938
+ });
1939
+ processUser(lifecycleSession, {
1940
+ message: { content: [{ type: "tool_result", tool_use_id: "toolu_2", content: "resumed" }] },
1941
+ tool_use_result: { success: true, resumedAgentId: "agent-1" },
1942
+ });
1943
+ const resumed = lifecycleSession.subagents.get("toolu_2");
1944
+ if (!resumed?.background || resumed.taskId !== "agent-1" || resumed.name !== "Explore") {
1945
+ throw new Error(`Claude resumed subagent self-test failed: ${JSON.stringify(resumed)}`);
1946
+ }
1947
+ finishTurn(lifecycleSession, null, 1);
1948
+ processUser(lifecycleSession, {
1949
+ origin: { kind: "task-notification" },
1950
+ message: { content: `<task-notification>
1951
+ <task-id>agent-1</task-id><tool-use-id>toolu_2</tool-use-id>
1952
+ <status>failed</status><summary>Agent failed</summary>
1953
+ </task-notification>` },
1954
+ });
1955
+ if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
1956
+ throw new Error("Claude failed task notification did not finish the resumed agent");
1957
+ }
1958
+ finishTurn(lifecycleSession, null, 1);
1959
+ } finally {
1960
+ process.stdout.write = stdoutWrite;
1961
+ }
1962
+ const lifecycleEvents = captured
1963
+ .join("")
1964
+ .trim()
1965
+ .split("\n")
1966
+ .filter(Boolean)
1967
+ .map((line) => JSON.parse(line));
1968
+ const lifecycleMethods = lifecycleEvents.map((event) => event.method);
1969
+ if (!lifecycleMethods.includes("turn/subagents/updated")
1970
+ || lifecycleMethods.filter((method) => method === "turn/started").length < 3
1971
+ || !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
1972
+ && event.params?.line?.kind === "error")) {
1973
+ throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
1737
1974
  }
1738
- const lifecycleSession = {
1739
- id: "self-test-session",
1740
- turn: { id: "parent-turn", sawStreamText: false },
1975
+ const openingSession = {
1976
+ id: "opening-self-test",
1977
+ model: "claude:default",
1978
+ models: [],
1979
+ turn: null,
1741
1980
  turnSequence: 1,
1981
+ itemSequence: 1,
1742
1982
  streamBlocks: new Map(),
1743
- tools: new Map([[
1744
- "toolu_1",
1745
- {
1746
- name: "Agent",
1747
- input: { subagent_type: "Explore", description: "Inspect files" },
1748
- item: { id: "toolu_1", type: "collabAgentToolCall", tool: { name: "Agent", arguments: {} } },
1749
- },
1750
- ]]),
1751
- subagents: new Map([[
1752
- "toolu_1",
1753
- {
1754
- id: "toolu_1",
1755
- taskId: "",
1756
- background: false,
1757
- name: "Explore",
1758
- description: "Inspect files",
1759
- tool: "",
1760
- startedAt: Date.now(),
1761
- },
1762
- ]]),
1983
+ tools: new Map(),
1984
+ tasks: new Map(),
1985
+ subagents: new Map(),
1763
1986
  knownSubagents: new Map(),
1764
1987
  lastContextUsage: null,
1988
+ lastContextWindow: 0,
1765
1989
  };
1766
- const captured = [];
1767
- const stdoutWrite = process.stdout.write;
1990
+ const openingCaptured = [];
1768
1991
  process.stdout.write = (chunk) => {
1769
- captured.push(String(chunk));
1992
+ openingCaptured.push(String(chunk));
1770
1993
  return true;
1771
1994
  };
1772
1995
  try {
1773
- processUser(lifecycleSession, {
1774
- message: { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "launched" }] },
1775
- tool_use_result: { isAsync: true, status: "async_launched", agentId: "agent-1" },
1776
- });
1777
- finishTurn(lifecycleSession, null, 1);
1778
- if (!lifecycleSession.subagents.has("toolu_1") || lifecycleSession.turn !== null) {
1779
- throw new Error("Claude background subagent did not survive its parent turn");
1780
- }
1781
- processUser(lifecycleSession, {
1782
- origin: { kind: "task-notification" },
1783
- message: { content: notification[0] && `<task-notification>
1784
- <task-id>agent-1</task-id><tool-use-id>toolu_1</tool-use-id>
1785
- <status>completed</status><summary>Agent finished</summary>
1786
- </task-notification>` },
1996
+ beginTurn(openingSession, [{ type: "text", text: "provider 메뉴를 수정해" }]);
1997
+ processAssistant(openingSession, {
1998
+ message: {
1999
+ content: [{ type: "tool_use", id: "read-1", name: "Read", input: { file_path: "src/main.rs" } }],
2000
+ },
1787
2001
  });
1788
- if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
1789
- throw new Error("Claude task notification did not finish the agent in an automatic turn");
1790
- }
1791
- finishTurn(lifecycleSession, null, 1);
1792
-
1793
- beginTurn(lifecycleSession);
1794
- lifecycleSession.tools.set("toolu_2", {
1795
- name: "SendMessage",
1796
- input: { to: "agent-1", summary: "Continue inspection" },
1797
- item: { id: "toolu_2", type: "dynamicToolCall", tool: "SendMessage", arguments: {} },
2002
+ } finally {
2003
+ process.stdout.write = stdoutWrite;
2004
+ }
2005
+ const openingEvents = openingCaptured
2006
+ .join("")
2007
+ .trim()
2008
+ .split("\n")
2009
+ .filter(Boolean)
2010
+ .map((line) => JSON.parse(line));
2011
+ const openingMessageIndex = openingEvents.findIndex((event) =>
2012
+ event.method === "item/completed"
2013
+ && event.params?.item?.type === "agentMessage"
2014
+ && event.params.item.text === "요청 내용을 확인하고 필요한 작업을 진행하겠습니다.");
2015
+ const openingToolIndex = openingEvents.findIndex((event) =>
2016
+ event.method === "item/started" && event.params?.item?.type === "dynamicToolCall");
2017
+ if (openingMessageIndex < 0 || openingToolIndex < 0 || openingMessageIndex > openingToolIndex) {
2018
+ throw new Error(`Claude opening notice order self-test failed: ${JSON.stringify(openingEvents)}`);
2019
+ }
2020
+ const languageCaptured = [];
2021
+ process.stdout.write = (chunk) => {
2022
+ languageCaptured.push(String(chunk));
2023
+ return true;
2024
+ };
2025
+ try {
2026
+ openingSession.streamBlocks.clear();
2027
+ await processStreamEvent(openingSession, {
2028
+ event: { type: "content_block_start", index: 0, content_block: { type: "text" } },
1798
2029
  });
1799
- processUser(lifecycleSession, {
1800
- message: { content: [{ type: "tool_result", tool_use_id: "toolu_2", content: "resumed" }] },
1801
- tool_use_result: { success: true, resumedAgentId: "agent-1" },
2030
+ await processStreamEvent(openingSession, {
2031
+ event: { type: "content_block_delta", index: 0, delta: { text: "Now the tile view logic." } },
1802
2032
  });
1803
- const resumed = lifecycleSession.subagents.get("toolu_2");
1804
- if (!resumed?.background || resumed.taskId !== "agent-1" || resumed.name !== "Explore") {
1805
- throw new Error(`Claude resumed subagent self-test failed: ${JSON.stringify(resumed)}`);
1806
- }
1807
- finishTurn(lifecycleSession, null, 1);
1808
- processUser(lifecycleSession, {
1809
- origin: { kind: "task-notification" },
1810
- message: { content: `<task-notification>
1811
- <task-id>agent-1</task-id><tool-use-id>toolu_2</tool-use-id>
1812
- <status>failed</status><summary>Agent failed</summary>
1813
- </task-notification>` },
2033
+ await processStreamEvent(openingSession, {
2034
+ event: { type: "content_block_stop", index: 0 },
1814
2035
  });
1815
- if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
1816
- throw new Error("Claude failed task notification did not finish the resumed agent");
1817
- }
1818
- finishTurn(lifecycleSession, null, 1);
1819
2036
  } finally {
1820
2037
  process.stdout.write = stdoutWrite;
1821
2038
  }
1822
- const lifecycleEvents = captured
2039
+ const languageEvents = languageCaptured
1823
2040
  .join("")
1824
2041
  .trim()
1825
2042
  .split("\n")
1826
2043
  .filter(Boolean)
1827
2044
  .map((line) => JSON.parse(line));
1828
- const lifecycleMethods = lifecycleEvents.map((event) => event.method);
1829
- if (!lifecycleMethods.includes("turn/subagents/updated")
1830
- || lifecycleMethods.filter((method) => method === "turn/started").length < 3
1831
- || !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
1832
- && event.params?.line?.kind === "error")) {
1833
- throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
2045
+ const visibleLanguage = languageEvents
2046
+ .filter((event) => event.method === "item/agentMessage/delta")
2047
+ .map((event) => event.params?.delta || "")
2048
+ .join("");
2049
+ if (visibleLanguage !== "다음 부분을 이어서 확인하겠습니다."
2050
+ || languageEvents.some((event) => JSON.stringify(event).includes("Now the tile view logic."))) {
2051
+ throw new Error(`Claude Korean progress normalization self-test failed: ${JSON.stringify(languageEvents)}`);
1834
2052
  }
1835
2053
  const smoothText = "Claude가 👨‍👩‍👧‍👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
1836
- const emitted = [];
1837
- const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
1838
- smooth.push(smoothText);
1839
- await smooth.finish();
1840
- if (emitted.length < 2 || emitted.join("") !== smoothText) {
1841
- throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
1842
- }
1843
- const flushed = [];
1844
- const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
1845
- interrupted.push(smoothText);
1846
- interrupted.flush();
1847
- await interrupted.finish();
1848
- if (flushed.join("") !== smoothText) {
1849
- throw new Error(`Claude smooth stream flush self-test failed: ${JSON.stringify(flushed)}`);
1850
- }
1851
- process.stdout.write("Claude bridge self-test passed\n");
1852
- }
1853
-
1854
- if (process.argv.includes("--self-test")) {
1855
- await runSelfTest();
1856
- process.exit(0);
1857
- }
2054
+ const emitted = [];
2055
+ const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
2056
+ smooth.push(smoothText);
2057
+ await smooth.finish();
2058
+ if (emitted.length < 2 || emitted.join("") !== smoothText) {
2059
+ throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
2060
+ }
2061
+ const flushed = [];
2062
+ const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
2063
+ interrupted.push(smoothText);
2064
+ interrupted.flush();
2065
+ await interrupted.finish();
2066
+ if (flushed.join("") !== smoothText) {
2067
+ throw new Error(`Claude smooth stream flush self-test failed: ${JSON.stringify(flushed)}`);
2068
+ }
2069
+ process.stdout.write("Claude bridge self-test passed\n");
2070
+ }
2071
+
2072
+ if (process.argv.includes("--self-test")) {
2073
+ await runSelfTest();
2074
+ process.exit(0);
2075
+ }
1858
2076
 
1859
2077
  const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
1860
2078
  lines.on("line", async (line) => {