devez-vibe 1.8.20 → 1.8.22

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
@@ -1364,6 +1364,8 @@ async function createSession(params, resumeId) {
1364
1364
  if (session.turn) finishTurn(session, error);
1365
1365
  })
1366
1366
  .finally(() => {
1367
+ clearUsageLimitWait(session);
1368
+ if (session.turn) finishTurn(session, new Error("Claude 연결이 종료되었습니다."));
1367
1369
  clearSubagents(session);
1368
1370
  });
1369
1371
  session.consumer = consumer;
@@ -2551,6 +2553,80 @@ function toolOutput(content, structured) {
2551
2553
  return content == null ? "" : JSON.stringify(content, null, 2);
2552
2554
  }
2553
2555
 
2556
+ function clearUsageLimitWait(session) {
2557
+ if (!session.usageLimitWait) return false;
2558
+ clearInterval(session.usageLimitWait.timer);
2559
+ session.usageLimitWait = null;
2560
+ notify("claude/usageLimit/waiting", { threadId: session.id, resetsAt: null });
2561
+ return true;
2562
+ }
2563
+
2564
+ function resumeAfterUsageLimit(session, wait, now = Date.now()) {
2565
+ if (!wait || session.usageLimitWait !== wait || now < wait.resetsAt) return;
2566
+ clearUsageLimitWait(session);
2567
+ if (session.turn !== wait.turn || session.turn.interruptRequested) return;
2568
+ // A long sleep must not launch unattended work when the machine wakes up.
2569
+ if (now - wait.lastTick > 30 * 60 * 1000) {
2570
+ finishTurn(session, new Error("사용량 한도가 초기화되었습니다. 계속하려면 요청을 보내 주세요."));
2571
+ return;
2572
+ }
2573
+ session.turn.rateLimitInfo = null;
2574
+ session.turn.assistantError = null;
2575
+ session.turn.sawStreamText = false;
2576
+ session.turn.sawVisibleText = false;
2577
+ notify("warning", { threadId: session.id, message: "Claude 사용량 한도 초기화 시각이 되어 작업을 이어갑니다." });
2578
+ // Reuse the live query: model, effort, role policy and permission checks stay
2579
+ // intact. Never replay the original prompt (which may contain side effects).
2580
+ try {
2581
+ session.queue.push({
2582
+ type: "user",
2583
+ message: { role: "user", content: [{ type: "text", text: "사용량 한도로 중단된 작업을 이어서 진행하세요. 이미 완료한 작업은 반복하지 말고 현재 상태를 확인한 뒤 남은 작업을 수행하세요." }] },
2584
+ parent_tool_use_id: null,
2585
+ session_id: session.id,
2586
+ });
2587
+ } catch (error) {
2588
+ finishTurn(session, error);
2589
+ }
2590
+ }
2591
+
2592
+ function waitForUsageLimit(session) {
2593
+ const turn = session.turn;
2594
+ const info = turn?.rateLimitInfo;
2595
+ const now = Date.now();
2596
+ const resetsAt = typeof info?.resetsAt === "number" ? info.resetsAt * 1000 + 1000 : NaN;
2597
+ if (!turn || turn.interruptRequested || turn.assistantError !== "rate_limit" || info?.status !== "rejected"
2598
+ || !["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"].includes(info.rateLimitType)
2599
+ || info.isUsingOverage || info.overageInUse
2600
+ || !Number.isFinite(resetsAt) || resetsAt <= now || resetsAt - now > 24 * 60 * 60 * 1000
2601
+ || (turn.usageLimitRetries || 0) >= 3) return false;
2602
+ if (session.usageLimitWait) return true;
2603
+ turn.usageLimitRetries = (turn.usageLimitRetries || 0) + 1;
2604
+ flushPendingPlan(session);
2605
+ clearForegroundSubagents(session);
2606
+ session.streamBlocks.clear();
2607
+ const wait = { turn, resetsAt, lastTick: now, timer: null };
2608
+ session.usageLimitWait = wait;
2609
+ notify("claude/usageLimit/waiting", { threadId: session.id, resetsAt: Math.ceil(resetsAt / 1000) });
2610
+ wait.timer = setInterval(() => {
2611
+ const tick = Date.now();
2612
+ resumeAfterUsageLimit(session, wait, tick);
2613
+ wait.lastTick = tick;
2614
+ }, 1000);
2615
+ notify("warning", {
2616
+ threadId: session.id,
2617
+ message: `Claude 사용량 한도에 도달했습니다. ${new Date(resetsAt).toLocaleString("ko-KR")} 이후 자동으로 이어갑니다.\nEsc로 대기를 취소할 수 있습니다.`,
2618
+ });
2619
+ return true;
2620
+ }
2621
+
2622
+ function cancelUsageLimitWait(session) {
2623
+ if (!clearUsageLimitWait(session)) return false;
2624
+ if (session.turn) session.turn.interruptRequested = true;
2625
+ finishTurn(session, null);
2626
+ notify("warning", { threadId: session.id, message: "Claude 사용량 한도 자동 재개 대기를 취소했습니다." });
2627
+ return true;
2628
+ }
2629
+
2554
2630
  async function processResult(session, message) {
2555
2631
  if (!session.turn) return;
2556
2632
  for (const denial of Array.isArray(message.permission_denials) ? message.permission_denials : []) {
@@ -2580,8 +2656,9 @@ async function processResult(session, message) {
2580
2656
  },
2581
2657
  });
