devez-vibe 1.9.59 → 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 +649 -141
- 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
|
|
|
@@ -2700,6 +2755,7 @@ function resumeBackgroundSubagent(session, toolUseId, pending, result) {
|
|
|
2700
2755
|
};
|
|
2701
2756
|
session.subagents.set(running.id, running);
|
|
2702
2757
|
session.knownSubagents.set(taskId, {
|
|
2758
|
+
...known,
|
|
2703
2759
|
rowId: running.id,
|
|
2704
2760
|
name: running.name,
|
|
2705
2761
|
description: running.description,
|
|
@@ -2742,6 +2798,8 @@ function upsertStructuredSubagent(session, message) {
|
|
|
2742
2798
|
|| session.ambientSubagentTasks?.has(taskId))) return null;
|
|
2743
2799
|
const byTool = toolUseId && findSubagent(session, toolUseId);
|
|
2744
2800
|
const byTask = taskId && findSubagent(session, taskId);
|
|
2801
|
+
const runId = subagentRunId(message.run_id);
|
|
2802
|
+
if (isStaleSubagentRun(session, taskId, runId, byTool, byTask)) return null;
|
|
2745
2803
|
let running = byTool || byTask;
|
|
2746
2804
|
// The SDK does not order the background level snapshot against task_started.
|
|
2747
2805
|
// If both paths created a row, keep the tool-use row and fold the placeholder
|
|
@@ -2751,6 +2809,7 @@ function upsertStructuredSubagent(session, message) {
|
|
|
2751
2809
|
byTool.command ||= byTask.command;
|
|
2752
2810
|
byTool.taskId ||= byTask.taskId;
|
|
2753
2811
|
byTool.toolUseId ||= byTask.toolUseId;
|
|
2812
|
+
if (byTask.runId && (!byTool.runId || byTool.runId < byTask.runId)) byTool.runId = byTask.runId;
|
|
2754
2813
|
if ((!byTool.name || byTool.name === "agent") && byTask.name) byTool.name = byTask.name;
|
|
2755
2814
|
if (!byTool.tool) byTool.tool = byTask.tool;
|
|
2756
2815
|
byTool.startedAt = Math.min(byTool.startedAt, byTask.startedAt);
|
|
@@ -2779,8 +2838,7 @@ function upsertStructuredSubagent(session, message) {
|
|
|
2779
2838
|
}
|
|
2780
2839
|
if (toolUseId) running.toolUseId = toolUseId;
|
|
2781
2840
|
if (taskId) running.taskId = taskId;
|
|
2782
|
-
|
|
2783
|
-
if (runId && !isLateRun(running, runId)) running.runId = runId;
|
|
2841
|
+
if (runId) running.runId = runId;
|
|
2784
2842
|
// 스크립트 Monitor는 local_bash 작업으로 와서 스냅숏 이름이 Bash가 된다.
|
|
2785
2843
|
if (session.tools?.get(toolUseId)?.name === "Monitor") running.name = "Monitor";
|
|
2786
2844
|
const parentTaskId = firstLine(message.parent_task_id || "", 80);
|
|
@@ -2789,16 +2847,7 @@ function upsertStructuredSubagent(session, message) {
|
|
|
2789
2847
|
if (subagentType && (!running.name || running.name === "agent")) running.name = subagentType;
|
|
2790
2848
|
if (subagentType || isSubagentTaskType(message.task_type)) running.backgroundTask = false;
|
|
2791
2849
|
running.lastSeenAt = Date.now();
|
|
2792
|
-
|
|
2793
|
-
const known = session.knownSubagents.get(running.taskId);
|
|
2794
|
-
session.knownSubagents.set(running.taskId, {
|
|
2795
|
-
rowId: running.id,
|
|
2796
|
-
name: running.name,
|
|
2797
|
-
// 진행 요약으로 바뀐 설명이 아니라 호출 설명을 기억해, 다시 실행된 줄이 그 설명으로 돌아오게 한다.
|
|
2798
|
-
description: known?.description || running.description,
|
|
2799
|
-
backgroundTask: running.backgroundTask === true,
|
|
2800
|
-
});
|
|
2801
|
-
}
|
|
2850
|
+
rememberSubagent(session, running);
|
|
2802
2851
|
return running;
|
|
2803
2852
|
}
|
|
2804
2853
|
|
|
@@ -2808,13 +2857,47 @@ function hasLiveChildren(session, taskId) {
|
|
|
2808
2857
|
|
|
2809
2858
|
// 같은 작업의 실행 id는 열린 순서대로 문자열 정렬된다. 지금 행보다 앞선 실행의
|
|
2810
2859
|
// 통지는 늦게 도착한 것이라 새 실행의 행을 지우면 안 된다.
|
|
2860
|
+
function subagentRunId(value) {
|
|
2861
|
+
return typeof value === "string" ? value : "";
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2811
2864
|
function isLateRun(running, runId) {
|
|
2812
|
-
return Boolean(runId && running
|
|
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)));
|
|
2813
2892
|
}
|
|
2814
2893
|
|
|
2815
2894
|
function finishStructuredSubagent(session, message) {
|
|
2816
2895
|
const taskId = firstLine(message.task_id || "", 80);
|
|
2817
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;
|
|
2818
2901
|
const wasTranscriptHidden = taskId
|
|
2819
2902
|
? session.hiddenSubagentTasks?.delete(taskId) === true
|
|
2820
2903
|
: false;
|
|
@@ -2822,12 +2905,10 @@ function finishStructuredSubagent(session, message) {
|
|
|
2822
2905
|
? session.ambientSubagentTasks?.delete(taskId) === true
|
|
2823
2906
|
: false;
|
|
2824
2907
|
const wasHidden = wasTranscriptHidden || wasAmbient;
|
|
2825
|
-
|
|
2826
|
-
|| (taskId && findSubagent(session, taskId));
|
|
2908
|
+
rememberSubagent(session, running || { taskId, runId }, true, runId || running?.runId);
|
|
2827
2909
|
if (!running) return wasHidden;
|
|
2828
2910
|
// 자기 백그라운드 명령을 기다리며 멈춘 에이전트는 그 명령이 끝나면 다시 돌므로 행을 남긴다.
|
|
2829
|
-
if (hasLiveChildren(session, running.taskId)
|
|
2830
|
-
|| isLateRun(running, firstLine(message.run_id || "", 80))) return true;
|
|
2911
|
+
if (hasLiveChildren(session, running.taskId)) return true;
|
|
2831
2912
|
session.subagents.delete(running.id);
|
|
2832
2913
|
emitSubagents(session);
|
|
2833
2914
|
return true;
|
|
@@ -2835,10 +2916,32 @@ function finishStructuredSubagent(session, message) {
|
|
|
2835
2916
|
|
|
2836
2917
|
function syncBackgroundSubagents(session, tasks) {
|
|
2837
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
|
+
}
|
|
2838
2936
|
session.ambientSubagentTasks = new Set(snapshot
|
|
2839
|
-
.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)))
|
|
2840
2940
|
.map((task) => firstLine(task?.task_id || "", 80))
|
|
2841
2941
|
.filter(Boolean));
|
|
2942
|
+
for (const taskId of previousAmbient) {
|
|
2943
|
+
if (staleIds.has(taskId)) session.ambientSubagentTasks.add(taskId);
|
|
2944
|
+
}
|
|
2842
2945
|
const live = snapshot.filter((task) => {
|
|
2843
2946
|
const taskId = firstLine(task?.task_id || "", 80);
|
|
2844
2947
|
return task?.ambient !== true
|
|
@@ -2848,12 +2951,15 @@ function syncBackgroundSubagents(session, tasks) {
|
|
|
2848
2951
|
let changed = false;
|
|
2849
2952
|
for (const [id, running] of session.subagents) {
|
|
2850
2953
|
if (running.taskId && session.ambientSubagentTasks.has(running.taskId)) {
|
|
2954
|
+
rememberSubagent(session, running);
|
|
2851
2955
|
session.subagents.delete(id);
|
|
2852
2956
|
changed = true;
|
|
2853
2957
|
continue;
|
|
2854
2958
|
}
|
|
2855
|
-
if (!running.background || !running.taskId || liveIds.has(running.taskId)
|
|
2959
|
+
if (!running.background || !running.taskId || liveIds.has(running.taskId) || staleIds.has(running.taskId)
|
|
2856
2960
|
|| live.some((task) => task?.parent_task_id === running.taskId)) continue;
|
|
2961
|
+
// level 스냅숏은 task_started와 순서가 정해져 있지 않아 완료 증거가 아니다.
|
|
2962
|
+
rememberSubagent(session, running);
|
|
2857
2963
|
session.subagents.delete(id);
|
|
2858
2964
|
changed = true;
|
|
2859
2965
|
}
|
|
@@ -2862,6 +2968,8 @@ function syncBackgroundSubagents(session, tasks) {
|
|
|
2862
2968
|
if (!taskId) continue;
|
|
2863
2969
|
let running = findSubagent(session, taskId);
|
|
2864
2970
|
const known = session.knownSubagents.get(taskId);
|
|
2971
|
+
const runId = subagentRunId(task?.run_id);
|
|
2972
|
+
if (isStaleSubagentRun(session, taskId, runId, running)) continue;
|
|
2865
2973
|
if (!running) {
|
|
2866
2974
|
running = {
|
|
2867
2975
|
id: known?.rowId || `task:${taskId}`,
|
|
@@ -2881,6 +2989,10 @@ function syncBackgroundSubagents(session, tasks) {
|
|
|
2881
2989
|
changed = true;
|
|
2882
2990
|
}
|
|
2883
2991
|
running.background = true;
|
|
2992
|
+
if (runId && runId !== running.runId) {
|
|
2993
|
+
running.runId = runId;
|
|
2994
|
+
changed = true;
|
|
2995
|
+
}
|
|
2884
2996
|
running.lastSeenAt = Date.now();
|
|
2885
2997
|
const parentTaskId = firstLine(task?.parent_task_id || "", 80);
|
|
2886
2998
|
if (parentTaskId && parentTaskId !== running.parentTaskId) {
|
|
@@ -2895,11 +3007,19 @@ function syncBackgroundSubagents(session, tasks) {
|
|
|
2895
3007
|
running.description = description;
|
|
2896
3008
|
changed = true;
|
|
2897
3009
|
}
|
|
3010
|
+
rememberSubagent(session, running);
|
|
2898
3011
|
}
|
|
2899
3012
|
if (changed || liveIds.size) emitSubagents(session);
|
|
2900
3013
|
}
|
|
2901
3014
|
|
|
2902
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
|
+
}
|
|
2903
3023
|
if (message.subtype === "task_started") {
|
|
2904
3024
|
if (message.skip_transcript === true || message.ambient === true) {
|
|
2905
3025
|
const taskId = firstLine(message.task_id || "", 80);
|
|
@@ -2912,6 +3032,7 @@ function processSubagentSystemMessage(session, message) {
|
|
|
2912
3032
|
}
|
|
2913
3033
|
const running = (toolUseId && findSubagent(session, toolUseId))
|
|
2914
3034
|
|| (taskId && findSubagent(session, taskId));
|
|
3035
|
+
rememberSubagent(session, running || { taskId }, false, subagentRunId(message.run_id));
|
|
2915
3036
|
if (running) {
|
|
2916
3037
|
session.subagents.delete(running.id);
|
|
2917
3038
|
emitSubagents(session);
|
|
@@ -2940,9 +3061,15 @@ function processSubagentSystemMessage(session, message) {
|
|
|
2940
3061
|
return finishStructuredSubagent(session, message);
|
|
2941
3062
|
}
|
|
2942
3063
|
const running = taskId && findSubagent(session, taskId);
|
|
2943
|
-
|
|
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;
|
|
2944
3070
|
if (patch.is_backgrounded === true) running.background = true;
|
|
2945
3071
|
running.lastSeenAt = Date.now();
|
|
3072
|
+
rememberSubagent(session, running);
|
|
2946
3073
|
emitSubagents(session);
|
|
2947
3074
|
return true;
|
|
2948
3075
|
}
|
|
@@ -2998,6 +3125,7 @@ function subagentToolLabel(block) {
|
|
|
2998
3125
|
function finishSubagent(session, toolUseId) {
|
|
2999
3126
|
const running = findSubagent(session, toolUseId);
|
|
3000
3127
|
if (!running) return;
|
|
3128
|
+
rememberSubagent(session, running, true);
|
|
3001
3129
|
session.subagents.delete(running.id);
|
|
3002
3130
|
emitSubagents(session);
|
|
3003
3131
|
}
|
|
@@ -3006,6 +3134,7 @@ function finishSubagentTask(session, taskId) {
|
|
|
3006
3134
|
if (!taskId) return false;
|
|
3007
3135
|
const entry = [...session.subagents.entries()].find(([, agent]) => agent.taskId === taskId);
|
|
3008
3136
|
if (!entry) return false;
|
|
3137
|
+
rememberSubagent(session, entry[1], true);
|
|
3009
3138
|
session.subagents.delete(entry[0]);
|
|
3010
3139
|
emitSubagents(session);
|
|
3011
3140
|
return true;
|
|
@@ -3015,6 +3144,7 @@ function clearForegroundSubagents(session) {
|
|
|
3015
3144
|
let changed = false;
|
|
3016
3145
|
for (const [id, agent] of session.subagents) {
|
|
3017
3146
|
if (agent.background) continue;
|
|
3147
|
+
rememberSubagent(session, agent, true);
|
|
3018
3148
|
session.subagents.delete(id);
|
|
3019
3149
|
changed = true;
|
|
3020
3150
|
}
|
|
@@ -3142,7 +3272,7 @@ function taskNotifications(message) {
|
|
|
3142
3272
|
toolUseId: notificationTag(body, "tool-use-id"),
|
|
3143
3273
|
status: notificationTag(body, "status"),
|
|
3144
3274
|
summary: notificationTag(body, "summary"),
|
|
3145
|
-
runId:
|
|
3275
|
+
runId: subagentRunId(message.origin?.runId),
|
|
3146
3276
|
});
|
|
3147
3277
|
}
|
|
3148
3278
|
return notifications;
|
|
@@ -3170,8 +3300,9 @@ function finishNotifiedSubagents(session, notifications) {
|
|
|
3170
3300
|
.find((agent) => agent.background && agent.name === name);
|
|
3171
3301
|
}
|
|
3172
3302
|
}
|
|
3173
|
-
if (
|
|
3174
|
-
|
|
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;
|
|
3175
3306
|
session.subagents.delete(running.id);
|
|
3176
3307
|
emitSubagents(session);
|
|
3177
3308
|
}
|
|
@@ -3451,7 +3582,7 @@ async function processResult(session, message) {
|
|
|
3451
3582
|
}
|
|
3452
3583
|
// Not awaited: the next run's frames queue behind this message, and the
|
|
3453
3584
|
// account lookup used to hold them back.
|
|
3454
|
-
Promise.all([safeAccount(
|
|
3585
|
+
withSessionQuery(session, (query) => Promise.all([safeAccount(query), safeUsage(query)]))
|
|
3455
3586
|
.then(([account, usage]) => notify("claude/account/updated", { threadId: session.id, account, usage }))
|
|
3456
3587
|
.catch(() => {});
|
|
3457
3588
|
if (!steerFollows) await runPendingPrompt(session);
|
|
@@ -3553,6 +3684,7 @@ function beginUntrackedTurn(session, message) {
|
|
|
3553
3684
|
}
|
|
3554
3685
|
|
|
3555
3686
|
async function consumeMessage(session, message) {
|
|
3687
|
+
if (session.closed) return;
|
|
3556
3688
|
adoptSessionId(session, message.session_id);
|
|
3557
3689
|
beginUntrackedTurn(session, message);
|
|
3558
3690
|
// A queued SDK response may beat the timer. It already resumes the work.
|
|
@@ -3727,55 +3859,60 @@ async function steerPrompt(params) {
|
|
|
3727
3859
|
}
|
|
3728
3860
|
|
|
3729
3861
|
async function runPrompt(session, params) {
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
if (
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
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;
|
|
3768
3915
|
}
|
|
3769
|
-
const content = await inputContent(params.input, params.handoffContext);
|
|
3770
|
-
const turnId = beginTurn(session, params.input);
|
|
3771
|
-
session.queue.push({
|
|
3772
|
-
type: "user",
|
|
3773
|
-
message: { role: "user", content },
|
|
3774
|
-
parent_tool_use_id: null,
|
|
3775
|
-
session_id: id,
|
|
3776
|
-
origin: { kind: "human" },
|
|
3777
|
-
});
|
|
3778
|
-
return { turn: { id: turnId } };
|
|
3779
3916
|
}
|
|
3780
3917
|
|
|
3781
3918
|
// A background task notification is an internal user message that starts its
|
|
@@ -4347,7 +4484,7 @@ async function dispatch(method, params = {}) {
|
|
|
4347
4484
|
if (!session) throw new Error("Claude 세션이 아직 시작되지 않았습니다.");
|
|
4348
4485
|
const name = String(params.name || "").trim();
|
|
4349
4486
|
if (!name) throw new Error("변경할 Claude MCP 서버 이름이 없습니다.");
|
|
4350
|
-
await session
|
|
4487
|
+
await withSessionQuery(session, (query) => query.toggleMcpServer(name, Boolean(params.enabled)));
|
|
4351
4488
|
return { effectiveEnabled: Boolean(params.enabled) };
|
|
4352
4489
|
}
|
|
4353
4490
|
if (method === "plugin/list") return claudePluginCatalog(params);
|
|
@@ -4445,20 +4582,22 @@ async function dispatch(method, params = {}) {
|
|
|
4445
4582
|
const id = liveSessionId(params.sessionId);
|
|
4446
4583
|
const existing = sessions.get(id);
|
|
4447
4584
|
if (existing) {
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
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
|
+
});
|
|
4462
4601
|
}
|
|
4463
4602
|
const dir = await readableCwd(id, params.cwd);
|
|
4464
4603
|
// The transcript itself decides whether there is anything to resume:
|
|
@@ -4508,7 +4647,7 @@ async function dispatch(method, params = {}) {
|
|
|
4508
4647
|
const taskId = (session && (findSubagent(session, id)?.taskId || session.rowTaskIds?.get(id)))
|
|
4509
4648
|
|| (id.startsWith("task:") ? id.slice(5) : "");
|
|
4510
4649
|
if (!session || !taskId || typeof session.query.getTaskOutput !== "function") return {};
|
|
4511
|
-
const result = await session
|
|
4650
|
+
const result = await withSessionQuery(session, (query) => query.getTaskOutput(taskId));
|
|
4512
4651
|
return { output: String(result?.output ?? "") };
|
|
4513
4652
|
}
|
|
4514
4653
|
if (method === "session/watchSubagent") {
|
|
@@ -4536,7 +4675,7 @@ async function dispatch(method, params = {}) {
|
|
|
4536
4675
|
const turn = session.turn;
|
|
4537
4676
|
turn.interruptRequested = true;
|
|
4538
4677
|
try {
|
|
4539
|
-
await session
|
|
4678
|
+
await withSessionQuery(session, (query) => query.interrupt());
|
|
4540
4679
|
} catch (error) {
|
|
4541
4680
|
if (session.turn === turn) delete turn.interruptRequested;
|
|
4542
4681
|
throw error;
|
|
@@ -4609,29 +4748,25 @@ async function dispatch(method, params = {}) {
|
|
|
4609
4748
|
if (method === "session/close") {
|
|
4610
4749
|
const session = lookupSession(params.sessionId);
|
|
4611
4750
|
if (session) {
|
|
4612
|
-
|
|
4613
|
-
clearSubagents(session);
|
|
4614
|
-
session.queue.close();
|
|
4615
|
-
session.query.close();
|
|
4616
|
-
sessions.delete(session.id);
|
|
4617
|
-
for (const [from, to] of sessionAliases) {
|
|
4618
|
-
if (to === session.id) sessionAliases.delete(from);
|
|
4619
|
-
}
|
|
4751
|
+
closeLiveSession(session);
|
|
4620
4752
|
if (params.delete) await deleteSession(session.id, { dir: session.cwd });
|
|
4621
4753
|
}
|
|
4622
4754
|
return {};
|
|
4623
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
|
+
}
|
|
4624
4762
|
if (method === "account/usage") {
|
|
4625
4763
|
const session = lookupSession(params.sessionId) || [...sessions.values()][0];
|
|
4626
4764
|
if (!session) return { account: null, usage: null };
|
|
4627
|
-
return { account: await safeAccount(
|
|
4765
|
+
return withSessionQuery(session, async (query) => ({ account: await safeAccount(query), usage: await safeUsage(query) }));
|
|
4628
4766
|
}
|
|
4629
4767
|
if (method === "shutdown") {
|
|
4630
4768
|
for (const session of sessions.values()) {
|
|
4631
|
-
|
|
4632
|
-
clearSubagents(session);
|
|
4633
|
-
session.queue.close();
|
|
4634
|
-
session.query.close();
|
|
4769
|
+
closeLiveSession(session);
|
|
4635
4770
|
}
|
|
4636
4771
|
sessions.clear();
|
|
4637
4772
|
sessionAliases.clear();
|
|
@@ -5009,6 +5144,271 @@ async function runSelfTest() {
|
|
|
5009
5144
|
else process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
|
|
5010
5145
|
await rm(configDir, { recursive: true, force: true });
|
|
5011
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
|
+
}
|
|
5012
5412
|
// A resumed transcript lists its Artifact pages once each at their latest
|
|
5013
5413
|
// publish, read through the file's toolUseResult spelling.
|
|
5014
5414
|
const artifactTranscript = [
|
|
@@ -5065,14 +5465,14 @@ async function runSelfTest() {
|
|
|
5065
5465
|
|| CLAUDE_TASK_TOOLS.some((tool) => !latestOptions.allowedTools.includes(tool))) {
|
|
5066
5466
|
throw new Error(`Claude latest SDK options self-test failed: ${JSON.stringify(latestOptions.allowedTools)}`);
|
|
5067
5467
|
}
|
|
5068
|
-
const
|
|
5468
|
+
const taskLookupSession = {
|
|
5069
5469
|
turn: { id: "task-get-turn" },
|
|
5070
5470
|
tools: new Map(),
|
|
5071
5471
|
tasks: new Map(),
|
|
5072
5472
|
planCreatePending: false,
|
|
5073
5473
|
};
|
|
5074
|
-
processToolUse(
|
|
5075
|
-
if (
|
|
5474
|
+
processToolUse(taskLookupSession, { id: "task-get", name: "TaskGet", input: { taskId: "1" } });
|
|
5475
|
+
if (taskLookupSession.tools.get("task-get")?.suppressed !== true) {
|
|
5076
5476
|
throw new Error("Claude TaskGet transcript suppression self-test failed");
|
|
5077
5477
|
}
|
|
5078
5478
|
const pluginCatalog = buildClaudePluginCatalog(
|
|
@@ -6216,6 +6616,114 @@ async function runSelfTest() {
|
|
|
6216
6616
|
throw new Error(`Claude waiting subagent self-test failed: ${waitingKept} ${settledAfterChild} ${keptAgainstLate} ${nestedSession.subagents.size}`);
|
|
6217
6617
|
}
|
|
6218
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
|
+
}
|
|
6219
6727
|
// 끝난 뒤 SDK가 같은 작업을 다시 돌리면 설명 없는 새 줄이 아니라 같은 줄로 돌아온다.
|
|
6220
6728
|
startSubagent(nestedSession, { id: "toolu_again", input: { description: "3분 대기", subagent_type: "general-purpose" } });
|
|
6221
6729
|
keepBackgroundSubagent(nestedSession, "toolu_again", { isAsync: true, agentId: "agent-again" });
|
|
@@ -6679,6 +7187,8 @@ async function runSelfTest() {
|
|
|
6679
7187
|
throw new Error("Claude idle background subagent self-test failed");
|
|
6680
7188
|
}
|
|
6681
7189
|
structuredSession.subagents.clear();
|
|
7190
|
+
// 턴 결과가 분리 실행한 계정 조회를 이 캡처 안에서 끝내 다음 표시 검사와 섞이지 않게 한다.
|
|
7191
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
6682
7192
|
} finally {
|
|
6683
7193
|
process.stdout.write = stdoutWrite;
|
|
6684
7194
|
}
|
|
@@ -7014,8 +7524,6 @@ lines.on("line", async (line) => {
|
|
|
7014
7524
|
|
|
7015
7525
|
lines.on("close", () => {
|
|
7016
7526
|
for (const session of sessions.values()) {
|
|
7017
|
-
|
|
7018
|
-
clearSubagents(session);
|
|
7019
|
-
session.query.close();
|
|
7527
|
+
closeLiveSession(session);
|
|
7020
7528
|
}
|
|
7021
7529
|
});
|