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
package/dist/index.js CHANGED
@@ -2346,10 +2346,10 @@ async function resizeImageIfNeeded(buffer, mediaType) {
2346
2346
  const willConvertToJpeg = isPng && (needsShrink || buffer.length > 2 * 1024 * 1024);
2347
2347
  const outputMediaType = willConvertToJpeg || !isPng ? "image/jpeg" : "image/png";
2348
2348
  const ext = outputMediaType === "image/png" ? ".png" : ".jpg";
2349
- const cachePath = join3(cacheDir, key2 + ext);
2350
- if (existsSync3(cachePath)) {
2349
+ const cachePath2 = join3(cacheDir, key2 + ext);
2350
+ if (existsSync3(cachePath2)) {
2351
2351
  console.log(`[image-resize] Cache hit for ${width}x${height} image`);
2352
- return { buffer: readFileSync2(cachePath), mediaType: outputMediaType };
2352
+ return { buffer: readFileSync2(cachePath2), mediaType: outputMediaType };
2353
2353
  }
2354
2354
  let pipeline = sharp(buffer);
2355
2355
  if (needsResize) {
@@ -2374,7 +2374,7 @@ async function resizeImageIfNeeded(buffer, mediaType) {
2374
2374
  }
2375
2375
  finalMediaType = "image/jpeg";
2376
2376
  }
2377
- writeFileSync2(cachePath, result);
2377
+ writeFileSync2(cachePath2, result);
2378
2378
  const resultMeta = await sharp(result).metadata();
2379
2379
  console.log(
2380
2380
  `[image-resize] ${width}x${height} -> ${resultMeta.width}x${resultMeta.height} (${(buffer.length / 1024).toFixed(0)}KB -> ${(result.length / 1024).toFixed(0)}KB, ${finalMediaType})`
@@ -7447,6 +7447,35 @@ function stripOrphanedToolResults(msg, removedIds) {
7447
7447
  if (parts.length === 0) return null;
7448
7448
  return { ...msg, content: parts };
7449
7449
  }
7450
+ function wrapToolsNeverThrow(tools) {
7451
+ if (!tools || typeof tools !== "object") return tools;
7452
+ const wrapped = {};
7453
+ for (const [name, t] of Object.entries(tools)) {
7454
+ if (!t || typeof t.execute !== "function") {
7455
+ wrapped[name] = t;
7456
+ continue;
7457
+ }
7458
+ const original = t.execute;
7459
+ wrapped[name] = {
7460
+ ...t,
7461
+ execute: async (input, opts) => {
7462
+ try {
7463
+ return await original.call(t, input, opts);
7464
+ } catch (err) {
7465
+ const message = err?.message ?? String(err);
7466
+ console.warn(`[tool:${name}] threw \u2014 converted to error result so the tool-call isn't orphaned:`, message);
7467
+ return {
7468
+ __error: true,
7469
+ tool: name,
7470
+ message,
7471
+ note: "Tool execution threw an exception. The agent should treat this as a failed tool call and decide how to recover."
7472
+ };
7473
+ }
7474
+ }
7475
+ };
7476
+ }
7477
+ return wrapped;
7478
+ }
7450
7479
  function repairToolPairing(messages) {
7451
7480
  const toolCallIds = /* @__PURE__ */ new Set();
7452
7481
  const toolResultIds = /* @__PURE__ */ new Set();
@@ -7812,6 +7841,120 @@ var init_web = __esm({
7812
7841
  }
7813
7842
  });
7814
7843
 
7844
+ // src/integrations/slack/persistence.ts
7845
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync3, renameSync } from "fs";
7846
+ import { join as join9, dirname as dirname6 } from "path";
7847
+ function cachePath() {
7848
+ return join9(ensureAppDataDirectory(), FILENAME);
7849
+ }
7850
+ function load() {
7851
+ if (loaded) return;
7852
+ loaded = true;
7853
+ const path = cachePath();
7854
+ if (!existsSync16(path)) return;
7855
+ try {
7856
+ const raw = readFileSync7(path, "utf-8");
7857
+ const parsed = JSON.parse(raw);
7858
+ if (parsed?.version !== FILE_VERSION) return;
7859
+ const now = Date.now();
7860
+ for (const [id, e] of Object.entries(parsed.users || {})) {
7861
+ if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
7862
+ userMap.set(id, { name: e.name ?? null, expiresAt: e.expiresAt });
7863
+ }
7864
+ }
7865
+ for (const [key2, e] of Object.entries(parsed.threads || {})) {
7866
+ if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
7867
+ threadMap.set(key2, { owned: !!e.owned, expiresAt: e.expiresAt });
7868
+ }
7869
+ }
7870
+ } catch (err) {
7871
+ console.warn(`[slack] could not load ${FILENAME}:`, err?.message || err);
7872
+ }
7873
+ }
7874
+ function evictIfOversized(map, max) {
7875
+ if (map.size <= max) return;
7876
+ const sorted = [...map.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt);
7877
+ const toRemove = map.size - max;
7878
+ for (let i = 0; i < toRemove; i++) map.delete(sorted[i][0]);
7879
+ }
7880
+ function scheduleSave() {
7881
+ dirty = true;
7882
+ if (saveTimer) return;
7883
+ saveTimer = setTimeout(() => {
7884
+ saveTimer = null;
7885
+ if (dirty) saveSync();
7886
+ }, SAVE_DEBOUNCE_MS);
7887
+ saveTimer.unref?.();
7888
+ }
7889
+ function saveSync() {
7890
+ dirty = false;
7891
+ try {
7892
+ const path = cachePath();
7893
+ const dir = dirname6(path);
7894
+ if (!existsSync16(dir)) mkdirSync6(dir, { recursive: true });
7895
+ const payload = {
7896
+ version: FILE_VERSION,
7897
+ users: Object.fromEntries(userMap),
7898
+ threads: Object.fromEntries(threadMap)
7899
+ };
7900
+ const tmp = `${path}.tmp`;
7901
+ writeFileSync3(tmp, JSON.stringify(payload), "utf-8");
7902
+ renameSync(tmp, path);
7903
+ } catch (err) {
7904
+ console.warn(`[slack] could not persist ${FILENAME}:`, err?.message || err);
7905
+ }
7906
+ }
7907
+ function hookExit() {
7908
+ if (exitHooked) return;
7909
+ exitHooked = true;
7910
+ const flush2 = () => {
7911
+ if (dirty) saveSync();
7912
+ };
7913
+ process.once("beforeExit", flush2);
7914
+ process.once("SIGINT", flush2);
7915
+ process.once("SIGTERM", flush2);
7916
+ }
7917
+ function getCachedUserName(userId) {
7918
+ load();
7919
+ return userMap.get(userId);
7920
+ }
7921
+ function setCachedUserName(userId, entry2) {
7922
+ load();
7923
+ hookExit();
7924
+ userMap.set(userId, entry2);
7925
+ evictIfOversized(userMap, MAX_USERS);
7926
+ scheduleSave();
7927
+ }
7928
+ function getCachedThreadOwnership(key2) {
7929
+ load();
7930
+ return threadMap.get(key2);
7931
+ }
7932
+ function setCachedThreadOwnership(key2, entry2) {
7933
+ load();
7934
+ hookExit();
7935
+ threadMap.set(key2, entry2);
7936
+ evictIfOversized(threadMap, MAX_THREADS);
7937
+ scheduleSave();
7938
+ }
7939
+ var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_USERS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
7940
+ var init_persistence = __esm({
7941
+ "src/integrations/slack/persistence.ts"() {
7942
+ "use strict";
7943
+ init_config();
7944
+ FILENAME = "slack-cache.json";
7945
+ FILE_VERSION = 1;
7946
+ SAVE_DEBOUNCE_MS = 500;
7947
+ MAX_USERS = 5e3;
7948
+ MAX_THREADS = 5e3;
7949
+ loaded = false;
7950
+ userMap = /* @__PURE__ */ new Map();
7951
+ threadMap = /* @__PURE__ */ new Map();
7952
+ dirty = false;
7953
+ saveTimer = null;
7954
+ exitHooked = false;
7955
+ }
7956
+ });
7957
+
7815
7958
  // src/integrations/slack/client.ts
