devez-vibe 1.9.39 → 1.9.41

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,11 +1857,15 @@ function prepareTaskPlanForCreate(tasks, subject) {
1857
1857
  // 한 단계에 걸린 시간은 진행 중으로 바뀐 순간과 완료된 순간의 차이다. 기록에는
1858
1858
  // 항목마다 시각이 남으므로, 다시 읽을 때도 같은 방식으로 되살릴 수 있다.
1859
1859
  function markTaskStatus(task, status, at) {
1860
- if (task.status !== status && at != null) {
1860
+ if (task.status !== status) {
1861
1861
  if (status === "in_progress") {
1862
- task.startedAt = task.startedAt ?? at;
1863
- } else if (status === "completed" && task.startedAt != null) {
1862
+ task.startedAt = at;
1863
+ delete task.elapsedMs;
1864
+ } else if (status === "completed" && task.startedAt != null && at != null) {
1864
1865
  task.elapsedMs = Math.max(0, at - task.startedAt);
1866
+ } else if (status === "pending") {
1867
+ delete task.startedAt;
1868
+ delete task.elapsedMs;
1865
1869
  }
1866
1870
  }
1867
1871
  task.status = status;
@@ -1985,13 +1989,17 @@ function updatePlanFromToolResult(session, pending, message) {
1985
1989
  session.tasks = new Map();
1986
1990
  for (const task of value.tasks || value) {
1987
1991
  const id = String(task.id);
1988
- session.tasks.set(id, {
1992
+ const restored = {
1989
1993
  id,
1990
1994
  subject: task.subject || task.description || "작업",
1991
- status: task.status || "pending",
1995
+ status: previous.get(id)?.status || "pending",
1992
1996
  // 이 턴에서 다룬 적 없는 작업은 이전 턴의 것이므로 원래 turnId를 지켜 준다.
1993
1997
  turnId: previous.get(id)?.turnId ?? turnId,
1994
- });
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);
1995
2003
  }
1996
2004
  session.tasks = latestTaskPlan(session.tasks);
1997
2005
  emitPlan(session);
@@ -2020,8 +2028,10 @@ function emitPlan(session) {
2020
2028
  threadId: session.id,
2021
2029
  turnId: session.turn?.id,
2022
2030
  plan: [...session.tasks.values()].map((task, index) => ({
2031
+ id: task.id,
2023
2032
  step: numberedTaskSubject(task.subject, index),
2024
2033
  status: planStatus(task.status),
2034
+ elapsedMs: task.elapsedMs ?? null,
2025
2035
  })),
2026
2036
  });
2027
2037
  }
@@ -2861,13 +2871,28 @@ async function processResult(session, message) {
2861
2871
  notify("warning", { threadId: session.id, provider: "Claude", message: `Claude response error: ${text}` });
2862
2872
  }
2863
2873
  if (error && waitForUsageLimit(session)) return;
2864
- finishTurn(session, error, message.duration_ms);
2865
- notify("claude/account/updated", {
2866
- threadId: session.id,
2867
- account: await safeAccount(session.query),
2868
- usage: await safeUsage(session.query),
2869
- });
2870
- await runPendingPrompt(session);
2874
+ // A steer the CLI could not fold into this run gets a run of its own, which
2875
+ // `queued_turn_count` announces. Closing the turn here dropped the host's
2876
+ // spinner and filed a completion until that answer's first frame arrived.
2877
+ // A count of 0 can still hide a steer read just after the result; the steer
2878
+ // debt opens a fresh turn for it then, as before.
2879
+ const steerFollows = !error && !interrupted && !assistantError
2880
+ && session.steerPending > 0 && message.queued_turn_count > 0;
2881
+ if (steerFollows) {
2882
+ session.steerPending -= 1;
2883
+ session.turn.awaitingSteer = true;
2884
+ releaseHeldProgress(session);
2885
+ flushPendingPlan(session);
2886
+ clearForegroundSubagents(session);
2887
+ } else {
2888
+ finishTurn(session, error, message.duration_ms);
2889
+ }
2890
+ // Not awaited: the next run's frames queue behind this message, and the
2891
+ // account lookup used to hold them back.
2892
+ Promise.all([safeAccount(session.query), safeUsage(session.query)])
2893
+ .then(([account, usage]) => notify("claude/account/updated", { threadId: session.id, account, usage }))
2894
+ .catch(() => {});
2895
+ if (!steerFollows) await runPendingPrompt(session);
2871
2896
  }
2872
2897
 
2873
2898
  // Compaction ends the turn's only assistant message, so the context figure would
@@ -2971,6 +2996,7 @@ async function consumeMessage(session, message) {
2971
2996
  // A queued SDK response may beat the timer. It already resumes the work.
2972
2997
  if (!message.parent_tool_use_id && (message.type === "assistant" || message.type === "stream_event")) {
2973
2998
  clearUsageLimitWait(session);
2999
+ if (session.turn) session.turn.awaitingSteer = false;
2974
3000
  }
2975
3001
  if (message.type === "stream_event") {
2976
3002
  if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
@@ -3348,16 +3374,18 @@ function historyState(messages) {
3348
3374
  tasks.clear();
3349
3375
  for (const task of message.tool_use_result.tasks) {
3350
3376
  const previous = known.get(String(task.id));
3351
- tasks.set(String(task.id), {
3377
+ const restored = {
3352
3378
  id: String(task.id),
3353
3379
  subject: task.subject || "작업",
3354
- status: task.status || "pending",
3380
+ status: previous?.status || "pending",
3355
3381
  turnId: previous?.turnId ?? turn.id,
3356
3382
  // A listing restates the plan; it does not re-run it, so the
3357
3383
  // timings already measured for these steps have to survive it.
3358
3384
  startedAt: previous?.startedAt,
3359
3385
  elapsedMs: previous?.elapsedMs,
3360
- });
3386
+ };
3387
+ markTaskStatus(restored, task.status || "pending", messageTime(message));
3388
+ tasks.set(String(task.id), restored);
3361
3389
  }
3362
3390
  const current = latestTaskPlan(tasks);
3363
3391
  tasks.clear();
@@ -3381,6 +3409,7 @@ function historyState(messages) {
3381
3409
  // The text alone cannot say how long a step took, so the measured times ride
3382
3410
  // alongside it and the restored plan shows its total instead of zero.
3383
3411
  const steps = [...tasks.values()].map((task, index) => ({
3412
+ id: task.id,
3384
3413
  step: numberedTaskSubject(task.subject, index),
3385
3414
  status: task.status || "pending",
3386
3415
  elapsedMs: task.elapsedMs ?? null,
@@ -3871,6 +3900,9 @@ async function dispatch(method, params = {}) {
3871
3900
  if (session.turn === turn) delete turn.interruptRequested;
3872
3901
  throw error;
3873
3902
  }
3903
+ // Kept open for a steer whose run has not begun: nothing is running to
3904
+ // send the result that would close it.
3905
+ if (session.turn === turn && turn.awaitingSteer) finishTurn(session, null);
3874
3906
  }
3875
3907
  return {};
3876
3908
  }
@@ -4450,6 +4482,31 @@ async function runSelfTest() {
4450
4482
  if (timedPlan?.steps?.[0]?.elapsedMs !== 6000 || timedPlan.steps[0].status !== "completed") {
4451
4483
  throw new Error(`Claude plan step timing self-test failed: ${JSON.stringify(timedPlan)}`);
4452
4484
  }
4485
+ const timedLiveSession = {
4486
+ id: "timed-live-plan",
4487
+ turn: { id: "timed-live-turn" },
4488
+ tasks: historyState(timedMessages).tasks,
4489
+ };
4490
+ const timedLiveEvents = [];
4491
+ const timedLiveWrite = process.stdout.write;
4492
+ process.stdout.write = (chunk) => { timedLiveEvents.push(JSON.parse(String(chunk))); return true; };
4493
+ try {
4494
+ updatePlanFromToolResult(timedLiveSession, { name: "TaskList" }, {
4495
+ tool_use_result: { tasks: [{ id: "t1", subject: "1. 확인한 설정", status: "completed" }] },
4496
+ });
4497
+ } finally {
4498
+ process.stdout.write = timedLiveWrite;
4499
+ }
4500
+ const liveMeasured = timedLiveEvents.at(-1)?.params?.plan?.[0];
4501
+ if (liveMeasured?.elapsedMs !== 6000 || timedLiveSession.tasks.get("t1")?.elapsedMs !== 6000) {
4502
+ throw new Error(`Claude live plan timing self-test failed: ${JSON.stringify(liveMeasured)}`);
4503
+ }
4504
+ const restartedTask = { status: "pending" };
4505
+ markTaskStatus(restartedTask, "in_progress", 1000);
4506
+ markTaskStatus(restartedTask, "completed", 7000);
4507
+ markTaskStatus(restartedTask, "in_progress", 9000);
4508
+ markTaskStatus(restartedTask, "completed", 10000);
4509
+ if (restartedTask.elapsedMs !== 1000) throw new Error("Claude restarted task timing self-test failed");
4453
4510
  const skippedTasks = new Map([
4454
4511
  ["1", { id: "1", subject: "1. 조사", status: "pending" }],
4455
4512
  ["2", { id: "2", subject: "2. 분석", status: "pending" }],
@@ -4831,6 +4888,64 @@ async function runSelfTest() {
4831
4888
  if (authTurn?.error?.codexErrorInfo !== "unauthorized" || !authTurn.error.message.includes("401")) {
4832
4889
  throw new Error(`Claude login failure lost its tag: ${JSON.stringify(authTurn)}`);
4833
4890
  }
4891
+ // A steer answered in a run of its own stays in the turn it joined.
4892
+ const steerNotices = [];
4893
+ const steerWrite = process.stdout.write;
4894
+ process.stdout.write = (chunk) => { steerNotices.push(JSON.parse(String(chunk))); return true; };
4895
+ const steerSession = {
4896
+ ...automaticTurnSession,
4897
+ id: "steer-continuation-self-test",
4898
+ turn: null,
4899
+ streamBlocks: new Map(),
4900
+ tools: new Map(),
4901
+ tasks: new Map(),
4902
+ subagents: new Map(),
4903
+ pendingPrompts: [],
4904
+ query: { ...automaticTurnSession.query, async interrupt() {} },
4905
+ };
4906
+ sessions.set(steerSession.id, steerSession);
4907
+ const steerResult = (queued) => ({ type: "result", is_error: false, stop_reason: "end_turn", modelUsage: {}, queued_turn_count: queued });
4908
+ const steerRun = { type: "stream_event", parent_tool_use_id: null, event: { type: "message_start", message: {} } };
4909
+ const steerLifecycle = () => steerNotices
4910
+ .filter((notice) => notice.method === "turn/started" || notice.method === "turn/completed")
4911
+ .map((notice) => notice.method)
4912
+ .join();
4913
+ try {
4914
+ const steerTurn = beginTurn(steerSession);
4915
+ steerSession.steerPending = 1;
4916
+ await consumeMessage(steerSession, steerResult(1));
4917
+ await consumeMessage(steerSession, steerRun);
4918
+ if (steerSession.turn?.id !== steerTurn || steerSession.steerPending !== 0) {
4919
+ throw new Error("Claude steer run left the turn it joined");
4920
+ }
4921
+ await consumeMessage(steerSession, steerResult(0));
4922
+ if (steerLifecycle() !== "turn/started,turn/completed") {
4923
+ throw new Error(`Claude steer run reopened its turn: ${steerLifecycle()}`);
4924
+ }
4925
+ // A steer read only after the result still gets a turn from the debt.
4926
+ steerNotices.length = 0;
4927
+ beginTurn(steerSession);
4928
+ steerSession.steerPending = 1;
4929
+ await consumeMessage(steerSession, steerResult(0));
4930
+ await consumeMessage(steerSession, steerRun);
4931
+ if (steerLifecycle() !== "turn/started,turn/completed,turn/started") {
4932
+ throw new Error(`Claude late steer lost its turn: ${steerLifecycle()}`);
4933
+ }
4934
+ // Esc before the steer's run begins closes the turn instead of stranding it.
4935
+ finishTurn(steerSession, null);
4936
+ beginTurn(steerSession);
4937
+ steerSession.steerPending = 1;
4938
+ await consumeMessage(steerSession, steerResult(1));
4939
+ steerNotices.length = 0;
4940
+ await dispatch("session/interrupt", { sessionId: steerSession.id });
4941
+ const stopped = steerNotices.find((notice) => notice.method === "turn/completed")?.params?.turn;
4942
+ if (steerSession.turn || stopped?.status !== "interrupted") {
4943
+ throw new Error(`Claude steer wait survived Esc: ${JSON.stringify(stopped)}`);
4944
+ }
4945
+ } finally {
4946
+ process.stdout.write = steerWrite;
4947
+ sessions.delete(steerSession.id);
4948
+ }
4834
4949
  beginTurn(limitSession);
4835
4950
  for (let attempt = 0; attempt < 4; attempt++) {
4836
4951
  await rejectLimit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.9.39",
3
+ "version": "1.9.41",
4
4
  "type": "module",
5
5
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
6
6
  "keywords": [