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.
@@ -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");
@@ -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(`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);
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 ? "running" : idleStatus,
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 timer = setTimeout(() => {
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 timer = setInterval(async () => {
43734
+ const timer2 = setInterval(async () => {
43312
43735
  if (inst.state !== "starting") {
43313
- clearInterval(timer);
43736
+ clearInterval(timer2);
43314
43737
  resolve();
43315
43738
  return;
43316
43739
  }
43317
43740
  if (this.deps.now() > deadline) {
43318
- clearInterval(timer);
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(timer);
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 timer = pendingChanges.get(appId);
43719
- if (timer) clearTimeout(timer);
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 timer of pendingChanges.values()) clearTimeout(timer);
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 timer = setTimeout(() => {
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 timer = setTimeout(() => {
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(config) {
44790
- switch (config?.sandboxMode) {
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 config = parseRuntimeConfig(session);
44852
- const sandbox = sandboxType(config);
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 config = parseRuntimeConfig(session);
44911
- const sandbox = sandboxType(config);
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
- 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) {
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
- if (entry.turnId) {
45692
- const now = (/* @__PURE__ */ new Date()).toISOString();
45693
- const staleItems = db2.prepare(
45694
- "UPDATE items SET status = 'failed', updated_at = ? WHERE status = 'in_progress' AND turn_id IN (SELECT id FROM turns WHERE session_id = ?)"
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
- entry.promptQueue.push({
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 === "background_tasks_changed") {
45871
- handleBackgroundTasksChanged(
45872
- message,
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 handleBackgroundTasksChanged(message, sessionId) {
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
- broadcast("background_tasks", { sessionId, tasks: m.tasks ?? [] });
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
- const errorMsg = message.errors?.join("; ") ?? "Claude turn failed";
46168
- const now = (/* @__PURE__ */ new Date()).toISOString();
46169
- db2.prepare("UPDATE turns SET status = ?, error = ?, updated_at = ? WHERE id = ?").run(
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 timer = setTimeout(() => controller.abort(), timeoutMs);
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(timer);
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 timer = setTimeout(resolve, delayMs);
47785
+ const timer2 = setTimeout(resolve, delayMs);
47063
47786
  signal?.addEventListener(
47064
47787
  "abort",
47065
47788
  () => {
47066
- clearTimeout(timer);
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 timer = setTimeout(() => controller.abort(), TITLE_TIMEOUT_MS);
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(timer);
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 failTurn(entry, error) {
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
- failTurn(entry, "Repoburg session no longer exists on the backend.");
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
- failTurn(entry, `Stream loop failed: ${err.message}`);
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
- failTurn(entry, err.message);
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);