sparkecoder 0.1.130 → 0.1.132

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 (216) hide show
  1. package/README.md +3 -3
  2. package/dist/agent/index.d.ts +3 -3
  3. package/dist/agent/index.js +1480 -638
  4. package/dist/agent/index.js.map +1 -1
  5. package/dist/cli.js +2281 -808
  6. package/dist/cli.js.map +1 -1
  7. package/dist/db/index.d.ts +2 -2
  8. package/dist/db/index.js.map +1 -1
  9. package/dist/{index-Bcz0aCAR.d.ts → index-BM99kjgq.d.ts} +177 -103
  10. package/dist/index.d.ts +5 -5
  11. package/dist/index.js +2215 -780
  12. package/dist/index.js.map +1 -1
  13. package/dist/{schema-BWbWmfDQ.d.ts → schema-Dz-wABVY.d.ts} +27 -4
  14. package/dist/{search-DOzC4ojH.d.ts → search-CVVfuBPZ.d.ts} +4 -4
  15. package/dist/server/index.js +2215 -780
  16. package/dist/server/index.js.map +1 -1
  17. package/dist/skills/default/build-context-and-solve-issue.md +74 -0
  18. package/dist/skills/default/doublecheck.md +95 -0
  19. package/dist/tools/index.d.ts +3 -3
  20. package/dist/tools/index.js +11 -2
  21. package/dist/tools/index.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/skills/default/build-context-and-solve-issue.md +74 -0
  24. package/src/skills/default/doublecheck.md +95 -0
  25. package/web/.next/BUILD_ID +1 -1
  26. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  27. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  28. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  29. package/web/.next/standalone/web/.next/server/app/(main)/agents/page/next-font-manifest.json +1 -1
  30. package/web/.next/standalone/web/.next/server/app/(main)/agents/page_client-reference-manifest.js +1 -1
  31. package/web/.next/standalone/web/.next/server/app/(main)/page/next-font-manifest.json +1 -1
  32. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  33. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page/next-font-manifest.json +1 -1
  34. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  35. package/web/.next/standalone/web/.next/server/app/(main)/settings/page/next-font-manifest.json +1 -1
  36. package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
  37. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  38. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/_not-found/page/next-font-manifest.json +1 -1
  45. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  46. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  47. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +3 -3
  48. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  49. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  51. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  54. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  55. package/web/.next/standalone/web/.next/server/app/agents.rsc +6 -6
  56. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +2 -2
  57. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +2 -2
  59. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +6 -6
  60. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +3 -3
  62. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +3 -3
  63. package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
  64. package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/installation/page/next-font-manifest.json +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/installation.html +4 -4
  68. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +6 -6
  69. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +6 -6
  70. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +3 -3
  72. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
  73. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +20 -21
  74. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +2 -2
  76. package/web/.next/standalone/web/.next/server/app/docs/page/next-font-manifest.json +1 -1
  77. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  78. package/web/.next/standalone/web/.next/server/app/docs/skills/page/next-font-manifest.json +1 -1
  79. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
  80. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  81. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +4 -4
  82. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +4 -4
  83. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +3 -3
  85. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
  86. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +2 -2
  89. package/web/.next/standalone/web/.next/server/app/docs/tools/page/next-font-manifest.json +1 -1
  90. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
  91. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  92. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +4 -4
  93. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +4 -4
  94. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  95. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +3 -3
  96. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
  97. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +2 -2
  98. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  99. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +2 -2
  100. package/web/.next/standalone/web/.next/server/app/docs.html +3 -3
  101. package/web/.next/standalone/web/.next/server/app/docs.rsc +5 -5
  102. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +5 -5
  103. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  104. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +3 -3
  105. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
  106. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +3 -3
  107. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +2 -2
  108. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  109. package/web/.next/standalone/web/.next/server/app/index.rsc +6 -6
  110. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  111. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
  112. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +6 -6
  113. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  114. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
  115. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +3 -3
  116. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  117. package/web/.next/standalone/web/.next/server/app/settings.rsc +6 -6
  118. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
  119. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  120. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +2 -2
  121. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +6 -6
  122. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  123. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +3 -3
  124. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +3 -3
  125. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__36edac7c._.js +1 -1
  126. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__397fadd4._.js +1 -1
  127. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__70cecda8._.js +1 -1
  128. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__be5e2967._.js +1 -1
  129. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_layout_tsx_453f6492._.js +1 -1
  130. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_page_tsx_5ac4794b._.js +1 -1
  131. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +2 -2
  132. package/web/.next/standalone/web/.next/server/next-font-manifest.js +1 -1
  133. package/web/.next/standalone/web/.next/server/next-font-manifest.json +9 -9
  134. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  135. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  136. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  137. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  138. package/web/.next/standalone/web/.next/static/chunks/185f69f6478ba713.js +1 -0
  139. package/web/.next/standalone/web/.next/static/chunks/20ca4e35e9bb3e94.js +3 -0
  140. package/web/.next/standalone/web/.next/static/chunks/{a7d5d0791c8c6223.css → 34d933785a17edf3.css} +1 -1
  141. package/web/.next/standalone/web/.next/static/chunks/7549a5b7c7f6786e.js +1 -0
  142. package/web/.next/standalone/web/.next/static/{static/chunks/c5dd884b71007965.js → chunks/a839c83078c56476.js} +1 -1
  143. package/web/.next/standalone/web/.next/static/media/4fa387ec64143e14-s.3b336396.woff2 +0 -0
  144. package/web/.next/standalone/web/.next/static/media/5ce348bf30bf5439-s.56c1f21e.woff2 +0 -0
  145. package/web/.next/standalone/web/.next/static/media/6306c77e7c8268e4-s.e3369375.woff2 +0 -0
  146. package/web/.next/standalone/web/.next/static/media/797e433ab948586e-s.p.29207c2f.woff2 +0 -0
  147. package/web/.next/standalone/web/.next/static/media/7d817b4c03b0c5f1-s.a40b9a8b.woff2 +0 -0
  148. package/web/.next/standalone/web/.next/static/media/bbc41e54d2fcbd21-s.fe42ddf4.woff2 +0 -0
  149. package/web/.next/standalone/web/.next/static/static/chunks/185f69f6478ba713.js +1 -0
  150. package/web/.next/standalone/web/.next/static/static/chunks/20ca4e35e9bb3e94.js +3 -0
  151. package/web/.next/standalone/web/.next/static/static/chunks/{a7d5d0791c8c6223.css → 34d933785a17edf3.css} +1 -1
  152. package/web/.next/standalone/web/.next/static/static/chunks/7549a5b7c7f6786e.js +1 -0
  153. package/web/.next/{static/chunks/c5dd884b71007965.js → standalone/web/.next/static/static/chunks/a839c83078c56476.js} +1 -1
  154. package/web/.next/standalone/web/.next/static/static/media/4fa387ec64143e14-s.3b336396.woff2 +0 -0
  155. package/web/.next/standalone/web/.next/static/static/media/5ce348bf30bf5439-s.56c1f21e.woff2 +0 -0
  156. package/web/.next/standalone/web/.next/static/static/media/6306c77e7c8268e4-s.e3369375.woff2 +0 -0
  157. package/web/.next/standalone/web/.next/static/static/media/797e433ab948586e-s.p.29207c2f.woff2 +0 -0
  158. package/web/.next/standalone/web/.next/static/static/media/7d817b4c03b0c5f1-s.a40b9a8b.woff2 +0 -0
  159. package/web/.next/standalone/web/.next/static/static/media/bbc41e54d2fcbd21-s.fe42ddf4.woff2 +0 -0
  160. package/web/.next/standalone/web/package-lock.json +21 -21
  161. package/web/.next/standalone/web/runtime-config.json +2 -1
  162. package/web/.next/standalone/web/src/app/(main)/page.tsx +2 -2
  163. package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +111 -11
  164. package/web/.next/standalone/web/src/app/__sfapi/[...path]/route.ts +96 -0
  165. package/web/.next/standalone/web/src/app/api/config/route.ts +5 -12
  166. package/web/.next/standalone/web/src/app/docs/installation/page.mdx +2 -2
  167. package/web/.next/standalone/web/src/app/docs/page.mdx +1 -1
  168. package/web/.next/standalone/web/src/components/sessions-sidebar.tsx +1 -1
  169. package/web/.next/standalone/web/src/lib/config.ts +26 -16
  170. package/web/.next/static/chunks/185f69f6478ba713.js +1 -0
  171. package/web/.next/static/chunks/20ca4e35e9bb3e94.js +3 -0
  172. package/web/.next/static/chunks/{a7d5d0791c8c6223.css → 34d933785a17edf3.css} +1 -1
  173. package/web/.next/static/chunks/7549a5b7c7f6786e.js +1 -0
  174. package/web/.next/{standalone/web/.next/static/chunks/c5dd884b71007965.js → static/chunks/a839c83078c56476.js} +1 -1
  175. package/web/.next/static/media/4fa387ec64143e14-s.3b336396.woff2 +0 -0
  176. package/web/.next/static/media/5ce348bf30bf5439-s.56c1f21e.woff2 +0 -0
  177. package/web/.next/static/media/6306c77e7c8268e4-s.e3369375.woff2 +0 -0
  178. package/web/.next/static/media/797e433ab948586e-s.p.29207c2f.woff2 +0 -0
  179. package/web/.next/static/media/7d817b4c03b0c5f1-s.a40b9a8b.woff2 +0 -0
  180. package/web/.next/static/media/bbc41e54d2fcbd21-s.fe42ddf4.woff2 +0 -0
  181. package/web/.next/standalone/web/.next/static/chunks/9b88f148788e4504.js +0 -3
  182. package/web/.next/standalone/web/.next/static/chunks/b203b9aa975135d3.js +0 -1
  183. package/web/.next/standalone/web/.next/static/chunks/ea89ca7892d8c557.js +0 -1
  184. package/web/.next/standalone/web/.next/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
  185. package/web/.next/standalone/web/.next/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
  186. package/web/.next/standalone/web/.next/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
  187. package/web/.next/standalone/web/.next/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
  188. package/web/.next/standalone/web/.next/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
  189. package/web/.next/standalone/web/.next/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
  190. package/web/.next/standalone/web/.next/static/static/chunks/9b88f148788e4504.js +0 -3
  191. package/web/.next/standalone/web/.next/static/static/chunks/b203b9aa975135d3.js +0 -1
  192. package/web/.next/standalone/web/.next/static/static/chunks/ea89ca7892d8c557.js +0 -1
  193. package/web/.next/standalone/web/.next/static/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
  194. package/web/.next/standalone/web/.next/static/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
  195. package/web/.next/standalone/web/.next/static/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
  196. package/web/.next/standalone/web/.next/static/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
  197. package/web/.next/standalone/web/.next/static/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
  198. package/web/.next/standalone/web/.next/static/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
  199. package/web/.next/static/chunks/9b88f148788e4504.js +0 -3
  200. package/web/.next/static/chunks/b203b9aa975135d3.js +0 -1
  201. package/web/.next/static/chunks/ea89ca7892d8c557.js +0 -1
  202. package/web/.next/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
  203. package/web/.next/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
  204. package/web/.next/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
  205. package/web/.next/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
  206. package/web/.next/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
  207. package/web/.next/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
  208. /package/web/.next/standalone/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_buildManifest.js +0 -0
  209. /package/web/.next/standalone/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_clientMiddlewareManifest.json +0 -0
  210. /package/web/.next/standalone/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_ssgManifest.js +0 -0
  211. /package/web/.next/standalone/web/.next/static/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_buildManifest.js +0 -0
  212. /package/web/.next/standalone/web/.next/static/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_clientMiddlewareManifest.json +0 -0
  213. /package/web/.next/standalone/web/.next/static/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_ssgManifest.js +0 -0
  214. /package/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_buildManifest.js +0 -0
  215. /package/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_clientMiddlewareManifest.json +0 -0
  216. /package/web/.next/static/{2mUQ8I-TCRE5uOBFhIWag → WaAcu3X3K00MDvfn1ik7H}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -305,8 +305,8 @@ var init_types = __esm({
305
305
  authKey: z.string().optional()
306
306
  }).optional();
