sparkecoder 0.1.111 → 0.1.112

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 (139) hide show
  1. package/dist/agent/index.js +10 -0
  2. package/dist/agent/index.js.map +1 -1
  3. package/dist/cli.js +261 -88
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +261 -88
  6. package/dist/index.js.map +1 -1
  7. package/dist/server/index.js +261 -88
  8. package/dist/server/index.js.map +1 -1
  9. package/dist/tools/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/web/.next/BUILD_ID +1 -1
  12. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  13. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  14. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  15. package/web/.next/standalone/web/.next/server/app/(main)/agents/page_client-reference-manifest.js +1 -1
  16. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  17. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  18. package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
  19. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  20. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  22. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
  29. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  30. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  35. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  36. package/web/.next/standalone/web/.next/server/app/agents.rsc +2 -2
  37. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +2 -2
  41. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +2 -2
  43. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +2 -2
  44. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  46. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +2 -2
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +2 -2
  48. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
  50. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
  51. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  57. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +2 -2
  58. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +2 -2
  59. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
  61. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
  62. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  67. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +2 -2
  68. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +2 -2
  69. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
  71. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
  72. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  76. package/web/.next/standalone/web/.next/server/app/docs.rsc +2 -2
  77. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +2 -2
  78. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
  80. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
  81. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  82. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  84. package/web/.next/standalone/web/.next/server/app/index.rsc +3 -3
  85. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  86. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +3 -3
  88. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  89. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  90. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  91. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  92. package/web/.next/standalone/web/.next/server/app/settings.rsc +3 -3
  93. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
  94. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  95. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  96. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  97. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  98. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  99. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  100. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__6097da17._.js +3 -3
  101. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_page_tsx_5ac4794b._.js +1 -1
  102. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +1 -1
  103. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  104. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  105. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  106. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  107. package/web/.next/standalone/web/.next/static/chunks/{ae4bb24474ff1ed0.js → a189cacf6d83cf0b.js} +5 -5
  108. package/web/.next/standalone/web/.next/static/chunks/bef6931fdd8428c8.js +1 -0
  109. package/web/.next/standalone/web/.next/static/chunks/c1f73b3fa4353c31.css +1 -0
  110. package/web/.next/standalone/web/.next/static/chunks/c5dd884b71007965.js +1 -0
  111. package/web/.next/standalone/web/.next/static/static/chunks/{ae4bb24474ff1ed0.js → a189cacf6d83cf0b.js} +5 -5
  112. package/web/.next/standalone/web/.next/static/static/chunks/bef6931fdd8428c8.js +1 -0
  113. package/web/.next/standalone/web/.next/static/static/chunks/c1f73b3fa4353c31.css +1 -0
  114. package/web/.next/standalone/web/.next/static/static/chunks/c5dd884b71007965.js +1 -0
  115. package/web/.next/standalone/web/src/app/(main)/page.tsx +36 -2
  116. package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +40 -7
  117. package/web/.next/standalone/web/src/components/chat-interface.tsx +34 -0
  118. package/web/.next/static/chunks/{ae4bb24474ff1ed0.js → a189cacf6d83cf0b.js} +5 -5
  119. package/web/.next/static/chunks/bef6931fdd8428c8.js +1 -0
  120. package/web/.next/static/chunks/c1f73b3fa4353c31.css +1 -0
  121. package/web/.next/static/chunks/c5dd884b71007965.js +1 -0
  122. package/web/.next/standalone/web/.next/static/chunks/344be859c2c8600b.css +0 -1
  123. package/web/.next/standalone/web/.next/static/chunks/f5fe518b79d1bf41.js +0 -1
  124. package/web/.next/standalone/web/.next/static/chunks/f6e2bbd3014e1fc9.js +0 -1
  125. package/web/.next/standalone/web/.next/static/static/chunks/344be859c2c8600b.css +0 -1
  126. package/web/.next/standalone/web/.next/static/static/chunks/f5fe518b79d1bf41.js +0 -1
  127. package/web/.next/standalone/web/.next/static/static/chunks/f6e2bbd3014e1fc9.js +0 -1
  128. package/web/.next/static/chunks/344be859c2c8600b.css +0 -1
  129. package/web/.next/static/chunks/f5fe518b79d1bf41.js +0 -1
  130. package/web/.next/static/chunks/f6e2bbd3014e1fc9.js +0 -1
  131. /package/web/.next/standalone/web/.next/static/{Tm_6r8b-tCAfCBoIt-U0X → static/x3G1ePtJHSb_uWa9Qs8dN}/_buildManifest.js +0 -0
  132. /package/web/.next/standalone/web/.next/static/{Tm_6r8b-tCAfCBoIt-U0X → static/x3G1ePtJHSb_uWa9Qs8dN}/_clientMiddlewareManifest.json +0 -0
  133. /package/web/.next/standalone/web/.next/static/{Tm_6r8b-tCAfCBoIt-U0X → static/x3G1ePtJHSb_uWa9Qs8dN}/_ssgManifest.js +0 -0
  134. /package/web/.next/standalone/web/.next/static/{static/Tm_6r8b-tCAfCBoIt-U0X → x3G1ePtJHSb_uWa9Qs8dN}/_buildManifest.js +0 -0
  135. /package/web/.next/standalone/web/.next/static/{static/Tm_6r8b-tCAfCBoIt-U0X → x3G1ePtJHSb_uWa9Qs8dN}/_clientMiddlewareManifest.json +0 -0
  136. /package/web/.next/standalone/web/.next/static/{static/Tm_6r8b-tCAfCBoIt-U0X → x3G1ePtJHSb_uWa9Qs8dN}/_ssgManifest.js +0 -0
  137. /package/web/.next/static/{Tm_6r8b-tCAfCBoIt-U0X → x3G1ePtJHSb_uWa9Qs8dN}/_buildManifest.js +0 -0
  138. /package/web/.next/static/{Tm_6r8b-tCAfCBoIt-U0X → x3G1ePtJHSb_uWa9Qs8dN}/_clientMiddlewareManifest.json +0 -0
  139. /package/web/.next/static/{Tm_6r8b-tCAfCBoIt-U0X → x3G1ePtJHSb_uWa9Qs8dN}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -826,9 +826,10 @@ function createDefaultConfig() {
826
826
  }
