devez-vibe 1.2.6 β†’ 1.2.7

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;
@@ -497,87 +497,87 @@ function emitItem(session, phase, item) {
497
497
  notify(`item/${phase}`, { threadId: session.id, turnId: session.turn?.id, item });
498
498
  }
499
499
 
500
- function emitDelta(session, method, itemId, delta) {
501
- notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
502
- }
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);
517
- }
518
-
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
- };
531
- }
532
-
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();
575
- }
576
- }
577
-
578
- function flushSmoothStreams(session) {
579
- for (const block of session.streamBlocks.values()) block.smooth?.flush();
580
- }
500
+ function emitDelta(session, method, itemId, delta) {
501
+ notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
502
+ }
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);
517
+ }
518
+
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
+ };
531
+ }
532
+
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();
575
+ }
576
+ }
577
+
578
+ function flushSmoothStreams(session) {
579
+ for (const block of session.streamBlocks.values()) block.smooth?.flush();
580
+ }
581
581
 
582
582
  function tokenBreakdown(usage) {
583
583
  if (!usage) return null;
@@ -625,44 +625,44 @@ function historyTokenUsage(messages, models, model) {
625
625
  };
626
626
  }
627
627
 
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
- }
635
- 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);
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
+ }
635
+ 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);
639
639
  const item = block.type === "text"
640
640
  ? { id, type: "agentMessage", text: "", provider: "Claude" }
641
641
  : { 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
- }
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
649
  if (event.type === "content_block_delta") {
650
650
  const current = session.streamBlocks.get(event.index);
651
651
  if (!current) return;
652
- const delta = event.delta?.text || event.delta?.thinking || "";
653
- if (!delta) return;
654
- current.text += delta;
655
- if (current.smooth) current.smooth.push(delta);
656
- else emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
657
- return;
658
- }
659
- if (event.type === "content_block_stop") {
660
- const current = session.streamBlocks.get(event.index);
661
- if (!current) return;
662
- 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] };
652
+ const delta = event.delta?.text || event.delta?.thinking || "";
653
+ if (!delta) return;
654
+ current.text += delta;
655
+ if (current.smooth) current.smooth.push(delta);
656
+ else emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
657
+ return;
658
+ }
659
+ if (event.type === "content_block_stop") {
660
+ const current = session.streamBlocks.get(event.index);
661
+ if (!current) return;
662
+ 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] };
666
666
  emitItem(session, "completed", item);
667
667
  session.streamBlocks.delete(event.index);
668
668
  }
@@ -751,52 +751,89 @@ function fileChanges(name, input) {
751
751
  return [{ path, kind: { type: name === "Write" ? "add" : "update" }, diff: `@@ -0,0 +1 @@\n${additions}` }];
752
752
  }
753
753
 
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) {
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, onIntermediate) {
766
766
  const task = tasks.get(String(input.taskId));
767
767
  if (!task) return false;
768
768
  if (input.subject) task.subject = input.subject;
769
- if (input.status) task.status = input.status;
770
- task.turnId = turnId;
771
- return true;
772
- }
769
+ const status = input.status;
770
+ if (status === "in_progress" || status === "completed") {
771
+ const entries = [...tasks.values()];
772
+ const targetIndex = entries.indexOf(task);
773
+
774
+ // Claude occasionally closes a later pending task at the end of a turn
775
+ // without ever starting it. Keep the visible plan truthful and sequential:
776
+ // every skipped predecessor and the target itself pass through in_progress.
777
+ for (let index = 0; index < targetIndex; index++) {
778
+ const previous = entries[index];
779
+ if (previous.status === "completed") continue;
780
+ if (previous.status !== "in_progress") {
781
+ previous.status = "in_progress";
782
+ previous.turnId = turnId;
783
+ onIntermediate?.();
784
+ }
785
+ previous.status = "completed";
786
+ previous.turnId = turnId;
787
+ onIntermediate?.();
788
+ }
789
+
790
+ for (let index = 0; index < entries.length; index++) {
791
+ const other = entries[index];
792
+ if (other === task || other.status !== "in_progress") continue;
793
+ other.status = index < targetIndex ? "completed" : "pending";
794
+ other.turnId = turnId;
795
+ onIntermediate?.();
796
+ }
773
797
 
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;
798
+ if (status === "completed" && task.status !== "in_progress" && task.status !== "completed") {
799
+ task.status = "in_progress";
800
+ task.turnId = turnId;
801
+ onIntermediate?.();
802
+ }
803
+ task.status = status;
804
+ } else if (status) {
805
+ task.status = status;
781
806
  }
782
- return new Map(entries.slice(start));
807
+ task.turnId = turnId;
808
+ return true;
783
809
  }
784
810
 