307
307
  SparkcoderConfigSchema = z.object({
308
- // Default model to use (Vercel AI Gateway format)
309
- defaultModel: z.string().default("anthropic/claude-opus-4.7"),
308
+ // Default model to use (LiteLLM model id)
309
+ defaultModel: z.string().default("gpt-5.5"),
310
310
  // Working directory for file operations
311
311
  workingDirectory: z.string().optional(),
312
312
  // Tool approval settings
@@ -345,6 +345,14 @@ var init_types = __esm({
345
345
  webhooks: z.object({
346
346
  token: z.string().optional()
347
347
  }).optional(),
348
+ // Self-update: when running as the managed service, periodically check
349
+ // npm for a newer published version and, if found, re-run the hosted
350
+ // installer (full upgrade + restart). Disabled automatically when not
351
+ // running from a global install (e.g. dev/source checkouts).
352
+ autoUpdate: z.object({
353
+ enabled: z.boolean().optional().default(true),
354
+ intervalHours: z.number().positive().optional().default(6)
355
+ }).optional().default({}),
348
356
  // Database path (used for local SQLite - ignored if remoteServer is configured)
349
357
  databasePath: z.string().optional().default("./sparkecoder.db"),
350
358
  // Remote server configuration (for centralized storage)
@@ -452,6 +460,7 @@ __export(config_exports, {
452
460
  requiresApproval: () => requiresApproval,
453
461
  saveAuthKey: () => saveAuthKey,
454
462
  setApiKey: () => setApiKey,
463
+ setCfAccessConfig: () => setCfAccessConfig,
455
464
  setMcpServers: () => setMcpServers,
456
465
  setPublicUrl: () => setPublicUrl,
457
466
  setSkillsAdditionalDirectories: () => setSkillsAdditionalDirectories,
@@ -640,12 +649,12 @@ function loadConfig(configPath, workingDirectory) {
640
649
  ]
641
650
  };
642
651
  const DEFAULT_REMOTE_URL = "https://agent-remote-server.sparkecode.com";
643
- const remoteUrl = process.env.SPARKECODER_REMOTE_URL || config.remoteServer?.url || DEFAULT_REMOTE_URL;
652
+ const remoteUrl2 = process.env.SPARKECODER_REMOTE_URL || config.remoteServer?.url || DEFAULT_REMOTE_URL;
644
653
  const remoteAuthKey = process.env.SPARKECODER_AUTH_KEY || config.remoteServer?.authKey || loadStoredAuthKey();
645
654
  const resolvedRemoteServer = {
646
- url: remoteUrl,
655
+ url: remoteUrl2,
647
656
  authKey: remoteAuthKey,
648
- isConfigured: !!remoteUrl && !!remoteAuthKey
657
+ isConfigured: !!remoteUrl2 && !!remoteAuthKey
649
658
  };
650
659
  const resolved = {
651
660
  ...config,
@@ -801,6 +810,40 @@ function setPublicUrl(publicUrl) {
801
810
  console.warn("[config] failed to persist publicUrl:", err?.message || err);
802
811
  }
803
812
  }
813
+ function setCfAccessConfig(input) {
814
+ const applyToAuth = (auth) => {
815
+ const curAuth = auth || {};
816
+ const curCf = curAuth.cfAccess || {};
817
+ const nextCf = { ...curCf };
818
+ if (input.enabled !== void 0) nextCf.enabled = input.enabled;
819
+ if (input.teamDomain !== void 0) nextCf.teamDomain = input.teamDomain;
820
+ if (input.audTag !== void 0) nextCf.audTag = input.audTag;
821
+ const nextAuth = { ...curAuth, cfAccess: nextCf };
822
+ if (input.allowedEmails !== void 0) nextAuth.allowedEmails = input.allowedEmails;
823
+ return nextAuth;
824
+ };
825
+ if (cachedConfig) {
826
+ cachedConfig.auth = applyToAuth(cachedConfig.auth);
827
+ }
828
+ try {
829
+ const cwdPath = resolve(process.cwd(), "sparkecoder.config.json");
830
+ const target = existsSync(cwdPath) ? cwdPath : join(ensureAppDataDirectory(), "sparkecoder.config.json");
831
+ let raw = {};
832
+ if (existsSync(target)) {
833
+ try {
834
+ raw = JSON.parse(readFileSync(target, "utf-8"));
835
+ } catch {
836
+ raw = {};
837
+ }
838
+ } else {
839
+ raw = createDefaultConfig();
840
+ }
841
+ raw.auth = applyToAuth(raw.auth);
842
+ writeFileSync(target, JSON.stringify(raw, null, 2));
843
+ } catch (err) {
844
+ console.warn("[config] failed to persist cf-access config:", err?.message || err);
845
+ }
846
+ }
804
847
  function clearSlackConfig() {
805
848
  if (cachedConfig) cachedConfig.slack = {};
806
849
  try {
@@ -842,7 +885,7 @@ function autoApproveAllTools(sessionConfig) {
842
885
  }
843
886
  function createDefaultConfig() {
844
887
  return {
845
- defaultModel: "anthropic/claude-opus-4.7",
888
+ defaultModel: "gpt-5.5",
846
889
  // workingDirectory is intentionally not set - defaults to where CLI is run
847
890
  toolApprovals: {
848
891
  bash: true,
@@ -864,6 +907,10 @@ function createDefaultConfig() {
864
907
  port: 3141,
865
908
  host: "0.0.0.0"
866
909
  },
910
+ autoUpdate: {
911
+ enabled: true,
912
+ intervalHours: 6
913
+ },
867
914
  databasePath: "./sparkecoder.db"
868
915
  };
869
916
  }
@@ -1090,6 +1137,7 @@ var init_config = __esm({
1090
1137
  openai: "OPENAI_API_KEY",
1091
1138
  google: "GOOGLE_GENERATIVE_AI_API_KEY",
1092
1139
  xai: "XAI_API_KEY",
1140
+ litellm: "LITELLM_API_KEY",
1093
1141
  "ai-gateway": "AI_GATEWAY_API_KEY"
1094
1142
  };
1095
1143
  SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
@@ -4862,11 +4910,11 @@ async function getRepoNamespace(workingDirectory, configuredNamespace) {
4862
4910
  if (configuredNamespace) {
4863
4911
  return configuredNamespace;
4864
4912
  }
4865
- const remoteUrl = getGitRemoteUrl(workingDirectory);
4866
- if (!remoteUrl) {
4913
+ const remoteUrl2 = getGitRemoteUrl(workingDirectory);
4914
+ if (!remoteUrl2) {
4867
4915
  return null;
4868
4916
  }
4869
- const parsed = parseGitRemoteUrl(remoteUrl);
4917
+ const parsed = parseGitRemoteUrl(remoteUrl2);
4870
4918
  if (!parsed) {
4871
4919
  return null;
4872
4920
  }
@@ -6563,7 +6611,8 @@ async function buildSystemPrompt(options) {
6563
6611
  sessionId,
6564
6612
  discoveredSkills,
6565
6613
  activeFiles = [],
6566
- customInstructions
6614
+ customInstructions,
6615
+ taskScopedSkills
6567
6616
  } = options;
6568
6617
  let alwaysLoadedContent = "";
6569
6618
  let globMatchedContent = "";
@@ -6584,6 +6633,22 @@ async function buildSystemPrompt(options) {
6584
6633
  const skills2 = await loadAllSkills2(skillsDirectories);
6585
6634
  onDemandSkillsContext = formatSkillsForContext(skills2);
6586
6635
  }
6636
+ let taskScopedSkillsBlock = "";
6637
+ if (taskScopedSkills && (taskScopedSkills.always.length > 0 || taskScopedSkills.onDemand.length > 0)) {
6638
+ const parts = ["<task_provided_skills>"];
6639
+ parts.push("These skills were supplied with this task and are available for this run only.");
6640
+ if (taskScopedSkills.always.length > 0) {
6641
+ parts.push(formatAlwaysLoadedSkills(taskScopedSkills.always));
6642
+ }
6643
+ if (taskScopedSkills.onDemand.length > 0) {
6644
+ parts.push("Load any of these on demand with the load_skill tool:");
6645
+ for (const s of taskScopedSkills.onDemand) {
6646
+ parts.push(`- ${s.name}: ${s.description}`);
6647
+ }
6648
+ }
6649
+ parts.push("</task_provided_skills>");
6650
+ taskScopedSkillsBlock = parts.join("\n");
6651
+ }
6587
6652
  const todos = await todoQueries.getBySession(sessionId);
6588
6653
  const todosContext = formatTodosForContext(todos);
6589
6654
  const plans = await readSessionPlans(workingDirectory, sessionId);
@@ -6876,6 +6941,8 @@ ${globMatchedContent}
6876
6941
  ${onDemandSkillsContext}
6877
6942
  </on_demand_skills>
6878
6943
 
6944
+ ${taskScopedSkillsBlock}
6945
+
6879
6946
  <current_task_list>
6880
6947
  ${todosContext}
6881
6948
  </current_task_list>
@@ -7497,6 +7564,111 @@ var init_sanitize_messages = __esm({
7497
7564
  }
7498
7565
  });
7499
7566
 
7567
+ // src/utils/cap-image-count.ts
7568
+ function isImagePart(part) {
7569
+ if (!part || typeof part !== "object") return false;
7570
+ const t = part.type;
7571
+ if (t === "image") return true;
7572
+ if (t === "image-data") return true;
7573
+ if (t === "media") {
7574
+ const data = part.data;
7575
+ const mt = part.mediaType;
7576
+ if (typeof data === "string" && typeof mt === "string" && mt.startsWith("image/")) {
7577
+ return true;
7578
+ }
7579
+ }
7580
+ return false;
7581
+ }
7582
+ function makePlaceholder() {
7583
+ return { type: "text", text: IMAGE_TRUNCATED_PLACEHOLDER };
7584
+ }
7585
+ function countImages(messages) {
7586
+ let n = 0;
7587
+ for (const msg of messages) {
7588
+ if (!Array.isArray(msg.content)) continue;
7589
+ for (const part of msg.content) {
7590
+ if (isImagePart(part)) {
7591
+ n++;
7592
+ continue;
7593
+ }
7594
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
7595
+ for (const sub of part.output.value) {
7596
+ if (isImagePart(sub)) n++;
7597
+ }
7598
+ }
7599
+ }
7600
+ }
7601
+ return n;
7602
+ }
7603
+ function capImageCount(messages, max = MAX_IMAGES_IN_CONTEXT) {
7604
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
7605
+ if (max < 0) throw new Error("capImageCount: max must be >= 0");
7606
+ const total = countImages(messages);
7607
+ if (total <= max) return messages;
7608
+ let toDrop = total - max;
7609
+ let mutated = false;
7610
+ const out = messages.slice();
7611
+ for (let i = 0; i < out.length && toDrop > 0; i++) {
7612
+ const msg = out[i];
7613
+ if (!Array.isArray(msg.content)) continue;
7614
+ let contentCloned = false;
7615
+ const ensureContentCloned = () => {
7616
+ if (contentCloned) return;
7617
+ out[i] = { ...msg, content: [...msg.content] };
7618
+ contentCloned = true;
7619
+ };
7620
+ const content = () => out[i].content;
7621
+ for (let j = 0; j < content().length && toDrop > 0; j++) {
7622
+ const part = content()[j];
7623
+ if (isImagePart(part)) {
7624
+ ensureContentCloned();
7625
+ out[i].content[j] = makePlaceholder();
7626
+ toDrop--;
7627
+ mutated = true;
7628
+ continue;
7629
+ }
7630
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
7631
+ const innerImages = [];
7632
+ const innerValue = part.output.value;
7633
+ for (let k = 0; k < innerValue.length; k++) {
7634
+ if (isImagePart(innerValue[k])) innerImages.push(k);
7635
+ }
7636
+ if (innerImages.length === 0) continue;
7637
+ const dropHere = Math.min(innerImages.length, toDrop);
7638
+ ensureContentCloned();
7639
+ const newOutputValue = [...innerValue];
7640
+ for (let d = 0; d < dropHere; d++) {
7641
+ newOutputValue[innerImages[d]] = makePlaceholder();
7642
+ }
7643
+ const newPart = {
7644
+ ...part,
7645
+ output: {
7646
+ ...part.output,
7647
+ value: newOutputValue
7648
+ }
7649
+ };
7650
+ out[i].content[j] = newPart;
7651
+ toDrop -= dropHere;
7652
+ mutated = true;
7653
+ }
7654
+ }
7655
+ }
7656
+ if (mutated) {
7657
+ console.warn(
7658
+ `[cap-image-count] Replaced ${total - max} oldest image(s) with text placeholder (total=${total}, kept=${max}). This prevents request-too-large errors at the model / tunnel layer.`
7659
+ );
7660
+ }
7661
+ return mutated ? out : messages;
7662
+ }
7663
+ var MAX_IMAGES_IN_CONTEXT, IMAGE_TRUNCATED_PLACEHOLDER;
7664
+ var init_cap_image_count = __esm({
7665
+ "src/utils/cap-image-count.ts"() {
7666
+ "use strict";
7667
+ MAX_IMAGES_IN_CONTEXT = 11;
7668
+ IMAGE_TRUNCATED_PLACEHOLDER = "[image truncated due to length of conversation]";
7669
+ }
7670
+ });
7671
+
7500
7672
  // src/agent/model-limits.ts
7501
7673
  function getModelLimits(modelId) {
7502
7674
  const normalized = modelId.trim().toLowerCase();
@@ -7512,18 +7684,9 @@ var init_model_limits = __esm({
7512
7684
  "src/agent/model-limits.ts"() {
7513
7685
  "use strict";
7514
7686
  MODEL_LIMITS = {
7515
- "anthropic/claude-opus-4.7": { contextWindow: 2e5, rollingTarget: 15e4 },
7516
- "anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
7517
- "anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
7518
- "anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
7519
- "anthropic/claude-3-haiku": { contextWindow: 2e5, rollingTarget: 15e4 },
7520
- "google/gemini-3-flash-preview": { contextWindow: 1e6, rollingTarget: 15e4 },
7521
- "google/gemini-2.5-pro": { contextWindow: 1e6, rollingTarget: 15e4 },
7522
- "google/gemini-2.5-flash": { contextWindow: 1e6, rollingTarget: 15e4 },
7523
- "openai/gpt-4o": { contextWindow: 128e3, rollingTarget: 78e3 },
7524
- "openai/gpt-4.1": { contextWindow: 1e6, rollingTarget: 15e4 },
7525
- "openai/o3": { contextWindow: 2e5, rollingTarget: 15e4 },
7526
- "xai/grok-3": { contextWindow: 131072, rollingTarget: 8e4 }
7687
+ "claude-opus-4-8": { contextWindow: 2e5, rollingTarget: 15e4 },
7688
+ "gpt-5.5": { contextWindow: 35e4, rollingTarget: 15e4 },
7689
+ "claude-fable-5": { contextWindow: 2e5, rollingTarget: 15e4 }
7527
7690
  };
7528
7691
  DEFAULT_LIMITS = { contextWindow: 2e5, rollingTarget: 15e4 };
7529
7692
  PREFIX_DEFAULTS = {
@@ -7597,6 +7760,32 @@ var init_conversation_archive = __esm({
7597
7760
 
7598
7761
  // src/agent/context.ts
7599
7762
  import { generateText as generateText2 } from "ai";
7763
+ function stripBinaryContentForSummary(value) {
7764
+ if (Array.isArray(value)) return value.map(stripBinaryContentForSummary);
7765
+ if (!value || typeof value !== "object") return value;
7766
+ const record = value;
7767
+ const type = record.type;
7768
+ if ((type === "image-data" || type === "file-data" || type === "media") && typeof record.data === "string") {
7769
+ const mediaType = typeof record.mediaType === "string" ? record.mediaType : "unknown media type";
7770
+ const filename = typeof record.filename === "string" ? ` ${record.filename}` : "";
7771
+ return {
7772
+ ...record,
7773
+ data: `[${type}${filename}; ${mediaType}; ${record.data.length} base64 chars omitted for summary]`
7774
+ };
7775
+ }
7776
+ if (type === "image" && typeof record.image === "string") {
7777
+ const filename = typeof record.filename === "string" ? ` ${record.filename}` : "";
7778
+ return {
7779
+ ...record,
7780
+ image: `[image${filename}; ${record.image.length} base64 chars omitted for summary]`
7781
+ };
7782
+ }
7783
+ const out = {};
7784
+ for (const [key2, nested] of Object.entries(record)) {
7785
+ out[key2] = stripBinaryContentForSummary(nested);
7786
+ }
7787
+ return out;
7788
+ }
7600
7789
  function stripOrphanedToolResults(msg, removedIds) {
7601
7790
  if (!Array.isArray(msg.content)) return msg;
7602
7791
  const parts = msg.content.filter((part) => {
@@ -7757,6 +7946,7 @@ var init_context = __esm({
7757
7946
  init_tokens();
7758
7947
  init_prompts();
7759
7948
  init_sanitize_messages();
7949
+ init_cap_image_count();
7760
7950
  init_model_limits();
7761
7951
  TOOL_OUTPUT_TRIM_CHARS = 400;
7762
7952
  COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
@@ -7806,6 +7996,7 @@ ${summaryContent}`
7806
7996
  messages = repairToolPairing(messages);
7807
7997
  messages = ensureToolResultsFollowCalls(messages);
7808
7998
  messages = ensureEndsWithUserOrTool(messages);
7999
+ messages = capImageCount(messages);
7809
8000
  return messages;
7810
8001
  }
7811
8002
  // ---------------------------------------------------------------------------
@@ -7923,7 +8114,7 @@ ${summaryContent}`
7923
8114
  }
7924
8115
  async summarizeChunk(chunk) {
7925
8116
  const historyText = chunk.map((msg) => {
7926
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
8117
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(stripBinaryContentForSummary(msg.content));
7927
8118
  return `[${msg.role}]: ${content}`;
7928
8119
  }).join("\n\n");
7929
8120
  try {
@@ -8171,6 +8362,127 @@ var init_persistence = __esm({
8171
8362
  });
8172
8363
 
8173
8364
  // src/integrations/slack/client.ts
8365
+ var client_exports = {};
8366
+ __export(client_exports, {
8367
+ LOADING_REACTION: () => LOADING_REACTION,
8368
+ RESULT_REACTIONS: () => RESULT_REACTIONS,
8369
+ addLoadingReaction: () => addLoadingReaction,
8370
+ addResultReaction: () => addResultReaction,
8371
+ botParticipatedInThread: () => botParticipatedInThread,
8372
+ ensureSlackSelfIdentity: () => ensureSlackSelfIdentity,
8373
+ getCachedSlackSelfIdentity: () => getCachedSlackSelfIdentity,
8374
+ getDefaultOrchestratorName: () => getDefaultOrchestratorName,
8375
+ getSlackAdapter: () => getSlackAdapter,
8376
+ getSlackAllowlistPolicy: () => getSlackAllowlistPolicy,
8377
+ getSlackBotToken: () => getSlackBotToken,
8378
+ getSlackDeniedReplyPolicy: () => getSlackDeniedReplyPolicy,
8379
+ getSlackSigningSecret: () => getSlackSigningSecret,
8380
+ isSlackConfigured: () => isSlackConfigured,
8381
+ normalizeSlackMentions: () => normalizeSlackMentions,
8382
+ noteBotPostedInThread: () => noteBotPostedInThread,
8383
+ postThreadMessage: () => postThreadMessage,
8384
+ removeLoadingReaction: () => removeLoadingReaction,
8385
+ resolveSlackUserInfo: () => resolveSlackUserInfo,
8386
+ resolveSlackUserName: () => resolveSlackUserName
8387
+ });
8388
+ function slackBackoffMs(attempt) {
8389
+ const expo = SLACK_BACKOFF_BASE_MS * 2 ** attempt;
8390
+ const jitter = Math.floor(Math.random() * SLACK_BACKOFF_BASE_MS);
8391
+ return Math.min(expo + jitter, SLACK_BACKOFF_CAP_MS);
8392
+ }
8393
+ async function slackFetchWithRetry(url, init, attempts = SLACK_FETCH_ATTEMPTS) {
8394
+ let lastErr;
8395
+ for (let i = 0; i < attempts; i++) {
8396
+ const isLast = i === attempts - 1;
8397
+ try {
8398
+ const res = await fetch(url, init);
8399
+ if ((res.status === 429 || res.status >= 500) && !isLast) {
8400
+ const ra = Number(res.headers.get("retry-after"));
8401
+ const waitMs = Number.isFinite(ra) && ra > 0 ? Math.min(ra * 1e3, SLACK_BACKOFF_CAP_MS) : slackBackoffMs(i);
8402
+ await new Promise((r) => setTimeout(r, waitMs));
8403
+ continue;
8404
+ }
8405
+ return res;
8406
+ } catch (err) {
8407
+ lastErr = err;
8408
+ if (isLast) throw err;
8409
+ await new Promise((r) => setTimeout(r, slackBackoffMs(i)));
8410
+ }
8411
+ }
8412
+ throw lastErr ?? new Error("slack fetch failed");
8413
+ }
8414
+ function reactionKey(channel, ts) {
8415
+ return `${channel}\u241F${ts}`;
8416
+ }
8417
+ async function addLoadingReaction(channel, timestamp) {
8418
+ const adapter = getSlackAdapter();
8419
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
8420
+ const key2 = reactionKey(channel, timestamp);
8421
+ const inFlight = (async () => {
8422
+ try {
8423
+ const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
8424
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
8425
+ console.warn(`[slack] addReaction ${LOADING_REACTION} failed on ${channel}/${timestamp}: ${res.error}`);
8426
+ }
8427
+ return res;
8428
+ } catch (err) {
8429
+ console.warn(`[slack] addReaction threw on ${channel}/${timestamp}:`, err?.message || err);
8430
+ return { ok: false, error: err?.message || "unknown" };
8431
+ }
8432
+ })();
8433
+ pendingAdds.set(key2, inFlight);
8434
+ void inFlight.finally(() => {
8435
+ if (pendingAdds.get(key2) === inFlight) pendingAdds.delete(key2);
8436
+ });
8437
+ return inFlight;
8438
+ }
8439
+ async function removeLoadingReaction(channel, timestamp) {
8440
+ const adapter = getSlackAdapter();
8441
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
8442
+ const pending = pendingAdds.get(reactionKey(channel, timestamp));
8443
+ if (pending) {
8444
+ try {
8445
+ await pending;
8446
+ } catch {
8447
+ }
8448
+ }
8449
+ try {
8450
+ const res = await adapter.removeReaction({ channel, timestamp, name: LOADING_REACTION });
8451
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
8452
+ console.warn(`[slack] removeReaction ${LOADING_REACTION} failed on ${channel}/${timestamp}: ${res.error}`);
8453
+ }
8454
+ return res;
8455
+ } catch (err) {
8456
+ console.warn(`[slack] removeReaction threw on ${channel}/${timestamp}:`, err?.message || err);
8457
+ return { ok: false, error: err?.message || "unknown" };
8458
+ }
8459
+ }
8460
+ async function addResultReaction(channel, timestamp, state2) {
8461
+ const name = RESULT_REACTIONS[state2];
8462
+ if (!name) return { ok: false, error: `no_reaction_for_state:${state2}` };
8463
+ const adapter = getSlackAdapter();
8464
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
8465
+ try {
8466
+ const res = await adapter.addReaction({ channel, timestamp, name });
8467
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
8468
+ console.warn(`[slack] addReaction ${name} failed on ${channel}/${timestamp}: ${res.error}`);
8469
+ }
8470
+ return res;
8471
+ } catch (err) {
8472
+ console.warn(`[slack] addResultReaction threw on ${channel}/${timestamp}:`, err?.message || err);
8473
+ return { ok: false, error: err?.message || "unknown" };
8474
+ }
8475
+ }
8476
+ async function postThreadMessage(channel, threadTs, text) {
8477
+ const adapter = getSlackAdapter();
8478
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
8479
+ try {
8480
+ return await adapter.postMessage({ channel, text, threadTs });
8481
+ } catch (err) {
8482
+ console.warn(`[slack] postThreadMessage threw on ${channel}/${threadTs}:`, err?.message || err);
8483
+ return { ok: false, error: err?.message || "unknown" };
8484
+ }
8485
+ }
8174
8486
  function readSlackConfig() {
8175
8487
  try {
8176
8488
  const cfg = getConfig();
@@ -8188,9 +8500,25 @@ function readSlackConfig() {
8188
8500
  function getSlackAdapter() {
8189
8501
  const cfg = readSlackConfig();
8190
8502
  if (!cfg) return void 0;
8503
+ const slackForm = async (endpoint, params) => {
8504
+ const body = new URLSearchParams(params).toString();
8505
+ const res = await fetch(`https://slack.com/api/${endpoint}`, {
8506
+ method: "POST",
8507
+ headers: {
8508
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
8509
+ Authorization: `Bearer ${cfg.botToken}`
8510
+ },
8511
+ body
8512
+ });
8513
+ const data = await res.json().catch(() => ({}));
8514
+ if (!res.ok || data?.ok === false) {
8515
+ return { ok: false, error: data?.error || `HTTP ${res.status}` };
8516
+ }
8517
+ return { ok: true };
8518
+ };
8191
8519
  return {
8192
8520
  async postMessage({ channel, text, threadTs }) {
8193
- const res = await fetch("https://slack.com/api/chat.postMessage", {
8521
+ const res = await slackFetchWithRetry("https://slack.com/api/chat.postMessage", {
8194
8522
  method: "POST",
8195
8523
  headers: {
8196
8524
  "Content-Type": "application/json; charset=utf-8",
@@ -8203,6 +8531,12 @@ function getSlackAdapter() {
8203
8531
  return { ok: false, error: data?.error || `HTTP ${res.status}` };
8204
8532
  }
8205
8533
  return { ok: true, ts: data?.ts };
8534
+ },
8535
+ addReaction({ channel, timestamp, name }) {
8536
+ return slackForm("reactions.add", { channel, timestamp, name });
8537
+ },
8538
+ removeReaction({ channel, timestamp, name }) {
8539
+ return slackForm("reactions.remove", { channel, timestamp, name });
8206
8540
  }
8207
8541
  };
8208
8542
  }
@@ -8403,12 +8737,31 @@ function getSlackDeniedReplyPolicy() {
8403
8737
  return { enabled: true, template: DEFAULT_DENIED_TEMPLATE };
8404
8738
  }
8405
8739
  }
8406
- var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS, threadOwnedInflight, DEFAULT_DENIED_TEMPLATE;
8740
+ var LOADING_REACTION, RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, pendingAdds, cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS, threadOwnedInflight, DEFAULT_DENIED_TEMPLATE;
8407
8741
  var init_client3 = __esm({
8408
8742
  "src/integrations/slack/client.ts"() {
8409
8743
  "use strict";
8410
8744
  init_config();
8411
8745
  init_persistence();
8746
+ LOADING_REACTION = "hourglass_flowing_sand";
8747
+ RESULT_REACTIONS = {
8748
+ responded: "white_check_mark",
8749
+ skipped: "zzz",
8750
+ handed_off: "eyes",
8751
+ failed: "warning"
8752
+ };
8753
+ SLACK_FETCH_ATTEMPTS = 3;
8754
+ SLACK_BACKOFF_BASE_MS = 400;
8755
+ SLACK_BACKOFF_CAP_MS = 3e4;
8756
+ REACTION_SOFT_ERRORS = /* @__PURE__ */ new Set([
8757
+ "already_reacted",
8758
+ // add: someone (or we) already added this emoji
8759
+ "no_reaction",
8760
+ // remove: the emoji isn't on the message
8761
+ "message_not_found"
8762
+ // remove/add: original message deleted
8763
+ ]);
8764
+ pendingAdds = /* @__PURE__ */ new Map();
8412
8765
  cachedSelf = null;
8413
8766
  selfInflight = null;
8414
8767
  USER_TTL_MS = 60 * 60 * 1e3;
@@ -8421,83 +8774,541 @@ var init_client3 = __esm({
8421
8774
  }
8422
8775
  });
8423
8776
 
8424
- // src/integrations/channels/slack.ts
8425
- function threadKey(channel, threadTs) {
8426
- return `${channel}\u241F${threadTs}`;
8427
- }
8428
- function markThreadOwned(channel, threadTs) {
8429
- ownedThreads.add(threadKey(channel, threadTs));
8430
- }
8431
- function isThreadOwned(channel, threadTs) {
8432
- return ownedThreads.has(threadKey(channel, threadTs));
8777
+ // src/agent/session-lock.ts
8778
+ async function withSessionLock(sessionId, fn) {
8779
+ let state2 = locks.get(sessionId);
8780
+ if (!state2) {
8781
+ state2 = { tail: Promise.resolve(), pending: 0 };
8782
+ locks.set(sessionId, state2);
8783
+ }
8784
+ state2.pending++;
8785
+ const prev = state2.tail;
8786
+ let release;
8787
+ const next = new Promise((resolve13) => {
8788
+ release = resolve13;
8789
+ });
8790
+ state2.tail = prev.then(() => next);
8791
+ await prev;
8792
+ try {
8793
+ return await fn();
8794
+ } finally {
8795
+ release();
8796
+ state2.pending--;
8797
+ if (state2.pending === 0 && locks.get(sessionId) === state2) {
8798
+ locks.delete(sessionId);
8799
+ }
8800
+ }
8433
8801
  }
8434
- function isSelfAuthored(event, self) {
8435
- if (!self) return true;
8436
- if (self.botId && event.bot_id && event.bot_id === self.botId) return true;
8437
- if (self.botUserId && event.user && event.user === self.botUserId) return true;
8438
- return false;
8802
+ function isSessionLocked(sessionId) {
8803
+ const s = locks.get(sessionId);
8804
+ return !!s && s.pending > 0;
8439
8805
  }
8440
- function slackEventToInboundResult(event, opts = {}) {
8441
- if (!event) return { event: null, dropReason: "empty_text" };
8442
- const self = opts.self ?? getCachedSlackSelfIdentity();
8443
- const isBotAuthored = !!event.bot_id || event.type === "message" && event.subtype === "bot_message";
8444
- if (isBotAuthored && isSelfAuthored(event, self)) {
8445
- return { event: null, dropReason: "bot_message" };
8446
- }
8447
- if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
8448
- return { event: null, dropReason: "ignored_subtype" };
8806
+ var locks;
8807
+ var init_session_lock = __esm({
8808
+ "src/agent/session-lock.ts"() {
8809
+ "use strict";
8810
+ locks = /* @__PURE__ */ new Map();
8449
8811
  }
8450
- const isDm = event.type === "message" && event.channel_type === "im";
8451
- const isThreadReply = event.type === "message" && !isDm && typeof event.thread_ts === "string" && event.thread_ts !== event.ts;
8452
- const isNonThreadChannelMsg = event.type === "message" && !isDm && !isThreadReply && (event.channel_type === "channel" || event.channel_type === "group" || event.channel_type === "mpim" || // Some payload shapes omit channel_type for channel messages.
8453
- typeof event.channel === "string");
8454
- if (event.type !== "app_mention" && !isDm && !isThreadReply) {
8455
- if (isNonThreadChannelMsg) {
8456
- return { event: null, dropReason: "non_thread_channel_msg" };
8812
+ });
8813
+
8814
+ // src/orchestrator/webhook-events.ts
8815
+ import { existsSync as existsSync17, readFileSync as readFileSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
8816
+ import { dirname as dirname7, join as join10 } from "path";
8817
+ import { nanoid as nanoid4 } from "nanoid";
8818
+ function logFilePath() {
8819
+ return join10(getAppDataDirectory(), "webhook-events.jsonl");
8820
+ }
8821
+ function ensureLoaded() {
8822
+ if (cache !== null) return cache;
8823
+ cache = [];
8824
+ try {
8825
+ const p = logFilePath();
8826
+ if (!existsSync17(p)) return cache;
8827
+ const lines = readFileSync8(p, "utf-8").split("\n").filter(Boolean);
8828
+ for (const line of lines) {
8829
+ try {
8830
+ cache.push(JSON.parse(line));
8831
+ } catch {
8832
+ }
8457
8833
  }
8458
- if (event.type !== "message") {
8459
- return { event: null, dropReason: "non_message_event" };
8834
+ if (cache.length > MAX_EVENTS) {
8835
+ cache = cache.slice(-MAX_EVENTS);
8836
+ try {
8837
+ writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
8838
+ } catch {
8839
+ }
8460
8840
  }
8461
- return { event: null, dropReason: "unsupported_type" };
8841
+ } catch {
8462
8842
  }
8463
- const text = (event.text ?? "").trim();
8464
- if (!text) return { event: null, dropReason: "empty_text" };
8465
- const policy = getSlackAllowlistPolicy();
8466
- const userAllowlistActive = policy.allowedUsers.length > 0;
8467
- const userOk = !userAllowlistActive || event.user && policy.allowedUsers.includes(event.user);
8468
- if (isDm) {
8469
- if (!policy.allowDmsFromAnyone && !(event.user && policy.allowedUsers.includes(event.user))) {
8470
- return { event: null, dropReason: "dm_blocked" };
8471
- }
8472
- } else {
8473
- const channelAllowlistActive = policy.allowedChannels.length > 0;
8474
- if (channelAllowlistActive && !policy.allowedChannels.includes(event.channel)) {
8475
- return { event: null, dropReason: "channel_not_allowed" };
8476
- }
8477
- if (!userOk) {
8478
- return { event: null, dropReason: "user_not_allowed" };
8479
- }
8843
+ return cache;
8844
+ }
8845
+ function appendEvent(ev) {
8846
+ const list = ensureLoaded();
8847
+ list.push(ev);
8848
+ if (list.length > MAX_EVENTS) list.shift();
8849
+ try {
8850
+ const p = logFilePath();
8851
+ mkdirSync7(dirname7(p), { recursive: true });
8852
+ appendFileSync3(p, JSON.stringify(ev) + "\n");
8853
+ } catch {
8480
8854
  }
8481
- const ref = {
8482
- channel: "slack",
8483
- slackChannel: event.channel,
8484
- threadTs: event.thread_ts || event.ts,
8485
- teamId: event.team,
8486
- user: event.user
8487
- };
8488
- const label = slackChannel.displayLabel(ref);
8489
- return {
8490
- event: {
8491
- ref,
8492
- content: `[${label}] ${text}`,
8493
- wake: "now",
8494
- enqueuedAt: /* @__PURE__ */ new Date()
8495
- }
8496
- };
8497
8855
  }
8498
- var ownedThreads, slackChannel, IGNORED_MESSAGE_SUBTYPES;
8499
- var init_slack = __esm({
8500
- "src/integrations/channels/slack.ts"() {
8856
+ function recordEvent(ev) {
8857
+ const full = {
8858
+ id: ev.id ?? nanoid4(),
8859
+ ts: ev.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
8860
+ source: ev.source,
8861
+ status: ev.status,
8862
+ subtype: ev.subtype,
8863
+ channel: ev.channel,
8864
+ user: ev.user,
8865
+ textSnippet: ev.textSnippet?.slice(0, 200),
8866
+ dropReason: ev.dropReason,
8867
+ error: ev.error,
8868
+ sessionId: ev.sessionId,
8869
+ durationMs: ev.durationMs,
8870
+ meta: ev.meta
8871
+ };
8872
+ appendEvent(full);
8873
+ return full.id;
8874
+ }
8875
+ function updateEvent(id, patch) {
8876
+ const list = ensureLoaded();
8877
+ const i = list.findIndex((e) => e.id === id);
8878
+ if (i < 0) return;
8879
+ list[i] = { ...list[i], ...patch };
8880
+ try {
8881
+ const p = logFilePath();
8882
+ mkdirSync7(dirname7(p), { recursive: true });
8883
+ writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
8884
+ } catch {
8885
+ }
8886
+ }
8887
+ function listEvents(filter = {}) {
8888
+ const list = ensureLoaded();
8889
+ const q = filter.q?.toLowerCase();
8890
+ const sinceTs = filter.since ? Date.parse(filter.since) : -Infinity;
8891
+ const beforeTs = filter.before ? Date.parse(filter.before) : Infinity;
8892
+ const matched = list.filter((e) => {
8893
+ if (filter.source && e.source !== filter.source) return false;
8894
+ if (filter.status && e.status !== filter.status) return false;
8895
+ const t = Date.parse(e.ts);
8896
+ if (t < sinceTs) return false;
8897
+ if (t >= beforeTs) return false;
8898
+ if (q) {
8899
+ const hay = `${e.channel ?? ""} ${e.user ?? ""} ${e.textSnippet ?? ""} ${e.dropReason ?? ""} ${e.error ?? ""} ${e.subtype ?? ""}`.toLowerCase();
8900
+ if (!hay.includes(q)) return false;
8901
+ }
8902
+ return true;
8903
+ });
8904
+ matched.reverse();
8905
+ const offset = Math.max(0, filter.offset ?? 0);
8906
+ const limit = Math.min(500, Math.max(1, filter.limit ?? 50));
8907
+ return {
8908
+ events: matched.slice(offset, offset + limit),
8909
+ total: matched.length
8910
+ };
8911
+ }
8912
+ function clearAllEvents() {
8913
+ cache = [];
8914
+ try {
8915
+ writeFileSync4(logFilePath(), "");
8916
+ } catch {
8917
+ }
8918
+ }
8919
+ var MAX_EVENTS, cache;
8920
+ var init_webhook_events = __esm({
8921
+ "src/orchestrator/webhook-events.ts"() {
8922
+ "use strict";
8923
+ init_config();
8924
+ MAX_EVENTS = 1e3;
8925
+ cache = null;
8926
+ }
8927
+ });
8928
+
8929
+ // src/orchestrator/inbox.ts
8930
+ var inbox_exports = {};
8931
+ __export(inbox_exports, {
8932
+ clearInbox: () => clearInbox,
8933
+ flush: () => flush,
8934
+ peekInbox: () => peekInbox,
8935
+ pushToInbox: () => pushToInbox,
8936
+ setFlushHandler: () => setFlushHandler
8937
+ });
8938
+ function setFlushHandler(fn) {
8939
+ flushHandler = fn;
8940
+ }
8941
+ function entryFor(sessionId) {
8942
+ let e = inboxes.get(sessionId);
8943
+ if (!e) {
8944
+ e = { pending: [] };
8945
+ inboxes.set(sessionId, e);
8946
+ }
8947
+ return e;
8948
+ }
8949
+ function pushToInbox(orchestratorSessionId, event) {
8950
+ const e = entryFor(orchestratorSessionId);
8951
+ e.pending.push(event);
8952
+ try {
8953
+ trackInbound(orchestratorSessionId, event);
8954
+ } catch {
8955
+ }
8956
+ if (event.wake === "now") {
8957
+ scheduleFlush(orchestratorSessionId);
8958
+ }
8959
+ }
8960
+ function scheduleFlush(sessionId) {
8961
+ const e = inboxes.get(sessionId);
8962
+ if (!e) return;
8963
+ if (e.timer) clearTimeout(e.timer);
8964
+ e.timer = setTimeout(() => {
8965
+ void flush(sessionId);
8966
+ }, FLUSH_DEBOUNCE_MS);
8967
+ }
8968
+ async function flush(sessionId) {
8969
+ const e = inboxes.get(sessionId);
8970
+ if (!e) return;
8971
+ if (e.timer) {
8972
+ clearTimeout(e.timer);
8973
+ e.timer = void 0;
8974
+ }
8975
+ const events = e.pending.splice(0);
8976
+ if (events.length === 0) return;
8977
+ if (!flushHandler) {
8978
+ console.warn("[orchestrator-inbox] flush called with no handler installed; dropping events");
8979
+ return;
8980
+ }
8981
+ try {
8982
+ await flushHandler(sessionId, events);
8983
+ } catch (err) {
8984
+ console.error("[orchestrator-inbox] flush handler threw:", err?.message || err);
8985
+ }
8986
+ }
8987
+ function peekInbox(sessionId) {
8988
+ return inboxes.get(sessionId)?.pending.slice() ?? [];
8989
+ }
8990
+ function clearInbox(sessionId) {
8991
+ const e = inboxes.get(sessionId);
8992
+ if (!e) return;
8993
+ if (e.timer) {
8994
+ clearTimeout(e.timer);
8995
+ e.timer = void 0;
8996
+ }
8997
+ e.pending.length = 0;
8998
+ }
8999
+ var inboxes, FLUSH_DEBOUNCE_MS, flushHandler;
9000
+ var init_inbox = __esm({
9001
+ "src/orchestrator/inbox.ts"() {
9002
+ "use strict";
9003
+ init_inbox_acks();
9004
+ inboxes = /* @__PURE__ */ new Map();
9005
+ FLUSH_DEBOUNCE_MS = 200;
9006
+ flushHandler = null;
9007
+ }
9008
+ });
9009
+
9010
+ // src/orchestrator/inbox-acks.ts
9011
+ var inbox_acks_exports = {};
9012
+ __export(inbox_acks_exports, {
9013
+ MAX_ATTEMPTS: () => MAX_ATTEMPTS,
9014
+ RECONCILE_EVERY_MS: () => RECONCILE_EVERY_MS,
9015
+ REPLAY_AFTER_MS: () => REPLAY_AFTER_MS,
9016
+ __getAck: () => __getAck,
9017
+ __listAcks: () => __listAcks,
9018
+ __resetAcks: () => __resetAcks,
9019
+ eventKey: () => eventKey,
9020
+ markRespondedForThread: () => markRespondedForThread,
9021
+ markState: () => markState,
9022
+ reconcileOnce: () => reconcileOnce,
9023
+ resolveBatchOnTurnEnd: () => resolveBatchOnTurnEnd,
9024
+ startReconciler: () => startReconciler,
9025
+ stopReconciler: () => stopReconciler,
9026
+ trackInbound: () => trackInbound
9027
+ });
9028
+ function eventKey(event) {
9029
+ const ref = event.ref;
9030
+ const ch = ref?.channel ?? "unknown";
9031
+ switch (ch) {
9032
+ case "slack":
9033
+ return `slack${SEP}${ref.slackChannel}${SEP}${ref.messageTs ?? ref.threadTs ?? ""}`;
9034
+ case "system":
9035
+ return `system${SEP}${ref.workerId}${SEP}${ref.kind}`;
9036
+ case "webhook":
9037
+ return `webhook${SEP}${ref.webhookId}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}`;
9038
+ case "schedule":
9039
+ return `schedule${SEP}${ref.scheduleId}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}`;
9040
+ default:
9041
+ return `${ch}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}${SEP}${event.content.slice(0, 40)}`;
9042
+ }
9043
+ }
9044
+ function trackInbound(sessionId, event) {
9045
+ if (event.wake === "never") return null;
9046
+ const key2 = eventKey(event);
9047
+ const existing = ledger.get(key2);
9048
+ if (existing) return key2;
9049
+ const ref = event.ref;
9050
+ const now = Date.now();
9051
+ const entry2 = {
9052
+ key: key2,
9053
+ sessionId,
9054
+ event,
9055
+ channel: ref?.channel ?? "unknown",
9056
+ state: "working",
9057
+ attempts: 0,
9058
+ trackedAt: now,
9059
+ updatedAt: now
9060
+ };
9061
+ if (ref?.channel === "slack") {
9062
+ entry2.slackChannel = ref.slackChannel;
9063
+ entry2.threadTs = ref.threadTs;
9064
+ entry2.messageTs = ref.messageTs;
9065
+ }
9066
+ ledger.set(key2, entry2);
9067
+ if (ledger.size > MAX_ENTRIES) pruneOldest();
9068
+ return key2;
9069
+ }
9070
+ function markState(key2, state2) {
9071
+ const entry2 = ledger.get(key2);
9072
+ if (!entry2) return;
9073
+ if (TERMINAL.has(entry2.state)) return;
9074
+ entry2.state = state2;
9075
+ entry2.updatedAt = Date.now();
9076
+ if (entry2.channel === "slack" && entry2.slackChannel && entry2.messageTs) {
9077
+ fireResultReaction(entry2.slackChannel, entry2.messageTs, state2);
9078
+ }
9079
+ }
9080
+ function markRespondedForThread(slackChannel2, threadTs) {
9081
+ if (!slackChannel2 || !threadTs) return;
9082
+ for (const entry2 of ledger.values()) {
9083
+ if (entry2.channel === "slack" && entry2.state === "working" && entry2.slackChannel === slackChannel2 && entry2.threadTs === threadTs) {
9084
+ markState(entry2.key, "responded");
9085
+ }
9086
+ }
9087
+ }
9088
+ function resolveBatchOnTurnEnd(events, ok) {
9089
+ if (!ok) return;
9090
+ for (const ev of events) {
9091
+ const key2 = eventKey(ev);
9092
+ const entry2 = ledger.get(key2);
9093
+ if (!entry2 || entry2.state !== "working") continue;
9094
+ if (entry2.channel === "slack") continue;
9095
+ markState(key2, "responded");
9096
+ }
9097
+ }
9098
+ async function reconcileOnce(now = Date.now()) {
9099
+ let pushToInbox2 = null;
9100
+ const toReplay = [];
9101
+ for (const entry2 of ledger.values()) {
9102
+ if (TERMINAL.has(entry2.state)) {
9103
+ if (now - entry2.updatedAt > PRUNE_AFTER_MS) ledger.delete(entry2.key);
9104
+ continue;
9105
+ }
9106
+ if (isSessionLocked(entry2.sessionId)) continue;
9107
+ if (now - entry2.updatedAt < REPLAY_AFTER_MS) continue;
9108
+ if (entry2.attempts >= MAX_ATTEMPTS) {
9109
+ failEntry(entry2);
9110
+ continue;
9111
+ }
9112
+ toReplay.push(entry2);
9113
+ }
9114
+ if (toReplay.length === 0) return;
9115
+ try {
9116
+ ({ pushToInbox: pushToInbox2 } = await Promise.resolve().then(() => (init_inbox(), inbox_exports)));
9117
+ } catch {
9118
+ return;
9119
+ }
9120
+ for (const entry2 of toReplay) {
9121
+ entry2.attempts += 1;
9122
+ entry2.updatedAt = Date.now();
9123
+ const nudged = {
9124
+ ...entry2.event,
9125
+ 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.]
9126
+ ${entry2.event.content}`,
9127
+ wake: "now"
9128
+ };
9129
+ try {
9130
+ pushToInbox2(entry2.sessionId, nudged);
9131
+ } catch {
9132
+ }
9133
+ }
9134
+ }
9135
+ function failEntry(entry2) {
9136
+ entry2.state = "failed";
9137
+ entry2.updatedAt = Date.now();
9138
+ if (entry2.channel === "slack" && entry2.slackChannel && entry2.messageTs) {
9139
+ fireResultReaction(entry2.slackChannel, entry2.messageTs, "failed");
9140
+ if (entry2.threadTs) {
9141
+ fireFallback(
9142
+ entry2.slackChannel,
9143
+ entry2.threadTs,
9144
+ `:warning: I wasn't able to handle this after ${entry2.attempts} attempt(s). It may need a human \u2014 flagging it here so it isn't lost.`
9145
+ );
9146
+ }
9147
+ }
9148
+ recordEvent({
9149
+ source: "daemon",
9150
+ status: "failed",
9151
+ channel: entry2.channel,
9152
+ sessionId: entry2.sessionId,
9153
+ error: `unacknowledged after ${entry2.attempts} replay attempt(s)`,
9154
+ textSnippet: entry2.event.content.slice(0, 200),
9155
+ meta: { ackKey: entry2.key, ackState: "failed" }
9156
+ });
9157
+ }
9158
+ function pruneOldest() {
9159
+ const terminal = [];
9160
+ for (const e of ledger.values()) if (TERMINAL.has(e.state)) terminal.push(e);
9161
+ terminal.sort((a, b) => a.updatedAt - b.updatedAt);
9162
+ for (const e of terminal) {
9163
+ if (ledger.size <= MAX_ENTRIES) break;
9164
+ ledger.delete(e.key);
9165
+ }
9166
+ while (ledger.size > MAX_ENTRIES) {
9167
+ const oldest = ledger.keys().next().value;
9168
+ if (!oldest) break;
9169
+ ledger.delete(oldest);
9170
+ }
9171
+ }
9172
+ function fireResultReaction(channel, ts, state2) {
9173
+ if (typeof addResultReaction !== "function") return;
9174
+ try {
9175
+ void Promise.resolve(addResultReaction(channel, ts, state2)).catch(() => {
9176
+ });
9177
+ } catch {
9178
+ }
9179
+ }
9180
+ function fireFallback(channel, threadTs, text) {
9181
+ if (typeof postThreadMessage !== "function") return;
9182
+ try {
9183
+ void Promise.resolve(postThreadMessage(channel, threadTs, text)).catch(() => {
9184
+ });
9185
+ } catch {
9186
+ }
9187
+ }
9188
+ function startReconciler() {
9189
+ if (reconcileTimer) return;
9190
+ reconcileTimer = setInterval(() => {
9191
+ void reconcileOnce();
9192
+ }, RECONCILE_EVERY_MS);
9193
+ if (typeof reconcileTimer.unref === "function") reconcileTimer.unref();
9194
+ }
9195
+ function stopReconciler() {
9196
+ if (reconcileTimer) {
9197
+ clearInterval(reconcileTimer);
9198
+ reconcileTimer = null;
9199
+ }
9200
+ }
9201
+ function __getAck(key2) {
9202
+ return ledger.get(key2);
9203
+ }
9204
+ function __listAcks() {
9205
+ return [...ledger.values()];
9206
+ }
9207
+ function __resetAcks() {
9208
+ ledger.clear();
9209
+ }
9210
+ var REPLAY_AFTER_MS, RECONCILE_EVERY_MS, MAX_ATTEMPTS, PRUNE_AFTER_MS, MAX_ENTRIES, TERMINAL, SEP, ledger, reconcileTimer;
9211
+ var init_inbox_acks = __esm({
9212
+ "src/orchestrator/inbox-acks.ts"() {
9213
+ "use strict";
9214
+ init_session_lock();
9215
+ init_webhook_events();
9216
+ init_client3();
9217
+ REPLAY_AFTER_MS = 3 * 6e4;
9218
+ RECONCILE_EVERY_MS = 6e4;
9219
+ MAX_ATTEMPTS = 2;
9220
+ PRUNE_AFTER_MS = 60 * 6e4;
9221
+ MAX_ENTRIES = 5e3;
9222
+ TERMINAL = /* @__PURE__ */ new Set(["responded", "skipped", "handed_off", "failed"]);
9223
+ SEP = "\u241F";
9224
+ ledger = /* @__PURE__ */ new Map();
9225
+ reconcileTimer = null;
9226
+ }
9227
+ });
9228
+
9229
+ // src/integrations/channels/slack.ts
9230
+ function threadKey(channel, threadTs) {
9231
+ return `${channel}\u241F${threadTs}`;
9232
+ }
9233
+ function markThreadOwned(channel, threadTs) {
9234
+ ownedThreads.add(threadKey(channel, threadTs));
9235
+ }
9236
+ function isThreadOwned(channel, threadTs) {
9237
+ return ownedThreads.has(threadKey(channel, threadTs));
9238
+ }
9239
+ function isSelfAuthored(event, self) {
9240
+ if (!self) return true;
9241
+ if (self.botId && event.bot_id && event.bot_id === self.botId) return true;
9242
+ if (self.botUserId && event.user && event.user === self.botUserId) return true;
9243
+ return false;
9244
+ }
9245
+ function slackEventToInboundResult(event, opts = {}) {
9246
+ if (!event) return { event: null, dropReason: "empty_text" };
9247
+ const self = opts.self ?? getCachedSlackSelfIdentity();
9248
+ const isBotAuthored = !!event.bot_id || event.type === "message" && event.subtype === "bot_message";
9249
+ if (isBotAuthored && isSelfAuthored(event, self)) {
9250
+ return { event: null, dropReason: "bot_message" };
9251
+ }
9252
+ if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
9253
+ return { event: null, dropReason: "ignored_subtype" };
9254
+ }
9255
+ const isDm = event.type === "message" && event.channel_type === "im";
9256
+ const isThreadReply = event.type === "message" && !isDm && typeof event.thread_ts === "string" && event.thread_ts !== event.ts;
9257
+ const isNonThreadChannelMsg = event.type === "message" && !isDm && !isThreadReply && (event.channel_type === "channel" || event.channel_type === "group" || event.channel_type === "mpim" || // Some payload shapes omit channel_type for channel messages.
9258
+ typeof event.channel === "string");
9259
+ if (event.type !== "app_mention" && !isDm && !isThreadReply) {
9260
+ if (isNonThreadChannelMsg) {
9261
+ return { event: null, dropReason: "non_thread_channel_msg" };
9262
+ }
9263
+ if (event.type !== "message") {
9264
+ return { event: null, dropReason: "non_message_event" };
9265
+ }
9266
+ return { event: null, dropReason: "unsupported_type" };
9267
+ }
9268
+ const text = (event.text ?? "").trim();
9269
+ const hasFiles = Array.isArray(event.files) && event.files.length > 0;
9270
+ if (!text && !hasFiles) return { event: null, dropReason: "empty_text" };
9271
+ const policy = getSlackAllowlistPolicy();
9272
+ const userAllowlistActive = policy.allowedUsers.length > 0;
9273
+ const userOk = !userAllowlistActive || event.user && policy.allowedUsers.includes(event.user);
9274
+ if (isDm) {
9275
+ if (!policy.allowDmsFromAnyone && !(event.user && policy.allowedUsers.includes(event.user))) {
9276
+ return { event: null, dropReason: "dm_blocked" };
9277
+ }
9278
+ } else {
9279
+ const channelAllowlistActive = policy.allowedChannels.length > 0;
9280
+ if (channelAllowlistActive && !policy.allowedChannels.includes(event.channel)) {
9281
+ return { event: null, dropReason: "channel_not_allowed" };
9282
+ }
9283
+ if (!userOk) {
9284
+ return { event: null, dropReason: "user_not_allowed" };
9285
+ }
9286
+ }
9287
+ const ref = {
9288
+ channel: "slack",
9289
+ slackChannel: event.channel,
9290
+ // For thread replies, threadTs points at the parent (so our reply
9291
+ // continues the thread). messageTs is the inbound message's own ts —
9292
+ // used by reaction add/remove (which target the message itself, not
9293
+ // its parent) and any future "edit this message" operations.
9294
+ threadTs: event.thread_ts || event.ts,
9295
+ messageTs: event.ts,
9296
+ teamId: event.team,
9297
+ user: event.user
9298
+ };
9299
+ const label = slackChannel.displayLabel(ref);
9300
+ return {
9301
+ event: {
9302
+ ref,
9303
+ content: `[${label}] ${text}`,
9304
+ wake: "now",
9305
+ enqueuedAt: /* @__PURE__ */ new Date()
9306
+ }
9307
+ };
9308
+ }
9309
+ var ownedThreads, slackChannel, IGNORED_MESSAGE_SUBTYPES;
9310
+ var init_slack = __esm({
9311
+ "src/integrations/channels/slack.ts"() {
8501
9312
  "use strict";
8502
9313
  init_client3();
8503
9314
  ownedThreads = /* @__PURE__ */ new Set();
@@ -8517,6 +9328,8 @@ var init_slack = __esm({
8517
9328
  if (r.slackChannel && r.threadTs) {
8518
9329
  markThreadOwned(r.slackChannel, r.threadTs);
8519
9330
  noteBotPostedInThread(r.slackChannel, r.threadTs);
9331
+ void Promise.resolve().then(() => (init_inbox_acks(), inbox_acks_exports)).then((m) => m.markRespondedForThread(r.slackChannel, r.threadTs)).catch(() => {
9332
+ });
8520
9333
  }
8521
9334
  },
8522
9335
  displayLabel(ref) {
@@ -8549,8 +9362,14 @@ var init_slack = __esm({
8549
9362
  // also-broadcast-to-channel replies; the regular thread reply already fires
8550
9363
  "message_replied",
8551
9364
  // legacy parent-thread bump
8552
- "file_share",
8553
- // we'd handle these later; for now skip to avoid double-handling
9365
+ // NOTE: `file_share` is intentionally NOT ignored. It's the subtype Slack
9366
+ // attaches to a normal user message that includes file uploads — the
9367
+ // event still has `text`, `user`, `channel`, and `files: [...]`. The
9368
+ // route handler (src/server/routes/slack.ts) hands the `files[]` array
9369
+ // to `ingestSlackFiles` (src/integrations/slack/files.ts), which
9370
+ // downloads each file using the bot token, re-uploads it to GCS via
9371
+ // the remote-server's /storage/upload-url endpoint, and appends the
9372
+ // resulting short public URLs to the inbound message content.
8554
9373
  "reply_broadcast",
8555
9374
  "tombstone",
8556
9375
  "huddle_thread"
@@ -8759,7 +9578,7 @@ var init_messenger = __esm({
8759
9578
  });
8760
9579
 
8761
9580
  // src/orchestrator/schedules-store.ts
8762
- import { nanoid as nanoid4 } from "nanoid";
9581
+ import { nanoid as nanoid5 } from "nanoid";
8763
9582
  async function readOrch(orchestratorSessionId) {
8764
9583
  const s = await sessionQueries.getById(orchestratorSessionId);
8765
9584
  if (!s) return null;
@@ -8774,7 +9593,7 @@ async function createSchedule(orchestratorSessionId, input) {
8774
9593
  const data = await readOrch(orchestratorSessionId);
8775
9594
  if (!data) throw new Error("orchestrator session not found");
8776
9595
  const row = {
8777
- id: `sch_${nanoid4(10)}`,
9596
+ id: `sch_${nanoid5(10)}`,
8778
9597
  name: input.name,
8779
9598
  cron: input.cron,
8780
9599
  prompt: input.prompt,
@@ -8811,7 +9630,7 @@ var init_schedules_store = __esm({
8811
9630
 
8812
9631
  // src/orchestrator/webhooks-store.ts
8813
9632
  import { randomBytes } from "crypto";
8814
- import { nanoid as nanoid5 } from "nanoid";
9633
+ import { nanoid as nanoid6 } from "nanoid";
8815
9634
  function newToken() {
8816
9635
  return randomBytes(24).toString("base64url");
8817
9636
  }
@@ -8828,7 +9647,7 @@ async function createWebhook(orchestratorSessionId, input) {
8828
9647
  const data = await readOrch2(orchestratorSessionId);
8829
9648
  if (!data) throw new Error("orchestrator session not found");
8830
9649
  const row = {
8831
- id: `whk_${nanoid5(10)}`,
9650
+ id: `whk_${nanoid6(10)}`,
8832
9651
  name: input.name,
8833
9652
  token: newToken(),
8834
9653
  wake: input.wake ?? "now",
@@ -8973,7 +9792,9 @@ function buildAgentTool(opts) {
8973
9792
  workingDirectory: input.workingDirectory ?? opts.defaultWorkingDirectory,
8974
9793
  name: input.name,
8975
9794
  maxIterations: input.maxIterations ?? 100,
8976
- orchestratorSessionId: opts.orchestratorSessionId
9795
+ orchestratorSessionId: opts.orchestratorSessionId,
9796
+ ...input.mcpServers ? { mcpServers: input.mcpServers } : {},
9797
+ ...input.skills ? { skills: input.skills } : {}
8977
9798
  }
8978
9799
  });
8979
9800
  return {
@@ -9146,6 +9967,26 @@ var init_orchestrator_actions = __esm({
9146
9967
  model: z14.string().optional().describe("spawn only: model override."),
9147
9968
  workingDirectory: z14.string().optional().describe("spawn only: working directory override."),
9148
9969
  maxIterations: z14.number().int().min(1).max(500).optional().describe("spawn only."),
9970
+ mcpServers: z14.array(
9971
+ z14.object({
9972
+ name: z14.string(),
9973
+ transport: z14.enum(["http", "sse", "stdio"]),
9974
+ url: z14.string().optional(),
9975
+ headers: z14.record(z14.string(), z14.string()).optional(),
9976
+ command: z14.string().optional(),
9977
+ args: z14.array(z14.string()).optional(),
9978
+ env: z14.record(z14.string(), z14.string()).optional()
9979
+ })
9980
+ ).optional().describe("spawn only: task-scoped MCP servers (with auth headers) connected for this worker only, tools exposed as mcp_<name>_<tool>."),
9981
+ skills: z14.array(
9982
+ z14.object({
9983
+ name: z14.string(),
9984
+ description: z14.string().optional(),
9985
+ content: z14.string(),
9986
+ alwaysApply: z14.boolean().optional(),
9987
+ globs: z14.array(z14.string()).optional()
9988
+ })
9989
+ ).optional().describe("spawn only: task-scoped skills (inline markdown) available to this worker only."),
9149
9990
  // message
9150
9991
  text: z14.string().optional().describe("message only: the text to deliver to the worker."),
9151
9992
  force: z14.boolean().optional().describe("message only: soft-interrupt the current step."),
@@ -9185,9 +10026,9 @@ var init_orchestrator_actions = __esm({
9185
10026
  });
9186
10027
 
9187
10028
  // src/integrations/mcp/store.ts
9188
- import { nanoid as nanoid6 } from "nanoid";
9189
- import { existsSync as existsSync17, readFileSync as readFileSync8 } from "fs";
9190
- import { resolve as resolve10, join as join10 } from "path";
10029
+ import { nanoid as nanoid7 } from "nanoid";
10030
+ import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
10031
+ import { resolve as resolve10, join as join11 } from "path";
9191
10032
  function readServers() {
9192
10033
  try {
9193
10034
  const cfg = getConfig();
@@ -9199,12 +10040,12 @@ function readServers() {
9199
10040
  function refreshMcpServersFromDisk() {
9200
10041
  const candidates = [
9201
10042
  resolve10(process.cwd(), "sparkecoder.config.json"),
9202
- join10(ensureAppDataDirectory(), "sparkecoder.config.json")
10043
+ join11(ensureAppDataDirectory(), "sparkecoder.config.json")
9203
10044
  ];
9204
10045
  for (const path of candidates) {
9205
- if (!existsSync17(path)) continue;
10046
+ if (!existsSync18(path)) continue;
9206
10047
  try {
9207
- const raw = JSON.parse(readFileSync8(path, "utf-8"));
10048
+ const raw = JSON.parse(readFileSync9(path, "utf-8"));
9208
10049
  const servers2 = Array.isArray(raw?.mcp?.servers) ? raw.mcp.servers : [];
9209
10050
  setMcpServers(servers2);
9210
10051
  return servers2;
@@ -9223,7 +10064,7 @@ function createMcpServer(input) {
9223
10064
  const all = readServers();
9224
10065
  validateInput(input);
9225
10066
  const row = {
9226
- id: `mcp_${nanoid6(10)}`,
10067
+ id: `mcp_${nanoid7(10)}`,
9227
10068
  name: sanitizeName(input.name),
9228
10069
  transport: input.transport,
9229
10070
  url: input.url,
@@ -9394,6 +10235,159 @@ var init_pool = __esm({
9394
10235
  }
9395
10236
  });
9396
10237
 
10238
+ // src/integrations/mcp/task-scoped.ts
10239
+ import { createMCPClient as createMCPClient2 } from "@ai-sdk/mcp";
10240
+ function sanitizeName2(raw) {
10241
+ return raw.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/_+/g, "_");
10242
+ }
10243
+ function buildHttpLikeTransport(server) {
10244
+ if (!server.url) {
10245
+ throw new Error(`${server.transport} transport requires a url`);
10246
+ }
10247
+ return {
10248
+ type: server.transport,
10249
+ url: server.url,
10250
+ headers: server.headers
10251
+ };
10252
+ }
10253
+ async function buildStdioTransport2(server) {
10254
+ if (!server.command) {
10255
+ throw new Error("stdio transport requires a command");
10256
+ }
10257
+ const mod = await import("@ai-sdk/mcp/mcp-stdio");
10258
+ const Cls = mod.Experimental_StdioMCPTransport || mod.StdioClientTransport;
10259
+ if (!Cls) throw new Error("@ai-sdk/mcp/mcp-stdio is missing the stdio transport class");
10260
+ return new Cls({
10261
+ command: server.command,
10262
+ args: server.args ?? [],
10263
+ env: server.env
10264
+ });
10265
+ }
10266
+ async function buildTransport(server) {
10267
+ return server.transport === "stdio" ? await buildStdioTransport2(server) : buildHttpLikeTransport(server);
10268
+ }
10269
+ async function connectTaskMcpServers(servers2, opts = {}) {
10270
+ const tools = {};
10271
+ const connected = [];
10272
+ const errors = [];
10273
+ const clients2 = [];
10274
+ for (const raw of servers2 ?? []) {
10275
+ const name = sanitizeName2(raw.name || "");
10276
+ if (!name) {
10277
+ errors.push({ name: raw.name || "(unnamed)", error: "server name is required" });
10278
+ continue;
10279
+ }
10280
+ let client = null;
10281
+ try {
10282
+ const transport = await buildTransport(raw);
10283
+ client = await createMCPClient2({ transport });
10284
+ clients2.push(client);
10285
+ const serverTools = await client.tools();
10286
+ for (const [toolName, t] of Object.entries(serverTools)) {
10287
+ tools[`mcp_${name}_${toolName}`] = t;
10288
+ }
10289
+ connected.push(name);
10290
+ } catch (err) {
10291
+ const message = err?.message || String(err);
10292
+ errors.push({ name, error: message });
10293
+ if (!opts.quiet) {
10294
+ console.warn(`[mcp:task] connecting "${name}" failed: ${message}`);
10295
+ }
10296
+ if (client) {
10297
+ try {
10298
+ await client.close();
10299
+ } catch {
10300
+ }
10301
+ const idx = clients2.indexOf(client);
10302
+ if (idx >= 0) clients2.splice(idx, 1);
10303
+ }
10304
+ }
10305
+ }
10306
+ let closed = false;
10307
+ const close = async () => {
10308
+ if (closed) return;
10309
+ closed = true;
10310
+ await Promise.all(
10311
+ clients2.map(async (c) => {
10312
+ try {
10313
+ await c.close();
10314
+ } catch {
10315
+ }
10316
+ })
10317
+ );
10318
+ };
10319
+ return { tools, connected, errors, close };
10320
+ }
10321
+ var init_task_scoped = __esm({
10322
+ "src/integrations/mcp/task-scoped.ts"() {
10323
+ "use strict";
10324
+ }
10325
+ });
10326
+
10327
+ // src/skills/task-scoped.ts
10328
+ import { mkdtemp, writeFile as writeFile5, rm } from "fs/promises";
10329
+ import { tmpdir } from "os";
10330
+ import { join as join12 } from "path";
10331
+ function safeFileName(name, index) {
10332
+ const base = name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
10333
+ return `${base || `skill-${index + 1}`}.md`;
10334
+ }
10335
+ function escapeFrontmatterValue(value) {
10336
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
10337
+ }
10338
+ function buildSkillFile(skill) {
10339
+ const lines = ["---"];
10340
+ lines.push(`name: ${escapeFrontmatterValue(skill.name)}`);
10341
+ lines.push(`description: ${escapeFrontmatterValue(skill.description || skill.name)}`);
10342
+ if (skill.alwaysApply) lines.push("alwaysApply: true");
10343
+ if (skill.globs && skill.globs.length > 0) {
10344
+ lines.push(`globs: [${skill.globs.map((g) => escapeFrontmatterValue(g)).join(", ")}]`);
10345
+ }
10346
+ lines.push("---");
10347
+ lines.push("");
10348
+ lines.push(skill.content);
10349
+ return lines.join("\n");
10350
+ }
10351
+ async function materializeTaskSkills(skills2, taskId) {
10352
+ if (!skills2 || skills2.length === 0) return null;
10353
+ const safeTaskId = taskId.replace(/[^a-zA-Z0-9_-]+/g, "_");
10354
+ const dir = await mkdtemp(join12(tmpdir(), `sparkecoder-task-skills-${safeTaskId}-`));
10355
+ const seen = /* @__PURE__ */ new Set();
10356
+ await Promise.all(
10357
+ skills2.map(async (skill, i) => {
10358
+ let fileName = safeFileName(skill.name, i);
10359
+ while (seen.has(fileName)) fileName = `dup-${i}-${fileName}`;
10360
+ seen.add(fileName);
10361
+ await writeFile5(join12(dir, fileName), buildSkillFile(skill), "utf-8");
10362
+ })
10363
+ );
10364
+ const loaded2 = await loadSkillsFromDirectory(dir, { priority: 1, defaultLoadType: "on_demand" });
10365
+ const alwaysSkills = loaded2.filter((s) => s.alwaysApply || s.loadType === "always");
10366
+ const onDemand = loaded2.filter((s) => !(s.alwaysApply || s.loadType === "always"));
10367
+ const always = (await Promise.all(
10368
+ alwaysSkills.map(async (s) => {
10369
+ const withContent = await loadSkillContent(s.name, [dir]);
10370
+ return withContent ? { ...s, content: withContent.content } : null;
10371
+ })
10372
+ )).filter((s) => s !== null);
10373
+ let cleaned = false;
10374
+ const cleanup2 = async () => {
10375
+ if (cleaned) return;
10376
+ cleaned = true;
10377
+ try {
10378
+ await rm(dir, { recursive: true, force: true });
10379
+ } catch {
10380
+ }
10381
+ };
10382
+ return { dir, always, onDemand, cleanup: cleanup2 };
10383
+ }
10384
+ var init_task_scoped2 = __esm({
10385
+ "src/skills/task-scoped.ts"() {
10386
+ "use strict";
10387
+ init_skills();
10388
+ }
10389
+ });
10390
+
9397
10391
  // src/utils/webhook.ts
9398
10392
  var webhook_exports = {};
9399
10393
  __export(webhook_exports, {
@@ -9552,79 +10546,57 @@ var init_pending_input = __esm({
9552
10546
  }
9553
10547
  });
9554
10548
 
9555
- // src/orchestrator/inbox.ts
9556
- var inbox_exports = {};
9557
- __export(inbox_exports, {
9558
- clearInbox: () => clearInbox,
9559
- flush: () => flush,
9560
- peekInbox: () => peekInbox,
9561
- pushToInbox: () => pushToInbox,
9562
- setFlushHandler: () => setFlushHandler
9563
- });
9564
- function setFlushHandler(fn) {
9565
- flushHandler = fn;
9566
- }
9567
- function entryFor(sessionId) {
9568
- let e = inboxes.get(sessionId);
9569
- if (!e) {
9570
- e = { pending: [] };
9571
- inboxes.set(sessionId, e);
9572
- }
9573
- return e;
10549
+ // src/utils/local-device-time.ts
10550
+ function formatLocalDeviceTimeLine(now = /* @__PURE__ */ new Date()) {
10551
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
10552
+ const formatted = now.toLocaleString("en-US", {
10553
+ weekday: "long",
10554
+ year: "numeric",
10555
+ month: "long",
10556
+ day: "numeric",
10557
+ hour: "numeric",
10558
+ minute: "2-digit",
10559
+ second: "2-digit",
10560
+ timeZoneName: "short"
10561
+ });
10562
+ return `${LOCAL_TIME_MARKER} ${formatted} (${timeZone})]`;
9574
10563
  }
9575
- function pushToInbox(orchestratorSessionId, event) {
9576
- const e = entryFor(orchestratorSessionId);
9577
- e.pending.push(event);
9578
- if (event.wake === "now") {
9579
- scheduleFlush(orchestratorSessionId);
9580
- }
10564
+ function hasLocalDeviceTimeLine(text) {
10565
+ return text.includes(LOCAL_TIME_MARKER);
9581
10566
  }
9582
- function scheduleFlush(sessionId) {
9583
- const e = inboxes.get(sessionId);
9584
- if (!e) return;
9585
- if (e.timer) clearTimeout(e.timer);
9586
- e.timer = setTimeout(() => {
9587
- void flush(sessionId);
9588
- }, FLUSH_DEBOUNCE_MS);
10567
+ function prependLocalDeviceTimeToUserMessage(text, now) {
10568
+ const trimmed = text.trim();
10569
+ if (!trimmed || hasLocalDeviceTimeLine(text)) return text;
10570
+ return `${formatLocalDeviceTimeLine(now)}
10571
+ ${text}`;
9589
10572
  }
9590
- async function flush(sessionId) {
9591
- const e = inboxes.get(sessionId);
9592
- if (!e) return;
9593
- if (e.timer) {
9594
- clearTimeout(e.timer);
9595
- e.timer = void 0;
9596
- }
9597
- const events = e.pending.splice(0);
9598
- if (events.length === 0) return;
9599
- if (!flushHandler) {
9600
- console.warn("[orchestrator-inbox] flush called with no handler installed; dropping events");
9601
- return;
10573
+ function prependLocalDeviceTimeToUserContent(content, now) {
10574
+ if (typeof content === "string") {
10575
+ return prependLocalDeviceTimeToUserMessage(content, now);
9602
10576
  }
9603
- try {
9604
- await flushHandler(sessionId, events);
9605
- } catch (err) {
9606
- console.error("[orchestrator-inbox] flush handler threw:", err?.message || err);
10577
+ const line = formatLocalDeviceTimeLine(now);
10578
+ if (content.some((p) => p.type === "text" && p.text && hasLocalDeviceTimeLine(p.text))) {
10579
+ return content;
9607
10580
  }
9608
- }
9609
- function peekInbox(sessionId) {
9610
- return inboxes.get(sessionId)?.pending.slice() ?? [];
9611
- }
9612
- function clearInbox(sessionId) {
9613
- const e = inboxes.get(sessionId);
9614
- if (!e) return;
9615
- if (e.timer) {
9616
- clearTimeout(e.timer);
9617
- e.timer = void 0;
10581
+ const userIdx = content.findIndex(
10582
+ (p) => p.type === "text" && p.text?.includes("[USER MESSAGE]")
10583
+ );
10584
+ if (userIdx >= 0 && content[userIdx].text) {
10585
+ const copy = content.map((p) => ({ ...p }));
10586
+ copy[userIdx] = {
10587
+ ...copy[userIdx],
10588
+ text: `${line}
10589
+ ${copy[userIdx].text}`
10590
+ };
10591
+ return copy;
9618
10592
  }
9619
- e.pending.length = 0;
10593
+ return [{ type: "text", text: line }, ...content];
9620
10594
  }
9621
- var inboxes, FLUSH_DEBOUNCE_MS, flushHandler;
9622
- var init_inbox = __esm({
9623
- "src/orchestrator/inbox.ts"() {
10595
+ var LOCAL_TIME_MARKER;
10596
+ var init_local_device_time = __esm({
10597
+ "src/utils/local-device-time.ts"() {
9624
10598
  "use strict";
9625
- inboxes = /* @__PURE__ */ new Map();
9626
- FLUSH_DEBOUNCE_MS = 200;
9627
- flushHandler = null;
10599
+ LOCAL_TIME_MARKER = "[Local device time:";
9628
10600
  }
9629
10601
  });
9630
10602
 
@@ -9836,10 +10808,10 @@ __export(recorder_exports, {
9836
10808
  });
9837
10809
  import { exec as exec5 } from "child_process";
9838
10810
  import { promisify as promisify5 } from "util";
9839
- import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
9840
- import { join as join11 } from "path";
9841
- import { tmpdir } from "os";
9842
- import { nanoid as nanoid7 } from "nanoid";
10811
+ import { writeFile as writeFile6, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm as rm2 } from "fs/promises";
10812
+ import { join as join13 } from "path";
10813
+ import { tmpdir as tmpdir2 } from "os";
10814
+ import { nanoid as nanoid8 } from "nanoid";
9843
10815
  async function checkFfmpeg() {
9844
10816
  try {
9845
10817
  await execAsync5("ffmpeg -version", { timeout: 5e3 });
@@ -9850,7 +10822,7 @@ async function checkFfmpeg() {
9850
10822
  }
9851
10823
  async function cleanup(dir) {
9852
10824
  try {
9853
- await rm(dir, { recursive: true, force: true });
10825
+ await rm2(dir, { recursive: true, force: true });
9854
10826
  } catch {
9855
10827
  }
9856
10828
  }
@@ -9894,21 +10866,21 @@ var init_recorder = __esm({
9894
10866
  */
9895
10867
  async encode() {
9896
10868
  if (this.frames.length === 0) return null;
9897
- const workDir = join11(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
10869
+ const workDir = join13(tmpdir2(), `sparkecoder-recording-${nanoid8(8)}`);
9898
10870
  await mkdir4(workDir, { recursive: true });
9899
10871
  try {
9900
10872
  for (let i = 0; i < this.frames.length; i++) {
9901
- const framePath = join11(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
9902
- await writeFile5(framePath, this.frames[i].data);
10873
+ const framePath = join13(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
10874
+ await writeFile6(framePath, this.frames[i].data);
9903
10875
  }
9904
10876
  const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
9905
10877
  const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
9906
10878
  const clampedFps = Math.max(1, Math.min(fps, 30));
9907
- const outputPath = join11(workDir, `recording_${this.sessionId}.mp4`);
10879
+ const outputPath = join13(workDir, `recording_${this.sessionId}.mp4`);
9908
10880
  const hasFfmpeg = await checkFfmpeg();
9909
10881
  if (hasFfmpeg) {
9910
10882
  await execAsync5(
9911
- `ffmpeg -y -framerate ${clampedFps} -i "${join11(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
10883
+ `ffmpeg -y -framerate ${clampedFps} -i "${join13(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
9912
10884
  { timeout: 12e4 }
9913
10885
  );
9914
10886
  } else {
@@ -9920,7 +10892,7 @@ var init_recorder = __esm({
9920
10892
  const files = await readdir5(workDir);
9921
10893
  for (const f of files) {
9922
10894
  if (f.startsWith("frame_")) {
9923
- await unlink2(join11(workDir, f)).catch(() => {
10895
+ await unlink2(join13(workDir, f)).catch(() => {
9924
10896
  });
9925
10897
  }
9926
10898
  }
@@ -9949,7 +10921,7 @@ import {
9949
10921
  stepCountIs as stepCountIs2
9950
10922
  } from "ai";
9951
10923
  import { z as z15 } from "zod";
9952
- import { nanoid as nanoid8 } from "nanoid";
10924
+ import { nanoid as nanoid9 } from "nanoid";
9953
10925
  function anySignal(signals) {
9954
10926
  const ctrl = new AbortController();
9955
10927
  for (const s of signals) {
@@ -9993,10 +10965,13 @@ var init_agent = __esm({
9993
10965
  init_prompts();
9994
10966
  init_orchestrator_actions();
9995
10967
  init_pool();
10968
+ init_task_scoped();
10969
+ init_task_scoped2();
9996
10970
  init_webhook2();
9997
10971
  init_questions();
9998
10972
  init_pending_input();
9999
10973
  init_inbox();
10974
+ init_local_device_time();
10000
10975
  init_system();
10001
10976
  init_context();
10002
10977
  init_prompts();
@@ -10159,9 +11134,11 @@ ${prompt}` });
10159
11134
  */
10160
11135
  async stream(options) {
10161
11136
  const config = getConfig();
10162
- const userContent = this.buildUserMessageContent(options.prompt, options.attachments);
11137
+ const prompt = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserMessage(options.prompt) : options.prompt;
11138
+ const userContent = this.buildUserMessageContent(prompt, options.attachments);
11139
+ const persistedUserContent = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserContent(userContent) : userContent;
10163
11140
  if (!options.skipSaveUserMessage) {
10164
- await this.context.addUserMessage(userContent);
11141
+ await this.context.addUserMessage(persistedUserContent);
10165
11142
  }
10166
11143
  await sessionQueries.updateStatus(this.session.id, "active");
10167
11144
  let systemPrompt = await buildSystemPrompt({
@@ -10239,7 +11216,8 @@ ${personality.trim()}
10239
11216
  */
10240
11217
  async run(options) {
10241
11218
  const config = getConfig();
10242
- await this.context.addUserMessage(options.prompt);
11219
+ const prompt = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserMessage(options.prompt) : options.prompt;
11220
+ await this.context.addUserMessage(prompt);
10243
11221
  const systemPrompt = await buildSystemPrompt({
10244
11222
  workingDirectory: this.session.workingDirectory,
10245
11223
  skillsDirectories: config.resolvedSkillsDirectories,
@@ -10283,355 +11261,387 @@ ${personality.trim()}
10283
11261
  */
10284
11262
  async runTask(options) {
10285
11263
  const config = getConfig();
10286
- const maxIterations = options.taskConfig.maxIterations ?? 50;
10287
- const webhookUrl = options.taskConfig.webhookUrl;
10288
- const parentTaskId = options.taskConfig.parentTaskId;
10289
- const fireWebhook = (type, data) => {
10290
- if (!webhookUrl) return;
10291
- sendWebhook(webhookUrl, {
10292
- type,
10293
- taskId: this.session.id,
10294
- sessionId: this.session.id,
10295
- ...parentTaskId ? { parentTaskId } : {},
10296
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10297
- data
10298
- });
10299
- };
10300
- const completion = { signal: null };
10301
- const onComplete = (signal) => {
10302
- completion.signal = signal;
10303
- };
10304
- let taskRecorder = null;
10305
- const sessionId = this.session.id;
10306
- const emit = options.writeSSE;
10307
- const bashProgressHandler = (progress) => {
10308
- options.onToolProgress?.({ toolName: "bash", data: progress });
10309
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "bash", data: progress })).catch(() => {
10310
- });
10311
- const port = progress.browserStreamPort;
10312
- if (port && progress.status === "started") {
10313
- Promise.resolve().then(() => (init_stream_proxy(), stream_proxy_exports)).then(({ getOrCreateProxy: getOrCreateProxy2 }) => {
10314
- const proxy = getOrCreateProxy2(sessionId, port);
10315
- if (!taskRecorder) {
10316
- Promise.resolve().then(() => (init_recorder(), recorder_exports)).then(({ FrameRecorder: FrameRecorder2 }) => {
10317
- taskRecorder = new FrameRecorder2(sessionId);
10318
- taskRecorder.start();
10319
- });
10320
- }
10321
- if (proxy.listenerCount("frame") === 0) {
10322
- proxy.on("frame", (frame) => {
10323
- taskRecorder?.addFrame(frame);
10324
- if (emit) emit(JSON.stringify({ type: "browser-frame", data: frame.data, metadata: frame.metadata })).catch(() => {
10325
- });
10326
- });
10327
- proxy.on("status", (s) => {
10328
- if (emit) emit(JSON.stringify({ type: "browser-status", ...s })).catch(() => {
10329
- });
10330
- });
10331
- }
11264
+ const taskScopedCleanups = [];
11265
+ try {
11266
+ const maxIterations = options.taskConfig.maxIterations ?? 50;
11267
+ const webhookUrl = options.taskConfig.webhookUrl;
11268
+ const parentTaskId = options.taskConfig.parentTaskId;
11269
+ const fireWebhook = (type, data) => {
11270
+ if (!webhookUrl) return;
11271
+ sendWebhook(webhookUrl, {
11272
+ type,
11273
+ taskId: this.session.id,
11274
+ sessionId: this.session.id,
11275
+ ...parentTaskId ? { parentTaskId } : {},
11276
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11277
+ data
10332
11278
  });
11279
+ };
11280
+ const completion = { signal: null };
11281
+ const onComplete = (signal) => {
11282
+ completion.signal = signal;
11283
+ };
11284
+ let taskMcpTools = {};
11285
+ if (options.mcpServers && options.mcpServers.length > 0) {
11286
+ const mcpConnection = await connectTaskMcpServers(options.mcpServers, { quiet: true });
11287
+ taskScopedCleanups.push(mcpConnection.close);
11288
+ taskMcpTools = mcpConnection.tools;
11289
+ if (mcpConnection.connected.length > 0) {
11290
+ console.log(`[TASK] connected ${mcpConnection.connected.length} task-scoped MCP server(s): ${mcpConnection.connected.join(", ")}`);
11291
+ }
11292
+ for (const e of mcpConnection.errors) {
11293
+ console.warn(`[TASK] task-scoped MCP server "${e.name}" failed to connect: ${e.error}`);
11294
+ if (options.writeSSE) await options.writeSSE(JSON.stringify({ type: "task-mcp-error", data: { name: e.name, error: e.error } }));
11295
+ }
10333
11296
  }
10334
- };
10335
- const taskTools = await createTools({
10336
- sessionId: this.session.id,
10337
- workingDirectory: this.session.workingDirectory,
10338
- skillsDirectories: config.resolvedSkillsDirectories,
10339
- onBashProgress: bashProgressHandler,
10340
- onWriteFileProgress: (progress) => {
10341
- options.onToolProgress?.({ toolName: "write_file", data: progress });
10342
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "write_file", data: progress })).catch(() => {
10343
- });
10344
- },
10345
- onSearchProgress: (progress) => {
10346
- options.onToolProgress?.({ toolName: "explore_agent", data: progress });
10347
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "explore_agent", data: progress })).catch(() => {
11297
+ const materializedSkills = await materializeTaskSkills(options.skills, this.session.id);
11298
+ if (materializedSkills) taskScopedCleanups.push(materializedSkills.cleanup);
11299
+ const taskSkillsDir = materializedSkills?.dir;
11300
+ let taskRecorder = null;
11301
+ const sessionId = this.session.id;
11302
+ const emit = options.writeSSE;
11303
+ const bashProgressHandler = (progress) => {
11304
+ options.onToolProgress?.({ toolName: "bash", data: progress });
11305
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "bash", data: progress })).catch(() => {
10348
11306
  });
10349
- },
10350
- taskTools: {
10351
- outputSchema: options.taskConfig.outputSchema,
10352
- onComplete,
10353
- onQuestion: async (question) => {
10354
- const payload = {
10355
- questionId: question.questionId,
10356
- question: question.question,
10357
- context: question.context,
10358
- choices: question.choices,
10359
- status: "pending"
10360
- };
10361
- const answerPromise = waitForTaskQuestionAnswer({
10362
- taskId: this.session.id,
10363
- questionId: question.questionId,
10364
- question: question.question,
10365
- context: question.context,
10366
- choices: question.choices
11307
+ const port = progress.browserStreamPort;
11308
+ if (port && progress.status === "started") {
11309
+ Promise.resolve().then(() => (init_stream_proxy(), stream_proxy_exports)).then(({ getOrCreateProxy: getOrCreateProxy2 }) => {
11310
+ const proxy = getOrCreateProxy2(sessionId, port);
11311
+ if (!taskRecorder) {
11312
+ Promise.resolve().then(() => (init_recorder(), recorder_exports)).then(({ FrameRecorder: FrameRecorder2 }) => {
11313
+ taskRecorder = new FrameRecorder2(sessionId);
11314
+ taskRecorder.start();
11315
+ });
11316
+ }
11317
+ if (proxy.listenerCount("frame") === 0) {
11318
+ proxy.on("frame", (frame) => {
11319
+ taskRecorder?.addFrame(frame);
11320
+ if (emit) emit(JSON.stringify({ type: "browser-frame", data: frame.data, metadata: frame.metadata })).catch(() => {
11321
+ });
11322
+ });
11323
+ proxy.on("status", (s) => {
11324
+ if (emit) emit(JSON.stringify({ type: "browser-status", ...s })).catch(() => {
11325
+ });
11326
+ });
11327
+ }
10367
11328
  });
10368
- fireWebhook("task.question", payload);
10369
- if (emit) {
10370
- await emit(JSON.stringify({ type: "task-question", data: payload }));
10371
- }
10372
- const orchId = this.session.config?.orchestratorSessionId;
10373
- if (orchId) {
10374
- pushToInbox(orchId, workerQuestionEvent(
10375
- this.session.id,
10376
- this.session.name || "worker",
10377
- question.question,
10378
- question.questionId
10379
- ));
10380
- }
10381
- const answer = await answerPromise;
10382
- const answeredPayload = {
10383
- questionId: question.questionId,
10384
- answer: answer.answer,
10385
- answeredBy: answer.answeredBy
10386
- };
10387
- fireWebhook("task.question_answered", answeredPayload);
10388
- if (emit) {
10389
- await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
11329
+ }
11330
+ };
11331
+ const taskTools = await createTools({
11332
+ sessionId: this.session.id,
11333
+ workingDirectory: this.session.workingDirectory,
11334
+ onBashProgress: bashProgressHandler,
11335
+ onWriteFileProgress: (progress) => {
11336
+ options.onToolProgress?.({ toolName: "write_file", data: progress });
11337
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "write_file", data: progress })).catch(() => {
11338
+ });
11339
+ },
11340
+ onSearchProgress: (progress) => {
11341
+ options.onToolProgress?.({ toolName: "explore_agent", data: progress });
11342
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "explore_agent", data: progress })).catch(() => {
11343
+ });
11344
+ },
11345
+ // Add the task-scoped skills temp dir (if any) so load_skill can list
11346
+ // and load the inline skills supplied with this task.
11347
+ skillsDirectories: taskSkillsDir ? [...config.resolvedSkillsDirectories, taskSkillsDir] : config.resolvedSkillsDirectories,
11348
+ taskTools: {
11349
+ outputSchema: options.taskConfig.outputSchema,
11350
+ onComplete,
11351
+ onQuestion: async (question) => {
11352
+ const payload = {
11353
+ questionId: question.questionId,
11354
+ question: question.question,
11355
+ context: question.context,
11356
+ choices: question.choices,
11357
+ status: "pending"
11358
+ };
11359
+ const answerPromise = waitForTaskQuestionAnswer({
11360
+ taskId: this.session.id,
11361
+ questionId: question.questionId,
11362
+ question: question.question,
11363
+ context: question.context,
11364
+ choices: question.choices
11365
+ });
11366
+ fireWebhook("task.question", payload);
11367
+ if (emit) {
11368
+ await emit(JSON.stringify({ type: "task-question", data: payload }));
11369
+ }
11370
+ const orchId = this.session.config?.orchestratorSessionId;
11371
+ if (orchId) {
11372
+ pushToInbox(orchId, workerQuestionEvent(
11373
+ this.session.id,
11374
+ this.session.name || "worker",
11375
+ question.question,
11376
+ question.questionId
11377
+ ));
11378
+ }
11379
+ const answer = await answerPromise;
11380
+ const answeredPayload = {
11381
+ questionId: question.questionId,
11382
+ answer: answer.answer,
11383
+ answeredBy: answer.answeredBy
11384
+ };
11385
+ fireWebhook("task.question_answered", answeredPayload);
11386
+ if (emit) {
11387
+ await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
11388
+ }
11389
+ return answer;
10390
11390
  }
10391
- return answer;
10392
11391
  }
11392
+ });
11393
+ for (const [name, t] of Object.entries(taskMcpTools)) {
11394
+ taskTools[name] = t;
10393
11395
  }
10394
- });
10395
- const baseSystemPrompt = await buildSystemPrompt({
10396
- workingDirectory: this.session.workingDirectory,
10397
- skillsDirectories: config.resolvedSkillsDirectories,
10398
- sessionId: this.session.id,
10399
- discoveredSkills: config.discoveredSkills,
10400
- activeFiles: []
10401
- });
10402
- const taskAddendum = buildTaskPromptAddendum(options.taskConfig.outputSchema);
10403
- const systemPrompt = `${baseSystemPrompt}
11396
+ const baseSystemPrompt = await buildSystemPrompt({
11397
+ workingDirectory: this.session.workingDirectory,
11398
+ skillsDirectories: taskSkillsDir ? [...config.resolvedSkillsDirectories, taskSkillsDir] : config.resolvedSkillsDirectories,
11399
+ sessionId: this.session.id,
11400
+ discoveredSkills: config.discoveredSkills,
11401
+ activeFiles: [],
11402
+ taskScopedSkills: materializedSkills ? { always: materializedSkills.always, onDemand: materializedSkills.onDemand } : void 0
11403
+ });
11404
+ const taskAddendum = buildTaskPromptAddendum(options.taskConfig.outputSchema);
11405
+ const systemPrompt = `${baseSystemPrompt}
10404
11406
 
10405
11407
  ${taskAddendum}`;
10406
- fireWebhook("task.started", { prompt: options.prompt });
10407
- if (emit) {
10408
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: options.prompt } }));
10409
- }
10410
- await this.context.addUserMessage(options.prompt);
10411
- let iteration = 0;
10412
- while (iteration < maxIterations) {
10413
- iteration++;
10414
- if (options.abortSignal?.aborted) {
10415
- const cancelError = "Task was cancelled";
10416
- fireWebhook("task.failed", { status: "failed", error: cancelError, iterations: iteration });
10417
- clearInterruptController(this.session.id);
10418
- return { status: "failed", error: cancelError, iterations: iteration };
11408
+ fireWebhook("task.started", { prompt: options.prompt });
11409
+ if (emit) {
11410
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: options.prompt } }));
10419
11411
  }
10420
- const pending = drainInputs(this.session.id);
10421
- for (const p of pending) {
10422
- const labelled = p.source === "orchestrator" ? `[message from orchestrator]
11412
+ await this.context.addUserMessage(options.prompt);
11413
+ let iteration = 0;
11414
+ while (iteration < maxIterations) {
11415
+ iteration++;
11416
+ if (options.abortSignal?.aborted) {
11417
+ const cancelError = "Task was cancelled";
11418
+ fireWebhook("task.failed", { status: "failed", error: cancelError, iterations: iteration });
11419
+ clearInterruptController(this.session.id);
11420
+ return { status: "failed", error: cancelError, iterations: iteration };
11421
+ }
11422
+ const pending = drainInputs(this.session.id);
11423
+ for (const p of pending) {
11424
+ const labelled = p.source === "orchestrator" ? `[message from orchestrator]
10423
11425
  ${p.text}` : p.source === "system" ? `[system note]
10424
11426
  ${p.text}` : p.text;
10425
- if (emit) {
10426
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: labelled } }));
10427
- }
10428
- await this.context.addUserMessage(labelled);
10429
- }
10430
- const interruptController = new AbortController();
10431
- registerInterruptController(this.session.id, interruptController);
10432
- const combinedAbort = options.abortSignal ? anySignal([options.abortSignal, interruptController.signal]) : interruptController.signal;
10433
- const messages = await this.context.getMessages();
10434
- const useAnthropic = isAnthropicModel(this.session.model);
10435
- if (emit) {
10436
- await emit(JSON.stringify({ type: "start", messageId: `msg_${Date.now()}` }));
10437
- }
10438
- let textStarted = false;
10439
- let textId = `text_${Date.now()}`;
10440
- let reasoningId = `reasoning_${Date.now()}`;
10441
- let reasoningStarted = false;
10442
- const toolCallStarts = /* @__PURE__ */ new Set();
10443
- const iterStream = streamText2({
10444
- model: resolveModel(this.session.model),
10445
- system: systemPrompt,
10446
- messages,
10447
- tools: wrapToolsNeverThrow(taskTools),
10448
- stopWhen: stepCountIs2(500),
10449
- abortSignal: combinedAbort,
10450
- providerOptions: useAnthropic ? {
10451
- anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
10452
- } : void 0,
10453
- // See the matching note in `stream()` — repair tool pairing before
10454
- // every step so we never feed the model an orphan tool-call.
10455
- prepareStep: async ({ messages: stepMessages }) => {
10456
- const paired = repairToolPairing(stepMessages);
10457
- const ordered = ensureToolResultsFollowCalls(paired);
10458
- if (ordered === stepMessages) return {};
10459
- return { messages: ordered };
10460
- },
10461
- onStepFinish: async (step) => {
10462
- options.onStepFinish?.(step);
10463
- fireWebhook("task.step_finished", { iteration, text: step.text });
10464
11427
  if (emit) {
10465
- if (textStarted) {
10466
- await emit(JSON.stringify({ type: "text-end", id: textId }));
10467
- textStarted = false;
10468
- textId = `text_${Date.now()}`;
10469
- }
10470
- await emit(JSON.stringify({ type: "finish-step" }));
11428
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: labelled } }));
10471
11429
  }
11430
+ await this.context.addUserMessage(labelled);
10472
11431
  }
10473
- });
10474
- for await (const part of iterStream.fullStream) {
10475
- if (part.type === "text-delta") {
10476
- if (emit) {
10477
- if (!textStarted) {
10478
- await emit(JSON.stringify({ type: "text-start", id: textId }));
10479
- textStarted = true;
11432
+ const interruptController = new AbortController();
11433
+ registerInterruptController(this.session.id, interruptController);
11434
+ const combinedAbort = options.abortSignal ? anySignal([options.abortSignal, interruptController.signal]) : interruptController.signal;
11435
+ const messages = await this.context.getMessages();
11436
+ const useAnthropic = isAnthropicModel(this.session.model);
11437
+ if (emit) {
11438
+ await emit(JSON.stringify({ type: "start", messageId: `msg_${Date.now()}` }));
11439
+ }
11440
+ let textStarted = false;
11441
+ let textId = `text_${Date.now()}`;
11442
+ let reasoningId = `reasoning_${Date.now()}`;
11443
+ let reasoningStarted = false;
11444
+ const toolCallStarts = /* @__PURE__ */ new Set();
11445
+ const iterStream = streamText2({
11446
+ model: resolveModel(this.session.model),
11447
+ system: systemPrompt,
11448
+ messages,
11449
+ tools: wrapToolsNeverThrow(taskTools),
11450
+ stopWhen: stepCountIs2(500),
11451
+ abortSignal: combinedAbort,
11452
+ providerOptions: useAnthropic ? {
11453
+ anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
11454
+ } : void 0,
11455
+ // See the matching note in `stream()` — repair tool pairing before
11456
+ // every step so we never feed the model an orphan tool-call.
11457
+ prepareStep: async ({ messages: stepMessages }) => {
11458
+ const paired = repairToolPairing(stepMessages);
11459
+ const ordered = ensureToolResultsFollowCalls(paired);
11460
+ if (ordered === stepMessages) return {};
11461
+ return { messages: ordered };
11462
+ },
11463
+ onStepFinish: async (step) => {
11464
+ options.onStepFinish?.(step);
11465
+ fireWebhook("task.step_finished", { iteration, text: step.text });
11466
+ if (emit) {
11467
+ if (textStarted) {
11468
+ await emit(JSON.stringify({ type: "text-end", id: textId }));
11469
+ textStarted = false;
11470
+ textId = `text_${Date.now()}`;
11471
+ }
11472
+ await emit(JSON.stringify({ type: "finish-step" }));
10480
11473
  }
10481
- await emit(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
10482
- }
10483
- } else if (part.type === "reasoning-start") {
10484
- if (emit) {
10485
- await emit(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
10486
- reasoningStarted = true;
10487
- }
10488
- } else if (part.type === "reasoning-delta") {
10489
- if (emit) {
10490
- await emit(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
10491
- }
10492
- } else if (part.type === "reasoning-end") {
10493
- if (emit && reasoningStarted) {
10494
- await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
10495
- reasoningStarted = false;
10496
- reasoningId = `reasoning_${Date.now()}`;
10497
- }
10498
- } else if (part.type === "tool-call-streaming-start") {
10499
- if (emit) {
10500
- const p = part;
10501
- await emit(JSON.stringify({ type: "tool-input-start", toolCallId: p.toolCallId, toolName: p.toolName }));
10502
- toolCallStarts.add(p.toolCallId);
10503
- }
10504
- } else if (part.type === "tool-call-delta") {
10505
- if (emit) {
10506
- const p = part;
10507
- await emit(JSON.stringify({ type: "tool-input-delta", toolCallId: p.toolCallId, argsTextDelta: p.argsTextDelta }));
10508
11474
  }
10509
- } else if (part.type === "tool-call") {
10510
- if (emit) {
10511
- if (!toolCallStarts.has(part.toolCallId)) {
10512
- await emit(JSON.stringify({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName }));
10513
- toolCallStarts.add(part.toolCallId);
11475
+ });
11476
+ for await (const part of iterStream.fullStream) {
11477
+ if (part.type === "text-delta") {
11478
+ if (emit) {
11479
+ if (!textStarted) {
11480
+ await emit(JSON.stringify({ type: "text-start", id: textId }));
11481
+ textStarted = true;
11482
+ }
11483
+ await emit(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
11484
+ }
11485
+ } else if (part.type === "reasoning-start") {
11486
+ if (emit) {
11487
+ await emit(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
11488
+ reasoningStarted = true;
11489
+ }
11490
+ } else if (part.type === "reasoning-delta") {
11491
+ if (emit) {
11492
+ await emit(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
11493
+ }
11494
+ } else if (part.type === "reasoning-end") {
11495
+ if (emit && reasoningStarted) {
11496
+ await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
11497
+ reasoningStarted = false;
11498
+ reasoningId = `reasoning_${Date.now()}`;
11499
+ }
11500
+ } else if (part.type === "tool-call-streaming-start") {
11501
+ if (emit) {
11502
+ const p = part;
11503
+ await emit(JSON.stringify({ type: "tool-input-start", toolCallId: p.toolCallId, toolName: p.toolName }));
11504
+ toolCallStarts.add(p.toolCallId);
11505
+ }
11506
+ } else if (part.type === "tool-call-delta") {
11507
+ if (emit) {
11508
+ const p = part;
11509
+ await emit(JSON.stringify({ type: "tool-input-delta", toolCallId: p.toolCallId, argsTextDelta: p.argsTextDelta }));
11510
+ }
11511
+ } else if (part.type === "tool-call") {
11512
+ if (emit) {
11513
+ if (!toolCallStarts.has(part.toolCallId)) {
11514
+ await emit(JSON.stringify({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName }));
11515
+ toolCallStarts.add(part.toolCallId);
11516
+ }
11517
+ const safeInput = part.toolName === "write_file" && part.input && typeof part.input === "object" ? truncateWriteFileInput(part.input) : part.input;
11518
+ await emit(JSON.stringify({ type: "tool-input-available", toolCallId: part.toolCallId, toolName: part.toolName, input: safeInput }));
11519
+ }
11520
+ } else if (part.type === "tool-result") {
11521
+ if (emit) {
11522
+ await emit(JSON.stringify({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output }));
11523
+ }
11524
+ } else if (part.type === "error") {
11525
+ console.error("Task stream error:", part.error);
11526
+ if (emit) {
11527
+ await emit(JSON.stringify({ type: "error", errorText: String(part.error) }));
10514
11528
  }
10515
- const safeInput = part.toolName === "write_file" && part.input && typeof part.input === "object" ? truncateWriteFileInput(part.input) : part.input;
10516
- await emit(JSON.stringify({ type: "tool-input-available", toolCallId: part.toolCallId, toolName: part.toolName, input: safeInput }));
10517
11529
  }
10518
- } else if (part.type === "tool-result") {
10519
- if (emit) {
10520
- await emit(JSON.stringify({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output }));
11530
+ }
11531
+ if (emit && textStarted) {
11532
+ await emit(JSON.stringify({ type: "text-end", id: textId }));
11533
+ }
11534
+ if (emit && reasoningStarted) {
11535
+ await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
11536
+ }
11537
+ const interrupted = interruptController.signal.aborted;
11538
+ clearInterruptController(this.session.id);
11539
+ const iterResponse = await iterStream.response;
11540
+ const responseMessages = iterResponse.messages;
11541
+ await this.context.addResponseMessages(responseMessages);
11542
+ const resultText = await iterStream.text;
11543
+ const resultSteps = await iterStream.steps;
11544
+ if (resultText) {
11545
+ options.onText?.(resultText);
11546
+ fireWebhook("task.message", { iteration, text: resultText });
11547
+ }
11548
+ for (const step of resultSteps) {
11549
+ if (step.toolCalls) {
11550
+ for (const tc of step.toolCalls) {
11551
+ options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
11552
+ fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
11553
+ }
10521
11554
  }
10522
- } else if (part.type === "error") {
10523
- console.error("Task stream error:", part.error);
10524
- if (emit) {
10525
- await emit(JSON.stringify({ type: "error", errorText: String(part.error) }));
11555
+ if (step.toolResults) {
11556
+ for (const tr of step.toolResults) {
11557
+ options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
11558
+ fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
11559
+ }
10526
11560
  }
10527
11561
  }
10528
- }
10529
- if (emit && textStarted) {
10530
- await emit(JSON.stringify({ type: "text-end", id: textId }));
10531
- }
10532
- if (emit && reasoningStarted) {
10533
- await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
10534
- }
10535
- const interrupted = interruptController.signal.aborted;
10536
- clearInterruptController(this.session.id);
10537
- const iterResponse = await iterStream.response;
10538
- const responseMessages = iterResponse.messages;
10539
- await this.context.addResponseMessages(responseMessages);
10540
- const resultText = await iterStream.text;
10541
- const resultSteps = await iterStream.steps;
10542
- if (resultText) {
10543
- options.onText?.(resultText);
10544
- fireWebhook("task.message", { iteration, text: resultText });
10545
- }
10546
- for (const step of resultSteps) {
10547
- if (step.toolCalls) {
10548
- for (const tc of step.toolCalls) {
10549
- options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
10550
- fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
11562
+ if (completion.signal) {
11563
+ const sig = completion.signal;
11564
+ const finalStatus = sig.status;
11565
+ let fileUrls;
11566
+ if (finalStatus === "completed" && sig.result && typeof sig.result === "object") {
11567
+ const resultObj = sig.result;
11568
+ const filePaths = Array.isArray(resultObj.files) ? resultObj.files : [];
11569
+ if (filePaths.length > 0) {
11570
+ fileUrls = await this.uploadTaskFiles(filePaths);
11571
+ }
10551
11572
  }
10552
- }
10553
- if (step.toolResults) {
10554
- for (const tr of step.toolResults) {
10555
- options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
10556
- fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
11573
+ const recordingUrls = await this.finishTaskRecording(taskRecorder);
11574
+ const allFileUrls = [...fileUrls || [], ...recordingUrls];
11575
+ const eventType = finalStatus === "completed" ? "task.completed" : "task.failed";
11576
+ fireWebhook(eventType, {
11577
+ status: finalStatus,
11578
+ result: sig.result,
11579
+ error: sig.error,
11580
+ iterations: iteration,
11581
+ fileUrls: allFileUrls.length > 0 ? allFileUrls : void 0,
11582
+ browserRecordingUrls: recordingUrls.length > 0 ? recordingUrls : void 0
11583
+ });
11584
+ const updatedTask2 = {
11585
+ ...options.taskConfig,
11586
+ status: finalStatus,
11587
+ result: sig.result,
11588
+ error: sig.error,
11589
+ iterations: iteration
11590
+ };
11591
+ await sessionQueries.update(this.session.id, {
11592
+ config: { ...this.session.config, task: updatedTask2 }
11593
+ });
11594
+ const orchId = this.session.config?.orchestratorSessionId;
11595
+ if (orchId) {
11596
+ const summary = finalStatus === "completed" ? typeof sig.result?.summary === "string" ? sig.result.summary : JSON.stringify(sig.result) : sig.error || "unknown error";
11597
+ pushToInbox(orchId, finalStatus === "completed" ? workerCompletedEvent(this.session.id, this.session.name || "worker", summary) : workerFailedEvent(this.session.id, this.session.name || "worker", summary));
10557
11598
  }
11599
+ return {
11600
+ status: finalStatus,
11601
+ result: sig.result,
11602
+ error: sig.error,
11603
+ iterations: iteration
11604
+ };
10558
11605
  }
10559
- }
10560
- if (completion.signal) {
10561
- const sig = completion.signal;
10562
- const finalStatus = sig.status;
10563
- let fileUrls;
10564
- if (finalStatus === "completed" && sig.result && typeof sig.result === "object") {
10565
- const resultObj = sig.result;
10566
- const filePaths = Array.isArray(resultObj.files) ? resultObj.files : [];
10567
- if (filePaths.length > 0) {
10568
- fileUrls = await this.uploadTaskFiles(filePaths);
11606
+ if (!interrupted) {
11607
+ const continuationPrompt = "Continue working on the task. Before calling `complete_task`, VERIFY your work is correct \u2014 re-read edited files, run the linter, run tests if applicable, and check the browser/server if you made UI or API changes. Make sure you searched the right directories and found everything relevant. When fully verified, call `complete_task` with the result. If you cannot complete it, call `task_failed` with a reason.";
11608
+ if (emit) {
11609
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: continuationPrompt } }));
10569
11610
  }
11611
+ await this.context.addUserMessage(continuationPrompt);
10570
11612
  }
10571
- const recordingUrls = await this.finishTaskRecording(taskRecorder);
10572
- const allFileUrls = [...fileUrls || [], ...recordingUrls];
10573
- const eventType = finalStatus === "completed" ? "task.completed" : "task.failed";
10574
- fireWebhook(eventType, {
10575
- status: finalStatus,
10576
- result: sig.result,
10577
- error: sig.error,
10578
- iterations: iteration,
10579
- fileUrls: allFileUrls.length > 0 ? allFileUrls : void 0,
10580
- browserRecordingUrls: recordingUrls.length > 0 ? recordingUrls : void 0
10581
- });
10582
- const updatedTask2 = {
10583
- ...options.taskConfig,
10584
- status: finalStatus,
10585
- result: sig.result,
10586
- error: sig.error,
10587
- iterations: iteration
10588
- };
10589
- await sessionQueries.update(this.session.id, {
10590
- config: { ...this.session.config, task: updatedTask2 }
10591
- });
10592
- const orchId = this.session.config?.orchestratorSessionId;
10593
- if (orchId) {
10594
- const summary = finalStatus === "completed" ? typeof sig.result?.summary === "string" ? sig.result.summary : JSON.stringify(sig.result) : sig.error || "unknown error";
10595
- pushToInbox(orchId, finalStatus === "completed" ? workerCompletedEvent(this.session.id, this.session.name || "worker", summary) : workerFailedEvent(this.session.id, this.session.name || "worker", summary));
10596
- }
10597
- return {
10598
- status: finalStatus,
10599
- result: sig.result,
10600
- error: sig.error,
10601
- iterations: iteration
10602
- };
10603
11613
  }
10604
- if (!interrupted) {
10605
- const continuationPrompt = "Continue working on the task. Before calling `complete_task`, VERIFY your work is correct \u2014 re-read edited files, run the linter, run tests if applicable, and check the browser/server if you made UI or API changes. Make sure you searched the right directories and found everything relevant. When fully verified, call `complete_task` with the result. If you cannot complete it, call `task_failed` with a reason.";
10606
- if (emit) {
10607
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: continuationPrompt } }));
11614
+ clearInterruptController(this.session.id);
11615
+ const timeoutError = `Task did not complete within ${maxIterations} iterations`;
11616
+ const timeoutRecordingUrls = await this.finishTaskRecording(taskRecorder);
11617
+ fireWebhook("task.failed", {
11618
+ status: "failed",
11619
+ error: timeoutError,
11620
+ iterations: iteration,
11621
+ browserRecordingUrls: timeoutRecordingUrls.length > 0 ? timeoutRecordingUrls : void 0
11622
+ });
11623
+ const updatedTask = {
11624
+ ...options.taskConfig,
11625
+ status: "failed",
11626
+ error: timeoutError,
11627
+ iterations: iteration
11628
+ };
11629
+ await sessionQueries.update(this.session.id, {
11630
+ config: { ...this.session.config, task: updatedTask }
11631
+ });
11632
+ const orchIdTimeout = this.session.config?.orchestratorSessionId;
11633
+ if (orchIdTimeout) {
11634
+ pushToInbox(orchIdTimeout, workerFailedEvent(this.session.id, this.session.name || "worker", timeoutError));
11635
+ }
11636
+ return { status: "failed", error: timeoutError, iterations: iteration };
11637
+ } finally {
11638
+ for (const cleanup2 of taskScopedCleanups) {
11639
+ try {
11640
+ await cleanup2();
11641
+ } catch {
10608
11642
  }
10609
- await this.context.addUserMessage(continuationPrompt);
10610
11643
  }
10611
11644
  }
10612
- clearInterruptController(this.session.id);
10613
- const timeoutError = `Task did not complete within ${maxIterations} iterations`;
10614
- const timeoutRecordingUrls = await this.finishTaskRecording(taskRecorder);
10615
- fireWebhook("task.failed", {
10616
- status: "failed",
10617
- error: timeoutError,
10618
- iterations: iteration,
10619
- browserRecordingUrls: timeoutRecordingUrls.length > 0 ? timeoutRecordingUrls : void 0
10620
- });
10621
- const updatedTask = {
10622
- ...options.taskConfig,
10623
- status: "failed",
10624
- error: timeoutError,
10625
- iterations: iteration
10626
- };
10627
- await sessionQueries.update(this.session.id, {
10628
- config: { ...this.session.config, task: updatedTask }
10629
- });
10630
- const orchIdTimeout = this.session.config?.orchestratorSessionId;
10631
- if (orchIdTimeout) {
10632
- pushToInbox(orchIdTimeout, workerFailedEvent(this.session.id, this.session.name || "worker", timeoutError));
10633
- }
10634
- return { status: "failed", error: timeoutError, iterations: iteration };
10635
11645
  }
10636
11646
  /**
10637
11647
  * Stop a task-mode browser recording, encode to MP4, upload to GCS.
@@ -10691,11 +11701,11 @@ ${p.text}` : p.text;
10691
11701
  const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
10692
11702
  if (!isRemoteConfigured2()) return [];
10693
11703
  const { readFile: readFile13 } = await import("fs/promises");
10694
- const { join: join18, basename: basename7 } = await import("path");
11704
+ const { join: join20, basename: basename7 } = await import("path");
10695
11705
  const urls = [];
10696
11706
  for (const filePath of filePaths) {
10697
11707
  try {
10698
- const fullPath = filePath.startsWith("/") ? filePath : join18(this.session.workingDirectory, filePath);
11708
+ const fullPath = filePath.startsWith("/") ? filePath : join20(this.session.workingDirectory, filePath);
10699
11709
  const fileName = basename7(fullPath);
10700
11710
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
10701
11711
  const mimeMap = {
@@ -10757,7 +11767,7 @@ ${p.text}` : p.text;
10757
11767
  description: originalTool.description || "",
10758
11768
  inputSchema: originalTool.inputSchema || z15.object({}),
10759
11769
  execute: async (input, toolOptions) => {
10760
- const toolCallId = toolOptions.toolCallId || nanoid8();
11770
+ const toolCallId = toolOptions.toolCallId || nanoid9();
10761
11771
  const execution = toolExecutionQueries.create({
10762
11772
  sessionId: this.session.id,
10763
11773
  toolName: name,
@@ -10798,219 +11808,71 @@ ${p.text}` : p.text;
10798
11808
  await toolExecutionQueries.complete(exec7.id, null, error.message);
10799
11809
  throw error;
10800
11810
  }
10801
- }
10802
- });
10803
- }
10804
- return wrappedTools;
10805
- }
10806
- /**
10807
- * Wait for all pending approvals
10808
- */
10809
- async waitForApprovals() {
10810
- return Array.from(this.pendingApprovals.values());
10811
- }
10812
- /**
10813
- * Approve a pending tool execution
10814
- */
10815
- async approve(toolCallId) {
10816
- const resolver = approvalResolvers.get(toolCallId);
10817
- if (resolver) {
10818
- resolver.resolve(true);
10819
- return { approved: true };
10820
- }
10821
- const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
10822
- const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
10823
- if (!execution) {
10824
- throw new Error(`No pending approval for tool call: ${toolCallId}`);
10825
- }
10826
- await toolExecutionQueries.approve(execution.id);
10827
- return { approved: true };
10828
- }
10829
- /**
10830
- * Reject a pending tool execution
10831
- */
10832
- async reject(toolCallId, reason) {
10833
- const resolver = approvalResolvers.get(toolCallId);
10834
- if (resolver) {
10835
- resolver.reason = reason;
10836
- resolver.resolve(false);
10837
- return { rejected: true };
10838
- }
10839
- const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
10840
- const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
10841
- if (!execution) {
10842
- throw new Error(`No pending approval for tool call: ${toolCallId}`);
10843
- }
10844
- await toolExecutionQueries.reject(execution.id);
10845
- return { rejected: true };
10846
- }
10847
- /**
10848
- * Get pending approvals
10849
- */
10850
- async getPendingApprovals() {
10851
- return toolExecutionQueries.getPendingApprovals(this.session.id);
10852
- }
10853
- /**
10854
- * Get context statistics
10855
- */
10856
- getContextStats() {
10857
- return this.context.getStats();
10858
- }
10859
- /**
10860
- * Clear conversation context (start fresh)
10861
- */
10862
- clearContext() {
10863
- this.context.clear();
10864
- }
10865
- };
10866
- }
10867
- });
10868
-
10869
- // src/agent/session-lock.ts
10870
- async function withSessionLock(sessionId, fn) {
10871
- let state2 = locks.get(sessionId);
10872
- if (!state2) {
10873
- state2 = { tail: Promise.resolve(), pending: 0 };
10874
- locks.set(sessionId, state2);
10875
- }
10876
- state2.pending++;
10877
- const prev = state2.tail;
10878
- let release;
10879
- const next = new Promise((resolve13) => {
10880
- release = resolve13;
10881
- });
10882
- state2.tail = prev.then(() => next);
10883
- await prev;
10884
- try {
10885
- return await fn();
10886
- } finally {
10887
- release();
10888
- state2.pending--;
10889
- if (state2.pending === 0 && locks.get(sessionId) === state2) {
10890
- locks.delete(sessionId);
10891
- }
10892
- }
10893
- }
10894
- var locks;
10895
- var init_session_lock = __esm({
10896
- "src/agent/session-lock.ts"() {
10897
- "use strict";
10898
- locks = /* @__PURE__ */ new Map();
10899
- }
10900
- });
10901
-
10902
- // src/orchestrator/webhook-events.ts
10903
- import { existsSync as existsSync18, readFileSync as readFileSync9, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
10904
- import { dirname as dirname7, join as join12 } from "path";
10905
- import { nanoid as nanoid9 } from "nanoid";
10906
- function logFilePath() {
10907
- return join12(getAppDataDirectory(), "webhook-events.jsonl");
10908
- }
10909
- function ensureLoaded() {
10910
- if (cache !== null) return cache;
10911
- cache = [];
10912
- try {
10913
- const p = logFilePath();
10914
- if (!existsSync18(p)) return cache;
10915
- const lines = readFileSync9(p, "utf-8").split("\n").filter(Boolean);
10916
- for (const line of lines) {
10917
- try {
10918
- cache.push(JSON.parse(line));
10919
- } catch {
10920
- }
10921
- }
10922
- if (cache.length > MAX_EVENTS) {
10923
- cache = cache.slice(-MAX_EVENTS);
10924
- try {
10925
- writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
10926
- } catch {
10927
- }
10928
- }
10929
- } catch {
10930
- }
10931
- return cache;
10932
- }
10933
- function appendEvent(ev) {
10934
- const list = ensureLoaded();
10935
- list.push(ev);
10936
- if (list.length > MAX_EVENTS) list.shift();
10937
- try {
10938
- const p = logFilePath();
10939
- mkdirSync7(dirname7(p), { recursive: true });
10940
- appendFileSync3(p, JSON.stringify(ev) + "\n");
10941
- } catch {
10942
- }
10943
- }
10944
- function recordEvent(ev) {
10945
- const full = {
10946
- id: ev.id ?? nanoid9(),
10947
- ts: ev.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
10948
- source: ev.source,
10949
- status: ev.status,
10950
- subtype: ev.subtype,
10951
- channel: ev.channel,
10952
- user: ev.user,
10953
- textSnippet: ev.textSnippet?.slice(0, 200),
10954
- dropReason: ev.dropReason,
10955
- error: ev.error,
10956
- sessionId: ev.sessionId,
10957
- durationMs: ev.durationMs,
10958
- meta: ev.meta
10959
- };
10960
- appendEvent(full);
10961
- return full.id;
10962
- }
10963
- function updateEvent(id, patch) {
10964
- const list = ensureLoaded();
10965
- const i = list.findIndex((e) => e.id === id);
10966
- if (i < 0) return;
10967
- list[i] = { ...list[i], ...patch };
10968
- try {
10969
- const p = logFilePath();
10970
- mkdirSync7(dirname7(p), { recursive: true });
10971
- writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
10972
- } catch {
10973
- }
10974
- }
10975
- function listEvents(filter = {}) {
10976
- const list = ensureLoaded();
10977
- const q = filter.q?.toLowerCase();
10978
- const sinceTs = filter.since ? Date.parse(filter.since) : -Infinity;
10979
- const beforeTs = filter.before ? Date.parse(filter.before) : Infinity;
10980
- const matched = list.filter((e) => {
10981
- if (filter.source && e.source !== filter.source) return false;
10982
- if (filter.status && e.status !== filter.status) return false;
10983
- const t = Date.parse(e.ts);
10984
- if (t < sinceTs) return false;
10985
- if (t >= beforeTs) return false;
10986
- if (q) {
10987
- const hay = `${e.channel ?? ""} ${e.user ?? ""} ${e.textSnippet ?? ""} ${e.dropReason ?? ""} ${e.error ?? ""} ${e.subtype ?? ""}`.toLowerCase();
10988
- if (!hay.includes(q)) return false;
10989
- }
10990
- return true;
10991
- });
10992
- matched.reverse();
10993
- const offset = Math.max(0, filter.offset ?? 0);
10994
- const limit = Math.min(500, Math.max(1, filter.limit ?? 50));
10995
- return {
10996
- events: matched.slice(offset, offset + limit),
10997
- total: matched.length
10998
- };
10999
- }
11000
- function clearAllEvents() {
11001
- cache = [];
11002
- try {
11003
- writeFileSync4(logFilePath(), "");
11004
- } catch {
11005
- }
11006
- }
11007
- var MAX_EVENTS, cache;
11008
- var init_webhook_events = __esm({
11009
- "src/orchestrator/webhook-events.ts"() {
11010
- "use strict";
11011
- init_config();
11012
- MAX_EVENTS = 1e3;
11013
- cache = null;
11811
+ }
11812
+ });
11813
+ }
11814
+ return wrappedTools;
11815
+ }
11816
+ /**
11817
+ * Wait for all pending approvals
11818
+ */
11819
+ async waitForApprovals() {
11820
+ return Array.from(this.pendingApprovals.values());
11821
+ }
11822
+ /**
11823
+ * Approve a pending tool execution
11824
+ */
11825
+ async approve(toolCallId) {
11826
+ const resolver = approvalResolvers.get(toolCallId);
11827
+ if (resolver) {
11828
+ resolver.resolve(true);
11829
+ return { approved: true };
11830
+ }
11831
+ const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
11832
+ const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
11833
+ if (!execution) {
11834
+ throw new Error(`No pending approval for tool call: ${toolCallId}`);
11835
+ }
11836
+ await toolExecutionQueries.approve(execution.id);
11837
+ return { approved: true };
11838
+ }
11839
+ /**
11840
+ * Reject a pending tool execution
11841
+ */
11842
+ async reject(toolCallId, reason) {
11843
+ const resolver = approvalResolvers.get(toolCallId);
11844
+ if (resolver) {
11845
+ resolver.reason = reason;
11846
+ resolver.resolve(false);
11847
+ return { rejected: true };
11848
+ }
11849
+ const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
11850
+ const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
11851
+ if (!execution) {
11852
+ throw new Error(`No pending approval for tool call: ${toolCallId}`);
11853
+ }
11854
+ await toolExecutionQueries.reject(execution.id);
11855
+ return { rejected: true };
11856
+ }
11857
+ /**
11858
+ * Get pending approvals
11859
+ */
11860
+ async getPendingApprovals() {
11861
+ return toolExecutionQueries.getPendingApprovals(this.session.id);
11862
+ }
11863
+ /**
11864
+ * Get context statistics
11865
+ */
11866
+ getContextStats() {
11867
+ return this.context.getStats();
11868
+ }
11869
+ /**
11870
+ * Clear conversation context (start fresh)
11871
+ */
11872
+ clearContext() {
11873
+ this.context.clear();
11874
+ }
11875
+ };
11014
11876
  }
11015
11877
  });
11016
11878
 
@@ -11086,7 +11948,24 @@ async function runDaemonTurn(sessionId, events) {
11086
11948
  durationMs: finishedAt.getTime() - startedAt.getTime(),
11087
11949
  meta: { triggeredBy: events.map((e) => e.content?.slice(0, 80)) }
11088
11950
  });
11951
+ try {
11952
+ resolveBatchOnTurnEnd(events, !error);
11953
+ } catch (err) {
11954
+ console.error("[daemon] ack bookkeeping threw:", err?.message || err);
11955
+ }
11089
11956
  broadcast({ sessionId, text: trimmed, triggeredBy: events, startedAt, finishedAt, error });
11957
+ const seen = /* @__PURE__ */ new Set();
11958
+ for (const ev of events) {
11959
+ if (ev.ref?.channel !== "slack") continue;
11960
+ const ref = ev.ref;
11961
+ const channel = ref.slackChannel;
11962
+ const ts = ref.messageTs;
11963
+ if (!channel || !ts) continue;
11964
+ const key2 = `${channel}\u241F${ts}`;
11965
+ if (seen.has(key2)) continue;
11966
+ seen.add(key2);
11967
+ void removeLoadingReaction(channel, ts);
11968
+ }
11090
11969
  }
11091
11970
  var listeners;
11092
11971
  var init_daemon = __esm({
@@ -11097,6 +11976,8 @@ var init_daemon = __esm({
11097
11976
  init_db();
11098
11977
  init_inbox();
11099
11978
  init_webhook_events();
11979
+ init_inbox_acks();
11980
+ init_client3();
11100
11981
  listeners = /* @__PURE__ */ new Map();
11101
11982
  }
11102
11983
  });
@@ -11195,6 +12076,233 @@ var init_ensure_orchestrator = __esm({
11195
12076
  }
11196
12077
  });
11197
12078
 
12079
+ // src/orchestrator/self-update.ts
12080
+ var self_update_exports = {};
12081
+ __export(self_update_exports, {
12082
+ __test: () => __test,
12083
+ startSelfUpdater: () => startSelfUpdater,
12084
+ stopSelfUpdater: () => stopSelfUpdater
12085
+ });
12086
+ import { spawn as spawn2, execFile } from "child_process";
12087
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync10 } from "fs";
12088
+ import { dirname as dirname10, join as join18 } from "path";
12089
+ import { fileURLToPath as fileURLToPath4 } from "url";
12090
+ function currentVersion2() {
12091
+ const here = dirname10(fileURLToPath4(import.meta.url));
12092
+ const candidates = [
12093
+ join18(here, "..", "..", "package.json"),
12094
+ join18(here, "..", "package.json"),
12095
+ join18(process.cwd(), "package.json")
12096
+ ];
12097
+ for (const p of candidates) {
12098
+ try {
12099
+ const pkg = JSON.parse(readFileSync11(p, "utf8"));
12100
+ if (pkg.name === "sparkecoder" && pkg.version) return pkg.version;
12101
+ } catch {
12102
+ }
12103
+ }
12104
+ return "0.0.0";
12105
+ }
12106
+ function isLikelyGlobalInstall() {
12107
+ const here = dirname10(fileURLToPath4(import.meta.url));
12108
+ return here.includes("/node_modules/sparkecoder/") || here.includes("\\node_modules\\sparkecoder\\");
12109
+ }
12110
+ function isEnabled() {
12111
+ if (process.env.SPARKECODER_AUTO_UPDATE === "false" || process.env.SPARKECODER_AUTO_UPDATE === "0") return false;
12112
+ try {
12113
+ const cfg = getConfig();
12114
+ if (cfg?.autoUpdate?.enabled === false) return false;
12115
+ } catch {
12116
+ }
12117
+ return true;
12118
+ }
12119
+ function remoteUrl() {
12120
+ try {
12121
+ const cfg = getConfig();
12122
+ const url = cfg?.remoteServer?.url;
12123
+ return typeof url === "string" && url.length > 0 ? url.replace(/\/+$/, "") : null;
12124
+ } catch {
12125
+ return null;
12126
+ }
12127
+ }
12128
+ function intervalMs() {
12129
+ try {
12130
+ const h = getConfig()?.autoUpdate?.intervalHours;
12131
+ if (typeof h === "number" && h > 0) return h * 60 * 6e4;
12132
+ } catch {
12133
+ }
12134
+ return DEFAULT_INTERVAL_HOURS * 60 * 6e4;
12135
+ }
12136
+ function semverGt(a, b) {
12137
+ const parse = (v) => v.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
12138
+ const pa = parse(a);
12139
+ const pb = parse(b);
12140
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
12141
+ const x = pa[i] ?? 0;
12142
+ const y = pb[i] ?? 0;
12143
+ if (x > y) return true;
12144
+ if (x < y) return false;
12145
+ }
12146
+ return false;
12147
+ }
12148
+ function statePath() {
12149
+ try {
12150
+ return join18(getAppDataDirectory(), "self-update-state.json");
12151
+ } catch {
12152
+ return null;
12153
+ }
12154
+ }
12155
+ function readState() {
12156
+ const p = statePath();
12157
+ if (!p) return {};
12158
+ try {
12159
+ return JSON.parse(readFileSync11(p, "utf8"));
12160
+ } catch {
12161
+ return {};
12162
+ }
12163
+ }
12164
+ function writeState(s) {
12165
+ const p = statePath();
12166
+ if (!p) return;
12167
+ try {
12168
+ mkdirSync10(dirname10(p), { recursive: true });
12169
+ writeFileSync7(p, JSON.stringify(s));
12170
+ } catch {
12171
+ }
12172
+ }
12173
+ function attemptedRecently(target, now) {
12174
+ const s = readState();
12175
+ return s.lastTarget === target && typeof s.lastAttemptAt === "number" && now - s.lastAttemptAt < RETRY_COOLDOWN_MS;
12176
+ }
12177
+ function latestPublishedVersion() {
12178
+ return new Promise((resolve13) => {
12179
+ execFile("npm", ["view", "sparkecoder", "version"], { timeout: 3e4 }, (err, stdout) => {
12180
+ if (err) {
12181
+ resolve13(null);
12182
+ return;
12183
+ }
12184
+ const v = String(stdout).trim();
12185
+ resolve13(/^\d+\.\d+\.\d+/.test(v) ? v : null);
12186
+ });
12187
+ });
12188
+ }
12189
+ function runInstaller(url) {
12190
+ const secret = process.env.SPARKECODER_SETUP_SECRET || process.env.SPARKECODER_TUNNEL_SECRET || "";
12191
+ const query = secret ? `?secret=${encodeURIComponent(secret)}` : "";
12192
+ const oneLiner = `bash -c "$(curl -fsSL '${url}/install.sh${query}')" >/tmp/sparkecoder-selfupdate.log 2>&1`;
12193
+ const child = spawn2("bash", ["-lc", oneLiner], {
12194
+ detached: true,
12195
+ stdio: "ignore"
12196
+ });
12197
+ child.unref();
12198
+ }
12199
+ async function checkAndUpdate() {
12200
+ if (upgrading || !isEnabled()) return;
12201
+ const url = remoteUrl();
12202
+ if (!url) return;
12203
+ const latest = await latestPublishedVersion();
12204
+ if (!latest) return;
12205
+ const current = currentVersion2();
12206
+ if (!semverGt(latest, current)) return;
12207
+ const now = Date.now();
12208
+ if (attemptedRecently(latest, now)) {
12209
+ console.log(`[self-update] v${latest} already attempted recently; skipping until cooldown elapses`);
12210
+ return;
12211
+ }
12212
+ upgrading = true;
12213
+ const announced = await announceUpdate(latest);
12214
+ const delay = announced ? ANNOUNCE_GRACE_MS : 0;
12215
+ if (announced) {
12216
+ console.log(`[self-update] announced v${latest} in Slack; updating in ${Math.round(delay / 6e4)}m`);
12217
+ }
12218
+ const t = setTimeout(() => doInstall(latest, url, current), delay);
12219
+ if (typeof t.unref === "function") t.unref();
12220
+ }
12221
+ function doInstall(latest, url, current) {
12222
+ const prev = readState();
12223
+ writeState({
12224
+ lastTarget: latest,
12225
+ lastAttemptAt: Date.now(),
12226
+ attempts: prev.lastTarget === latest ? (prev.attempts ?? 0) + 1 : 1
12227
+ });
12228
+ console.log(`[self-update] newer version available: v${current} \u2192 v${latest}; re-running installer`);
12229
+ try {
12230
+ runInstaller(url);
12231
+ } catch (err) {
12232
+ upgrading = false;
12233
+ console.warn("[self-update] failed to launch installer:", err?.message || err);
12234
+ }
12235
+ }
12236
+ async function findOrchestratorId() {
12237
+ try {
12238
+ const { sessionQueries: sessionQueries2 } = await Promise.resolve().then(() => (init_db(), db_exports));
12239
+ const all = await sessionQueries2.list(500, 0);
12240
+ const orch = all.find((s) => s?.config?.role === "orchestrator");
12241
+ return orch?.id ?? null;
12242
+ } catch {
12243
+ return null;
12244
+ }
12245
+ }
12246
+ async function announceUpdate(target) {
12247
+ try {
12248
+ const { isSlackConfigured: isSlackConfigured2 } = await Promise.resolve().then(() => (init_client3(), client_exports));
12249
+ if (!isSlackConfigured2()) return false;
12250
+ const orchId = await findOrchestratorId();
12251
+ if (!orchId) return false;
12252
+ const { pushToInbox: pushToInbox2 } = await Promise.resolve().then(() => (init_inbox(), inbox_exports));
12253
+ pushToInbox2(orchId, {
12254
+ ref: { channel: "system", kind: "worker.completed", workerId: "self-update", workerName: "self-update" },
12255
+ content: `[SYSTEM self-update] A software update to v${target} will begin in about 5 minutes and the service will restart briefly (expect ~1\u20132 minutes of downtime). Send a short Slack heads-up to whoever you normally talk to / the bot owner that you'll be offline for a quick update, then carry on. Use the messenger tool to deliver it. If you genuinely don't know who to tell, post in the most relevant channel you've recently been active in; if Slack isn't reachable, skip silently \u2014 the update will proceed regardless.`,
12256
+ wake: "now",
12257
+ enqueuedAt: /* @__PURE__ */ new Date()
12258
+ });
12259
+ return true;
12260
+ } catch {
12261
+ return false;
12262
+ }
12263
+ }
12264
+ function startSelfUpdater() {
12265
+ if (started) return;
12266
+ started = true;
12267
+ if (!isEnabled()) {
12268
+ console.log("[self-update] disabled");
12269
+ return;
12270
+ }
12271
+ if (!isLikelyGlobalInstall()) {
12272
+ console.log("[self-update] skipped (not a global install)");
12273
+ return;
12274
+ }
12275
+ const kickoff = setTimeout(() => {
12276
+ void checkAndUpdate();
12277
+ timer = setInterval(() => {
12278
+ void checkAndUpdate();
12279
+ }, intervalMs());
12280
+ if (typeof timer.unref === "function") timer.unref();
12281
+ }, INITIAL_DELAY_MS);
12282
+ if (typeof kickoff.unref === "function") kickoff.unref();
12283
+ }
12284
+ function stopSelfUpdater() {
12285
+ if (timer) {
12286
+ clearInterval(timer);
12287
+ timer = null;
12288
+ }
12289
+ }
12290
+ var INITIAL_DELAY_MS, DEFAULT_INTERVAL_HOURS, ANNOUNCE_GRACE_MS, RETRY_COOLDOWN_MS, timer, started, upgrading, __test;
12291
+ var init_self_update = __esm({
12292
+ "src/orchestrator/self-update.ts"() {
12293
+ "use strict";
12294
+ init_config();
12295
+ INITIAL_DELAY_MS = 5 * 6e4;
12296
+ DEFAULT_INTERVAL_HOURS = 6;
12297
+ ANNOUNCE_GRACE_MS = 5 * 6e4;
12298
+ RETRY_COOLDOWN_MS = 24 * 60 * 6e4;
12299
+ timer = null;
12300
+ started = false;
12301
+ upgrading = false;
12302
+ __test = { currentVersion: currentVersion2, semverGt, isLikelyGlobalInstall };
12303
+ }
12304
+ });
12305
+
11198
12306
  // src/tasks/scheduler.ts
11199
12307
  var scheduler_exports = {};
11200
12308
  __export(scheduler_exports, {
@@ -11281,11 +12389,11 @@ import { Hono as Hono10 } from "hono";
11281
12389
  import { serve } from "@hono/node-server";
11282
12390
  import { cors } from "hono/cors";
11283
12391
  import { logger } from "hono/logger";
11284
- import { existsSync as existsSync22, mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "fs";
11285
- import { resolve as resolve12, dirname as dirname10, join as join17 } from "path";
11286
- import { spawn as spawn2 } from "child_process";
12392
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, writeFileSync as writeFileSync8 } from "fs";
12393
+ import { resolve as resolve12, dirname as dirname11, join as join19 } from "path";
12394
+ import { spawn as spawn3 } from "child_process";
11287
12395
  import { createServer as createNetServer } from "net";
11288
- import { fileURLToPath as fileURLToPath4 } from "url";
12396
+ import { fileURLToPath as fileURLToPath5 } from "url";
11289
12397
 
11290
12398
  // src/server/routes/sessions.ts
11291
12399
  init_db();
@@ -11298,7 +12406,7 @@ import { zValidator } from "@hono/zod-validator";
11298
12406
  import { z as z16 } from "zod";
11299
12407
  import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
11300
12408
  import { readdir as readdir6 } from "fs/promises";
11301
- import { join as join13, basename as basename5, extname as extname8, relative as relative9 } from "path";
12409
+ import { join as join14, basename as basename5, extname as extname8, relative as relative9 } from "path";
11302
12410
  import { nanoid as nanoid10 } from "nanoid";
11303
12411
 
11304
12412
  // src/tasks/agent-status.ts
@@ -11939,7 +13047,7 @@ sessions2.get("/:id/diff/:filePath", async (c) => {
11939
13047
  });
11940
13048
  function getAttachmentsDir(sessionId) {
11941
13049
  const appDataDir = getAppDataDirectory();
11942
- return join13(appDataDir, "attachments", sessionId);
13050
+ return join14(appDataDir, "attachments", sessionId);
11943
13051
  }
11944
13052
  function ensureAttachmentsDir(sessionId) {
11945
13053
  const dir = getAttachmentsDir(sessionId);
@@ -11960,7 +13068,7 @@ sessions2.get("/:id/attachments", async (c) => {
11960
13068
  }
11961
13069
  const files = readdirSync3(dir);
11962
13070
  const attachments = files.map((filename) => {
11963
- const filePath = join13(dir, filename);
13071
+ const filePath = join14(dir, filename);
11964
13072
  const stats = statSync2(filePath);
11965
13073
  return {
11966
13074
  id: filename.split("_")[0],
@@ -11995,7 +13103,7 @@ sessions2.post("/:id/attachments", async (c) => {
11995
13103
  const id = nanoid10(10);
11996
13104
  const ext = extname8(file.name) || "";
11997
13105
  const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
11998
- const filePath = join13(dir, safeFilename);
13106
+ const filePath = join14(dir, safeFilename);
11999
13107
  const arrayBuffer = await file.arrayBuffer();
12000
13108
  writeFileSync5(filePath, Buffer.from(arrayBuffer));
12001
13109
  return c.json({
@@ -12021,7 +13129,7 @@ sessions2.post("/:id/attachments", async (c) => {
12021
13129
  const id = nanoid10(10);
12022
13130
  const ext = extname8(body.filename) || "";
12023
13131
  const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
12024
- const filePath = join13(dir, safeFilename);
13132
+ const filePath = join14(dir, safeFilename);
12025
13133
  let base64Data = body.data;
12026
13134
  if (base64Data.includes(",")) {
12027
13135
  base64Data = base64Data.split(",")[1];
@@ -12058,7 +13166,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
12058
13166
  if (!file) {
12059
13167
  return c.json({ error: "Attachment not found" }, 404);
12060
13168
  }
12061
- const filePath = join13(dir, file);
13169
+ const filePath = join14(dir, file);
12062
13170
  unlinkSync2(filePath);
12063
13171
  return c.json({ success: true, id: attachmentId });
12064
13172
  });
@@ -12141,7 +13249,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
12141
13249
  const entries = await readdir6(currentDir, { withFileTypes: true });
12142
13250
  for (const entry2 of entries) {
12143
13251
  if (results.length >= limit * 2) break;
12144
- const fullPath = join13(currentDir, entry2.name);
13252
+ const fullPath = join14(currentDir, entry2.name);
12145
13253
  const relativePath = relative9(baseDir, fullPath);
12146
13254
  if (entry2.isDirectory() && IGNORED_DIRECTORIES.has(entry2.name)) {
12147
13255
  continue;
@@ -12301,7 +13409,7 @@ import { Hono as Hono2 } from "hono";
12301
13409
  import { zValidator as zValidator2 } from "@hono/zod-validator";
12302
13410
  import { z as z17 } from "zod";
12303
13411
  import { existsSync as existsSync20, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
12304
- import { join as join14 } from "path";
13412
+ import { join as join15 } from "path";
12305
13413
 
12306
13414
  // src/agent/missing-tool-recovery.ts
12307
13415
  init_db();
@@ -12568,6 +13676,7 @@ init_stream_proxy();
12568
13676
  init_recorder();
12569
13677
  init_remote();
12570
13678
  init_resize_image();
13679
+ init_local_device_time();
12571
13680
  var sessionRecorders = /* @__PURE__ */ new Map();
12572
13681
  var MAX_TOOL_INPUT_LENGTH = 8 * 1024;
12573
13682
  var MAX_TOOL_INPUT_PREVIEW = 2 * 1024;
@@ -12683,7 +13792,7 @@ var rejectSchema = z17.object({
12683
13792
  var streamAbortControllers = /* @__PURE__ */ new Map();
12684
13793
  function getAttachmentsDirectory(sessionId) {
12685
13794
  const appDataDir = getAppDataDirectory();
12686
- return join14(appDataDir, "attachments", sessionId);
13795
+ return join15(appDataDir, "attachments", sessionId);
12687
13796
  }
12688
13797
  async function saveAttachmentToDisk(sessionId, attachment, index) {
12689
13798
  const attachmentsDir = getAttachmentsDirectory(sessionId);
@@ -12706,7 +13815,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
12706
13815
  attachment.mediaType = resized.mediaType;
12707
13816
  attachment.data = buffer.toString("base64");
12708
13817
  }
12709
- const filePath = join14(attachmentsDir, filename);
13818
+ const filePath = join15(attachmentsDir, filename);
12710
13819
  writeFileSync6(filePath, buffer);
12711
13820
  return filePath;
12712
13821
  }
@@ -13083,9 +14192,12 @@ agents.post(
13083
14192
  if (!session) {
13084
14193
  return c.json({ error: "Session not found" }, 404);
13085
14194
  }
13086
- if (session.config?.role === "orchestrator" && !/^\[\w+/.test(prompt)) {
14195
+ if (session.config?.role === "orchestrator" && !/^\[(WEB|SLACK|SYSTEM|SCHEDULE|WEBHOOK)\b/.test(prompt)) {
13087
14196
  prompt = `[WEB] ${prompt}`;
13088
14197
  }
14198
+ if (session.config?.role === "orchestrator") {
14199
+ prompt = prependLocalDeviceTimeToUserMessage(prompt);
14200
+ }
13089
14201
  const nextSequence = await messageQueries.getNextSequence(id);
13090
14202
  await createCheckpoint(id, session.workingDirectory, nextSequence);
13091
14203
  let userMessageContent;
@@ -13695,17 +14807,17 @@ import { zValidator as zValidator3 } from "@hono/zod-validator";
13695
14807
  import { z as z18 } from "zod";
13696
14808
  import { readFileSync as readFileSync10 } from "fs";
13697
14809
  import { fileURLToPath as fileURLToPath3 } from "url";
13698
- import { dirname as dirname8, join as join15 } from "path";
14810
+ import { dirname as dirname8, join as join16 } from "path";
13699
14811
  var __filename = fileURLToPath3(import.meta.url);
13700
14812
  var __dirname = dirname8(__filename);
13701
14813
  var possiblePaths = [
13702
- join15(__dirname, "../package.json"),
14814
+ join16(__dirname, "../package.json"),
13703
14815
  // From dist/server -> dist/../package.json
13704
- join15(__dirname, "../../package.json"),
14816
+ join16(__dirname, "../../package.json"),
13705
14817
  // From dist/server (if nested differently)
13706
- join15(__dirname, "../../../package.json"),
14818
+ join16(__dirname, "../../../package.json"),
13707
14819
  // From src/server/routes (development)
13708
- join15(process.cwd(), "package.json")
14820
+ join16(process.cwd(), "package.json")
13709
14821
  // From current working directory
13710
14822
  ];
13711
14823
  var currentVersion = "0.0.0";
@@ -14166,6 +15278,25 @@ import { nanoid as nanoid12 } from "nanoid";
14166
15278
  init_questions();
14167
15279
  var tasks = new Hono5();
14168
15280
  var taskAbortControllers = /* @__PURE__ */ new Map();
15281
+ var taskMcpServerSchema = z20.object({
15282
+ name: z20.string().min(1).describe("Tool prefix + display name."),
15283
+ transport: z20.enum(["http", "sse", "stdio"]),
15284
+ url: z20.string().url().optional().describe("http/sse transports."),
15285
+ headers: z20.record(z20.string(), z20.string()).optional().describe("Auth / custom headers for http/sse."),
15286
+ command: z20.string().optional().describe("stdio transport."),
15287
+ args: z20.array(z20.string()).optional(),
15288
+ env: z20.record(z20.string(), z20.string()).optional().describe("Env vars for stdio child process.")
15289
+ }).refine(
15290
+ (s) => s.transport === "stdio" ? !!s.command : !!s.url,
15291
+ { message: 'http/sse require "url"; stdio requires "command".' }
15292
+ );
15293
+ var taskSkillSchema = z20.object({
15294
+ name: z20.string().min(1),
15295
+ description: z20.string().optional(),
15296
+ content: z20.string().min(1).describe("Full markdown body of the skill."),
15297
+ alwaysApply: z20.boolean().optional().describe("Inject into the system prompt up-front (vs load on demand)."),
15298
+ globs: z20.array(z20.string()).optional()
15299
+ });
14169
15300
  var createTaskSchema = z20.object({
14170
15301
  prompt: z20.string().min(1),
14171
15302
  outputSchema: z20.record(z20.string(), z20.unknown()),
@@ -14177,8 +15308,30 @@ var createTaskSchema = z20.object({
14177
15308
  parentTaskId: z20.string().optional(),
14178
15309
  /** When set, the spawning orchestrator's session id. Stamped on the
14179
15310
  * worker's config so terminal events can wake the orchestrator. */
14180
- orchestratorSessionId: z20.string().optional()
15311
+ orchestratorSessionId: z20.string().optional(),
15312
+ /** Task-scoped MCP servers — auto-connected for this task only. */
15313
+ mcpServers: z20.array(taskMcpServerSchema).optional(),
15314
+ /** Task-scoped skills — available to this task only. */
15315
+ skills: z20.array(taskSkillSchema).optional()
14181
15316
  });
15317
+ function redactMcpServers(servers2) {
15318
+ if (!servers2 || servers2.length === 0) return void 0;
15319
+ return servers2.map((s) => ({
15320
+ name: s.name,
15321
+ transport: s.transport,
15322
+ url: s.url,
15323
+ hasHeaders: !!(s.headers && Object.keys(s.headers).length > 0),
15324
+ command: s.command
15325
+ }));
15326
+ }
15327
+ function redactSkills(skills2) {
15328
+ if (!skills2 || skills2.length === 0) return void 0;
15329
+ return skills2.map((s) => ({
15330
+ name: s.name,
15331
+ description: s.description,
15332
+ alwaysApply: s.alwaysApply
15333
+ }));
15334
+ }
14182
15335
  tasks.post(
14183
15336
  "/",
14184
15337
  zValidator5("json", createTaskSchema),
@@ -14191,7 +15344,9 @@ tasks.post(
14191
15344
  webhookUrl: body.webhookUrl,
14192
15345
  maxIterations: body.maxIterations ?? 50,
14193
15346
  status: "running",
14194
- parentTaskId: body.parentTaskId
15347
+ parentTaskId: body.parentTaskId,
15348
+ mcpServers: redactMcpServers(body.mcpServers),
15349
+ skills: redactSkills(body.skills)
14195
15350
  };
14196
15351
  let agent;
14197
15352
  if (body.parentTaskId) {
@@ -14268,7 +15423,9 @@ tasks.post(
14268
15423
  prompt: body.prompt,
14269
15424
  taskConfig,
14270
15425
  abortSignal: abortController.signal,
14271
- writeSSE
15426
+ writeSSE,
15427
+ mcpServers: body.mcpServers,
15428
+ skills: body.skills
14272
15429
  });
14273
15430
  await writeSSE(JSON.stringify({ type: "finish" }));
14274
15431
  } catch (err) {
@@ -14362,6 +15519,8 @@ tasks.get("/:id", async (c) => {
14362
15519
  model: session.model,
14363
15520
  name: session.name,
14364
15521
  parentTaskId: task.parentTaskId,
15522
+ mcpServers: task.mcpServers,
15523
+ skills: task.skills,
14365
15524
  createdAt: session.createdAt.toISOString(),
14366
15525
  updatedAt: session.updatedAt.toISOString(),
14367
15526
  browserRecordings: browserRecordings.length > 0 ? browserRecordings : void 0
@@ -14484,6 +15643,204 @@ function verifySlackSignature(opts) {
14484
15643
  // src/server/routes/slack.ts
14485
15644
  init_client3();
14486
15645
  init_slack();
15646
+
15647
+ // src/integrations/slack/files.ts
15648
+ init_client3();
15649
+ var MAX_BYTES = 100 * 1024 * 1024;
15650
+ var INGEST_TIMEOUT_MS = 2500;
15651
+ function inferFileName(file) {
15652
+ return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
15653
+ }
15654
+ function inferContentType(file) {
15655
+ if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
15656
+ return "application/octet-stream";
15657
+ }
15658
+ function formatBytes(n) {
15659
+ if (!Number.isFinite(n) || n <= 0) return "?";
15660
+ if (n < 1024) return `${n} B`;
15661
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
15662
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
15663
+ }
15664
+ function withTimeout(p, ms, label) {
15665
+ return new Promise((resolve13, reject) => {
15666
+ const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
15667
+ p.then(
15668
+ (v) => {
15669
+ clearTimeout(t);
15670
+ resolve13(v);
15671
+ },
15672
+ (e) => {
15673
+ clearTimeout(t);
15674
+ reject(e);
15675
+ }
15676
+ );
15677
+ });
15678
+ }
15679
+ async function ingestOne(file, sessionId, botToken) {
15680
+ const fileName = inferFileName(file);
15681
+ const contentType = inferContentType(file);
15682
+ const declaredSize = typeof file.size === "number" ? file.size : 0;
15683
+ const base = {
15684
+ slackFileId: file.id,
15685
+ fileName,
15686
+ contentType,
15687
+ sizeBytes: declaredSize
15688
+ };
15689
+ const sourceUrl = file.url_private_download || file.url_private;
15690
+ if (!sourceUrl || typeof sourceUrl !== "string") {
15691
+ return { ...base, shortUrl: null, error: "no_source_url" };
15692
+ }
15693
+ if (declaredSize > MAX_BYTES) {
15694
+ return { ...base, shortUrl: null, error: "size_exceeded" };
15695
+ }
15696
+ let bytes;
15697
+ try {
15698
+ const res = await fetch(sourceUrl, {
15699
+ headers: { Authorization: `Bearer ${botToken}` }
15700
+ });
15701
+ if (!res.ok) {
15702
+ return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
15703
+ }
15704
+ const ab = await res.arrayBuffer();
15705
+ if (ab.byteLength > MAX_BYTES) {
15706
+ return { ...base, shortUrl: null, error: "size_exceeded" };
15707
+ }
15708
+ bytes = Buffer.from(ab);
15709
+ } catch (err) {
15710
+ return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
15711
+ }
15712
+ const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
15713
+ let upload;
15714
+ try {
15715
+ upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
15716
+ } catch (err) {
15717
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
15718
+ }
15719
+ try {
15720
+ const putRes = await fetch(upload.uploadUrl, {
15721
+ method: "PUT",
15722
+ headers: { "Content-Type": contentType },
15723
+ body: bytes
15724
+ });
15725
+ if (!putRes.ok) {
15726
+ return {
15727
+ ...base,
15728
+ sizeBytes: bytes.length,
15729
+ shortUrl: null,
15730
+ error: `gcs_put_${putRes.status}`
15731
+ };
15732
+ }
15733
+ } catch (err) {
15734
+ return {
15735
+ ...base,
15736
+ sizeBytes: bytes.length,
15737
+ shortUrl: null,
15738
+ error: `gcs_put_error:${err?.message || "unknown"}`
15739
+ };
15740
+ }
15741
+ try {
15742
+ await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
15743
+ } catch (err) {
15744
+ console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
15745
+ }
15746
+ const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
15747
+ // server somehow forgot to return it (older remote-server versions).
15748
+ inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
15749
+ return {
15750
+ ...base,
15751
+ sizeBytes: bytes.length,
15752
+ shortUrl
15753
+ };
15754
+ }
15755
+ function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
15756
+ try {
15757
+ const u = new URL(uploadUrl);
15758
+ if (u.hostname.endsWith(".googleapis.com")) return null;
15759
+ return `${u.origin}/f/${fileId}`;
15760
+ } catch {
15761
+ return null;
15762
+ }
15763
+ }
15764
+ async function ingestSlackFiles(files, sessionId, options = {}) {
15765
+ if (!Array.isArray(files) || files.length === 0) return [];
15766
+ const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
15767
+ if (!isRemoteConfigured2()) {
15768
+ console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
15769
+ return files.map((f) => ({
15770
+ slackFileId: f.id,
15771
+ fileName: inferFileName(f),
15772
+ contentType: inferContentType(f),
15773
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
15774
+ shortUrl: null,
15775
+ error: "storage_unconfigured"
15776
+ }));
15777
+ }
15778
+ const botToken = getSlackBotToken();
15779
+ if (!botToken) {
15780
+ console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
15781
+ return files.map((f) => ({
15782
+ slackFileId: f.id,
15783
+ fileName: inferFileName(f),
15784
+ contentType: inferContentType(f),
15785
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
15786
+ shortUrl: null,
15787
+ error: "no_bot_token"
15788
+ }));
15789
+ }
15790
+ const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
15791
+ const startedAt = Date.now();
15792
+ const pipeline = Promise.allSettled(
15793
+ files.map((f) => ingestOne(f, sessionId, botToken))
15794
+ );
15795
+ let settled;
15796
+ try {
15797
+ settled = await withTimeout(pipeline, timeoutMs, "ingest");
15798
+ } catch (err) {
15799
+ console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
15800
+ return files.map((f) => ({
15801
+ slackFileId: f.id,
15802
+ fileName: inferFileName(f),
15803
+ contentType: inferContentType(f),
15804
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
15805
+ shortUrl: null,
15806
+ error: "timeout"
15807
+ }));
15808
+ }
15809
+ const results = settled.map((s, i) => {
15810
+ if (s.status === "fulfilled") return s.value;
15811
+ const f = files[i];
15812
+ return {
15813
+ slackFileId: f.id,
15814
+ fileName: inferFileName(f),
15815
+ contentType: inferContentType(f),
15816
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
15817
+ shortUrl: null,
15818
+ error: `unexpected:${s.reason?.message || String(s.reason)}`
15819
+ };
15820
+ });
15821
+ const okCount = results.filter((r) => r.shortUrl).length;
15822
+ console.log(
15823
+ `[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
15824
+ );
15825
+ return results;
15826
+ }
15827
+ function formatFileBlock(files) {
15828
+ if (!files || files.length === 0) return "";
15829
+ const lines = ["[files]"];
15830
+ for (const f of files) {
15831
+ const sizeLabel = formatBytes(f.sizeBytes);
15832
+ if (f.shortUrl) {
15833
+ lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
15834
+ } else {
15835
+ lines.push(
15836
+ ` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
15837
+ );
15838
+ }
15839
+ }
15840
+ return lines.join("\n");
15841
+ }
15842
+
15843
+ // src/server/routes/slack.ts
14487
15844
  init_webhook_events();
14488
15845
  init_inbox();
14489
15846
  var recentlyHandled = /* @__PURE__ */ new Map();
@@ -14570,9 +15927,43 @@ slack.post("/events", async (c) => {
14570
15927
  inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
14571
15928
  }
14572
15929
  }
14573
- pushToInbox(orchestratorId, inbound);
15930
+ const slackFiles = Array.isArray(ev.files) ? ev.files : [];
14574
15931
  markHandled(ev.channel, ev.ts);
14575
- updateEvent(auditId, { status: "routed", sessionId: orchestratorId });
15932
+ if (ev.channel && ev.ts) {
15933
+ void addLoadingReaction(String(ev.channel), String(ev.ts));
15934
+ }
15935
+ let ingestedCount = 0;
15936
+ if (slackFiles.length > 0) {
15937
+ try {
15938
+ const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
15939
+ const block = formatFileBlock(ingested);
15940
+ if (block) inbound.content = `${inbound.content}
15941
+ ${block}`;
15942
+ ingestedCount = ingested.filter((f) => f.shortUrl).length;
15943
+ } catch (err) {
15944
+ console.warn("[slack-files] ingestion threw:", err?.message || err);
15945
+ inbound.content = `${inbound.content}
15946
+ [files] (ingestion failed: ${err?.message || "unknown"})`;
15947
+ }
15948
+ }
15949
+ pushToInbox(orchestratorId, inbound);
15950
+ updateEvent(auditId, {
15951
+ status: "routed",
15952
+ sessionId: orchestratorId,
15953
+ ...slackFiles.length > 0 ? {
15954
+ // Preserve the original meta (ts, thread_ts, team,
15955
+ // event_subtype) from recordEvent above — updateEvent does a
15956
+ // shallow merge, so we have to re-include them.
15957
+ meta: {
15958
+ ts: ev.ts,
15959
+ thread_ts: ev.thread_ts,
15960
+ team: ev.team,
15961
+ event_subtype: ev.subtype,
15962
+ fileCount: slackFiles.length,
15963
+ ingestedCount
15964
+ }
15965
+ } : {}
15966
+ });
14576
15967
  } else {
14577
15968
  updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
14578
15969
  }
@@ -14792,10 +16183,35 @@ integrations.get("/", async (c) => {
14792
16183
  cfAccess: {
14793
16184
  enabled: !!cfg?.auth?.cfAccess?.enabled,
14794
16185
  teamDomain: cfg?.auth?.cfAccess?.teamDomain || null,
16186
+ audTag: cfg?.auth?.cfAccess?.audTag || null,
14795
16187
  allowedEmails: cfg?.auth?.allowedEmails || []
14796
16188
  }
14797
16189
  });
14798
16190
  });
16191
+ var cfAccessSchema = z21.object({
16192
+ enabled: z21.boolean().optional(),
16193
+ teamDomain: z21.string().optional(),
16194
+ audTag: z21.string().optional(),
16195
+ // Email allowlist for the public (cloudflared) surface. Empty array = allow
16196
+ // any email that passes the Cloudflare Access policy (no extra filtering).
16197
+ allowedEmails: z21.array(z21.string().trim().toLowerCase()).optional()
16198
+ });
16199
+ integrations.post("/cf-access", zValidator6("json", cfAccessSchema), async (c) => {
16200
+ const body = c.req.valid("json");
16201
+ if (body.enabled) {
16202
+ const cfg = getConfig();
16203
+ const teamDomain = body.teamDomain ?? cfg?.auth?.cfAccess?.teamDomain;
16204
+ const audTag = body.audTag ?? cfg?.auth?.cfAccess?.audTag;
16205
+ if (!teamDomain || !audTag) {
16206
+ return c.json(
16207
+ { error: "teamDomain and audTag are required to enable Cloudflare Access" },
16208
+ 400
16209
+ );
16210
+ }
16211
+ }
16212
+ setCfAccessConfig(body);
16213
+ return c.json({ ok: true });
16214
+ });
14799
16215
  var slackConfigSchema = z21.object({
14800
16216
  botToken: z21.string().optional(),
14801
16217
  signingSecret: z21.string().optional(),
@@ -14978,8 +16394,8 @@ import { Hono as Hono9 } from "hono";
14978
16394
  import { zValidator as zValidator7 } from "@hono/zod-validator";
14979
16395
  import { z as z22 } from "zod";
14980
16396
  import { existsSync as existsSync21, statSync as statSync3 } from "fs";
14981
- import { readFile as readFile12, writeFile as writeFile6, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
14982
- import { resolve as resolve11, join as join16, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
16397
+ import { readFile as readFile12, writeFile as writeFile7, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
16398
+ import { resolve as resolve11, join as join17, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
14983
16399
  var skills = new Hono9();
14984
16400
  function encodeId(filePath) {
14985
16401
  return Buffer.from(filePath, "utf-8").toString("base64url");
@@ -15127,13 +16543,13 @@ skills.post(
15127
16543
  const safeName = basename6(fileName).replace(/[^A-Za-z0-9._-]/g, "-");
15128
16544
  const ext = extname9(safeName).toLowerCase();
15129
16545
  const finalName = ext === ".md" || ext === ".mdc" ? safeName : `${safeName}.md`;
15130
- const filePath = join16(targetDir, finalName);
16546
+ const filePath = join17(targetDir, finalName);
15131
16547
  if (existsSync21(filePath)) {
15132
16548
  return c.json({ error: `file already exists: ${finalName}` }, 409);
15133
16549
  }
15134
16550
  try {
15135
16551
  await mkdir5(targetDir, { recursive: true });
15136
- await writeFile6(filePath, content, "utf-8");
16552
+ await writeFile7(filePath, content, "utf-8");
15137
16553
  } catch (err) {
15138
16554
  return c.json({ error: err?.message || "write failed" }, 500);
15139
16555
  }
@@ -15149,7 +16565,7 @@ skills.put(
15149
16565
  if (filePath.includes("/skills/default")) {
15150
16566
  return c.json({ error: "built-in skills are read-only" }, 400);
15151
16567
  }
15152
- await writeFile6(filePath, c.req.valid("json").content, "utf-8");
16568
+ await writeFile7(filePath, c.req.valid("json").content, "utf-8");
15153
16569
  return c.json({ ok: true });
15154
16570
  }
15155
16571
  );
@@ -15193,6 +16609,14 @@ skills.delete("/directories", (c) => {
15193
16609
  init_config();
15194
16610
  import { createRemoteJWKSet, jwtVerify } from "jose";
15195
16611
  var EXEMPT_PATH_PREFIXES = ["/health", "/api/slack/events", "/api/inbox/", "/w/"];
16612
+ function isExemptPath(path) {
16613
+ return EXEMPT_PATH_PREFIXES.some((p) => {
16614
+ if (p.endsWith("/")) {
16615
+ return path === p.slice(0, -1) || path === p || path.startsWith(p);
16616
+ }
16617
+ return path === p || path.startsWith(p + "/") || path.startsWith(p + "?");
16618
+ });
16619
+ }
15196
16620
  var cachedJWKS = null;
15197
16621
  var cachedJWKSUrl = null;
15198
16622
  function getOrCreateJWKS(teamDomain) {
@@ -15222,12 +16646,13 @@ function cfAccessMiddleware() {
15222
16646
  return next();
15223
16647
  }
15224
16648
  const path = c.req.path;
15225
- if (EXEMPT_PATH_PREFIXES.some((p) => path === p || path.startsWith(p + "/") || path.startsWith(p + "?"))) {
16649
+ if (isExemptPath(path)) {
15226
16650
  return next();
15227
16651
  }
15228
16652
  const host = c.req.header("host");
15229
16653
  const remote = c.req.raw?.socket?.remoteAddress;
15230
- if (isLoopback(host, remote)) {
16654
+ const hasCfJwt = !!c.req.header("cf-access-jwt-assertion");
16655
+ if (!hasCfJwt && isLoopback(host, remote)) {
15231
16656
  return next();
15232
16657
  }
15233
16658
  const teamDomain = cfg.teamDomain;
@@ -15246,8 +16671,10 @@ function cfAccessMiddleware() {
15246
16671
  audience: aud
15247
16672
  });
15248
16673
  const email = String(payload.email || "").toLowerCase();
15249
- const allowed = (auth?.allowedEmails || []).map((e) => e.toLowerCase());
15250
- if (allowed.length > 0 && !allowed.includes(email)) {
16674
+ const emailDomain = email.split("@")[1] || "";
16675
+ const allowed = (auth?.allowedEmails || []).map((e) => e.toLowerCase().trim().replace(/^@/, "")).filter(Boolean);
16676
+ const isAllowed = allowed.length === 0 || allowed.some((entry2) => entry2.includes("@") ? entry2 === email : entry2 === emailDomain);
16677
+ if (!isAllowed) {
15251
16678
  console.warn(`[cf-access] rejected ${email}: not in allowlist`);
15252
16679
  return c.json({ error: "Email not allowed" }, 403);
15253
16680
  }
@@ -15349,13 +16776,13 @@ var DEFAULT_WEB_PORT = 6969;
15349
16776
  var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
15350
16777
  function getWebDirectory() {
15351
16778
  try {
15352
- const currentDir = dirname10(fileURLToPath4(import.meta.url));
16779
+ const currentDir = dirname11(fileURLToPath5(import.meta.url));
15353
16780
  const webDir = resolve12(currentDir, "..", "web");
15354
- if (existsSync22(webDir) && existsSync22(join17(webDir, "package.json"))) {
16781
+ if (existsSync22(webDir) && existsSync22(join19(webDir, "package.json"))) {
15355
16782
  return webDir;
15356
16783
  }
15357
16784
  const altWebDir = resolve12(currentDir, "..", "..", "web");
15358
- if (existsSync22(altWebDir) && existsSync22(join17(altWebDir, "package.json"))) {
16785
+ if (existsSync22(altWebDir) && existsSync22(join19(altWebDir, "package.json"))) {
15359
16786
  return altWebDir;
15360
16787
  }
15361
16788
  return null;
@@ -15413,20 +16840,20 @@ async function findWebPort(preferredPort) {
15413
16840
  return { port: preferredPort, alreadyRunning: false };
15414
16841
  }
15415
16842
  function hasProductionBuild(webDir) {
15416
- const buildIdPath = join17(webDir, ".next", "BUILD_ID");
16843
+ const buildIdPath = join19(webDir, ".next", "BUILD_ID");
15417
16844
  return existsSync22(buildIdPath);
15418
16845
  }
15419
16846
  function hasSourceFiles(webDir) {
15420
- const appDir = join17(webDir, "src", "app");
15421
- const pagesDir = join17(webDir, "src", "pages");
15422
- const rootAppDir = join17(webDir, "app");
15423
- const rootPagesDir = join17(webDir, "pages");
16847
+ const appDir = join19(webDir, "src", "app");
16848
+ const pagesDir = join19(webDir, "src", "pages");
16849
+ const rootAppDir = join19(webDir, "app");
16850
+ const rootPagesDir = join19(webDir, "pages");
15424
16851
  return existsSync22(appDir) || existsSync22(pagesDir) || existsSync22(rootAppDir) || existsSync22(rootPagesDir);
15425
16852
  }
15426
16853
  function getStandaloneServerPath(webDir) {
15427
16854
  const possiblePaths2 = [
15428
- join17(webDir, ".next", "standalone", "server.js"),
15429
- join17(webDir, ".next", "standalone", "web", "server.js")
16855
+ join19(webDir, ".next", "standalone", "server.js"),
16856
+ join19(webDir, ".next", "standalone", "web", "server.js")
15430
16857
  ];
15431
16858
  for (const serverPath of possiblePaths2) {
15432
16859
  if (existsSync22(serverPath)) {
@@ -15437,7 +16864,7 @@ function getStandaloneServerPath(webDir) {
15437
16864
  }
15438
16865
  function runCommand(command, args, cwd, env) {
15439
16866
  return new Promise((resolve13) => {
15440
- const child = spawn2(command, args, {
16867
+ const child = spawn3(command, args, {
15441
16868
  cwd,
15442
16869
  stdio: ["ignore", "pipe", "pipe"],
15443
16870
  env,
@@ -15469,15 +16896,15 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15469
16896
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
15470
16897
  return { process: null, port: actualPort };
15471
16898
  }
15472
- const usePnpm = existsSync22(join17(webDir, "pnpm-lock.yaml"));
15473
- const useNpm = !usePnpm && existsSync22(join17(webDir, "package-lock.json"));
16899
+ const usePnpm = existsSync22(join19(webDir, "pnpm-lock.yaml"));
16900
+ const useNpm = !usePnpm && existsSync22(join19(webDir, "package-lock.json"));
15474
16901
  const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
15475
16902
  const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv } = process.env;
15476
16903
  const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
15477
- const runtimeConfig = { apiBaseUrl: apiUrl };
15478
- const runtimeConfigPath = join17(webDir, "runtime-config.json");
16904
+ const runtimeConfig = { apiBaseUrl: apiUrl, localApiBaseUrl: `http://127.0.0.1:${apiPort}` };
16905
+ const runtimeConfigPath = join19(webDir, "runtime-config.json");
15479
16906
  try {
15480
- writeFileSync7(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
16907
+ writeFileSync8(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
15481
16908
  if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
15482
16909
  } catch (err) {
15483
16910
  if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
@@ -15497,7 +16924,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15497
16924
  if (standaloneServerPath) {
15498
16925
  command = "node";
15499
16926
  args = ["server.js"];
15500
- cwd = dirname10(standaloneServerPath);
16927
+ cwd = dirname11(standaloneServerPath);
15501
16928
  webEnv.PORT = String(actualPort);
15502
16929
  webEnv.HOSTNAME = "0.0.0.0";
15503
16930
  if (!quiet) console.log(" \u{1F4E6} Starting Web UI from standalone build...");
@@ -15527,7 +16954,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15527
16954
  }
15528
16955
  return { process: null, port: actualPort };
15529
16956
  }
15530
- const child = spawn2(command, args, {
16957
+ const child = spawn3(command, args, {
15531
16958
  cwd,
15532
16959
  stdio: ["ignore", "pipe", "pipe"],
15533
16960
  env: webEnv,
@@ -15535,12 +16962,12 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15535
16962
  shell: true
15536
16963
  });
15537
16964
  const startupTimeout = 3e4;
15538
- let started = false;
16965
+ let started2 = false;
15539
16966
  let exited = false;
15540
16967
  let exitCode = null;
15541
16968
  const startedPromise = new Promise((resolve13) => {
15542
16969
  const timeout = setTimeout(() => {
15543
- if (!started && !exited) {
16970
+ if (!started2 && !exited) {
15544
16971
  resolve13(false);
15545
16972
  }
15546
16973
  }, startupTimeout);
@@ -15552,8 +16979,8 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15552
16979
  console.log(` Web UI: ${line}`);
15553
16980
  }
15554
16981
  }
15555
- if (!started && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
15556
- started = true;
16982
+ if (!started2 && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
16983
+ started2 = true;
15557
16984
  clearTimeout(timeout);
15558
16985
  resolve13(true);
15559
16986
  }
@@ -15572,7 +16999,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15572
16999
  child.on("exit", (code) => {
15573
17000
  exited = true;
15574
17001
  exitCode = code;
15575
- if (!started) {
17002
+ if (!started2) {
15576
17003
  clearTimeout(timeout);
15577
17004
  resolve13(false);
15578
17005
  }
@@ -15692,7 +17119,7 @@ async function startServer(options = {}) {
15692
17119
  config.resolvedWorkingDirectory = options.workingDirectory;
15693
17120
  }
15694
17121
  if (!existsSync22(config.resolvedWorkingDirectory)) {
15695
- mkdirSync10(config.resolvedWorkingDirectory, { recursive: true });
17122
+ mkdirSync11(config.resolvedWorkingDirectory, { recursive: true });
15696
17123
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
15697
17124
  }
15698
17125
  if (!config.resolvedRemoteServer.url) {
@@ -15723,9 +17150,17 @@ async function startServer(options = {}) {
15723
17150
  try {
15724
17151
  const { startOrchestratorDaemon: startOrchestratorDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
15725
17152
  startOrchestratorDaemon2();
17153
+ const { startReconciler: startReconciler2 } = await Promise.resolve().then(() => (init_inbox_acks(), inbox_acks_exports));
17154
+ startReconciler2();
15726
17155
  } catch (err) {
15727
17156
  if (!options.quiet) console.warn(`[daemon] start skipped: ${err.message}`);
15728
17157
  }
17158
+ try {
17159
+ const { startSelfUpdater: startSelfUpdater2 } = await Promise.resolve().then(() => (init_self_update(), self_update_exports));
17160
+ startSelfUpdater2();
17161
+ } catch (err) {
17162
+ if (!options.quiet) console.warn(`[self-update] start skipped: ${err.message}`);
17163
+ }
15729
17164
  try {
15730
17165
  const { startScheduler: startScheduler2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
15731
17166
  startScheduler2({ quiet: options.quiet });