sparkecoder 0.1.139 → 0.1.141

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 (120) hide show
  1. package/dist/agent/index.d.ts +3 -3
  2. package/dist/agent/index.js +661 -10
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +756 -294
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/index.d.ts +2 -2
  7. package/dist/{index-BAsQWqZj.d.ts → index-Cl_eUatM.d.ts} +96 -96
  8. package/dist/index.d.ts +5 -5
  9. package/dist/index.js +752 -292
  10. package/dist/index.js.map +1 -1
  11. package/dist/{schema-Dz-wABVY.d.ts → schema-BSz4MzhJ.d.ts} +3 -3
  12. package/dist/{search-CVVfuBPZ.d.ts → search-DOzC4ojH.d.ts} +4 -4
  13. package/dist/server/index.js +752 -292
  14. package/dist/server/index.js.map +1 -1
  15. package/dist/tools/index.d.ts +3 -3
  16. package/dist/tools/index.js.map +1 -1
  17. package/package.json +1 -1
  18. package/web/.next/BUILD_ID +1 -1
  19. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  20. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  21. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  22. package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  24. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  39. package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  48. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +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 +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  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 +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  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.html +2 -2
  66. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  75. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  82. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  83. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  85. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  86. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  89. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  90. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  91. package/web/.next/standalone/web/.next/server/app/settings.rsc +2 -2
  92. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
  93. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  94. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  95. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  96. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  97. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  98. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  99. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +1 -1
  100. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  101. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  102. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  103. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  104. package/web/.next/standalone/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
  105. package/web/.next/standalone/web/.next/static/static/chunks/001c7ddc8dad6764.js +3 -0
  106. package/web/.next/standalone/web/runtime-config.json +2 -2
  107. package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +14 -3
  108. package/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
  109. package/web/.next/standalone/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
  110. package/web/.next/standalone/web/.next/static/static/chunks/20ca4e35e9bb3e94.js +0 -3
  111. package/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
  112. /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
  113. /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
  114. /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
  115. /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
  116. /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
  117. /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
  118. /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
  119. /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
  120. /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
@@ -1798,6 +1798,17 @@ function hookExit() {
1798
1798
  process.once("SIGINT", flush2);
1799
1799
  process.once("SIGTERM", flush2);
1800
1800
  }
