sparkecoder 0.1.121 → 0.1.122

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.
Files changed (102) hide show
  1. package/dist/agent/index.js +182 -21
  2. package/dist/agent/index.js.map +1 -1
  3. package/dist/cli.js +483 -157
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +444 -118
  6. package/dist/index.js.map +1 -1
  7. package/dist/server/index.js +444 -118
  8. package/dist/server/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/web/.next/BUILD_ID +1 -1
  11. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  12. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  13. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  14. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  15. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  16. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  17. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  18. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  19. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  20. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  22. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  30. package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  48. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  57. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  66. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  74. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  82. package/web/.next/standalone/web/.next/server/app/settings.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  85. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  86. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  89. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  90. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  91. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  92. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  93. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  94. /package/web/.next/standalone/web/.next/static/{static/wP9z41wtqT4k-O6AlEXqw → BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
  95. /package/web/.next/standalone/web/.next/static/{static/wP9z41wtqT4k-O6AlEXqw → BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
  96. /package/web/.next/standalone/web/.next/static/{static/wP9z41wtqT4k-O6AlEXqw → BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
  97. /package/web/.next/standalone/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → static/BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
  98. /package/web/.next/standalone/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → static/BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
  99. /package/web/.next/standalone/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → static/BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
  100. /package/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
  101. /package/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
  102. /package/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
@@ -2329,10 +2329,10 @@ async function resizeImageIfNeeded(buffer, mediaType) {
2329
2329
  const willConvertToJpeg = isPng && (needsShrink || buffer.length > 2 * 1024 * 1024);
2330
2330
  const outputMediaType = willConvertToJpeg || !isPng ? "image/jpeg" : "image/png";
2331
2331
  const ext = outputMediaType === "image/png" ? ".png" : ".jpg";
2332
- const cachePath = join3(cacheDir, key2 + ext);
2333
- if (existsSync3(cachePath)) {
2332
+ const cachePath2 = join3(cacheDir, key2 + ext);
2333
+ if (existsSync3(cachePath2)) {
2334
2334
  console.log(`[image-resize] Cache hit for ${width}x${height} image`);
2335
- return { buffer: readFileSync2(cachePath), mediaType: outputMediaType };
2335
+ return { buffer: readFileSync2(cachePath2), mediaType: outputMediaType };
2336
2336
  }
2337
2337
  let pipeline = sharp(buffer);
2338
2338
  if (needsResize) {
@@ -2357,7 +2357,7 @@ async function resizeImageIfNeeded(buffer, mediaType) {
2357
2357
  }
2358
2358
  finalMediaType = "image/jpeg";
2359
2359
  }
2360
- writeFileSync2(cachePath, result);
2360
+ writeFileSync2(cachePath2, result);
2361
2361
  const resultMeta = await sharp(result).metadata();
2362
2362
  console.log(
2363
2363
  `[image-resize] ${width}x${height} -> ${resultMeta.width}x${resultMeta.height} (${(buffer.length / 1024).toFixed(0)}KB -> ${(result.length / 1024).toFixed(0)}KB, ${finalMediaType})`
@@ -7430,6 +7430,35 @@ function stripOrphanedToolResults(msg, removedIds) {
7430
7430
  if (parts.length === 0) return null;
7431
7431
  return { ...msg, content: parts };
7432
7432
  }
7433
+ function wrapToolsNeverThrow(tools) {
7434
+ if (!tools || typeof tools !== "object") return tools;
7435
+ const wrapped = {};
7436
+ for (const [name, t] of Object.entries(tools)) {
7437
+ if (!t || typeof t.execute !== "function") {
7438
+ wrapped[name] = t;
7439
+ continue;
7440
+ }
7441
+ const original = t.execute;
7442
+ wrapped[name] = {
7443
+ ...t,
7444
+ execute: async (input, opts) => {
7445
+ try {
7446
+ return await original.call(t, input, opts);
7447
+ } catch (err) {
7448
+ const message = err?.message ?? String(err);
7449
+ console.warn(`[tool:${name}] threw \u2014 converted to error result so the tool-call isn't orphaned:`, message);
7450
+ return {
7451
+ __error: true,
7452
+ tool: name,
7453
+ message,
7454
+ note: "Tool execution threw an exception. The agent should treat this as a failed tool call and decide how to recover."
7455
+ };
7456
+ }
7457
+ }
7458
+ };
7459
+ }
7460
+ return wrapped;
7461
+ }
7433
7462
  function repairToolPairing(messages) {
7434
7463
  const toolCallIds = /* @__PURE__ */ new Set();
7435
7464
  const toolResultIds = /* @__PURE__ */ new Set();
@@ -7795,6 +7824,120 @@ var init_web = __esm({
7795
7824
  }
7796
7825
  });
7797
7826
 
7827
+ // src/integrations/slack/persistence.ts
7828
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync3, renameSync } from "fs";
7829
+ import { join as join9, dirname as dirname6 } from "path";
7830
+ function cachePath() {
7831
+ return join9(ensureAppDataDirectory(), FILENAME);
7832
+ }
7833
+ function load() {
7834
+ if (loaded) return;
7835
+ loaded = true;
7836
+ const path = cachePath();
7837
+ if (!existsSync16(path)) return;
7838
+ try {
7839
+ const raw = readFileSync7(path, "utf-8");
7840
+ const parsed = JSON.parse(raw);
7841
+ if (parsed?.version !== FILE_VERSION) return;
7842
+ const now = Date.now();
7843
+ for (const [id, e] of Object.entries(parsed.users || {})) {
7844
+ if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
7845
+ userMap.set(id, { name: e.name ?? null, expiresAt: e.expiresAt });
7846
+ }
7847
+ }
7848
+ for (const [key2, e] of Object.entries(parsed.threads || {})) {
7849
+ if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
7850
+ threadMap.set(key2, { owned: !!e.owned, expiresAt: e.expiresAt });
7851
+ }
7852
+ }
7853
+ } catch (err) {
7854
+ console.warn(`[slack] could not load ${FILENAME}:`, err?.message || err);
7855
+ }
7856
+ }
7857
+ function evictIfOversized(map, max) {
7858
+ if (map.size <= max) return;
7859
+ const sorted = [...map.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt);
7860
+ const toRemove = map.size - max;
7861
+ for (let i = 0; i < toRemove; i++) map.delete(sorted[i][0]);
7862
+ }
7863
+ function scheduleSave() {
7864
+ dirty = true;
7865
+ if (saveTimer) return;
7866
+ saveTimer = setTimeout(() => {
7867
+ saveTimer = null;
7868
+ if (dirty) saveSync();
7869
+ }, SAVE_DEBOUNCE_MS);
7870
+ saveTimer.unref?.();
7871
+ }
7872
+ function saveSync() {
7873
+ dirty = false;
7874
+ try {
7875
+ const path = cachePath();
7876
+ const dir = dirname6(path);
7877
+ if (!existsSync16(dir)) mkdirSync6(dir, { recursive: true });
7878
+ const payload = {
7879
+ version: FILE_VERSION,
7880
+ users: Object.fromEntries(userMap),
7881
+ threads: Object.fromEntries(threadMap)
7882
+ };
7883
+ const tmp = `${path}.tmp`;
7884
+ writeFileSync3(tmp, JSON.stringify(payload), "utf-8");
7885
+ renameSync(tmp, path);
7886
+ } catch (err) {
7887
+ console.warn(`[slack] could not persist ${FILENAME}:`, err?.message || err);
7888
+ }
7889
+ }
7890
+ function hookExit() {
7891
+ if (exitHooked) return;
7892
+ exitHooked = true;
7893
+ const flush2 = () => {
7894
+ if (dirty) saveSync();
7895
+ };
7896
+ process.once("beforeExit", flush2);
7897
+ process.once("SIGINT", flush2);
7898
+ process.once("SIGTERM", flush2);
7899
+ }
7900
+ function getCachedUserName(userId) {
7901
+ load();
7902
+ return userMap.get(userId);
7903
+ }
7904
+ function setCachedUserName(userId, entry2) {
7905
+ load();
7906
+ hookExit();
7907
+ userMap.set(userId, entry2);
7908
+ evictIfOversized(userMap, MAX_USERS);
7909
+ scheduleSave();
7910
+ }
7911
+ function getCachedThreadOwnership(key2) {
7912
+ load();
7913
+ return threadMap.get(key2);
7914
+ }
7915
+ function setCachedThreadOwnership(key2, entry2) {
7916
+ load();
7917
+ hookExit();
7918
+ threadMap.set(key2, entry2);
7919
+ evictIfOversized(threadMap, MAX_THREADS);
7920
+ scheduleSave();
7921
+ }
7922
+ var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_USERS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
7923
+ var init_persistence = __esm({
7924
+ "src/integrations/slack/persistence.ts"() {
7925
+ "use strict";
7926
+ init_config();
7927
+ FILENAME = "slack-cache.json";
7928
+ FILE_VERSION = 1;
7929
+ SAVE_DEBOUNCE_MS = 500;
7930
+ MAX_USERS = 5e3;
7931
+ MAX_THREADS = 5e3;
7932
+ loaded = false;
7933
+ userMap = /* @__PURE__ */ new Map();
7934
+ threadMap = /* @__PURE__ */ new Map();
7935
+ dirty = false;
7936
+ saveTimer = null;
7937
+ exitHooked = false;
7938
+ }
7939
+ });
7940
+
7798
7941
  // src/integrations/slack/client.ts
7799
7942
  function readSlackConfig() {
7800
7943
  try {
@@ -7917,13 +8060,13 @@ async function fetchSlackUserName(userId) {
7917
8060
  async function resolveSlackUserName(userId) {
7918
8061
  if (!userId) return null;
7919
8062
  const now = Date.now();
7920
- const hit = userNameCache.get(userId);
8063
+ const hit = getCachedUserName(userId);
7921
8064
  if (hit && hit.expiresAt > now) return hit.name;
7922
8065
  const inflight = userInflight.get(userId);
7923
8066
  if (inflight) return inflight;
7924
8067
  const p = (async () => {
7925
8068
  const name = await fetchSlackUserName(userId);
7926
- userNameCache.set(userId, {
8069
+ setCachedUserName(userId, {
7927
8070
  name,
7928
8071
  expiresAt: now + (name ? USER_TTL_MS : USER_FAIL_TTL_MS)
7929
8072
  });
@@ -7945,11 +8088,63 @@ async function normalizeSlackMentions(text) {
7945
8088
  }
7946
8089
  return text.replace(userMentionRe, (_full, id, label) => {
7947
8090
  if (label) return `${label} <@${id}>`;
7948
- const cached = userNameCache.get(id);
8091
+ const cached = getCachedUserName(id);
7949
8092
  const name = cached?.name;
7950
8093
  return name ? `${name} <@${id}>` : `<@${id}>`;
7951
8094
  });
7952
8095
  }
8096
+ function threadCacheKey(channel, threadTs) {
8097
+ return `${channel}\u241F${threadTs}`;
8098
+ }
8099
+ async function fetchBotParticipatedInThread(channel, threadTs) {
8100
+ const token = getSlackBotToken();
8101
+ if (!token) return false;
8102
+ const self = await ensureSlackSelfIdentity();
8103
+ if (!self) return false;
8104
+ try {
8105
+ const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${encodeURIComponent(threadTs)}&limit=200`;
8106
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
8107
+ const data = await res.json().catch(() => ({}));
8108
+ if (!data?.ok) {
8109
+ console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
8110
+ return false;
8111
+ }
8112
+ const messages = Array.isArray(data.messages) ? data.messages : [];
8113
+ return messages.some(
8114
+ (m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
8115
+ );
8116
+ } catch (err) {
8117
+ console.warn(`[slack] conversations.replies error:`, err?.message || err);
8118
+ return false;
8119
+ }
8120
+ }
8121
+ async function botParticipatedInThread(channel, threadTs) {
8122
+ if (!channel || !threadTs) return false;
8123
+ const key2 = threadCacheKey(channel, threadTs);
8124
+ const now = Date.now();
8125
+ const hit = getCachedThreadOwnership(key2);
8126
+ if (hit && hit.expiresAt > now) return hit.owned;
8127
+ const inflight = threadOwnedInflight.get(key2);
8128
+ if (inflight) return inflight;
8129
+ const p = (async () => {
8130
+ const owned = await fetchBotParticipatedInThread(channel, threadTs);
8131
+ setCachedThreadOwnership(key2, {
8132
+ owned,
8133
+ expiresAt: now + (owned ? THREAD_OWNED_TTL_MS : THREAD_NEG_TTL_MS)
8134
+ });
8135
+ threadOwnedInflight.delete(key2);
8136
+ return owned;
8137
+ })();
8138
+ threadOwnedInflight.set(key2, p);
8139
+ return p;
8140
+ }
8141
+ function noteBotPostedInThread(channel, threadTs) {
8142
+ if (!channel || !threadTs) return;
8143
+ setCachedThreadOwnership(threadCacheKey(channel, threadTs), {
8144
+ owned: true,
8145
+ expiresAt: Date.now() + THREAD_OWNED_TTL_MS
8146
+ });
8147
+ }
7953
8148
  function getSlackDeniedReplyPolicy() {
7954
8149
  try {
7955
8150
  const cfg = getConfig();
@@ -7962,17 +8157,20 @@ function getSlackDeniedReplyPolicy() {
7962
8157
  return { enabled: true, template: DEFAULT_DENIED_TEMPLATE };
7963
8158
  }
7964
8159
  }
7965
- var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userNameCache, userInflight, DEFAULT_DENIED_TEMPLATE;
8160
+ var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS, threadOwnedInflight, DEFAULT_DENIED_TEMPLATE;
7966
8161
  var init_client3 = __esm({
7967
8162
  "src/integrations/slack/client.ts"() {
7968
8163
  "use strict";
7969
8164
  init_config();
8165
+ init_persistence();
7970
8166
  cachedSelf = null;
7971
8167
  selfInflight = null;
7972
8168
  USER_TTL_MS = 60 * 60 * 1e3;
7973
8169
  USER_FAIL_TTL_MS = 5 * 60 * 1e3;
7974
- userNameCache = /* @__PURE__ */ new Map();
7975
8170
  userInflight = /* @__PURE__ */ new Map();
8171
+ THREAD_OWNED_TTL_MS = 60 * 60 * 1e3;
8172
+ THREAD_NEG_TTL_MS = 5 * 60 * 1e3;
8173
+ threadOwnedInflight = /* @__PURE__ */ new Map();
7976
8174
  DEFAULT_DENIED_TEMPLATE = "Sorry, you don't have permission to use this bot. (Contact the bot owner if you think this is a mistake.)";
7977
8175
  }
7978
8176
  });
@@ -8072,6 +8270,7 @@ var init_slack = __esm({
8072
8270
  if (!result.ok) throw new Error(`slack post failed: ${result.error}`);
8073
8271
  if (r.slackChannel && r.threadTs) {
8074
8272
  markThreadOwned(r.slackChannel, r.threadTs);
8273
+ noteBotPostedInThread(r.slackChannel, r.threadTs);
8075
8274
  }
8076
8275
  },
8077
8276
  displayLabel(ref) {
@@ -8729,8 +8928,8 @@ var init_orchestrator_actions = __esm({
8729
8928
 
8730
8929
  // src/integrations/mcp/store.ts
8731
8930
  import { nanoid as nanoid6 } from "nanoid";
8732
- import { existsSync as existsSync16, readFileSync as readFileSync7 } from "fs";
8733
- import { resolve as resolve10, join as join9 } from "path";
8931
+ import { existsSync as existsSync17, readFileSync as readFileSync8 } from "fs";
8932
+ import { resolve as resolve10, join as join10 } from "path";
8734
8933
  function readServers() {
8735
8934
  try {
8736
8935
  const cfg = getConfig();
@@ -8742,12 +8941,12 @@ function readServers() {
8742
8941
  function refreshMcpServersFromDisk() {
8743
8942
  const candidates = [
8744
8943
  resolve10(process.cwd(), "sparkecoder.config.json"),
8745
- join9(ensureAppDataDirectory(), "sparkecoder.config.json")
8944
+ join10(ensureAppDataDirectory(), "sparkecoder.config.json")
8746
8945
  ];
8747
8946
  for (const path of candidates) {
8748
- if (!existsSync16(path)) continue;
8947
+ if (!existsSync17(path)) continue;
8749
8948
  try {
8750
- const raw = JSON.parse(readFileSync7(path, "utf-8"));
8949
+ const raw = JSON.parse(readFileSync8(path, "utf-8"));
8751
8950
  const servers2 = Array.isArray(raw?.mcp?.servers) ? raw.mcp.servers : [];
8752
8951
  setMcpServers(servers2);
8753
8952
  return servers2;
@@ -9360,7 +9559,7 @@ __export(recorder_exports, {
9360
9559
  import { exec as exec5 } from "child_process";
9361
9560
  import { promisify as promisify5 } from "util";
9362
9561
  import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
9363
- import { join as join10 } from "path";
9562
+ import { join as join11 } from "path";
9364
9563
  import { tmpdir } from "os";
9365
9564
  import { nanoid as nanoid7 } from "nanoid";
9366
9565
  async function checkFfmpeg() {
@@ -9417,21 +9616,21 @@ var init_recorder = __esm({
9417
9616
  */
9418
9617
  async encode() {
9419
9618
  if (this.frames.length === 0) return null;
9420
- const workDir = join10(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
9619
+ const workDir = join11(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
9421
9620
  await mkdir4(workDir, { recursive: true });
9422
9621
  try {
9423
9622
  for (let i = 0; i < this.frames.length; i++) {
9424
- const framePath = join10(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
9623
+ const framePath = join11(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
9425
9624
  await writeFile5(framePath, this.frames[i].data);
9426
9625
  }
9427
9626
  const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
9428
9627
  const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
9429
9628
  const clampedFps = Math.max(1, Math.min(fps, 30));
9430
- const outputPath = join10(workDir, `recording_${this.sessionId}.mp4`);
9629
+ const outputPath = join11(workDir, `recording_${this.sessionId}.mp4`);
9431
9630
  const hasFfmpeg = await checkFfmpeg();
9432
9631
  if (hasFfmpeg) {
9433
9632
  await execAsync5(
9434
- `ffmpeg -y -framerate ${clampedFps} -i "${join10(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
9633
+ `ffmpeg -y -framerate ${clampedFps} -i "${join11(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
9435
9634
  { timeout: 12e4 }
9436
9635
  );
9437
9636
  } else {
@@ -9443,7 +9642,7 @@ var init_recorder = __esm({
9443
9642
  const files = await readdir5(workDir);
9444
9643
  for (const f of files) {
9445
9644
  if (f.startsWith("frame_")) {
9446
- await unlink2(join10(workDir, f)).catch(() => {
9645
+ await unlink2(join11(workDir, f)).catch(() => {
9447
9646
  });
9448
9647
  }
9449
9648
  }
@@ -9710,7 +9909,8 @@ ${personality.trim()}`;
9710
9909
  }
9711
9910
  const messages = await this.context.getMessages();
9712
9911
  const tools = options.onToolProgress ? await this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
9713
- const wrappedTools = this.wrapToolsWithApproval(options, tools);
9912
+ const approvalWrapped = this.wrapToolsWithApproval(options, tools);
9913
+ const wrappedTools = wrapToolsNeverThrow(approvalWrapped);
9714
9914
  const useAnthropic = isAnthropicModel(this.session.model);
9715
9915
  const stream = streamText2({
9716
9916
  model: resolveModel(this.session.model),
@@ -9724,6 +9924,17 @@ ${personality.trim()}`;
9724
9924
  providerOptions: useAnthropic ? {
9725
9925
  anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
9726
9926
  } : void 0,
9927
+ // Run repairToolPairing before EVERY step's model call, not just the
9928
+ // first one. The AI SDK's multi-step loop can otherwise feed the model
9929
+ // a prompt containing an orphan tool-call (e.g. when a previous step's
9930
+ // tool result was lost, dropped during compaction, or the stream was
9931
+ // aborted mid-tool). Repairing in `prepareStep` guarantees no orphan
9932
+ // ever reaches the model and we never hit AI_MissingToolResultsError.
9933
+ prepareStep: async ({ messages: stepMessages }) => {
9934
+ const repaired = repairToolPairing(stepMessages);
9935
+ if (repaired === stepMessages) return {};
9936
+ return { messages: repaired };
9937
+ },
9727
9938
  onStepFinish: async (step) => {
9728
9939
  options.onStepFinish?.(step);
9729
9940
  },
@@ -9759,7 +9970,7 @@ ${personality.trim()}`;
9759
9970
  });
9760
9971
  const messages = await this.context.getMessages();
9761
9972
  const tools = options.onToolProgress ? await this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
9762
- const wrappedTools = this.wrapToolsWithApproval(options, tools);
9973
+ const wrappedTools = wrapToolsNeverThrow(this.wrapToolsWithApproval(options, tools));
9763
9974
  const useAnthropic = isAnthropicModel(this.session.model);
9764
9975
  const result = await generateText3({
9765
9976
  model: resolveModel(this.session.model),
@@ -9770,7 +9981,13 @@ ${personality.trim()}`;
9770
9981
  // Enable extended thinking/reasoning for models that support it
9771
9982
  providerOptions: useAnthropic ? {
9772
9983
  anthropic: getAnthropicProviderOptions(this.session.model)
9773
- } : void 0
9984
+ } : void 0,
9985
+ // Repair tool pairing before every step (see `stream()` for full rationale).
9986
+ prepareStep: async ({ messages: stepMessages }) => {
9987
+ const repaired = repairToolPairing(stepMessages);
9988
+ if (repaired === stepMessages) return {};
9989
+ return { messages: repaired };
9990
+ }
9774
9991
  });
9775
9992
  const responseMessages = result.response.messages;
9776
9993
  this.context.addResponseMessages(responseMessages);
@@ -9947,12 +10164,19 @@ ${p.text}` : p.text;
9947
10164
  model: resolveModel(this.session.model),
9948
10165
  system: systemPrompt,
9949
10166
  messages,
9950
- tools: taskTools,
10167
+ tools: wrapToolsNeverThrow(taskTools),
9951
10168
  stopWhen: stepCountIs2(500),
9952
10169
  abortSignal: combinedAbort,
9953
10170
  providerOptions: useAnthropic ? {
9954
10171
  anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
9955
10172
  } : void 0,
10173
+ // See the matching note in `stream()` — repair tool pairing before
10174
+ // every step so we never feed the model an orphan tool-call.
10175
+ prepareStep: async ({ messages: stepMessages }) => {
10176
+ const repaired = repairToolPairing(stepMessages);
10177
+ if (repaired === stepMessages) return {};
10178
+ return { messages: repaired };
10179
+ },
9956
10180
  onStepFinish: async (step) => {
9957
10181
  options.onStepFinish?.(step);
9958
10182
  fireWebhook("task.step_finished", { iteration, text: step.text });
@@ -10186,11 +10410,11 @@ ${p.text}` : p.text;
10186
10410
  const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
10187
10411
  if (!isRemoteConfigured2()) return [];
10188
10412
  const { readFile: readFile13 } = await import("fs/promises");
10189
- const { join: join17, basename: basename7 } = await import("path");
10413
+ const { join: join18, basename: basename7 } = await import("path");
10190
10414
  const urls = [];
10191
10415
  for (const filePath of filePaths) {
10192
10416
  try {
10193
- const fullPath = filePath.startsWith("/") ? filePath : join17(this.session.workingDirectory, filePath);
10417
+ const fullPath = filePath.startsWith("/") ? filePath : join18(this.session.workingDirectory, filePath);
10194
10418
  const fileName = basename7(fullPath);
10195
10419
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
10196
10420
  const mimeMap = {
@@ -10395,19 +10619,19 @@ var init_session_lock = __esm({
10395
10619
  });
10396
10620
 
10397
10621
  // src/orchestrator/webhook-events.ts
10398
- import { existsSync as existsSync17, readFileSync as readFileSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync6 } from "fs";
10399
- import { dirname as dirname6, join as join11 } from "path";
10622
+ import { existsSync as existsSync18, readFileSync as readFileSync9, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
10623
+ import { dirname as dirname7, join as join12 } from "path";
10400
10624
  import { nanoid as nanoid9 } from "nanoid";
10401
10625
  function logFilePath() {
10402
- return join11(getAppDataDirectory(), "webhook-events.jsonl");
10626
+ return join12(getAppDataDirectory(), "webhook-events.jsonl");
10403
10627
  }
10404
10628
  function ensureLoaded() {
10405
10629
  if (cache !== null) return cache;
10406
10630
  cache = [];
10407
10631
  try {
10408
10632
  const p = logFilePath();
10409
- if (!existsSync17(p)) return cache;
10410
- const lines = readFileSync8(p, "utf-8").split("\n").filter(Boolean);
10633
+ if (!existsSync18(p)) return cache;
10634
+ const lines = readFileSync9(p, "utf-8").split("\n").filter(Boolean);
10411
10635
  for (const line of lines) {
10412
10636
  try {
10413
10637
  cache.push(JSON.parse(line));
@@ -10417,7 +10641,7 @@ function ensureLoaded() {
10417
10641
  if (cache.length > MAX_EVENTS) {
10418
10642
  cache = cache.slice(-MAX_EVENTS);
10419
10643
  try {
10420
- writeFileSync3(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
10644
+ writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
10421
10645
  } catch {
10422
10646
  }
10423
10647
  }
@@ -10431,7 +10655,7 @@ function appendEvent(ev) {
10431
10655
  if (list.length > MAX_EVENTS) list.shift();
10432
10656
  try {
10433
10657
  const p = logFilePath();
10434
- mkdirSync6(dirname6(p), { recursive: true });
10658
+ mkdirSync7(dirname7(p), { recursive: true });
10435
10659
  appendFileSync3(p, JSON.stringify(ev) + "\n");
10436
10660
  } catch {
10437
10661
  }
@@ -10462,8 +10686,8 @@ function updateEvent(id, patch) {
10462
10686
  list[i] = { ...list[i], ...patch };
10463
10687
  try {
10464
10688
  const p = logFilePath();
10465
- mkdirSync6(dirname6(p), { recursive: true });
10466
- writeFileSync3(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
10689
+ mkdirSync7(dirname7(p), { recursive: true });
10690
+ writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
10467
10691
  } catch {
10468
10692
  }
10469
10693
  }
@@ -10495,7 +10719,7 @@ function listEvents(filter = {}) {
10495
10719
  function clearAllEvents() {
10496
10720
  cache = [];
10497
10721
  try {
10498
- writeFileSync3(logFilePath(), "");
10722
+ writeFileSync4(logFilePath(), "");
10499
10723
  } catch {
10500
10724
  }
10501
10725
  }
@@ -10766,8 +10990,8 @@ import { Hono as Hono10 } from "hono";
10766
10990
  import { serve } from "@hono/node-server";
10767
10991
  import { cors } from "hono/cors";
10768
10992
  import { logger } from "hono/logger";
10769
- import { existsSync as existsSync21, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
10770
- import { resolve as resolve12, dirname as dirname9, join as join16 } from "path";
10993
+ import { existsSync as existsSync22, mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "fs";
10994
+ import { resolve as resolve12, dirname as dirname10, join as join17 } from "path";
10771
10995
  import { spawn as spawn2 } from "child_process";
10772
10996
  import { createServer as createNetServer } from "net";
10773
10997
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -10781,9 +11005,9 @@ init_checkpoints();
10781
11005
  import { Hono } from "hono";
10782
11006
  import { zValidator } from "@hono/zod-validator";
10783
11007
  import { z as z16 } from "zod";
10784
- import { existsSync as existsSync18, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
11008
+ import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
10785
11009
  import { readdir as readdir6 } from "fs/promises";
10786
- import { join as join12, basename as basename5, extname as extname8, relative as relative9 } from "path";
11010
+ import { join as join13, basename as basename5, extname as extname8, relative as relative9 } from "path";
10787
11011
  import { nanoid as nanoid10 } from "nanoid";
10788
11012
 
10789
11013
  // src/tasks/agent-status.ts
@@ -11421,12 +11645,12 @@ sessions2.get("/:id/diff/:filePath", async (c) => {
11421
11645
  });
11422
11646
  function getAttachmentsDir(sessionId) {
11423
11647
  const appDataDir = getAppDataDirectory();
11424
- return join12(appDataDir, "attachments", sessionId);
11648
+ return join13(appDataDir, "attachments", sessionId);
11425
11649
  }
11426
11650
  function ensureAttachmentsDir(sessionId) {
11427
11651
  const dir = getAttachmentsDir(sessionId);
11428
- if (!existsSync18(dir)) {
11429
- mkdirSync7(dir, { recursive: true });
11652
+ if (!existsSync19(dir)) {
11653
+ mkdirSync8(dir, { recursive: true });
11430
11654
  }
11431
11655
  return dir;
11432
11656
  }
@@ -11437,12 +11661,12 @@ sessions2.get("/:id/attachments", async (c) => {
11437
11661
  return c.json({ error: "Session not found" }, 404);
11438
11662
  }
11439
11663
  const dir = getAttachmentsDir(sessionId);
11440
- if (!existsSync18(dir)) {
11664
+ if (!existsSync19(dir)) {
11441
11665
  return c.json({ sessionId, attachments: [], count: 0 });
11442
11666
  }
11443
11667
  const files = readdirSync3(dir);
11444
11668
  const attachments = files.map((filename) => {
11445
- const filePath = join12(dir, filename);
11669
+ const filePath = join13(dir, filename);
11446
11670
  const stats = statSync2(filePath);
11447
11671
  return {
11448
11672
  id: filename.split("_")[0],
@@ -11477,9 +11701,9 @@ sessions2.post("/:id/attachments", async (c) => {
11477
11701
  const id = nanoid10(10);
11478
11702
  const ext = extname8(file.name) || "";
11479
11703
  const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
11480
- const filePath = join12(dir, safeFilename);
11704
+ const filePath = join13(dir, safeFilename);
11481
11705
  const arrayBuffer = await file.arrayBuffer();
11482
- writeFileSync4(filePath, Buffer.from(arrayBuffer));
11706
+ writeFileSync5(filePath, Buffer.from(arrayBuffer));
11483
11707
  return c.json({
11484
11708
  id,
11485
11709
  filename: file.name,
@@ -11503,13 +11727,13 @@ sessions2.post("/:id/attachments", async (c) => {
11503
11727
  const id = nanoid10(10);
11504
11728
  const ext = extname8(body.filename) || "";
11505
11729
  const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
11506
- const filePath = join12(dir, safeFilename);
11730
+ const filePath = join13(dir, safeFilename);
11507
11731
  let base64Data = body.data;
11508
11732
  if (base64Data.includes(",")) {
11509
11733
  base64Data = base64Data.split(",")[1];
11510
11734
  }
11511
11735
  const buffer = Buffer.from(base64Data, "base64");
11512
- writeFileSync4(filePath, buffer);
11736
+ writeFileSync5(filePath, buffer);
11513
11737
  return c.json({
11514
11738
  id,
11515
11739
  filename: body.filename,
@@ -11532,7 +11756,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
11532
11756
  return c.json({ error: "Session not found" }, 404);
11533
11757
  }
11534
11758
  const dir = getAttachmentsDir(sessionId);
11535
- if (!existsSync18(dir)) {
11759
+ if (!existsSync19(dir)) {
11536
11760
  return c.json({ error: "Attachment not found" }, 404);
11537
11761
  }
11538
11762
  const files = readdirSync3(dir);
@@ -11540,7 +11764,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
11540
11764
  if (!file) {
11541
11765
  return c.json({ error: "Attachment not found" }, 404);
11542
11766
  }
11543
- const filePath = join12(dir, file);
11767
+ const filePath = join13(dir, file);
11544
11768
  unlinkSync2(filePath);
11545
11769
  return c.json({ success: true, id: attachmentId });
11546
11770
  });
@@ -11623,7 +11847,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
11623
11847
  const entries = await readdir6(currentDir, { withFileTypes: true });
11624
11848
  for (const entry2 of entries) {
11625
11849
  if (results.length >= limit * 2) break;
11626
- const fullPath = join12(currentDir, entry2.name);
11850
+ const fullPath = join13(currentDir, entry2.name);
11627
11851
  const relativePath = relative9(baseDir, fullPath);
11628
11852
  if (entry2.isDirectory() && IGNORED_DIRECTORIES.has(entry2.name)) {
11629
11853
  continue;
@@ -11671,7 +11895,7 @@ sessions2.get(
11671
11895
  return c.json({ error: "Session not found" }, 404);
11672
11896
  }
11673
11897
  const workingDirectory = session.workingDirectory;
11674
- if (!existsSync18(workingDirectory)) {
11898
+ if (!existsSync19(workingDirectory)) {
11675
11899
  return c.json({
11676
11900
  sessionId,
11677
11901
  workingDirectory,
@@ -11779,14 +12003,101 @@ sessions2.get("/:id/browser-recording", async (c) => {
11779
12003
 
11780
12004
  // src/server/routes/agents.ts
11781
12005
  init_db();
11782
- init_agent();
11783
- init_session_lock();
11784
- init_config();
11785
12006
  import { Hono as Hono2 } from "hono";
11786
12007
  import { zValidator as zValidator2 } from "@hono/zod-validator";
11787
12008
  import { z as z17 } from "zod";
11788
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
11789
- import { join as join13 } from "path";
12009
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
12010
+ import { join as join14 } from "path";
12011
+
12012
+ // src/agent/missing-tool-recovery.ts
12013
+ init_db();
12014
+ function extractMissingToolCallIds(error) {
12015
+ if (!error) return [];
12016
+ const e = error;
12017
+ if (Array.isArray(e.toolCallIds) && e.toolCallIds.every((x) => typeof x === "string")) {
12018
+ return e.toolCallIds;
12019
+ }
12020
+ const msg = typeof e.message === "string" ? e.message : "";
12021
+ const ids = /* @__PURE__ */ new Set();
12022
+ for (const m of msg.matchAll(/(?:tool call|tool calls)\s+([a-zA-Z0-9_\-, ]+?)(?:\.|$)/g)) {
12023
+ for (const id of m[1].split(/\s*,\s*/)) {
12024
+ const trimmed = id.trim();
12025
+ if (trimmed) ids.add(trimmed);
12026
+ }
12027
+ }
12028
+ return [...ids];
12029
+ }
12030
+ function isMissingToolResultsError(error) {
12031
+ if (!error) return false;
12032
+ const e = error;
12033
+ if (e.name === "AI_MissingToolResultsError") return true;
12034
+ if (Array.isArray(e.toolCallIds)) return true;
12035
+ return typeof e.message === "string" && /tool result.*is missing for tool call/i.test(e.message);
12036
+ }
12037
+ async function recoverFromMissingToolResults(sessionId, error) {
12038
+ const toolCallIds = extractMissingToolCallIds(error);
12039
+ if (toolCallIds.length === 0) return null;
12040
+ let history = [];
12041
+ try {
12042
+ history = await messageQueries.getModelMessages(sessionId);
12043
+ } catch (err) {
12044
+ console.warn("[missing-tool-recovery] could not load messages:", err?.message || err);
12045
+ }
12046
+ const toolNameByCallId = /* @__PURE__ */ new Map();
12047
+ const existingResultIds = /* @__PURE__ */ new Set();
12048
+ for (const msg of history) {
12049
+ if (!Array.isArray(msg.content)) continue;
12050
+ for (const part of msg.content) {
12051
+ if (part?.type === "tool-call" && typeof part.toolCallId === "string") {
12052
+ if (typeof part.toolName === "string") toolNameByCallId.set(part.toolCallId, part.toolName);
12053
+ }
12054
+ if (part?.type === "tool-result" && typeof part.toolCallId === "string") {
12055
+ existingResultIds.add(part.toolCallId);
12056
+ }
12057
+ }
12058
+ }
12059
+ const resolved = toolCallIds.map((id) => ({
12060
+ toolCallId: id,
12061
+ toolName: toolNameByCallId.get(id) ?? "unknown",
12062
+ foundInAssistantMessage: toolNameByCallId.has(id)
12063
+ }));
12064
+ const stillOrphaned = toolCallIds.filter((id) => !existingResultIds.has(id));
12065
+ let syntheticToolMessageSaved = false;
12066
+ if (stillOrphaned.length > 0) {
12067
+ const syntheticParts = stillOrphaned.map((id) => ({
12068
+ type: "tool-result",
12069
+ toolCallId: id,
12070
+ toolName: toolNameByCallId.get(id) ?? "unknown",
12071
+ output: {
12072
+ type: "text",
12073
+ value: "[Auto-recovered: the original tool execution never produced a result (crash, timeout, or message stripped during context compaction). This synthetic placeholder lets the conversation continue.]"
12074
+ }
12075
+ }));
12076
+ try {
12077
+ await messageQueries.create(sessionId, {
12078
+ role: "tool",
12079
+ content: syntheticParts
12080
+ });
12081
+ syntheticToolMessageSaved = true;
12082
+ } catch (err) {
12083
+ console.error("[missing-tool-recovery] failed to persist synthetic tool message:", err?.message || err);
12084
+ }
12085
+ }
12086
+ const lastFewMessageRoles = history.slice(-6).map((m) => m.role);
12087
+ return {
12088
+ kind: "missing_tool_results",
12089
+ toolCallIds,
12090
+ resolved,
12091
+ syntheticToolMessageSaved,
12092
+ lastFewMessageRoles,
12093
+ hint: syntheticToolMessageSaved ? "Synthetic tool-result(s) saved. Re-send your message to continue." : "Could not auto-recover; please reset the session or revert to the previous checkpoint."
12094
+ };
12095
+ }
12096
+
12097
+ // src/server/routes/agents.ts
12098
+ init_agent();
12099
+ init_session_lock();
12100
+ init_config();
11790
12101
 
11791
12102
  // src/server/resumable-stream.ts
11792
12103
  import { createResumableStreamContext } from "resumable-stream/generic";
@@ -11993,12 +12304,12 @@ var rejectSchema = z17.object({
11993
12304
  var streamAbortControllers = /* @__PURE__ */ new Map();
11994
12305
  function getAttachmentsDirectory(sessionId) {
11995
12306
  const appDataDir = getAppDataDirectory();
11996
- return join13(appDataDir, "attachments", sessionId);
12307
+ return join14(appDataDir, "attachments", sessionId);
11997
12308
  }
11998
12309
  async function saveAttachmentToDisk(sessionId, attachment, index) {
11999
12310
  const attachmentsDir = getAttachmentsDirectory(sessionId);
12000
- if (!existsSync19(attachmentsDir)) {
12001
- mkdirSync8(attachmentsDir, { recursive: true });
12311
+ if (!existsSync20(attachmentsDir)) {
12312
+ mkdirSync9(attachmentsDir, { recursive: true });
12002
12313
  }
12003
12314
  let filename = attachment.filename;
12004
12315
  if (!filename) {
@@ -12016,8 +12327,8 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
12016
12327
  attachment.mediaType = resized.mediaType;
12017
12328
  attachment.data = buffer.toString("base64");
12018
12329
  }
12019
- const filePath = join13(attachmentsDir, filename);
12020
- writeFileSync5(filePath, buffer);
12330
+ const filePath = join14(attachmentsDir, filename);
12331
+ writeFileSync6(filePath, buffer);
12021
12332
  return filePath;
12022
12333
  }
12023
12334
  function stripDataUrlPrefix2(data) {
@@ -12354,7 +12665,20 @@ ${prompt}` });
12354
12665
  await writeSSE(JSON.stringify({ type: "abort" }));
12355
12666
  } else {
12356
12667
  console.error("Agent error:", error);
12357
- await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
12668
+ let debugPayload = void 0;
12669
+ if (isMissingToolResultsError(error)) {
12670
+ try {
12671
+ const recovery = await recoverFromMissingToolResults(sessionId, error);
12672
+ if (recovery) debugPayload = recovery;
12673
+ } catch (recErr) {
12674
+ console.error("[missing-tool-recovery] failed:", recErr?.message || recErr);
12675
+ }
12676
+ }
12677
+ await writeSSE(JSON.stringify({
12678
+ type: "error",
12679
+ errorText: error.message,
12680
+ ...debugPayload ? { debug: debugPayload } : {}
12681
+ }));
12358
12682
  try {
12359
12683
  await activeStreamQueries.markError(streamId);
12360
12684
  } catch {
@@ -12894,7 +13218,20 @@ agents.post(
12894
13218
  await writeSSE(JSON.stringify({ type: "abort" }));
12895
13219
  } else {
12896
13220
  console.error("Agent error:", error);
12897
- await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
13221
+ let debugPayload = void 0;
13222
+ if (isMissingToolResultsError(error)) {
13223
+ try {
13224
+ const recovery = await recoverFromMissingToolResults(session.id, error);
13225
+ if (recovery) debugPayload = recovery;
13226
+ } catch (recErr) {
13227
+ console.error("[missing-tool-recovery] failed:", recErr?.message || recErr);
13228
+ }
13229
+ }
13230
+ await writeSSE(JSON.stringify({
13231
+ type: "error",
13232
+ errorText: error.message,
13233
+ ...debugPayload ? { debug: debugPayload } : {}
13234
+ }));
12898
13235
  await activeStreamQueries.markError(streamId);
12899
13236
  }
12900
13237
  } finally {
@@ -12977,26 +13314,26 @@ init_config();
12977
13314
  import { Hono as Hono3 } from "hono";
12978
13315
  import { zValidator as zValidator3 } from "@hono/zod-validator";
12979
13316
  import { z as z18 } from "zod";
12980
- import { readFileSync as readFileSync9 } from "fs";
13317
+ import { readFileSync as readFileSync10 } from "fs";
12981
13318
  import { fileURLToPath as fileURLToPath3 } from "url";
12982
- import { dirname as dirname7, join as join14 } from "path";
13319
+ import { dirname as dirname8, join as join15 } from "path";
12983
13320
  var __filename = fileURLToPath3(import.meta.url);
12984
- var __dirname = dirname7(__filename);
13321
+ var __dirname = dirname8(__filename);
12985
13322
  var possiblePaths = [
12986
- join14(__dirname, "../package.json"),
13323
+ join15(__dirname, "../package.json"),
12987
13324
  // From dist/server -> dist/../package.json
12988
- join14(__dirname, "../../package.json"),
13325
+ join15(__dirname, "../../package.json"),
12989
13326
  // From dist/server (if nested differently)
12990
- join14(__dirname, "../../../package.json"),
13327
+ join15(__dirname, "../../../package.json"),
12991
13328
  // From src/server/routes (development)
12992
- join14(process.cwd(), "package.json")
13329
+ join15(process.cwd(), "package.json")
12993
13330
  // From current working directory
12994
13331
  ];
12995
13332
  var currentVersion = "0.0.0";
12996
13333
  var packageName = "sparkecoder";
12997
13334
  for (const packageJsonPath of possiblePaths) {
12998
13335
  try {
12999
- const packageJson = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
13336
+ const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
13000
13337
  if (packageJson.name === "sparkecoder") {
13001
13338
  currentVersion = packageJson.version || "0.0.0";
13002
13339
  packageName = packageJson.name || "sparkecoder";
@@ -13814,12 +14151,13 @@ slack.post("/events", async (c) => {
13814
14151
  if (inbound) {
13815
14152
  const isThreadReply = ev.type === "message" && ev.channel_type !== "im" && typeof ev.thread_ts === "string" && ev.thread_ts !== ev.ts;
13816
14153
  if (isThreadReply) {
13817
- const ours = isThreadOwned(ev.channel, ev.thread_ts) || await threadBelongsToUs(ev.channel, ev.thread_ts);
14154
+ const ours = isThreadOwned(ev.channel, ev.thread_ts) || await botParticipatedInThread(ev.channel, ev.thread_ts);
13818
14155
  if (!ours) {
13819
14156
  console.log(`[slack] dropping thread reply in unknown thread: channel=${ev.channel} thread=${ev.thread_ts}`);
13820
14157
  updateEvent(auditId, { status: "dropped", dropReason: "thread_not_owned" });
13821
14158
  return c.json({ ok: true });
13822
14159
  }
14160
+ markThreadOwned(ev.channel, ev.thread_ts);
13823
14161
  }
13824
14162
  if (ev.type === "app_mention" && ev.channel && (ev.thread_ts || ev.ts)) {
13825
14163
  markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
@@ -13858,18 +14196,6 @@ slack.post("/events", async (c) => {
13858
14196
  }
13859
14197
  return c.json({ ok: true });
13860
14198
  });
13861
- async function threadBelongsToUs(channel, threadTs) {
13862
- try {
13863
- const sessions3 = await sessionQueries.list(500, 0);
13864
- return sessions3.some((s) => {
13865
- const slack2 = s.config?.slack;
13866
- return slack2?.channel === channel && slack2?.threadTs === threadTs;
13867
- });
13868
- } catch (err) {
13869
- console.warn("[slack] threadBelongsToUs lookup failed:", err?.message ?? err);
13870
- return false;
13871
- }
13872
- }
13873
14199
  async function findOrCreateOrchestratorId() {
13874
14200
  try {
13875
14201
  const all = await sessionQueries.list(500, 0);
@@ -14258,9 +14584,9 @@ init_skills();
14258
14584
  import { Hono as Hono9 } from "hono";
14259
14585
  import { zValidator as zValidator7 } from "@hono/zod-validator";
14260
14586
  import { z as z22 } from "zod";
14261
- import { existsSync as existsSync20, statSync as statSync3 } from "fs";
14587
+ import { existsSync as existsSync21, statSync as statSync3 } from "fs";
14262
14588
  import { readFile as readFile12, writeFile as writeFile6, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
14263
- import { resolve as resolve11, join as join15, basename as basename6, dirname as dirname8, extname as extname9 } from "path";
14589
+ import { resolve as resolve11, join as join16, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
14264
14590
  var skills = new Hono9();
14265
14591
  function encodeId(filePath) {
14266
14592
  return Buffer.from(filePath, "utf-8").toString("base64url");
@@ -14319,13 +14645,13 @@ function pathToLabel(path) {
14319
14645
  if (path.includes("/.sparkecoder/skills")) return ".sparkecoder/skills";
14320
14646
  if (path.includes("/.cursor/rules")) return ".cursor/rules";
14321
14647
  if (path.includes("/.claude/skills")) return ".claude/skills";
14322
- return basename6(dirname8(path)) + "/" + basename6(path);
14648
+ return basename6(dirname9(path)) + "/" + basename6(path);
14323
14649
  }
14324
14650
  skills.get("/", async (c) => {
14325
14651
  const dirs = listAllDirectories();
14326
14652
  const allSkills = [];
14327
14653
  for (const dir of dirs) {
14328
- if (!existsSync20(dir.path)) continue;
14654
+ if (!existsSync21(dir.path)) continue;
14329
14655
  try {
14330
14656
  const list = await loadSkillsFromDirectory(dir.path, {
14331
14657
  priority: dir.priority,
@@ -14362,7 +14688,7 @@ skills.get("/", async (c) => {
14362
14688
  label: d.label,
14363
14689
  source: d.source,
14364
14690
  alwaysApply: d.alwaysApply,
14365
- exists: existsSync20(d.path),
14691
+ exists: existsSync21(d.path),
14366
14692
  writable: isWritable(d.path)
14367
14693
  })),
14368
14694
  skills: allSkills
@@ -14370,7 +14696,7 @@ skills.get("/", async (c) => {
14370
14696
  });
14371
14697
  function isWritable(dir) {
14372
14698
  try {
14373
- if (!existsSync20(dir)) return false;
14699
+ if (!existsSync21(dir)) return false;
14374
14700
  if (dir.includes("/skills/default")) return false;
14375
14701
  return true;
14376
14702
  } catch {
@@ -14379,7 +14705,7 @@ function isWritable(dir) {
14379
14705
  }
14380
14706
  skills.get("/:id", async (c) => {
14381
14707
  const filePath = decodeId(c.req.param("id"));
14382
- if (!filePath || !existsSync20(filePath)) {
14708
+ if (!filePath || !existsSync21(filePath)) {
14383
14709
  return c.json({ error: "skill not found" }, 404);
14384
14710
  }
14385
14711
  const content = await readFile12(filePath, "utf-8");
@@ -14408,8 +14734,8 @@ skills.post(
14408
14734
  const safeName = basename6(fileName).replace(/[^A-Za-z0-9._-]/g, "-");
14409
14735
  const ext = extname9(safeName).toLowerCase();
14410
14736
  const finalName = ext === ".md" || ext === ".mdc" ? safeName : `${safeName}.md`;
14411
- const filePath = join15(targetDir, finalName);
14412
- if (existsSync20(filePath)) {
14737
+ const filePath = join16(targetDir, finalName);
14738
+ if (existsSync21(filePath)) {
14413
14739
  return c.json({ error: `file already exists: ${finalName}` }, 409);
14414
14740
  }
14415
14741
  try {
@@ -14426,7 +14752,7 @@ skills.put(
14426
14752
  zValidator7("json", z22.object({ content: z22.string() })),
14427
14753
  async (c) => {
14428
14754
  const filePath = decodeId(c.req.param("id"));
14429
- if (!filePath || !existsSync20(filePath)) return c.json({ error: "skill not found" }, 404);
14755
+ if (!filePath || !existsSync21(filePath)) return c.json({ error: "skill not found" }, 404);
14430
14756
  if (filePath.includes("/skills/default")) {
14431
14757
  return c.json({ error: "built-in skills are read-only" }, 400);
14432
14758
  }
@@ -14436,7 +14762,7 @@ skills.put(
14436
14762
  );
14437
14763
  skills.delete("/:id", async (c) => {
14438
14764
  const filePath = decodeId(c.req.param("id"));
14439
- if (!filePath || !existsSync20(filePath)) return c.json({ error: "skill not found" }, 404);
14765
+ if (!filePath || !existsSync21(filePath)) return c.json({ error: "skill not found" }, 404);
14440
14766
  if (filePath.includes("/skills/default")) {
14441
14767
  return c.json({ error: "built-in skills are read-only" }, 400);
14442
14768
  }
@@ -14456,7 +14782,7 @@ skills.post(
14456
14782
  }
14457
14783
  const next = [...current, abs];
14458
14784
  setSkillsAdditionalDirectories(next);
14459
- return c.json({ ok: true, path: abs, exists: existsSync20(abs) }, 201);
14785
+ return c.json({ ok: true, path: abs, exists: existsSync21(abs) }, 201);
14460
14786
  }
14461
14787
  );
14462
14788
  skills.delete("/directories", (c) => {
@@ -14630,13 +14956,13 @@ var DEFAULT_WEB_PORT = 6969;
14630
14956
  var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
14631
14957
  function getWebDirectory() {
14632
14958
  try {
14633
- const currentDir = dirname9(fileURLToPath4(import.meta.url));
14959
+ const currentDir = dirname10(fileURLToPath4(import.meta.url));
14634
14960
  const webDir = resolve12(currentDir, "..", "web");
14635
- if (existsSync21(webDir) && existsSync21(join16(webDir, "package.json"))) {
14961
+ if (existsSync22(webDir) && existsSync22(join17(webDir, "package.json"))) {
14636
14962
  return webDir;
14637
14963
  }
14638
14964
  const altWebDir = resolve12(currentDir, "..", "..", "web");
14639
- if (existsSync21(altWebDir) && existsSync21(join16(altWebDir, "package.json"))) {
14965
+ if (existsSync22(altWebDir) && existsSync22(join17(altWebDir, "package.json"))) {
14640
14966
  return altWebDir;
14641
14967
  }
14642
14968
  return null;
@@ -14694,23 +15020,23 @@ async function findWebPort(preferredPort) {
14694
15020
  return { port: preferredPort, alreadyRunning: false };
14695
15021
  }
14696
15022
  function hasProductionBuild(webDir) {
14697
- const buildIdPath = join16(webDir, ".next", "BUILD_ID");
14698
- return existsSync21(buildIdPath);
15023
+ const buildIdPath = join17(webDir, ".next", "BUILD_ID");
15024
+ return existsSync22(buildIdPath);
14699
15025
  }
14700
15026
  function hasSourceFiles(webDir) {
14701
- const appDir = join16(webDir, "src", "app");
14702
- const pagesDir = join16(webDir, "src", "pages");
14703
- const rootAppDir = join16(webDir, "app");
14704
- const rootPagesDir = join16(webDir, "pages");
14705
- return existsSync21(appDir) || existsSync21(pagesDir) || existsSync21(rootAppDir) || existsSync21(rootPagesDir);
15027
+ const appDir = join17(webDir, "src", "app");
15028
+ const pagesDir = join17(webDir, "src", "pages");
15029
+ const rootAppDir = join17(webDir, "app");
15030
+ const rootPagesDir = join17(webDir, "pages");
15031
+ return existsSync22(appDir) || existsSync22(pagesDir) || existsSync22(rootAppDir) || existsSync22(rootPagesDir);
14706
15032
  }
14707
15033
  function getStandaloneServerPath(webDir) {
14708
15034
  const possiblePaths2 = [
14709
- join16(webDir, ".next", "standalone", "server.js"),
14710
- join16(webDir, ".next", "standalone", "web", "server.js")
15035
+ join17(webDir, ".next", "standalone", "server.js"),
15036
+ join17(webDir, ".next", "standalone", "web", "server.js")
14711
15037
  ];
14712
15038
  for (const serverPath of possiblePaths2) {
14713
- if (existsSync21(serverPath)) {
15039
+ if (existsSync22(serverPath)) {
14714
15040
  return serverPath;
14715
15041
  }
14716
15042
  }
@@ -14750,15 +15076,15 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
14750
15076
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
14751
15077
  return { process: null, port: actualPort };
14752
15078
  }
14753
- const usePnpm = existsSync21(join16(webDir, "pnpm-lock.yaml"));
14754
- const useNpm = !usePnpm && existsSync21(join16(webDir, "package-lock.json"));
15079
+ const usePnpm = existsSync22(join17(webDir, "pnpm-lock.yaml"));
15080
+ const useNpm = !usePnpm && existsSync22(join17(webDir, "package-lock.json"));
14755
15081
  const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
14756
15082
  const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv } = process.env;
14757
15083
  const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
14758
15084
  const runtimeConfig = { apiBaseUrl: apiUrl };
14759
- const runtimeConfigPath = join16(webDir, "runtime-config.json");
15085
+ const runtimeConfigPath = join17(webDir, "runtime-config.json");
14760
15086
  try {
14761
- writeFileSync6(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
15087
+ writeFileSync7(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
14762
15088
  if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
14763
15089
  } catch (err) {
14764
15090
  if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
@@ -14778,7 +15104,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
14778
15104
  if (standaloneServerPath) {
14779
15105
  command = "node";
14780
15106
  args = ["server.js"];
14781
- cwd = dirname9(standaloneServerPath);
15107
+ cwd = dirname10(standaloneServerPath);
14782
15108
  webEnv.PORT = String(actualPort);
14783
15109
  webEnv.HOSTNAME = "0.0.0.0";
14784
15110
  if (!quiet) console.log(" \u{1F4E6} Starting Web UI from standalone build...");
@@ -14972,8 +15298,8 @@ async function startServer(options = {}) {
14972
15298
  if (options.workingDirectory) {
14973
15299
  config.resolvedWorkingDirectory = options.workingDirectory;
14974
15300
  }
14975
- if (!existsSync21(config.resolvedWorkingDirectory)) {
14976
- mkdirSync9(config.resolvedWorkingDirectory, { recursive: true });
15301
+ if (!existsSync22(config.resolvedWorkingDirectory)) {
15302
+ mkdirSync10(config.resolvedWorkingDirectory, { recursive: true });
14977
15303
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
14978
15304
  }
14979
15305
  if (!config.resolvedRemoteServer.url) {