repoburg 1.3.265 → 1.3.267
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 +847 -119
- package/agent-pane/backend/dist/server.js +39686 -38955
- package/agent-pane/frontend/dist/assets/index-4oRwnsa5.js +542 -0
- package/agent-pane/frontend/dist/assets/index-CMUSmeTN.css +1 -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 +2 -2
- 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-Da2a9Nw3.css +0 -1
- package/agent-pane/frontend/dist/assets/index-yoIgKvsu.js +0 -532
|
@@ -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");
|
|
@@ -41337,6 +41337,15 @@ function getProvider(runtimeType) {
|
|
|
41337
41337
|
function hasProvider(runtimeType) {
|
|
41338
41338
|
return providers.has(runtimeType);
|
|
41339
41339
|
}
|
|
41340
|
+
function listAllBackgroundTasks() {
|
|
41341
|
+
const bySession = {};
|
|
41342
|
+
for (const provider of providers.values()) {
|
|
41343
|
+
for (const [sessionId, tasks] of Object.entries(provider.listBackgroundTasks?.() ?? {})) {
|
|
41344
|
+
if (tasks.length > 0) bySession[sessionId] = [...bySession[sessionId] ?? [], ...tasks];
|
|
41345
|
+
}
|
|
41346
|
+
}
|
|
41347
|
+
return bySession;
|
|
41348
|
+
}
|
|
41340
41349
|
async function shutdownAllProviders() {
|
|
41341
41350
|
for (const provider of providers.values()) {
|
|
41342
41351
|
await provider.shutdown();
|
|
@@ -41784,8 +41793,13 @@ function isSessionBusy(db2, session, opts) {
|
|
|
41784
41793
|
return false;
|
|
41785
41794
|
}
|
|
41786
41795
|
function recomputeSessionRunning(db2, sessionId, idleStatus = "active", hasUnreviewed = false) {
|
|
41787
|
-
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);
|
|
41788
41801
|
const isRunning = live.c > 0 ? 1 : 0;
|
|
41802
|
+
const liveStatus = live.c > 0 && live.w === live.c ? "interrupted" : "running";
|
|
41789
41803
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41790
41804
|
if (hasUnreviewed && isRunning === 0) {
|
|
41791
41805
|
db2.prepare("UPDATE sessions SET is_running = ?, status = ?, has_unreviewed = 1, updated_at = ? WHERE id = ?").run(
|
|
@@ -41797,13 +41811,406 @@ function recomputeSessionRunning(db2, sessionId, idleStatus = "active", hasUnrev
|
|
|
41797
41811
|
} else {
|
|
41798
41812
|
db2.prepare("UPDATE sessions SET is_running = ?, status = ?, updated_at = ? WHERE id = ?").run(
|
|
41799
41813
|
isRunning,
|
|
41800
|
-
isRunning ?
|
|
41814
|
+
isRunning ? liveStatus : idleStatus,
|
|
41801
41815
|
now,
|
|
41802
41816
|
sessionId
|
|
41803
41817
|
);
|
|
41804
41818
|
}
|
|
41805
41819
|
}
|
|
41806
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
|
+
|
|
41807
42214
|
// src/routes/turns.ts
|
|
41808
42215
|
async function registerTurnRoutes(fastify) {
|
|
41809
42216
|
fastify.post(
|
|
@@ -41901,6 +42308,22 @@ async function registerTurnRoutes(fastify) {
|
|
|
41901
42308
|
}
|
|
41902
42309
|
return { success: true };
|
|
41903
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
|
+
);
|
|
41904
42327
|
}
|
|
41905
42328
|
|
|
41906
42329
|
// src/routes/threads.ts
|
|
@@ -42611,14 +43034,14 @@ var AppServerClient = class {
|
|
|
42611
43034
|
}
|
|
42612
43035
|
const id = this.nextId++;
|
|
42613
43036
|
const message = JSON.stringify({ id, method, params }) + "\n";
|
|
42614
|
-
const
|
|
43037
|
+
const timer2 = setTimeout(() => {
|
|
42615
43038
|
this.pending.delete(id);
|
|
42616
43039
|
reject(new Error(`Request '${method}' (id=${id}) timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
42617
43040
|
}, REQUEST_TIMEOUT_MS);
|
|
42618
43041
|
this.pending.set(id, {
|
|
42619
43042
|
resolve,
|
|
42620
43043
|
reject,
|
|
42621
|
-
timer
|
|
43044
|
+
timer: timer2
|
|
42622
43045
|
});
|
|
42623
43046
|
this.write(message);
|
|
42624
43047
|
});
|
|
@@ -43308,14 +43731,14 @@ var VscodeWebManager = class {
|
|
|
43308
43731
|
waitForReady(inst) {
|
|
43309
43732
|
const deadline = this.deps.now() + STARTUP_TIMEOUT_MS;
|
|
43310
43733
|
return new Promise((resolve) => {
|
|
43311
|
-
const
|
|
43734
|
+
const timer2 = setInterval(async () => {
|
|
43312
43735
|
if (inst.state !== "starting") {
|
|
43313
|
-
clearInterval(
|
|
43736
|
+
clearInterval(timer2);
|
|
43314
43737
|
resolve();
|
|
43315
43738
|
return;
|
|
43316
43739
|
}
|
|
43317
43740
|
if (this.deps.now() > deadline) {
|
|
43318
|
-
clearInterval(
|
|
43741
|
+
clearInterval(timer2);
|
|
43319
43742
|
inst.state = "error";
|
|
43320
43743
|
inst.error = `serve-web did not become ready within ${STARTUP_TIMEOUT_MS / 1e3}s`;
|
|
43321
43744
|
inst.stopping = true;
|
|
@@ -43327,7 +43750,7 @@ var VscodeWebManager = class {
|
|
|
43327
43750
|
}
|
|
43328
43751
|
try {
|
|
43329
43752
|
await this.deps.probe(inst.port, inst.token);
|
|
43330
|
-
clearInterval(
|
|
43753
|
+
clearInterval(timer2);
|
|
43331
43754
|
if (inst.state === "starting") {
|
|
43332
43755
|
inst.state = "running";
|
|
43333
43756
|
this.markAccessed(inst.id);
|
|
@@ -43715,8 +44138,8 @@ function startAppsWatch(onAppChange) {
|
|
|
43715
44138
|
const file = segs[segs.length - 1];
|
|
43716
44139
|
if (!appId || appId === path3.basename(root) || !APP_ID_RE.test(appId) || file === "state.json") return;
|
|
43717
44140
|
if (segs.length === 1 && isRegularFile(path3.join(root, appId))) return;
|
|
43718
|
-
const
|
|
43719
|
-
if (
|
|
44141
|
+
const timer2 = pendingChanges.get(appId);
|
|
44142
|
+
if (timer2) clearTimeout(timer2);
|
|
43720
44143
|
pendingChanges.set(
|
|
43721
44144
|
appId,
|
|
43722
44145
|
setTimeout(() => {
|
|
@@ -43734,7 +44157,7 @@ function stopAppsWatch() {
|
|
|
43734
44157
|
watcher.close();
|
|
43735
44158
|
watcher = null;
|
|
43736
44159
|
}
|
|
43737
|
-
for (const
|
|
44160
|
+
for (const timer2 of pendingChanges.values()) clearTimeout(timer2);
|
|
43738
44161
|
pendingChanges.clear();
|
|
43739
44162
|
}
|
|
43740
44163
|
|
|
@@ -44130,7 +44553,7 @@ function getHeadlessToolHandler(appId, workspace = "") {
|
|
|
44130
44553
|
function dispatchHeadlessToolCall(handler, tool, args) {
|
|
44131
44554
|
const callId = v4_default();
|
|
44132
44555
|
const result = new Promise((resolve, reject) => {
|
|
44133
|
-
const
|
|
44556
|
+
const timer2 = setTimeout(() => {
|
|
44134
44557
|
pendingCalls.delete(callId);
|
|
44135
44558
|
reject(
|
|
44136
44559
|
new Error(
|
|
@@ -44138,7 +44561,7 @@ function dispatchHeadlessToolCall(handler, tool, args) {
|
|
|
44138
44561
|
)
|
|
44139
44562
|
);
|
|
44140
44563
|
}, HEADLESS_TOOL_TIMEOUT_MS);
|
|
44141
|
-
pendingCalls.set(callId, { resolve, reject, timer });
|
|
44564
|
+
pendingCalls.set(callId, { resolve, reject, timer: timer2 });
|
|
44142
44565
|
});
|
|
44143
44566
|
try {
|
|
44144
44567
|
handler("tool_call", { callId, tool, name: tool, args: args ?? {} });
|
|
@@ -44216,7 +44639,7 @@ function dispatchAppTool(toolName, args, workspace = "") {
|
|
|
44216
44639
|
}
|
|
44217
44640
|
const callId = v4_default();
|
|
44218
44641
|
const result = new Promise((resolve, reject) => {
|
|
44219
|
-
const
|
|
44642
|
+
const timer2 = setTimeout(() => {
|
|
44220
44643
|
pendingCalls2.delete(callId);
|
|
44221
44644
|
reject(
|
|
44222
44645
|
new Error(
|
|
@@ -44224,7 +44647,7 @@ function dispatchAppTool(toolName, args, workspace = "") {
|
|
|
44224
44647
|
)
|
|
44225
44648
|
);
|
|
44226
44649
|
}, TOOL_CALL_TIMEOUT_MS);
|
|
44227
|
-
pendingCalls2.set(callId, { resolve, reject, timer, appId });
|
|
44650
|
+
pendingCalls2.set(callId, { resolve, reject, timer: timer2, appId });
|
|
44228
44651
|
broadcast("app:tool_call", { appId, callId, tool: localName, args: args ?? {} });
|
|
44229
44652
|
});
|
|
44230
44653
|
return { callId, result };
|
|
@@ -44786,8 +45209,8 @@ function parseRuntimeConfig(session) {
|
|
|
44786
45209
|
return null;
|
|
44787
45210
|
}
|
|
44788
45211
|
}
|
|
44789
|
-
function sandboxType(
|
|
44790
|
-
switch (
|
|
45212
|
+
function sandboxType(config2) {
|
|
45213
|
+
switch (config2?.sandboxMode) {
|
|
44791
45214
|
case "read-only":
|
|
44792
45215
|
return "read-only";
|
|
44793
45216
|
case "danger-full-access":
|
|
@@ -44848,8 +45271,8 @@ async function getOrCreateThread(session) {
|
|
|
44848
45271
|
if (session.origin_app_id) {
|
|
44849
45272
|
autoApproveToolUserInput(client);
|
|
44850
45273
|
}
|
|
44851
|
-
const
|
|
44852
|
-
const sandbox = sandboxType(
|
|
45274
|
+
const config2 = parseRuntimeConfig(session);
|
|
45275
|
+
const sandbox = sandboxType(config2);
|
|
44853
45276
|
const instructions = threadInstructionParams(cwd);
|
|
44854
45277
|
let threadId;
|
|
44855
45278
|
if (session.external_thread_id) {
|
|
@@ -44907,8 +45330,8 @@ async function startTurn(session, turn) {
|
|
|
44907
45330
|
setSessionStatus(db2, session.id, 1, "running", now);
|
|
44908
45331
|
broadcast("turn:started", { sessionId: session.id, turnId: turn.id });
|
|
44909
45332
|
const { client, threadId } = entry;
|
|
44910
|
-
const
|
|
44911
|
-
const sandbox = sandboxType(
|
|
45333
|
+
const config2 = parseRuntimeConfig(session);
|
|
45334
|
+
const sandbox = sandboxType(config2);
|
|
44912
45335
|
let turnDone = false;
|
|
44913
45336
|
const handleItemStarted = (params) => {
|
|
44914
45337
|
if (turnDone) return;
|
|
@@ -45582,6 +46005,42 @@ async function refreshContext(entry, sessionId, db2) {
|
|
|
45582
46005
|
publish(entry, sessionId, db2);
|
|
45583
46006
|
}
|
|
45584
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
|
+
|
|
45585
46044
|
// src/claude/claude-provider.ts
|
|
45586
46045
|
var PromptQueue = class {
|
|
45587
46046
|
buffer = [];
|
|
@@ -45628,6 +46087,40 @@ var cachedModels = null;
|
|
|
45628
46087
|
var cachedCommands = null;
|
|
45629
46088
|
var thinkingAccumulator = /* @__PURE__ */ new Map();
|
|
45630
46089
|
var streamingMessageIds = /* @__PURE__ */ new Map();
|
|
46090
|
+
var publishedBackgroundTasks = /* @__PURE__ */ new Map();
|
|
46091
|
+
var THREAD_KEY_SEPARATOR = ":thread:";
|
|
46092
|
+
function sessionIdOfKey(key) {
|
|
46093
|
+
const i = key.indexOf(THREAD_KEY_SEPARATOR);
|
|
46094
|
+
return i === -1 ? key : key.slice(0, i);
|
|
46095
|
+
}
|
|
46096
|
+
function sessionBackgroundTasks(sessionId) {
|
|
46097
|
+
const tasks = [];
|
|
46098
|
+
for (const [key, entry] of sessionMap2) {
|
|
46099
|
+
if (sessionIdOfKey(key) === sessionId) tasks.push(...entry.backgroundTasks ?? []);
|
|
46100
|
+
}
|
|
46101
|
+
return tasks;
|
|
46102
|
+
}
|
|
46103
|
+
function publishBackgroundTasks(sessionId) {
|
|
46104
|
+
const tasks = sessionBackgroundTasks(sessionId);
|
|
46105
|
+
const json = JSON.stringify(tasks);
|
|
46106
|
+
if ((publishedBackgroundTasks.get(sessionId) ?? "[]") === json) return;
|
|
46107
|
+
if (tasks.length > 0) publishedBackgroundTasks.set(sessionId, json);
|
|
46108
|
+
else publishedBackgroundTasks.delete(sessionId);
|
|
46109
|
+
broadcast("background_tasks", { sessionId, tasks });
|
|
46110
|
+
}
|
|
46111
|
+
function dropBackgroundTasks(sessionId, entry) {
|
|
46112
|
+
entry.backgroundTasks = [];
|
|
46113
|
+
publishBackgroundTasks(sessionId);
|
|
46114
|
+
}
|
|
46115
|
+
function listBackgroundTasks() {
|
|
46116
|
+
const bySession = {};
|
|
46117
|
+
for (const [key, entry] of sessionMap2) {
|
|
46118
|
+
if (!entry.backgroundTasks?.length) continue;
|
|
46119
|
+
const sessionId = sessionIdOfKey(key);
|
|
46120
|
+
bySession[sessionId] = [...bySession[sessionId] ?? [], ...entry.backgroundTasks];
|
|
46121
|
+
}
|
|
46122
|
+
return bySession;
|
|
46123
|
+
}
|
|
45631
46124
|
function setSessionStatus2(db2, sessionId, isRunning, status, now, hasUnreviewed = false) {
|
|
45632
46125
|
if (hasUnreviewed) {
|
|
45633
46126
|
db2.prepare("UPDATE sessions SET is_running = ?, status = ?, has_unreviewed = 1, updated_at = ? WHERE id = ?").run(
|
|
@@ -45645,10 +46138,153 @@ function setSessionStatus2(db2, sessionId, isRunning, status, now, hasUnreviewed
|
|
|
45645
46138
|
);
|
|
45646
46139
|
}
|
|
45647
46140
|
}
|
|
45648
|
-
|
|
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) {
|
|
45649
46283
|
const db2 = getDb();
|
|
45650
46284
|
const key = mapKey ?? session.id;
|
|
45651
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);
|
|
45652
46288
|
const options = {
|
|
45653
46289
|
cwd: session.cwd ?? process.cwd(),
|
|
45654
46290
|
model: session.model ?? void 0,
|
|
@@ -45657,26 +46293,45 @@ async function ensureSessionStarted(session, mapKey, resumeSessionId) {
|
|
|
45657
46293
|
includePartialMessages: true,
|
|
45658
46294
|
...CLI_OVERRIDE,
|
|
45659
46295
|
...session.model_reasoning_effort ? { effort: session.model_reasoning_effort } : {},
|
|
45660
|
-
...resumeId ? { resume: resumeId } : {}
|
|
46296
|
+
...resumeId ? { resume: resumeId } : {},
|
|
46297
|
+
...env ? { env } : {}
|
|
45661
46298
|
};
|
|
45662
46299
|
const promptQueue = new PromptQueue();
|
|
45663
46300
|
const q = query({ prompt: promptQueue, options });
|
|
45664
46301
|
const entry = {
|
|
45665
46302
|
query: q,
|
|
45666
46303
|
sessionId: null,
|
|
45667
|
-
turnId: null,
|
|
46304
|
+
turnId: resume?.turnId ?? null,
|
|
45668
46305
|
cwd: session.cwd ?? process.cwd(),
|
|
45669
46306
|
promptQueue,
|
|
45670
|
-
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
|
|
45671
46311
|
};
|
|
45672
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
|
+
}
|
|
45673
46322
|
entry.messageLoop = (async () => {
|
|
45674
46323
|
try {
|
|
45675
46324
|
for await (const message of q) {
|
|
46325
|
+
if (isBackgroundTasksChanged(message)) {
|
|
46326
|
+
handleBackgroundTasksChanged(message, session.id, entry);
|
|
46327
|
+
continue;
|
|
46328
|
+
}
|
|
45676
46329
|
const currentTurnId = entry.turnId;
|
|
45677
46330
|
if (!currentTurnId) continue;
|
|
46331
|
+
if (entry.resume && !trackResumeFrame(entry, message, db2)) continue;
|
|
45678
46332
|
await handleMessage(message, session.id, currentTurnId, entry, db2);
|
|
45679
46333
|
}
|
|
46334
|
+
if (entry.closeReason === "shutdown") return;
|
|
45680
46335
|
if (entry.turnId) {
|
|
45681
46336
|
const currentTurn = db2.prepare("SELECT status, token_usage FROM turns WHERE id = ?").get(entry.turnId);
|
|
45682
46337
|
if (currentTurn && currentTurn.status === "running") {
|
|
@@ -45687,47 +46342,21 @@ async function ensureSessionStarted(session, mapKey, resumeSessionId) {
|
|
|
45687
46342
|
}
|
|
45688
46343
|
} catch (err) {
|
|
45689
46344
|
const errMsg = err.message;
|
|
46345
|
+
clearResume(entry);
|
|
46346
|
+
if (entry.closeReason === "shutdown") return;
|
|
45690
46347
|
console.error(`[claude] message loop error for session ${session.id}:`, errMsg);
|
|
45691
|
-
|
|
45692
|
-
|
|
45693
|
-
|
|
45694
|
-
|
|
45695
|
-
).run(now, session.id);
|
|
45696
|
-
db2.prepare("UPDATE turns SET status = ?, error = ?, updated_at = ? WHERE id = ?").run(
|
|
45697
|
-
"failed",
|
|
45698
|
-
errMsg,
|
|
45699
|
-
now,
|
|
45700
|
-
entry.turnId
|
|
45701
|
-
);
|
|
45702
|
-
recomputeSessionRunning(db2, session.id, "failed");
|
|
45703
|
-
broadcast("turn:failed", { sessionId: session.id, turnId: entry.turnId, error: errMsg });
|
|
45704
|
-
if (staleItems.changes > 0) {
|
|
45705
|
-
const staleRows = db2.prepare(
|
|
45706
|
-
"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 = ?"
|
|
45707
|
-
).all(session.id, now);
|
|
45708
|
-
for (const row of staleRows) {
|
|
45709
|
-
broadcast("item:completed", {
|
|
45710
|
-
sessionId: session.id,
|
|
45711
|
-
turnId: row.turn_id,
|
|
45712
|
-
item: {
|
|
45713
|
-
id: row.id,
|
|
45714
|
-
turn_id: row.turn_id,
|
|
45715
|
-
external_item_id: row.external_item_id,
|
|
45716
|
-
type: row.type,
|
|
45717
|
-
data: row.data ? JSON.parse(row.data) : {},
|
|
45718
|
-
status: "failed",
|
|
45719
|
-
created_at: row.created_at,
|
|
45720
|
-
updated_at: now
|
|
45721
|
-
}
|
|
45722
|
-
});
|
|
45723
|
-
}
|
|
45724
|
-
}
|
|
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);
|
|
45725
46352
|
}
|
|
45726
46353
|
} finally {
|
|
46354
|
+
clearResume(entry);
|
|
45727
46355
|
if (entry.turnId) {
|
|
45728
46356
|
streamingMessageIds.delete(entry.turnId);
|
|
45729
46357
|
}
|
|
45730
|
-
sessionMap2.delete(key);
|
|
46358
|
+
if (sessionMap2.get(key) === entry) sessionMap2.delete(key);
|
|
46359
|
+
dropBackgroundTasks(session.id, entry);
|
|
45731
46360
|
}
|
|
45732
46361
|
})();
|
|
45733
46362
|
return entry;
|
|
@@ -45770,13 +46399,20 @@ async function startTurn2(session, turn) {
|
|
|
45770
46399
|
streamingMessageIds.delete(entry.turnId);
|
|
45771
46400
|
}
|
|
45772
46401
|
entry.turnId = turn.id;
|
|
46402
|
+
entry.lastApiError = null;
|
|
46403
|
+
clearResume(entry);
|
|
45773
46404
|
db2.prepare("UPDATE turns SET status = ?, updated_at = ? WHERE id = ?").run("running", now, turn.id);
|
|
45774
46405
|
setSessionStatus2(db2, session.id, 1, "running", now);
|
|
45775
46406
|
broadcast("turn:started", { sessionId: session.id, turnId: turn.id });
|
|
46407
|
+
pushPrompt(entry, buildUserContent(turn));
|
|
46408
|
+
}
|
|
46409
|
+
function buildUserContent(turn) {
|
|
45776
46410
|
let content;
|
|
45777
46411
|
const rawAttachments = turn.attachments;
|
|
45778
46412
|
let attachments = null;
|
|
45779
|
-
if (rawAttachments) {
|
|
46413
|
+
if (Array.isArray(rawAttachments)) {
|
|
46414
|
+
attachments = rawAttachments;
|
|
46415
|
+
} else if (rawAttachments) {
|
|
45780
46416
|
try {
|
|
45781
46417
|
attachments = JSON.parse(rawAttachments);
|
|
45782
46418
|
} catch {
|
|
@@ -45812,11 +46448,7 @@ ${decoded}
|
|
|
45812
46448
|
} else {
|
|
45813
46449
|
content = turn.user_prompt;
|
|
45814
46450
|
}
|
|
45815
|
-
|
|
45816
|
-
type: "user",
|
|
45817
|
-
message: { role: "user", content },
|
|
45818
|
-
parent_tool_use_id: null
|
|
45819
|
-
});
|
|
46451
|
+
return content;
|
|
45820
46452
|
}
|
|
45821
46453
|
async function handleMessage(message, sessionId, turnId, entry, db2) {
|
|
45822
46454
|
switch (message.type) {
|
|
@@ -45867,11 +46499,17 @@ async function handleMessage(message, sessionId, turnId, entry, db2) {
|
|
|
45867
46499
|
entry,
|
|
45868
46500
|
db2
|
|
45869
46501
|
);
|
|
45870
|
-
} else if (sub === "
|
|
45871
|
-
|
|
45872
|
-
|
|
45873
|
-
sessionId
|
|
45874
|
-
|
|
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
|
+
});
|
|
45875
46513
|
}
|
|
45876
46514
|
}
|
|
45877
46515
|
break;
|
|
@@ -45914,6 +46552,26 @@ function handleAssistantMessage(message, sessionId, turnId, entry, db2) {
|
|
|
45914
46552
|
if (message.message?.model !== "<synthetic>") {
|
|
45915
46553
|
recordApiUsage(entry, sessionId, db2, message.message?.id ?? null, message.message?.usage);
|
|
45916
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
|
+
}
|
|
45917
46575
|
const content = message.message.content;
|
|
45918
46576
|
const anthropicMsgId = message.message?.id ?? messageId;
|
|
45919
46577
|
const thinkingTexts = thinkingAccumulator.get(messageId);
|
|
@@ -46149,9 +46807,13 @@ function handleTaskNotification(message, sessionId, turnId, entry, db2) {
|
|
|
46149
46807
|
"item:completed"
|
|
46150
46808
|
);
|
|
46151
46809
|
}
|
|
46152
|
-
function
|
|
46810
|
+
function isBackgroundTasksChanged(message) {
|
|
46811
|
+
return message.type === "system" && "subtype" in message && message.subtype === "background_tasks_changed";
|
|
46812
|
+
}
|
|
46813
|
+
function handleBackgroundTasksChanged(message, sessionId, entry) {
|
|
46153
46814
|
const m = message;
|
|
46154
|
-
|
|
46815
|
+
entry.backgroundTasks = (m.tasks ?? []).filter((t) => !t.ambient).map((t) => ({ task_id: t.task_id, task_type: t.task_type, description: t.description }));
|
|
46816
|
+
publishBackgroundTasks(sessionId);
|
|
46155
46817
|
}
|
|
46156
46818
|
async function handleResultMessage(message, sessionId, turnId, entry, db2) {
|
|
46157
46819
|
const parentToolUseId = message.parent_tool_use_id ?? null;
|
|
@@ -46161,22 +46823,29 @@ async function handleResultMessage(message, sessionId, turnId, entry, db2) {
|
|
|
46161
46823
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46162
46824
|
db2.prepare("UPDATE turns SET token_usage = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(usage), now, turnId);
|
|
46163
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
|
+
}
|
|
46164
46839
|
await refreshContext(entry, sessionId, db2);
|
|
46165
46840
|
completeTurn(db2, sessionId, turnId, usage);
|
|
46166
46841
|
} else {
|
|
46167
|
-
|
|
46168
|
-
const
|
|
46169
|
-
db2
|
|
46170
|
-
"failed",
|
|
46171
|
-
errorMsg,
|
|
46172
|
-
now,
|
|
46173
|
-
turnId
|
|
46174
|
-
);
|
|
46175
|
-
recomputeSessionRunning(db2, sessionId, "failed");
|
|
46176
|
-
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);
|
|
46177
46845
|
}
|
|
46178
46846
|
}
|
|
46179
46847
|
function completeTurn(db2, sessionId, turnId, usage) {
|
|
46848
|
+
markTurnResumed(db2, turnId, { settle: true });
|
|
46180
46849
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46181
46850
|
if (usage) {
|
|
46182
46851
|
db2.prepare("UPDATE turns SET status = ?, token_usage = ?, updated_at = ? WHERE id = ?").run(
|
|
@@ -46247,17 +46916,22 @@ async function interruptTurn2(sessionId, threadId) {
|
|
|
46247
46916
|
await entry.query.interrupt();
|
|
46248
46917
|
const db2 = getDb();
|
|
46249
46918
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46919
|
+
clearResume(entry);
|
|
46250
46920
|
db2.prepare("UPDATE turns SET status = ?, updated_at = ? WHERE id = ?").run("aborted", now, turnId);
|
|
46921
|
+
markResumeCancelled(db2, turnId);
|
|
46251
46922
|
recomputeSessionRunning(db2, sessionId);
|
|
46252
46923
|
broadcast("turn:cancelled", { sessionId, turnId });
|
|
46253
46924
|
streamingMessageIds.delete(turnId);
|
|
46254
46925
|
entry.turnId = null;
|
|
46255
46926
|
return true;
|
|
46256
46927
|
} catch {
|
|
46928
|
+
entry.closeReason = "clear";
|
|
46929
|
+
clearResume(entry);
|
|
46257
46930
|
entry.promptQueue.close();
|
|
46258
46931
|
entry.query.close();
|
|
46259
46932
|
streamingMessageIds.delete(turnId);
|
|
46260
46933
|
sessionMap2.delete(entryKey);
|
|
46934
|
+
dropBackgroundTasks(sessionId, entry);
|
|
46261
46935
|
}
|
|
46262
46936
|
}
|
|
46263
46937
|
try {
|
|
@@ -46268,6 +46942,7 @@ async function interruptTurn2(sessionId, threadId) {
|
|
|
46268
46942
|
if (!orphan) return false;
|
|
46269
46943
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
46270
46944
|
db2.prepare("UPDATE turns SET status = ?, updated_at = ? WHERE id = ?").run("aborted", now, orphan.id);
|
|
46945
|
+
markResumeCancelled(db2, orphan.id);
|
|
46271
46946
|
recomputeSessionRunning(db2, sessionId);
|
|
46272
46947
|
broadcast("turn:cancelled", { sessionId, turnId: orphan.id });
|
|
46273
46948
|
streamingMessageIds.delete(orphan.id);
|
|
@@ -46279,43 +46954,55 @@ async function interruptTurn2(sessionId, threadId) {
|
|
|
46279
46954
|
function clearThread2(sessionId) {
|
|
46280
46955
|
const entry = sessionMap2.get(sessionId);
|
|
46281
46956
|
if (entry) {
|
|
46957
|
+
entry.closeReason ??= "clear";
|
|
46958
|
+
clearResume(entry);
|
|
46282
46959
|
entry.promptQueue.close();
|
|
46283
46960
|
entry.query.close();
|
|
46284
46961
|
if (entry.turnId) {
|
|
46285
46962
|
streamingMessageIds.delete(entry.turnId);
|
|
46286
46963
|
}
|
|
46287
46964
|
sessionMap2.delete(sessionId);
|
|
46965
|
+
entry.backgroundTasks = [];
|
|
46288
46966
|
}
|
|
46289
46967
|
for (const [key, threadEntry] of sessionMap2) {
|
|
46290
46968
|
if (key.startsWith(`${sessionId}:thread:`)) {
|
|
46969
|
+
threadEntry.closeReason ??= "clear";
|
|
46291
46970
|
threadEntry.promptQueue.close();
|
|
46292
46971
|
threadEntry.query.close();
|
|
46293
46972
|
if (threadEntry.turnId) {
|
|
46294
46973
|
streamingMessageIds.delete(threadEntry.turnId);
|
|
46295
46974
|
}
|
|
46296
46975
|
sessionMap2.delete(key);
|
|
46976
|
+
threadEntry.backgroundTasks = [];
|
|
46297
46977
|
}
|
|
46298
46978
|
}
|
|
46979
|
+
publishBackgroundTasks(sessionId);
|
|
46299
46980
|
}
|
|
46300
46981
|
function clearThreadBranch(sessionId, threadId) {
|
|
46301
46982
|
const key = `${sessionId}:thread:${threadId}`;
|
|
46302
46983
|
const entry = sessionMap2.get(key);
|
|
46303
46984
|
if (entry) {
|
|
46985
|
+
entry.closeReason ??= "clear";
|
|
46304
46986
|
entry.promptQueue.close();
|
|
46305
46987
|
entry.query.close();
|
|
46306
46988
|
if (entry.turnId) {
|
|
46307
46989
|
streamingMessageIds.delete(entry.turnId);
|
|
46308
46990
|
}
|
|
46309
46991
|
sessionMap2.delete(key);
|
|
46992
|
+
dropBackgroundTasks(sessionId, entry);
|
|
46310
46993
|
}
|
|
46311
46994
|
}
|
|
46312
46995
|
function clearAllThreads2() {
|
|
46313
46996
|
for (const [, entry] of sessionMap2) {
|
|
46997
|
+
entry.closeReason ??= "clear";
|
|
46998
|
+
clearResume(entry);
|
|
46314
46999
|
entry.promptQueue.close();
|
|
46315
47000
|
entry.query.close();
|
|
47001
|
+
entry.backgroundTasks = [];
|
|
46316
47002
|
}
|
|
46317
47003
|
sessionMap2.clear();
|
|
46318
47004
|
titleGenerating2.clear();
|
|
47005
|
+
for (const sessionId of [...publishedBackgroundTasks.keys()]) publishBackgroundTasks(sessionId);
|
|
46319
47006
|
}
|
|
46320
47007
|
async function setModel(sessionId, model) {
|
|
46321
47008
|
const entry = sessionMap2.get(sessionId);
|
|
@@ -46372,8 +47059,42 @@ function mapSdkCommands(commands) {
|
|
|
46372
47059
|
}));
|
|
46373
47060
|
}
|
|
46374
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
|
+
}
|
|
46375
47079
|
clearAllThreads2();
|
|
46376
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
|
+
}
|
|
46377
47098
|
function syncThreadTitle2(sessionId) {
|
|
46378
47099
|
const db2 = getDb();
|
|
46379
47100
|
const session = db2.prepare("SELECT title FROM sessions WHERE id = ?").get(sessionId);
|
|
@@ -46514,6 +47235,8 @@ var claudeProvider = {
|
|
|
46514
47235
|
summarizeThread: summarizeClaudeThread,
|
|
46515
47236
|
setModel,
|
|
46516
47237
|
stopTask,
|
|
47238
|
+
listBackgroundTasks,
|
|
47239
|
+
resumeInterruptedTurn,
|
|
46517
47240
|
shutdown: shutdown2
|
|
46518
47241
|
};
|
|
46519
47242
|
|
|
@@ -46623,12 +47346,12 @@ var HttpError = class extends Error {
|
|
|
46623
47346
|
};
|
|
46624
47347
|
async function fetchJson(url, init, timeoutMs = 5e3) {
|
|
46625
47348
|
const controller = new AbortController();
|
|
46626
|
-
const
|
|
47349
|
+
const timer2 = setTimeout(() => controller.abort(), timeoutMs);
|
|
46627
47350
|
let response;
|
|
46628
47351
|
try {
|
|
46629
47352
|
response = await fetch(url, { ...init, signal: controller.signal });
|
|
46630
47353
|
} finally {
|
|
46631
|
-
clearTimeout(
|
|
47354
|
+
clearTimeout(timer2);
|
|
46632
47355
|
}
|
|
46633
47356
|
const text = await response.text();
|
|
46634
47357
|
if (!response.ok) {
|
|
@@ -47059,11 +47782,11 @@ async function readBody(body, onMessage, opts) {
|
|
|
47059
47782
|
function reconnectDelay(delayMs, signal) {
|
|
47060
47783
|
return new Promise((resolve) => {
|
|
47061
47784
|
if (signal?.aborted) return resolve();
|
|
47062
|
-
const
|
|
47785
|
+
const timer2 = setTimeout(resolve, delayMs);
|
|
47063
47786
|
signal?.addEventListener(
|
|
47064
47787
|
"abort",
|
|
47065
47788
|
() => {
|
|
47066
|
-
clearTimeout(
|
|
47789
|
+
clearTimeout(timer2);
|
|
47067
47790
|
resolve();
|
|
47068
47791
|
},
|
|
47069
47792
|
{ once: true }
|
|
@@ -47448,7 +48171,7 @@ var TurnProjector = class {
|
|
|
47448
48171
|
);
|
|
47449
48172
|
throwawayId = created.id;
|
|
47450
48173
|
const controller = new AbortController();
|
|
47451
|
-
const
|
|
48174
|
+
const timer2 = setTimeout(() => controller.abort(), TITLE_TIMEOUT_MS);
|
|
47452
48175
|
try {
|
|
47453
48176
|
await this.deps.client.createInput(throwawayId, {
|
|
47454
48177
|
user_prompt: buildTitlePrompt(firstTurn.user_prompt, firstTurn.agent_response),
|
|
@@ -47475,7 +48198,7 @@ var TurnProjector = class {
|
|
|
47475
48198
|
const title = cleanTitle(reply);
|
|
47476
48199
|
if (title) this.applyTitle(title);
|
|
47477
48200
|
} finally {
|
|
47478
|
-
clearTimeout(
|
|
48201
|
+
clearTimeout(timer2);
|
|
47479
48202
|
}
|
|
47480
48203
|
} catch {
|
|
47481
48204
|
} finally {
|
|
@@ -47811,7 +48534,7 @@ function failTurnBeforeStart(db2, sessionId, turnId, error) {
|
|
|
47811
48534
|
recomputeSessionRunning(db2, sessionId, "failed");
|
|
47812
48535
|
broadcast("turn:failed", { sessionId, turnId, error });
|
|
47813
48536
|
}
|
|
47814
|
-
function
|
|
48537
|
+
function failTurn2(entry, error) {
|
|
47815
48538
|
const active = entry.activeTurn;
|
|
47816
48539
|
if (!active) return;
|
|
47817
48540
|
entry.activeTurn = null;
|
|
@@ -47854,7 +48577,7 @@ async function runStreamLoop(entry, projector) {
|
|
|
47854
48577
|
if (stallTimer) clearTimeout(stallTimer);
|
|
47855
48578
|
if (projector.isDone || entry.activeTurn?.projector !== projector) return;
|
|
47856
48579
|
if (reason === "session-not-found") {
|
|
47857
|
-
|
|
48580
|
+
failTurn2(entry, "Repoburg session no longer exists on the backend.");
|
|
47858
48581
|
return;
|
|
47859
48582
|
}
|
|
47860
48583
|
if (reason === "aborted") {
|
|
@@ -47946,7 +48669,7 @@ async function startTurn3(session, turn) {
|
|
|
47946
48669
|
void ensureContextWindow(entry);
|
|
47947
48670
|
runStreamLoop(entry, projector).catch((err) => {
|
|
47948
48671
|
console.error("[repoburg] stream loop crashed:", err);
|
|
47949
|
-
|
|
48672
|
+
failTurn2(entry, `Stream loop failed: ${err.message}`);
|
|
47950
48673
|
});
|
|
47951
48674
|
try {
|
|
47952
48675
|
const images = turnImageDataUrls(turn);
|
|
@@ -47957,7 +48680,7 @@ async function startTurn3(session, turn) {
|
|
|
47957
48680
|
});
|
|
47958
48681
|
} catch (err) {
|
|
47959
48682
|
if (isConnectionError(err)) invalidateBackend(entry.baseUrl);
|
|
47960
|
-
|
|
48683
|
+
failTurn2(entry, err.message);
|
|
47961
48684
|
}
|
|
47962
48685
|
}
|
|
47963
48686
|
async function interruptTurn3(sessionId, threadId) {
|
|
@@ -48147,6 +48870,9 @@ async function createAgentPaneApp(dbPath) {
|
|
|
48147
48870
|
await registerVscodeWebRoutes(fastify);
|
|
48148
48871
|
await registerAppRoutes(fastify);
|
|
48149
48872
|
const wss2 = initWs();
|
|
48873
|
+
wss2.on("connection", (ws) => {
|
|
48874
|
+
ws.send(JSON.stringify({ event: "background_tasks:snapshot", data: { sessions: listAllBackgroundTasks() } }));
|
|
48875
|
+
});
|
|
48150
48876
|
return { fastify, wss: wss2 };
|
|
48151
48877
|
}
|
|
48152
48878
|
|
|
@@ -48180,8 +48906,10 @@ async function main() {
|
|
|
48180
48906
|
fastify.log.error(err);
|
|
48181
48907
|
process.exit(1);
|
|
48182
48908
|
}
|
|
48909
|
+
startAutoResume();
|
|
48183
48910
|
async function gracefulShutdown(signal) {
|
|
48184
48911
|
fastify.log.info(`Received ${signal}, shutting down...`);
|
|
48912
|
+
stopAutoResume();
|
|
48185
48913
|
await shutdownAllProviders();
|
|
48186
48914
|
await fastify.close();
|
|
48187
48915
|
process.exit(0);
|