2582
2658
  const error = message.is_error && !interrupted
2583
- ? { message: message.errors?.join("\n") || message.stop_reason || "Claude 실행 실패" }
2659
+ ? { message: message.errors?.join("\n") || message.result || message.stop_reason || "Claude 실행 실패" }
2584
2660
  : null;
2661
+ if (error && waitForUsageLimit(session)) return;
2585
2662
  finishTurn(session, error, message.duration_ms);
2586
2663
  notify("claude/account/updated", {
2587
2664
  threadId: session.id,
@@ -2654,10 +2731,11 @@ async function runPendingPrompt(session) {
2654
2731
  }
2655
2732
 
2656
2733
  function finishTurn(session, error, durationMs) {
2734
+ clearUsageLimitWait(session);
2657
2735
  if (!session.turn) return;
2658
2736
  flushPendingPlan(session);
2659
2737
  clearForegroundSubagents(session);
2660
- const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
2738
+ const turn = { id: session.turn.id, status: error ? "failed" : session.turn.interruptRequested ? "interrupted" : "completed" };
2661
2739
  if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
2662
2740
  if (durationMs != null) turn.durationMs = durationMs;
2663
2741
  notify("turn/completed", { threadId: session.id, turn });
@@ -2681,12 +2759,19 @@ function beginUntrackedTurn(session, message) {
2681
2759
  async function consumeMessage(session, message) {
2682
2760
  adoptSessionId(session, message.session_id);
2683
2761
  beginUntrackedTurn(session, message);
2762
+ // A queued SDK response may beat the timer. It already resumes the work.
2763
+ if (!message.parent_tool_use_id && (message.type === "assistant" || message.type === "stream_event")) {
2764
+ clearUsageLimitWait(session);
2765
+ }
2684
2766
  if (message.type === "stream_event") {
2685
2767
  if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
2686
2768
  if (session.turn) session.turn.sawStreamText = true;
2687
2769
  }
2688
2770
  await processStreamEvent(session, message);
2689
- } else if (message.type === "assistant") processAssistant(session, message);
2771
+ } else if (message.type === "assistant") {
2772
+ if (session.turn && !message.parent_tool_use_id) session.turn.assistantError = message.error || null;
2773
+ processAssistant(session, message);
2774
+ }
2690
2775
  else if (message.type === "user") processUser(session, message);
2691
2776
  else if (message.type === "result") await processResult(session, message);
2692
2777
  else if (message.type === "system" && processSubagentSystemMessage(session, message)) {
@@ -2702,6 +2787,7 @@ async function consumeMessage(session, message) {
2702
2787
  reason: message.decision_reason || message.decision_reason_type,
2703
2788
  });
2704
2789
  } else if (message.type === "rate_limit_event") {
2790
+ if (session.turn) session.turn.rateLimitInfo = message.rate_limit_info;
2705
2791
  notify("claude/account/updated", { threadId: session.id, rateLimitInfo: message.rate_limit_info });
2706
2792
  } else if (message.type === "system" && message.subtype === "api_retry") {
2707
2793
  notify("warning", { threadId: session.id, provider: "Claude", message: `Claude API 재시도 ${message.attempt}/${message.max_retries}` });
@@ -2785,6 +2871,7 @@ async function startPrompt(params) {
2785
2871
  const id = liveSessionId(params.sessionId);
2786
2872
  const session = sessions.get(id);
2787
2873
  if (!session) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
2874
+ cancelUsageLimitWait(session);
2788
2875
  // Claude runs one turn at a time, so extra input waits its turn instead of
2789
2876
  // failing — the same queueing the CLI does for a prompt typed while it works.
2790
2877
  if (session.turn) {
@@ -2801,6 +2888,7 @@ async function steerPrompt(params) {
2801
2888
  const id = liveSessionId(params.sessionId);
2802
2889
  const session = sessions.get(id);
2803
2890
  if (!session) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
2891
+ cancelUsageLimitWait(session);
2804
2892
  if (!session.turn) return runPrompt(session, params);
2805
2893
  if (params.expectedTurnId && params.expectedTurnId !== session.turn.id) {
2806
2894
  throw new Error(`turn ID가 일치하지 않습니다: ${params.expectedTurnId}`);
@@ -3501,6 +3589,7 @@ async function dispatch(method, params = {}) {
3501
3589
  if (session) {
3502
3590
  session.pendingPrompts.length = 0;
3503
3591
  session.steerPending = 0;
3592
+ if (cancelUsageLimitWait(session)) return {};
3504
3593
  }
3505
3594
  if (session?.turn) {
3506
3595
  const turn = session.turn;
@@ -3543,6 +3632,7 @@ async function dispatch(method, params = {}) {
3543
3632
  if (method === "session/close") {
3544
3633
  const session = lookupSession(params.sessionId);
3545
3634
  if (session) {
3635
+ clearUsageLimitWait(session);
3546
3636
  clearSubagents(session);
3547
3637
  session.queue.close();
3548
3638
  session.query.close();
@@ -3561,6 +3651,7 @@ async function dispatch(method, params = {}) {
3561
3651
  }
3562
3652
  if (method === "shutdown") {
3563
3653
  for (const session of sessions.values()) {
3654
+ clearUsageLimitWait(session);
3564
3655
  clearSubagents(session);
3565
3656
  session.queue.close();
3566
3657
  session.query.close();
@@ -4206,6 +4297,107 @@ async function runSelfTest() {
4206
4297
  async usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET() { return null; },
4207
4298
  },
4208
4299
  };
4300
+ // Replay SDK limit/result frames without spending the account's allowance.
4301
+ const retryInputs = [];
4302
+ const limitSession = {
4303
+ ...automaticTurnSession,
4304
+ id: "usage-limit-self-test",
4305
+ query: { ...automaticTurnSession.query, async applyFlagSettings() {} },
4306
+ subagents: new Map(),
4307
+ streamBlocks: new Map(),
4308
+ queue: { push(value) { retryInputs.push(value); } },
4309
+ };
4310
+ sessions.set(limitSession.id, limitSession);
4311
+ const failedResult = { type: "result", is_error: true, errors: ["Usage limit reached"], modelUsage: {} };
4312
+ const rejectLimit = async (extra = {}) => {
4313
+ await consumeMessage(limitSession, {
4314
+ type: "rate_limit_event",
4315
+ rate_limit_info: { status: "rejected", rateLimitType: "five_hour", resetsAt: Date.now() / 1000 + 60, ...extra },
4316
+ });
4317
+ await consumeMessage(limitSession, {
4318
+ type: "assistant", error: "rate_limit", message: { content: [] },
4319
+ });
4320
+ };
4321
+ beginTurn(limitSession);
4322
+ await rejectLimit();
4323
+ await consumeMessage(limitSession, failedResult);
4324
+ if (!limitSession.usageLimitWait || !limitSession.turn) throw new Error("Usage limit did not wait in the active turn");
4325
+ const waitingTurn = limitSession.turn;
4326
+ const wait = limitSession.usageLimitWait;
4327
+ resumeAfterUsageLimit(limitSession, wait, wait.resetsAt);
4328
+ resumeAfterUsageLimit(limitSession, wait, wait.resetsAt);
4329
+ if (retryInputs.length !== 1 || limitSession.turn !== waitingTurn
4330
+ || retryInputs[0].message.content[0].text.includes("Usage limit reached")) {
4331
+ throw new Error("Usage limit continuation duplicated or replaced the session turn");
4332
+ }
4333
+ await rejectLimit();
4334
+ await consumeMessage(limitSession, failedResult);
4335
+ const cancelledWait = limitSession.usageLimitWait;
4336
+ await dispatch("session/interrupt", { sessionId: limitSession.id });
4337
+ resumeAfterUsageLimit(limitSession, cancelledWait, cancelledWait.resetsAt);
4338
+ if (limitSession.turn || retryInputs.length !== 1) throw new Error("Cancelled usage limit resumed");
4339
+ for (const info of [
4340
+ { resetsAt: undefined }, { resetsAt: NaN }, { resetsAt: Date.now() / 1000 - 1 },
4341
+ { resetsAt: Date.now() / 1000 + 90000 }, { status: "allowed_warning" },
4342
+ { rateLimitType: "overage" }, { isUsingOverage: true },
4343
+ ]) {
4344
+ beginTurn(limitSession);
4345
+ await rejectLimit(info);
4346
+ await consumeMessage(limitSession, failedResult);
4347
+ if (limitSession.usageLimitWait || limitSession.turn) throw new Error("Ineligible usage limit started a wait");
4348
+ }
4349
+ beginTurn(limitSession);
4350
+ await rejectLimit();
4351
+ await consumeMessage(limitSession, { ...failedResult, is_error: false });
4352
+ if (limitSession.usageLimitWait) throw new Error("Successful turn started a usage wait");
4353
+ beginTurn(limitSession);
4354
+ await rejectLimit();
4355
+ await consumeMessage(limitSession, { type: "assistant", error: "server_error", message: { content: [] } });
4356
+ await consumeMessage(limitSession, failedResult);
4357
+ if (limitSession.usageLimitWait) throw new Error("Unrelated server error started a usage wait");
4358
+ beginTurn(limitSession);
4359
+ for (let attempt = 0; attempt < 4; attempt++) {
4360
+ await rejectLimit();
4361
+ await consumeMessage(limitSession, failedResult);
4362
+ const retry = limitSession.usageLimitWait;
4363
+ if (attempt < 3) {
4364
+ if (!retry) throw new Error("Usage limit retry ended too early");
4365
+ resumeAfterUsageLimit(limitSession, retry, retry.resetsAt);
4366
+ } else if (retry || limitSession.turn) throw new Error("Repeated limits retried without a bound");
4367
+ }
4368
+ beginTurn(limitSession);
4369
+ await rejectLimit();
4370
+ await consumeMessage(limitSession, failedResult);
4371
+ const sleptWait = limitSession.usageLimitWait;
4372
+ const beforeSleep = retryInputs.length;
4373
+ resumeAfterUsageLimit(limitSession, sleptWait, sleptWait.resetsAt + 31 * 60 * 1000);
4374
+ if (limitSession.turn || retryInputs.length !== beforeSleep) throw new Error("Long sleep resumed unattended work");
4375
+ beginTurn(limitSession);
4376
+ await rejectLimit();
4377
+ await consumeMessage(limitSession, failedResult);
4378
+ const replacedWait = limitSession.usageLimitWait;
4379
+ await startPrompt({ sessionId: limitSession.id, input: [{ type: "text", text: "새 요청" }] });
4380
+ resumeAfterUsageLimit(limitSession, replacedWait, replacedWait.resetsAt);
4381
+ if (retryInputs.length !== beforeSleep + 1
4382
+ || retryInputs.at(-1).message.content[0].text !== "새 요청") throw new Error("New prompt did not replace the wait");
4383
+ finishTurn(limitSession, null);
4384
+ beginTurn(limitSession);
4385
+ await rejectLimit();
4386
+ await consumeMessage(limitSession, failedResult);
4387
+ limitSession.usageLimitWait.resetsAt = Date.now() + 10;
4388
+ await new Promise((resolve) => setTimeout(resolve, 1100));
4389
+ if (limitSession.usageLimitWait || retryInputs.length !== beforeSleep + 2) throw new Error("Usage limit timer did not resume");
4390
+ finishTurn(limitSession, null);
4391
+ beginTurn(limitSession);
4392
+ await rejectLimit();
4393
+ await consumeMessage(limitSession, failedResult);
4394
+ const closedWait = limitSession.usageLimitWait;
4395
+ limitSession.query.close = () => {};
4396
+ limitSession.queue.close = () => {};
4397
+ await dispatch("session/close", { sessionId: limitSession.id });
4398
+ resumeAfterUsageLimit(limitSession, closedWait, closedWait.resetsAt);
4399
+ if (retryInputs.length !== beforeSleep + 2) throw new Error("Closed session resumed");
4400
+ sessions.delete(limitSession.id);
4209
4401
  await consumeMessage(automaticTurnSession, {
4210
4402
  type: "assistant",
4211
4403
  parent_tool_use_id: null,
@@ -4994,6 +5186,7 @@ lines.on("line", async (line) => {
4994
5186
 
4995
5187
  lines.on("close", () => {
4996
5188
  for (const session of sessions.values()) {
5189
+ clearUsageLimitWait(session);
4997
5190
  clearSubagents(session);
4998
5191
  session.query.close();
4999
5192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.8.20",
3
+ "version": "1.8.22",
4
4
  "type": "module",
5
5
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
6
6
  "keywords": [