7816
7959
  function readSlackConfig() {
7817
7960
  try {
@@ -7934,13 +8077,13 @@ async function fetchSlackUserName(userId) {
7934
8077
  async function resolveSlackUserName(userId) {
7935
8078
  if (!userId) return null;
7936
8079
  const now = Date.now();
7937
- const hit = userNameCache.get(userId);
8080
+ const hit = getCachedUserName(userId);
7938
8081
  if (hit && hit.expiresAt > now) return hit.name;
7939
8082
  const inflight = userInflight.get(userId);
7940
8083
  if (inflight) return inflight;
7941
8084
  const p = (async () => {
7942
8085
  const name = await fetchSlackUserName(userId);
7943
- userNameCache.set(userId, {
8086
+ setCachedUserName(userId, {
7944
8087
  name,
7945
8088
  expiresAt: now + (name ? USER_TTL_MS : USER_FAIL_TTL_MS)
7946
8089
  });
@@ -7962,11 +8105,63 @@ async function normalizeSlackMentions(text) {
7962
8105
  }
7963
8106
  return text.replace(userMentionRe, (_full, id, label) => {
7964
8107
  if (label) return `${label} <@${id}>`;
7965
- const cached = userNameCache.get(id);
8108
+ const cached = getCachedUserName(id);
7966
8109
  const name = cached?.name;
7967
8110
  return name ? `${name} <@${id}>` : `<@${id}>`;
7968
8111
  });
7969
8112
  }
8113
+ function threadCacheKey(channel, threadTs) {
8114
+ return `${channel}\u241F${threadTs}`;
8115
+ }
8116
+ async function fetchBotParticipatedInThread(channel, threadTs) {
8117
+ const token = getSlackBotToken();
8118
+ if (!token) return false;
8119
+ const self = await ensureSlackSelfIdentity();
8120
+ if (!self) return false;
8121
+ try {
8122
+ const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${encodeURIComponent(threadTs)}&limit=200`;
8123
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
8124
+ const data = await res.json().catch(() => ({}));
8125
+ if (!data?.ok) {
8126
+ console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
8127
+ return false;
8128
+ }
8129
+ const messages = Array.isArray(data.messages) ? data.messages : [];
8130
+ return messages.some(
8131
+ (m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
8132
+ );
8133
+ } catch (err) {
8134
+ console.warn(`[slack] conversations.replies error:`, err?.message || err);
8135
+ return false;
8136
+ }
8137
+ }
8138
+ async function botParticipatedInThread(channel, threadTs) {
8139
+ if (!channel || !threadTs) return false;
8140
+ const key2 = threadCacheKey(channel, threadTs);
8141
+ const now = Date.now();
8142
+ const hit = getCachedThreadOwnership(key2);
8143
+ if (hit && hit.expiresAt > now) return hit.owned;
8144
+ const inflight = threadOwnedInflight.get(key2);
8145
+ if (inflight) return inflight;
8146
+ const p = (async () => {
8147
+ const owned = await fetchBotParticipatedInThread(channel, threadTs);
8148
+ setCachedThreadOwnership(key2, {
8149
+ owned,
8150
+ expiresAt: now + (owned ? THREAD_OWNED_TTL_MS : THREAD_NEG_TTL_MS)
8151
+ });
8152
+ threadOwnedInflight.delete(key2);
8153
+ return owned;
8154
+ })();
8155
+ threadOwnedInflight.set(key2, p);
8156
+ return p;
8157
+ }
8158
+ function noteBotPostedInThread(channel, threadTs) {
8159
+ if (!channel || !threadTs) return;
8160
+ setCachedThreadOwnership(threadCacheKey(channel, threadTs), {
8161
+ owned: true,
8162
+ expiresAt: Date.now() + THREAD_OWNED_TTL_MS
8163
+ });
8164
+ }
7970
8165
  function getSlackDeniedReplyPolicy() {
7971
8166
  try {
7972
8167
  const cfg = getConfig();
@@ -7979,17 +8174,20 @@ function getSlackDeniedReplyPolicy() {
7979
8174
  return { enabled: true, template: DEFAULT_DENIED_TEMPLATE };
7980
8175
  }
7981
8176
  }
7982
- var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userNameCache, userInflight, DEFAULT_DENIED_TEMPLATE;
8177
+ var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS, threadOwnedInflight, DEFAULT_DENIED_TEMPLATE;
7983
8178
  var init_client3 = __esm({
7984
8179
  "src/integrations/slack/client.ts"() {
7985
8180
  "use strict";
7986
8181
  init_config();
8182
+ init_persistence();
7987
8183
  cachedSelf = null;
7988
8184
  selfInflight = null;
7989
8185
  USER_TTL_MS = 60 * 60 * 1e3;
7990
8186
  USER_FAIL_TTL_MS = 5 * 60 * 1e3;
7991
- userNameCache = /* @__PURE__ */ new Map();
7992
8187
  userInflight = /* @__PURE__ */ new Map();
8188
+ THREAD_OWNED_TTL_MS = 60 * 60 * 1e3;
8189
+ THREAD_NEG_TTL_MS = 5 * 60 * 1e3;
8190
+ threadOwnedInflight = /* @__PURE__ */ new Map();
7993
8191
  DEFAULT_DENIED_TEMPLATE = "Sorry, you don't have permission to use this bot. (Contact the bot owner if you think this is a mistake.)";
7994
8192
  }
7995
8193
  });
@@ -8089,6 +8287,7 @@ var init_slack = __esm({
8089
8287
  if (!result.ok) throw new Error(`slack post failed: ${result.error}`);
8090
8288
  if (r.slackChannel && r.threadTs) {
8091
8289
  markThreadOwned(r.slackChannel, r.threadTs);
8290
+ noteBotPostedInThread(r.slackChannel, r.threadTs);
8092
8291
  }
8093
8292
  },
8094
8293
  displayLabel(ref) {
@@ -8746,8 +8945,8 @@ var init_orchestrator_actions = __esm({
8746
8945
 
8747
8946
  // src/integrations/mcp/store.ts
8748
8947
  import { nanoid as nanoid6 } from "nanoid";
8749
- import { existsSync as existsSync16, readFileSync as readFileSync7 } from "fs";
8750
- import { resolve as resolve10, join as join9 } from "path";
8948
+ import { existsSync as existsSync17, readFileSync as readFileSync8 } from "fs";
8949
+ import { resolve as resolve10, join as join10 } from "path";
8751
8950
  function readServers() {
8752
8951
  try {
8753
8952
  const cfg = getConfig();
@@ -8759,12 +8958,12 @@ function readServers() {
8759
8958
  function refreshMcpServersFromDisk() {
8760
8959
  const candidates = [
8761
8960
  resolve10(process.cwd(), "sparkecoder.config.json"),
8762
- join9(ensureAppDataDirectory(), "sparkecoder.config.json")
8961
+ join10(ensureAppDataDirectory(), "sparkecoder.config.json")
8763
8962
  ];
8764
8963
  for (const path of candidates) {
8765
- if (!existsSync16(path)) continue;
8964
+ if (!existsSync17(path)) continue;
8766
8965
  try {
8767
- const raw = JSON.parse(readFileSync7(path, "utf-8"));
8966
+ const raw = JSON.parse(readFileSync8(path, "utf-8"));
8768
8967
  const servers2 = Array.isArray(raw?.mcp?.servers) ? raw.mcp.servers : [];
8769
8968
  setMcpServers(servers2);
8770
8969
  return servers2;
@@ -9377,7 +9576,7 @@ __export(recorder_exports, {
9377
9576
  import { exec as exec5 } from "child_process";
9378
9577
  import { promisify as promisify5 } from "util";
9379
9578
  import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
9380
- import { join as join10 } from "path";
9579
+ import { join as join11 } from "path";
9381
9580
  import { tmpdir } from "os";
9382
9581
  import { nanoid as nanoid7 } from "nanoid";
9383
9582
  async function checkFfmpeg() {
@@ -9434,21 +9633,21 @@ var init_recorder = __esm({
9434
9633
  */
9435
9634
  async encode() {
9436
9635
  if (this.frames.length === 0) return null;
9437
- const workDir = join10(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
9636
+ const workDir = join11(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
9438
9637
  await mkdir4(workDir, { recursive: true });
9439
9638
  try {
9440
9639
  for (let i = 0; i < this.frames.length; i++) {
9441
- const framePath = join10(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
9640
+ const framePath = join11(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
9442
9641
  await writeFile5(framePath, this.frames[i].data);
9443
9642
  }
9444
9643
  const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
9445
9644
  const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
9446
9645
  const clampedFps = Math.max(1, Math.min(fps, 30));
9447
- const outputPath = join10(workDir, `recording_${this.sessionId}.mp4`);
9646
+ const outputPath = join11(workDir, `recording_${this.sessionId}.mp4`);
9448
9647
  const hasFfmpeg = await checkFfmpeg();
9449
9648
  if (hasFfmpeg) {
9450
9649
  await execAsync5(
9451
- `ffmpeg -y -framerate ${clampedFps} -i "${join10(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
9650
+ `ffmpeg -y -framerate ${clampedFps} -i "${join11(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
9452
9651
  { timeout: 12e4 }
9453
9652
  );
9454
9653
  } else {
@@ -9460,7 +9659,7 @@ var init_recorder = __esm({
9460
9659
  const files = await readdir5(workDir);
9461
9660
  for (const f of files) {
9462
9661
  if (f.startsWith("frame_")) {
9463
- await unlink2(join10(workDir, f)).catch(() => {
9662
+ await unlink2(join11(workDir, f)).catch(() => {
9464
9663
  });
9465
9664
  }
9466
9665
  }
@@ -9727,7 +9926,8 @@ ${personality.trim()}`;
9727
9926
  }
9728
9927
  const messages = await this.context.getMessages();
9729
9928
  const tools = options.onToolProgress ? await this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
9730
- const wrappedTools = this.wrapToolsWithApproval(options, tools);
9929
+ const approvalWrapped = this.wrapToolsWithApproval(options, tools);
9930
+ const wrappedTools = wrapToolsNeverThrow(approvalWrapped);
9731
9931
  const useAnthropic = isAnthropicModel(this.session.model);
9732
9932
  const stream = streamText2({
9733
9933
  model: resolveModel(this.session.model),
@@ -9741,6 +9941,17 @@ ${personality.trim()}`;
9741
9941
  providerOptions: useAnthropic ? {
9742
9942
  anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
9743
9943
  } : void 0,
9944
+ // Run repairToolPairing before EVERY step's model call, not just the
9945
+ // first one. The AI SDK's multi-step loop can otherwise feed the model
9946
+ // a prompt containing an orphan tool-call (e.g. when a previous step's
9947
+ // tool result was lost, dropped during compaction, or the stream was
9948
+ // aborted mid-tool). Repairing in `prepareStep` guarantees no orphan
9949
+ // ever reaches the model and we never hit AI_MissingToolResultsError.
9950
+ prepareStep: async ({ messages: stepMessages }) => {
9951
+ const repaired = repairToolPairing(stepMessages);
9952
+ if (repaired === stepMessages) return {};
9953
+ return { messages: repaired };
9954
+ },
9744
9955
  onStepFinish: async (step) => {
9745
9956
  options.onStepFinish?.(step);
9746
9957
  },
@@ -9776,7 +9987,7 @@ ${personality.trim()}`;
9776
9987
  });
9777
9988
  const messages = await this.context.getMessages();
9778
9989
  const tools = options.onToolProgress ? await this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
9779
- const wrappedTools = this.wrapToolsWithApproval(options, tools);
9990
+ const wrappedTools = wrapToolsNeverThrow(this.wrapToolsWithApproval(options, tools));
9780
9991
  const useAnthropic = isAnthropicModel(this.session.model);
9781
9992
  const result = await generateText3({
9782
9993
  model: resolveModel(this.session.model),
@@ -9787,7 +9998,13 @@ ${personality.trim()}`;
9787
9998
  // Enable extended thinking/reasoning for models that support it
9788
9999
  providerOptions: useAnthropic ? {
9789
10000
  anthropic: getAnthropicProviderOptions(this.session.model)
9790
- } : void 0
10001
+ } : void 0,
10002
+ // Repair tool pairing before every step (see `stream()` for full rationale).
10003
+ prepareStep: async ({ messages: stepMessages }) => {
10004
+ const repaired = repairToolPairing(stepMessages);
10005
+ if (repaired === stepMessages) return {};
10006
+ return { messages: repaired };
10007
+ }
9791
10008
  });
9792
10009
  const responseMessages = result.response.messages;
9793
10010
  this.context.addResponseMessages(responseMessages);
@@ -9964,12 +10181,19 @@ ${p.text}` : p.text;
9964
10181
  model: resolveModel(this.session.model),
9965
10182
  system: systemPrompt,
9966
10183
  messages,
9967
- tools: taskTools,
10184
+ tools: wrapToolsNeverThrow(taskTools),
9968
10185
  stopWhen: stepCountIs2(500),
9969
10186
  abortSignal: combinedAbort,
9970
10187
  providerOptions: useAnthropic ? {
9971
10188
  anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
9972
10189
  } : void 0,
10190
+ // See the matching note in `stream()` — repair tool pairing before
10191
+ // every step so we never feed the model an orphan tool-call.
10192
+ prepareStep: async ({ messages: stepMessages }) => {
10193
+ const repaired = repairToolPairing(stepMessages);
10194
+ if (repaired === stepMessages) return {};
10195
+ return { messages: repaired };
10196
+ },
9973
10197
  onStepFinish: async (step) => {
9974
10198
  options.onStepFinish?.(step);
9975
10199
  fireWebhook("task.step_finished", { iteration, text: step.text });
@@ -10203,11 +10427,11 @@ ${p.text}` : p.text;
10203
10427
  const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
10204
10428
  if (!isRemoteConfigured2()) return [];
10205
10429
  const { readFile: readFile13 } = await import("fs/promises");
10206
- const { join: join17, basename: basename7 } = await import("path");
10430
+ const { join: join18, basename: basename7 } = await import("path");
10207
10431
  const urls = [];
10208
10432
  for (const filePath of filePaths) {
10209
10433
  try {
10210
- const fullPath = filePath.startsWith("/") ? filePath : join17(this.session.workingDirectory, filePath);
10434
+ const fullPath = filePath.startsWith("/") ? filePath : join18(this.session.workingDirectory, filePath);
10211
10435
  const fileName = basename7(fullPath);
10212
10436
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
10213
10437
  const mimeMap = {
@@ -10412,19 +10636,19 @@ var init_session_lock = __esm({
10412
10636
  });
10413
10637
 
10414
10638
  // src/orchestrator/webhook-events.ts
10415
- import { existsSync as existsSync17, readFileSync as readFileSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync6 } from "fs";
10416
- import { dirname as dirname6, join as join11 } from "path";
10639
+ import { existsSync as existsSync18, readFileSync as readFileSync9, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
10640
+ import { dirname as dirname7, join as join12 } from "path";
10417
10641
  import { nanoid as nanoid9 } from "nanoid";
10418
10642
  function logFilePath() {
10419
- return join11(getAppDataDirectory(), "webhook-events.jsonl");
10643
+ return join12(getAppDataDirectory(), "webhook-events.jsonl");
10420
10644
  }
10421
10645
  function ensureLoaded() {
10422
10646
  if (cache !== null) return cache;
10423
10647
  cache = [];
10424
10648
  try {
10425
10649
  const p = logFilePath();
10426
- if (!existsSync17(p)) return cache;
10427
- const lines = readFileSync8(p, "utf-8").split("\n").filter(Boolean);
10650
+ if (!existsSync18(p)) return cache;
10651
+ const lines = readFileSync9(p, "utf-8").split("\n").filter(Boolean);
10428
10652
  for (const line of lines) {
10429
10653
  try {
10430
10654
  cache.push(JSON.parse(line));
@@ -10434,7 +10658,7 @@ function ensureLoaded() {
10434
10658
  if (cache.length > MAX_EVENTS) {
10435
10659
  cache = cache.slice(-MAX_EVENTS);
10436
10660
  try {
10437
- writeFileSync3(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
10661
+ writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
10438
10662
  } catch {
10439
10663
  }
10440
10664
  }
@@ -10448,7 +10672,7 @@ function appendEvent(ev) {
10448
10672
  if (list.length > MAX_EVENTS) list.shift();
10449
10673
  try {
10450
10674
  const p = logFilePath();
10451
- mkdirSync6(dirname6(p), { recursive: true });
10675
+ mkdirSync7(dirname7(p), { recursive: true });
10452
10676
  appendFileSync3(p, JSON.stringify(ev) + "\n");
10453
10677
  } catch {
10454
10678
  }
@@ -10479,8 +10703,8 @@ function updateEvent(id, patch) {
10479
10703
  list[i] = { ...list[i], ...patch };
10480
10704
  try {
10481
10705
  const p = logFilePath();
10482
- mkdirSync6(dirname6(p), { recursive: true });
10483
- writeFileSync3(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
10706
+ mkdirSync7(dirname7(p), { recursive: true });
10707
+ writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
10484
10708
  } catch {
10485
10709
  }
10486
10710
  }
@@ -10512,7 +10736,7 @@ function listEvents(filter = {}) {
10512
10736
  function clearAllEvents() {
10513
10737
  cache = [];
10514
10738
  try {
10515
- writeFileSync3(logFilePath(), "");
10739
+ writeFileSync4(logFilePath(), "");
10516
10740
  } catch {
10517
10741
  }
10518
10742
  }
@@ -10786,8 +11010,8 @@ import { Hono as Hono10 } from "hono";
10786
11010
  import { serve } from "@hono/node-server";
10787
11011
  import { cors } from "hono/cors";
10788
11012
  import { logger } from "hono/logger";
10789
- import { existsSync as existsSync21, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
10790
- import { resolve as resolve12, dirname as dirname9, join as join16 } from "path";
11013
+ import { existsSync as existsSync22, mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "fs";
11014
+ import { resolve as resolve12, dirname as dirname10, join as join17 } from "path";
10791
11015
  import { spawn as spawn2 } from "child_process";
10792
11016
  import { createServer as createNetServer } from "net";
10793
11017
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -10801,9 +11025,9 @@ init_checkpoints();
10801
11025
  import { Hono } from "hono";
10802
11026
  import { zValidator } from "@hono/zod-validator";
10803
11027
  import { z as z16 } from "zod";
10804
- import { existsSync as existsSync18, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
11028
+ import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
10805
11029
  import { readdir as readdir6 } from "fs/promises";
10806
- import { join as join12, basename as basename5, extname as extname8, relative as relative9 } from "path";
11030
+ import { join as join13, basename as basename5, extname as extname8, relative as relative9 } from "path";
10807
11031
  import { nanoid as nanoid10 } from "nanoid";
10808
11032
 
10809
11033
  // src/tasks/agent-status.ts
@@ -11441,12 +11665,12 @@ sessions2.get("/:id/diff/:filePath", async (c) => {
11441
11665
  });
11442
11666
  function getAttachmentsDir(sessionId) {
11443
11667
  const appDataDir = getAppDataDirectory();
11444
- return join12(appDataDir, "attachments", sessionId);
11668
+ return join13(appDataDir, "attachments", sessionId);
11445
11669
  }
11446
11670
  function ensureAttachmentsDir(sessionId) {
11447
11671
  const dir = getAttachmentsDir(sessionId);
11448
- if (!existsSync18(dir)) {
11449
- mkdirSync7(dir, { recursive: true });
11672
+ if (!existsSync19(dir)) {
11673
+ mkdirSync8(dir, { recursive: true });
11450
11674
  }
11451
11675
  return dir;
11452
11676
  }
@@ -11457,12 +11681,12 @@ sessions2.get("/:id/attachments", async (c) => {
11457
11681
  return c.json({ error: "Session not found" }, 404);
11458
11682
  }
11459
11683
  const dir = getAttachmentsDir(sessionId);
11460
- if (!existsSync18(dir)) {
11684
+ if (!existsSync19(dir)) {
11461
11685
  return c.json({ sessionId, attachments: [], count: 0 });
11462
11686
  }
11463
11687
  const files = readdirSync3(dir);
11464
11688
  const attachments = files.map((filename) => {
11465
- const filePath = join12(dir, filename);
11689
+ const filePath = join13(dir, filename);
11466
11690
  const stats = statSync2(filePath);
11467
11691
  return {
11468
11692
  id: filename.split("_")[0],
@@ -11497,9 +11721,9 @@ sessions2.post("/:id/attachments", async (c) => {
11497
11721
  const id = nanoid10(10);
11498
11722
  const ext = extname8(file.name) || "";
11499
11723
  const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
11500
- const filePath = join12(dir, safeFilename);
11724
+ const filePath = join13(dir, safeFilename);
11501
11725
  const arrayBuffer = await file.arrayBuffer();
11502
- writeFileSync4(filePath, Buffer.from(arrayBuffer));
11726
+ writeFileSync5(filePath, Buffer.from(arrayBuffer));
11503
11727
  return c.json({
11504
11728
  id,
11505
11729
  filename: file.name,
@@ -11523,13 +11747,13 @@ sessions2.post("/:id/attachments", async (c) => {
11523
11747
  const id = nanoid10(10);
11524
11748
  const ext = extname8(body.filename) || "";
11525
11749
  const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
11526
- const filePath = join12(dir, safeFilename);
11750
+ const filePath = join13(dir, safeFilename);
11527
11751
  let base64Data = body.data;
11528
11752
  if (base64Data.includes(",")) {
11529
11753
  base64Data = base64Data.split(",")[1];
11530
11754
  }
11531
11755
  const buffer = Buffer.from(base64Data, "base64");
11532
- writeFileSync4(filePath, buffer);
11756
+ writeFileSync5(filePath, buffer);
11533
11757
  return c.json({
11534
11758
  id,
11535
11759
  filename: body.filename,
@@ -11552,7 +11776,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
11552
11776
  return c.json({ error: "Session not found" }, 404);
11553
11777
  }
11554
11778
  const dir = getAttachmentsDir(sessionId);
11555
- if (!existsSync18(dir)) {
11779
+ if (!existsSync19(dir)) {
11556
11780
  return c.json({ error: "Attachment not found" }, 404);
11557
11781
  }
11558
11782
  const files = readdirSync3(dir);
@@ -11560,7 +11784,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
11560
11784
  if (!file) {
11561
11785
  return c.json({ error: "Attachment not found" }, 404);
11562
11786
  }
11563
- const filePath = join12(dir, file);
11787
+ const filePath = join13(dir, file);
11564
11788
  unlinkSync2(filePath);
11565
11789
  return c.json({ success: true, id: attachmentId });
11566
11790
  });
@@ -11643,7 +11867,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
11643
11867
  const entries = await readdir6(currentDir, { withFileTypes: true });
11644
11868
  for (const entry2 of entries) {
11645
11869
  if (results.length >= limit * 2) break;
11646
- const fullPath = join12(currentDir, entry2.name);
11870
+ const fullPath = join13(currentDir, entry2.name);
11647
11871
  const relativePath = relative9(baseDir, fullPath);
11648
11872
  if (entry2.isDirectory() && IGNORED_DIRECTORIES.has(entry2.name)) {
11649
11873
  continue;
@@ -11691,7 +11915,7 @@ sessions2.get(
11691
11915
  return c.json({ error: "Session not found" }, 404);
11692
11916
  }
11693
11917
  const workingDirectory = session.workingDirectory;
11694
- if (!existsSync18(workingDirectory)) {
11918
+ if (!existsSync19(workingDirectory)) {
11695
11919
  return c.json({
11696
11920
  sessionId,
11697
11921
  workingDirectory,
@@ -11799,14 +12023,101 @@ sessions2.get("/:id/browser-recording", async (c) => {
11799
12023
 
11800
12024
  // src/server/routes/agents.ts
11801
12025
  init_db();
11802
- init_agent();
11803
- init_session_lock();
11804
- init_config();
11805
12026
  import { Hono as Hono2 } from "hono";
11806
12027
  import { zValidator as zValidator2 } from "@hono/zod-validator";
11807
12028
  import { z as z17 } from "zod";
11808
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
11809
- import { join as join13 } from "path";
12029
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
12030
+ import { join as join14 } from "path";
12031
+
12032
+ // src/agent/missing-tool-recovery.ts
12033
+ init_db();
12034
+ function extractMissingToolCallIds(error) {
12035
+ if (!error) return [];
12036
+ const e = error;
12037
+ if (Array.isArray(e.toolCallIds) && e.toolCallIds.every((x) => typeof x === "string")) {
12038
+ return e.toolCallIds;
12039
+ }
12040
+ const msg = typeof e.message === "string" ? e.message : "";
12041
+ const ids = /* @__PURE__ */ new Set();
12042
+ for (const m of msg.matchAll(/(?:tool call|tool calls)\s+([a-zA-Z0-9_\-, ]+?)(?:\.|$)/g)) {
12043
+ for (const id of m[1].split(/\s*,\s*/)) {
12044
+ const trimmed = id.trim();
12045
+ if (trimmed) ids.add(trimmed);
12046
+ }
12047
+ }
12048
+ return [...ids];
12049
+ }
12050
+ function isMissingToolResultsError(error) {
12051
+ if (!error) return false;
12052
+ const e = error;
12053
+ if (e.name === "AI_MissingToolResultsError") return true;
12054
+ if (Array.isArray(e.toolCallIds)) return true;
12055
+ return typeof e.message === "string" && /tool result.*is missing for tool call/i.test(e.message);
12056
+ }
12057
+ async function recoverFromMissingToolResults(sessionId, error) {
12058
+ const toolCallIds = extractMissingToolCallIds(error);
12059
+ if (toolCallIds.length === 0) return null;
12060
+ let history = [];
12061
+ try {
12062
+ history = await messageQueries.getModelMessages(sessionId);
12063
+ } catch (err) {
12064
+ console.warn("[missing-tool-recovery] could not load messages:", err?.message || err);
12065
+ }
12066
+ const toolNameByCallId = /* @__PURE__ */ new Map();
12067
+ const existingResultIds = /* @__PURE__ */ new Set();
12068
+ for (const msg of history) {
12069
+ if (!Array.isArray(msg.content)) continue;
12070
+ for (const part of msg.content) {
12071
+ if (part?.type === "tool-call" && typeof part.toolCallId === "string") {
12072
+ if (typeof part.toolName === "string") toolNameByCallId.set(part.toolCallId, part.toolName);
12073
+ }
12074
+ if (part?.type === "tool-result" && typeof part.toolCallId === "string") {
12075
+ existingResultIds.add(part.toolCallId);
12076
+ }
12077
+ }
12078
+ }
12079
+ const resolved = toolCallIds.map((id) => ({
12080
+ toolCallId: id,
12081
+ toolName: toolNameByCallId.get(id) ?? "unknown",
12082
+ foundInAssistantMessage: toolNameByCallId.has(id)
12083
+ }));
12084
+ const stillOrphaned = toolCallIds.filter((id) => !existingResultIds.has(id));
12085
+ let syntheticToolMessageSaved = false;
12086
+ if (stillOrphaned.length > 0) {
12087
+ const syntheticParts = stillOrphaned.map((id) => ({
12088
+ type: "tool-result",
12089
+ toolCallId: id,
12090
+ toolName: toolNameByCallId.get(id) ?? "unknown",
12091
+ output: {
12092
+ type: "text",
12093
+ 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.]"
12094
+ }
12095
+ }));
12096
+ try {
12097
+ await messageQueries.create(sessionId, {
12098
+ role: "tool",
12099
+ content: syntheticParts
12100
+ });
12101
+ syntheticToolMessageSaved = true;
12102
+ } catch (err) {
12103
+ console.error("[missing-tool-recovery] failed to persist synthetic tool message:", err?.message || err);
12104
+ }
12105
+ }
12106
+ const lastFewMessageRoles = history.slice(-6).map((m) => m.role);
12107
+ return {
12108
+ kind: "missing_tool_results",
12109
+ toolCallIds,
12110
+ resolved,
12111
+ syntheticToolMessageSaved,
12112
+ lastFewMessageRoles,
12113
+ 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."
12114
+ };
12115
+ }
12116
+
12117
+ // src/server/routes/agents.ts
12118
+ init_agent();
12119
+ init_session_lock();
12120
+ init_config();
11810
12121
 
11811
12122
  // src/server/resumable-stream.ts
11812
12123
  import { createResumableStreamContext } from "resumable-stream/generic";
@@ -12013,12 +12324,12 @@ var rejectSchema = z17.object({
12013
12324
  var streamAbortControllers = /* @__PURE__ */ new Map();
12014
12325
  function getAttachmentsDirectory(sessionId) {
12015
12326
  const appDataDir = getAppDataDirectory();
12016
- return join13(appDataDir, "attachments", sessionId);
12327
+ return join14(appDataDir, "attachments", sessionId);
12017
12328
  }
12018
12329
  async function saveAttachmentToDisk(sessionId, attachment, index) {
12019
12330
  const attachmentsDir = getAttachmentsDirectory(sessionId);
12020
- if (!existsSync19(attachmentsDir)) {
12021
- mkdirSync8(attachmentsDir, { recursive: true });
12331
+ if (!existsSync20(attachmentsDir)) {
12332
+ mkdirSync9(attachmentsDir, { recursive: true });
12022
12333
  }
12023
12334
  let filename = attachment.filename;
12024
12335
  if (!filename) {
@@ -12036,8 +12347,8 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
12036
12347
  attachment.mediaType = resized.mediaType;
12037
12348
  attachment.data = buffer.toString("base64");
12038
12349
  }
12039
- const filePath = join13(attachmentsDir, filename);
12040
- writeFileSync5(filePath, buffer);
12350
+ const filePath = join14(attachmentsDir, filename);
12351
+ writeFileSync6(filePath, buffer);
12041
12352
  return filePath;
12042
12353
  }
12043
12354
  function stripDataUrlPrefix2(data) {
@@ -12374,7 +12685,20 @@ ${prompt}` });
12374
12685
  await writeSSE(JSON.stringify({ type: "abort" }));
12375
12686
  } else {
12376
12687
  console.error("Agent error:", error);
12377
- await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
12688
+ let debugPayload = void 0;
12689
+ if (isMissingToolResultsError(error)) {
12690
+ try {
12691
+ const recovery = await recoverFromMissingToolResults(sessionId, error);
12692
+ if (recovery) debugPayload = recovery;
12693
+ } catch (recErr) {
12694
+ console.error("[missing-tool-recovery] failed:", recErr?.message || recErr);
12695
+ }
12696
+ }
12697
+ await writeSSE(JSON.stringify({
12698
+ type: "error",
12699
+ errorText: error.message,
12700
+ ...debugPayload ? { debug: debugPayload } : {}
12701
+ }));
12378
12702
  try {
12379
12703
  await activeStreamQueries.markError(streamId);
12380
12704
  } catch {
@@ -12914,7 +13238,20 @@ agents.post(
12914
13238
  await writeSSE(JSON.stringify({ type: "abort" }));
12915
13239
  } else {
12916
13240
  console.error("Agent error:", error);
12917
- await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
13241
+ let debugPayload = void 0;
13242
+ if (isMissingToolResultsError(error)) {
13243
+ try {
13244
+ const recovery = await recoverFromMissingToolResults(session.id, error);
13245
+ if (recovery) debugPayload = recovery;
13246
+ } catch (recErr) {
13247
+ console.error("[missing-tool-recovery] failed:", recErr?.message || recErr);
13248
+ }
13249
+ }
13250
+ await writeSSE(JSON.stringify({
13251
+ type: "error",
13252
+ errorText: error.message,
13253
+ ...debugPayload ? { debug: debugPayload } : {}
13254
+ }));
12918
13255
  await activeStreamQueries.markError(streamId);
12919
13256
  }
12920
13257
  } finally {
@@ -12997,26 +13334,26 @@ init_config();
12997
13334
  import { Hono as Hono3 } from "hono";
12998
13335
  import { zValidator as zValidator3 } from "@hono/zod-validator";
12999
13336
  import { z as z18 } from "zod";
13000
- import { readFileSync as readFileSync9 } from "fs";
13337
+ import { readFileSync as readFileSync10 } from "fs";
13001
13338
  import { fileURLToPath as fileURLToPath3 } from "url";
13002
- import { dirname as dirname7, join as join14 } from "path";
13339
+ import { dirname as dirname8, join as join15 } from "path";
13003
13340
  var __filename = fileURLToPath3(import.meta.url);
13004
- var __dirname = dirname7(__filename);
13341
+ var __dirname = dirname8(__filename);
13005
13342
  var possiblePaths = [
13006
- join14(__dirname, "../package.json"),
13343
+ join15(__dirname, "../package.json"),
13007
13344
  // From dist/server -> dist/../package.json
13008
- join14(__dirname, "../../package.json"),
13345
+ join15(__dirname, "../../package.json"),
13009
13346
  // From dist/server (if nested differently)
13010
- join14(__dirname, "../../../package.json"),
13347
+ join15(__dirname, "../../../package.json"),
13011
13348
  // From src/server/routes (development)
13012
- join14(process.cwd(), "package.json")
13349
+ join15(process.cwd(), "package.json")
13013
13350
  // From current working directory
13014
13351
  ];
13015
13352
  var currentVersion = "0.0.0";
13016
13353
  var packageName = "sparkecoder";
13017
13354
  for (const packageJsonPath of possiblePaths) {
13018
13355
  try {
13019
- const packageJson = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
13356
+ const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
13020
13357
  if (packageJson.name === "sparkecoder") {
13021
13358
  currentVersion = packageJson.version || "0.0.0";
13022
13359
  packageName = packageJson.name || "sparkecoder";
@@ -13834,12 +14171,13 @@ slack.post("/events", async (c) => {
13834
14171
  if (inbound) {
13835
14172
  const isThreadReply = ev.type === "message" && ev.channel_type !== "im" && typeof ev.thread_ts === "string" && ev.thread_ts !== ev.ts;
13836
14173
  if (isThreadReply) {
13837
- const ours = isThreadOwned(ev.channel, ev.thread_ts) || await threadBelongsToUs(ev.channel, ev.thread_ts);
14174
+ const ours = isThreadOwned(ev.channel, ev.thread_ts) || await botParticipatedInThread(ev.channel, ev.thread_ts);
13838
14175
  if (!ours) {
13839
14176
  console.log(`[slack] dropping thread reply in unknown thread: channel=${ev.channel} thread=${ev.thread_ts}`);
13840
14177
  updateEvent(auditId, { status: "dropped", dropReason: "thread_not_owned" });
13841
14178
  return c.json({ ok: true });
13842
14179
  }
14180
+ markThreadOwned(ev.channel, ev.thread_ts);
13843
14181
  }
13844
14182
  if (ev.type === "app_mention" && ev.channel && (ev.thread_ts || ev.ts)) {
13845
14183
  markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
@@ -13878,18 +14216,6 @@ slack.post("/events", async (c) => {
13878
14216
  }
13879
14217
  return c.json({ ok: true });
13880
14218
  });
13881
- async function threadBelongsToUs(channel, threadTs) {
13882
- try {
13883
- const sessions3 = await sessionQueries.list(500, 0);
13884
- return sessions3.some((s) => {
13885
- const slack2 = s.config?.slack;
13886
- return slack2?.channel === channel && slack2?.threadTs === threadTs;
13887
- });
13888
- } catch (err) {
13889
- console.warn("[slack] threadBelongsToUs lookup failed:", err?.message ?? err);
13890
- return false;
13891
- }
13892
- }
13893
14219
  async function findOrCreateOrchestratorId() {
13894
14220
  try {
13895
14221
  const all = await sessionQueries.list(500, 0);
@@ -14278,9 +14604,9 @@ init_skills();
14278
14604
  import { Hono as Hono9 } from "hono";
14279
14605
  import { zValidator as zValidator7 } from "@hono/zod-validator";
14280
14606
  import { z as z22 } from "zod";
14281
- import { existsSync as existsSync20, statSync as statSync3 } from "fs";
14607
+ import { existsSync as existsSync21, statSync as statSync3 } from "fs";
14282
14608
  import { readFile as readFile12, writeFile as writeFile6, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
14283
- import { resolve as resolve11, join as join15, basename as basename6, dirname as dirname8, extname as extname9 } from "path";
14609
+ import { resolve as resolve11, join as join16, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
14284
14610
  var skills = new Hono9();
14285
14611
  function encodeId(filePath) {
14286
14612
  return Buffer.from(filePath, "utf-8").toString("base64url");
@@ -14339,13 +14665,13 @@ function pathToLabel(path) {
14339
14665
  if (path.includes("/.sparkecoder/skills")) return ".sparkecoder/skills";
14340
14666
  if (path.includes("/.cursor/rules")) return ".cursor/rules";
14341
14667
  if (path.includes("/.claude/skills")) return ".claude/skills";
14342
- return basename6(dirname8(path)) + "/" + basename6(path);
14668
+ return basename6(dirname9(path)) + "/" + basename6(path);
14343
14669
  }
14344
14670
  skills.get("/", async (c) => {
14345
14671
  const dirs = listAllDirectories();
14346
14672
  const allSkills = [];
14347
14673
  for (const dir of dirs) {
14348
- if (!existsSync20(dir.path)) continue;
14674
+ if (!existsSync21(dir.path)) continue;
14349
14675
  try {
14350
14676
  const list = await loadSkillsFromDirectory(dir.path, {
14351
14677
  priority: dir.priority,
@@ -14382,7 +14708,7 @@ skills.get("/", async (c) => {
14382
14708
  label: d.label,
14383
14709
  source: d.source,
14384
14710
  alwaysApply: d.alwaysApply,
14385
- exists: existsSync20(d.path),
14711
+ exists: existsSync21(d.path),
14386
14712
  writable: isWritable(d.path)
14387
14713
  })),
14388
14714
  skills: allSkills
@@ -14390,7 +14716,7 @@ skills.get("/", async (c) => {
14390
14716
  });
14391
14717
  function isWritable(dir) {
14392
14718
  try {
14393
- if (!existsSync20(dir)) return false;
14719
+ if (!existsSync21(dir)) return false;
14394
14720
  if (dir.includes("/skills/default")) return false;
14395
14721
  return true;
14396
14722
  } catch {
@@ -14399,7 +14725,7 @@ function isWritable(dir) {
14399
14725
  }
14400
14726
  skills.get("/:id", async (c) => {
14401
14727
  const filePath = decodeId(c.req.param("id"));
14402
- if (!filePath || !existsSync20(filePath)) {
14728
+ if (!filePath || !existsSync21(filePath)) {
14403
14729
  return c.json({ error: "skill not found" }, 404);
14404
14730
  }
14405
14731
  const content = await readFile12(filePath, "utf-8");
@@ -14428,8 +14754,8 @@ skills.post(
14428
14754
  const safeName = basename6(fileName).replace(/[^A-Za-z0-9._-]/g, "-");
14429
14755
  const ext = extname9(safeName).toLowerCase();
14430
14756
  const finalName = ext === ".md" || ext === ".mdc" ? safeName : `${safeName}.md`;
14431
- const filePath = join15(targetDir, finalName);
14432
- if (existsSync20(filePath)) {
14757
+ const filePath = join16(targetDir, finalName);
14758
+ if (existsSync21(filePath)) {
14433
14759
  return c.json({ error: `file already exists: ${finalName}` }, 409);
14434
14760
  }
14435
14761
  try {
@@ -14446,7 +14772,7 @@ skills.put(
14446
14772
  zValidator7("json", z22.object({ content: z22.string() })),
14447
14773
  async (c) => {
14448
14774
  const filePath = decodeId(c.req.param("id"));
14449
- if (!filePath || !existsSync20(filePath)) return c.json({ error: "skill not found" }, 404);
14775
+ if (!filePath || !existsSync21(filePath)) return c.json({ error: "skill not found" }, 404);
14450
14776
  if (filePath.includes("/skills/default")) {
14451
14777
  return c.json({ error: "built-in skills are read-only" }, 400);
14452
14778
  }
@@ -14456,7 +14782,7 @@ skills.put(
14456
14782
  );
14457
14783
  skills.delete("/:id", async (c) => {
14458
14784
  const filePath = decodeId(c.req.param("id"));
14459
- if (!filePath || !existsSync20(filePath)) return c.json({ error: "skill not found" }, 404);
14785
+ if (!filePath || !existsSync21(filePath)) return c.json({ error: "skill not found" }, 404);
14460
14786
  if (filePath.includes("/skills/default")) {
14461
14787
  return c.json({ error: "built-in skills are read-only" }, 400);
14462
14788
  }
@@ -14476,7 +14802,7 @@ skills.post(
14476
14802
  }
14477
14803
  const next = [...current, abs];
14478
14804
  setSkillsAdditionalDirectories(next);
14479
- return c.json({ ok: true, path: abs, exists: existsSync20(abs) }, 201);
14805
+ return c.json({ ok: true, path: abs, exists: existsSync21(abs) }, 201);
14480
14806
  }
14481
14807
  );
14482
14808
  skills.delete("/directories", (c) => {
@@ -14650,13 +14976,13 @@ var DEFAULT_WEB_PORT = 6969;
14650
14976
  var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
14651
14977
  function getWebDirectory() {
14652
14978
  try {
14653
- const currentDir = dirname9(fileURLToPath4(import.meta.url));
14979
+ const currentDir = dirname10(fileURLToPath4(import.meta.url));
14654
14980
  const webDir = resolve12(currentDir, "..", "web");
14655
- if (existsSync21(webDir) && existsSync21(join16(webDir, "package.json"))) {
14981
+ if (existsSync22(webDir) && existsSync22(join17(webDir, "package.json"))) {
14656
14982
  return webDir;
14657
14983
  }
14658
14984
  const altWebDir = resolve12(currentDir, "..", "..", "web");
14659
- if (existsSync21(altWebDir) && existsSync21(join16(altWebDir, "package.json"))) {
14985
+ if (existsSync22(altWebDir) && existsSync22(join17(altWebDir, "package.json"))) {
14660
14986
  return altWebDir;
14661
14987
  }
14662
14988
  return null;
@@ -14714,23 +15040,23 @@ async function findWebPort(preferredPort) {
14714
15040
  return { port: preferredPort, alreadyRunning: false };
14715
15041
  }
14716
15042
  function hasProductionBuild(webDir) {
14717
- const buildIdPath = join16(webDir, ".next", "BUILD_ID");
14718
- return existsSync21(buildIdPath);
15043
+ const buildIdPath = join17(webDir, ".next", "BUILD_ID");
15044
+ return existsSync22(buildIdPath);
14719
15045
  }
14720
15046
  function hasSourceFiles(webDir) {
14721
- const appDir = join16(webDir, "src", "app");
14722
- const pagesDir = join16(webDir, "src", "pages");
14723
- const rootAppDir = join16(webDir, "app");
14724
- const rootPagesDir = join16(webDir, "pages");
14725
- return existsSync21(appDir) || existsSync21(pagesDir) || existsSync21(rootAppDir) || existsSync21(rootPagesDir);
15047
+ const appDir = join17(webDir, "src", "app");
15048
+ const pagesDir = join17(webDir, "src", "pages");
15049
+ const rootAppDir = join17(webDir, "app");
15050
+ const rootPagesDir = join17(webDir, "pages");
15051
+ return existsSync22(appDir) || existsSync22(pagesDir) || existsSync22(rootAppDir) || existsSync22(rootPagesDir);
14726
15052
  }
14727
15053
  function getStandaloneServerPath(webDir) {
14728
15054
  const possiblePaths2 = [
14729
- join16(webDir, ".next", "standalone", "server.js"),
14730
- join16(webDir, ".next", "standalone", "web", "server.js")
15055
+ join17(webDir, ".next", "standalone", "server.js"),
15056
+ join17(webDir, ".next", "standalone", "web", "server.js")
14731
15057
  ];
14732
15058
  for (const serverPath of possiblePaths2) {
14733
- if (existsSync21(serverPath)) {
15059
+ if (existsSync22(serverPath)) {
14734
15060
  return serverPath;
14735
15061
  }
14736
15062
  }
@@ -14770,15 +15096,15 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
14770
15096
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
14771
15097
  return { process: null, port: actualPort };
14772
15098
  }
14773
- const usePnpm = existsSync21(join16(webDir, "pnpm-lock.yaml"));
14774
- const useNpm = !usePnpm && existsSync21(join16(webDir, "package-lock.json"));
15099
+ const usePnpm = existsSync22(join17(webDir, "pnpm-lock.yaml"));
15100
+ const useNpm = !usePnpm && existsSync22(join17(webDir, "package-lock.json"));
14775
15101
  const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
14776
15102
  const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv } = process.env;
14777
15103
  const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
14778
15104
  const runtimeConfig = { apiBaseUrl: apiUrl };
14779
- const runtimeConfigPath = join16(webDir, "runtime-config.json");
15105
+ const runtimeConfigPath = join17(webDir, "runtime-config.json");
14780
15106
  try {
14781
- writeFileSync6(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
15107
+ writeFileSync7(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
14782
15108
  if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
14783
15109
  } catch (err) {
14784
15110
  if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
@@ -14798,7 +15124,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
14798
15124
  if (standaloneServerPath) {
14799
15125
  command = "node";
14800
15126
  args = ["server.js"];
14801
- cwd = dirname9(standaloneServerPath);
15127
+ cwd = dirname10(standaloneServerPath);
14802
15128
  webEnv.PORT = String(actualPort);
14803
15129
  webEnv.HOSTNAME = "0.0.0.0";
14804
15130
  if (!quiet) console.log(" \u{1F4E6} Starting Web UI from standalone build...");
@@ -14992,8 +15318,8 @@ async function startServer(options = {}) {
14992
15318
  if (options.workingDirectory) {
14993
15319
  config.resolvedWorkingDirectory = options.workingDirectory;
14994
15320
  }
14995
- if (!existsSync21(config.resolvedWorkingDirectory)) {
14996
- mkdirSync9(config.resolvedWorkingDirectory, { recursive: true });
15321
+ if (!existsSync22(config.resolvedWorkingDirectory)) {
15322
+ mkdirSync10(config.resolvedWorkingDirectory, { recursive: true });
14997
15323
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
14998
15324
  }
14999
15325
  if (!config.resolvedRemoteServer.url) {