devez-vibe 1.9.58 → 1.9.60
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 +696 -151
- package/package.json +1 -1
package/bin/dvz.exe
CHANGED
|
Binary file
|
|
@@ -107,7 +107,7 @@ function rpcError(error) {
|
|
|
107
107
|
*/
|
|
108
108
|
const HOST_REQUEST_RUN = Math.random().toString(36).slice(2, 8);
|
|
109
109
|
|
|
110
|
-
function hostRequest(method, params, signal) {
|
|
110
|
+
function hostRequest(method, params, signal, sessionId) {
|
|
111
111
|
const id = `claude-host-${HOST_REQUEST_RUN}-${nextHostRequest++}`;
|
|
112
112
|
return new Promise((resolve, reject) => {
|
|
113
113
|
const abort = () => {
|
|
@@ -117,6 +117,7 @@ function hostRequest(method, params, signal) {
|
|
|
117
117
|
if (signal?.aborted) return abort();
|
|
118
118
|
signal?.addEventListener("abort", abort, { once: true });
|
|
119
119
|
pendingHostRequests.set(id, {
|
|
120
|
+
session: sessionId ? lookupSession(sessionId) : null,
|
|
120
121
|
resolve: (value) => {
|
|
121
122
|
signal?.removeEventListener("abort", abort);
|
|
122
123
|
resolve(value);
|
|
@@ -538,7 +539,7 @@ async function applyPermissionMode(session) {
|
|
|
538
539
|
return;
|
|
539
540
|
}
|
|
540
541
|
try {
|
|
541
|
-
await session
|
|
542
|
+
await withSessionQuery(session, (query) => query.setPermissionMode(PREFERRED_PERMISSION_MODE));
|
|
542
543
|
session.permissionMode = PREFERRED_PERMISSION_MODE;
|
|
543
544
|
session.permissionModeVerified = true;
|
|
544
545
|
return;
|
|
@@ -766,7 +767,7 @@ function makeOptions(params, sessionId, resume) {
|
|
|
766
767
|
if (resume) options.resume = resume;
|
|
767
768
|
else options.sessionId = sessionId;
|
|
768
769
|
options.canUseTool = (toolName, input, permission) =>
|
|
769
|
-
requestToolPermission(toolName, input, permission);
|
|
770
|
+
requestToolPermission(toolName, input, permission, sessionId);
|
|
770
771
|
// Hooks run whatever the permission mode, so a read-only role turn holds even
|
|
771
772
|
// under bypass. The policy itself arrives with each prompt.
|
|
772
773
|
options.hooks = {
|
|
@@ -884,7 +885,7 @@ function permissionSuggestionLabel(suggestions) {
|
|
|
884
885
|
: `Don't ask again ${scope}`;
|
|
885
886
|
}
|
|
886
887
|
|
|
887
|
-
async function requestToolPermission(toolName, input, permission) {
|
|
888
|
+
async function requestToolPermission(toolName, input, permission, sessionId) {
|
|
888
889
|
if (toolName === "AskUserQuestion") {
|
|
889
890
|
const questions = (input.questions || []).map((question, index) => ({
|
|
890
891
|
id: `q${index}`,
|
|
@@ -907,6 +908,7 @@ async function requestToolPermission(toolName, input, permission) {
|
|
|
907
908
|
payload: Buffer.from(JSON.stringify({ questions }), "utf8").toString("base64"),
|
|
908
909
|
},
|
|
909
910
|
permission.signal,
|
|
911
|
+
sessionId,
|
|
910
912
|
);
|
|
911
913
|
let response;
|
|
912
914
|
try {
|
|
@@ -981,7 +983,7 @@ async function requestToolPermission(toolName, input, permission) {
|
|
|
981
983
|
reason: permission.decisionReason || permission.description,
|
|
982
984
|
};
|
|
983
985
|
}
|
|
984
|
-
const response = await hostRequest(method, params, permission.signal);
|
|
986
|
+
const response = await hostRequest(method, params, permission.signal, sessionId);
|
|
985
987
|
if (!approvalGranted(response)) return { behavior: "deny", message: "사용자가 작업을 거부했습니다." };
|
|
986
988
|
return {
|
|
987
989
|
behavior: "allow",
|
|
@@ -1778,7 +1780,7 @@ async function supportedClaudeSkills(params) {
|
|
|
1778
1780
|
const cwd = params.cwd || process.cwd();
|
|
1779
1781
|
const live = [...sessions.values()].find((session) =>
|
|
1780
1782
|
session.query && sameCwd(session.cwd, cwd));
|
|
1781
|
-
if (live) return filterClaudeSkillCommands(await
|
|
1783
|
+
if (live) return withSessionQuery(live, async (query) => filterClaudeSkillCommands(await query.supportedCommands()));
|
|
1782
1784
|
const cacheKey = `${params.claudePath || "claude"}\n${params.cwd || process.cwd()}`;
|
|
1783
1785
|
if (params.forceReload) skillCatalogs.delete(cacheKey);
|
|
1784
1786
|
if (skillCatalogs.has(cacheKey)) return skillCatalogs.get(cacheKey);
|
|
@@ -1864,18 +1866,20 @@ async function claudeMcpStatus(params) {
|
|
|
1864
1866
|
unavailableReason: "Claude 세션이 아직 시작되지 않았습니다.",
|
|
1865
1867
|
};
|
|
1866
1868
|
}
|
|
1867
|
-
return claudeMcpStatusValue(await
|
|
1869
|
+
return withSessionQuery(session, async (query) => claudeMcpStatusValue(await query.mcpServerStatus()));
|
|
1868
1870
|
}
|
|
1869
1871
|
|
|
1870
1872
|
async function reconnectClaudeMcp(session, requested) {
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1873
|
+
return withSessionQuery(session, async (query) => {
|
|
1874
|
+
const names = requested
|
|
1875
|
+
? [requested]
|
|
1876
|
+
: (await query.mcpServerStatus())
|
|
1877
|
+
.filter((server) => server?.status !== "disabled")
|
|
1878
|
+
.map((server) => String(server.name || ""))
|
|
1879
|
+
.filter(Boolean);
|
|
1880
|
+
for (const name of names) await query.reconnectMcpServer(name);
|
|
1881
|
+
return names;
|
|
1882
|
+
});
|
|
1879
1883
|
}
|
|
1880
1884
|
|
|
1881
1885
|
function isQuestionDeliveryError(error) {
|
|
@@ -1918,6 +1922,7 @@ async function createSession(params, resumeId) {
|
|
|
1918
1922
|
models: [],
|
|
1919
1923
|
queue,
|
|
1920
1924
|
query: null,
|
|
1925
|
+
initializing: true,
|
|
1921
1926
|
turn: null,
|
|
1922
1927
|
// Prompts that arrived while a turn was running, run in order afterwards.
|
|
1923
1928
|
pendingPrompts: [],
|
|
@@ -1951,6 +1956,7 @@ async function createSession(params, resumeId) {
|
|
|
1951
1956
|
sessions.set(id, session);
|
|
1952
1957
|
const consumer = consume(session)
|
|
1953
1958
|
.catch((error) => {
|
|
1959
|
+
if (session.closed) return;
|
|
1954
1960
|
notify("error", {
|
|
1955
1961
|
threadId: id,
|
|
1956
1962
|
provider: "Claude",
|
|
@@ -1962,16 +1968,11 @@ async function createSession(params, resumeId) {
|
|
|
1962
1968
|
.finally(() => {
|
|
1963
1969
|
clearUsageLimitWait(session);
|
|
1964
1970
|
if (session.turn) finishTurn(session, new Error("Claude 연결이 종료되었습니다."));
|
|
1965
|
-
clearSubagents(session);
|
|
1966
1971
|
// A crashed CLI (Windows reports an access violation as exit code
|
|
1967
1972
|
// 3221225477) leaves a query that rejects every later request. Dropping
|
|
1968
1973
|
// the session makes the next prompt fail as missing, which the host answers
|
|
1969
1974
|
// by resuming it from the transcript instead of the user reopening it.
|
|
1970
|
-
|
|
1971
|
-
sessions.delete(session.id);
|
|
1972
|
-
session.queue.close();
|
|
1973
|
-
session.query.close();
|
|
1974
|
-
}
|
|
1975
|
+
closeLiveSession(session);
|
|
1975
1976
|
});
|
|
1976
1977
|
session.consumer = consumer;
|
|
1977
1978
|
let initialization;
|
|
@@ -1979,9 +1980,7 @@ async function createSession(params, resumeId) {
|
|
|
1979
1980
|
initialization = await agentQuery.initializationResult();
|
|
1980
1981
|
await applyPermissionMode(session);
|
|
1981
1982
|
} catch (error) {
|
|
1982
|
-
|
|
1983
|
-
queue.close();
|
|
1984
|
-
agentQuery.close();
|
|
1983
|
+
closeLiveSession(session);
|
|
1985
1984
|
await Promise.race([consumer, new Promise((resolve) => setTimeout(resolve, 1000))]);
|
|
1986
1985
|
throw error;
|
|
1987
1986
|
}
|
|
@@ -2018,9 +2017,70 @@ async function createSession(params, resumeId) {
|
|
|
2018
2017
|
session.effort = supportedEffort(capabilities, params.effort);
|
|
2019
2018
|
const account = initialization.account || await safeAccount(agentQuery);
|
|
2020
2019
|
const usage = await safeUsage(agentQuery);
|
|
2020
|
+
if (session.closed || sessions.get(session.id) !== session || session.query !== agentQuery) {
|
|
2021
|
+
closeLiveSession(session);
|
|
2022
|
+
throw new Error("Claude 초기화 중 연결이 종료되었습니다.");
|
|
2023
|
+
}
|
|
2024
|
+
session.initializing = false;
|
|
2025
|
+
// 새 연결이 준비된 뒤 이전 유휴 연결을 거둔다. /btw의 부모처럼 작업 중인
|
|
2026
|
+
// 세션과 다른 createSession의 초기화 중인 연결은 그대로 둔다.
|
|
2027
|
+
for (const previous of [...sessions.values()]) {
|
|
2028
|
+
if (previous !== session && !sessionHasLiveWork(previous)) closeLiveSession(previous);
|
|
2029
|
+
}
|
|
2021
2030
|
return { session, initialization, account, usage };
|
|
2022
2031
|
}
|
|
2023
2032
|
|
|
2033
|
+
function sessionHasLiveWork(session) {
|
|
2034
|
+
return Boolean(session.initializing || session.promptStarting || session.queryRequests > 0 || session.turn
|
|
2035
|
+
|| session.pendingPrompts?.length || session.queue?.values?.length
|
|
2036
|
+
|| session.steerPending > 0 || session.automaticTurnsPending > 0 || session.usageLimitWait
|
|
2037
|
+
|| session.subagents?.size || session.hiddenSubagentTasks?.size || session.ambientSubagentTasks?.size
|
|
2038
|
+
|| [...pendingHostRequests.values()].some((request) => request.session === session));
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// await 뒤 같은 query로 이어지는 제어·조회 요청이 유휴 정리와 겹치지 않게 한다.
|
|
2042
|
+
async function withSessionQuery(session, action) {
|
|
2043
|
+
const query = session.query;
|
|
2044
|
+
session.queryRequests = (session.queryRequests || 0) + 1;
|
|
2045
|
+
try {
|
|
2046
|
+
return await action(query);
|
|
2047
|
+
} finally {
|
|
2048
|
+
session.queryRequests -= 1;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// 저장된 transcript는 남기고 CLI와 이 연결의 참조만 해제한다.
|
|
2053
|
+
function closeLiveSession(session) {
|
|
2054
|
+
if (session.closed) return;
|
|
2055
|
+
session.closed = true;
|
|
2056
|
+
clearUsageLimitWait(session);
|
|
2057
|
+
watchSubagent(session, null);
|
|
2058
|
+
clearSubagents(session);
|
|
2059
|
+
for (const [id, request] of pendingHostRequests) {
|
|
2060
|
+
if (request.session !== session) continue;
|
|
2061
|
+
pendingHostRequests.delete(id);
|
|
2062
|
+
request.reject(new Error("Claude 연결이 종료되어 사용자 입력 요청이 취소되었습니다."));
|
|
2063
|
+
}
|
|
2064
|
+
if (sessions.get(session.id) === session) {
|
|
2065
|
+
const aliases = [...sessionAliases.keys()].filter((from) => liveSessionId(from) === session.id);
|
|
2066
|
+
for (const from of aliases) sessionAliases.delete(from);
|
|
2067
|
+
sessions.delete(session.id);
|
|
2068
|
+
}
|
|
2069
|
+
session.queue?.close();
|
|
2070
|
+
if (session.queue?.values) session.queue.values.length = 0;
|
|
2071
|
+
session.query?.close();
|
|
2072
|
+
session.queue = null;
|
|
2073
|
+
session.query = null;
|
|
2074
|
+
session.consumer = null;
|
|
2075
|
+
session.pendingPrompts?.splice(0);
|
|
2076
|
+
session.steerPending = 0;
|
|
2077
|
+
session.automaticTurnsPending = 0;
|
|
2078
|
+
session.streamBlocks?.clear();
|
|
2079
|
+
session.tools?.clear();
|
|
2080
|
+
session.knownSubagents?.clear();
|
|
2081
|
+
session.rowTaskIds?.clear();
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2024
2084
|
async function safeAccount(agentQuery) {
|
|
2025
2085
|
try { return await agentQuery.accountInfo(); } catch { return null; }
|
|
2026
2086
|
}
|
|
@@ -2660,19 +2720,14 @@ function keepBackgroundSubagent(session, toolUseId, result) {
|
|
|
2660
2720
|
running.lastSeenAt = Date.now();
|
|
2661
2721
|
// level 스냅숏이 접수 결과보다 먼저 와서 자리표시자 행을 만들었으면 여기서 접는다.
|
|
2662
2722
|
// task_started가 오지 않는 빌드에서는 이 경로가 유일한 병합 지점이다.
|
|
2663
|
-
const placeholder = running.taskId ? session.subagents.
|
|
2723
|
+
const placeholder = running.taskId ? [...session.subagents.values()]
|
|
2724
|
+
.find((agent) => agent !== running && agent.taskId === running.taskId) : null;
|
|
2664
2725
|
if (placeholder && placeholder !== running) {
|
|
2665
2726
|
running.command ||= placeholder.command;
|
|
2727
|
+
if (placeholder.runId && (!running.runId || running.runId < placeholder.runId)) running.runId = placeholder.runId;
|
|
2666
2728
|
session.subagents.delete(placeholder.id);
|
|
2667
2729
|
}
|
|
2668
|
-
|
|
2669
|
-
session.knownSubagents.set(running.taskId, {
|
|
2670
|
-
rowId: running.id,
|
|
2671
|
-
name: running.name,
|
|
2672
|
-
description: running.description,
|
|
2673
|
-
backgroundTask: running.backgroundTask === true,
|
|
2674
|
-
});
|
|
2675
|
-
}
|
|
2730
|
+
rememberSubagent(session, running);
|
|
2676
2731
|
return true;
|
|
2677
2732
|
}
|
|
2678
2733
|
|
|
@@ -2686,8 +2741,9 @@ function resumeBackgroundSubagent(session, toolUseId, pending, result) {
|
|
|
2686
2741
|
if (existing) return true;
|
|
2687
2742
|
const known = session.knownSubagents.get(taskId);
|
|
2688
2743
|
const input = pending?.input || {};
|
|
2744
|
+
// 끝난 줄과 같은 id로 돌아와야 호스트가 같은 작업의 idle 줄과 실행 줄을 함께 세우지 않는다.
|
|
2689
2745
|
const running = {
|
|
2690
|
-
id: toolUseId,
|
|
2746
|
+
id: known?.rowId || toolUseId,
|
|
2691
2747
|
toolUseId,
|
|
2692
2748
|
taskId,
|
|
2693
2749
|
background: true,
|
|
@@ -2697,8 +2753,10 @@ function resumeBackgroundSubagent(session, toolUseId, pending, result) {
|
|
|
2697
2753
|
startedAt: Date.now(),
|
|
2698
2754
|
lastSeenAt: Date.now(),
|
|
2699
2755
|
};
|
|
2700
|
-
session.subagents.set(
|
|
2756
|
+
session.subagents.set(running.id, running);
|
|
2701
2757
|
session.knownSubagents.set(taskId, {
|
|
2758
|
+
...known,
|
|
2759
|
+
rowId: running.id,
|
|
2702
2760
|
name: running.name,
|
|
2703
2761
|
description: running.description,
|
|
2704
2762
|
backgroundTask: false,
|
|
@@ -2740,6 +2798,8 @@ function upsertStructuredSubagent(session, message) {
|
|
|
2740
2798
|
|| session.ambientSubagentTasks?.has(taskId))) return null;
|
|
2741
2799
|
const byTool = toolUseId && findSubagent(session, toolUseId);
|
|
2742
2800
|
const byTask = taskId && findSubagent(session, taskId);
|
|
2801
|
+
const runId = subagentRunId(message.run_id);
|
|
2802
|
+
if (isStaleSubagentRun(session, taskId, runId, byTool, byTask)) return null;
|
|
2743
2803
|
let running = byTool || byTask;
|
|
2744
2804
|
// The SDK does not order the background level snapshot against task_started.
|
|
2745
2805
|
// If both paths created a row, keep the tool-use row and fold the placeholder
|
|
@@ -2749,6 +2809,7 @@ function upsertStructuredSubagent(session, message) {
|
|
|
2749
2809
|
byTool.command ||= byTask.command;
|
|
2750
2810
|
byTool.taskId ||= byTask.taskId;
|
|
2751
2811
|
byTool.toolUseId ||= byTask.toolUseId;
|
|
2812
|
+
if (byTask.runId && (!byTool.runId || byTool.runId < byTask.runId)) byTool.runId = byTask.runId;
|
|
2752
2813
|
if ((!byTool.name || byTool.name === "agent") && byTask.name) byTool.name = byTask.name;
|
|
2753
2814
|
if (!byTool.tool) byTool.tool = byTask.tool;
|
|
2754
2815
|
byTool.startedAt = Math.min(byTool.startedAt, byTask.startedAt);
|
|
@@ -2777,6 +2838,7 @@ function upsertStructuredSubagent(session, message) {
|
|
|
2777
2838
|
}
|
|
2778
2839
|
if (toolUseId) running.toolUseId = toolUseId;
|
|
2779
2840
|
if (taskId) running.taskId = taskId;
|
|
2841
|
+
if (runId) running.runId = runId;
|
|
2780
2842
|
// 스크립트 Monitor는 local_bash 작업으로 와서 스냅숏 이름이 Bash가 된다.
|
|
2781
2843
|
if (session.tools?.get(toolUseId)?.name === "Monitor") running.name = "Monitor";
|
|
2782
2844
|
const parentTaskId = firstLine(message.parent_task_id || "", 80);
|
|
@@ -2785,14 +2847,7 @@ function upsertStructuredSubagent(session, message) {
|
|
|
2785
2847
|
if (subagentType && (!running.name || running.name === "agent")) running.name = subagentType;
|
|
2786
2848
|
if (subagentType || isSubagentTaskType(message.task_type)) running.backgroundTask = false;
|
|
2787
2849
|
running.lastSeenAt = Date.now();
|
|
2788
|
-
|
|
2789
|
-
session.knownSubagents.set(running.taskId, {
|
|
2790
|
-
rowId: running.id,
|
|
2791
|
-
name: running.name,
|
|
2792
|
-
description: running.description,
|
|
2793
|
-
backgroundTask: running.backgroundTask === true,
|
|
2794
|
-
});
|
|
2795
|
-
}
|
|
2850
|
+
rememberSubagent(session, running);
|
|
2796
2851
|
return running;
|
|
2797
2852
|
}
|
|
2798
2853
|
|
|
@@ -2800,9 +2855,49 @@ function hasLiveChildren(session, taskId) {
|
|
|
2800
2855
|
return !!taskId && [...session.subagents.values()].some((agent) => agent.parentTaskId === taskId);
|
|
2801
2856
|
}
|
|
2802
2857
|
|
|
2858
|
+
// 같은 작업의 실행 id는 열린 순서대로 문자열 정렬된다. 지금 행보다 앞선 실행의
|
|
2859
|
+
// 통지는 늦게 도착한 것이라 새 실행의 행을 지우면 안 된다.
|
|
2860
|
+
function subagentRunId(value) {
|
|
2861
|
+
return typeof value === "string" ? value : "";
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
function isLateRun(running, runId) {
|
|
2865
|
+
return Boolean(runId && running?.runId && runId < running.runId);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// 행을 거둬도 마지막 실행과 완료 실행을 기억한다. 같은 실행의 늦은 진행·스냅숏은
|
|
2869
|
+
// 행을 되살리지 못하고, 더 큰 실행 id로 시작한 정상 재실행은 그대로 허용한다.
|
|
2870
|
+
function rememberSubagent(session, running, completed = false, runId = running.runId) {
|
|
2871
|
+
if (!running.taskId) return;
|
|
2872
|
+
const known = (session.knownSubagents ||= new Map()).get(running.taskId);
|
|
2873
|
+
const latestRunId = [known?.runId, running.runId, runId].filter(Boolean).sort().at(-1);
|
|
2874
|
+
const completedRunId = [known?.completedRunId, completed && (runId || running.runId)].filter(Boolean).sort().at(-1);
|
|
2875
|
+
session.knownSubagents.set(running.taskId, {
|
|
2876
|
+
...known,
|
|
2877
|
+
rowId: running.id || known?.rowId,
|
|
2878
|
+
name: running.name || known?.name,
|
|
2879
|
+
// 진행 요약 대신 호출 설명을 보존해 재실행한 행이 원래 설명으로 돌아오게 한다.
|
|
2880
|
+
description: known?.description || running.description || "",
|
|
2881
|
+
backgroundTask: running.backgroundTask === true,
|
|
2882
|
+
...(latestRunId ? { runId: latestRunId } : {}),
|
|
2883
|
+
...(completedRunId ? { completedRunId } : {}),
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function isStaleSubagentRun(session, taskId, runId, ...running) {
|
|
2888
|
+
if (!runId) return false;
|
|
2889
|
+
const known = taskId && session.knownSubagents?.get(taskId);
|
|
2890
|
+
return Boolean((known?.completedRunId && runId <= known.completedRunId)
|
|
2891
|
+
|| isLateRun(known, runId) || running.some((row) => isLateRun(row, runId)));
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2803
2894
|
function finishStructuredSubagent(session, message) {
|
|
2804
2895
|
const taskId = firstLine(message.task_id || "", 80);
|
|
2805
2896
|
const toolUseId = firstLine(message.tool_use_id || "", 80);
|
|
2897
|
+
const running = (taskId && findSubagent(session, taskId))
|
|
2898
|
+
|| (toolUseId && findSubagent(session, toolUseId));
|
|
2899
|
+
const runId = subagentRunId(message.run_id);
|
|
2900
|
+
if (isStaleSubagentRun(session, taskId, runId, running)) return true;
|
|
2806
2901
|
const wasTranscriptHidden = taskId
|
|
2807
2902
|
? session.hiddenSubagentTasks?.delete(taskId) === true
|
|
2808
2903
|
: false;
|
|
@@ -2810,8 +2905,7 @@ function finishStructuredSubagent(session, message) {
|
|
|
2810
2905
|
? session.ambientSubagentTasks?.delete(taskId) === true
|
|
2811
2906
|
: false;
|
|
2812
2907
|
const wasHidden = wasTranscriptHidden || wasAmbient;
|
|
2813
|
-
|
|
2814
|
-
|| (taskId && findSubagent(session, taskId));
|
|
2908
|
+
rememberSubagent(session, running || { taskId, runId }, true, runId || running?.runId);
|
|
2815
2909
|
if (!running) return wasHidden;
|
|
2816
2910
|
// 자기 백그라운드 명령을 기다리며 멈춘 에이전트는 그 명령이 끝나면 다시 돌므로 행을 남긴다.
|
|
2817
2911
|
if (hasLiveChildren(session, running.taskId)) return true;
|
|
@@ -2822,10 +2916,32 @@ function finishStructuredSubagent(session, message) {
|
|
|
2822
2916
|
|
|
2823
2917
|
function syncBackgroundSubagents(session, tasks) {
|
|
2824
2918
|
const snapshot = Array.isArray(tasks) ? tasks : [];
|
|
2919
|
+
const staleIds = new Set(snapshot
|
|
2920
|
+
.filter((task) => {
|
|
2921
|
+
const taskId = firstLine(task?.task_id || "", 80);
|
|
2922
|
+
return isStaleSubagentRun(session, taskId, subagentRunId(task?.run_id), findSubagent(session, taskId));
|
|
2923
|
+
})
|
|
2924
|
+
.map((task) => firstLine(task?.task_id || "", 80))
|
|
2925
|
+
.filter(Boolean));
|
|
2926
|
+
const previousAmbient = session.ambientSubagentTasks || new Set();
|
|
2927
|
+
for (const task of snapshot) {
|
|
2928
|
+
if (task?.ambient !== true) continue;
|
|
2929
|
+
const taskId = firstLine(task?.task_id || "", 80);
|
|
2930
|
+
const runId = subagentRunId(task?.run_id);
|
|
2931
|
+
const running = findSubagent(session, taskId);
|
|
2932
|
+
if (!isStaleSubagentRun(session, taskId, runId, running)) {
|
|
2933
|
+
rememberSubagent(session, running || { taskId }, false, runId);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2825
2936
|
session.ambientSubagentTasks = new Set(snapshot
|
|
2826
|
-
.filter((task) => task?.ambient === true
|
|
2937
|
+
.filter((task) => task?.ambient === true && !isStaleSubagentRun(session,
|
|
2938
|
+
firstLine(task?.task_id || "", 80), subagentRunId(task?.run_id),
|
|
2939
|
+
findSubagent(session, task?.task_id)))
|
|
2827
2940
|
.map((task) => firstLine(task?.task_id || "", 80))
|
|
2828
2941
|
.filter(Boolean));
|
|
2942
|
+
for (const taskId of previousAmbient) {
|
|
2943
|
+
if (staleIds.has(taskId)) session.ambientSubagentTasks.add(taskId);
|
|
2944
|
+
}
|
|
2829
2945
|
const live = snapshot.filter((task) => {
|
|
2830
2946
|
const taskId = firstLine(task?.task_id || "", 80);
|
|
2831
2947
|
return task?.ambient !== true
|
|
@@ -2835,12 +2951,15 @@ function syncBackgroundSubagents(session, tasks) {
|
|
|
2835
2951
|
let changed = false;
|
|
2836
2952
|
for (const [id, running] of session.subagents) {
|
|
2837
2953
|
if (running.taskId && session.ambientSubagentTasks.has(running.taskId)) {
|
|
2954
|
+
rememberSubagent(session, running);
|
|
2838
2955
|
session.subagents.delete(id);
|
|
2839
2956
|
changed = true;
|
|
2840
2957
|
continue;
|
|
2841
2958
|
}
|
|
2842
|
-
if (!running.background || !running.taskId || liveIds.has(running.taskId)
|
|
2959
|
+
if (!running.background || !running.taskId || liveIds.has(running.taskId) || staleIds.has(running.taskId)
|
|
2843
2960
|
|| live.some((task) => task?.parent_task_id === running.taskId)) continue;
|
|
2961
|
+
// level 스냅숏은 task_started와 순서가 정해져 있지 않아 완료 증거가 아니다.
|
|
2962
|
+
rememberSubagent(session, running);
|
|
2844
2963
|
session.subagents.delete(id);
|
|
2845
2964
|
changed = true;
|
|
2846
2965
|
}
|
|
@@ -2849,6 +2968,8 @@ function syncBackgroundSubagents(session, tasks) {
|
|
|
2849
2968
|
if (!taskId) continue;
|
|
2850
2969
|
let running = findSubagent(session, taskId);
|
|
2851
2970
|
const known = session.knownSubagents.get(taskId);
|
|
2971
|
+
const runId = subagentRunId(task?.run_id);
|
|
2972
|
+
if (isStaleSubagentRun(session, taskId, runId, running)) continue;
|
|
2852
2973
|
if (!running) {
|
|
2853
2974
|
running = {
|
|
2854
2975
|
id: known?.rowId || `task:${taskId}`,
|
|
@@ -2868,6 +2989,10 @@ function syncBackgroundSubagents(session, tasks) {
|
|
|
2868
2989
|
changed = true;
|
|
2869
2990
|
}
|
|
2870
2991
|
running.background = true;
|
|
2992
|
+
if (runId && runId !== running.runId) {
|
|
2993
|
+
running.runId = runId;
|
|
2994
|
+
changed = true;
|
|
2995
|
+
}
|
|
2871
2996
|
running.lastSeenAt = Date.now();
|
|
2872
2997
|
const parentTaskId = firstLine(task?.parent_task_id || "", 80);
|
|
2873
2998
|
if (parentTaskId && parentTaskId !== running.parentTaskId) {
|
|
@@ -2882,11 +3007,19 @@ function syncBackgroundSubagents(session, tasks) {
|
|
|
2882
3007
|
running.description = description;
|
|
2883
3008
|
changed = true;
|
|
2884
3009
|
}
|
|
3010
|
+
rememberSubagent(session, running);
|
|
2885
3011
|
}
|
|
2886
3012
|
if (changed || liveIds.size) emitSubagents(session);
|
|
2887
3013
|
}
|
|
2888
3014
|
|
|
2889
3015
|
function processSubagentSystemMessage(session, message) {
|
|
3016
|
+
if (String(message.subtype || "").startsWith("task_")) {
|
|
3017
|
+
const taskId = firstLine(message.task_id || "", 80);
|
|
3018
|
+
const runId = subagentRunId(message.run_id);
|
|
3019
|
+
const running = (taskId && findSubagent(session, taskId))
|
|
3020
|
+
|| (message.tool_use_id && findSubagent(session, message.tool_use_id));
|
|
3021
|
+
if (isStaleSubagentRun(session, taskId, runId, running)) return true;
|
|
3022
|
+
}
|
|
2890
3023
|
if (message.subtype === "task_started") {
|
|
2891
3024
|
if (message.skip_transcript === true || message.ambient === true) {
|
|
2892
3025
|
const taskId = firstLine(message.task_id || "", 80);
|
|
@@ -2899,6 +3032,7 @@ function processSubagentSystemMessage(session, message) {
|
|
|
2899
3032
|
}
|
|
2900
3033
|
const running = (toolUseId && findSubagent(session, toolUseId))
|
|
2901
3034
|
|| (taskId && findSubagent(session, taskId));
|
|
3035
|
+
rememberSubagent(session, running || { taskId }, false, subagentRunId(message.run_id));
|
|
2902
3036
|
if (running) {
|
|
2903
3037
|
session.subagents.delete(running.id);
|
|
2904
3038
|
emitSubagents(session);
|
|
@@ -2927,9 +3061,15 @@ function processSubagentSystemMessage(session, message) {
|
|
|
2927
3061
|
return finishStructuredSubagent(session, message);
|
|
2928
3062
|
}
|
|
2929
3063
|
const running = taskId && findSubagent(session, taskId);
|
|
2930
|
-
|
|
3064
|
+
const runId = subagentRunId(message.run_id);
|
|
3065
|
+
if (!running) {
|
|
3066
|
+
rememberSubagent(session, { taskId }, false, runId);
|
|
3067
|
+
return false;
|
|
3068
|
+
}
|
|
3069
|
+
if (runId) running.runId = runId;
|
|
2931
3070
|
if (patch.is_backgrounded === true) running.background = true;
|
|
2932
3071
|
running.lastSeenAt = Date.now();
|
|
3072
|
+
rememberSubagent(session, running);
|
|
2933
3073
|
emitSubagents(session);
|
|
2934
3074
|
return true;
|
|
2935
3075
|
}
|
|
@@ -2985,6 +3125,7 @@ function subagentToolLabel(block) {
|
|
|
2985
3125
|
function finishSubagent(session, toolUseId) {
|
|
2986
3126
|
const running = findSubagent(session, toolUseId);
|
|
2987
3127
|
if (!running) return;
|
|
3128
|
+
rememberSubagent(session, running, true);
|
|
2988
3129
|
session.subagents.delete(running.id);
|
|
2989
3130
|
emitSubagents(session);
|
|
2990
3131
|
}
|
|
@@ -2993,6 +3134,7 @@ function finishSubagentTask(session, taskId) {
|
|
|
2993
3134
|
if (!taskId) return false;
|
|
2994
3135
|
const entry = [...session.subagents.entries()].find(([, agent]) => agent.taskId === taskId);
|
|
2995
3136
|
if (!entry) return false;
|
|
3137
|
+
rememberSubagent(session, entry[1], true);
|
|
2996
3138
|
session.subagents.delete(entry[0]);
|
|
2997
3139
|
emitSubagents(session);
|
|
2998
3140
|
return true;
|
|
@@ -3002,6 +3144,7 @@ function clearForegroundSubagents(session) {
|
|
|
3002
3144
|
let changed = false;
|
|
3003
3145
|
for (const [id, agent] of session.subagents) {
|
|
3004
3146
|
if (agent.background) continue;
|
|
3147
|
+
rememberSubagent(session, agent, true);
|
|
3005
3148
|
session.subagents.delete(id);
|
|
3006
3149
|
changed = true;
|
|
3007
3150
|
}
|
|
@@ -3129,6 +3272,7 @@ function taskNotifications(message) {
|
|
|
3129
3272
|
toolUseId: notificationTag(body, "tool-use-id"),
|
|
3130
3273
|
status: notificationTag(body, "status"),
|
|
3131
3274
|
summary: notificationTag(body, "summary"),
|
|
3275
|
+
runId: subagentRunId(message.origin?.runId),
|
|
3132
3276
|
});
|
|
3133
3277
|
}
|
|
3134
3278
|
return notifications;
|
|
@@ -3156,7 +3300,9 @@ function finishNotifiedSubagents(session, notifications) {
|
|
|
3156
3300
|
.find((agent) => agent.background && agent.name === name);
|
|
3157
3301
|
}
|
|
3158
3302
|
}
|
|
3159
|
-
if (
|
|
3303
|
+
if (isStaleSubagentRun(session, notification.taskId, notification.runId, running)) continue;
|
|
3304
|
+
rememberSubagent(session, running || { taskId: notification.taskId, runId: notification.runId }, true, notification.runId || running?.runId);
|
|
3305
|
+
if (!running || hasLiveChildren(session, running.taskId)) continue;
|
|
3160
3306
|
session.subagents.delete(running.id);
|
|
3161
3307
|
emitSubagents(session);
|
|
3162
3308
|
}
|
|
@@ -3436,7 +3582,7 @@ async function processResult(session, message) {
|
|
|
3436
3582
|
}
|
|
3437
3583
|
// Not awaited: the next run's frames queue behind this message, and the
|
|
3438
3584
|
// account lookup used to hold them back.
|
|
3439
|
-
Promise.all([safeAccount(
|
|
3585
|
+
withSessionQuery(session, (query) => Promise.all([safeAccount(query), safeUsage(query)]))
|
|
3440
3586
|
.then(([account, usage]) => notify("claude/account/updated", { threadId: session.id, account, usage }))
|
|
3441
3587
|
.catch(() => {});
|
|
3442
3588
|
if (!steerFollows) await runPendingPrompt(session);
|
|
@@ -3538,6 +3684,7 @@ function beginUntrackedTurn(session, message) {
|
|
|
3538
3684
|
}
|
|
3539
3685
|
|
|
3540
3686
|
async function consumeMessage(session, message) {
|
|
3687
|
+
if (session.closed) return;
|
|
3541
3688
|
adoptSessionId(session, message.session_id);
|
|
3542
3689
|
beginUntrackedTurn(session, message);
|
|
3543
3690
|
// A queued SDK response may beat the timer. It already resumes the work.
|
|
@@ -3712,55 +3859,60 @@ async function steerPrompt(params) {
|
|
|
3712
3859
|
}
|
|
3713
3860
|
|
|
3714
3861
|
async function runPrompt(session, params) {
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
if (
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3862
|
+
session.promptStarting = (session.promptStarting || 0) + 1;
|
|
3863
|
+
try {
|
|
3864
|
+
const id = session.id;
|
|
3865
|
+
// The session already runs this model when it started on it (a fork inherits
|
|
3866
|
+
// the parent's), and re-setting it makes the CLI probe the API again, which
|
|
3867
|
+
// fails the whole prompt when that check times out.
|
|
3868
|
+
if (params.model && visibleModel(params.model) !== session.model) {
|
|
3869
|
+
const model = stripClaudeModel(params.model);
|
|
3870
|
+
await session.query.setModel(model);
|
|
3871
|
+
session.model = visibleModel(params.model);
|
|
3872
|
+
}
|
|
3873
|
+
session.steerPending = 0;
|
|
3874
|
+
// Steering carries no effort of its own, so a steer that lands here because the
|
|
3875
|
+
// turn it meant to join is gone must keep the level the user picked.
|
|
3876
|
+
const effort = supportedEffort(
|
|
3877
|
+
modelCapabilities(session.models, params.model || session.model),
|
|
3878
|
+
params.effort ?? session.effort,
|
|
3879
|
+
);
|
|
3880
|
+
// Ultracode is xhigh plus standing workflows; a steer keeps the session's.
|
|
3881
|
+
const ultracode = (params.ultracode ?? session.ultracode) === true && effort === "xhigh";
|
|
3882
|
+
if (effort) {
|
|
3883
|
+
await session.query.applyFlagSettings({ effortLevel: effort, ultracode });
|
|
3884
|
+
}
|
|
3885
|
+
// The SDK takes the flag without complaint even where dynamic workflows are
|
|
3886
|
+
// off, so ask whether it actually took hold.
|
|
3887
|
+
if (ultracode) {
|
|
3888
|
+
const settings = await session.query.getSettings?.().catch(() => null);
|
|
3889
|
+
if (settings?.applied?.ultracode === false) notify("claude/ultracode/unavailable", { threadId: id });
|
|
3890
|
+
}
|
|
3891
|
+
session.effort = effort;
|
|
3892
|
+
session.ultracode = ultracode;
|
|
3893
|
+
// The role's read-only policy, or none: it binds this turn and the background
|
|
3894
|
+
// work it starts, until the next prompt replaces it. Steering carries no
|
|
3895
|
+
// policy of its own — it joins the turn already running — so a steer that
|
|
3896
|
+
// lands here because that turn is gone must keep the policy in force rather
|
|
3897
|
+
// than silently lifting the role's restrictions.
|
|
3898
|
+
if ("toolPolicy" in params) {
|
|
3899
|
+
session.toolPolicy = params.toolPolicy && typeof params.toolPolicy === "object"
|
|
3900
|
+
? params.toolPolicy
|
|
3901
|
+
: null;
|
|
3902
|
+
}
|
|
3903
|
+
const content = await inputContent(params.input, params.handoffContext);
|
|
3904
|
+
const turnId = beginTurn(session, params.input);
|
|
3905
|
+
session.queue.push({
|
|
3906
|
+
type: "user",
|
|
3907
|
+
message: { role: "user", content },
|
|
3908
|
+
parent_tool_use_id: null,
|
|
3909
|
+
session_id: id,
|
|
3910
|
+
origin: { kind: "human" },
|
|
3911
|
+
});
|
|
3912
|
+
return { turn: { id: turnId } };
|
|
3913
|
+
} finally {
|
|
3914
|
+
session.promptStarting -= 1;
|
|
3753
3915
|
}
|
|
3754
|
-
const content = await inputContent(params.input, params.handoffContext);
|
|
3755
|
-
const turnId = beginTurn(session, params.input);
|
|
3756
|
-
session.queue.push({
|
|
3757
|
-
type: "user",
|
|
3758
|
-
message: { role: "user", content },
|
|
3759
|
-
parent_tool_use_id: null,
|
|
3760
|
-
session_id: id,
|
|
3761
|
-
origin: { kind: "human" },
|
|
3762
|
-
});
|
|
3763
|
-
return { turn: { id: turnId } };
|
|
3764
3916
|
}
|
|
3765
3917
|
|
|
3766
3918
|
// A background task notification is an internal user message that starts its
|
|
@@ -4332,7 +4484,7 @@ async function dispatch(method, params = {}) {
|
|
|
4332
4484
|
if (!session) throw new Error("Claude 세션이 아직 시작되지 않았습니다.");
|
|
4333
4485
|
const name = String(params.name || "").trim();
|
|
4334
4486
|
if (!name) throw new Error("변경할 Claude MCP 서버 이름이 없습니다.");
|
|
4335
|
-
await session
|
|
4487
|
+
await withSessionQuery(session, (query) => query.toggleMcpServer(name, Boolean(params.enabled)));
|
|
4336
4488
|
return { effectiveEnabled: Boolean(params.enabled) };
|
|
4337
4489
|
}
|
|
4338
4490
|
if (method === "plugin/list") return claudePluginCatalog(params);
|
|
@@ -4430,20 +4582,22 @@ async function dispatch(method, params = {}) {
|
|
|
4430
4582
|
const id = liveSessionId(params.sessionId);
|
|
4431
4583
|
const existing = sessions.get(id);
|
|
4432
4584
|
if (existing) {
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4585
|
+
return withSessionQuery(existing, async (query) => {
|
|
4586
|
+
const messages = await sessionMessages(id, existing.cwd);
|
|
4587
|
+
if (!existing.tasks.size) existing.tasks = historyState(messages).tasks;
|
|
4588
|
+
restoreArtifacts(id, messages);
|
|
4589
|
+
return {
|
|
4590
|
+
id,
|
|
4591
|
+
thread: { id, turns: [] },
|
|
4592
|
+
initialTurnsPage: { data: [], nextCursor: null },
|
|
4593
|
+
cwd: existing.cwd,
|
|
4594
|
+
model: existing.model,
|
|
4595
|
+
reasoningEffort: existing.effort,
|
|
4596
|
+
account: await safeAccount(query),
|
|
4597
|
+
usage: await safeUsage(query),
|
|
4598
|
+
tokenUsage: historyTokenUsage(messages, existing.models, existing.model),
|
|
4599
|
+
};
|
|
4600
|
+
});
|
|
4447
4601
|
}
|
|
4448
4602
|
const dir = await readableCwd(id, params.cwd);
|
|
4449
4603
|
// The transcript itself decides whether there is anything to resume:
|
|
@@ -4493,7 +4647,7 @@ async function dispatch(method, params = {}) {
|
|
|
4493
4647
|
const taskId = (session && (findSubagent(session, id)?.taskId || session.rowTaskIds?.get(id)))
|
|
4494
4648
|
|| (id.startsWith("task:") ? id.slice(5) : "");
|
|
4495
4649
|
if (!session || !taskId || typeof session.query.getTaskOutput !== "function") return {};
|
|
4496
|
-
const result = await session
|
|
4650
|
+
const result = await withSessionQuery(session, (query) => query.getTaskOutput(taskId));
|
|
4497
4651
|
return { output: String(result?.output ?? "") };
|
|
4498
4652
|
}
|
|
4499
4653
|
if (method === "session/watchSubagent") {
|
|
@@ -4521,7 +4675,7 @@ async function dispatch(method, params = {}) {
|
|
|
4521
4675
|
const turn = session.turn;
|
|
4522
4676
|
turn.interruptRequested = true;
|
|
4523
4677
|
try {
|
|
4524
|
-
await session
|
|
4678
|
+
await withSessionQuery(session, (query) => query.interrupt());
|
|
4525
4679
|
} catch (error) {
|
|
4526
4680
|
if (session.turn === turn) delete turn.interruptRequested;
|
|
4527
4681
|
throw error;
|
|
@@ -4594,29 +4748,25 @@ async function dispatch(method, params = {}) {
|
|
|
4594
4748
|
if (method === "session/close") {
|
|
4595
4749
|
const session = lookupSession(params.sessionId);
|
|
4596
4750
|
if (session) {
|
|
4597
|
-
|
|
4598
|
-
clearSubagents(session);
|
|
4599
|
-
session.queue.close();
|
|
4600
|
-
session.query.close();
|
|
4601
|
-
sessions.delete(session.id);
|
|
4602
|
-
for (const [from, to] of sessionAliases) {
|
|
4603
|
-
if (to === session.id) sessionAliases.delete(from);
|
|
4604
|
-
}
|
|
4751
|
+
closeLiveSession(session);
|
|
4605
4752
|
if (params.delete) await deleteSession(session.id, { dir: session.cwd });
|
|
4606
4753
|
}
|
|
4607
4754
|
return {};
|
|
4608
4755
|
}
|
|
4756
|
+
if (method === "session/release") {
|
|
4757
|
+
const session = lookupSession(params.sessionId);
|
|
4758
|
+
if (session && sessionHasLiveWork(session)) return { released: false, busy: true };
|
|
4759
|
+
if (session) closeLiveSession(session);
|
|
4760
|
+
return { released: true, busy: false };
|
|
4761
|
+
}
|
|
4609
4762
|
if (method === "account/usage") {
|
|
4610
4763
|
const session = lookupSession(params.sessionId) || [...sessions.values()][0];
|
|
4611
4764
|
if (!session) return { account: null, usage: null };
|
|
4612
|
-
return { account: await safeAccount(
|
|
4765
|
+
return withSessionQuery(session, async (query) => ({ account: await safeAccount(query), usage: await safeUsage(query) }));
|
|
4613
4766
|
}
|
|
4614
4767
|
if (method === "shutdown") {
|
|
4615
4768
|
for (const session of sessions.values()) {
|
|
4616
|
-
|
|
4617
|
-
clearSubagents(session);
|
|
4618
|
-
session.queue.close();
|
|
4619
|
-
session.query.close();
|
|
4769
|
+
closeLiveSession(session);
|
|
4620
4770
|
}
|
|
4621
4771
|
sessions.clear();
|
|
4622
4772
|
sessionAliases.clear();
|
|
@@ -4994,6 +5144,271 @@ async function runSelfTest() {
|
|
|
4994
5144
|
else process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
|
|
4995
5145
|
await rm(configDir, { recursive: true, force: true });
|
|
4996
5146
|
}
|
|
5147
|
+
// SDK 프로세스만 mock으로 바꾸고 저장 전사와 실제 release/resume 경로를 검사한다.
|
|
5148
|
+
const releaseConfigDir = join(tmpdir(), `devez-release-self-test-${randomUUID()}`);
|
|
5149
|
+
const releaseId = randomUUID();
|
|
5150
|
+
const releaseProjectDir = join(releaseConfigDir, "projects", "release-test");
|
|
5151
|
+
const releaseTranscriptPath = join(releaseProjectDir, `${releaseId}.jsonl`);
|
|
5152
|
+
const releaseTranscript = `${JSON.stringify({
|
|
5153
|
+
type: "user", uuid: randomUUID(), sessionId: releaseId, cwd: process.cwd(),
|
|
5154
|
+
message: { role: "user", content: "해제 뒤에도 남을 대화" },
|
|
5155
|
+
})}\n`;
|
|
5156
|
+
await mkdir(releaseProjectDir, { recursive: true });
|
|
5157
|
+
await writeFile(releaseTranscriptPath, releaseTranscript);
|
|
5158
|
+
const originalStartAgentQuery = startAgentQuery;
|
|
5159
|
+
const originalEnsureExecutable = ensureClaudeExecutableChoice;
|
|
5160
|
+
const releaseQueries = [];
|
|
5161
|
+
const releaseNotices = [];
|
|
5162
|
+
const releaseStdoutWrite = process.stdout.write;
|
|
5163
|
+
let nextReleaseInitialization = null;
|
|
5164
|
+
let nextReleaseUsage = null;
|
|
5165
|
+
process.stdout.write = (chunk) => { releaseNotices.push(JSON.parse(String(chunk))); return true; };
|
|
5166
|
+
process.env.CLAUDE_CONFIG_DIR = releaseConfigDir;
|
|
5167
|
+
ensureClaudeExecutableChoice = async () => {};
|
|
5168
|
+
startAgentQuery = async (_queue, options) => {
|
|
5169
|
+
const events = new AsyncQueue();
|
|
5170
|
+
const initialization = nextReleaseInitialization;
|
|
5171
|
+
nextReleaseInitialization = null;
|
|
5172
|
+
const pendingUsage = nextReleaseUsage;
|
|
5173
|
+
nextReleaseUsage = null;
|
|
5174
|
+
const query = {
|
|
5175
|
+
events, options, closeCalls: 0, finishOnClose: releaseQueries.length > 0,
|
|
5176
|
+
async initializationResult() { return initialization ? await initialization : { models: [], account: {} }; },
|
|
5177
|
+
async setPermissionMode() {},
|
|
5178
|
+
async accountInfo() { return {}; },
|
|
5179
|
+
async usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET() { return pendingUsage ? await pendingUsage : {}; },
|
|
5180
|
+
close() { this.closeCalls += 1; if (this.finishOnClose) events.close(); },
|
|
5181
|
+
[Symbol.asyncIterator]() {
|
|
5182
|
+
const iterator = events[Symbol.asyncIterator]();
|
|
5183
|
+
return {
|
|
5184
|
+
next: async () => {
|
|
5185
|
+
const step = await iterator.next();
|
|
5186
|
+
if (this.iteratorError) throw this.iteratorError;
|
|
5187
|
+
// 실제 SDK도 close 이후 for-await를 정상 완료 대신 AbortError로 끝낼 수 있다.
|
|
5188
|
+
if (step.done && this.closeCalls) {
|
|
5189
|
+
throw Object.assign(new Error("mock intended query close"), { name: "AbortError" });
|
|
5190
|
+
}
|
|
5191
|
+
return step;
|
|
5192
|
+
},
|
|
5193
|
+
};
|
|
5194
|
+
},
|
|
5195
|
+
};
|
|
5196
|
+
releaseQueries.push(query);
|
|
5197
|
+
return query;
|
|
5198
|
+
};
|
|
5199
|
+
try {
|
|
5200
|
+
const { session } = await createSession({ cwd: process.cwd() }, releaseId);
|
|
5201
|
+
const oldConsumer = session.consumer;
|
|
5202
|
+
const oldQueue = session.queue;
|
|
5203
|
+
const oldQuery = session.query;
|
|
5204
|
+
const assertBusy = async () => {
|
|
5205
|
+
const reply = await dispatch("session/release", { sessionId: releaseId });
|
|
5206
|
+
if (reply.released !== false || reply.busy !== true || lookupSession(releaseId) !== session
|
|
5207
|
+
|| oldQuery.closeCalls || oldQueue.closed) throw new Error("Claude release terminated live work");
|
|
5208
|
+
};
|
|
5209
|
+
for (const key of ["initializing", "promptStarting", "steerPending", "automaticTurnsPending", "usageLimitWait", "turn"]) {
|
|
5210
|
+
session[key] = key === "turn" ? { id: "busy-turn" } : 1;
|
|
5211
|
+
await assertBusy();
|
|
5212
|
+
session[key] = null;
|
|
5213
|
+
}
|
|
5214
|
+
session.pendingPrompts.push({ input: [] });
|
|
5215
|
+
await assertBusy();
|
|
5216
|
+
session.pendingPrompts.length = 0;
|
|
5217
|
+
oldQueue.push({ type: "user" });
|
|
5218
|
+
await assertBusy();
|
|
5219
|
+
oldQueue.values.length = 0;
|
|
5220
|
+
session.subagents.set("busy-child", { id: "busy-child", background: true });
|
|
5221
|
+
await assertBusy();
|
|
5222
|
+
session.subagents.clear();
|
|
5223
|
+
for (const hidden of [session.hiddenSubagentTasks, session.ambientSubagentTasks]) {
|
|
5224
|
+
hidden.add("busy-hidden-child");
|
|
5225
|
+
await assertBusy();
|
|
5226
|
+
hidden.clear();
|
|
5227
|
+
}
|
|
5228
|
+
const question = hostRequest("item/tool/requestUserInput", {}, undefined, releaseId);
|
|
5229
|
+
await assertBusy();
|
|
5230
|
+
const [questionId, request] = [...pendingHostRequests].find(([, pending]) => pending.session === session);
|
|
5231
|
+
pendingHostRequests.delete(questionId);
|
|
5232
|
+
request.resolve({ answers: {} });
|
|
5233
|
+
await question;
|
|
5234
|
+
watchSubagent(session, "untracked-child");
|
|
5235
|
+
sessionAliases.set("release-old-alias", "release-middle-alias");
|
|
5236
|
+
sessionAliases.set("release-middle-alias", releaseId);
|
|
5237
|
+
const reply = await dispatch("session/release", { sessionId: "claude:release-old-alias" });
|
|
5238
|
+
const repeated = await dispatch("session/release", { sessionId: releaseId });
|
|
5239
|
+
if (!reply.released || reply.busy || !repeated.released || repeated.busy || sessions.has(releaseId)
|
|
5240
|
+
|| oldQuery.closeCalls !== 1 || !oldQueue.closed || session.query || session.queue || session.subagentWatch
|
|
5241
|
+
|| sessionAliases.has("release-old-alias") || sessionAliases.has("release-middle-alias")
|
|
5242
|
+
|| await readFile(releaseTranscriptPath, "utf8") !== releaseTranscript) {
|
|
5243
|
+
throw new Error("Claude idle release did not clean live references while preserving its transcript");
|
|
5244
|
+
}
|
|
5245
|
+
const resumed = await dispatch("session/resume", { sessionId: releaseId, cwd: process.cwd() });
|
|
5246
|
+
const resumedSession = lookupSession(releaseId);
|
|
5247
|
+
if (resumed.id !== releaseId || !resumedSession || resumedSession === session
|
|
5248
|
+
|| resumedSession.query.options.resume !== releaseId
|
|
5249
|
+
|| !historyTurns(await sessionMessages(releaseId, process.cwd()))
|
|
5250
|
+
.some((turn) => turn.items.some((item) => item.content?.[0]?.text === "해제 뒤에도 남을 대화"))) {
|
|
5251
|
+
throw new Error("Claude released session could not resume its saved transcript");
|
|
5252
|
+
}
|
|
5253
|
+
oldQuery.events.push({ type: "system", subtype: "task_started", session_id: "stale-session-id", task_id: "stale-task" });
|
|
5254
|
+
oldQuery.events.push({ type: "system", subtype: "task_started", session_id: releaseId,
|
|
5255
|
+
task_id: "stale-task", task_type: "local_agent", subagent_type: "Explore", is_backgrounded: true });
|
|
5256
|
+
oldQuery.events.close();
|
|
5257
|
+
await oldConsumer;
|
|
5258
|
+
if (lookupSession(releaseId) !== resumedSession || resumedSession.query.closeCalls || session.subagents.size
|
|
5259
|
+
|| sessions.has("stale-session-id")) throw new Error("Claude old consumer removed a resumed same-id query");
|
|
5260
|
+
const resumedConsumer = resumedSession.consumer;
|
|
5261
|
+
await dispatch("session/release", { sessionId: releaseId });
|
|
5262
|
+
await resumedConsumer;
|
|
5263
|
+
const { session: mainSession } = await createSession({ cwd: process.cwd() });
|
|
5264
|
+
const mainQuery = mainSession.query;
|
|
5265
|
+
const mainConsumer = mainSession.consumer;
|
|
5266
|
+
let finishMetadata;
|
|
5267
|
+
nextReleaseUsage = new Promise((resolve) => { finishMetadata = resolve; });
|
|
5268
|
+
const metadataCreate = createSession({ cwd: process.cwd() });
|
|
5269
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
5270
|
+
const metadataSession = [...sessions.values()].find((candidate) => candidate !== mainSession);
|
|
5271
|
+
const metadataConsumer = metadataSession?.consumer;
|
|
5272
|
+
if (!metadataSession?.query || !metadataConsumer) throw new Error("Claude metadata-wait fixture did not start");
|
|
5273
|
+
metadataSession.query.events.close();
|
|
5274
|
+
await metadataConsumer;
|
|
5275
|
+
finishMetadata({});
|
|
5276
|
+
let metadataFailed = false;
|
|
5277
|
+
try { await metadataCreate; } catch { metadataFailed = true; }
|
|
5278
|
+
if (!metadataFailed || lookupSession(mainSession.id) !== mainSession || mainQuery.closeCalls) {
|
|
5279
|
+
throw new Error("Claude closed initializing query returned success or pruned the previous query");
|
|
5280
|
+
}
|
|
5281
|
+
nextReleaseInitialization = Promise.reject(new Error("mock initialization failure"));
|
|
5282
|
+
let initializationFailed = false;
|
|
5283
|
+
try { await createSession({ cwd: process.cwd() }); } catch { initializationFailed = true; }
|
|
5284
|
+
if (!initializationFailed || lookupSession(mainSession.id) !== mainSession || mainQuery.closeCalls) {
|
|
5285
|
+
throw new Error("Claude failed new query closed the previous idle query");
|
|
5286
|
+
}
|
|
5287
|
+
let finishInitialization;
|
|
5288
|
+
nextReleaseInitialization = new Promise((resolve) => { finishInitialization = resolve; });
|
|
5289
|
+
const pendingCreate = createSession({ cwd: process.cwd() });
|
|
5290
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
5291
|
+
const initializingSession = [...sessions.values()].find((candidate) => candidate !== mainSession);
|
|
5292
|
+
if (!initializingSession?.initializing || mainQuery.closeCalls) {
|
|
5293
|
+
throw new Error("Claude initializing query prematurely closed the previous idle query");
|
|
5294
|
+
}
|
|
5295
|
+
const initializingQuery = initializingSession.query;
|
|
5296
|
+
const { session: readySession } = await createSession({ cwd: process.cwd() });
|
|
5297
|
+
await mainConsumer;
|
|
5298
|
+
if (!mainQuery.closeCalls || lookupSession(mainSession.id) || !initializingSession.initializing
|
|
5299
|
+
|| initializingQuery.closeCalls) throw new Error("Claude ready query did not preserve another initializing query");
|
|
5300
|
+
readySession.turn = { id: "main-active-turn" };
|
|
5301
|
+
finishInitialization({ models: [], account: {} });
|
|
5302
|
+
const { session: btwSession } = await pendingCreate;
|
|
5303
|
+
if (lookupSession(readySession.id) !== readySession || readySession.query.closeCalls
|
|
5304
|
+
|| lookupSession(btwSession.id) !== btwSession) throw new Error("Claude auxiliary query closed its working parent");
|
|
5305
|
+
readySession.turn = null;
|
|
5306
|
+
const readyConsumer = readySession.consumer;
|
|
5307
|
+
const btwConsumer = btwSession.consumer;
|
|
5308
|
+
const { session: newestSession } = await createSession({ cwd: process.cwd() });
|
|
5309
|
+
await Promise.all([readyConsumer, btwConsumer]);
|
|
5310
|
+
if (lookupSession(readySession.id) || lookupSession(btwSession.id) || sessions.size !== 1) {
|
|
5311
|
+
throw new Error("Claude successful new query retained idle main or auxiliary queries");
|
|
5312
|
+
}
|
|
5313
|
+
const newestConsumer = newestSession.consumer;
|
|
5314
|
+
await dispatch("session/release", { sessionId: newestSession.id });
|
|
5315
|
+
await newestConsumer;
|
|
5316
|
+
if (releaseNotices.some((notice) => notice.method === "error"
|
|
5317
|
+
|| notice.method === "turn/subagents/updated")) {
|
|
5318
|
+
throw new Error("Claude intended release or idle pruning emitted an error or revived a subagent row");
|
|
5319
|
+
}
|
|
5320
|
+
// 명시적으로 닫지 않은 실제 연결 실패는 기존 오류 통지를 유지한다.
|
|
5321
|
+
const { session: failedSession } = await createSession({ cwd: process.cwd() });
|
|
5322
|
+
const failedConsumer = failedSession.consumer;
|
|
5323
|
+
failedSession.query.iteratorError = new Error("mock unexpected query failure");
|
|
5324
|
+
failedSession.query.events.close();
|
|
5325
|
+
await failedConsumer;
|
|
5326
|
+
const unexpectedErrors = releaseNotices.filter((notice) => notice.method === "error");
|
|
5327
|
+
if (unexpectedErrors.length !== 1 || unexpectedErrors[0].params.threadId !== failedSession.id
|
|
5328
|
+
|| unexpectedErrors[0].params.error.message !== "mock unexpected query failure") {
|
|
5329
|
+
throw new Error("Claude unexpected open-query failure lost its error notification");
|
|
5330
|
+
}
|
|
5331
|
+
// 메인 MCP 요청처럼 턴 밖에서 await하는 실제 SDK 제어·조회 함수도 보호한다.
|
|
5332
|
+
for (const kind of ["reconnect", "status", "toggle", "permission", "account", "skills", "output", "resume"]) {
|
|
5333
|
+
const { session: requestSession } = await createSession({ cwd: process.cwd() });
|
|
5334
|
+
const requestQuery = requestSession.query;
|
|
5335
|
+
const requestConsumer = requestSession.consumer;
|
|
5336
|
+
let finishRequest;
|
|
5337
|
+
const pendingResult = new Promise((resolve) => { finishRequest = resolve; });
|
|
5338
|
+
let requestPromise;
|
|
5339
|
+
let result;
|
|
5340
|
+
if (kind === "reconnect" || kind === "status") {
|
|
5341
|
+
result = [{ name: "connected", status: "connected" }];
|
|
5342
|
+
requestQuery.mcpServerStatus = () => pendingResult;
|
|
5343
|
+
requestQuery.reconnectMcpServer = async () => {};
|
|
5344
|
+
requestPromise = kind === "reconnect"
|
|
5345
|
+
? reconnectClaudeMcp(requestSession, "")
|
|
5346
|
+
: dispatch("mcpServerStatus/list", { sessionId: requestSession.id });
|
|
5347
|
+
} else if (kind === "toggle") {
|
|
5348
|
+
requestQuery.toggleMcpServer = () => pendingResult;
|
|
5349
|
+
requestPromise = dispatch("mcp/toggle", { sessionId: requestSession.id, name: "connected", enabled: true });
|
|
5350
|
+
} else if (kind === "permission") {
|
|
5351
|
+
requestSession.permissionModeVerified = false;
|
|
5352
|
+
requestQuery.setPermissionMode = () => pendingResult;
|
|
5353
|
+
requestPromise = applyPermissionMode(requestSession);
|
|
5354
|
+
} else if (kind === "account" || kind === "resume") {
|
|
5355
|
+
result = {};
|
|
5356
|
+
requestQuery.accountInfo = () => pendingResult;
|
|
5357
|
+
requestPromise = dispatch(kind === "account" ? "account/usage" : "session/resume", { sessionId: requestSession.id });
|
|
5358
|
+
} else if (kind === "skills") {
|
|
5359
|
+
result = [];
|
|
5360
|
+
requestQuery.supportedCommands = () => pendingResult;
|
|
5361
|
+
requestPromise = supportedClaudeSkills({ cwd: requestSession.cwd, forceReload: true });
|
|
5362
|
+
} else {
|
|
5363
|
+
result = { output: "saved task output" };
|
|
5364
|
+
requestSession.rowTaskIds = new Map([["finished-row", "finished-task"]]);
|
|
5365
|
+
requestQuery.getTaskOutput = () => pendingResult;
|
|
5366
|
+
requestPromise = dispatch("session/taskOutput", { sessionId: requestSession.id, id: "finished-row" });
|
|
5367
|
+
}
|
|
5368
|
+
const blockedRelease = await dispatch("session/release", { sessionId: requestSession.id });
|
|
5369
|
+
const { session: competingSession } = await createSession({ cwd: process.cwd() });
|
|
5370
|
+
if (!blockedRelease.busy || blockedRelease.released || !requestSession.queryRequests
|
|
5371
|
+
|| lookupSession(requestSession.id) !== requestSession || requestQuery.closeCalls) {
|
|
5372
|
+
throw new Error(`Claude active SDK ${kind} request was treated as idle`);
|
|
5373
|
+
}
|
|
5374
|
+
finishRequest(result);
|
|
5375
|
+
await requestPromise;
|
|
5376
|
+
if (requestSession.queryRequests || sessionHasLiveWork(requestSession)) {
|
|
5377
|
+
throw new Error(`Claude completed SDK ${kind} request retained its busy guard`);
|
|
5378
|
+
}
|
|
5379
|
+
if (kind === "reconnect") {
|
|
5380
|
+
requestQuery.reconnectMcpServer = async () => { throw new Error("mock reconnect failure"); };
|
|
5381
|
+
let failedRequest = false;
|
|
5382
|
+
try { await reconnectClaudeMcp(requestSession, "connected"); } catch { failedRequest = true; }
|
|
5383
|
+
if (!failedRequest || requestSession.queryRequests || sessionHasLiveWork(requestSession)) {
|
|
5384
|
+
throw new Error("Claude rejected SDK request retained its busy guard");
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
const { session: afterRequestSession } = await createSession({ cwd: process.cwd() });
|
|
5388
|
+
await requestConsumer;
|
|
5389
|
+
if (!requestQuery.closeCalls || lookupSession(requestSession.id) || lookupSession(competingSession.id)) {
|
|
5390
|
+
throw new Error(`Claude completed SDK ${kind} request prevented later idle pruning`);
|
|
5391
|
+
}
|
|
5392
|
+
const afterRequestConsumer = afterRequestSession.consumer;
|
|
5393
|
+
await dispatch("session/release", { sessionId: afterRequestSession.id });
|
|
5394
|
+
await afterRequestConsumer;
|
|
5395
|
+
}
|
|
5396
|
+
if (releaseNotices.filter((notice) => notice.method === "error").length !== 1) {
|
|
5397
|
+
throw new Error("Claude SDK request pruning emitted a false close error");
|
|
5398
|
+
}
|
|
5399
|
+
} finally {
|
|
5400
|
+
const live = lookupSession(releaseId);
|
|
5401
|
+
if (live) closeLiveSession(live);
|
|
5402
|
+
for (const query of releaseQueries) query.events.close();
|
|
5403
|
+
startAgentQuery = originalStartAgentQuery;
|
|
5404
|
+
ensureClaudeExecutableChoice = originalEnsureExecutable;
|
|
5405
|
+
if (previousConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
5406
|
+
else process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
|
|
5407
|
+
transcriptPaths.delete(releaseId);
|
|
5408
|
+
transcriptCwds.delete(releaseId);
|
|
5409
|
+
process.stdout.write = releaseStdoutWrite;
|
|
5410
|
+
await rm(releaseConfigDir, { recursive: true, force: true });
|
|
5411
|
+
}
|
|
4997
5412
|
// A resumed transcript lists its Artifact pages once each at their latest
|
|
4998
5413
|
// publish, read through the file's toolUseResult spelling.
|
|
4999
5414
|
const artifactTranscript = [
|
|
@@ -5050,14 +5465,14 @@ async function runSelfTest() {
|
|
|
5050
5465
|
|| CLAUDE_TASK_TOOLS.some((tool) => !latestOptions.allowedTools.includes(tool))) {
|
|
5051
5466
|
throw new Error(`Claude latest SDK options self-test failed: ${JSON.stringify(latestOptions.allowedTools)}`);
|
|
5052
5467
|
}
|
|
5053
|
-
const
|
|
5468
|
+
const taskLookupSession = {
|
|
5054
5469
|
turn: { id: "task-get-turn" },
|
|
5055
5470
|
tools: new Map(),
|
|
5056
5471
|
tasks: new Map(),
|
|
5057
5472
|
planCreatePending: false,
|
|
5058
5473
|
};
|
|
5059
|
-
processToolUse(
|
|
5060
|
-
if (
|
|
5474
|
+
processToolUse(taskLookupSession, { id: "task-get", name: "TaskGet", input: { taskId: "1" } });
|
|
5475
|
+
if (taskLookupSession.tools.get("task-get")?.suppressed !== true) {
|
|
5061
5476
|
throw new Error("Claude TaskGet transcript suppression self-test failed");
|
|
5062
5477
|
}
|
|
5063
5478
|
const pluginCatalog = buildClaudePluginCatalog(
|
|
@@ -6010,8 +6425,10 @@ async function runSelfTest() {
|
|
|
6010
6425
|
message: { content: [{ type: "tool_result", tool_use_id: "toolu_2", content: "resumed" }] },
|
|
6011
6426
|
tool_use_result: { success: true, resumedAgentId: "agent-1" },
|
|
6012
6427
|
});
|
|
6013
|
-
|
|
6014
|
-
|
|
6428
|
+
// 재개된 줄은 끝난 줄과 같은 id(처음 Agent 호출 id)로 돌아오고, SendMessage 호출 id로도 찾힌다.
|
|
6429
|
+
const resumed = lifecycleSession.subagents.get("toolu_1");
|
|
6430
|
+
if (!resumed?.background || resumed.taskId !== "agent-1" || resumed.name !== "Explore"
|
|
6431
|
+
|| findSubagent(lifecycleSession, "toolu_2") !== resumed) {
|
|
6015
6432
|
throw new Error(`Claude resumed subagent self-test failed: ${JSON.stringify(resumed)}`);
|
|
6016
6433
|
}
|
|
6017
6434
|
finishTurn(lifecycleSession, null, 1);
|
|
@@ -6161,36 +6578,164 @@ async function runSelfTest() {
|
|
|
6161
6578
|
}
|
|
6162
6579
|
clearSubagents(nestedSession);
|
|
6163
6580
|
// 자기 백그라운드 명령을 기다리며 멈춘 에이전트는 그 명령이 도는 동안 행을 지킨다.
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
subtype: "task_started",
|
|
6167
|
-
task_id: "agent-wait",
|
|
6168
|
-
tool_use_id: "toolu_wait",
|
|
6169
|
-
task_type: "local_agent",
|
|
6170
|
-
subagent_type: "general-purpose",
|
|
6171
|
-
});
|
|
6172
|
-
nestedSession.subagents.get("toolu_wait").background = true;
|
|
6581
|
+
// SDK 순서(접수 결과, task_started, 스냅숏, task_updated·task_notification, 스냅숏,
|
|
6582
|
+
// XML 통지)를 그대로 재생하고, 다시 실행된 뒤 늦게 온 첫 실행 통지도 무시한다.
|
|
6173
6583
|
const waitSnapshot = (tasks) => processSubagentSystemMessage(nestedSession, {
|
|
6174
6584
|
type: "system",
|
|
6175
6585
|
subtype: "background_tasks_changed",
|
|
6176
6586
|
tasks,
|
|
6177
6587
|
});
|
|
6178
|
-
|
|
6179
|
-
processSubagentSystemMessage(nestedSession, {
|
|
6588
|
+
const waitStructured = (subtype, extra) => processSubagentSystemMessage(nestedSession, {
|
|
6180
6589
|
type: "system",
|
|
6181
|
-
subtype
|
|
6590
|
+
subtype,
|
|
6182
6591
|
task_id: "agent-wait",
|
|
6183
|
-
|
|
6592
|
+
...extra,
|
|
6184
6593
|
});
|
|
6594
|
+
const waitXml = (runId) => finishNotifiedSubagents(nestedSession, taskNotifications({
|
|
6595
|
+
origin: { kind: "task-notification", runId },
|
|
6596
|
+
message: { content: [{ type: "text", text: "<task-notification><task-id>agent-wait</task-id><status>completed</status><summary>Agent \"general-purpose\" finished</summary></task-notification>" }] },
|
|
6597
|
+
}));
|
|
6598
|
+
const agentRow = { task_id: "agent-wait", task_type: "local_agent", description: "wait" };
|
|
6599
|
+
const shellRow = { task_id: "shell-wait", task_type: "local_bash", description: "sleep 300", parent_task_id: "agent-wait" };
|
|
6600
|
+
startSubagent(nestedSession, { id: "toolu_wait", input: { description: "대기", subagent_type: "general-purpose" } });
|
|
6601
|
+
keepBackgroundSubagent(nestedSession, "toolu_wait", { isAsync: true, agentId: "agent-wait" });
|
|
6602
|
+
waitStructured("task_started", { tool_use_id: "toolu_wait", task_type: "local_agent", is_backgrounded: true, run_id: "r1" });
|
|
6603
|
+
waitSnapshot([agentRow, shellRow]);
|
|
6604
|
+
waitStructured("task_updated", { patch: { status: "completed" } });
|
|
6605
|
+
waitStructured("task_notification", { status: "completed", run_id: "r1" });
|
|
6606
|
+
waitSnapshot([shellRow]);
|
|
6607
|
+
waitXml("r1");
|
|
6185
6608
|
const waitingKept = nestedSession.subagents.has("toolu_wait");
|
|
6186
6609
|
waitSnapshot([]);
|
|
6187
|
-
|
|
6188
|
-
|
|
6610
|
+
const settledAfterChild = !nestedSession.subagents.has("toolu_wait");
|
|
6611
|
+
waitStructured("task_started", { task_type: "local_agent", is_backgrounded: true, run_id: "r2" });
|
|
6612
|
+
waitXml("r1");
|
|
6613
|
+
const keptAgainstLate = nestedSession.subagents.has("toolu_wait");
|
|
6614
|
+
waitStructured("task_notification", { status: "completed", run_id: "r2" });
|
|
6615
|
+
if (!waitingKept || !settledAfterChild || !keptAgainstLate || nestedSession.subagents.size !== 0) {
|
|
6616
|
+
throw new Error(`Claude waiting subagent self-test failed: ${waitingKept} ${settledAfterChild} ${keptAgainstLate} ${nestedSession.subagents.size}`);
|
|
6189
6617
|
}
|
|
6190
6618
|
clearSubagents(nestedSession);
|
|
6619
|
+
// 완료된 실행의 지연 프레임과 새 실행의 스냅숏을 함께 재생한다.
|
|
6620
|
+
const lateSession = {
|
|
6621
|
+
id: "late-run-self-test", turn: null, subagents: new Map(), knownSubagents: new Map(),
|
|
6622
|
+
hiddenSubagentTasks: new Set(), ambientSubagentTasks: new Set(),
|
|
6623
|
+
};
|
|
6624
|
+
const lateEvent = (subtype, runId, extra = {}) => processSubagentSystemMessage(lateSession, {
|
|
6625
|
+
type: "system", subtype, task_id: "agent-late", run_id: runId,
|
|
6626
|
+
task_type: "local_agent", subagent_type: "Explore", is_backgrounded: true, ...extra,
|
|
6627
|
+
});
|
|
6628
|
+
const lateSnapshot = (runId) => syncBackgroundSubagents(lateSession, [{
|
|
6629
|
+
task_id: "agent-late", run_id: runId, task_type: "local_agent",
|
|
6630
|
+
}]);
|
|
6631
|
+
const lateXml = (runId) => finishNotifiedSubagents(lateSession, [{
|
|
6632
|
+
taskId: "agent-late", runId, summary: 'Agent "Explore" finished',
|
|
6633
|
+
}]);
|
|
6634
|
+
try {
|
|
6635
|
+
lateEvent("task_started", "001", { tool_use_id: "toolu_late" });
|
|
6636
|
+
lateEvent("task_notification", "001", { status: "completed" });
|
|
6637
|
+
lateEvent("task_started", "002", { tool_use_id: "toolu_latest" });
|
|
6638
|
+
lateEvent("task_progress", "002", { summary: "최신 진행", last_tool_name: "Read" });
|
|
6639
|
+
const beforeLate = JSON.stringify([...lateSession.subagents]);
|
|
6640
|
+
lateEvent("task_progress", "001", { tool_use_id: "toolu_old", summary: "예전 진행", last_tool_name: "Write" });
|
|
6641
|
+
lateEvent("task_updated", "001", { patch: { is_backgrounded: true } });
|
|
6642
|
+
lateEvent("task_started", "001", { tool_use_id: "toolu_old", skip_transcript: true });
|
|
6643
|
+
lateSnapshot("001");
|
|
6644
|
+
syncBackgroundSubagents(lateSession, [{
|
|
6645
|
+
task_id: "agent-late", run_id: "001", task_type: "local_agent", ambient: true,
|
|
6646
|
+
}]);
|
|
6647
|
+
if (!sessionHasLiveWork(lateSession)) {
|
|
6648
|
+
throw new Error("Claude stale ambient snapshot made a newer task idle");
|
|
6649
|
+
}
|
|
6650
|
+
lateXml("001");
|
|
6651
|
+
lateEvent("task_notification", "001", { status: "completed" });
|
|
6652
|
+
if (JSON.stringify([...lateSession.subagents]) !== beforeLate) {
|
|
6653
|
+
throw new Error("Claude late run modified a newer subagent row");
|
|
6654
|
+
}
|
|
6655
|
+
lateEvent("task_notification", "002", { status: "completed" });
|
|
6656
|
+
for (const runId of ["001", "002"]) {
|
|
6657
|
+
lateEvent("task_progress", runId, { summary: "끝난 진행" });
|
|
6658
|
+
lateEvent("task_started", runId);
|
|
6659
|
+
lateSnapshot(runId);
|
|
6660
|
+
}
|
|
6661
|
+
if (lateSession.subagents.size) throw new Error("Claude late run resurrected a completed subagent");
|
|
6662
|
+
lateSnapshot("003");
|
|
6663
|
+
const fromSnapshot = findSubagent(lateSession, "agent-late");
|
|
6664
|
+
lateEvent("task_notification", "002", { status: "completed" });
|
|
6665
|
+
lateXml("002");
|
|
6666
|
+
if (!fromSnapshot || fromSnapshot.runId !== "003" || lateSession.subagents.size !== 1) {
|
|
6667
|
+
throw new Error("Claude late completion removed a newer snapshot run");
|
|
6668
|
+
}
|
|
6669
|
+
startSubagent(lateSession, { id: "toolu_merge", input: { name: "Explore" } });
|
|
6670
|
+
keepBackgroundSubagent(lateSession, "toolu_merge", { isAsync: true, agentId: "agent-late" });
|
|
6671
|
+
lateXml("002");
|
|
6672
|
+
if (lateSession.subagents.size !== 1 || findSubagent(lateSession, "agent-late")?.runId !== "003") {
|
|
6673
|
+
throw new Error("Claude placeholder merge forgot its newer run");
|
|
6674
|
+
}
|
|
6675
|
+
lateEvent("task_notification", "003", { status: "completed" });
|
|
6676
|
+
resumeBackgroundSubagent(lateSession, "toolu_send_late", {}, { resumedAgentId: "agent-late" });
|
|
6677
|
+
lateXml("003");
|
|
6678
|
+
lateEvent("task_progress", "003", { summary: "재개 전 진행" });
|
|
6679
|
+
lateEvent("task_started", "004", { tool_use_id: "toolu_send_late" });
|
|
6680
|
+
if (lateSession.subagents.size !== 1 || findSubagent(lateSession, "agent-late")?.runId !== "004") {
|
|
6681
|
+
throw new Error("Claude latest resumed run failed after a completed run");
|
|
6682
|
+
}
|
|
6683
|
+
lateXml("004");
|
|
6684
|
+
if (lateSession.subagents.size) throw new Error("Claude latest resumed run did not finish");
|
|
6685
|
+
// level/edge 순서가 뒤집혀 빈 스냅숏이 먼저 와도 같은 실행의 정상 진행은 복구된다.
|
|
6686
|
+
lateEvent("task_started", "005");
|
|
6687
|
+
syncBackgroundSubagents(lateSession, []);
|
|
6688
|
+
lateEvent("task_progress", "005", { summary: "스냅숏 뒤 정상 진행" });
|
|
6689
|
+
if (findSubagent(lateSession, "agent-late")?.description !== "스냅숏 뒤 정상 진행") {
|
|
6690
|
+
throw new Error("Claude empty snapshot blocked the current run's progress");
|
|
6691
|
+
}
|
|
6692
|
+
syncBackgroundSubagents(lateSession, []);
|
|
6693
|
+
lateSnapshot("005");
|
|
6694
|
+
if (findSubagent(lateSession, "agent-late")?.runId !== "005") {
|
|
6695
|
+
throw new Error("Claude empty snapshot blocked the current run's snapshot recovery");
|
|
6696
|
+
}
|
|
6697
|
+
lateEvent("task_notification", "005", { status: "completed" });
|
|
6698
|
+
lateEvent("task_progress", "005", { summary: "완료 뒤 지연 진행" });
|
|
6699
|
+
if (lateSession.subagents.size) throw new Error("Claude authoritative completion lost its tombstone");
|
|
6700
|
+
// 실행 식별자는 표시용 글자가 아니므로 길이·줄바꿈을 바꾸지 않는다.
|
|
6701
|
+
const longPrefix = "x".repeat(80);
|
|
6702
|
+
const firstLongRun = `${longPrefix}a`;
|
|
6703
|
+
const nextLongRun = `${longPrefix}b\nnext`;
|
|
6704
|
+
lateEvent("task_started", firstLongRun);
|
|
6705
|
+
lateXml(firstLongRun);
|
|
6706
|
+
lateEvent("task_started", nextLongRun);
|
|
6707
|
+
lateSnapshot(nextLongRun);
|
|
6708
|
+
if (findSubagent(lateSession, "agent-late")?.runId !== nextLongRun) {
|
|
6709
|
+
throw new Error("Claude truncated an opaque run identifier");
|
|
6710
|
+
}
|
|
6711
|
+
lateXml(firstLongRun);
|
|
6712
|
+
if (!sessionHasLiveWork(lateSession)) throw new Error("Claude older long run removed its successor");
|
|
6713
|
+
lateXml(nextLongRun);
|
|
6714
|
+
if (lateSession.subagents.size) throw new Error("Claude latest long run did not finish");
|
|
6715
|
+
syncBackgroundSubagents(lateSession, [{
|
|
6716
|
+
task_id: "agent-late", run_id: `${longPrefix}c`, task_type: "local_agent", ambient: true,
|
|
6717
|
+
}]);
|
|
6718
|
+
syncBackgroundSubagents(lateSession, [{
|
|
6719
|
+
task_id: "agent-late", run_id: firstLongRun, task_type: "local_agent", ambient: true,
|
|
6720
|
+
}]);
|
|
6721
|
+
if (!lateSession.ambientSubagentTasks.has("agent-late") || !sessionHasLiveWork(lateSession)) {
|
|
6722
|
+
throw new Error("Claude stale ambient snapshot erased a newer ambient task");
|
|
6723
|
+
}
|
|
6724
|
+
} finally {
|
|
6725
|
+
clearSubagents(lateSession);
|
|
6726
|
+
}
|
|
6191
6727
|
// 끝난 뒤 SDK가 같은 작업을 다시 돌리면 설명 없는 새 줄이 아니라 같은 줄로 돌아온다.
|
|
6192
6728
|
startSubagent(nestedSession, { id: "toolu_again", input: { description: "3분 대기", subagent_type: "general-purpose" } });
|
|
6193
6729
|
keepBackgroundSubagent(nestedSession, "toolu_again", { isAsync: true, agentId: "agent-again" });
|
|
6730
|
+
// 진행 요약이 설명을 바꿔도 다시 실행된 줄은 호출 설명으로 돌아온다.
|
|
6731
|
+
for (let tick = 0; tick < 2; tick += 1) {
|
|
6732
|
+
processSubagentSystemMessage(nestedSession, {
|
|
6733
|
+
type: "system",
|
|
6734
|
+
subtype: "task_progress",
|
|
6735
|
+
task_id: "agent-again",
|
|
6736
|
+
summary: "로그를 살피는 중",
|
|
6737
|
+
});
|
|
6738
|
+
}
|
|
6194
6739
|
waitSnapshot([]);
|
|
6195
6740
|
processSubagentSystemMessage(nestedSession, {
|
|
6196
6741
|
type: "system",
|
|
@@ -6642,6 +7187,8 @@ async function runSelfTest() {
|
|
|
6642
7187
|
throw new Error("Claude idle background subagent self-test failed");
|
|
6643
7188
|
}
|
|
6644
7189
|
structuredSession.subagents.clear();
|
|
7190
|
+
// 턴 결과가 분리 실행한 계정 조회를 이 캡처 안에서 끝내 다음 표시 검사와 섞이지 않게 한다.
|
|
7191
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
6645
7192
|
} finally {
|
|
6646
7193
|
process.stdout.write = stdoutWrite;
|
|
6647
7194
|
}
|
|
@@ -6977,8 +7524,6 @@ lines.on("line", async (line) => {
|
|
|
6977
7524
|
|
|
6978
7525
|
lines.on("close", () => {
|
|
6979
7526
|
for (const session of sessions.values()) {
|
|
6980
|
-
|
|
6981
|
-
clearSubagents(session);
|
|
6982
|
-
session.query.close();
|
|
7527
|
+
closeLiveSession(session);
|
|
6983
7528
|
}
|
|
6984
7529
|
});
|