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 +0 -0
- package/bridge/claude-agent-sdk-bridge.mjs +196 -3
- package/package.json +1 -1
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")
|
|
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
|
}
|