devez-vibe 1.2.8 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dvz.exe +0 -0
- package/bridge/claude-agent-sdk-bridge.mjs +403 -257
- package/package.json +1 -1
package/bin/dvz.exe
CHANGED
|
Binary file
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { createReadStream } from "node:fs";
|
|
5
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
5
8
|
import { createInterface } from "node:readline";
|
|
6
9
|
import {
|
|
7
10
|
deleteSession,
|
|
@@ -435,9 +438,10 @@ async function createSession(params, resumeId) {
|
|
|
435
438
|
turnSequence: 1,
|
|
436
439
|
itemSequence: 1,
|
|
437
440
|
streamBlocks: new Map(),
|
|
438
|
-
tools: new Map(),
|
|
439
|
-
tasks: new Map(),
|
|
440
|
-
|
|
441
|
+
tools: new Map(),
|
|
442
|
+
tasks: new Map(),
|
|
443
|
+
planCreatePending: false,
|
|
444
|
+
subagents: new Map(),
|
|
441
445
|
knownSubagents: new Map(),
|
|
442
446
|
lastContextUsage: null,
|
|
443
447
|
lastContextWindow: 0,
|
|
@@ -497,53 +501,53 @@ function emitItem(session, phase, item) {
|
|
|
497
501
|
notify(`item/${phase}`, { threadId: session.id, turnId: session.turn?.id, item });
|
|
498
502
|
}
|
|
499
503
|
|
|
500
|
-
function emitDelta(session, method, itemId, delta) {
|
|
501
|
-
notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
|
|
502
|
-
}
|
|
503
|
-
|
|
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);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function openingNotice(input) {
|
|
513
|
-
return isKoreanPrompt(input)
|
|
514
|
-
? "요청 내용을 확인하고 필요한 작업을 진행하겠습니다."
|
|
515
|
-
: "I’ll review the request and proceed with the necessary work.";
|
|
516
|
-
}
|
|
517
|
-
|
|
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 "다음 부분을 이어서 확인하겠습니다.";
|
|
526
|
-
}
|
|
527
|
-
return value;
|
|
528
|
-
}
|
|
529
|
-
|
|
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;
|
|
541
|
-
}
|
|
504
|
+
function emitDelta(session, method, itemId, delta) {
|
|
505
|
+
notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function isKoreanPrompt(input) {
|
|
509
|
+
const prompt = (Array.isArray(input) ? input : [])
|
|
510
|
+
.filter((item) => item?.type === "text")
|
|
511
|
+
.map((item) => String(item.text || ""))
|
|
512
|
+
.join("\n");
|
|
513
|
+
return /[\uac00-\ud7a3]/.test(prompt);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function openingNotice(input) {
|
|
517
|
+
return isKoreanPrompt(input)
|
|
518
|
+
? "요청 내용을 확인하고 필요한 작업을 진행하겠습니다."
|
|
519
|
+
: "I’ll review the request and proceed with the necessary work.";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function normalizeProgressText(turn, text) {
|
|
523
|
+
const value = String(text || "");
|
|
524
|
+
const trimmed = value.trim();
|
|
525
|
+
if (turn?.koreanRequest
|
|
526
|
+
&& trimmed.length <= 160
|
|
527
|
+
&& !/[\uac00-\ud7a3]/.test(trimmed)
|
|
528
|
+
&& /^Now\b[^\r\n]*[.!?]?$/i.test(trimmed)) {
|
|
529
|
+
return "다음 부분을 이어서 확인하겠습니다.";
|
|
530
|
+
}
|
|
531
|
+
return value;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Claude can answer with a tool_use as its first and only content block even
|
|
535
|
+
// when the prompt asks for an opening update. Keep the visible contract stable
|
|
536
|
+
// without duplicating a real model-written update.
|
|
537
|
+
function emitOpeningNotice(session) {
|
|
538
|
+
if (!session.turn || session.turn.openingNoticeEmitted) return;
|
|
539
|
+
const text = session.turn.openingNotice;
|
|
540
|
+
const id = nextItemId(session, "opening");
|
|
541
|
+
const item = { id, type: "agentMessage", text, provider: "Claude" };
|
|
542
|
+
emitItem(session, "started", item);
|
|
543
|
+
emitItem(session, "completed", item);
|
|
544
|
+
session.turn.openingNoticeEmitted = true;
|
|
545
|
+
}
|
|
542
546
|
|
|
543
547
|
// Windows rounds larger timer delays up to the next scheduler slice. Ten
|
|
544
548
|
// milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
|
|
545
549
|
const SMOOTH_TEXT_INTERVAL_MS = 10;
|
|
546
|
-
const SMOOTH_TEXT_TARGET_FRAMES =
|
|
550
|
+
const SMOOTH_TEXT_TARGET_FRAMES = 10;
|
|
547
551
|
const SMOOTH_TEXT_MAX_GRAPHEMES = 24;
|
|
548
552
|
const graphemeSegmenter = typeof Intl.Segmenter === "function"
|
|
549
553
|
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
@@ -578,10 +582,21 @@ class SmoothTextStream {
|
|
|
578
582
|
this.waiters = [];
|
|
579
583
|
}
|
|
580
584
|
|
|
581
|
-
push(text) {
|
|
582
|
-
this.pending += text;
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
+
push(text) {
|
|
586
|
+
this.pending += text;
|
|
587
|
+
this.schedule();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
schedule() {
|
|
591
|
+
if (this.timer != null) return;
|
|
592
|
+
// Wait one visual frame before the first drain. Claude often sends several
|
|
593
|
+
// tiny deltas back-to-back; batching them removes the uneven one-character
|
|
594
|
+
// jumps while keeping added latency below one frame.
|
|
595
|
+
this.timer = setTimeout(() => {
|
|
596
|
+
this.timer = null;
|
|
597
|
+
this.drain();
|
|
598
|
+
}, this.intervalMs);
|
|
599
|
+
}
|
|
585
600
|
|
|
586
601
|
drain() {
|
|
587
602
|
if (!this.pending) {
|
|
@@ -590,14 +605,13 @@ class SmoothTextStream {
|
|
|
590
605
|
return;
|
|
591
606
|
}
|
|
592
607
|
const { chunk, rest } = takeSmoothTextChunk(this.pending);
|
|
593
|
-
this.pending = rest;
|
|
594
|
-
this.emit(chunk);
|
|
595
|
-
if (this.pending) {
|
|
596
|
-
this.
|
|
597
|
-
} else {
|
|
598
|
-
this.
|
|
599
|
-
|
|
600
|
-
}
|
|
608
|
+
this.pending = rest;
|
|
609
|
+
this.emit(chunk);
|
|
610
|
+
if (this.pending) {
|
|
611
|
+
this.schedule();
|
|
612
|
+
} else {
|
|
613
|
+
for (const resolve of this.waiters.splice(0)) resolve();
|
|
614
|
+
}
|
|
601
615
|
}
|
|
602
616
|
|
|
603
617
|
finish() {
|
|
@@ -671,7 +685,7 @@ async function processStreamEvent(session, message) {
|
|
|
671
685
|
flushSmoothStreams(session);
|
|
672
686
|
session.streamBlocks.clear();
|
|
673
687
|
}
|
|
674
|
-
if (event.type === "content_block_start") {
|
|
688
|
+
if (event.type === "content_block_start") {
|
|
675
689
|
const block = event.content_block || {};
|
|
676
690
|
if (block.type !== "text" && block.type !== "thinking") return;
|
|
677
691
|
const id = nextItemId(session, block.type);
|
|
@@ -681,54 +695,54 @@ async function processStreamEvent(session, message) {
|
|
|
681
695
|
const smooth = block.type === "text"
|
|
682
696
|
? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
|
|
683
697
|
: 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
|
-
});
|
|
698
|
+
session.streamBlocks.set(event.index, {
|
|
699
|
+
id,
|
|
700
|
+
type: block.type,
|
|
701
|
+
text: "",
|
|
702
|
+
smooth,
|
|
703
|
+
languagePending: block.type === "text" && session.turn.koreanRequest ? "" : null,
|
|
704
|
+
holdEnglishProgress: false,
|
|
705
|
+
});
|
|
692
706
|
emitItem(session, "started", item);
|
|
693
707
|
return;
|
|
694
708
|
}
|
|
695
|
-
if (event.type === "content_block_delta") {
|
|
709
|
+
if (event.type === "content_block_delta") {
|
|
696
710
|
const current = session.streamBlocks.get(event.index);
|
|
697
711
|
if (!current) return;
|
|
698
|
-
const delta = event.delta?.text || event.delta?.thinking || "";
|
|
699
|
-
if (!delta) return;
|
|
700
|
-
current.text += 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);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
712
|
+
const delta = event.delta?.text || event.delta?.thinking || "";
|
|
713
|
+
if (!delta) return;
|
|
714
|
+
current.text += delta;
|
|
715
|
+
if (current.type === "text") session.turn.sawVisibleText = true;
|
|
716
|
+
if (!current.smooth) {
|
|
717
|
+
emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (current.languagePending != null) {
|
|
721
|
+
current.languagePending += delta;
|
|
722
|
+
const probe = current.languagePending.trimStart();
|
|
723
|
+
const lower = probe.toLowerCase();
|
|
724
|
+
if (!current.holdEnglishProgress && "now".startsWith(lower)) return;
|
|
725
|
+
if (/^now(?:\s|$)/i.test(probe)) {
|
|
726
|
+
current.holdEnglishProgress = true;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
current.smooth.push(current.languagePending);
|
|
730
|
+
current.languagePending = null;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
current.smooth.push(delta);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
722
736
|
if (event.type === "content_block_stop") {
|
|
723
|
-
const current = session.streamBlocks.get(event.index);
|
|
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
|
-
}
|
|
731
|
-
await current.smooth?.finish();
|
|
737
|
+
const current = session.streamBlocks.get(event.index);
|
|
738
|
+
if (!current) return;
|
|
739
|
+
if (current.languagePending != null) {
|
|
740
|
+
const visible = normalizeProgressText(session.turn, current.languagePending);
|
|
741
|
+
current.text = visible;
|
|
742
|
+
current.smooth?.push(visible);
|
|
743
|
+
current.languagePending = null;
|
|
744
|
+
}
|
|
745
|
+
await current.smooth?.finish();
|
|
732
746
|
const item = current.type === "text"
|
|
733
747
|
? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
|
|
734
748
|
: { id: current.id, type: "reasoning", summary: [current.text] };
|
|
@@ -753,31 +767,31 @@ function processAssistant(session, message) {
|
|
|
753
767
|
message.message?.model,
|
|
754
768
|
session.model,
|
|
755
769
|
);
|
|
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
|
-
}
|
|
770
|
+
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
771
|
+
const hasToolUse = content.some((block) => block.type === "tool_use");
|
|
772
|
+
const hasVisibleText = session.turn.sawVisibleText || content.some(
|
|
773
|
+
(block) => block.type === "text" && String(block.text || "").trim(),
|
|
774
|
+
);
|
|
775
|
+
// Without partial SDK events, replay completed text before tool items so the
|
|
776
|
+
// visible order still matches the assistant content order.
|
|
777
|
+
if (!session.streamBlocks.size && !session.turn.sawStreamText) {
|
|
778
|
+
for (const block of content) {
|
|
779
|
+
if (block.type !== "text" && block.type !== "thinking") continue;
|
|
780
|
+
const id = nextItemId(session, block.type);
|
|
781
|
+
const item = block.type === "text"
|
|
782
|
+
? { id, type: "agentMessage", text: normalizeProgressText(session.turn, block.text), provider: "Claude" }
|
|
783
|
+
: { id, type: "reasoning", summary: [block.thinking || ""] };
|
|
784
|
+
emitItem(session, "started", item);
|
|
785
|
+
emitItem(session, "completed", item);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (hasToolUse && !hasVisibleText) emitOpeningNotice(session);
|
|
789
|
+
for (const block of content) {
|
|
790
|
+
if (block.type === "tool_use") processToolUse(session, block);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
779
793
|
|
|
780
|
-
function processToolUse(session, block) {
|
|
794
|
+
function processToolUse(session, block) {
|
|
781
795
|
const name = block.name || "Tool";
|
|
782
796
|
const input = block.input || {};
|
|
783
797
|
if (name === "TaskCreate" || name === "TaskUpdate" || name === "TaskList") {
|
|
@@ -785,11 +799,13 @@ function processToolUse(session, block) {
|
|
|
785
799
|
session.tools.set(block.id, { name, input, suppressed: true });
|
|
786
800
|
return;
|
|
787
801
|
}
|
|
788
|
-
if (name === "AskUserQuestion") {
|
|
789
|
-
session
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
802
|
+
if (name === "AskUserQuestion") {
|
|
803
|
+
flushPendingPlan(session);
|
|
804
|
+
session.tools.set(block.id, { name, input, suppressed: true });
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
flushPendingPlan(session);
|
|
808
|
+
const item = toolItem(session, block.id, name, input);
|
|
793
809
|
session.tools.set(block.id, { name, input, item });
|
|
794
810
|
emitItem(session, "started", item);
|
|
795
811
|
if (SUBAGENT_TOOLS.includes(name)) startSubagent(session, block);
|
|
@@ -895,21 +911,30 @@ function latestTaskPlan(tasks) {
|
|
|
895
911
|
return new Map(entries.slice(start));
|
|
896
912
|
}
|
|
897
913
|
|
|
898
|
-
function updatePlanFromToolUse(session, name, toolUseId, input) {
|
|
914
|
+
function updatePlanFromToolUse(session, name, toolUseId, input) {
|
|
899
915
|
const turnId = session.turn?.id;
|
|
900
916
|
if (name === "TaskCreate") {
|
|
901
917
|
prepareTaskPlanForCreate(session.tasks, input.subject);
|
|
902
|
-
session.tasks.set(`pending:${toolUseId}`, {
|
|
918
|
+
session.tasks.set(`pending:${toolUseId}`, {
|
|
903
919
|
id: `pending:${toolUseId}`,
|
|
904
920
|
subject: input.subject || input.description || "작업",
|
|
905
921
|
status: "pending",
|
|
906
|
-
turnId,
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
|
|
922
|
+
turnId,
|
|
923
|
+
});
|
|
924
|
+
session.planCreatePending = true;
|
|
925
|
+
return;
|
|
926
|
+
} else if (name === "TaskUpdate") {
|
|
927
|
+
session.planCreatePending = false;
|
|
928
|
+
applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
|
|
929
|
+
}
|
|
930
|
+
if (name === "TaskUpdate") emitPlan(session);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function flushPendingPlan(session) {
|
|
934
|
+
if (!session.planCreatePending) return;
|
|
935
|
+
session.planCreatePending = false;
|
|
936
|
+
emitPlan(session);
|
|
937
|
+
}
|
|
913
938
|
|
|
914
939
|
function updatePlanFromToolResult(session, pending, message) {
|
|
915
940
|
const value = message.tool_use_result;
|
|
@@ -1322,8 +1347,9 @@ async function runPendingPrompt(session) {
|
|
|
1322
1347
|
}
|
|
1323
1348
|
}
|
|
1324
1349
|
|
|
1325
|
-
function finishTurn(session, error, durationMs) {
|
|
1326
|
-
if (!session.turn) return;
|
|
1350
|
+
function finishTurn(session, error, durationMs) {
|
|
1351
|
+
if (!session.turn) return;
|
|
1352
|
+
flushPendingPlan(session);
|
|
1327
1353
|
flushSmoothStreams(session);
|
|
1328
1354
|
clearForegroundSubagents(session);
|
|
1329
1355
|
const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
|
|
@@ -1411,7 +1437,7 @@ async function startPrompt(params) {
|
|
|
1411
1437
|
return runPrompt(session, params);
|
|
1412
1438
|
}
|
|
1413
1439
|
|
|
1414
|
-
async function runPrompt(session, params) {
|
|
1440
|
+
async function runPrompt(session, params) {
|
|
1415
1441
|
const id = session.id;
|
|
1416
1442
|
if (params.model) {
|
|
1417
1443
|
const model = stripClaudeModel(params.model);
|
|
@@ -1422,13 +1448,13 @@ async function runPrompt(session, params) {
|
|
|
1422
1448
|
if (effort) {
|
|
1423
1449
|
await session.query.applyFlagSettings({ effortLevel: effort });
|
|
1424
1450
|
}
|
|
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 },
|
|
1451
|
+
session.effort = effort;
|
|
1452
|
+
await applyPermissionMode(session, params.permissionMode);
|
|
1453
|
+
const content = await inputContent(params.input, params.handoffContext);
|
|
1454
|
+
const turnId = beginTurn(session, params.input);
|
|
1455
|
+
session.queue.push({
|
|
1456
|
+
type: "user",
|
|
1457
|
+
message: { role: "user", content },
|
|
1432
1458
|
parent_tool_use_id: null,
|
|
1433
1459
|
session_id: id,
|
|
1434
1460
|
origin: { kind: "human" },
|
|
@@ -1438,16 +1464,16 @@ async function runPrompt(session, params) {
|
|
|
1438
1464
|
|
|
1439
1465
|
// A background task notification is an internal user message that starts its
|
|
1440
1466
|
// own Claude response even though the host did not submit a new prompt.
|
|
1441
|
-
function beginTurn(session, input = []) {
|
|
1442
|
-
const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
|
|
1443
|
-
session.turn = {
|
|
1444
|
-
id: turnId,
|
|
1445
|
-
sawStreamText: false,
|
|
1446
|
-
sawVisibleText: false,
|
|
1447
|
-
koreanRequest: isKoreanPrompt(input),
|
|
1448
|
-
openingNotice: openingNotice(input),
|
|
1449
|
-
openingNoticeEmitted: false,
|
|
1450
|
-
};
|
|
1467
|
+
function beginTurn(session, input = []) {
|
|
1468
|
+
const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
|
|
1469
|
+
session.turn = {
|
|
1470
|
+
id: turnId,
|
|
1471
|
+
sawStreamText: false,
|
|
1472
|
+
sawVisibleText: false,
|
|
1473
|
+
koreanRequest: isKoreanPrompt(input),
|
|
1474
|
+
openingNotice: openingNotice(input),
|
|
1475
|
+
openingNoticeEmitted: false,
|
|
1476
|
+
};
|
|
1451
1477
|
session.lastContextUsage = null;
|
|
1452
1478
|
notify("turn/started", { threadId: session.id, turn: { id: turnId } });
|
|
1453
1479
|
return turnId;
|
|
@@ -1569,6 +1595,75 @@ function historyTurns(messages) {
|
|
|
1569
1595
|
return historyState(messages).turns;
|
|
1570
1596
|
}
|
|
1571
1597
|
|
|
1598
|
+
// A transcript lives in a folder encoded from the cwd string, so two spellings of
|
|
1599
|
+
// the same path (Windows differs only in case) resolve to different folders and a
|
|
1600
|
+
// session recorded under one spelling is invisible to the other. Remember the
|
|
1601
|
+
// spelling the transcript was written with, keyed by session id.
|
|
1602
|
+
const transcriptCwds = new Map();
|
|
1603
|
+
|
|
1604
|
+
function claudeProjectsDir() {
|
|
1605
|
+
return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "projects");
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/** Read the cwd a transcript records. Only the head is scanned: the opening
|
|
1609
|
+
* records carry no cwd, but a real turn shows up long before the limit. */
|
|
1610
|
+
async function readTranscriptCwd(path, limit = 200) {
|
|
1611
|
+
const stream = createReadStream(path, { encoding: "utf8" });
|
|
1612
|
+
try {
|
|
1613
|
+
const reader = createInterface({ input: stream, crlfDelay: Infinity });
|
|
1614
|
+
try {
|
|
1615
|
+
let seen = 0;
|
|
1616
|
+
for await (const line of reader) {
|
|
1617
|
+
if (++seen > limit) break;
|
|
1618
|
+
let cwd;
|
|
1619
|
+
try {
|
|
1620
|
+
cwd = JSON.parse(line)?.cwd;
|
|
1621
|
+
} catch {
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (typeof cwd === "string" && cwd) return cwd;
|
|
1625
|
+
}
|
|
1626
|
+
return null;
|
|
1627
|
+
} finally {
|
|
1628
|
+
reader.close();
|
|
1629
|
+
}
|
|
1630
|
+
} finally {
|
|
1631
|
+
stream.destroy();
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/** Locate the transcript of `id` under any project folder and report the cwd it
|
|
1636
|
+
* records, which is the spelling the SDK needs to find it again. */
|
|
1637
|
+
async function transcriptCwd(id) {
|
|
1638
|
+
if (transcriptCwds.has(id)) return transcriptCwds.get(id);
|
|
1639
|
+
let entries;
|
|
1640
|
+
try {
|
|
1641
|
+
entries = await readdir(claudeProjectsDir(), { withFileTypes: true });
|
|
1642
|
+
} catch {
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
for (const entry of entries) {
|
|
1646
|
+
if (!entry.isDirectory()) continue;
|
|
1647
|
+
let cwd;
|
|
1648
|
+
try {
|
|
1649
|
+
cwd = await readTranscriptCwd(join(claudeProjectsDir(), entry.name, `${id}.jsonl`));
|
|
1650
|
+
} catch {
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
if (cwd) {
|
|
1654
|
+
transcriptCwds.set(id, cwd);
|
|
1655
|
+
return cwd;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/** The cwd to read `id`'s transcript with: the one the transcript itself records,
|
|
1662
|
+
* falling back to the host's when no transcript is on disk. */
|
|
1663
|
+
async function readableCwd(id, cwd) {
|
|
1664
|
+
return await transcriptCwd(id) || cwd;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1572
1667
|
async function dispatch(method, params = {}) {
|
|
1573
1668
|
if (method === "model/list") return loadModelCatalog(params);
|
|
1574
1669
|
if (method === "session/permissionMode") {
|
|
@@ -1608,15 +1703,19 @@ async function dispatch(method, params = {}) {
|
|
|
1608
1703
|
tokenUsage: historyTokenUsage(messages, existing.models, existing.model),
|
|
1609
1704
|
};
|
|
1610
1705
|
}
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1706
|
+
const dir = await readableCwd(id, params.cwd);
|
|
1707
|
+
// The transcript itself decides whether there is anything to resume:
|
|
1708
|
+
// getSessionInfo only sees sessions the CLI indexed, and a bridge-run session
|
|
1709
|
+
// whose transcript is intact can be missing from that index.
|
|
1710
|
+
const messages = await getSessionMessages(id, { dir, includeSystemMessages: true });
|
|
1711
|
+
if (!messages.length) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
|
|
1712
|
+
const info = await getSessionInfo(id, { dir });
|
|
1614
1713
|
const lastModel = [...messages].reverse().find((message) => message.type === "assistant")?.message?.model;
|
|
1615
1714
|
// The transcript's own model outranks the host's fallback, which is only what
|
|
1616
1715
|
// a new session would have opened on.
|
|
1617
1716
|
const { session, account, usage } = await createSession({
|
|
1618
1717
|
...params,
|
|
1619
|
-
cwd: info
|
|
1718
|
+
cwd: info?.cwd || dir,
|
|
1620
1719
|
model: params.model || lastModel || params.fallbackModel,
|
|
1621
1720
|
effort: params.effort || params.fallbackEffort,
|
|
1622
1721
|
}, id);
|
|
@@ -1657,7 +1756,7 @@ async function dispatch(method, params = {}) {
|
|
|
1657
1756
|
}
|
|
1658
1757
|
if (method === "session/history") {
|
|
1659
1758
|
const id = liveSessionId(params.sessionId);
|
|
1660
|
-
const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
|
|
1759
|
+
const messages = await getSessionMessages(id, { dir: await readableCwd(id, params.cwd), includeSystemMessages: true });
|
|
1661
1760
|
return { data: historyTurns(messages), nextCursor: null };
|
|
1662
1761
|
}
|
|
1663
1762
|
if (method === "session/prompt") return startPrompt(params);
|
|
@@ -1683,7 +1782,7 @@ async function dispatch(method, params = {}) {
|
|
|
1683
1782
|
}
|
|
1684
1783
|
if (method === "session/fork") {
|
|
1685
1784
|
const source = liveSessionId(params.sessionId);
|
|
1686
|
-
const forked = await forkSession(source, { dir: params.cwd });
|
|
1785
|
+
const forked = await forkSession(source, { dir: await readableCwd(source, params.cwd) });
|
|
1687
1786
|
const id = forked.sessionId || forked;
|
|
1688
1787
|
const { session, account, usage } = await createSession(params, id);
|
|
1689
1788
|
return {
|
|
@@ -1837,9 +1936,53 @@ async function runSelfTest() {
|
|
|
1837
1936
|
}
|
|
1838
1937
|
prepareTaskPlanForCreate(restoredTasks, "7. 추가 작업");
|
|
1839
1938
|
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
|
|
1939
|
+
prepareTaskPlanForCreate(restoredTasks, "1. 새 작업");
|
|
1940
|
+
if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
|
|
1941
|
+
const batchedPlanSession = {
|
|
1942
|
+
id: "batched-plan-self-test",
|
|
1943
|
+
turn: { id: "batched-plan-turn" },
|
|
1944
|
+
tasks: new Map(),
|
|
1945
|
+
planCreatePending: false,
|
|
1946
|
+
};
|
|
1947
|
+
const batchedPlanCaptured = [];
|
|
1948
|
+
const batchedPlanWrite = process.stdout.write;
|
|
1949
|
+
process.stdout.write = (chunk) => {
|
|
1950
|
+
batchedPlanCaptured.push(String(chunk));
|
|
1951
|
+
return true;
|
|
1952
|
+
};
|
|
1953
|
+
try {
|
|
1954
|
+
for (let index = 1; index <= 3; index++) {
|
|
1955
|
+
const toolUseId = `batched-create-${index}`;
|
|
1956
|
+
const subject = `${index}. 작업 ${index}`;
|
|
1957
|
+
updatePlanFromToolUse(batchedPlanSession, "TaskCreate", toolUseId, { subject });
|
|
1958
|
+
updatePlanFromToolResult(
|
|
1959
|
+
batchedPlanSession,
|
|
1960
|
+
{ name: "TaskCreate", toolUseId },
|
|
1961
|
+
{ tool_use_result: { task: { id: String(index), subject } } },
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
updatePlanFromToolUse(
|
|
1965
|
+
batchedPlanSession,
|
|
1966
|
+
"TaskUpdate",
|
|
1967
|
+
"batched-update-1",
|
|
1968
|
+
{ taskId: "1", status: "in_progress" },
|
|
1969
|
+
);
|
|
1970
|
+
} finally {
|
|
1971
|
+
process.stdout.write = batchedPlanWrite;
|
|
1972
|
+
}
|
|
1973
|
+
const batchedPlanEvents = batchedPlanCaptured
|
|
1974
|
+
.join("")
|
|
1975
|
+
.trim()
|
|
1976
|
+
.split("\n")
|
|
1977
|
+
.filter(Boolean)
|
|
1978
|
+
.map((line) => JSON.parse(line))
|
|
1979
|
+
.filter((event) => event.method === "turn/plan/updated");
|
|
1980
|
+
if (batchedPlanEvents.length !== 1
|
|
1981
|
+
|| batchedPlanEvents[0].params?.plan?.length !== 3
|
|
1982
|
+
|| batchedPlanEvents[0].params.plan[0]?.status !== "inProgress") {
|
|
1983
|
+
throw new Error(`Claude batched plan self-test failed: ${JSON.stringify(batchedPlanEvents)}`);
|
|
1984
|
+
}
|
|
1985
|
+
const usage = tokenBreakdown({
|
|
1843
1986
|
input_tokens: 2,
|
|
1844
1987
|
cache_read_input_tokens: 68_000,
|
|
1845
1988
|
cache_creation_input_tokens: 500,
|
|
@@ -1966,95 +2109,98 @@ async function runSelfTest() {
|
|
|
1966
2109
|
.filter(Boolean)
|
|
1967
2110
|
.map((line) => JSON.parse(line));
|
|
1968
2111
|
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}`);
|
|
1974
|
-
}
|
|
1975
|
-
const openingSession = {
|
|
1976
|
-
id: "opening-self-test",
|
|
1977
|
-
model: "claude:default",
|
|
1978
|
-
models: [],
|
|
1979
|
-
turn: null,
|
|
1980
|
-
turnSequence: 1,
|
|
1981
|
-
itemSequence: 1,
|
|
1982
|
-
streamBlocks: new Map(),
|
|
1983
|
-
tools: new Map(),
|
|
1984
|
-
tasks: new Map(),
|
|
1985
|
-
subagents: new Map(),
|
|
1986
|
-
knownSubagents: new Map(),
|
|
1987
|
-
lastContextUsage: null,
|
|
1988
|
-
lastContextWindow: 0,
|
|
1989
|
-
};
|
|
1990
|
-
const openingCaptured = [];
|
|
1991
|
-
process.stdout.write = (chunk) => {
|
|
1992
|
-
openingCaptured.push(String(chunk));
|
|
1993
|
-
return true;
|
|
1994
|
-
};
|
|
1995
|
-
try {
|
|
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
|
-
},
|
|
2001
|
-
});
|
|
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" } },
|
|
2029
|
-
});
|
|
2030
|
-
await processStreamEvent(openingSession, {
|
|
2031
|
-
event: { type: "content_block_delta", index: 0, delta: { text: "Now the tile view logic." } },
|
|
2032
|
-
});
|
|
2033
|
-
await processStreamEvent(openingSession, {
|
|
2034
|
-
event: { type: "content_block_stop", index: 0 },
|
|
2035
|
-
});
|
|
2036
|
-
} finally {
|
|
2037
|
-
process.stdout.write = stdoutWrite;
|
|
2038
|
-
}
|
|
2039
|
-
const languageEvents = languageCaptured
|
|
2040
|
-
.join("")
|
|
2041
|
-
.trim()
|
|
2042
|
-
.split("\n")
|
|
2043
|
-
.filter(Boolean)
|
|
2044
|
-
.map((line) => JSON.parse(line));
|
|
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)}`);
|
|
2112
|
+
if (!lifecycleMethods.includes("turn/subagents/updated")
|
|
2113
|
+
|| lifecycleMethods.filter((method) => method === "turn/started").length < 3
|
|
2114
|
+
|| !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
|
|
2115
|
+
&& event.params?.line?.kind === "error")) {
|
|
2116
|
+
throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
|
|
2117
|
+
}
|
|
2118
|
+
const openingSession = {
|
|
2119
|
+
id: "opening-self-test",
|
|
2120
|
+
model: "claude:default",
|
|
2121
|
+
models: [],
|
|
2122
|
+
turn: null,
|
|
2123
|
+
turnSequence: 1,
|
|
2124
|
+
itemSequence: 1,
|
|
2125
|
+
streamBlocks: new Map(),
|
|
2126
|
+
tools: new Map(),
|
|
2127
|
+
tasks: new Map(),
|
|
2128
|
+
subagents: new Map(),
|
|
2129
|
+
knownSubagents: new Map(),
|
|
2130
|
+
lastContextUsage: null,
|
|
2131
|
+
lastContextWindow: 0,
|
|
2132
|
+
};
|
|
2133
|
+
const openingCaptured = [];
|
|
2134
|
+
process.stdout.write = (chunk) => {
|
|
2135
|
+
openingCaptured.push(String(chunk));
|
|
2136
|
+
return true;
|
|
2137
|
+
};
|
|
2138
|
+
try {
|
|
2139
|
+
beginTurn(openingSession, [{ type: "text", text: "provider 메뉴를 수정해" }]);
|
|
2140
|
+
processAssistant(openingSession, {
|
|
2141
|
+
message: {
|
|
2142
|
+
content: [{ type: "tool_use", id: "read-1", name: "Read", input: { file_path: "src/main.rs" } }],
|
|
2143
|
+
},
|
|
2144
|
+
});
|
|
2145
|
+
} finally {
|
|
2146
|
+
process.stdout.write = stdoutWrite;
|
|
2147
|
+
}
|
|
2148
|
+
const openingEvents = openingCaptured
|
|
2149
|
+
.join("")
|
|
2150
|
+
.trim()
|
|
2151
|
+
.split("\n")
|
|
2152
|
+
.filter(Boolean)
|
|
2153
|
+
.map((line) => JSON.parse(line));
|
|
2154
|
+
const openingMessageIndex = openingEvents.findIndex((event) =>
|
|
2155
|
+
event.method === "item/completed"
|
|
2156
|
+
&& event.params?.item?.type === "agentMessage"
|
|
2157
|
+
&& event.params.item.text === "요청 내용을 확인하고 필요한 작업을 진행하겠습니다.");
|
|
2158
|
+
const openingToolIndex = openingEvents.findIndex((event) =>
|
|
2159
|
+
event.method === "item/started" && event.params?.item?.type === "dynamicToolCall");
|
|
2160
|
+
if (openingMessageIndex < 0 || openingToolIndex < 0 || openingMessageIndex > openingToolIndex) {
|
|
2161
|
+
throw new Error(`Claude opening notice order self-test failed: ${JSON.stringify(openingEvents)}`);
|
|
2162
|
+
}
|
|
2163
|
+
const languageCaptured = [];
|
|
2164
|
+
process.stdout.write = (chunk) => {
|
|
2165
|
+
languageCaptured.push(String(chunk));
|
|
2166
|
+
return true;
|
|
2167
|
+
};
|
|
2168
|
+
try {
|
|
2169
|
+
openingSession.streamBlocks.clear();
|
|
2170
|
+
await processStreamEvent(openingSession, {
|
|
2171
|
+
event: { type: "content_block_start", index: 0, content_block: { type: "text" } },
|
|
2172
|
+
});
|
|
2173
|
+
await processStreamEvent(openingSession, {
|
|
2174
|
+
event: { type: "content_block_delta", index: 0, delta: { text: "Now the tile view logic." } },
|
|
2175
|
+
});
|
|
2176
|
+
await processStreamEvent(openingSession, {
|
|
2177
|
+
event: { type: "content_block_stop", index: 0 },
|
|
2178
|
+
});
|
|
2179
|
+
} finally {
|
|
2180
|
+
process.stdout.write = stdoutWrite;
|
|
2181
|
+
}
|
|
2182
|
+
const languageEvents = languageCaptured
|
|
2183
|
+
.join("")
|
|
2184
|
+
.trim()
|
|
2185
|
+
.split("\n")
|
|
2186
|
+
.filter(Boolean)
|
|
2187
|
+
.map((line) => JSON.parse(line));
|
|
2188
|
+
const visibleLanguage = languageEvents
|
|
2189
|
+
.filter((event) => event.method === "item/agentMessage/delta")
|
|
2190
|
+
.map((event) => event.params?.delta || "")
|
|
2191
|
+
.join("");
|
|
2192
|
+
if (visibleLanguage !== "다음 부분을 이어서 확인하겠습니다."
|
|
2193
|
+
|| languageEvents.some((event) => JSON.stringify(event).includes("Now the tile view logic."))) {
|
|
2194
|
+
throw new Error(`Claude Korean progress normalization self-test failed: ${JSON.stringify(languageEvents)}`);
|
|
2195
|
+
}
|
|
2196
|
+
const smoothText = "Claude가 👨👩👧👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
|
|
2197
|
+
const emitted = [];
|
|
2198
|
+
const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
|
|
2199
|
+
smooth.push(smoothText);
|
|
2200
|
+
if (emitted.length !== 0) {
|
|
2201
|
+
throw new Error(`Claude smooth stream did not batch its first frame: ${JSON.stringify(emitted)}`);
|
|
2052
2202
|
}
|
|
2053
|
-
|
|
2054
|
-
const emitted = [];
|
|
2055
|
-
const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
|
|
2056
|
-
smooth.push(smoothText);
|
|
2057
|
-
await smooth.finish();
|
|
2203
|
+
await smooth.finish();
|
|
2058
2204
|
if (emitted.length < 2 || emitted.join("") !== smoothText) {
|
|
2059
2205
|
throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
|
|
2060
2206
|
}
|