devez-vibe 1.9.40 → 1.9.42

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 CHANGED
Binary file
@@ -1857,15 +1857,15 @@ function prepareTaskPlanForCreate(tasks, subject) {
1857
1857
  // 한 단계에 걸린 시간은 진행 중으로 바뀐 순간과 완료된 순간의 차이다. 기록에는
1858
1858
  // 항목마다 시각이 남으므로, 다시 읽을 때도 같은 방식으로 되살릴 수 있다.
1859
1859
  function markTaskStatus(task, status, at) {
1860
- if (task.status !== status) {
1861
- if (status === "in_progress") {
1862
- task.startedAt = at;
1863
- delete task.elapsedMs;
1864
- } else if (status === "completed" && task.startedAt != null && at != null) {
1865
- task.elapsedMs = Math.max(0, at - task.startedAt);
1866
- } else if (status === "pending") {
1867
- delete task.startedAt;
1868
- delete task.elapsedMs;
1860
+ if (task.status !== status) {
1861
+ if (status === "in_progress") {
1862
+ task.startedAt = at;
1863
+ delete task.elapsedMs;
1864
+ } else if (status === "completed" && task.startedAt != null && at != null) {
1865
+ task.elapsedMs = Math.max(0, at - task.startedAt);
1866
+ } else if (status === "pending") {
1867
+ delete task.startedAt;
1868
+ delete task.elapsedMs;
1869
1869
  }
1870
1870
  }
1871
1871
  task.status = status;
@@ -1987,19 +1987,19 @@ function updatePlanFromToolResult(session, pending, message) {
1987
1987
  const turnId = session.turn?.id;
1988
1988
  const previous = session.tasks;
1989
1989
  session.tasks = new Map();
1990
- for (const task of value.tasks || value) {
1991
- const id = String(task.id);
1992
- const restored = {
1993
- id,
1994
- subject: task.subject || task.description || "작업",
1995
- status: previous.get(id)?.status || "pending",
1990
+ for (const task of value.tasks || value) {
1991
+ const id = String(task.id);
1992
+ const restored = {
1993
+ id,
1994
+ subject: task.subject || task.description || "작업",
1995
+ status: previous.get(id)?.status || "pending",
1996
1996
  // 이 턴에서 다룬 적 없는 작업은 이전 턴의 것이므로 원래 turnId를 지켜 준다.
1997
- turnId: previous.get(id)?.turnId ?? turnId,
1998
- startedAt: previous.get(id)?.startedAt,
1999
- elapsedMs: previous.get(id)?.elapsedMs,
2000
- };
2001
- markTaskStatus(restored, task.status || "pending", Date.now());
2002
- session.tasks.set(id, restored);
1997
+ turnId: previous.get(id)?.turnId ?? turnId,
1998
+ startedAt: previous.get(id)?.startedAt,
1999
+ elapsedMs: previous.get(id)?.elapsedMs,
2000
+ };
2001
+ markTaskStatus(restored, task.status || "pending", Date.now());
2002
+ session.tasks.set(id, restored);
2003
2003
  }
2004
2004
  session.tasks = latestTaskPlan(session.tasks);
2005
2005
  emitPlan(session);
@@ -2027,11 +2027,11 @@ function emitPlan(session) {
2027
2027
  notify("turn/plan/updated", {
2028
2028
  threadId: session.id,
2029
2029
  turnId: session.turn?.id,
2030
- plan: [...session.tasks.values()].map((task, index) => ({
2031
- id: task.id,
2032
- step: numberedTaskSubject(task.subject, index),
2033
- status: planStatus(task.status),
2034
- elapsedMs: task.elapsedMs ?? null,
2030
+ plan: [...session.tasks.values()].map((task, index) => ({
2031
+ id: task.id,
2032
+ step: numberedTaskSubject(task.subject, index),
2033
+ status: planStatus(task.status),
2034
+ elapsedMs: task.elapsedMs ?? null,
2035
2035
  })),
2036
2036
  });
2037
2037
  }
@@ -2238,6 +2238,8 @@ function upsertStructuredSubagent(session, message) {
2238
2238
  }
2239
2239
  if (toolUseId) running.toolUseId = toolUseId;
2240
2240
  if (taskId) running.taskId = taskId;
2241
+ const parentTaskId = firstLine(message.parent_task_id || "", 80);
2242
+ if (parentTaskId) running.parentTaskId = parentTaskId;
2241
2243
  // Agent 호출이 붙인 작업 이름이 있으면 SDK가 뒤늦게 알려 주는 에이전트 종류로 덮지 않는다.
2242
2244
  if (subagentType && (!running.name || running.name === "agent")) running.name = subagentType;
2243
2245
  if (subagentType || isSubagentTaskType(message.task_type)) running.backgroundTask = false;
@@ -2319,6 +2321,11 @@ function syncBackgroundSubagents(session, tasks) {
2319
2321
  }
2320
2322
  running.background = true;
2321
2323
  running.lastSeenAt = Date.now();
2324
+ const parentTaskId = firstLine(task?.parent_task_id || "", 80);
2325
+ if (parentTaskId && parentTaskId !== running.parentTaskId) {
2326
+ running.parentTaskId = parentTaskId;
2327
+ changed = true;
2328
+ }
2322
2329
  // 에이전트 행은 이름만 보이지만, 백그라운드 명령 행은 명령문이 곧 내용이다.
2323
2330
  const description = isSubagentTaskType(task?.task_type)
2324
2331
  ? ""
@@ -2412,8 +2419,14 @@ function processSubagentSystemMessage(session, message) {
2412
2419
 
2413
2420
  // 서브에이전트가 실제로 무엇을 했는지는 자식 메시지에만 남는다. 열람용 기록은 여기서
2414
2421
  // 한 줄씩 흘려보내고, 목록 행에 쓸 현재 도구만 따로 갱신한다.
2422
+ // 메시지의 agent_id는 그 하위 에이전트 작업의 task_id라 재개 뒤에도 같은 행을 찾는다.
2423
+ function messageSubagent(session, message) {
2424
+ return findSubagent(session, message.parent_tool_use_id)
2425
+ || (message.agent_id ? findSubagent(session, message.agent_id) : undefined);
2426
+ }
2427
+
2415
2428
  function recordSubagentMessage(session, message) {
2416
- const running = findSubagent(session, message.parent_tool_use_id);
2429
+ const running = messageSubagent(session, message);
2417
2430
  if (!running) return;
2418
2431
  running.lastSeenAt = Date.now();
2419
2432
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
@@ -2436,7 +2449,7 @@ function recordSubagentMessage(session, message) {
2436
2449
  }
2437
2450
 
2438
2451
  function recordSubagentResult(session, message) {
2439
- const running = findSubagent(session, message.parent_tool_use_id);
2452
+ const running = messageSubagent(session, message);
2440
2453
  if (!running) return;
2441
2454
  running.lastSeenAt = Date.now();
2442
2455
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
@@ -2515,6 +2528,37 @@ function firstLine(value, limit) {
2515
2528
  return String(value ?? "").split("\n")[0].trim().slice(0, limit);
2516
2529
  }
2517
2530
 
2531
+ // SDK 0.3.292부터 하위 에이전트가 띄운 에이전트·명령에 parent_task_id가 붙는다.
2532
+ // 자식 행을 부모 행 바로 아래로 모아 깊이와 함께 보내면 호스트가 들여 그린다.
2533
+ // 부모가 이미 끝났거나 모르는 id면 최상위에 둔다.
2534
+ function subagentRows(session) {
2535
+ const agents = [...session.subagents.values()];
2536
+ const byTask = new Map(agents.filter((agent) => agent.taskId).map((agent) => [agent.taskId, agent]));
2537
+ const children = new Map();
2538
+ const roots = [];
2539
+ for (const agent of agents) {
2540
+ const parent = agent.parentTaskId ? byTask.get(agent.parentTaskId) : null;
2541
+ if (parent && parent !== agent) {
2542
+ if (!children.has(parent)) children.set(parent, []);
2543
+ children.get(parent).push(agent);
2544
+ } else {
2545
+ roots.push(agent);
2546
+ }
2547
+ }
2548
+ const rows = [];
2549
+ const placed = new Set();
2550
+ const place = (agent, depth) => {
2551
+ if (placed.has(agent)) return;
2552
+ placed.add(agent);
2553
+ rows.push({ agent, depth });
2554
+ for (const child of children.get(agent) || []) place(child, depth + 1);
2555
+ };
2556
+ for (const agent of roots) place(agent, 0);
2557
+ // 서로를 부모로 가리키는 행은 뿌리가 없으므로 최상위로 살린다.
2558
+ for (const agent of agents) place(agent, 0);
2559
+ return rows;
2560
+ }
2561
+
2518
2562
  function emitSubagents(session) {
2519
2563
  // 백그라운드 행은 조용하다고 지우지 않는다. 오래 걸리는 조사 에이전트는 진행
2520
2564
  // 신호 없이 몇 분을 보내므로, 종료 통지와 level 스냅숏만 행을 거둔다.
@@ -2528,13 +2572,14 @@ function emitSubagents(session) {
2528
2572
  notify("turn/subagents/updated", {
2529
2573
  threadId: session.id,
2530
2574
  turnId: session.turn?.id,
2531
- subagents: [...session.subagents.values()].map((agent) => ({
2575
+ subagents: subagentRows(session).map(({ agent, depth }) => ({
2532
2576
  id: agent.id,
2533
2577
  name: agent.name,
2534
2578
  description: agent.description,
2535
2579
  backgroundTask: agent.backgroundTask === true,
2536
2580
  tool: agent.tool,
2537
2581
  elapsedMs: Date.now() - agent.startedAt,
2582
+ depth,
2538
2583
  })),
2539
2584
  });
2540
2585
  }
@@ -2778,6 +2823,7 @@ function cancelUsageLimitWait(session) {
2778
2823
  // 조치를 바로 알기 어려우므로 사유가 있을 때만 앞줄에 한국어 안내를 붙인다.
2779
2824
  const STARTUP_FAILURE_GUIDES = {
2780
2825
  org_pin_api_key_conflict: "조직 설정이 지정한 로그인 대신 API 키가 설정되어 있습니다. API 키·인증 토큰 환경 변수를 지우고 지정된 계정으로 로그인하세요.",
2826
+ provider_not_allowed: "조직 설정이 허용하지 않는 API 제공자로 연결하도록 설정되어 있습니다. 허용된 제공자로 로그인하거나 관리자에게 문의하세요.",
2781
2827
  org_verify_failed: "로그인 계정의 조직을 확인하지 못했습니다. 네트워크를 확인하고 다시 로그인하세요.",
2782
2828
  org_pin_mismatch: "조직 설정이 허용하지 않는 계정으로 로그인되어 있습니다. 허용된 조직 계정으로 다시 로그인하세요.",
2783
2829
  managed_settings_invalid: "조직 관리 설정을 읽지 못했습니다. 관리자에게 설정 확인을 요청하세요.",
@@ -2871,13 +2917,28 @@ async function processResult(session, message) {
2871
2917
  notify("warning", { threadId: session.id, provider: "Claude", message: `Claude response error: ${text}` });
2872
2918
  }
2873
2919
  if (error && waitForUsageLimit(session)) return;
2874
- finishTurn(session, error, message.duration_ms);
2875
- notify("claude/account/updated", {
2876
- threadId: session.id,
2877
- account: await safeAccount(session.query),
2878
- usage: await safeUsage(session.query),
2879
- });
2880
- await runPendingPrompt(session);
2920
+ // A steer the CLI could not fold into this run gets a run of its own, which
2921
+ // `queued_turn_count` announces. Closing the turn here dropped the host's
2922
+ // spinner and filed a completion until that answer's first frame arrived.
2923
+ // A count of 0 can still hide a steer read just after the result; the steer
2924
+ // debt opens a fresh turn for it then, as before.
2925
+ const steerFollows = !error && !interrupted && !assistantError
2926
+ && session.steerPending > 0 && message.queued_turn_count > 0;
2927
+ if (steerFollows) {
2928
+ session.steerPending -= 1;
2929
+ session.turn.awaitingSteer = true;
2930
+ releaseHeldProgress(session);
2931
+ flushPendingPlan(session);
2932
+ clearForegroundSubagents(session);
2933
+ } else {
2934
+ finishTurn(session, error, message.duration_ms);
2935
+ }
2936
+ // Not awaited: the next run's frames queue behind this message, and the
2937
+ // account lookup used to hold them back.
2938
+ Promise.all([safeAccount(session.query), safeUsage(session.query)])
2939
+ .then(([account, usage]) => notify("claude/account/updated", { threadId: session.id, account, usage }))
2940
+ .catch(() => {});
2941
+ if (!steerFollows) await runPendingPrompt(session);
2881
2942
  }
2882
2943
 
2883
2944
  // Compaction ends the turn's only assistant message, so the context figure would
@@ -2975,12 +3036,13 @@ function beginUntrackedTurn(session, message) {
2975
3036
  return true;
2976
3037
  }
2977
3038
 
2978
- async function consumeMessage(session, message) {
2979
- adoptSessionId(session, message.session_id);
3039
+ async function consumeMessage(session, message) {
3040
+ adoptSessionId(session, message.session_id);
2980
3041
  beginUntrackedTurn(session, message);
2981
3042
  // A queued SDK response may beat the timer. It already resumes the work.
2982
3043
  if (!message.parent_tool_use_id && (message.type === "assistant" || message.type === "stream_event")) {
2983
3044
  clearUsageLimitWait(session);
3045
+ if (session.turn) session.turn.awaitingSteer = false;
2984
3046
  }
2985
3047
  if (message.type === "stream_event") {
2986
3048
  if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
@@ -3356,20 +3418,20 @@ function historyState(messages) {
3356
3418
  } else if (pending.name === "TaskList" && Array.isArray(message.tool_use_result?.tasks)) {
3357
3419
  const known = new Map(tasks);
3358
3420
  tasks.clear();
3359
- for (const task of message.tool_use_result.tasks) {
3360
- const previous = known.get(String(task.id));
3361
- const restored = {
3421
+ for (const task of message.tool_use_result.tasks) {
3422
+ const previous = known.get(String(task.id));
3423
+ const restored = {
3362
3424
  id: String(task.id),
3363
3425
  subject: task.subject || "작업",
3364
- status: previous?.status || "pending",
3426
+ status: previous?.status || "pending",
3365
3427
  turnId: previous?.turnId ?? turn.id,
3366
3428
  // A listing restates the plan; it does not re-run it, so the
3367
3429
  // timings already measured for these steps have to survive it.
3368
3430
  startedAt: previous?.startedAt,
3369
3431
  elapsedMs: previous?.elapsedMs,
3370
- };
3371
- markTaskStatus(restored, task.status || "pending", messageTime(message));
3372
- tasks.set(String(task.id), restored);
3432
+ };
3433
+ markTaskStatus(restored, task.status || "pending", messageTime(message));
3434
+ tasks.set(String(task.id), restored);
3373
3435
  }
3374
3436
  const current = latestTaskPlan(tasks);
3375
3437
  tasks.clear();
@@ -3392,9 +3454,9 @@ function historyState(messages) {
3392
3454
  const text = [...tasks.values()].map((task, index) => `${task.status === "completed" ? "✓" : task.status === "in_progress" ? "▸" : "□"} ${numberedTaskSubject(task.subject, index)}`).join("\n");
3393
3455
  // The text alone cannot say how long a step took, so the measured times ride
3394
3456
  // alongside it and the restored plan shows its total instead of zero.
3395
- const steps = [...tasks.values()].map((task, index) => ({
3396
- id: task.id,
3397
- step: numberedTaskSubject(task.subject, index),
3457
+ const steps = [...tasks.values()].map((task, index) => ({
3458
+ id: task.id,
3459
+ step: numberedTaskSubject(task.subject, index),
3398
3460
  status: task.status || "pending",
3399
3461
  elapsedMs: task.elapsedMs ?? null,
3400
3462
  }));
@@ -3884,6 +3946,9 @@ async function dispatch(method, params = {}) {
3884
3946
  if (session.turn === turn) delete turn.interruptRequested;
3885
3947
  throw error;
3886
3948
  }
3949
+ // Kept open for a steer whose run has not begun: nothing is running to
3950
+ // send the result that would close it.
3951
+ if (session.turn === turn && turn.awaitingSteer) finishTurn(session, null);
3887
3952
  }
3888
3953
  return {};
3889
3954
  }
@@ -4460,35 +4525,35 @@ async function runSelfTest() {
4460
4525
  at(taskUse("timed-done", "timed-done", "TaskUpdate", { taskId: "t1", status: "completed" }), "2026-08-07T01:00:09.000Z"),
4461
4526
  ];
4462
4527
  const timedPlan = historyState(timedMessages).turns.at(-1)?.items.find((item) => item.type === "plan");
4463
- if (timedPlan?.steps?.[0]?.elapsedMs !== 6000 || timedPlan.steps[0].status !== "completed") {
4464
- throw new Error(`Claude plan step timing self-test failed: ${JSON.stringify(timedPlan)}`);
4465
- }
4466
- const timedLiveSession = {
4467
- id: "timed-live-plan",
4468
- turn: { id: "timed-live-turn" },
4469
- tasks: historyState(timedMessages).tasks,
4470
- };
4471
- const timedLiveEvents = [];
4472
- const timedLiveWrite = process.stdout.write;
4473
- process.stdout.write = (chunk) => { timedLiveEvents.push(JSON.parse(String(chunk))); return true; };
4474
- try {
4475
- updatePlanFromToolResult(timedLiveSession, { name: "TaskList" }, {
4476
- tool_use_result: { tasks: [{ id: "t1", subject: "1. 확인한 설정", status: "completed" }] },
4477
- });
4478
- } finally {
4479
- process.stdout.write = timedLiveWrite;
4480
- }
4481
- const liveMeasured = timedLiveEvents.at(-1)?.params?.plan?.[0];
4482
- if (liveMeasured?.elapsedMs !== 6000 || timedLiveSession.tasks.get("t1")?.elapsedMs !== 6000) {
4483
- throw new Error(`Claude live plan timing self-test failed: ${JSON.stringify(liveMeasured)}`);
4484
- }
4485
- const restartedTask = { status: "pending" };
4486
- markTaskStatus(restartedTask, "in_progress", 1000);
4487
- markTaskStatus(restartedTask, "completed", 7000);
4488
- markTaskStatus(restartedTask, "in_progress", 9000);
4489
- markTaskStatus(restartedTask, "completed", 10000);
4490
- if (restartedTask.elapsedMs !== 1000) throw new Error("Claude restarted task timing self-test failed");
4491
- const skippedTasks = new Map([
4528
+ if (timedPlan?.steps?.[0]?.elapsedMs !== 6000 || timedPlan.steps[0].status !== "completed") {
4529
+ throw new Error(`Claude plan step timing self-test failed: ${JSON.stringify(timedPlan)}`);
4530
+ }
4531
+ const timedLiveSession = {
4532
+ id: "timed-live-plan",
4533
+ turn: { id: "timed-live-turn" },
4534
+ tasks: historyState(timedMessages).tasks,
4535
+ };
4536
+ const timedLiveEvents = [];
4537
+ const timedLiveWrite = process.stdout.write;
4538
+ process.stdout.write = (chunk) => { timedLiveEvents.push(JSON.parse(String(chunk))); return true; };
4539
+ try {
4540
+ updatePlanFromToolResult(timedLiveSession, { name: "TaskList" }, {
4541
+ tool_use_result: { tasks: [{ id: "t1", subject: "1. 확인한 설정", status: "completed" }] },
4542
+ });
4543
+ } finally {
4544
+ process.stdout.write = timedLiveWrite;
4545
+ }
4546
+ const liveMeasured = timedLiveEvents.at(-1)?.params?.plan?.[0];
4547
+ if (liveMeasured?.elapsedMs !== 6000 || timedLiveSession.tasks.get("t1")?.elapsedMs !== 6000) {
4548
+ throw new Error(`Claude live plan timing self-test failed: ${JSON.stringify(liveMeasured)}`);
4549
+ }
4550
+ const restartedTask = { status: "pending" };
4551
+ markTaskStatus(restartedTask, "in_progress", 1000);
4552
+ markTaskStatus(restartedTask, "completed", 7000);
4553
+ markTaskStatus(restartedTask, "in_progress", 9000);
4554
+ markTaskStatus(restartedTask, "completed", 10000);
4555
+ if (restartedTask.elapsedMs !== 1000) throw new Error("Claude restarted task timing self-test failed");
4556
+ const skippedTasks = new Map([
4492
4557
  ["1", { id: "1", subject: "1. 조사", status: "pending" }],
4493
4558
  ["2", { id: "2", subject: "2. 분석", status: "pending" }],
4494
4559
  ["3", { id: "3", subject: "3. 검증", status: "pending" }],
@@ -4869,6 +4934,64 @@ async function runSelfTest() {
4869
4934
  if (authTurn?.error?.codexErrorInfo !== "unauthorized" || !authTurn.error.message.includes("401")) {
4870
4935
  throw new Error(`Claude login failure lost its tag: ${JSON.stringify(authTurn)}`);
4871
4936
  }
4937
+ // A steer answered in a run of its own stays in the turn it joined.
4938
+ const steerNotices = [];
4939
+ const steerWrite = process.stdout.write;
4940
+ process.stdout.write = (chunk) => { steerNotices.push(JSON.parse(String(chunk))); return true; };
4941
+ const steerSession = {
4942
+ ...automaticTurnSession,
4943
+ id: "steer-continuation-self-test",
4944
+ turn: null,
4945
+ streamBlocks: new Map(),
4946
+ tools: new Map(),
4947
+ tasks: new Map(),
4948
+ subagents: new Map(),
4949
+ pendingPrompts: [],
4950
+ query: { ...automaticTurnSession.query, async interrupt() {} },
4951
+ };
4952
+ sessions.set(steerSession.id, steerSession);
4953
+ const steerResult = (queued) => ({ type: "result", is_error: false, stop_reason: "end_turn", modelUsage: {}, queued_turn_count: queued });
4954
+ const steerRun = { type: "stream_event", parent_tool_use_id: null, event: { type: "message_start", message: {} } };
4955
+ const steerLifecycle = () => steerNotices
4956
+ .filter((notice) => notice.method === "turn/started" || notice.method === "turn/completed")
4957
+ .map((notice) => notice.method)
4958
+ .join();
4959
+ try {
4960
+ const steerTurn = beginTurn(steerSession);
4961
+ steerSession.steerPending = 1;
4962
+ await consumeMessage(steerSession, steerResult(1));
4963
+ await consumeMessage(steerSession, steerRun);
4964
+ if (steerSession.turn?.id !== steerTurn || steerSession.steerPending !== 0) {
4965
+ throw new Error("Claude steer run left the turn it joined");
4966
+ }
4967
+ await consumeMessage(steerSession, steerResult(0));
4968
+ if (steerLifecycle() !== "turn/started,turn/completed") {
4969
+ throw new Error(`Claude steer run reopened its turn: ${steerLifecycle()}`);
4970
+ }
4971
+ // A steer read only after the result still gets a turn from the debt.
4972
+ steerNotices.length = 0;
4973
+ beginTurn(steerSession);
4974
+ steerSession.steerPending = 1;
4975
+ await consumeMessage(steerSession, steerResult(0));
4976
+ await consumeMessage(steerSession, steerRun);
4977
+ if (steerLifecycle() !== "turn/started,turn/completed,turn/started") {
4978
+ throw new Error(`Claude late steer lost its turn: ${steerLifecycle()}`);
4979
+ }
4980
+ // Esc before the steer's run begins closes the turn instead of stranding it.
4981
+ finishTurn(steerSession, null);
4982
+ beginTurn(steerSession);
4983
+ steerSession.steerPending = 1;
4984
+ await consumeMessage(steerSession, steerResult(1));
4985
+ steerNotices.length = 0;
4986
+ await dispatch("session/interrupt", { sessionId: steerSession.id });
4987
+ const stopped = steerNotices.find((notice) => notice.method === "turn/completed")?.params?.turn;
4988
+ if (steerSession.turn || stopped?.status !== "interrupted") {
4989
+ throw new Error(`Claude steer wait survived Esc: ${JSON.stringify(stopped)}`);
4990
+ }
4991
+ } finally {
4992
+ process.stdout.write = steerWrite;
4993
+ sessions.delete(steerSession.id);
4994
+ }
4872
4995
  beginTurn(limitSession);
4873
4996
  for (let attempt = 0; attempt < 4; attempt++) {
4874
4997
  await rejectLimit();
@@ -5163,6 +5286,68 @@ async function runSelfTest() {
5163
5286
  throw new Error("Claude background task snapshot did not remove a finished subagent");
5164
5287
  }
5165
5288
 
5289
+ // parent_task_id puts what a subagent launched right under its row, one
5290
+ // level in; a parent that is gone leaves the child at the top level.
5291
+ const nestedSession = {
5292
+ id: "nested-self-test",
5293
+ turn: null,
5294
+ subagents: new Map(),
5295
+ knownSubagents: new Map(),
5296
+ hiddenSubagentTasks: new Set(),
5297
+ ambientSubagentTasks: new Set(),
5298
+ subagentPulse: null,
5299
+ };
5300
+ processSubagentSystemMessage(nestedSession, {
5301
+ type: "system",
5302
+ subtype: "task_started",
5303
+ task_id: "agent-outer",
5304
+ tool_use_id: "toolu_outer",
5305
+ task_type: "local_agent",
5306
+ subagent_type: "Explore",
5307
+ });
5308
+ processSubagentSystemMessage(nestedSession, {
5309
+ type: "system",
5310
+ subtype: "background_tasks_changed",
5311
+ tasks: [
5312
+ { task_id: "shell-orphan", task_type: "local_bash", description: "npm run dev", parent_task_id: "agent-gone" },
5313
+ { task_id: "shell-inner", task_type: "local_bash", description: "ping", parent_task_id: "agent-outer" },
5314
+ ],
5315
+ });
5316
+ processSubagentSystemMessage(nestedSession, {
5317
+ type: "system",
5318
+ subtype: "task_started",
5319
+ task_id: "agent-inner",
5320
+ tool_use_id: "toolu_inner",
5321
+ task_type: "local_agent",
5322
+ subagent_type: "Plan",
5323
+ parent_task_id: "agent-outer",
5324
+ });
5325
+ const nestedRows = () => captured.join("").trim().split("\n").filter(Boolean)
5326
+ .map((line) => JSON.parse(line))
5327
+ .filter((line) => line.method === "turn/subagents/updated" && line.params.threadId === "nested-self-test")
5328
+ .at(-1)?.params.subagents.map((row) => `${row.depth}:${row.id}`);
5329
+ const expectedNesting = ["0:toolu_outer", "1:task:shell-inner", "1:toolu_inner", "0:task:shell-orphan"];
5330
+ if (JSON.stringify(nestedRows()) !== JSON.stringify(expectedNesting)) {
5331
+ throw new Error(`Claude nested subagent self-test failed: ${JSON.stringify(nestedRows())}`);
5332
+ }
5333
+ // Rows naming each other as parent have no root; none may vanish or repeat.
5334
+ nestedSession.subagents.get("toolu_outer").parentTaskId = "agent-inner";
5335
+ emitSubagents(nestedSession);
5336
+ const looped = nestedRows() || [];
5337
+ if (looped.length !== 4 || new Set(looped.map((row) => row.split(":").slice(1).join(":"))).size !== 4) {
5338
+ throw new Error(`Claude subagent parent loop self-test failed: ${JSON.stringify(looped)}`);
5339
+ }
5340
+ // A resumed subagent's messages find its row through agent_id.
5341
+ recordSubagentMessage(nestedSession, {
5342
+ parent_tool_use_id: "toolu_unknown",
5343
+ agent_id: "agent-inner",
5344
+ message: { content: [{ type: "tool_use", id: "toolu_read", name: "Read", input: { file_path: "a.rs" } }] },
5345
+ });
5346
+ if (nestedSession.subagents.get("toolu_inner")?.tool !== "Read(a.rs)") {
5347
+ throw new Error("Claude subagent agent_id self-test failed");
5348
+ }
5349
+ clearSubagents(nestedSession);
5350
+
5166
5351
  processSubagentSystemMessage(structuredSession, {
5167
5352
  type: "system",
5168
5353
  subtype: "background_tasks_changed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.9.40",
3
+ "version": "1.9.42",
4
4
  "type": "module",
5
5
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
6
6
  "keywords": [
@@ -45,6 +45,6 @@
45
45
  "postinstall": "node install-skills.mjs && node prepare-search-deps.mjs"
46
46
  },
47
47
  "dependencies": {
48
- "@anthropic-ai/claude-agent-sdk": "0.3.283"
48
+ "@anthropic-ai/claude-agent-sdk": "0.3.292"
49
49
  }
50
50
  }