1801
+ function getCachedUserName(userId) {
1802
+ load();
1803
+ return userMap.get(userId);
1804
+ }
1805
+ function setCachedUserName(userId, entry2) {
1806
+ load();
1807
+ hookExit();
1808
+ userMap.set(userId, entry2);
1809
+ evictIfOversized(userMap, MAX_USERS);
1810
+ scheduleSave();
1811
+ }
1801
1812
  function setCachedThreadOwnership(key2, entry2) {
1802
1813
  load();
1803
1814
  hookExit();
@@ -1805,7 +1816,7 @@ function setCachedThreadOwnership(key2, entry2) {
1805
1816
  evictIfOversized(threadMap, MAX_THREADS);
1806
1817
  scheduleSave();
1807
1818
  }
1808
- var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
1819
+ var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_USERS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
1809
1820
  var init_persistence = __esm({
1810
1821
  "src/integrations/slack/persistence.ts"() {
1811
1822
  "use strict";
@@ -1813,6 +1824,7 @@ var init_persistence = __esm({
1813
1824
  FILENAME = "slack-cache.json";
1814
1825
  FILE_VERSION = 1;
1815
1826
  SAVE_DEBOUNCE_MS = 500;
1827
+ MAX_USERS = 5e3;
1816
1828
  MAX_THREADS = 5e3;
1817
1829
  loaded = false;
1818
1830
  userMap = /* @__PURE__ */ new Map();
@@ -1936,6 +1948,58 @@ function getSlackAdapter() {
1936
1948
  function isSlackConfigured() {
1937
1949
  return readSlackConfig() !== void 0;
1938
1950
  }
1951
+ function getSlackBotToken() {
1952
+ return readSlackConfig()?.botToken ?? null;
1953
+ }
1954
+ async function fetchSlackUserInfo(userId) {
1955
+ const token = getSlackBotToken();
1956
+ if (!token) return null;
1957
+ try {
1958
+ const res = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, {
1959
+ headers: { Authorization: `Bearer ${token}` }
1960
+ });
1961
+ const data = await res.json().catch(() => ({}));
1962
+ if (!data?.ok) {
1963
+ console.warn(`[slack] users.info(${userId}) failed: ${data?.error || `HTTP ${res.status}`}`);
1964
+ return null;
1965
+ }
1966
+ const profile = data.user?.profile || {};
1967
+ const name = profile.display_name_normalized || profile.display_name || profile.real_name_normalized || profile.real_name || data.user?.real_name || data.user?.name || null;
1968
+ const realName = profile.real_name_normalized || profile.real_name || data.user?.real_name || null;
1969
+ const email = profile.email || null;
1970
+ return {
1971
+ name: name ? String(name) : null,
1972
+ realName: realName ? String(realName) : null,
1973
+ email: email ? String(email) : null
1974
+ };
1975
+ } catch (err) {
1976
+ console.warn(`[slack] users.info(${userId}) error:`, err?.message || err);
1977
+ return null;
1978
+ }
1979
+ }
1980
+ async function resolveSlackUserInfo(userId) {
1981
+ if (!userId) return null;
1982
+ const now = Date.now();
1983
+ const hit = getCachedUserName(userId);
1984
+ if (hit && hit.expiresAt > now) {
1985
+ return { name: hit.name, realName: hit.realName ?? null, email: hit.email ?? null };
1986
+ }
1987
+ const inflight = userInflight.get(userId);
1988
+ if (inflight) return inflight;
1989
+ const p = (async () => {
1990
+ const info = await fetchSlackUserInfo(userId);
1991
+ setCachedUserName(userId, {
1992
+ name: info?.name ?? null,
1993
+ realName: info?.realName ?? null,
1994
+ email: info?.email ?? null,
1995
+ expiresAt: now + (info?.name ? USER_TTL_MS : USER_FAIL_TTL_MS)
1996
+ });
1997
+ userInflight.delete(userId);
1998
+ return info;
1999
+ })();
2000
+ userInflight.set(userId, p);
2001
+ return p;
2002
+ }
1939
2003
  function threadCacheKey(channel, threadTs) {
1940
2004
  return `${channel}\u241F${threadTs}`;
1941
2005
  }
@@ -1946,7 +2010,7 @@ function noteBotPostedInThread(channel, threadTs) {
1946
2010
  expiresAt: Date.now() + THREAD_OWNED_TTL_MS
1947
2011
  });
1948
2012
  }
1949
- var RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, USER_TTL_MS, USER_FAIL_TTL_MS, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS;
2013
+ var RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS;
1950
2014
  var init_client2 = __esm({
1951
2015
  "src/integrations/slack/client.ts"() {
1952
2016
  "use strict";
@@ -1971,6 +2035,7 @@ var init_client2 = __esm({
1971
2035
  ]);
1972
2036
  USER_TTL_MS = 60 * 60 * 1e3;
1973
2037
  USER_FAIL_TTL_MS = 5 * 60 * 1e3;
2038
+ userInflight = /* @__PURE__ */ new Map();
1974
2039
  THREAD_OWNED_TTL_MS = 60 * 60 * 1e3;
1975
2040
  THREAD_NEG_TTL_MS = 5 * 60 * 1e3;
1976
2041
  }
@@ -2278,7 +2343,7 @@ async function reconcileOnce(now = Date.now()) {
2278
2343
  entry2.updatedAt = Date.now();
2279
2344
  const nudged = {
2280
2345
  ...entry2.event,
2281
- content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no reply, you can ignore it.]
2346
+ content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no substantive reply, post a brief acknowledgement.]
2282
2347
  ${entry2.event.content}`,
2283
2348
  wake: "now"
2284
2349
  };
@@ -2392,6 +2457,428 @@ var init_inbox_acks = __esm({
2392
2457
  }
2393
2458
  });
2394
2459
 
2460
+ // src/integrations/slack/files.ts
2461
+ var files_exports = {};
2462
+ __export(files_exports, {
2463
+ INGEST_TIMEOUT_MS: () => INGEST_TIMEOUT_MS,
2464
+ MAX_BYTES: () => MAX_BYTES,
2465
+ formatFileBlock: () => formatFileBlock,
2466
+ ingestSlackFiles: () => ingestSlackFiles
2467
+ });
2468
+ function inferFileName(file) {
2469
+ return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
2470
+ }
2471
+ function inferContentType(file) {
2472
+ if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
2473
+ return "application/octet-stream";
2474
+ }
2475
+ function formatBytes(n) {
2476
+ if (!Number.isFinite(n) || n <= 0) return "?";
2477
+ if (n < 1024) return `${n} B`;
2478
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
2479
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
2480
+ }
2481
+ function sleep(ms) {
2482
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
2483
+ }
2484
+ function imageMagic(bytes) {
2485
+ if (bytes.length < 12) return null;
2486
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
2487
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
2488
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
2489
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
2490
+ return null;
2491
+ }
2492
+ function requiresRasterMagic(contentType) {
2493
+ const normalized = contentType.toLowerCase().split(";", 1)[0].trim();
2494
+ return normalized === "image/jpeg" || normalized === "image/jpg" || normalized === "image/png" || normalized === "image/gif" || normalized === "image/webp";
2495
+ }
2496
+ function validateDownloadedBytes(bytes, declaredContentType) {
2497
+ if (!requiresRasterMagic(declaredContentType)) return null;
2498
+ const actual = imageMagic(bytes);
2499
+ if (!actual) return "invalid_image_bytes";
2500
+ return null;
2501
+ }
2502
+ async function fetchSlackPrivateFile(sourceUrl, botToken) {
2503
+ let lastError = "unknown";
2504
+ for (let attempt = 0; attempt < 3; attempt++) {
2505
+ try {
2506
+ const res = await fetch(sourceUrl, {
2507
+ headers: { Authorization: `Bearer ${botToken}` }
2508
+ });
2509
+ if (res.status === 429 || res.status >= 500) {
2510
+ lastError = `slack_fetch_${res.status}`;
2511
+ const retryAfter = Number(res.headers.get("retry-after"));
2512
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, 2e3) : 250 * (attempt + 1);
2513
+ if (attempt < 2) {
2514
+ await sleep(waitMs);
2515
+ continue;
2516
+ }
2517
+ }
2518
+ if (!res.ok) {
2519
+ return { ok: false, error: `slack_fetch_${res.status}` };
2520
+ }
2521
+ const contentLength = Number(res.headers.get("content-length"));
2522
+ if (Number.isFinite(contentLength) && contentLength > MAX_BYTES) {
2523
+ return { ok: false, error: "size_exceeded" };
2524
+ }
2525
+ const ab = await res.arrayBuffer();
2526
+ if (ab.byteLength > MAX_BYTES) {
2527
+ return { ok: false, error: "size_exceeded" };
2528
+ }
2529
+ return { ok: true, bytes: Buffer.from(ab) };
2530
+ } catch (err) {
2531
+ lastError = `slack_fetch_error:${err?.message || "unknown"}`;
2532
+ if (attempt < 2) {
2533
+ await sleep(250 * (attempt + 1));
2534
+ continue;
2535
+ }
2536
+ }
2537
+ }
2538
+ return { ok: false, error: lastError };
2539
+ }
2540
+ function withTimeout(p, ms, label) {
2541
+ return new Promise((resolve11, reject) => {
2542
+ const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
2543
+ p.then(
2544
+ (v) => {
2545
+ clearTimeout(t);
2546
+ resolve11(v);
2547
+ },
2548
+ (e) => {
2549
+ clearTimeout(t);
2550
+ reject(e);
2551
+ }
2552
+ );
2553
+ });
2554
+ }
2555
+ async function ingestOne(file, sessionId, botToken) {
2556
+ const fileName = inferFileName(file);
2557
+ const contentType = inferContentType(file);
2558
+ const declaredSize = typeof file.size === "number" ? file.size : 0;
2559
+ const base = {
2560
+ slackFileId: file.id,
2561
+ fileName,
2562
+ contentType,
2563
+ sizeBytes: declaredSize
2564
+ };
2565
+ const sourceUrl = file.url_private_download || file.url_private;
2566
+ if (!sourceUrl || typeof sourceUrl !== "string") {
2567
+ return { ...base, shortUrl: null, error: "no_source_url" };
2568
+ }
2569
+ if (declaredSize > MAX_BYTES) {
2570
+ return { ...base, shortUrl: null, error: "size_exceeded" };
2571
+ }
2572
+ let bytes;
2573
+ const fetched = await fetchSlackPrivateFile(sourceUrl, botToken);
2574
+ if (!fetched.ok) {
2575
+ return { ...base, shortUrl: null, error: fetched.error };
2576
+ }
2577
+ bytes = fetched.bytes;
2578
+ const byteError = validateDownloadedBytes(bytes, contentType);
2579
+ if (byteError) {
2580
+ console.warn(
2581
+ `[slack-files] refusing to upload ${fileName}: Slack metadata says ${contentType}, but downloaded bytes are not a supported image (${bytes.slice(0, 32).toString("utf8").replace(/\s+/g, " ").slice(0, 32)})`
2582
+ );
2583
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: byteError };
2584
+ }
2585
+ const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
2586
+ let upload;
2587
+ try {
2588
+ upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
2589
+ } catch (err) {
2590
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
2591
+ }
2592
+ try {
2593
+ const putRes = await fetch(upload.uploadUrl, {
2594
+ method: "PUT",
2595
+ headers: { "Content-Type": contentType },
2596
+ body: bytes
2597
+ });
2598
+ if (!putRes.ok) {
2599
+ return {
2600
+ ...base,
2601
+ sizeBytes: bytes.length,
2602
+ shortUrl: null,
2603
+ error: `gcs_put_${putRes.status}`
2604
+ };
2605
+ }
2606
+ } catch (err) {
2607
+ return {
2608
+ ...base,
2609
+ sizeBytes: bytes.length,
2610
+ shortUrl: null,
2611
+ error: `gcs_put_error:${err?.message || "unknown"}`
2612
+ };
2613
+ }
2614
+ try {
2615
+ await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
2616
+ } catch (err) {
2617
+ console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
2618
+ }
2619
+ const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
2620
+ // server somehow forgot to return it (older remote-server versions).
2621
+ inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
2622
+ return {
2623
+ ...base,
2624
+ sizeBytes: bytes.length,
2625
+ shortUrl
2626
+ };
2627
+ }
2628
+ function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
2629
+ try {
2630
+ const u = new URL(uploadUrl);
2631
+ if (u.hostname.endsWith(".googleapis.com")) return null;
2632
+ return `${u.origin}/f/${fileId}`;
2633
+ } catch {
2634
+ return null;
2635
+ }
2636
+ }
2637
+ async function ingestSlackFiles(files, sessionId, options = {}) {
2638
+ if (!Array.isArray(files) || files.length === 0) return [];
2639
+ const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
2640
+ if (!isRemoteConfigured2()) {
2641
+ console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
2642
+ return files.map((f) => ({
2643
+ slackFileId: f.id,
2644
+ fileName: inferFileName(f),
2645
+ contentType: inferContentType(f),
2646
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
2647
+ shortUrl: null,
2648
+ error: "storage_unconfigured"
2649
+ }));
2650
+ }
2651
+ const botToken = getSlackBotToken();
2652
+ if (!botToken) {
2653
+ console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
2654
+ return files.map((f) => ({
2655
+ slackFileId: f.id,
2656
+ fileName: inferFileName(f),
2657
+ contentType: inferContentType(f),
2658
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
2659
+ shortUrl: null,
2660
+ error: "no_bot_token"
2661
+ }));
2662
+ }
2663
+ const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
2664
+ const startedAt = Date.now();
2665
+ const pipeline = Promise.allSettled(
2666
+ files.map(
2667
+ (f) => withTimeout(ingestOne(f, sessionId, botToken), timeoutMs, `ingest:${f.id}`).catch((err) => {
2668
+ if (String(err?.message || err).includes("_timeout")) {
2669
+ return {
2670
+ slackFileId: f.id,
2671
+ fileName: inferFileName(f),
2672
+ contentType: inferContentType(f),
2673
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
2674
+ shortUrl: null,
2675
+ error: "timeout"
2676
+ };
2677
+ }
2678
+ throw err;
2679
+ })
2680
+ )
2681
+ );
2682
+ const settled = await pipeline;
2683
+ const results = settled.map((s, i) => {
2684
+ if (s.status === "fulfilled") return s.value;
2685
+ const f = files[i];
2686
+ return {
2687
+ slackFileId: f.id,
2688
+ fileName: inferFileName(f),
2689
+ contentType: inferContentType(f),
2690
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
2691
+ shortUrl: null,
2692
+ error: `unexpected:${s.reason?.message || String(s.reason)}`
2693
+ };
2694
+ });
2695
+ const okCount = results.filter((r) => r.shortUrl).length;
2696
+ console.log(
2697
+ `[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
2698
+ );
2699
+ return results;
2700
+ }
2701
+ function formatFileBlock(files) {
2702
+ if (!files || files.length === 0) return "";
2703
+ const lines = ["[files]"];
2704
+ for (const f of files) {
2705
+ const sizeLabel = formatBytes(f.sizeBytes);
2706
+ if (f.shortUrl) {
2707
+ lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
2708
+ } else {
2709
+ lines.push(
2710
+ ` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
2711
+ );
2712
+ }
2713
+ }
2714
+ return lines.join("\n");
2715
+ }
2716
+ var MAX_BYTES, INGEST_TIMEOUT_MS;
2717
+ var init_files = __esm({
2718
+ "src/integrations/slack/files.ts"() {
2719
+ "use strict";
2720
+ init_client2();
2721
+ MAX_BYTES = 100 * 1024 * 1024;
2722
+ INGEST_TIMEOUT_MS = 2500;
2723
+ }
2724
+ });
2725
+
2726
+ // src/integrations/slack/read.ts
2727
+ var read_exports = {};
2728
+ __export(read_exports, {
2729
+ findChannelByName: () => findChannelByName,
2730
+ findUsers: () => findUsers,
2731
+ getChannelHistory: () => getChannelHistory,
2732
+ getPermalink: () => getPermalink,
2733
+ getThreadReplies: () => getThreadReplies,
2734
+ ingestMessageFiles: () => ingestMessageFiles
2735
+ });
2736
+ async function slackGet(method, params) {
2737
+ const token = getSlackBotToken();
2738
+ if (!token) return { ok: false, error: "slack_not_configured" };
2739
+ try {
2740
+ const qs = new URLSearchParams(params).toString();
2741
+ const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
2742
+ headers: { Authorization: `Bearer ${token}` }
2743
+ });
2744
+ const json = await res.json().catch(() => ({}));
2745
+ if (!json?.ok) return { ok: false, error: json?.error || `HTTP ${res.status}` };
2746
+ return { ok: true, json };
2747
+ } catch (err) {
2748
+ return { ok: false, error: err?.message || "unknown" };
2749
+ }
2750
+ }
2751
+ function clampLimit(n, def, max) {
2752
+ const v = typeof n === "number" && Number.isFinite(n) ? n : def;
2753
+ return String(Math.min(Math.max(Math.floor(v), 1), max));
2754
+ }
2755
+ function liteMessage(m) {
2756
+ return {
2757
+ ts: String(m?.ts ?? ""),
2758
+ threadTs: typeof m?.thread_ts === "string" ? m.thread_ts : void 0,
2759
+ user: typeof m?.user === "string" ? m.user : void 0,
2760
+ botId: typeof m?.bot_id === "string" ? m.bot_id : void 0,
2761
+ text: typeof m?.text === "string" ? m.text.slice(0, 4e3) : "",
2762
+ files: Array.isArray(m?.files) ? m.files.map((f) => ({ id: String(f?.id ?? ""), name: f?.name || f?.title, mimetype: f?.mimetype, size: typeof f?.size === "number" ? f.size : void 0 })) : void 0,
2763
+ replyCount: typeof m?.reply_count === "number" ? m.reply_count : void 0
2764
+ };
2765
+ }
2766
+ async function enrichUserNames(messages) {
2767
+ const ids = [...new Set(messages.map((m) => m.user).filter((u) => !!u))];
2768
+ const names = /* @__PURE__ */ new Map();
2769
+ await Promise.all(
2770
+ ids.map(async (id) => {
2771
+ const info = await resolveSlackUserInfo(id).catch(() => null);
2772
+ if (info?.name) names.set(id, info.name);
2773
+ })
2774
+ );
2775
+ return messages.map((m) => m.user && names.has(m.user) ? { ...m, userName: names.get(m.user) } : m);
2776
+ }
2777
+ async function getChannelHistory(channel, limit) {
2778
+ const r = await slackGet("conversations.history", { channel, limit: clampLimit(limit, 20, 100) });
2779
+ if (!r.ok) return { ok: false, error: r.error };
2780
+ const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
2781
+ return { ok: true, data: await enrichUserNames(messages) };
2782
+ }
2783
+ async function getThreadReplies(channel, threadTs, limit) {
2784
+ const r = await slackGet("conversations.replies", { channel, ts: threadTs, limit: clampLimit(limit, 50, 200) });
2785
+ if (!r.ok) return { ok: false, error: r.error };
2786
+ const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
2787
+ return { ok: true, data: await enrichUserNames(messages) };
2788
+ }
2789
+ async function getPermalink(channel, messageTs) {
2790
+ const r = await slackGet("chat.getPermalink", { channel, message_ts: messageTs });
2791
+ if (!r.ok) return { ok: false, error: r.error };
2792
+ return { ok: true, data: { permalink: String(r.json.permalink ?? "") } };
2793
+ }
2794
+ async function findChannelByName(name) {
2795
+ const clean = name.replace(/^#/, "").trim().toLowerCase();
2796
+ let cursor;
2797
+ for (let page = 0; page < 10; page++) {
2798
+ const params = {
2799
+ types: "public_channel,private_channel",
2800
+ exclude_archived: "true",
2801
+ limit: "999"
2802
+ };
2803
+ if (cursor) params.cursor = cursor;
2804
+ const r = await slackGet("conversations.list", params);
2805
+ if (!r.ok) return { ok: false, error: r.error };
2806
+ const match = (r.json.channels || []).find((c) => String(c?.name ?? "").toLowerCase() === clean);
2807
+ if (match) return { ok: true, data: { id: String(match.id), name: String(match.name) } };
2808
+ cursor = r.json.response_metadata?.next_cursor || "";
2809
+ if (!cursor) break;
2810
+ }
2811
+ return { ok: false, error: "channel_not_found" };
2812
+ }
2813
+ async function findUsers(query, max = 10) {
2814
+ const q = query.trim().toLowerCase();
2815
+ if (!q) return { ok: false, error: "empty_query" };
2816
+ if (q.includes("@")) {
2817
+ const r = await slackGet("users.lookupByEmail", { email: query.trim() });
2818
+ if (r.ok && r.json.user) {
2819
+ const u = r.json.user;
2820
+ return { ok: true, data: [{ id: String(u.id), name: u.profile?.display_name || u.real_name || u.name, realName: u.real_name, email: u.profile?.email }] };
2821
+ }
2822
+ if (r.error && !["users_not_found", "user_not_found", "not_found"].includes(r.error)) {
2823
+ return { ok: false, error: r.error };
2824
+ }
2825
+ }
2826
+ const matches = [];
2827
+ let cursor;
2828
+ for (let page = 0; page < 10 && matches.length < max; page++) {
2829
+ const params = { limit: "1000" };
2830
+ if (cursor) params.cursor = cursor;
2831
+ const r = await slackGet("users.list", params);
2832
+ if (!r.ok) return { ok: false, error: r.error };
2833
+ for (const u of r.json.members || []) {
2834
+ if (u?.deleted || u?.is_bot) continue;
2835
+ const dn = String(u?.profile?.display_name_normalized || u?.profile?.real_name || u?.name || "");
2836
+ const email = String(u?.profile?.email || "");
2837
+ if (dn.toLowerCase().includes(q) || email.toLowerCase().includes(q)) {
2838
+ matches.push({ id: String(u.id), name: dn || String(u.name), realName: u?.profile?.real_name, email: email || void 0 });
2839
+ if (matches.length >= max) break;
2840
+ }
2841
+ }
2842
+ cursor = r.json.response_metadata?.next_cursor || "";
2843
+ if (!cursor) break;
2844
+ }
2845
+ return matches.length > 0 ? { ok: true, data: matches } : { ok: false, error: "no_match" };
2846
+ }
2847
+ async function getSingleMessage(channel, ts, threadTs) {
2848
+ if (threadTs) {
2849
+ const rr2 = await slackGet("conversations.replies", { channel, ts: threadTs, limit: "200" });
2850
+ if (rr2.ok && Array.isArray(rr2.json.messages)) {
2851
+ const match = rr2.json.messages.find((m) => m?.ts === ts);
2852
+ if (match) return match;
2853
+ }
2854
+ }
2855
+ const r = await slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: "true", limit: "1" });
2856
+ if (r.ok && Array.isArray(r.json.messages) && r.json.messages[0]) return r.json.messages[0];
2857
+ const rr = await slackGet("conversations.replies", { channel, ts, limit: "1" });
2858
+ if (rr.ok && Array.isArray(rr.json.messages)) {
2859
+ return rr.json.messages.find((m) => m?.ts === ts) || rr.json.messages[0] || null;
2860
+ }
2861
+ return null;
2862
+ }
2863
+ async function ingestMessageFiles(channel, ts, orchestratorSessionId, threadTs) {
2864
+ const msg = await getSingleMessage(channel, ts, threadTs);
2865
+ if (!msg) return { ok: false, error: "message_not_found" };
2866
+ const files = Array.isArray(msg.files) ? msg.files : [];
2867
+ if (files.length === 0) return { ok: true, data: [] };
2868
+ const { ingestSlackFiles: ingestSlackFiles2 } = await Promise.resolve().then(() => (init_files(), files_exports));
2869
+ const ingested = await ingestSlackFiles2(files, orchestratorSessionId);
2870
+ return {
2871
+ ok: true,
2872
+ data: ingested.map((f) => ({ name: f.fileName, url: f.shortUrl, error: f.error }))
2873
+ };
2874
+ }
2875
+ var init_read = __esm({
2876
+ "src/integrations/slack/read.ts"() {
2877
+ "use strict";
2878
+ init_client2();
2879
+ }
2880
+ });
2881
+
2395
2882
  // src/browser/stream-proxy.ts
2396
2883
  var stream_proxy_exports = {};
2397
2884
  __export(stream_proxy_exports, {
@@ -7734,6 +8221,99 @@ function capImageCount(messages, max = MAX_IMAGES_IN_CONTEXT) {
7734
8221
  return mutated ? out : messages;
7735
8222
  }
7736
8223
 
8224
+ // src/utils/sanitize-images.ts
8225
+ var SUPPORTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
8226
+ var INVALID_IMAGE_PLACEHOLDER = "[invalid image omitted \u2014 the data was not a valid jpeg/png/gif/webp]";
8227
+ function hasImageMagic(bytes) {
8228
+ if (bytes.length < 12) return false;
8229
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return true;
8230
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return true;
8231
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return true;
8232
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return true;
8233
+ return false;
8234
+ }
8235
+ function extractBase64(part) {
8236
+ const raw = typeof part?.data === "string" ? part.data : typeof part?.image === "string" ? part.image : null;
8237
+ if (typeof raw !== "string" || raw.length === 0) return null;
8238
+ if (/^https?:\/\//i.test(raw)) return null;
8239
+ const dataUrl = raw.match(/^data:[^;,]+;base64,([\s\S]*)$/);
8240
+ return dataUrl ? dataUrl[1] : raw;
8241
+ }
8242
+ function isInvalidImagePart(part) {
8243
+ if (!part || typeof part !== "object") return false;
8244
+ const t = part.type;
8245
+ if (t !== "image" && t !== "image-data" && t !== "media") return false;
8246
+ const mt = part.mediaType;
8247
+ const b64 = extractBase64(part);
8248
+ if (b64 === null) {
8249
+ return typeof mt === "string" && mt.startsWith("image/") && !SUPPORTED_IMAGE_TYPES.includes(mt);
8250
+ }
8251
+ let bytes;
8252
+ try {
8253
+ bytes = Buffer.from(b64, "base64");
8254
+ } catch {
8255
+ return true;
8256
+ }
8257
+ if (bytes.length === 0) return true;
8258
+ return !hasImageMagic(bytes);
8259
+ }
8260
+ function placeholder() {
8261
+ return { type: "text", text: INVALID_IMAGE_PLACEHOLDER };
8262
+ }
8263
+ function sanitizeInvalidImages(messages) {
8264
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
8265
+ let mutated = false;
8266
+ let dropped = 0;
8267
+ const out = messages.slice();
8268
+ for (let i = 0; i < out.length; i++) {
8269
+ const msg = out[i];
8270
+ if (!Array.isArray(msg.content)) continue;
8271
+ let contentCloned = false;
8272
+ const ensureCloned = () => {
8273
+ if (contentCloned) return;
8274
+ out[i] = { ...msg, content: [...msg.content] };
8275
+ contentCloned = true;
8276
+ };
8277
+ const content = () => out[i].content;
8278
+ for (let j = 0; j < content().length; j++) {
8279
+ const part = content()[j];
8280
+ if (isInvalidImagePart(part)) {
8281
+ ensureCloned();
8282
+ out[i].content[j] = placeholder();
8283
+ mutated = true;
8284
+ dropped++;
8285
+ continue;
8286
+ }
8287
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
8288
+ const innerValue = part.output.value;
8289
+ let innerMutated = false;
8290
+ const newValue = innerValue.slice();
8291
+ for (let k = 0; k < newValue.length; k++) {
8292
+ if (isInvalidImagePart(newValue[k])) {
8293
+ newValue[k] = placeholder();
8294
+ innerMutated = true;
8295
+ dropped++;
8296
+ }
8297
+ }
8298
+ if (innerMutated) {
8299
+ ensureCloned();
8300
+ out[i].content[j] = {
8301
+ ...part,
8302
+ output: { ...part.output, value: newValue }
8303
+ };
8304
+ mutated = true;
8305
+ }
8306
+ }
8307
+ }
8308
+ }
8309
+ if (mutated) {
8310
+ console.warn(
8311
+ `[sanitize-images] Replaced ${dropped} invalid image part(s) with a text placeholder (non-image bytes / unsupported type) to avoid provider 400 "invalid image" errors.`
8312
+ );
8313
+ }
8314
+ return mutated ? out : messages;
8315
+ }
8316
+
7737
8317
  // src/agent/model-limits.ts
7738
8318
  var MODEL_LIMITS = {
7739
8319
  "claude-opus-4-8": { contextWindow: 2e5, rollingTarget: 15e4 },
@@ -7813,6 +8393,7 @@ ${summaryContent}`
7813
8393
  messages = repairToolPairing(messages);
7814
8394
  messages = ensureToolResultsFollowCalls(messages);
7815
8395
  messages = ensureEndsWithUserOrTool(messages);
8396
+ messages = sanitizeInvalidImages(messages);
7816
8397
  messages = capImageCount(messages);
7817
8398
  messages = this.enforceHardCap(messages);
7818
8399
  return messages;
@@ -7824,28 +8405,35 @@ ${summaryContent}`
7824
8405
  * repairs tool pairing so dropping can't orphan a tool result.
7825
8406
  */
7826
8407
  enforceHardCap(messages) {
7827
- const { contextWindow } = getModelLimits(this.modelId);
7828
- const ceiling = Math.max(2e4, Math.floor(contextWindow * 0.9));
8408
+ const { rollingTarget } = getModelLimits(this.modelId);
8409
+ const MAX_MESSAGES = 120;
8410
+ const tokenCeiling = Math.max(2e4, Math.floor(rollingTarget * 1.5));
7829
8411
  const tokens = messages.map((m) => this.messageTokens(m));
7830
8412
  let total = tokens.reduce((a, b) => a + b, 0);
7831
- if (total <= ceiling) return messages;
7832
8413
  const hasLeadingSummary = messages.length > 0 && messages[0].role === "system";
7833
8414
  const firstBody = hasLeadingSummary ? 1 : 0;
7834
8415
  const lastIndex = messages.length - 1;
8416
+ const bodyCount = messages.length - firstBody;
8417
+ if (bodyCount <= MAX_MESSAGES && total <= tokenCeiling) return messages;
7835
8418
  let start = firstBody;
7836
- while (start < lastIndex && total > ceiling) {
8419
+ const countDropTarget = messages.length - MAX_MESSAGES;
8420
+ while (start < countDropTarget && start < lastIndex) {
8421
+ total -= tokens[start];
8422
+ start += 1;
8423
+ }
8424
+ while (start < lastIndex && total > tokenCeiling) {
7837
8425
  total -= tokens[start];
7838
8426
  start += 1;
7839
8427
  }
7840
8428
  let out = hasLeadingSummary ? [messages[0], ...messages.slice(start)] : messages.slice(start);
7841
- if (total > ceiling) {
8429
+ if (total > tokenCeiling) {
7842
8430
  out = out.map((m) => hardTruncateMessageText(m));
7843
8431
  }
7844
8432
  out = repairToolPairing(out);
7845
8433
  out = ensureToolResultsFollowCalls(out);
7846
8434
  out = ensureEndsWithUserOrTool(out);
7847
8435
  console.warn(
7848
- `[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${ceiling} tokens).`
8436
+ `[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${tokenCeiling} est-tokens / ${MAX_MESSAGES} msgs).`
7849
8437
  );
7850
8438
  return out;
7851
8439
  }
@@ -8427,7 +9015,16 @@ async function postMessage(opts) {
8427
9015
  let ref;
8428
9016
  switch (opts.channel) {
8429
9017
  case "slack": {
8430
- const slackRef = { channel: "slack", slackChannel: opts.to, threadTs: opts.threadTs };
9018
+ let slackChannel2 = opts.to.trim();
9019
+ if (slackChannel2.startsWith("#")) {
9020
+ const { findChannelByName: findChannelByName2 } = await Promise.resolve().then(() => (init_read(), read_exports));
9021
+ const found = await findChannelByName2(slackChannel2);
9022
+ if (!found.ok || !found.data?.id) {
9023
+ return { ok: false, error: `slack channel lookup failed: ${found.error || "channel_not_found"}` };
9024
+ }
9025
+ slackChannel2 = found.data.id;
9026
+ }
9027
+ const slackRef = { channel: "slack", slackChannel: slackChannel2, threadTs: opts.threadTs };
8431
9028
  ref = slackRef;
8432
9029
  break;
8433
9030
  }
@@ -8449,6 +9046,10 @@ function describeConfiguredChannels() {
8449
9046
  }));
8450
9047
  }
8451
9048
 
9049
+ // src/tools/orchestrator-actions.ts
9050
+ init_read();
9051
+ init_client2();
9052
+
8452
9053
  // src/orchestrator/schedules-store.ts
8453
9054
  init_db();
8454
9055
  import { nanoid as nanoid5 } from "nanoid";
@@ -8757,6 +9358,55 @@ function buildMessengerTool() {
8757
9358
  }
8758
9359
  });
8759
9360
  }
9361
+ var slackInputSchema = z14.object({
9362
+ action: z14.enum(["history", "replies", "permalink", "find_channel", "find_user", "user_info", "fetch_files"]),
9363
+ channel: z14.string().optional().describe("channel id (C0123/G0123/D0123). Required for history/replies/permalink/fetch_files."),
9364
+ threadTs: z14.string().optional().describe("replies/fetch_files: parent message ts of the thread."),
9365
+ messageTs: z14.string().optional().describe("permalink/fetch_files: ts of the target message; for thread-reply files also pass threadTs."),
9366
+ query: z14.string().optional().describe("find_channel: channel name (no #). find_user: name or email."),
9367
+ user: z14.string().optional().describe("user_info: user id (U0123)."),
9368
+ limit: z14.number().optional().describe("history/replies: max messages (history \u2264100, replies \u2264200).")
9369
+ });
9370
+ function buildSlackTool(opts) {
9371
+ return tool13({
9372
+ description: "Read from Slack like a human would. Actions: history (channel[, limit] \u2192 recent messages, newest first, with sender names + file metadata), replies (channel, threadTs[, limit] \u2192 full thread), permalink (channel, messageTs \u2192 shareable link), find_channel (query=name \u2192 channel id), find_user (query=name|email \u2192 matching members), user_info (user=U0123 \u2192 name/email), fetch_files (channel, messageTs[, threadTs] \u2192 re-uploads that message's attachments and returns fetchable URLs you can curl). Use this to scroll back, read context, and open files others posted before replying.",
9373
+ inputSchema: slackInputSchema,
9374
+ execute: async (input) => {
9375
+ if (!isSlackConfigured()) return { ok: false, error: "slack not configured" };
9376
+ switch (input.action) {
9377
+ case "history": {
9378
+ if (!input.channel) return { ok: false, error: "channel required" };
9379
+ return getChannelHistory(input.channel, input.limit);
9380
+ }
9381
+ case "replies": {
9382
+ if (!input.channel || !input.threadTs) return { ok: false, error: "channel and threadTs required" };
9383
+ return getThreadReplies(input.channel, input.threadTs, input.limit);
9384
+ }
9385
+ case "permalink": {
9386
+ if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
9387
+ return getPermalink(input.channel, input.messageTs);
9388
+ }
9389
+ case "find_channel": {
9390
+ if (!input.query) return { ok: false, error: "query (channel name) required" };
9391
+ return findChannelByName(input.query);
9392
+ }
9393
+ case "find_user": {
9394
+ if (!input.query) return { ok: false, error: "query (name or email) required" };
9395
+ return findUsers(input.query);
9396
+ }
9397
+ case "user_info": {
9398
+ if (!input.user) return { ok: false, error: "user required" };
9399
+ const info = await resolveSlackUserInfo(input.user);
9400
+ return info ? { ok: true, data: { id: input.user, ...info } } : { ok: false, error: "not_found" };
9401
+ }
9402
+ case "fetch_files": {
9403
+ if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
9404
+ return ingestMessageFiles(input.channel, input.messageTs, opts.orchestratorSessionId, input.threadTs);
9405
+ }
9406
+ }
9407
+ }
9408
+ });
9409
+ }
8760
9410
  var scheduleInputSchema = z14.object({
8761
9411
  action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
8762
9412
  // create / update
@@ -8857,6 +9507,7 @@ function createOrchestratorActionTools(opts) {
8857
9507
  return {
8858
9508
  agent: buildAgentTool(opts),
8859
9509
  messenger: buildMessengerTool(),
9510
+ slack: buildSlackTool(opts),
8860
9511
  schedule: buildScheduleTool(opts),
8861
9512
  webhook: buildWebhookTool(opts)
8862
9513
  };