811
+ // TaskListμ—λŠ” μ˜ˆμ „ κ³„νšκΉŒμ§€ ν•¨κ»˜ λ“€μ–΄μ˜¬ 수 μžˆλ‹€. λ§ˆμ§€λ§‰μœΌλ‘œ λ²ˆν˜Έκ°€ 1λΆ€ν„°
812
+ // μ‹œμž‘ν•œ 묢음만 ν˜„μž¬ κ³„νšμœΌλ‘œ μ‚Όλ˜, λ²ˆν˜Έκ°€ μ—†λŠ” λͺ©λ‘μ€ 손싀 없이 κ·ΈλŒ€λ‘œ λ‘”λ‹€.
813
+ function latestTaskPlan(tasks) {
814
+ const entries = [...tasks.entries()];
815
+ let start = 0;
816
+ for (let index = 0; index < entries.length; index++) {
817
+ if (numberedTaskIndex(entries[index][1].subject) === 1) start = index;
818
+ }
819
+ return new Map(entries.slice(start));
820
+ }
821
+
785
822
  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 || "μž‘μ—…",
823
+ const turnId = session.turn?.id;
824
+ if (name === "TaskCreate") {
825
+ prepareTaskPlanForCreate(session.tasks, input.subject);
826
+ session.tasks.set(`pending:${toolUseId}`, {
827
+ id: `pending:${toolUseId}`,
828
+ subject: input.subject || input.description || "μž‘μ—…",
792
829
  status: "pending",
793
- turnId,
794
- });
830
+ turnId,
831
+ });
795
832
  } else if (name === "TaskUpdate") {
796
- applyTaskUpdate(session.tasks, input, turnId);
833
+ applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
797
834
  }
798
- emitPlan(session);
799
- }
835
+ emitPlan(session);
836
+ }
800
837
 
801
838
  function updatePlanFromToolResult(session, pending, message) {
802
839
  const value = message.tool_use_result;
@@ -824,10 +861,10 @@ function updatePlanFromToolResult(session, pending, message) {
824
861
  turnId: previous.get(id)?.turnId ?? turnId,
825
862
  });
826
863
  }
827
- session.tasks = latestTaskPlan(session.tasks);
828
- emitPlan(session);
829
- }
830
- }
864
+ session.tasks = latestTaskPlan(session.tasks);
865
+ emitPlan(session);
866
+ }
867
+ }
831
868
 
832
869
  function taskCreatedResult(structured, content) {
833
870
  const created = structured?.task || structured;
@@ -868,78 +905,78 @@ function numberedTaskSubject(subject, index) {
868
905
  // κ·Έ ID둜 λ¬Άμ–΄ 두면 μ§€κΈˆ μ–΄λ–€ μ—μ΄μ „νŠΈκ°€ 무슨 도ꡬλ₯Ό λŒλ¦¬λŠ”μ§€ κ·ΈλŒ€λ‘œ 볡원할 수 μžˆλ‹€.
869
906
  const SUBAGENT_TOOLS = ["Agent", "Task"];
870
907
 
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),
908
+ function startSubagent(session, block) {
909
+ const input = block.input || {};
910
+ session.subagents.set(block.id, {
911
+ id: block.id,
912
+ taskId: "",
913
+ background: false,
914
+ name: firstLine(input.subagent_type || input.agentType || "agent", 40),
878
915
  description: firstLine(input.description || input.prompt || "", 120),
879
916
  tool: "",
880
917
  startedAt: Date.now(),
881
918
  });
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
- }
919
+ emitSubagents(session);
920
+ }
921
+
922
+ function isBackgroundSubagentResult(result) {
923
+ return result?.isAsync === true || result?.status === "async_launched";
924
+ }
925
+
926
+ // Claude Code treats an async Agent result as a launch receipt. The agent remains
927
+ // live until its later task-notification names the same task or tool-use id.
928
+ function keepBackgroundSubagent(session, toolUseId, result) {
929
+ if (!isBackgroundSubagentResult(result)) return false;
930
+ const running = session.subagents.get(toolUseId);
931
+ if (!running) return false;
932
+ running.background = true;
933
+ running.taskId = firstLine(result?.agentId || result?.taskId || "", 80);
934
+ if (running.taskId) {
935
+ session.knownSubagents.set(running.taskId, {
936
+ name: running.name,
937
+ description: running.description,
938
+ });
939
+ }
940
+ return true;
941
+ }
942
+
943
+ // SendMessage can wake a completed agent from its transcript. Its new parent
944
+ // tool-use id owns this run, while the stable agent id lets later notifications
945
+ // and further resumes recover the original label.
946
+ function resumeBackgroundSubagent(session, toolUseId, pending, result) {
947
+ const taskId = firstLine(result?.resumedAgentId || "", 80);
948
+ if (!taskId) return false;
949
+ const existing = [...session.subagents.values()].find((agent) => agent.taskId === taskId);
950
+ if (existing) return true;
951
+ const known = session.knownSubagents.get(taskId);
952
+ const input = pending?.input || {};
953
+ const running = {
954
+ id: toolUseId,
955
+ taskId,
956
+ background: true,
957
+ name: known?.name || "agent",
958
+ description: known?.description || firstLine(input.summary || input.message || "", 120),
959
+ tool: "",
960
+ startedAt: Date.now(),
961
+ };
962
+ session.subagents.set(toolUseId, running);
963
+ session.knownSubagents.set(taskId, {
964
+ name: running.name,
965
+ description: running.description,
966
+ });
967
+ emitSubagents(session);
968
+ return true;
969
+ }
970
+
971
+ function findSubagent(session, id) {
972
+ return session.subagents.get(id)
973
+ || [...session.subagents.values()].find((agent) => agent.taskId === id);
974
+ }
938
975
 