827
827
  function loadStoredAuthKey() {
828
828
  const locations = [
829
+ process.env.SPARKECODER_AUTH_KEY_PATH,
829
830
  join(process.cwd(), ".sparkecoder", AUTH_KEY_FILE),
830
831
  join(getAppDataDirectory(), AUTH_KEY_FILE)
831
- ];
832
+ ].filter((p) => !!p);
832
833
  for (const keysPath of locations) {
833
834
  if (!existsSync(keysPath)) continue;
834
835
  try {
@@ -847,15 +848,44 @@ function saveAuthKey(authKey3, userId) {
847
848
  userId
848
849
  };
849
850
  const json = JSON.stringify(data, null, 2);
850
- const appDir = ensureAppDataDirectory();
851
- writeFileSync(join(appDir, AUTH_KEY_FILE), json, { mode: 384 });
851
+ const targets = [];
852
+ if (process.env.SPARKECODER_AUTH_KEY_PATH) {
853
+ targets.push({
854
+ label: "SPARKECODER_AUTH_KEY_PATH",
855
+ path: process.env.SPARKECODER_AUTH_KEY_PATH
856
+ });
857
+ }
852
858
  try {
853
- const workspaceAuthDir = join(process.cwd(), ".sparkecoder");
854
- if (!existsSync(workspaceAuthDir)) {
855
- mkdirSync(workspaceAuthDir, { recursive: true });
859
+ const appDir = ensureAppDataDirectory();
860
+ targets.push({ label: "app-data", path: join(appDir, AUTH_KEY_FILE) });
861
+ } catch (err) {
862
+ console.warn(`[auth-key] could not ensure app data dir: ${err?.message ?? err}`);
863
+ }
864
+ targets.push({
865
+ label: "workspace",
866
+ path: join(process.cwd(), ".sparkecoder", AUTH_KEY_FILE)
867
+ });
868
+ const successes = [];
869
+ const failures = [];
870
+ for (const { label, path } of targets) {
871
+ try {
872
+ mkdirSync(dirname(path), { recursive: true });
873
+ writeFileSync(path, json, { mode: 384 });
874
+ successes.push(`${label}:${path}`);
875
+ } catch (err) {
876
+ failures.push({ label, path, error: err?.message ?? String(err) });
856
877
  }
857
- writeFileSync(join(workspaceAuthDir, AUTH_KEY_FILE), json, { mode: 384 });
858
- } catch {
878
+ }
879
+ if (successes.length === 0) {
880
+ const detail = failures.map((f) => `${f.label} (${f.path}): ${f.error}`).join("; ");
881
+ throw new Error(`Failed to persist auth-key.json to any location. ${detail}`);
882
+ }
883
+ if (failures.length > 0) {
884
+ console.warn(
885
+ `[auth-key] wrote to ${successes.length}/${targets.length} locations. Success: ${successes.join(", ")}. Failed: ${failures.map((f) => `${f.label} (${f.path}): ${f.error}`).join("; ")}`
886
+ );
887
+ } else if (process.env.SPARKECODER_VERBOSE_CONFIG) {
888
+ console.log(`[auth-key] saved to: ${successes.join(", ")}`);
859
889
  }
860
890
  }
861
891
  function getStoredAuthKeyInfo() {
@@ -8295,15 +8325,30 @@ var init_client3 = __esm({
8295
8325
  });
8296
8326
 
8297
8327
  // src/integrations/channels/slack.ts
8328
+ function threadKey(channel, threadTs) {
8329
+ return `${channel}\u241F${threadTs}`;
8330
+ }
8331
+ function markThreadOwned(channel, threadTs) {
8332
+ ownedThreads.add(threadKey(channel, threadTs));
8333
+ }
8334
+ function isThreadOwned(channel, threadTs) {
8335
+ return ownedThreads.has(threadKey(channel, threadTs));
8336
+ }
8298
8337
  function stripMention(text) {
8299
8338
  return String(text || "").replace(/<@[^>]+>/g, "").trim();
8300
8339
  }
8301
8340
  function slackEventToInboundResult(event) {
8302
8341
  if (!event) return { event: null, dropReason: "empty_text" };
8303
- if (event.bot_id || event.subtype === "bot_message") return { event: null, dropReason: "bot_message" };
8342
+ if (event.bot_id) return { event: null, dropReason: "bot_message" };
8343
+ if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
8344
+ return { event: null, dropReason: "bot_message" };
8345
+ }
8304
8346
  const isDm = event.type === "message" && event.channel_type === "im";
8305
- if (event.type !== "app_mention" && !isDm) return { event: null, dropReason: "unsupported_type" };
8306
- const text = stripMention(event.text);
8347
+ const isThreadReply = event.type === "message" && !isDm && typeof event.thread_ts === "string" && event.thread_ts !== event.ts;
8348
+ if (event.type !== "app_mention" && !isDm && !isThreadReply) {
8349
+ return { event: null, dropReason: "unsupported_type" };
8350
+ }
8351
+ const text = event.type === "app_mention" ? stripMention(event.text) : (event.text ?? "").trim();
8307
8352
  if (!text) return { event: null, dropReason: "empty_text" };
8308
8353
  const policy = getSlackAllowlistPolicy();
8309
8354
  const userAllowlistActive = policy.allowedUsers.length > 0;
@@ -8338,11 +8383,12 @@ function slackEventToInboundResult(event) {
8338
8383
  }
8339
8384
  };
8340
8385
  }
8341
- var slackChannel;
8386
+ var ownedThreads, slackChannel, IGNORED_MESSAGE_SUBTYPES;
8342
8387
  var init_slack = __esm({
8343
8388
  "src/integrations/channels/slack.ts"() {
8344
8389
  "use strict";
8345
8390
  init_client3();
8391
+ ownedThreads = /* @__PURE__ */ new Set();
8346
8392
  slackChannel = {
8347
8393
  id: "slack",
8348
8394
  canSend: () => isSlackConfigured(),
@@ -8356,6 +8402,9 @@ var init_slack = __esm({
8356
8402
  threadTs: r.threadTs
8357
8403
  });
8358
8404
  if (!result.ok) throw new Error(`slack post failed: ${result.error}`);
8405
+ if (r.slackChannel && r.threadTs) {
8406
+ markThreadOwned(r.slackChannel, r.threadTs);
8407
+ }
8359
8408
  },
8360
8409
  displayLabel(ref) {
8361
8410
  const r = ref;
@@ -8366,6 +8415,29 @@ var init_slack = __esm({
8366
8415
  return parts.join(" ");
8367
8416
  }
8368
8417
  };
8418
+ IGNORED_MESSAGE_SUBTYPES = /* @__PURE__ */ new Set([
8419
+ "bot_message",
8420
+ "message_changed",
8421
+ "message_deleted",
8422
+ "channel_join",
8423
+ "channel_leave",
8424
+ "channel_topic",
8425
+ "channel_purpose",
8426
+ "channel_name",
8427
+ "channel_archive",
8428
+ "channel_unarchive",
8429
+ "pinned_item",
8430
+ "unpinned_item",
8431
+ "thread_broadcast",
8432
+ // also-broadcast-to-channel replies; the regular thread reply already fires
8433
+ "message_replied",
8434
+ // legacy parent-thread bump
8435
+ "file_share",
8436
+ // we'd handle these later; for now skip to avoid double-handling
8437
+ "reply_broadcast",
8438
+ "tombstone",
8439
+ "huddle_thread"
8440
+ ]);
8369
8441
  }
8370
8442
  });
8371
8443
 
@@ -10667,6 +10739,81 @@ var init_session_lock = __esm({
10667
10739
  }
10668
10740
  });
10669
10741
 
10742
+ // src/orchestrator/daemon.ts
10743
+ var daemon_exports = {};
10744
+ __export(daemon_exports, {
10745
+ startOrchestratorDaemon: () => startOrchestratorDaemon,
10746
+ subscribeToDaemonOutput: () => subscribeToDaemonOutput
10747
+ });
10748
+ function subscribeToDaemonOutput(sessionId, fn) {
10749
+ let set = listeners.get(sessionId);
10750
+ if (!set) {
10751
+ set = /* @__PURE__ */ new Set();
10752
+ listeners.set(sessionId, set);
10753
+ }
10754
+ set.add(fn);
10755
+ return () => {
10756
+ const s = listeners.get(sessionId);
10757
+ if (!s) return;
10758
+ s.delete(fn);
10759
+ if (s.size === 0) listeners.delete(sessionId);
10760
+ };
10761
+ }
10762
+ function broadcast(out) {
10763
+ const set = listeners.get(out.sessionId);
10764
+ if (!set) return;
10765
+ for (const fn of set) {
10766
+ try {
10767
+ fn(out);
10768
+ } catch (err) {
10769
+ console.error("[daemon] listener threw:", err?.message || err);
10770
+ }
10771
+ }
10772
+ }
10773
+ function startOrchestratorDaemon() {
10774
+ setFlushHandler(async (sessionId, events) => {
10775
+ await runDaemonTurn(sessionId, events);
10776
+ });
10777
+ }
10778
+ async function runDaemonTurn(sessionId, events) {
10779
+ const startedAt = /* @__PURE__ */ new Date();
10780
+ const session = await sessionQueries.getById(sessionId).catch(() => void 0);
10781
+ if (!session) {
10782
+ console.warn(`[daemon] flush for unknown session ${sessionId}; dropping ${events.length} event(s)`);
10783
+ return;
10784
+ }
10785
+ const prompt = events.map((e) => e.content).join("\n\n");
10786
+ let text = "";
10787
+ let error;
10788
+ await withSessionLock(sessionId, async () => {
10789
+ try {
10790
+ const agent = await Agent.create({ sessionId });
10791
+ const result = await agent.stream({ prompt });
10792
+ for await (const part of result.stream.fullStream) {
10793
+ if (part.type === "text-delta") text += part.text || "";
10794
+ }
10795
+ await result.saveResponseMessages();
10796
+ } catch (err) {
10797
+ error = err?.message || String(err);
10798
+ console.error(`[daemon] turn failed for ${sessionId}:`, error);
10799
+ }
10800
+ });
10801
+ const finishedAt = /* @__PURE__ */ new Date();
10802
+ const trimmed = text.trim();
10803
+ broadcast({ sessionId, text: trimmed, triggeredBy: events, startedAt, finishedAt, error });
10804
+ }
10805
+ var listeners;
10806
+ var init_daemon = __esm({
10807
+ "src/orchestrator/daemon.ts"() {
10808
+ "use strict";
10809
+ init_agent();
10810
+ init_session_lock();
10811
+ init_db();
10812
+ init_inbox();
10813
+ listeners = /* @__PURE__ */ new Map();
10814
+ }
10815
+ });
10816
+
10670
10817
  // src/tasks/boot-recovery.ts
10671
10818
  var boot_recovery_exports = {};
10672
10819
  __export(boot_recovery_exports, {
@@ -10754,81 +10901,6 @@ var init_ensure_orchestrator = __esm({
10754
10901
  }
10755
10902
  });
10756
10903
 
10757
- // src/orchestrator/daemon.ts
10758
- var daemon_exports = {};
10759
- __export(daemon_exports, {
10760
- startOrchestratorDaemon: () => startOrchestratorDaemon,
10761
- subscribeToDaemonOutput: () => subscribeToDaemonOutput
10762
- });
10763
- function subscribeToDaemonOutput(sessionId, fn) {
10764
- let set = listeners.get(sessionId);
10765
- if (!set) {
10766
- set = /* @__PURE__ */ new Set();
10767
- listeners.set(sessionId, set);
10768
- }
10769
- set.add(fn);
10770
- return () => {
10771
- const s = listeners.get(sessionId);
10772
- if (!s) return;
10773
- s.delete(fn);
10774
- if (s.size === 0) listeners.delete(sessionId);
10775
- };
10776
- }
10777
- function broadcast(out) {
10778
- const set = listeners.get(out.sessionId);
10779
- if (!set) return;
10780
- for (const fn of set) {
10781
- try {
10782
- fn(out);
10783
- } catch (err) {
10784
- console.error("[daemon] listener threw:", err?.message || err);
10785
- }
10786
- }
10787
- }
10788
- function startOrchestratorDaemon() {
10789
- setFlushHandler(async (sessionId, events) => {
10790
- await runDaemonTurn(sessionId, events);
10791
- });
10792
- }
10793
- async function runDaemonTurn(sessionId, events) {
10794
- const startedAt = /* @__PURE__ */ new Date();
10795
- const session = await sessionQueries.getById(sessionId).catch(() => void 0);
10796
- if (!session) {
10797
- console.warn(`[daemon] flush for unknown session ${sessionId}; dropping ${events.length} event(s)`);
10798
- return;
10799
- }
10800
- const prompt = events.map((e) => e.content).join("\n\n");
10801
- let text = "";
10802
- let error;
10803
- await withSessionLock(sessionId, async () => {
10804
- try {
10805
- const agent = await Agent.create({ sessionId });
10806
- const result = await agent.stream({ prompt });
10807
- for await (const part of result.stream.fullStream) {
10808
- if (part.type === "text-delta") text += part.text || "";
10809
- }
10810
- await result.saveResponseMessages();
10811
- } catch (err) {
10812
- error = err?.message || String(err);
10813
- console.error(`[daemon] turn failed for ${sessionId}:`, error);
10814
- }
10815
- });
10816
- const finishedAt = /* @__PURE__ */ new Date();
10817
- const trimmed = text.trim();
10818
- broadcast({ sessionId, text: trimmed, triggeredBy: events, startedAt, finishedAt, error });
10819
- }
10820
- var listeners;
10821
- var init_daemon = __esm({
10822
- "src/orchestrator/daemon.ts"() {
10823
- "use strict";
10824
- init_agent();
10825
- init_session_lock();
10826
- init_db();
10827
- init_inbox();
10828
- listeners = /* @__PURE__ */ new Map();
10829
- }
10830
- });
10831
-
10832
10904
  // src/tasks/scheduler.ts
10833
10905
  var scheduler_exports = {};
10834
10906
  __export(scheduler_exports, {
@@ -10960,6 +11032,7 @@ function deriveAgentStatus(input) {
10960
11032
 
10961
11033
  // src/server/routes/sessions.ts
10962
11034
  init_pending_input();
11035
+ init_daemon();
10963
11036
 
10964
11037
  // src/server/devtools-store.ts
10965
11038
  var devtoolsContextStore = /* @__PURE__ */ new Map();
@@ -11102,6 +11175,63 @@ sessions2.post(
11102
11175
  }, 201);
11103
11176
  }
11104
11177
  );
11178
+ sessions2.get("/:id/updates", async (c) => {
11179
+ const id = c.req.param("id");
11180
+ const session = await sessionQueries.getById(id);
11181
+ if (!session) return c.json({ error: "Session not found" }, 404);
11182
+ const stream = new ReadableStream({
11183
+ start(controller) {
11184
+ const encoder = new TextEncoder();
11185
+ const send = (data) => {
11186
+ try {
11187
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
11188
+
11189
+ `));
11190
+ } catch {
11191
+ }
11192
+ };
11193
+ send({ type: "ready", sessionId: id });
11194
+ const unsubscribe = subscribeToDaemonOutput(id, (output) => {
11195
+ send({
11196
+ type: "turn-complete",
11197
+ sessionId: id,
11198
+ finishedAt: output.finishedAt.toISOString(),
11199
+ triggeredBy: output.triggeredBy.map((e) => e.content?.slice(0, 80)),
11200
+ error: output.error ?? null
11201
+ });
11202
+ });
11203
+ let cleaned = false;
11204
+ const cleanup2 = () => {
11205
+ if (cleaned) return;
11206
+ cleaned = true;
11207
+ clearInterval(keepalive);
11208
+ unsubscribe();
11209
+ try {
11210
+ controller.close();
11211
+ } catch {
11212
+ }
11213
+ };
11214
+ const keepalive = setInterval(() => {
11215
+ try {
11216
+ controller.enqueue(encoder.encode(`: keepalive
11217
+
11218
+ `));
11219
+ } catch {
11220
+ cleanup2();
11221
+ }
11222
+ }, 25e3);
11223
+ c.req.raw.signal?.addEventListener("abort", cleanup2);
11224
+ }
11225
+ });
11226
+ return new Response(stream, {
11227
+ headers: {
11228
+ "Content-Type": "text/event-stream",
11229
+ "Cache-Control": "no-cache",
11230
+ Connection: "keep-alive",
11231
+ "X-Accel-Buffering": "no"
11232
+ }
11233
+ });
11234
+ });
11105
11235
  sessions2.get("/:id", async (c) => {
11106
11236
  const id = c.req.param("id");
11107
11237
  const session = await sessionQueries.getById(id);
@@ -13849,6 +13979,19 @@ function verifySlackSignature(opts) {
13849
13979
  init_client3();
13850
13980
  init_slack();
13851
13981
  init_inbox();
13982
+ var recentlyHandled = /* @__PURE__ */ new Map();
13983
+ var MAX_RECENT = 1e3;
13984
+ function alreadyHandled(channel, ts) {
13985
+ if (!channel || !ts) return false;
13986
+ const key2 = `${channel}\u241F${ts}`;
13987
+ if (recentlyHandled.has(key2)) return true;
13988
+ recentlyHandled.set(key2, Date.now());
13989
+ if (recentlyHandled.size > MAX_RECENT) {
13990
+ const oldest = recentlyHandled.keys().next().value;
13991
+ if (oldest) recentlyHandled.delete(oldest);
13992
+ }
13993
+ return false;
13994
+ }
13852
13995
  var slack = new Hono6();
13853
13996
  slack.post("/events", async (c) => {
13854
13997
  const signingSecret = getSlackSigningSecret();
@@ -13874,8 +14017,26 @@ slack.post("/events", async (c) => {
13874
14017
  return c.json({ challenge: payload.challenge });
13875
14018
  }
13876
14019
  if (payload?.type === "event_callback" && payload?.event) {
13877
- const { event: inbound, dropReason } = slackEventToInboundResult(payload.event);
14020
+ const ev = payload.event;
14021
+ if (alreadyHandled(ev.channel, ev.ts)) {
14022
+ return c.json({ ok: true });
14023
+ }
14024
+ const { event: inbound, dropReason } = slackEventToInboundResult(ev);
13878
14025
  if (inbound) {
14026
+ const isThreadReply = ev.type === "message" && ev.channel_type !== "im" && typeof ev.thread_ts === "string" && ev.thread_ts !== ev.ts;
14027
+ if (isThreadReply) {
14028
+ const ours = isThreadOwned(ev.channel, ev.thread_ts) || await threadBelongsToUs(ev.channel, ev.thread_ts);
14029
+ if (!ours) {
14030
+ console.log(`[slack] dropping thread reply in unknown thread: channel=${ev.channel} thread=${ev.thread_ts}`);
14031
+ return c.json({ ok: true });
14032
+ }
14033
+ }
14034
+ if (ev.type === "app_mention" && ev.channel && (ev.thread_ts || ev.ts)) {
14035
+ markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
14036
+ }
14037
+ if (ev.type === "message" && ev.channel_type === "im" && ev.channel && (ev.thread_ts || ev.ts)) {
14038
+ markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
14039
+ }
13879
14040
  const orchestratorId = await findOrCreateOrchestratorId();
13880
14041
  if (orchestratorId) {
13881
14042
  pushToInbox(orchestratorId, inbound);
@@ -13892,6 +14053,18 @@ slack.post("/events", async (c) => {
13892
14053
  }
13893
14054
  return c.json({ ok: true });
13894
14055
  });
14056
+ async function threadBelongsToUs(channel, threadTs) {
14057
+ try {
14058
+ const sessions3 = await sessionQueries.list(500, 0);
14059
+ return sessions3.some((s) => {
14060
+ const slack2 = s.config?.slack;
14061
+ return slack2?.channel === channel && slack2?.threadTs === threadTs;
14062
+ });
14063
+ } catch (err) {
14064
+ console.warn("[slack] threadBelongsToUs lookup failed:", err?.message ?? err);
14065
+ return false;
14066
+ }
14067
+ }
13895
14068
  async function findOrCreateOrchestratorId() {
13896
14069
  try {
13897
14070
  const all = await sessionQueries.list(500, 0);