repoburg 1.3.266 → 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.
@@ -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 timer = null;
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 (timer) {
774
- clearTimeout(timer);
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
- timer = setTimeout(function() {
780
+ timer2 = setTimeout(function() {
781
781
  debug("timed out", name);
782
- timer = null;
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 timer = setTimeout(() => {
1369
+ let timer2 = setTimeout(() => {
1370
1370
  debug("timed out", name);
1371
- timer = null;
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 (timer) {
1384
- clearTimeout(timer);
1385
- timer = null;
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 = 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 config = {
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(`SELECT COUNT(*) AS c FROM turns WHERE session_id = ? AND status IN ('pending', 'running')`).get(sessionId);
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 ? "running" : idleStatus,
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 timer = setTimeout(() => {
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 timer = setInterval(async () => {
43734
+ const timer2 = setInterval(async () => {
43321
43735
  if (inst.state !== "starting") {
43322
- clearInterval(timer);
43736
+ clearInterval(timer2);
43323
43737
  resolve();
43324
43738
  return;
43325
43739
  }
43326
43740
  if (this.deps.now() > deadline) {
43327
- clearInterval(timer);
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(timer);
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 timer = pendingChanges.get(appId);
43728
- if (timer) clearTimeout(timer);
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 timer of pendingChanges.values()) clearTimeout(timer);
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 timer = setTimeout(() => {
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 timer = setTimeout(() => {
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(config) {
44799
- switch (config?.sandboxMode) {
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 config = parseRuntimeConfig(session);
44861
- const sandbox = sandboxType(config);
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 config = parseRuntimeConfig(session);
44920
- const sandbox = sandboxType(config);
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
- async function ensureSessionStarted(session, mapKey, resumeSessionId) {
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
- if (entry.turnId) {
45739
- const now = (/* @__PURE__ */ new Date()).toISOString();
45740
- const staleItems = db2.prepare(
45741
- "UPDATE items SET status = 'failed', updated_at = ? WHERE status = 'in_progress' AND turn_id IN (SELECT id FROM turns WHERE session_id = ?)"
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
- entry.promptQueue.push({
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
- const errorMsg = message.errors?.join("; ") ?? "Claude turn failed";
46215
- const now = (/* @__PURE__ */ new Date()).toISOString();
46216
- db2.prepare("UPDATE turns SET status = ?, error = ?, updated_at = ? WHERE id = ?").run(
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 timer = setTimeout(() => controller.abort(), timeoutMs);
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(timer);
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 timer = setTimeout(resolve, delayMs);
47785
+ const timer2 = setTimeout(resolve, delayMs);
47118
47786
  signal?.addEventListener(
47119
47787
  "abort",
47120
47788
  () => {
47121
- clearTimeout(timer);
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 timer = setTimeout(() => controller.abort(), TITLE_TIMEOUT_MS);
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(timer);
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 failTurn(entry, error) {
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
- failTurn(entry, "Repoburg session no longer exists on the backend.");
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
- failTurn(entry, `Stream loop failed: ${err.message}`);
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
- failTurn(entry, err.message);
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);