939
976
  // μ„œλΈŒμ—μ΄μ „νŠΈκ°€ μ‹€μ œλ‘œ 무엇을 ν–ˆλŠ”μ§€λŠ” μžμ‹ λ©”μ‹œμ§€μ—λ§Œ λ‚¨λŠ”λ‹€. μ—΄λžŒμš© 기둝은 μ—¬κΈ°μ„œ
940
977
  // ν•œ 쀄씩 ν˜λ €λ³΄λ‚΄κ³ , λͺ©λ‘ 행에 μ“Έ ν˜„μž¬ λ„κ΅¬λ§Œ λ”°λ‘œ κ°±μ‹ ν•œλ‹€.
941
- function recordSubagentMessage(session, message) {
942
- const running = findSubagent(session, message.parent_tool_use_id);
978
+ function recordSubagentMessage(session, message) {
979
+ const running = findSubagent(session, message.parent_tool_use_id);
943
980
  if (!running) return;
944
981
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
945
982
  let toolChanged = false;
@@ -960,8 +997,8 @@ function recordSubagentMessage(session, message) {
960
997
  if (toolChanged) emitSubagents(session);
961
998
  }
962
999
 
963
- function recordSubagentResult(session, message) {
964
- const running = findSubagent(session, message.parent_tool_use_id);
1000
+ function recordSubagentResult(session, message) {
1001
+ const running = findSubagent(session, message.parent_tool_use_id);
965
1002
  if (!running) return;
966
1003
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
967
1004
  for (const block of content) {
@@ -997,32 +1034,32 @@ function subagentToolLabel(block) {
997
1034
  return text ? `${name}(${text})` : name;
998
1035
  }
999
1036
 
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;
1037
+ function finishSubagent(session, toolUseId) {
1038
+ if (!session.subagents.delete(toolUseId)) return;
1039
+ emitSubagents(session);
1040
+ }
1041
+
1042
+ function finishSubagentTask(session, taskId) {
1043
+ if (!taskId) return false;
1044
+ const entry = [...session.subagents.entries()].find(([, agent]) => agent.taskId === taskId);
1045
+ if (!entry) return false;
1046
+ session.subagents.delete(entry[0]);
1047
+ emitSubagents(session);
1048
+ return true;
1049
+ }
1050
+
1051
+ function clearForegroundSubagents(session) {
1052
+ let changed = false;
1053
+ for (const [id, agent] of session.subagents) {
1054
+ if (agent.background) continue;
1055
+ session.subagents.delete(id);
1056
+ changed = true;
1057
+ }
1058
+ if (changed) emitSubagents(session);
1059
+ }
1060
+
1061
+ function clearSubagents(session) {
1062
+ if (!session.subagents.size) return;
1026
1063
  session.subagents.clear();
1027
1064
  emitSubagents(session);
1028
1065
  }
@@ -1042,87 +1079,87 @@ function emitSubagents(session) {
1042
1079
  tool: agent.tool,
1043
1080
  elapsedMs: Date.now() - agent.startedAt,
1044
1081
  })),
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) {
1082
+ });
1083
+ }
1084
+
1085
+ function messageTextParts(message) {
1086
+ const content = message.message?.content;
1087
+ if (typeof content === "string") return [content];
1088
+ if (!Array.isArray(content)) return [];
1089
+ return content
1090
+ .filter((block) => block?.type === "text" && typeof block.text === "string")
1091
+ .map((block) => block.text);
1092
+ }
1093
+
1094
+ function notificationTag(body, name) {
1095
+ const match = body.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
1096
+ return match?.[1]?.trim() || "";
1097
+ }
1098
+
1099
+ function taskNotifications(message) {
1100
+ if (message.origin?.kind !== "task-notification"
1101
+ && !messageTextParts(message).some((text) => text.includes("<task-notification>"))) {
1102
+ return [];
1103
+ }
1104
+ const notifications = [];
1105
+ for (const text of messageTextParts(message)) {
1106
+ for (const match of text.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)) {
1107
+ const body = match[1];
1108
+ notifications.push({
1109
+ taskId: notificationTag(body, "task-id"),
1110
+ toolUseId: notificationTag(body, "tool-use-id"),
1111
+ status: notificationTag(body, "status"),
1112
+ summary: notificationTag(body, "summary"),
1113
+ });
1114
+ }
1115
+ }
1116
+ return notifications;
1117
+ }
1118
+
1119
+ function finishNotifiedSubagents(session, notifications) {
1120
+ for (const notification of notifications) {
1121
+ const byToolUse = notification.toolUseId
1122
+ ? session.subagents.get(notification.toolUseId)
1123
+ : null;
1124
+ const running = byToolUse || (notification.taskId
1125
+ ? [...session.subagents.values()].find((agent) => agent.taskId === notification.taskId)
1126
+ : null);
1127
+ if (!running) continue;
1128
+ emitSubagentLine(session, running.id, {
1129
+ kind: notification.status === "completed" ? "result" : "error",
1130
+ text: notification.summary || notification.status || "μ™„λ£Œλ¨",
1131
+ });
1132
+ session.subagents.delete(running.id);
1133
+ emitSubagents(session);
1134
+ }
1135
+ }
1136
+
1137
+ function processUser(session, message) {
1101
1138
  // μžμ‹ tool_result의 tool_use_idλŠ” λΆ€λͺ¨ μ„Έμ…˜μ˜ 것과 λ‹€λ₯Έ κ³΅κ°„μ΄λ―€λ‘œ, λΆ€λͺ¨ 흐름에
1102
1139
  // μ„žμ΄κΈ° 전에 μ„œλΈŒμ—μ΄μ „νŠΈ 기둝으둜 보낸닀.
1103
1140
  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;
1141
+ recordSubagentResult(session, message);
1142
+ return;
1143
+ }
1144
+ const notifications = taskNotifications(message);
1145
+ if (notifications.length) {
1146
+ if (!session.turn) beginTurn(session);
1147
+ finishNotifiedSubagents(session, notifications);
1148
+ return;
1149
+ }
1150
+ const content = Array.isArray(message.message?.content) ? message.message.content : [];
1151
+ for (const block of content) {
1152
+ if (block.type !== "tool_result") continue;
1153
+ const pending = session.tools.get(block.tool_use_id);
1154
+ const staysInBackground = SUBAGENT_TOOLS.includes(pending?.name)
1155
+ && keepBackgroundSubagent(session, block.tool_use_id, message.tool_use_result);
1156
+ if (!staysInBackground) finishSubagent(session, block.tool_use_id);
1157
+ if (pending?.name === "SendMessage") {
1158
+ resumeBackgroundSubagent(session, block.tool_use_id, pending, message.tool_use_result);
1159
+ } else if (pending?.name === "TaskStop" && message.tool_use_result?.success !== false) {
1160
+ finishSubagentTask(session, firstLine(pending.input?.task_id || "", 80));
1161
+ }
1162
+ if (!pending) continue;
1126
1163
  pending.toolUseId = block.tool_use_id;
1127
1164
  if (pending.suppressed) {
1128
1165
  updatePlanFromToolResult(session, pending, message);
@@ -1209,10 +1246,10 @@ async function runPendingPrompt(session) {
1209
1246
  }
1210
1247
  }
1211
1248
 
1212
- function finishTurn(session, error, durationMs) {
1213
- if (!session.turn) return;
1214
- flushSmoothStreams(session);
1215
- clearForegroundSubagents(session);
1249
+ function finishTurn(session, error, durationMs) {
1250
+ if (!session.turn) return;
1251
+ flushSmoothStreams(session);
1252
+ clearForegroundSubagents(session);
1216
1253
  const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
1217
1254
  if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
1218
1255
  if (durationMs != null) turn.durationMs = durationMs;
@@ -1228,7 +1265,7 @@ async function consume(session) {
1228
1265
  if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
1229
1266
  if (session.turn) session.turn.sawStreamText = true;
1230
1267
  }
1231
- await processStreamEvent(session, message);
1268
+ await processStreamEvent(session, message);
1232
1269
  } else if (message.type === "assistant") processAssistant(session, message);
1233
1270
  else if (message.type === "user") processUser(session, message);
1234
1271
  else if (message.type === "result") await processResult(session, message);
@@ -1298,7 +1335,7 @@ async function startPrompt(params) {
1298
1335
  return runPrompt(session, params);
1299
1336
  }
1300
1337
 
1301
- async function runPrompt(session, params) {
1338
+ async function runPrompt(session, params) {
1302
1339
  const id = session.id;
1303
1340
  if (params.model) {
1304
1341
  const model = stripClaudeModel(params.model);
@@ -1311,7 +1348,7 @@ async function runPrompt(session, params) {
1311
1348
  }
1312
1349
  session.effort = effort;
1313
1350
  await applyPermissionMode(session, params.permissionMode);
1314
- const turnId = beginTurn(session);
1351
+ const turnId = beginTurn(session);
1315
1352
  session.queue.push({
1316
1353
  type: "user",
1317
1354
  message: { role: "user", content: await inputContent(params.input, params.handoffContext) },
@@ -1319,18 +1356,18 @@ async function runPrompt(session, params) {
1319
1356
  session_id: id,
1320
1357
  origin: { kind: "human" },
1321
1358
  });
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) {
1328
- 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
- }
1359
+ return { turn: { id: turnId } };
1360
+ }
1361
+
1362
+ // A background task notification is an internal user message that starts its
1363
+ // own Claude response even though the host did not submit a new prompt.
1364
+ function beginTurn(session) {
1365
+ const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
1366
+ session.turn = { id: turnId, sawStreamText: false };
1367
+ session.lastContextUsage = null;
1368
+ notify("turn/started", { threadId: session.id, turn: { id: turnId } });
1369
+ return turnId;
1370
+ }
1334
1371
 
1335
1372
  function contentBlocks(message) {
1336
1373
  const content = message?.content;
@@ -1355,7 +1392,7 @@ function isInternalHistoryText(message, text) {
1355
1392
  ].includes(tag);
1356
1393
  }
1357
1394
 
1358
- function historyState(messages) {
1395
+ function historyState(messages) {
1359
1396
  const turns = [];
1360
1397
  let turn = null;
1361
1398
  const tools = new Map();
@@ -1391,14 +1428,14 @@ function historyState(messages) {
1391
1428
  if (block.type === "text") turn.items.push({ id: `${message.uuid}-text`, type: "agentMessage", text: block.text || "", provider: "Claude" });
1392
1429
  else if (block.type === "thinking") turn.items.push({ id: `${message.uuid}-thinking`, type: "reasoning", summary: [block.thinking || ""] });
1393
1430
  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);
1431
+ const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
1432
+ tools.set(block.id, pending);
1433
+ if (block.name === "TaskCreate") {
1434
+ prepareTaskPlanForCreate(tasks, block.input?.subject);
1435
+ tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "μž‘μ—…", status: "pending", turnId: turn.id });
1436
+ } else if (block.name === "TaskUpdate") {
1437
+ applyTaskUpdate(tasks, block.input || {}, turn.id);
1438
+ } else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
1402
1439
  }
1403
1440
  }
1404
1441
  } else if (message.type === "user") {
@@ -1415,12 +1452,12 @@ function historyState(messages) {
1415
1452
  tasks.set(temporary.id, temporary);
1416
1453
  }
1417
1454
  } 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);
