repoburg 1.3.266 → 1.3.268
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/agent-pane/backend/dist/index.js +781 -111
- package/agent-pane/backend/dist/server.js +39624 -38951
- package/agent-pane/frontend/dist/assets/index-CGMuuJwp.css +1 -0
- package/agent-pane/frontend/dist/assets/index-DJdEb4sk.js +567 -0
- package/agent-pane/frontend/dist/index.html +2 -2
- package/daemon/dist/agentPane.js +5 -0
- package/daemon/dist/agentPane.spec.js +16 -2
- package/daemon/dist/index.js +8 -0
- package/package.json +1 -1
- package/tabtape/extension/dist/build-info.json +3 -3
- package/tabtape/extension/dist/manifest.json +1 -1
- package/tabtape/ui/dist/build-info.json +3 -3
- package/agent-pane/frontend/dist/assets/index-BWI1sdtn.js +0 -532
- package/agent-pane/frontend/dist/assets/index-CsD8SrgL.css +0 -1
|
@@ -757,7 +757,7 @@ var require_plugin = __commonJS({
|
|
|
757
757
|
const name = this.name;
|
|
758
758
|
let completed = false;
|
|
759
759
|
this.options = typeof this.options === "function" ? this.options(this.server) : this.options;
|
|
760
|
-
let
|
|
760
|
+
let timer2 = null;
|
|
761
761
|
const done = (execErr) => {
|
|
762
762
|
if (completed) {
|
|
763
763
|
debug("loading complete", name);
|
|
@@ -770,16 +770,16 @@ var require_plugin = __commonJS({
|
|
|
770
770
|
debug("exec completed", name);
|
|
771
771
|
}
|
|
772
772
|
completed = true;
|
|
773
|
-
if (
|
|
774
|
-
clearTimeout(
|
|
773
|
+
if (timer2) {
|
|
774
|
+
clearTimeout(timer2);
|
|
775
775
|
}
|
|
776
776
|
callback(execErr);
|
|
777
777
|
};
|
|
778
778
|
if (this.timeout > 0) {
|
|
779
779
|
debug("setting up timeout", name, this.timeout);
|
|
780
|
-
|
|
780
|
+
timer2 = setTimeout(function() {
|
|
781
781
|
debug("timed out", name);
|
|
782
|
-
|
|
782
|
+
timer2 = null;
|
|
783
783
|
const readyTimeoutErr = new AVV_ERR_PLUGIN_EXEC_TIMEOUT(name);
|
|
784
784
|
readyTimeoutErr.fn = func;
|
|
785
785
|
done(readyTimeoutErr);
|
|
@@ -1366,9 +1366,9 @@ var require_avvio = __commonJS({
|
|
|
1366
1366
|
function timeoutCall(func, rootErr, context, cb) {
|
|
1367
1367
|
const name = func.unwrappedName ?? func.name;
|
|
1368
1368
|
debug("setting up ready timeout", name, this._opts.timeout);
|
|
1369
|
-
let
|
|
1369
|
+
let timer2 = setTimeout(() => {
|
|
1370
1370
|
debug("timed out", name);
|
|
1371
|
-
|
|
1371
|
+
timer2 = null;
|
|
1372
1372
|
const toutErr = new AVV_ERR_READY_TIMEOUT(name);
|
|
1373
1373
|
toutErr.fn = func;
|
|
1374
1374
|
this._error = toutErr;
|
|
@@ -1380,9 +1380,9 @@ var require_avvio = __commonJS({
|
|
|
1380
1380
|
func(rootErr, context, timeoutCb.bind(this));
|
|
1381
1381
|
}
|
|
1382
1382
|
function timeoutCb(err) {
|
|
1383
|
-
if (
|
|
1384
|
-
clearTimeout(
|
|
1385
|
-
|
|
1383
|
+
if (timer2) {
|
|
1384
|
+
clearTimeout(timer2);
|
|
1385
|
+
timer2 = null;
|
|
1386
1386
|
this._error = err;
|
|
1387
1387
|
process.nextTick(cb, this._error);
|
|
1388
1388
|
} else {
|
|
@@ -13402,7 +13402,7 @@ var require_context = __commonJS({
|
|
|
13402
13402
|
function Context({
|
|
13403
13403
|
schema,
|
|
13404
13404
|
handler,
|
|
13405
|
-
config,
|
|
13405
|
+
config: config2,
|
|
13406
13406
|
childLoggerFactory,
|
|
13407
13407
|
errorHandler,
|
|
13408
13408
|
bodyLimit,
|
|
@@ -13432,7 +13432,7 @@ var require_context = __commonJS({
|
|
|
13432
13432
|
this.onResponse = null;
|
|
13433
13433
|
this.preSerialization = null;
|
|
13434
13434
|
this.onRequestAbort = null;
|
|
13435
|
-
this.config =
|
|
13435
|
+
this.config = config2;
|
|
13436
13436
|
this.errorHandler = errorHandler || server[kErrorHandler];
|
|
13437
13437
|
this.childLoggerFactory = childLoggerFactory || server[kChildLoggerFactory];
|
|
13438
13438
|
this._middie = null;
|
|
@@ -33623,7 +33623,7 @@ var require_route = __commonJS({
|
|
|
33623
33623
|
}
|
|
33624
33624
|
}
|
|
33625
33625
|
const constraints = opts.constraints || {};
|
|
33626
|
-
const
|
|
33626
|
+
const config2 = {
|
|
33627
33627
|
...opts.config,
|
|
33628
33628
|
url,
|
|
33629
33629
|
method: opts.method
|
|
@@ -33631,7 +33631,7 @@ var require_route = __commonJS({
|
|
|
33631
33631
|
const context = new Context({
|
|
33632
33632
|
schema: opts.schema,
|
|
33633
33633
|
handler: opts.handler.bind(this),
|
|
33634
|
-
config,
|
|
33634
|
+
config: config2,
|
|
33635
33635
|
errorHandler: opts.errorHandler,
|
|
33636
33636
|
childLoggerFactory: opts.childLoggerFactory,
|
|
33637
33637
|
bodyLimit: opts.bodyLimit,
|
|
@@ -41006,7 +41006,6 @@ function initDb(path9 = "data.db") {
|
|
|
41006
41006
|
db.pragma("journal_mode = WAL");
|
|
41007
41007
|
db.pragma("foreign_keys = ON");
|
|
41008
41008
|
createTables(db);
|
|
41009
|
-
cleanupStaleState(db);
|
|
41010
41009
|
return db;
|
|
41011
41010
|
}
|
|
41012
41011
|
function getDb() {
|
|
@@ -41015,17 +41014,6 @@ function getDb() {
|
|
|
41015
41014
|
}
|
|
41016
41015
|
return db;
|
|
41017
41016
|
}
|
|
41018
|
-
function cleanupStaleState(database) {
|
|
41019
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41020
|
-
const staleItems = database.prepare(
|
|
41021
|
-
"UPDATE items SET status = 'failed', updated_at = ? WHERE status = 'in_progress'"
|
|
41022
|
-
).run(now);
|
|
41023
|
-
const staleSessions = database.prepare("UPDATE sessions SET is_running = 0, status = ?, updated_at = ? WHERE is_running = 1").run("active", now);
|
|
41024
|
-
const staleTurns = database.prepare("UPDATE turns SET status = 'failed', error = ?, updated_at = ? WHERE status = 'running'").run("Backend restarted while turn was running", now);
|
|
41025
|
-
if (staleSessions.changes > 0 || staleTurns.changes > 0 || staleItems.changes > 0) {
|
|
41026
|
-
console.log(`[db] Cleaned up stale state: ${staleSessions.changes} sessions, ${staleTurns.changes} turns, ${staleItems.changes} items`);
|
|
41027
|
-
}
|
|
41028
|
-
}
|
|
41029
41017
|
function createTables(database) {
|
|
41030
41018
|
database.exec(`
|
|
41031
41019
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -41169,6 +41157,18 @@ CREATE INDEX IF NOT EXISTS idx_workspaces_path ON workspaces(path);
|
|
|
41169
41157
|
if (!turnColumns.some((c) => c.name === "runtime_input_ids")) {
|
|
41170
41158
|
database.exec("ALTER TABLE turns ADD COLUMN runtime_input_ids TEXT");
|
|
41171
41159
|
}
|
|
41160
|
+
if (!turnColumns.some((c) => c.name === "interrupted_at")) {
|
|
41161
|
+
database.exec("ALTER TABLE turns ADD COLUMN interrupted_at TEXT");
|
|
41162
|
+
}
|
|
41163
|
+
if (!turnColumns.some((c) => c.name === "interrupt_reason")) {
|
|
41164
|
+
database.exec("ALTER TABLE turns ADD COLUMN interrupt_reason TEXT");
|
|
41165
|
+
}
|
|
41166
|
+
if (!turnColumns.some((c) => c.name === "resume_state")) {
|
|
41167
|
+
database.exec("ALTER TABLE turns ADD COLUMN resume_state TEXT");
|
|
41168
|
+
}
|
|
41169
|
+
if (!turnColumns.some((c) => c.name === "resume_attempts")) {
|
|
41170
|
+
database.exec("ALTER TABLE turns ADD COLUMN resume_attempts INTEGER NOT NULL DEFAULT 0");
|
|
41171
|
+
}
|
|
41172
41172
|
const threadColumns = database.prepare("PRAGMA table_info(discussion_threads)").all();
|
|
41173
41173
|
if (!threadColumns.some((c) => c.name === "external_thread_id")) {
|
|
41174
41174
|
database.exec("ALTER TABLE discussion_threads ADD COLUMN external_thread_id TEXT");
|
|
@@ -41793,8 +41793,13 @@ function isSessionBusy(db2, session, opts) {
|
|
|
41793
41793
|
return false;
|
|
41794
41794
|
}
|
|
41795
41795
|
function recomputeSessionRunning(db2, sessionId, idleStatus = "active", hasUnreviewed = false) {
|
|
41796
|
-
const live = db2.prepare(
|
|
41796
|
+
const live = db2.prepare(
|
|
41797
|
+
`SELECT COUNT(*) AS c,
|
|
41798
|
+
COALESCE(SUM(CASE WHEN status = 'pending' AND resume_state = 'waiting' THEN 1 ELSE 0 END), 0) AS w
|
|
41799
|
+
FROM turns WHERE session_id = ? AND status IN ('pending', 'running')`
|
|
41800
|
+
).get(sessionId);
|
|
41797
41801
|
const isRunning = live.c > 0 ? 1 : 0;
|
|
41802
|
+
const liveStatus = live.c > 0 && live.w === live.c ? "interrupted" : "running";
|
|
41798
41803
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41799
41804
|
if (hasUnreviewed && isRunning === 0) {
|
|
41800
41805
|
db2.prepare("UPDATE sessions SET is_running = ?, status = ?, has_unreviewed = 1, updated_at = ? WHERE id = ?").run(
|
|
@@ -41806,13 +41811,406 @@ function recomputeSessionRunning(db2, sessionId, idleStatus = "active", hasUnrev
|
|
|
41806
41811
|
} else {
|
|
41807
41812
|
db2.prepare("UPDATE sessions SET is_running = ?, status = ?, updated_at = ? WHERE id = ?").run(
|
|
41808
41813
|
isRunning,
|
|
41809
|
-
isRunning ?
|
|
41814
|
+
isRunning ? liveStatus : idleStatus,
|
|
41810
41815
|
now,
|
|
41811
41816
|
sessionId
|
|
41812
41817
|
);
|
|
41813
41818
|
}
|
|
41814
41819
|
}
|
|
41815
41820
|
|
|
41821
|
+
// src/resume/interruptions.ts
|
|
41822
|
+
var HOST_RESTART_ERROR = "Backend restarted while turn was running";
|
|
41823
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
41824
|
+
function onTurnInterrupted(listener) {
|
|
41825
|
+
listeners.add(listener);
|
|
41826
|
+
return () => listeners.delete(listener);
|
|
41827
|
+
}
|
|
41828
|
+
function getTurn(db2, turnId) {
|
|
41829
|
+
return db2.prepare(
|
|
41830
|
+
`SELECT id, session_id, status, thread_id, interrupted_at, interrupt_reason, resume_state, resume_attempts, updated_at
|
|
41831
|
+
FROM turns WHERE id = ?`
|
|
41832
|
+
).get(turnId);
|
|
41833
|
+
}
|
|
41834
|
+
function isLive(status) {
|
|
41835
|
+
return status === "pending" || status === "running";
|
|
41836
|
+
}
|
|
41837
|
+
function addInterruptionItem(db2, sessionId, turnId, data) {
|
|
41838
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41839
|
+
const item = {
|
|
41840
|
+
id: v4_default(),
|
|
41841
|
+
turn_id: turnId,
|
|
41842
|
+
external_item_id: `interruption-${v4_default()}`,
|
|
41843
|
+
type: "interruption",
|
|
41844
|
+
data,
|
|
41845
|
+
status: "completed",
|
|
41846
|
+
created_at: now,
|
|
41847
|
+
updated_at: now
|
|
41848
|
+
};
|
|
41849
|
+
db2.prepare(
|
|
41850
|
+
"INSERT INTO items (id, turn_id, external_item_id, type, data, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
41851
|
+
).run(item.id, turnId, item.external_item_id, item.type, JSON.stringify(data), item.status, now, now);
|
|
41852
|
+
broadcast("item:completed", { sessionId, turnId, item });
|
|
41853
|
+
return item;
|
|
41854
|
+
}
|
|
41855
|
+
function updateLatestInterruptionItem(db2, sessionId, turnId, patch) {
|
|
41856
|
+
const row = db2.prepare(
|
|
41857
|
+
"SELECT id, external_item_id, data, created_at FROM items WHERE turn_id = ? AND type = 'interruption' ORDER BY created_at DESC, rowid DESC LIMIT 1"
|
|
41858
|
+
).get(turnId);
|
|
41859
|
+
if (!row) return;
|
|
41860
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41861
|
+
const data = { ...row.data ? JSON.parse(row.data) : {}, ...patch };
|
|
41862
|
+
db2.prepare("UPDATE items SET data = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(data), now, row.id);
|
|
41863
|
+
const item = {
|
|
41864
|
+
id: row.id,
|
|
41865
|
+
turn_id: turnId,
|
|
41866
|
+
external_item_id: row.external_item_id,
|
|
41867
|
+
type: "interruption",
|
|
41868
|
+
data,
|
|
41869
|
+
status: "completed",
|
|
41870
|
+
created_at: row.created_at,
|
|
41871
|
+
updated_at: now
|
|
41872
|
+
};
|
|
41873
|
+
broadcast("item:completed", { sessionId, turnId, item });
|
|
41874
|
+
}
|
|
41875
|
+
function markTurnInterrupted(db2, turnId, reason, opts = {}) {
|
|
41876
|
+
const turn = getTurn(db2, turnId);
|
|
41877
|
+
if (!turn || !isLive(turn.status)) return false;
|
|
41878
|
+
if (turn.status === "pending" && turn.resume_state === "waiting") return false;
|
|
41879
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41880
|
+
const detail = opts.detail ?? null;
|
|
41881
|
+
const retry = turn.resume_state === "resuming";
|
|
41882
|
+
const at = retry ? turn.interrupted_at ?? now : opts.at ?? now;
|
|
41883
|
+
db2.prepare(
|
|
41884
|
+
`UPDATE turns SET status = 'pending', resume_state = 'waiting', interrupt_reason = ?, interrupted_at = ?, error = NULL, updated_at = ?
|
|
41885
|
+
WHERE id = ?`
|
|
41886
|
+
).run(reason, at, now, turnId);
|
|
41887
|
+
if (retry) {
|
|
41888
|
+
updateLatestInterruptionItem(db2, turn.session_id, turnId, { reason, outcome: "waiting", detail });
|
|
41889
|
+
} else {
|
|
41890
|
+
addInterruptionItem(db2, turn.session_id, turnId, {
|
|
41891
|
+
reason,
|
|
41892
|
+
interrupted_at: at,
|
|
41893
|
+
resumed_at: null,
|
|
41894
|
+
outcome: "waiting",
|
|
41895
|
+
detail
|
|
41896
|
+
});
|
|
41897
|
+
}
|
|
41898
|
+
recomputeSessionRunning(db2, turn.session_id);
|
|
41899
|
+
broadcast("turn:interrupted", {
|
|
41900
|
+
sessionId: turn.session_id,
|
|
41901
|
+
turnId,
|
|
41902
|
+
reason,
|
|
41903
|
+
interruptedAt: at,
|
|
41904
|
+
attempts: turn.resume_attempts,
|
|
41905
|
+
detail
|
|
41906
|
+
});
|
|
41907
|
+
if (opts.notify !== false) {
|
|
41908
|
+
for (const listener of listeners) {
|
|
41909
|
+
try {
|
|
41910
|
+
listener({ sessionId: turn.session_id, turnId, reason });
|
|
41911
|
+
} catch (err) {
|
|
41912
|
+
console.error("[resume] interruption listener failed:", err);
|
|
41913
|
+
}
|
|
41914
|
+
}
|
|
41915
|
+
}
|
|
41916
|
+
return true;
|
|
41917
|
+
}
|
|
41918
|
+
function markTurnResuming(db2, turnId, opts = {}) {
|
|
41919
|
+
const turn = getTurn(db2, turnId);
|
|
41920
|
+
if (!turn || !isResumable(turn, !!opts.manual)) return null;
|
|
41921
|
+
const counted = opts.countAttempt ?? !opts.manual;
|
|
41922
|
+
const attempt = turn.resume_attempts + (counted ? 1 : 0);
|
|
41923
|
+
const reason = turn.interrupt_reason ?? "host_restart";
|
|
41924
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41925
|
+
db2.prepare(
|
|
41926
|
+
`UPDATE turns SET status = 'running', resume_state = 'resuming', resume_attempts = ?, error = NULL, updated_at = ?
|
|
41927
|
+
WHERE id = ?`
|
|
41928
|
+
).run(attempt, now, turnId);
|
|
41929
|
+
updateLatestInterruptionItem(db2, turn.session_id, turnId, { outcome: "resuming" });
|
|
41930
|
+
recomputeSessionRunning(db2, turn.session_id);
|
|
41931
|
+
broadcast("turn:resuming", { sessionId: turn.session_id, turnId, attempt, reason, manual: !!opts.manual });
|
|
41932
|
+
return { sessionId: turn.session_id, attempt, reason };
|
|
41933
|
+
}
|
|
41934
|
+
function isResumable(turn, manual) {
|
|
41935
|
+
if (turn.status === "pending" && turn.resume_state === "waiting") return true;
|
|
41936
|
+
return manual && turn.status === "failed" && (turn.resume_state === "gave_up" || turn.resume_state === "skipped");
|
|
41937
|
+
}
|
|
41938
|
+
function markTurnResumed(db2, turnId, opts = {}) {
|
|
41939
|
+
const turn = getTurn(db2, turnId);
|
|
41940
|
+
if (!turn) return;
|
|
41941
|
+
if (turn.resume_state !== "resuming" && !(opts.settle && turn.resume_state === "waiting")) return;
|
|
41942
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41943
|
+
db2.prepare("UPDATE turns SET resume_state = 'resumed', updated_at = ? WHERE id = ?").run(now, turnId);
|
|
41944
|
+
updateLatestInterruptionItem(db2, turn.session_id, turnId, { outcome: "resumed", resumed_at: now });
|
|
41945
|
+
broadcast("turn:resumed", { sessionId: turn.session_id, turnId, resumedAt: now });
|
|
41946
|
+
}
|
|
41947
|
+
function finalizeInterruptedTurn(db2, turnId, state, error) {
|
|
41948
|
+
const turn = getTurn(db2, turnId);
|
|
41949
|
+
if (!turn || !isLive(turn.status)) return false;
|
|
41950
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41951
|
+
db2.prepare("UPDATE turns SET status = 'failed', resume_state = ?, error = ?, updated_at = ? WHERE id = ?").run(
|
|
41952
|
+
state,
|
|
41953
|
+
error,
|
|
41954
|
+
now,
|
|
41955
|
+
turnId
|
|
41956
|
+
);
|
|
41957
|
+
updateLatestInterruptionItem(db2, turn.session_id, turnId, { outcome: state });
|
|
41958
|
+
recomputeSessionRunning(db2, turn.session_id, "failed");
|
|
41959
|
+
broadcast("turn:failed", { sessionId: turn.session_id, turnId, error, resumeState: state });
|
|
41960
|
+
return true;
|
|
41961
|
+
}
|
|
41962
|
+
function markResumeCancelled(db2, turnId) {
|
|
41963
|
+
const turn = getTurn(db2, turnId);
|
|
41964
|
+
if (!turn || turn.resume_state !== "waiting" && turn.resume_state !== "resuming") return;
|
|
41965
|
+
db2.prepare("UPDATE turns SET resume_state = 'cancelled' WHERE id = ?").run(turnId);
|
|
41966
|
+
updateLatestInterruptionItem(db2, turn.session_id, turnId, { outcome: "cancelled" });
|
|
41967
|
+
}
|
|
41968
|
+
function recoverInterruptedTurns(db2, opts) {
|
|
41969
|
+
const live = db2.prepare(
|
|
41970
|
+
`SELECT t.id, t.session_id, t.status, t.thread_id, t.interrupted_at, t.interrupt_reason, t.resume_state,
|
|
41971
|
+
t.resume_attempts, t.updated_at, s.runtime_type,
|
|
41972
|
+
(SELECT MAX(i.updated_at) FROM items i WHERE i.turn_id = t.id AND i.type != 'interruption') AS last_item_at
|
|
41973
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
41974
|
+
WHERE t.status IN ('pending', 'running')
|
|
41975
|
+
ORDER BY t.created_at ASC`
|
|
41976
|
+
).all();
|
|
41977
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41978
|
+
const items = db2.prepare("UPDATE items SET status = 'failed', updated_at = ? WHERE status = 'in_progress'").run(now);
|
|
41979
|
+
let waiting = 0;
|
|
41980
|
+
let failed = 0;
|
|
41981
|
+
for (const turn of live) {
|
|
41982
|
+
const lastActivity = turn.last_item_at && turn.last_item_at > turn.updated_at ? turn.last_item_at : turn.updated_at;
|
|
41983
|
+
if (turn.thread_id || !opts.canResume(turn.runtime_type)) {
|
|
41984
|
+
if (turn.resume_state !== "waiting") {
|
|
41985
|
+
markTurnInterrupted(db2, turn.id, "host_restart", { at: lastActivity, notify: false });
|
|
41986
|
+
}
|
|
41987
|
+
finalizeInterruptedTurn(db2, turn.id, "unsupported", HOST_RESTART_ERROR);
|
|
41988
|
+
failed++;
|
|
41989
|
+
continue;
|
|
41990
|
+
}
|
|
41991
|
+
if (turn.status === "pending" && turn.resume_state === "waiting") {
|
|
41992
|
+
recomputeSessionRunning(db2, turn.session_id);
|
|
41993
|
+
waiting++;
|
|
41994
|
+
continue;
|
|
41995
|
+
}
|
|
41996
|
+
markTurnInterrupted(db2, turn.id, "host_restart", { at: lastActivity, notify: false });
|
|
41997
|
+
waiting++;
|
|
41998
|
+
}
|
|
41999
|
+
const flagged = db2.prepare("SELECT id FROM sessions WHERE is_running = 1").all();
|
|
42000
|
+
for (const s of flagged) recomputeSessionRunning(db2, s.id);
|
|
42001
|
+
if (waiting > 0 || failed > 0 || items.changes > 0) {
|
|
42002
|
+
console.log(
|
|
42003
|
+
`[resume] recovered interrupted state: ${waiting} turns waiting to resume, ${failed} failed, ${items.changes} items closed`
|
|
42004
|
+
);
|
|
42005
|
+
}
|
|
42006
|
+
return { waiting, failed, items: items.changes };
|
|
42007
|
+
}
|
|
42008
|
+
|
|
42009
|
+
// src/resume/net-probe.ts
|
|
42010
|
+
var DEFAULT_API_URL = "https://api.anthropic.com";
|
|
42011
|
+
function probeTarget(env = process.env) {
|
|
42012
|
+
if (env.HTTPS_PROXY || env.https_proxy || env.ALL_PROXY || env.all_proxy) return null;
|
|
42013
|
+
if (env.CLAUDE_CODE_USE_BEDROCK === "1" || env.CLAUDE_CODE_USE_VERTEX === "1") return null;
|
|
42014
|
+
return env.ANTHROPIC_BASE_URL || DEFAULT_API_URL;
|
|
42015
|
+
}
|
|
42016
|
+
async function probeApi(opts = {}) {
|
|
42017
|
+
const url = opts.url ?? probeTarget(opts.env);
|
|
42018
|
+
if (!url) return true;
|
|
42019
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
42020
|
+
try {
|
|
42021
|
+
const res = await fetchImpl(url, {
|
|
42022
|
+
method: "HEAD",
|
|
42023
|
+
redirect: "manual",
|
|
42024
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
|
|
42025
|
+
});
|
|
42026
|
+
await res.body?.cancel().catch(() => {
|
|
42027
|
+
});
|
|
42028
|
+
return true;
|
|
42029
|
+
} catch {
|
|
42030
|
+
return false;
|
|
42031
|
+
}
|
|
42032
|
+
}
|
|
42033
|
+
|
|
42034
|
+
// src/resume/resume-manager.ts
|
|
42035
|
+
var HOUR = 60 * 60 * 1e3;
|
|
42036
|
+
function envNumber(env, name, fallback) {
|
|
42037
|
+
const raw = env[name];
|
|
42038
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
42039
|
+
const n = Number(raw);
|
|
42040
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
42041
|
+
}
|
|
42042
|
+
function readAutoResumeConfig(env = process.env) {
|
|
42043
|
+
return {
|
|
42044
|
+
enabled: env.AGENT_PANE_AUTO_RESUME !== "0",
|
|
42045
|
+
maxAgeMs: envNumber(env, "AGENT_PANE_AUTO_RESUME_MAX_AGE_MS", 24 * HOUR),
|
|
42046
|
+
maxAttempts: envNumber(env, "AGENT_PANE_AUTO_RESUME_MAX_ATTEMPTS", 3),
|
|
42047
|
+
delayMs: envNumber(env, "AGENT_PANE_AUTO_RESUME_DELAY_MS", 3e3),
|
|
42048
|
+
networkDelayMs: envNumber(env, "AGENT_PANE_AUTO_RESUME_NETWORK_DELAY_MS", 15e3),
|
|
42049
|
+
staggerMs: 1500,
|
|
42050
|
+
probeMaxDelayMs: 6e4,
|
|
42051
|
+
probeFailuresBeforeForce: 10
|
|
42052
|
+
};
|
|
42053
|
+
}
|
|
42054
|
+
var config = null;
|
|
42055
|
+
var probe = () => probeApi();
|
|
42056
|
+
var unsubscribe = null;
|
|
42057
|
+
var timer = null;
|
|
42058
|
+
var timerDue = 0;
|
|
42059
|
+
var scanning = null;
|
|
42060
|
+
var rescanDelay = null;
|
|
42061
|
+
var probeFailures = 0;
|
|
42062
|
+
function canResumeRuntime(runtimeType) {
|
|
42063
|
+
return hasProvider(runtimeType) && typeof getProvider(runtimeType).resumeInterruptedTurn === "function";
|
|
42064
|
+
}
|
|
42065
|
+
function startAutoResume(opts = {}) {
|
|
42066
|
+
if (config) return null;
|
|
42067
|
+
const cfg = { ...readAutoResumeConfig(), ...opts.config };
|
|
42068
|
+
config = cfg;
|
|
42069
|
+
probe = opts.probe ?? (() => probeApi());
|
|
42070
|
+
probeFailures = 0;
|
|
42071
|
+
const summary = recoverInterruptedTurns(getDb(), { canResume: canResumeRuntime });
|
|
42072
|
+
unsubscribe = onTurnInterrupted((event) => {
|
|
42073
|
+
scheduleScan(event.reason === "network" ? cfg.networkDelayMs : cfg.delayMs);
|
|
42074
|
+
});
|
|
42075
|
+
if (!cfg.enabled) console.log("[resume] automatic resume is off (AGENT_PANE_AUTO_RESUME=0)");
|
|
42076
|
+
scheduleScan(cfg.delayMs);
|
|
42077
|
+
return summary;
|
|
42078
|
+
}
|
|
42079
|
+
function stopAutoResume() {
|
|
42080
|
+
if (timer) clearTimeout(timer);
|
|
42081
|
+
timer = null;
|
|
42082
|
+
unsubscribe?.();
|
|
42083
|
+
unsubscribe = null;
|
|
42084
|
+
config = null;
|
|
42085
|
+
rescanDelay = null;
|
|
42086
|
+
}
|
|
42087
|
+
function scheduleScan(delayMs) {
|
|
42088
|
+
if (!config) return;
|
|
42089
|
+
if (scanning) {
|
|
42090
|
+
rescanDelay = rescanDelay === null ? delayMs : Math.min(rescanDelay, delayMs);
|
|
42091
|
+
return;
|
|
42092
|
+
}
|
|
42093
|
+
const due = Date.now() + delayMs;
|
|
42094
|
+
if (timer && timerDue <= due) return;
|
|
42095
|
+
if (timer) clearTimeout(timer);
|
|
42096
|
+
timerDue = due;
|
|
42097
|
+
timer = setTimeout(() => {
|
|
42098
|
+
timer = null;
|
|
42099
|
+
void runScan();
|
|
42100
|
+
}, delayMs);
|
|
42101
|
+
timer.unref?.();
|
|
42102
|
+
}
|
|
42103
|
+
async function runScan() {
|
|
42104
|
+
if (scanning) return scanning;
|
|
42105
|
+
scanning = (async () => {
|
|
42106
|
+
try {
|
|
42107
|
+
await scan();
|
|
42108
|
+
} catch (err) {
|
|
42109
|
+
console.error("[resume] scan failed:", err);
|
|
42110
|
+
} finally {
|
|
42111
|
+
scanning = null;
|
|
42112
|
+
const again = rescanDelay;
|
|
42113
|
+
rescanDelay = null;
|
|
42114
|
+
if (again !== null) scheduleScan(again);
|
|
42115
|
+
}
|
|
42116
|
+
})();
|
|
42117
|
+
return scanning;
|
|
42118
|
+
}
|
|
42119
|
+
async function scan() {
|
|
42120
|
+
const cfg = config;
|
|
42121
|
+
if (!cfg || !cfg.enabled) return;
|
|
42122
|
+
const db2 = getDb();
|
|
42123
|
+
const waiting = db2.prepare(
|
|
42124
|
+
`SELECT id, session_id, interrupted_at, resume_attempts FROM turns
|
|
42125
|
+
WHERE status = 'pending' AND resume_state = 'waiting'
|
|
42126
|
+
ORDER BY interrupted_at ASC, created_at ASC`
|
|
42127
|
+
).all();
|
|
42128
|
+
const due = [];
|
|
42129
|
+
for (const turn of waiting) {
|
|
42130
|
+
const at = turn.interrupted_at ? Date.parse(turn.interrupted_at) : NaN;
|
|
42131
|
+
if (Number.isFinite(at) && Date.now() - at > cfg.maxAgeMs) {
|
|
42132
|
+
finalizeInterruptedTurn(db2, turn.id, "skipped", "Interrupted too long ago to resume automatically");
|
|
42133
|
+
} else if (turn.resume_attempts >= cfg.maxAttempts) {
|
|
42134
|
+
finalizeInterruptedTurn(
|
|
42135
|
+
db2,
|
|
42136
|
+
turn.id,
|
|
42137
|
+
"gave_up",
|
|
42138
|
+
`Automatic resume stopped after ${turn.resume_attempts} attempts`
|
|
42139
|
+
);
|
|
42140
|
+
} else {
|
|
42141
|
+
due.push(turn);
|
|
42142
|
+
}
|
|
42143
|
+
}
|
|
42144
|
+
if (due.length === 0) {
|
|
42145
|
+
probeFailures = 0;
|
|
42146
|
+
return;
|
|
42147
|
+
}
|
|
42148
|
+
let countAttempt = true;
|
|
42149
|
+
if (!await probe()) {
|
|
42150
|
+
probeFailures++;
|
|
42151
|
+
if (probeFailures < cfg.probeFailuresBeforeForce) {
|
|
42152
|
+
const delay = Math.min(cfg.networkDelayMs * 2 ** (probeFailures - 1), cfg.probeMaxDelayMs);
|
|
42153
|
+
console.log(`[resume] model API unreachable \u2014 ${due.length} turn(s) wait, next check in ${delay}ms`);
|
|
42154
|
+
scheduleScan(Math.max(delay, 1e3));
|
|
42155
|
+
return;
|
|
42156
|
+
}
|
|
42157
|
+
console.log(`[resume] model API still unreachable after ${probeFailures} checks \u2014 resuming anyway`);
|
|
42158
|
+
countAttempt = false;
|
|
42159
|
+
}
|
|
42160
|
+
probeFailures = 0;
|
|
42161
|
+
for (let i = 0; i < due.length; i++) {
|
|
42162
|
+
if (config !== cfg) return;
|
|
42163
|
+
if (i > 0) await new Promise((resolve) => setTimeout(resolve, cfg.staggerMs));
|
|
42164
|
+
const result = await resumeTurn(due[i].id, { manual: false, countAttempt });
|
|
42165
|
+
if (!result.ok && result.status === 409) {
|
|
42166
|
+
finalizeInterruptedTurn(db2, due[i].id, "skipped", result.error);
|
|
42167
|
+
}
|
|
42168
|
+
}
|
|
42169
|
+
}
|
|
42170
|
+
function resumeBlocker(db2, session, turn, manual) {
|
|
42171
|
+
if (turn.thread_id) return { status: 409, error: "Discussion-thread turns cannot be resumed" };
|
|
42172
|
+
if (!isResumable(turn, manual)) return { status: 409, error: "Turn is not waiting to be resumed" };
|
|
42173
|
+
if (!canResumeRuntime(session.runtime_type)) {
|
|
42174
|
+
return { status: 409, error: `The ${session.runtime_type} runtime cannot resume turns` };
|
|
42175
|
+
}
|
|
42176
|
+
const latest = db2.prepare("SELECT id FROM turns WHERE session_id = ? AND thread_id IS NULL ORDER BY created_at DESC, rowid DESC LIMIT 1").get(session.id);
|
|
42177
|
+
if (latest?.id !== turn.id) return { status: 409, error: "Only the latest turn of a session can be resumed" };
|
|
42178
|
+
const other = db2.prepare(
|
|
42179
|
+
"SELECT 1 FROM turns WHERE session_id = ? AND thread_id IS NULL AND id != ? AND status IN ('pending', 'running') LIMIT 1"
|
|
42180
|
+
).get(session.id, turn.id);
|
|
42181
|
+
if (other) return { status: 409, error: "Another turn is running in this session" };
|
|
42182
|
+
return null;
|
|
42183
|
+
}
|
|
42184
|
+
async function resumeTurn(turnId, opts) {
|
|
42185
|
+
const db2 = getDb();
|
|
42186
|
+
const turn = db2.prepare("SELECT * FROM turns WHERE id = ?").get(turnId);
|
|
42187
|
+
if (!turn) return { ok: false, status: 404, error: "Turn not found" };
|
|
42188
|
+
const session = db2.prepare("SELECT * FROM sessions WHERE id = ?").get(turn.session_id);
|
|
42189
|
+
if (!session) return { ok: false, status: 404, error: "Session not found" };
|
|
42190
|
+
const blocker = resumeBlocker(db2, session, turn, opts.manual);
|
|
42191
|
+
if (blocker) return { ok: false, ...blocker };
|
|
42192
|
+
const resuming = markTurnResuming(db2, turnId, { manual: opts.manual, countAttempt: opts.countAttempt });
|
|
42193
|
+
if (!resuming) return { ok: false, status: 409, error: "Turn is not waiting to be resumed" };
|
|
42194
|
+
console.log(
|
|
42195
|
+
`[resume] resuming turn ${turnId} of session ${session.id} (${resuming.reason}, attempt ${resuming.attempt}${opts.manual ? ", manual" : ""})`
|
|
42196
|
+
);
|
|
42197
|
+
try {
|
|
42198
|
+
await getProvider(session.runtime_type).resumeInterruptedTurn(session, turn, { reason: resuming.reason });
|
|
42199
|
+
} catch (err) {
|
|
42200
|
+
const message = err.message;
|
|
42201
|
+
console.error(`[resume] could not resume turn ${turnId}:`, message);
|
|
42202
|
+
markTurnInterrupted(db2, turnId, resuming.reason, { detail: `Resume failed: ${message}` });
|
|
42203
|
+
return { ok: false, status: 500, error: message };
|
|
42204
|
+
}
|
|
42205
|
+
return { ok: true };
|
|
42206
|
+
}
|
|
42207
|
+
async function resumeNow(sessionId, turnId) {
|
|
42208
|
+
const db2 = getDb();
|
|
42209
|
+
const row = db2.prepare("SELECT session_id FROM turns WHERE id = ?").get(turnId);
|
|
42210
|
+
if (!row || row.session_id !== sessionId) return { ok: false, status: 404, error: "Turn not found" };
|
|
42211
|
+
return resumeTurn(turnId, { manual: true });
|
|
42212
|
+
}
|
|
42213
|
+
|
|
41816
42214
|
// src/routes/turns.ts
|
|
41817
42215
|
async function registerTurnRoutes(fastify) {
|
|
41818
42216
|
fastify.post(
|
|
@@ -41910,6 +42308,22 @@ async function registerTurnRoutes(fastify) {
|
|
|
41910
42308
|
}
|
|
41911
42309
|
return { success: true };
|
|
41912
42310
|
});
|
|
42311
|
+
fastify.post(
|
|
42312
|
+
"/api/sessions/:id/turns/:turnId/resume",
|
|
42313
|
+
async (request, reply) => {
|
|
42314
|
+
const { id, turnId } = request.params;
|
|
42315
|
+
const db2 = getDb();
|
|
42316
|
+
const session = db2.prepare("SELECT id FROM sessions WHERE id = ?").get(id);
|
|
42317
|
+
if (!session) {
|
|
42318
|
+
return reply.code(404).send({ error: "Session not found" });
|
|
42319
|
+
}
|
|
42320
|
+
const result = await resumeNow(id, turnId);
|
|
42321
|
+
if (!result.ok) {
|
|
42322
|
+
return reply.code(result.status).send({ error: result.error });
|
|
42323
|
+
}
|
|
42324
|
+
return reply.code(202).send({ success: true });
|
|
42325
|
+
}
|
|
42326
|
+
);
|
|
41913
42327
|
}
|
|
41914
42328
|
|
|
41915
42329
|
// src/routes/threads.ts
|
|
@@ -42620,14 +43034,14 @@ var AppServerClient = class {
|
|
|
42620
43034
|
}
|
|
42621
43035
|
const id = this.nextId++;
|
|
42622
43036
|
const message = JSON.stringify({ id, method, params }) + "\n";
|
|
42623
|
-
const
|
|
43037
|
+
const timer2 = setTimeout(() => {
|
|
42624
43038
|
this.pending.delete(id);
|
|
42625
43039
|
reject(new Error(`Request '${method}' (id=${id}) timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
42626
43040
|
}, REQUEST_TIMEOUT_MS);
|
|
42627
43041
|
this.pending.set(id, {
|
|
42628
43042
|
resolve,
|
|
42629
43043
|
reject,
|
|
42630
|
-
timer
|
|
43044
|
+
timer: timer2
|
|
42631
43045
|
});
|
|
42632
43046
|
this.write(message);
|
|
42633
43047
|
});
|
|
@@ -43317,14 +43731,14 @@ var VscodeWebManager = class {
|
|
|
43317
43731
|
waitForReady(inst) {
|
|
43318
43732
|
const deadline = this.deps.now() + STARTUP_TIMEOUT_MS;
|
|
43319
43733
|
return new Promise((resolve) => {
|
|
43320
|
-
const
|
|
43734
|
+
const timer2 = setInterval(async () => {
|
|
43321
43735
|
if (inst.state !== "starting") {
|
|
43322
|
-
clearInterval(
|
|
43736
|
+
clearInterval(timer2);
|
|
43323
43737
|
resolve();
|
|
43324
43738
|
return;
|
|
43325
43739
|
}
|
|
43326
43740
|
if (this.deps.now() > deadline) {
|
|
43327
|
-
clearInterval(
|
|
43741
|
+
clearInterval(timer2);
|
|
43328
43742
|
inst.state = "error";
|
|
43329
43743
|
inst.error = `serve-web did not become ready within ${STARTUP_TIMEOUT_MS / 1e3}s`;
|
|
43330
43744
|
inst.stopping = true;
|
|
@@ -43336,7 +43750,7 @@ var VscodeWebManager = class {
|
|
|
43336
43750
|
}
|
|
43337
43751
|
try {
|
|
43338
43752
|
await this.deps.probe(inst.port, inst.token);
|
|
43339
|
-
clearInterval(
|
|
43753
|
+
clearInterval(timer2);
|
|
43340
43754
|
if (inst.state === "starting") {
|
|
43341
43755
|
inst.state = "running";
|
|
43342
43756
|
this.markAccessed(inst.id);
|
|
@@ -43724,8 +44138,8 @@ function startAppsWatch(onAppChange) {
|
|
|
43724
44138
|
const file = segs[segs.length - 1];
|
|
43725
44139
|
if (!appId || appId === path3.basename(root) || !APP_ID_RE.test(appId) || file === "state.json") return;
|
|
43726
44140
|
if (segs.length === 1 && isRegularFile(path3.join(root, appId))) return;
|
|
43727
|
-
const
|
|
43728
|
-
if (
|
|
44141
|
+
const timer2 = pendingChanges.get(appId);
|
|
44142
|
+
if (timer2) clearTimeout(timer2);
|
|
43729
44143
|
pendingChanges.set(
|
|
43730
44144
|
appId,
|
|
43731
44145
|
setTimeout(() => {
|
|
@@ -43743,7 +44157,7 @@ function stopAppsWatch() {
|
|
|
43743
44157
|
watcher.close();
|
|
43744
44158
|
watcher = null;
|
|
43745
44159
|
}
|
|
43746
|
-
for (const
|
|
44160
|
+
for (const timer2 of pendingChanges.values()) clearTimeout(timer2);
|
|
43747
44161
|
pendingChanges.clear();
|
|
43748
44162
|
}
|
|
43749
44163
|
|
|
@@ -44139,7 +44553,7 @@ function getHeadlessToolHandler(appId, workspace = "") {
|
|
|
44139
44553
|
function dispatchHeadlessToolCall(handler, tool, args) {
|
|
44140
44554
|
const callId = v4_default();
|
|
44141
44555
|
const result = new Promise((resolve, reject) => {
|
|
44142
|
-
const
|
|
44556
|
+
const timer2 = setTimeout(() => {
|
|
44143
44557
|
pendingCalls.delete(callId);
|
|
44144
44558
|
reject(
|
|
44145
44559
|
new Error(
|
|
@@ -44147,7 +44561,7 @@ function dispatchHeadlessToolCall(handler, tool, args) {
|
|
|
44147
44561
|
)
|
|
44148
44562
|
);
|
|
44149
44563
|
}, HEADLESS_TOOL_TIMEOUT_MS);
|
|
44150
|
-
pendingCalls.set(callId, { resolve, reject, timer });
|
|
44564
|
+
pendingCalls.set(callId, { resolve, reject, timer: timer2 });
|
|
44151
44565
|
});
|
|
44152
44566
|
try {
|
|
44153
44567
|
handler("tool_call", { callId, tool, name: tool, args: args ?? {} });
|
|
@@ -44225,7 +44639,7 @@ function dispatchAppTool(toolName, args, workspace = "") {
|
|
|
44225
44639
|
}
|
|
44226
44640
|
const callId = v4_default();
|
|
44227
44641
|
const result = new Promise((resolve, reject) => {
|
|
44228
|
-
const
|
|
44642
|
+
const timer2 = setTimeout(() => {
|
|
44229
44643
|
pendingCalls2.delete(callId);
|
|
44230
44644
|
reject(
|
|
44231
44645
|
new Error(
|
|
@@ -44233,7 +44647,7 @@ function dispatchAppTool(toolName, args, workspace = "") {
|
|
|
44233
44647
|
)
|
|
44234
44648
|
);
|
|
44235
44649
|
}, TOOL_CALL_TIMEOUT_MS);
|
|
44236
|
-
pendingCalls2.set(callId, { resolve, reject, timer, appId });
|
|
44650
|
+
pendingCalls2.set(callId, { resolve, reject, timer: timer2, appId });
|
|
44237
44651
|
broadcast("app:tool_call", { appId, callId, tool: localName, args: args ?? {} });
|
|
44238
44652
|
});
|
|
44239
44653
|
return { callId, result };
|
|
@@ -44795,8 +45209,8 @@ function parseRuntimeConfig(session) {
|
|
|
44795
45209
|
return null;
|
|
44796
45210
|
}
|
|
44797
45211
|
}
|
|
44798
|
-
function sandboxType(
|
|
44799
|
-
switch (
|
|
45212
|
+
function sandboxType(config2) {
|
|
45213
|
+
switch (config2?.sandboxMode) {
|
|
44800
45214
|
case "read-only":
|
|
44801
45215
|
return "read-only";
|
|
44802
45216
|
case "danger-full-access":
|
|
@@ -44857,8 +45271,8 @@ async function getOrCreateThread(session) {
|
|
|
44857
45271
|
if (session.origin_app_id) {
|
|
44858
45272
|
autoApproveToolUserInput(client);
|
|
44859
45273
|
}
|
|
44860
|
-
const
|
|
44861
|
-
const sandbox = sandboxType(
|
|
45274
|
+
const config2 = parseRuntimeConfig(session);
|
|
45275
|
+
const sandbox = sandboxType(config2);
|
|
44862
45276
|
const instructions = threadInstructionParams(cwd);
|
|
44863
45277
|
let threadId;
|
|
44864
45278
|
if (session.external_thread_id) {
|
|
@@ -44916,8 +45330,8 @@ async function startTurn(session, turn) {
|
|
|
44916
45330
|
setSessionStatus(db2, session.id, 1, "running", now);
|
|
44917
45331
|
broadcast("turn:started", { sessionId: session.id, turnId: turn.id });
|
|
44918
45332
|
const { client, threadId } = entry;
|
|
44919
|
-
const
|
|
44920
|
-
const sandbox = sandboxType(
|
|
45333
|
+
const config2 = parseRuntimeConfig(session);
|
|
45334
|
+
const sandbox = sandboxType(config2);
|
|
44921
45335
|
let turnDone = false;
|
|
44922
45336
|
const handleItemStarted = (params) => {
|
|
44923
45337
|
if (turnDone) return;
|
|
@@ -45591,6 +46005,42 @@ async function refreshContext(entry, sessionId, db2) {
|
|
|
45591
46005
|
publish(entry, sessionId, db2);
|
|
45592
46006
|
}
|
|
45593
46007
|
|
|
46008
|
+
// src/claude/api-errors.ts
|
|
46009
|
+
var FATAL_ERRORS = /* @__PURE__ */ new Set([
|
|
46010
|
+
"authentication_failed",
|
|
46011
|
+
"oauth_org_not_allowed",
|
|
46012
|
+
"account_on_hold",
|
|
46013
|
+
"verification_required",
|
|
46014
|
+
"billing_error",
|
|
46015
|
+
"rate_limit",
|
|
46016
|
+
"invalid_request",
|
|
46017
|
+
"model_not_found",
|
|
46018
|
+
"max_output_tokens",
|
|
46019
|
+
"cloud_credential_error"
|
|
46020
|
+
]);
|
|
46021
|
+
var TRANSIENT_ERRORS = /* @__PURE__ */ new Set(["server_error", "overloaded"]);
|
|
46022
|
+
var NETWORK_TEXTS = [
|
|
46023
|
+
/connection (error|lost|dropped|closed|refused|reset)/i,
|
|
46024
|
+
/unable to connect/i,
|
|
46025
|
+
/request timed out/i,
|
|
46026
|
+
/check your internet connection/i,
|
|
46027
|
+
/no response from (the )?api/i,
|
|
46028
|
+
/\b(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH|EPIPE)\b/,
|
|
46029
|
+
/socket hang up|fetch failed|network error/i,
|
|
46030
|
+
/overloaded/i
|
|
46031
|
+
];
|
|
46032
|
+
function classifyApiError({ error, status, text }) {
|
|
46033
|
+
if (error && FATAL_ERRORS.has(error)) return "fatal";
|
|
46034
|
+
if (error && TRANSIENT_ERRORS.has(error)) return "network";
|
|
46035
|
+
if (typeof status === "number") {
|
|
46036
|
+
if (status === 408 || status >= 500) return "network";
|
|
46037
|
+
return "fatal";
|
|
46038
|
+
}
|
|
46039
|
+
if (text && NETWORK_TEXTS.some((re) => re.test(text))) return "network";
|
|
46040
|
+
if (status === null && (!error || error === "unknown")) return "network";
|
|
46041
|
+
return "fatal";
|
|
46042
|
+
}
|
|
46043
|
+
|
|
45594
46044
|
// src/claude/claude-provider.ts
|
|
45595
46045
|
var PromptQueue = class {
|
|
45596
46046
|
buffer = [];
|
|
@@ -45688,10 +46138,153 @@ function setSessionStatus2(db2, sessionId, isRunning, status, now, hasUnreviewed
|
|
|
45688
46138
|
);
|
|
45689
46139
|
}
|
|
45690
46140
|
}
|
|
45691
|
-
|
|
46141
|
+
var CONTINUE_PROMPT = "Continue from where you left off.";
|
|
46142
|
+
function rescueWaitMs() {
|
|
46143
|
+
const ms = Number(process.env.AGENT_PANE_RESCUE_WAIT_MS);
|
|
46144
|
+
return Number.isFinite(ms) && ms > 0 ? ms : 15e3;
|
|
46145
|
+
}
|
|
46146
|
+
var RESCUE_ENV = ["CLAUDE_CODE_RESUME_INTERRUPTED_TURN", "CLAUDE_CODE_RESUME_REASON"];
|
|
46147
|
+
function spawnEnv(resume) {
|
|
46148
|
+
const inherited = RESCUE_ENV.some((k) => k in process.env);
|
|
46149
|
+
if (!resume && !inherited) return void 0;
|
|
46150
|
+
const env = { ...process.env };
|
|
46151
|
+
for (const k of RESCUE_ENV) delete env[k];
|
|
46152
|
+
if (resume) {
|
|
46153
|
+
env.CLAUDE_CODE_RESUME_INTERRUPTED_TURN = "1";
|
|
46154
|
+
env.CLAUDE_CODE_RESUME_REASON = resume.reason;
|
|
46155
|
+
}
|
|
46156
|
+
return env;
|
|
46157
|
+
}
|
|
46158
|
+
function isSignalExit(message) {
|
|
46159
|
+
if (/terminated by signal SIG/.test(message)) return true;
|
|
46160
|
+
const m = /exited with code (\d+)/.exec(message);
|
|
46161
|
+
if (!m) return false;
|
|
46162
|
+
const code = Number(m[1]);
|
|
46163
|
+
return code > 128 && code < 160;
|
|
46164
|
+
}
|
|
46165
|
+
function clearResume(entry) {
|
|
46166
|
+
if (entry.resume?.watchdog) clearTimeout(entry.resume.watchdog);
|
|
46167
|
+
entry.resume = null;
|
|
46168
|
+
}
|
|
46169
|
+
function pushPrompt(entry, content) {
|
|
46170
|
+
entry.promptQueue.push({
|
|
46171
|
+
type: "user",
|
|
46172
|
+
message: { role: "user", content },
|
|
46173
|
+
parent_tool_use_id: null
|
|
46174
|
+
});
|
|
46175
|
+
}
|
|
46176
|
+
function armRescueWatchdog(entry, ms = rescueWaitMs()) {
|
|
46177
|
+
const r = entry.resume;
|
|
46178
|
+
if (!r || r.phase !== "awaiting_rescue") return;
|
|
46179
|
+
if (r.watchdog) clearTimeout(r.watchdog);
|
|
46180
|
+
r.watchdog = setTimeout(() => {
|
|
46181
|
+
r.watchdog = null;
|
|
46182
|
+
if (entry.resume !== r || r.phase !== "awaiting_rescue" || entry.closeReason) return;
|
|
46183
|
+
let content = CONTINUE_PROMPT;
|
|
46184
|
+
try {
|
|
46185
|
+
const db2 = getDb();
|
|
46186
|
+
const turn = db2.prepare("SELECT * FROM turns WHERE id = ?").get(r.turnId);
|
|
46187
|
+
if (!turn || turn.status !== "running") return;
|
|
46188
|
+
const produced = db2.prepare("SELECT 1 FROM items WHERE turn_id = ? AND type NOT IN ('interruption', 'error') LIMIT 1").get(r.turnId);
|
|
46189
|
+
if (!produced) content = buildUserContent(turn);
|
|
46190
|
+
} catch (err) {
|
|
46191
|
+
console.error("[claude] resume fallback failed:", err.message);
|
|
46192
|
+
return;
|
|
46193
|
+
}
|
|
46194
|
+
console.log(`[claude] no interrupted-turn rescue for turn ${r.turnId} \u2014 continuing it with a prompt`);
|
|
46195
|
+
r.phase = "continuing";
|
|
46196
|
+
pushPrompt(entry, content);
|
|
46197
|
+
}, ms);
|
|
46198
|
+
r.watchdog.unref?.();
|
|
46199
|
+
}
|
|
46200
|
+
function isModelFrame(message) {
|
|
46201
|
+
if (message.parent_tool_use_id) return false;
|
|
46202
|
+
if (message.type === "stream_event") return true;
|
|
46203
|
+
if (message.type === "assistant") return message.message?.model !== "<synthetic>";
|
|
46204
|
+
return false;
|
|
46205
|
+
}
|
|
46206
|
+
function trackResumeFrame(entry, message, db2) {
|
|
46207
|
+
const r = entry.resume;
|
|
46208
|
+
if (!r) return true;
|
|
46209
|
+
if (r.turnId !== entry.turnId) {
|
|
46210
|
+
clearResume(entry);
|
|
46211
|
+
return true;
|
|
46212
|
+
}
|
|
46213
|
+
if (r.phase === "awaiting_rescue") {
|
|
46214
|
+
if (message.resume_reason) {
|
|
46215
|
+
if (r.watchdog) clearTimeout(r.watchdog);
|
|
46216
|
+
r.watchdog = null;
|
|
46217
|
+
r.phase = "continuing";
|
|
46218
|
+
} else {
|
|
46219
|
+
if (message.type === "result") return false;
|
|
46220
|
+
if (message.type === "system") {
|
|
46221
|
+
const sub = message.subtype;
|
|
46222
|
+
if (sub === "api_retry" || sub === "status") armRescueWatchdog(entry);
|
|
46223
|
+
}
|
|
46224
|
+
return true;
|
|
46225
|
+
}
|
|
46226
|
+
}
|
|
46227
|
+
if (isModelFrame(message)) {
|
|
46228
|
+
clearResume(entry);
|
|
46229
|
+
markTurnResumed(db2, r.turnId);
|
|
46230
|
+
} else if (message.type === "result") {
|
|
46231
|
+
clearResume(entry);
|
|
46232
|
+
}
|
|
46233
|
+
return true;
|
|
46234
|
+
}
|
|
46235
|
+
function failTurn(db2, sessionId, turnId, error) {
|
|
46236
|
+
const row = db2.prepare("SELECT resume_state FROM turns WHERE id = ?").get(turnId);
|
|
46237
|
+
if (row?.resume_state === "waiting" || row?.resume_state === "resuming") {
|
|
46238
|
+
if (finalizeInterruptedTurn(db2, turnId, "gave_up", error)) return;
|
|
46239
|
+
}
|
|
46240
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46241
|
+
db2.prepare("UPDATE turns SET status = ?, error = ?, updated_at = ? WHERE id = ?").run("failed", error, now, turnId);
|
|
46242
|
+
recomputeSessionRunning(db2, sessionId, "failed");
|
|
46243
|
+
broadcast("turn:failed", { sessionId, turnId, error });
|
|
46244
|
+
}
|
|
46245
|
+
function handleLoopError(entry, sessionId, errMsg, db2) {
|
|
46246
|
+
if (!entry.turnId) return;
|
|
46247
|
+
const turnId = entry.turnId;
|
|
46248
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46249
|
+
const staleItems = db2.prepare(
|
|
46250
|
+
"UPDATE items SET status = 'failed', updated_at = ? WHERE status = 'in_progress' AND turn_id IN (SELECT id FROM turns WHERE session_id = ?)"
|
|
46251
|
+
).run(now, sessionId);
|
|
46252
|
+
const turn = db2.prepare("SELECT status FROM turns WHERE id = ?").get(turnId);
|
|
46253
|
+
if (turn && (turn.status === "pending" || turn.status === "running")) {
|
|
46254
|
+
if (!entry.threadId && !entry.closeReason && isSignalExit(errMsg)) {
|
|
46255
|
+
markTurnInterrupted(db2, turnId, "process_exit", { detail: errMsg });
|
|
46256
|
+
} else {
|
|
46257
|
+
failTurn(db2, sessionId, turnId, errMsg);
|
|
46258
|
+
}
|
|
46259
|
+
}
|
|
46260
|
+
if (staleItems.changes > 0) {
|
|
46261
|
+
const staleRows = db2.prepare(
|
|
46262
|
+
"SELECT id, turn_id, external_item_id, type, data, created_at FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?) AND status = 'failed' AND updated_at = ?"
|
|
46263
|
+
).all(sessionId, now);
|
|
46264
|
+
for (const row of staleRows) {
|
|
46265
|
+
broadcast("item:completed", {
|
|
46266
|
+
sessionId,
|
|
46267
|
+
turnId: row.turn_id,
|
|
46268
|
+
item: {
|
|
46269
|
+
id: row.id,
|
|
46270
|
+
turn_id: row.turn_id,
|
|
46271
|
+
external_item_id: row.external_item_id,
|
|
46272
|
+
type: row.type,
|
|
46273
|
+
data: row.data ? JSON.parse(row.data) : {},
|
|
46274
|
+
status: "failed",
|
|
46275
|
+
created_at: row.created_at,
|
|
46276
|
+
updated_at: now
|
|
46277
|
+
}
|
|
46278
|
+
});
|
|
46279
|
+
}
|
|
46280
|
+
}
|
|
46281
|
+
}
|
|
46282
|
+
async function ensureSessionStarted(session, mapKey, resumeSessionId, resume) {
|
|
45692
46283
|
const db2 = getDb();
|
|
45693
46284
|
const key = mapKey ?? session.id;
|
|
45694
46285
|
const resumeId = mapKey !== void 0 ? resumeSessionId : resumeSessionId ?? session.external_thread_id ?? void 0;
|
|
46286
|
+
const rescue = resume && resumeId ? resume : void 0;
|
|
46287
|
+
const env = spawnEnv(rescue);
|
|
45695
46288
|
const options = {
|
|
45696
46289
|
cwd: session.cwd ?? process.cwd(),
|
|
45697
46290
|
model: session.model ?? void 0,
|
|
@@ -45700,19 +46293,32 @@ async function ensureSessionStarted(session, mapKey, resumeSessionId) {
|
|
|
45700
46293
|
includePartialMessages: true,
|
|
45701
46294
|
...CLI_OVERRIDE,
|
|
45702
46295
|
...session.model_reasoning_effort ? { effort: session.model_reasoning_effort } : {},
|
|
45703
|
-
...resumeId ? { resume: resumeId } : {}
|
|
46296
|
+
...resumeId ? { resume: resumeId } : {},
|
|
46297
|
+
...env ? { env } : {}
|
|
45704
46298
|
};
|
|
45705
46299
|
const promptQueue = new PromptQueue();
|
|
45706
46300
|
const q = query({ prompt: promptQueue, options });
|
|
45707
46301
|
const entry = {
|
|
45708
46302
|
query: q,
|
|
45709
46303
|
sessionId: null,
|
|
45710
|
-
turnId: null,
|
|
46304
|
+
turnId: resume?.turnId ?? null,
|
|
45711
46305
|
cwd: session.cwd ?? process.cwd(),
|
|
45712
46306
|
promptQueue,
|
|
45713
|
-
messageLoop: null
|
|
46307
|
+
messageLoop: null,
|
|
46308
|
+
closeReason: null,
|
|
46309
|
+
resume: resume ? { turnId: resume.turnId, phase: rescue ? "awaiting_rescue" : "continuing", watchdog: null } : null,
|
|
46310
|
+
lastApiError: null
|
|
45714
46311
|
};
|
|
45715
46312
|
sessionMap2.set(key, entry);
|
|
46313
|
+
if (rescue) {
|
|
46314
|
+
armRescueWatchdog(entry, rescueWaitMs() * 4);
|
|
46315
|
+
q.initializationResult().then(
|
|
46316
|
+
() => armRescueWatchdog(entry),
|
|
46317
|
+
() => {
|
|
46318
|
+
}
|
|
46319
|
+
// start failed — the message loop reports it
|
|
46320
|
+
);
|
|
46321
|
+
}
|
|
45716
46322
|
entry.messageLoop = (async () => {
|
|
45717
46323
|
try {
|
|
45718
46324
|
for await (const message of q) {
|
|
@@ -45722,8 +46328,10 @@ async function ensureSessionStarted(session, mapKey, resumeSessionId) {
|
|
|
45722
46328
|
}
|
|
45723
46329
|
const currentTurnId = entry.turnId;
|
|
45724
46330
|
if (!currentTurnId) continue;
|
|
46331
|
+
if (entry.resume && !trackResumeFrame(entry, message, db2)) continue;
|
|
45725
46332
|
await handleMessage(message, session.id, currentTurnId, entry, db2);
|
|
45726
46333
|
}
|
|
46334
|
+
if (entry.closeReason === "shutdown") return;
|
|
45727
46335
|
if (entry.turnId) {
|
|
45728
46336
|
const currentTurn = db2.prepare("SELECT status, token_usage FROM turns WHERE id = ?").get(entry.turnId);
|
|
45729
46337
|
if (currentTurn && currentTurn.status === "running") {
|
|
@@ -45734,43 +46342,16 @@ async function ensureSessionStarted(session, mapKey, resumeSessionId) {
|
|
|
45734
46342
|
}
|
|
45735
46343
|
} catch (err) {
|
|
45736
46344
|
const errMsg = err.message;
|
|
46345
|
+
clearResume(entry);
|
|
46346
|
+
if (entry.closeReason === "shutdown") return;
|
|
45737
46347
|
console.error(`[claude] message loop error for session ${session.id}:`, errMsg);
|
|
45738
|
-
|
|
45739
|
-
|
|
45740
|
-
|
|
45741
|
-
|
|
45742
|
-
).run(now, session.id);
|
|
45743
|
-
db2.prepare("UPDATE turns SET status = ?, error = ?, updated_at = ? WHERE id = ?").run(
|
|
45744
|
-
"failed",
|
|
45745
|
-
errMsg,
|
|
45746
|
-
now,
|
|
45747
|
-
entry.turnId
|
|
45748
|
-
);
|
|
45749
|
-
recomputeSessionRunning(db2, session.id, "failed");
|
|
45750
|
-
broadcast("turn:failed", { sessionId: session.id, turnId: entry.turnId, error: errMsg });
|
|
45751
|
-
if (staleItems.changes > 0) {
|
|
45752
|
-
const staleRows = db2.prepare(
|
|
45753
|
-
"SELECT id, turn_id, external_item_id, type, data, created_at FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?) AND status = 'failed' AND updated_at = ?"
|
|
45754
|
-
).all(session.id, now);
|
|
45755
|
-
for (const row of staleRows) {
|
|
45756
|
-
broadcast("item:completed", {
|
|
45757
|
-
sessionId: session.id,
|
|
45758
|
-
turnId: row.turn_id,
|
|
45759
|
-
item: {
|
|
45760
|
-
id: row.id,
|
|
45761
|
-
turn_id: row.turn_id,
|
|
45762
|
-
external_item_id: row.external_item_id,
|
|
45763
|
-
type: row.type,
|
|
45764
|
-
data: row.data ? JSON.parse(row.data) : {},
|
|
45765
|
-
status: "failed",
|
|
45766
|
-
created_at: row.created_at,
|
|
45767
|
-
updated_at: now
|
|
45768
|
-
}
|
|
45769
|
-
});
|
|
45770
|
-
}
|
|
45771
|
-
}
|
|
46348
|
+
try {
|
|
46349
|
+
handleLoopError(entry, session.id, errMsg, db2);
|
|
46350
|
+
} catch (cleanupErr) {
|
|
46351
|
+
console.error(`[claude] cleanup after loop error failed for session ${session.id}:`, cleanupErr);
|
|
45772
46352
|
}
|
|
45773
46353
|
} finally {
|
|
46354
|
+
clearResume(entry);
|
|
45774
46355
|
if (entry.turnId) {
|
|
45775
46356
|
streamingMessageIds.delete(entry.turnId);
|
|
45776
46357
|
}
|
|
@@ -45818,13 +46399,20 @@ async function startTurn2(session, turn) {
|
|
|
45818
46399
|
streamingMessageIds.delete(entry.turnId);
|
|
45819
46400
|
}
|
|
45820
46401
|
entry.turnId = turn.id;
|
|
46402
|
+
entry.lastApiError = null;
|
|
46403
|
+
clearResume(entry);
|
|
45821
46404
|
db2.prepare("UPDATE turns SET status = ?, updated_at = ? WHERE id = ?").run("running", now, turn.id);
|
|
45822
46405
|
setSessionStatus2(db2, session.id, 1, "running", now);
|
|
45823
46406
|
broadcast("turn:started", { sessionId: session.id, turnId: turn.id });
|
|
46407
|
+
pushPrompt(entry, buildUserContent(turn));
|
|
46408
|
+
}
|
|
46409
|
+
function buildUserContent(turn) {
|
|
45824
46410
|
let content;
|
|
45825
46411
|
const rawAttachments = turn.attachments;
|
|
45826
46412
|
let attachments = null;
|
|
45827
|
-
if (rawAttachments) {
|
|
46413
|
+
if (Array.isArray(rawAttachments)) {
|
|
46414
|
+
attachments = rawAttachments;
|
|
46415
|
+
} else if (rawAttachments) {
|
|
45828
46416
|
try {
|
|
45829
46417
|
attachments = JSON.parse(rawAttachments);
|
|
45830
46418
|
} catch {
|
|
@@ -45860,11 +46448,7 @@ ${decoded}
|
|
|
45860
46448
|
} else {
|
|
45861
46449
|
content = turn.user_prompt;
|
|
45862
46450
|
}
|
|
45863
|
-
|
|
45864
|
-
type: "user",
|
|
45865
|
-
message: { role: "user", content },
|
|
45866
|
-
parent_tool_use_id: null
|
|
45867
|
-
});
|
|
46451
|
+
return content;
|
|
45868
46452
|
}
|
|
45869
46453
|
async function handleMessage(message, sessionId, turnId, entry, db2) {
|
|
45870
46454
|
switch (message.type) {
|
|
@@ -45915,6 +46499,17 @@ async function handleMessage(message, sessionId, turnId, entry, db2) {
|
|
|
45915
46499
|
entry,
|
|
45916
46500
|
db2
|
|
45917
46501
|
);
|
|
46502
|
+
} else if (sub === "api_retry") {
|
|
46503
|
+
const m = message;
|
|
46504
|
+
broadcast("turn:retrying", {
|
|
46505
|
+
sessionId,
|
|
46506
|
+
turnId,
|
|
46507
|
+
attempt: m.attempt,
|
|
46508
|
+
maxRetries: m.max_retries,
|
|
46509
|
+
retryDelayMs: m.retry_delay_ms,
|
|
46510
|
+
errorStatus: m.error_status ?? null,
|
|
46511
|
+
error: m.error ?? null
|
|
46512
|
+
});
|
|
45918
46513
|
}
|
|
45919
46514
|
}
|
|
45920
46515
|
break;
|
|
@@ -45957,6 +46552,26 @@ function handleAssistantMessage(message, sessionId, turnId, entry, db2) {
|
|
|
45957
46552
|
if (message.message?.model !== "<synthetic>") {
|
|
45958
46553
|
recordApiUsage(entry, sessionId, db2, message.message?.id ?? null, message.message?.usage);
|
|
45959
46554
|
}
|
|
46555
|
+
const apiError = message.error;
|
|
46556
|
+
if (apiError && message.message?.model === "<synthetic>") {
|
|
46557
|
+
const blocks = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
46558
|
+
const text = blocks.map((b) => {
|
|
46559
|
+
const block = b;
|
|
46560
|
+
return block?.type === "text" && typeof block.text === "string" ? block.text : "";
|
|
46561
|
+
}).join("").trim() || `API error (${apiError})`;
|
|
46562
|
+
entry.lastApiError = { error: apiError, text };
|
|
46563
|
+
thinkingAccumulator.delete(messageId);
|
|
46564
|
+
streamingMessageIds.delete(turnId);
|
|
46565
|
+
upsertItem(
|
|
46566
|
+
db2,
|
|
46567
|
+
{ type: "error", data: { message: text, error: apiError }, externalId: `api-error-${messageId}` },
|
|
46568
|
+
sessionId,
|
|
46569
|
+
turnId,
|
|
46570
|
+
"completed",
|
|
46571
|
+
"item:completed"
|
|
46572
|
+
);
|
|
46573
|
+
return;
|
|
46574
|
+
}
|
|
45960
46575
|
const content = message.message.content;
|
|
45961
46576
|
const anthropicMsgId = message.message?.id ?? messageId;
|
|
45962
46577
|
const thinkingTexts = thinkingAccumulator.get(messageId);
|
|
@@ -46208,22 +46823,29 @@ async function handleResultMessage(message, sessionId, turnId, entry, db2) {
|
|
|
46208
46823
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46209
46824
|
db2.prepare("UPDATE turns SET token_usage = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(usage), now, turnId);
|
|
46210
46825
|
broadcast("turn:tokenUsage", { sessionId, turnId, usage });
|
|
46826
|
+
const apiError = entry.lastApiError;
|
|
46827
|
+
entry.lastApiError = null;
|
|
46828
|
+
if (message.is_error) {
|
|
46829
|
+
const row = db2.prepare("SELECT status FROM turns WHERE id = ?").get(turnId);
|
|
46830
|
+
if (row?.status !== "pending" && row?.status !== "running") return;
|
|
46831
|
+
const text = (message.result || apiError?.text || "Claude API error").trim();
|
|
46832
|
+
const kind = classifyApiError({ error: apiError?.error, status: message.api_error_status, text });
|
|
46833
|
+
if (kind === "network" && !entry.threadId && markTurnInterrupted(db2, turnId, "network", { detail: text })) {
|
|
46834
|
+
return;
|
|
46835
|
+
}
|
|
46836
|
+
failTurn(db2, sessionId, turnId, text);
|
|
46837
|
+
return;
|
|
46838
|
+
}
|
|
46211
46839
|
await refreshContext(entry, sessionId, db2);
|
|
46212
46840
|
completeTurn(db2, sessionId, turnId, usage);
|
|
46213
46841
|
} else {
|
|
46214
|
-
|
|
46215
|
-
const
|
|
46216
|
-
db2
|
|
46217
|
-
"failed",
|
|
46218
|
-
errorMsg,
|
|
46219
|
-
now,
|
|
46220
|
-
turnId
|
|
46221
|
-
);
|
|
46222
|
-
recomputeSessionRunning(db2, sessionId, "failed");
|
|
46223
|
-
broadcast("turn:failed", { sessionId, turnId, error: errorMsg });
|
|
46842
|
+
entry.lastApiError = null;
|
|
46843
|
+
const errorMsg = message.errors?.join("; ") || "Claude turn failed";
|
|
46844
|
+
failTurn(db2, sessionId, turnId, errorMsg);
|
|
46224
46845
|
}
|
|
46225
46846
|
}
|
|
46226
46847
|
function completeTurn(db2, sessionId, turnId, usage) {
|
|
46848
|
+
markTurnResumed(db2, turnId, { settle: true });
|
|
46227
46849
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46228
46850
|
if (usage) {
|
|
46229
46851
|
db2.prepare("UPDATE turns SET status = ?, token_usage = ?, updated_at = ? WHERE id = ?").run(
|
|
@@ -46294,13 +46916,17 @@ async function interruptTurn2(sessionId, threadId) {
|
|
|
46294
46916
|
await entry.query.interrupt();
|
|
46295
46917
|
const db2 = getDb();
|
|
46296
46918
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46919
|
+
clearResume(entry);
|
|
46297
46920
|
db2.prepare("UPDATE turns SET status = ?, updated_at = ? WHERE id = ?").run("aborted", now, turnId);
|
|
46921
|
+
markResumeCancelled(db2, turnId);
|
|
46298
46922
|
recomputeSessionRunning(db2, sessionId);
|
|
46299
46923
|
broadcast("turn:cancelled", { sessionId, turnId });
|
|
46300
46924
|
streamingMessageIds.delete(turnId);
|
|
46301
46925
|
entry.turnId = null;
|
|
46302
46926
|
return true;
|
|
46303
46927
|
} catch {
|
|
46928
|
+
entry.closeReason = "clear";
|
|
46929
|
+
clearResume(entry);
|
|
46304
46930
|
entry.promptQueue.close();
|
|
46305
46931
|
entry.query.close();
|
|
46306
46932
|
streamingMessageIds.delete(turnId);
|
|
@@ -46316,6 +46942,7 @@ async function interruptTurn2(sessionId, threadId) {
|
|
|
46316
46942
|
if (!orphan) return false;
|
|
46317
46943
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46318
46944
|
db2.prepare("UPDATE turns SET status = ?, updated_at = ? WHERE id = ?").run("aborted", now, orphan.id);
|
|
46945
|
+
markResumeCancelled(db2, orphan.id);
|
|
46319
46946
|
recomputeSessionRunning(db2, sessionId);
|
|
46320
46947
|
broadcast("turn:cancelled", { sessionId, turnId: orphan.id });
|
|
46321
46948
|
streamingMessageIds.delete(orphan.id);
|
|
@@ -46327,6 +46954,8 @@ async function interruptTurn2(sessionId, threadId) {
|
|
|
46327
46954
|
function clearThread2(sessionId) {
|
|
46328
46955
|
const entry = sessionMap2.get(sessionId);
|
|
46329
46956
|
if (entry) {
|
|
46957
|
+
entry.closeReason ??= "clear";
|
|
46958
|
+
clearResume(entry);
|
|
46330
46959
|
entry.promptQueue.close();
|
|
46331
46960
|
entry.query.close();
|
|
46332
46961
|
if (entry.turnId) {
|
|
@@ -46337,6 +46966,7 @@ function clearThread2(sessionId) {
|
|
|
46337
46966
|
}
|
|
46338
46967
|
for (const [key, threadEntry] of sessionMap2) {
|
|
46339
46968
|
if (key.startsWith(`${sessionId}:thread:`)) {
|
|
46969
|
+
threadEntry.closeReason ??= "clear";
|
|
46340
46970
|
threadEntry.promptQueue.close();
|
|
46341
46971
|
threadEntry.query.close();
|
|
46342
46972
|
if (threadEntry.turnId) {
|
|
@@ -46352,6 +46982,7 @@ function clearThreadBranch(sessionId, threadId) {
|
|
|
46352
46982
|
const key = `${sessionId}:thread:${threadId}`;
|
|
46353
46983
|
const entry = sessionMap2.get(key);
|
|
46354
46984
|
if (entry) {
|
|
46985
|
+
entry.closeReason ??= "clear";
|
|
46355
46986
|
entry.promptQueue.close();
|
|
46356
46987
|
entry.query.close();
|
|
46357
46988
|
if (entry.turnId) {
|
|
@@ -46363,6 +46994,8 @@ function clearThreadBranch(sessionId, threadId) {
|
|
|
46363
46994
|
}
|
|
46364
46995
|
function clearAllThreads2() {
|
|
46365
46996
|
for (const [, entry] of sessionMap2) {
|
|
46997
|
+
entry.closeReason ??= "clear";
|
|
46998
|
+
clearResume(entry);
|
|
46366
46999
|
entry.promptQueue.close();
|
|
46367
47000
|
entry.query.close();
|
|
46368
47001
|
entry.backgroundTasks = [];
|
|
@@ -46426,8 +47059,42 @@ function mapSdkCommands(commands) {
|
|
|
46426
47059
|
}));
|
|
46427
47060
|
}
|
|
46428
47061
|
async function shutdown2() {
|
|
47062
|
+
let db2 = null;
|
|
47063
|
+
try {
|
|
47064
|
+
db2 = getDb();
|
|
47065
|
+
} catch {
|
|
47066
|
+
db2 = null;
|
|
47067
|
+
}
|
|
47068
|
+
for (const entry of sessionMap2.values()) {
|
|
47069
|
+
entry.closeReason = "shutdown";
|
|
47070
|
+
clearResume(entry);
|
|
47071
|
+
if (db2 && entry.turnId && !entry.threadId) {
|
|
47072
|
+
try {
|
|
47073
|
+
markTurnInterrupted(db2, entry.turnId, "host_shutdown", { notify: false });
|
|
47074
|
+
} catch (err) {
|
|
47075
|
+
console.error("[claude] could not park turn for resume:", err.message);
|
|
47076
|
+
}
|
|
47077
|
+
}
|
|
47078
|
+
}
|
|
46429
47079
|
clearAllThreads2();
|
|
46430
47080
|
}
|
|
47081
|
+
async function resumeInterruptedTurn(session, turn, opts) {
|
|
47082
|
+
if (turn.thread_id) throw new Error("Discussion-thread turns cannot be resumed");
|
|
47083
|
+
const live = sessionMap2.get(session.id);
|
|
47084
|
+
if (live && !live.closeReason) {
|
|
47085
|
+
if (live.turnId && live.turnId !== turn.id) streamingMessageIds.delete(live.turnId);
|
|
47086
|
+
clearResume(live);
|
|
47087
|
+
live.turnId = turn.id;
|
|
47088
|
+
live.threadId = null;
|
|
47089
|
+
live.lastApiError = null;
|
|
47090
|
+
live.resume = { turnId: turn.id, phase: "continuing", watchdog: null };
|
|
47091
|
+
pushPrompt(live, CONTINUE_PROMPT);
|
|
47092
|
+
return;
|
|
47093
|
+
}
|
|
47094
|
+
const entry = await ensureSessionStarted(session, void 0, void 0, { turnId: turn.id, reason: opts.reason });
|
|
47095
|
+
entry.threadId = null;
|
|
47096
|
+
if (entry.resume?.phase === "continuing") pushPrompt(entry, buildUserContent(turn));
|
|
47097
|
+
}
|
|
46431
47098
|
function syncThreadTitle2(sessionId) {
|
|
46432
47099
|
const db2 = getDb();
|
|
46433
47100
|
const session = db2.prepare("SELECT title FROM sessions WHERE id = ?").get(sessionId);
|
|
@@ -46569,6 +47236,7 @@ var claudeProvider = {
|
|
|
46569
47236
|
setModel,
|
|
46570
47237
|
stopTask,
|
|
46571
47238
|
listBackgroundTasks,
|
|
47239
|
+
resumeInterruptedTurn,
|
|
46572
47240
|
shutdown: shutdown2
|
|
46573
47241
|
};
|
|
46574
47242
|
|
|
@@ -46678,12 +47346,12 @@ var HttpError = class extends Error {
|
|
|
46678
47346
|
};
|
|
46679
47347
|
async function fetchJson(url, init, timeoutMs = 5e3) {
|
|
46680
47348
|
const controller = new AbortController();
|
|
46681
|
-
const
|
|
47349
|
+
const timer2 = setTimeout(() => controller.abort(), timeoutMs);
|
|
46682
47350
|
let response;
|
|
46683
47351
|
try {
|
|
46684
47352
|
response = await fetch(url, { ...init, signal: controller.signal });
|
|
46685
47353
|
} finally {
|
|
46686
|
-
clearTimeout(
|
|
47354
|
+
clearTimeout(timer2);
|
|
46687
47355
|
}
|
|
46688
47356
|
const text = await response.text();
|
|
46689
47357
|
if (!response.ok) {
|
|
@@ -47114,11 +47782,11 @@ async function readBody(body, onMessage, opts) {
|
|
|
47114
47782
|
function reconnectDelay(delayMs, signal) {
|
|
47115
47783
|
return new Promise((resolve) => {
|
|
47116
47784
|
if (signal?.aborted) return resolve();
|
|
47117
|
-
const
|
|
47785
|
+
const timer2 = setTimeout(resolve, delayMs);
|
|
47118
47786
|
signal?.addEventListener(
|
|
47119
47787
|
"abort",
|
|
47120
47788
|
() => {
|
|
47121
|
-
clearTimeout(
|
|
47789
|
+
clearTimeout(timer2);
|
|
47122
47790
|
resolve();
|
|
47123
47791
|
},
|
|
47124
47792
|
{ once: true }
|
|
@@ -47503,7 +48171,7 @@ var TurnProjector = class {
|
|
|
47503
48171
|
);
|
|
47504
48172
|
throwawayId = created.id;
|
|
47505
48173
|
const controller = new AbortController();
|
|
47506
|
-
const
|
|
48174
|
+
const timer2 = setTimeout(() => controller.abort(), TITLE_TIMEOUT_MS);
|
|
47507
48175
|
try {
|
|
47508
48176
|
await this.deps.client.createInput(throwawayId, {
|
|
47509
48177
|
user_prompt: buildTitlePrompt(firstTurn.user_prompt, firstTurn.agent_response),
|
|
@@ -47530,7 +48198,7 @@ var TurnProjector = class {
|
|
|
47530
48198
|
const title = cleanTitle(reply);
|
|
47531
48199
|
if (title) this.applyTitle(title);
|
|
47532
48200
|
} finally {
|
|
47533
|
-
clearTimeout(
|
|
48201
|
+
clearTimeout(timer2);
|
|
47534
48202
|
}
|
|
47535
48203
|
} catch {
|
|
47536
48204
|
} finally {
|
|
@@ -47866,7 +48534,7 @@ function failTurnBeforeStart(db2, sessionId, turnId, error) {
|
|
|
47866
48534
|
recomputeSessionRunning(db2, sessionId, "failed");
|
|
47867
48535
|
broadcast("turn:failed", { sessionId, turnId, error });
|
|
47868
48536
|
}
|
|
47869
|
-
function
|
|
48537
|
+
function failTurn2(entry, error) {
|
|
47870
48538
|
const active = entry.activeTurn;
|
|
47871
48539
|
if (!active) return;
|
|
47872
48540
|
entry.activeTurn = null;
|
|
@@ -47909,7 +48577,7 @@ async function runStreamLoop(entry, projector) {
|
|
|
47909
48577
|
if (stallTimer) clearTimeout(stallTimer);
|
|
47910
48578
|
if (projector.isDone || entry.activeTurn?.projector !== projector) return;
|
|
47911
48579
|
if (reason === "session-not-found") {
|
|
47912
|
-
|
|
48580
|
+
failTurn2(entry, "Repoburg session no longer exists on the backend.");
|
|
47913
48581
|
return;
|
|
47914
48582
|
}
|
|
47915
48583
|
if (reason === "aborted") {
|
|
@@ -48001,7 +48669,7 @@ async function startTurn3(session, turn) {
|
|
|
48001
48669
|
void ensureContextWindow(entry);
|
|
48002
48670
|
runStreamLoop(entry, projector).catch((err) => {
|
|
48003
48671
|
console.error("[repoburg] stream loop crashed:", err);
|
|
48004
|
-
|
|
48672
|
+
failTurn2(entry, `Stream loop failed: ${err.message}`);
|
|
48005
48673
|
});
|
|
48006
48674
|
try {
|
|
48007
48675
|
const images = turnImageDataUrls(turn);
|
|
@@ -48012,7 +48680,7 @@ async function startTurn3(session, turn) {
|
|
|
48012
48680
|
});
|
|
48013
48681
|
} catch (err) {
|
|
48014
48682
|
if (isConnectionError(err)) invalidateBackend(entry.baseUrl);
|
|
48015
|
-
|
|
48683
|
+
failTurn2(entry, err.message);
|
|
48016
48684
|
}
|
|
48017
48685
|
}
|
|
48018
48686
|
async function interruptTurn3(sessionId, threadId) {
|
|
@@ -48238,8 +48906,10 @@ async function main() {
|
|
|
48238
48906
|
fastify.log.error(err);
|
|
48239
48907
|
process.exit(1);
|
|
48240
48908
|
}
|
|
48909
|
+
startAutoResume();
|
|
48241
48910
|
async function gracefulShutdown(signal) {
|
|
48242
48911
|
fastify.log.info(`Received ${signal}, shutting down...`);
|
|
48912
|
+
stopAutoResume();
|
|
48243
48913
|
await shutdownAllProviders();
|
|
48244
48914
|
await fastify.close();
|
|
48245
48915
|
process.exit(0);
|