1455
+ const known = new Map(tasks);
1456
+ tasks.clear();
1457
+ 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 });
1458
+ const current = latestTaskPlan(tasks);
1459
+ tasks.clear();
1460
+ for (const [id, task] of current) tasks.set(id, task);
1424
1461
  } else if (pending.item) {
1425
1462
  const output = toolOutput(block.content, message.tool_use_result);
1426
1463
  Object.assign(pending.item, pending.item.type === "commandExecution"
@@ -1436,17 +1473,17 @@ function historyState(messages) {
1436
1473
  const text = [...tasks.values()].map((task, index) => `${task.status === "completed" ? "βœ“" : task.status === "in_progress" ? "β–Έ" : "β–‘"} ${numberedTaskSubject(task.subject, index)}`).join("\n");
1437
1474
  turn.items.push({ id: "claude-plan-latest", type: "plan", text });
1438
1475
  }
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
- }
1476
+ return {
1477
+ tasks,
1478
+ turns: turns
1479
+ .filter((candidate) => !candidate.synthetic)
1480
+ .map(({ synthetic: _, ...candidate }) => candidate),
1481
+ };
1482
+ }
1483
+
1484
+ function historyTurns(messages) {
1485
+ return historyState(messages).turns;
1486
+ }
1450
1487
 
1451
1488
  async function dispatch(method, params = {}) {
1452
1489
  if (method === "model/list") return loadModelCatalog(params);
@@ -1471,11 +1508,11 @@ async function dispatch(method, params = {}) {
1471
1508
  }
1472
1509
  if (method === "session/resume") {
1473
1510
  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 {
1511
+ const existing = sessions.get(id);
1512
+ if (existing) {
1513
+ const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
1514
+ if (!existing.tasks.size) existing.tasks = historyState(messages).tasks;
1515
+ return {
1479
1516
  id,
1480
1517
  thread: { id, turns: [] },
1481
1518
  initialTurnsPage: { data: [], nextCursor: null },
@@ -1499,11 +1536,11 @@ async function dispatch(method, params = {}) {
1499
1536
  model: params.model || lastModel || params.fallbackModel,
1500
1537
  effort: params.effort || params.fallbackEffort,
1501
1538
  }, 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;
1539
+ const tokenUsage = historyTokenUsage(messages, session.models, session.model);
1540
+ // Seed the live session so the next turn keeps reporting a full context.
1541
+ session.lastContextUsage = tokenUsage?.last || null;
1542
+ session.lastContextWindow = tokenUsage?.modelContextWindow || 0;
1543
+ session.tasks = historyState(messages).tasks;
1507
1544
  return {
1508
1545
  id,
1509
1546
  thread: { id, turns: [] },
@@ -1605,7 +1642,7 @@ async function dispatch(method, params = {}) {
1605
1642
  throw new Error(`μ§€μ›ν•˜μ§€ μ•ŠλŠ” Claude λΈŒλ¦¬μ§€ λ©”μ„œλ“œ: ${method}`);
1606
1643
  }
1607
1644
 
1608
- async function runSelfTest() {
1645
+ async function runSelfTest() {
1609
1646
  const user = (uuid, text) => ({
1610
1647
  type: "user",
1611
1648
  uuid,
@@ -1636,50 +1673,50 @@ async function runSelfTest() {
1636
1673
  ["say hello", "claude:claude-sonnet-5"],
1637
1674
  ["hay zzz", "claude:claude-haiku-4-5-20251001"],
1638
1675
  ];
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");
1676
+ if (turns.length !== expected.length
1677
+ || prompts.some((prompt, index) => prompt?.content?.[0]?.text !== expected[index][0]
1678
+ || prompt.model !== expected[index][1])) {
1679
+ throw new Error(`Claude history self-test failed: ${JSON.stringify(turns)}`);
1680
+ }
1681
+ const taskUse = (uuid, id, name, input) => ({
1682
+ type: "assistant",
1683
+ uuid,
1684
+ message: {
1685
+ role: "assistant",
1686
+ model: "claude-opus-5",
1687
+ content: [{ type: "tool_use", id, name, input }],
1688
+ },
1689
+ });
1690
+ const taskResult = (uuid, id, toolUseResult, content) => ({
1691
+ type: "user",
1692
+ uuid,
1693
+ message: { role: "user", content: [{ type: "tool_result", tool_use_id: id, content }] },
1694
+ tool_use_result: toolUseResult,
1695
+ });
1696
+ const taskMessages = [user("plan", "μž‘μ—…μ„ μ§„ν–‰ν•΄")];
1697
+ for (let index = 1; index <= 6; index++) {
1698
+ const id = String(24 + index);
1699
+ const toolId = `create-${id}`;
1700
+ const subject = `${index}. μž‘μ—… ${index}`;
1701
+ taskMessages.push(
1702
+ taskUse(`create-use-${id}`, toolId, "TaskCreate", { subject }),
1703
+ taskResult(`create-result-${id}`, toolId, { task: { id, subject } }, `Task #${id} created successfully: ${subject}`),
1704
+ );
1705
+ }
1706
+ taskMessages.push(
1707
+ taskUse("update-25-start", "update-25-start", "TaskUpdate", { taskId: "25", status: "in_progress" }),
1708
+ taskUse("update-25-done", "update-25-done", "TaskUpdate", { taskId: "25", status: "completed" }),
1709
+ taskUse("update-26-start", "update-26-start", "TaskUpdate", { taskId: "26", status: "in_progress" }),
1710
+ );
1711
+ const restoredTasks = historyState(taskMessages).tasks;
1712
+ if ([...restoredTasks.keys()].join(",") !== "25,26,27,28,29,30"
1713
+ || restoredTasks.get("25")?.status !== "completed"
1714
+ || restoredTasks.get("26")?.status !== "in_progress"
1715
+ || [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
1716
+ throw new Error(`Claude task resume self-test failed: ${JSON.stringify([...restoredTasks])}`);
1717
+ }
1718
+ applyTaskUpdate(restoredTasks, { taskId: "26", status: "completed" }, "resumed-turn");
1719
+ applyTaskUpdate(restoredTasks, { taskId: "27", status: "in_progress" }, "resumed-turn");
1683
1720
  if (restoredTasks.size !== 6
1684
1721
  || restoredTasks.get("25")?.status !== "completed"
1685
1722
  || restoredTasks.get("26")?.status !== "completed"
@@ -1687,19 +1724,38 @@ async function runSelfTest() {
1687
1724
  || [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
1688
1725
  throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
1689
1726
  }
1690
- const mixedPlans = new Map([
1691
- ["old-1", { subject: "1. 이전 μž‘μ—…", status: "completed" }],
1692
- ["old-2", { subject: "2. 이전 검증", status: "completed" }],
1693
- ...[...restoredTasks],
1727
+ const skippedTasks = new Map([
1728
+ ["1", { id: "1", subject: "1. 쑰사", status: "pending" }],
1729
+ ["2", { id: "2", subject: "2. 뢄석", status: "pending" }],
1730
+ ["3", { id: "3", subject: "3. 검증", status: "pending" }],
1694
1731
  ]);
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])}`);
1732
+ const transitions = [];
1733
+ const snapshot = () => transitions.push([...skippedTasks.values()].map((task) => task.status).join(","));
1734
+ applyTaskUpdate(skippedTasks, { taskId: "3", status: "completed" }, "turn", snapshot);
1735
+ snapshot();
1736
+ if (transitions.join("|") !== [
1737
+ "in_progress,pending,pending",
1738
+ "completed,pending,pending",
1739
+ "completed,in_progress,pending",
1740
+ "completed,completed,pending",
1741
+ "completed,completed,in_progress",
1742
+ "completed,completed,completed",
1743
+ ].join("|")) {
1744
+ throw new Error(`Claude skipped task transition self-test failed: ${transitions.join("|")}`);
1697
1745
  }
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({
1746
+ const mixedPlans = new Map([
1747
+ ["old-1", { subject: "1. 이전 μž‘μ—…", status: "completed" }],
1748
+ ["old-2", { subject: "2. 이전 검증", status: "completed" }],
1749
+ ...[...restoredTasks],
1750
+ ]);
1751
+ if ([...latestTaskPlan(mixedPlans).keys()].join(",") !== "25,26,27,28,29,30") {
1752
+ throw new Error(`Claude latest task plan self-test failed: ${JSON.stringify([...mixedPlans])}`);
1753
+ }
1754
+ prepareTaskPlanForCreate(restoredTasks, "7. μΆ”κ°€ μž‘μ—…");
1755
+ if (restoredTasks.size !== 6) throw new Error("Claude appended task unexpectedly reset the plan");
1756
+ prepareTaskPlanForCreate(restoredTasks, "1. μƒˆ μž‘μ—…");
1757
+ if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
1758
+ const usage = tokenBreakdown({
1703
1759
  input_tokens: 2,
1704
1760
  cache_read_input_tokens: 68_000,
1705
1761
  cache_creation_input_tokens: 500,
@@ -1713,148 +1769,148 @@ async function runSelfTest() {
1713
1769
  catalogEntry({ value: "sonnet", resolvedModel: "claude-sonnet-5" }, "").contextWindow,
1714
1770
  catalogEntry({ value: "haiku", resolvedModel: "x", contextWindow: 300_000 }, "").contextWindow,
1715
1771
  ];
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)}`);
1737
- }
1738
- const lifecycleSession = {
1739
- id: "self-test-session",
1740
- turn: { id: "parent-turn", sawStreamText: false },
1741
- turnSequence: 1,
1742
- 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
- ]]),
1763
- knownSubagents: new Map(),
1764
- lastContextUsage: null,
1765
- };
1766
- const captured = [];
1767
- const stdoutWrite = process.stdout.write;
1768
- process.stdout.write = (chunk) => {
1769
- captured.push(String(chunk));
1770
- return true;
1771
- };
1772
- 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>` },
1787
- });
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: {} },
1798
- });
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" },
1802
- });
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>` },
1814
- });
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
- } finally {
1820
- process.stdout.write = stdoutWrite;
1821
- }
1822
- const lifecycleEvents = captured
1823
- .join("")
1824
- .trim()
1825
- .split("\n")
1826
- .filter(Boolean)
1827
- .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}`);
1834
- }
1835
- 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
- }
1772
+ if (windows.join(",") !== "1000000,200000,300000") {
1773
+ throw new Error(`Claude context window self-test failed: ${windows.join(",")}`);
1774
+ }
1775
+ const notification = taskNotifications({
1776
+ origin: { kind: "task-notification" },
1777
+ message: {
1778
+ content: `<task-notification>
1779
+ <task-id>agent-1</task-id>
1780
+ <tool-use-id>toolu_1</tool-use-id>
1781
+ <status>completed</status>
1782
+ <summary>Agent "Explore" finished</summary>
1783
+ <result>done</result>
1784
+ </task-notification>`,
1785
+ },
1786
+ });
1787
+ if (notification.length !== 1
1788
+ || notification[0].taskId !== "agent-1"
1789
+ || notification[0].toolUseId !== "toolu_1"
1790
+ || notification[0].status !== "completed"
1791
+ || notification[0].summary !== 'Agent "Explore" finished') {
1792
+ throw new Error(`Claude task notification self-test failed: ${JSON.stringify(notification)}`);
1793
+ }
1794
+ const lifecycleSession = {
1795
+ id: "self-test-session",
1796
+ turn: { id: "parent-turn", sawStreamText: false },
1797
+ turnSequence: 1,
1798
+ streamBlocks: new Map(),
1799
+ tools: new Map([[
1800
+ "toolu_1",
1801
+ {
1802
+ name: "Agent",
1803
+ input: { subagent_type: "Explore", description: "Inspect files" },
1804
+ item: { id: "toolu_1", type: "collabAgentToolCall", tool: { name: "Agent", arguments: {} } },
1805
+ },
1806
+ ]]),
1807
+ subagents: new Map([[
1808
+ "toolu_1",
1809
+ {
1810
+ id: "toolu_1",
1811
+ taskId: "",
1812
+ background: false,
1813
+ name: "Explore",
1814
+ description: "Inspect files",
1815
+ tool: "",
1816
+ startedAt: Date.now(),
1817
+ },
1818
+ ]]),
1819
+ knownSubagents: new Map(),
1820
+ lastContextUsage: null,
1821
+ };
1822
+ const captured = [];
1823
+ const stdoutWrite = process.stdout.write;
1824
+ process.stdout.write = (chunk) => {
1825
+ captured.push(String(chunk));
1826
+ return true;
1827
+ };
1828
+ try {
1829
+ processUser(lifecycleSession, {
1830
+ message: { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "launched" }] },
1831
+ tool_use_result: { isAsync: true, status: "async_launched", agentId: "agent-1" },
1832
+ });
1833
+ finishTurn(lifecycleSession, null, 1);
1834
+ if (!lifecycleSession.subagents.has("toolu_1") || lifecycleSession.turn !== null) {
1835
+ throw new Error("Claude background subagent did not survive its parent turn");
1836
+ }
1837
+ processUser(lifecycleSession, {
1838
+ origin: { kind: "task-notification" },
1839
+ message: { content: notification[0] && `<task-notification>
1840
+ <task-id>agent-1</task-id><tool-use-id>toolu_1</tool-use-id>
1841
+ <status>completed</status><summary>Agent finished</summary>
1842
+ </task-notification>` },
1843
+ });
1844
+ if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
1845
+ throw new Error("Claude task notification did not finish the agent in an automatic turn");
1846
+ }
1847
+ finishTurn(lifecycleSession, null, 1);
1848
+
1849
+ beginTurn(lifecycleSession);
1850
+ lifecycleSession.tools.set("toolu_2", {
1851
+ name: "SendMessage",
1852
+ input: { to: "agent-1", summary: "Continue inspection" },
1853
+ item: { id: "toolu_2", type: "dynamicToolCall", tool: "SendMessage", arguments: {} },
1854
+ });
1855
+ processUser(lifecycleSession, {
1856
+ message: { content: [{ type: "tool_result", tool_use_id: "toolu_2", content: "resumed" }] },
1857
+ tool_use_result: { success: true, resumedAgentId: "agent-1" },
1858
+ });
1859
+ const resumed = lifecycleSession.subagents.get("toolu_2");
1860
+ if (!resumed?.background || resumed.taskId !== "agent-1" || resumed.name !== "Explore") {
1861
+ throw new Error(`Claude resumed subagent self-test failed: ${JSON.stringify(resumed)}`);
1862
+ }
1863
+ finishTurn(lifecycleSession, null, 1);
1864
+ processUser(lifecycleSession, {
1865
+ origin: { kind: "task-notification" },
1866
+ message: { content: `<task-notification>
1867
+ <task-id>agent-1</task-id><tool-use-id>toolu_2</tool-use-id>
1868
+ <status>failed</status><summary>Agent failed</summary>
1869
+ </task-notification>` },
1870
+ });
1871
+ if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
1872
+ throw new Error("Claude failed task notification did not finish the resumed agent");
1873
+ }
1874
+ finishTurn(lifecycleSession, null, 1);
1875
+ } finally {
1876
+ process.stdout.write = stdoutWrite;
1877
+ }
1878
+ const lifecycleEvents = captured
1879
+ .join("")
1880
+ .trim()
1881
+ .split("\n")
1882
+ .filter(Boolean)
1883
+ .map((line) => JSON.parse(line));
1884
+ const lifecycleMethods = lifecycleEvents.map((event) => event.method);
1885
+ if (!lifecycleMethods.includes("turn/subagents/updated")
1886
+ || lifecycleMethods.filter((method) => method === "turn/started").length < 3
1887
+ || !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
1888
+ && event.params?.line?.kind === "error")) {
1889
+ throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
1890
+ }
1891
+ const smoothText = "Claudeκ°€ πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ ν•œ λ¬Έμž₯을 ν•œκΊΌλ²ˆμ— 보내도 λΆ€λ“œλŸ½κ²Œ ν‘œμ‹œν•©λ‹ˆλ‹€.";
1892
+ const emitted = [];
1893
+ const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
1894
+ smooth.push(smoothText);
1895
+ await smooth.finish();
1896
+ if (emitted.length < 2 || emitted.join("") !== smoothText) {
1897
+ throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
1898
+ }
1899
+ const flushed = [];
1900
+ const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
1901
+ interrupted.push(smoothText);
1902
+ interrupted.flush();
1903
+ await interrupted.finish();
1904
+ if (flushed.join("") !== smoothText) {
1905
+ throw new Error(`Claude smooth stream flush self-test failed: ${JSON.stringify(flushed)}`);
1906
+ }
1907
+ process.stdout.write("Claude bridge self-test passed\n");
1908
+ }
1909
+
1910
+ if (process.argv.includes("--self-test")) {
1911
+ await runSelfTest();
1912
+ process.exit(0);
1913
+ }
1858
1914
 
1859
1915
  const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
1860
1916
  lines.on("line", async (line) => {