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
@@ -818,8 +818,8 @@ var init_types = __esm({
818
818
  authKey: z.string().optional()
819
819
  }).optional();
820
820
  SparkcoderConfigSchema = z.object({
821
- // Default model to use (Vercel AI Gateway format)
822
- defaultModel: z.string().default("anthropic/claude-opus-4.7"),
821
+ // Default model to use (LiteLLM model id)
822
+ defaultModel: z.string().default("gpt-5.5"),
823
823
  // Working directory for file operations
824
824
  workingDirectory: z.string().optional(),
825
825
  // Tool approval settings
@@ -858,6 +858,14 @@ var init_types = __esm({
858
858
  webhooks: z.object({
859
859
  token: z.string().optional()
860
860
  }).optional(),
861
+ // Self-update: when running as the managed service, periodically check
862
+ // npm for a newer published version and, if found, re-run the hosted
863
+ // installer (full upgrade + restart). Disabled automatically when not
864
+ // running from a global install (e.g. dev/source checkouts).
865
+ autoUpdate: z.object({
866
+ enabled: z.boolean().optional().default(true),
867
+ intervalHours: z.number().positive().optional().default(6)
868
+ }).optional().default({}),
861
869
  // Database path (used for local SQLite - ignored if remoteServer is configured)
862
870
  databasePath: z.string().optional().default("./sparkecoder.db"),
863
871
  // Remote server configuration (for centralized storage)
@@ -965,6 +973,7 @@ __export(config_exports, {
965
973
  requiresApproval: () => requiresApproval,
966
974
  saveAuthKey: () => saveAuthKey,
967
975
  setApiKey: () => setApiKey,
976
+ setCfAccessConfig: () => setCfAccessConfig,
968
977
  setMcpServers: () => setMcpServers,
969
978
  setPublicUrl: () => setPublicUrl,
970
979
  setSkillsAdditionalDirectories: () => setSkillsAdditionalDirectories,
@@ -1153,12 +1162,12 @@ function loadConfig(configPath, workingDirectory) {
1153
1162
  ]
1154
1163
  };
1155
1164
  const DEFAULT_REMOTE_URL = "https://agent-remote-server.sparkecode.com";
1156
- const remoteUrl = process.env.SPARKECODER_REMOTE_URL || config.remoteServer?.url || DEFAULT_REMOTE_URL;
1165
+ const remoteUrl2 = process.env.SPARKECODER_REMOTE_URL || config.remoteServer?.url || DEFAULT_REMOTE_URL;
1157
1166
  const remoteAuthKey = process.env.SPARKECODER_AUTH_KEY || config.remoteServer?.authKey || loadStoredAuthKey();
1158
1167
  const resolvedRemoteServer = {
1159
- url: remoteUrl,
1168
+ url: remoteUrl2,
1160
1169
  authKey: remoteAuthKey,
1161
- isConfigured: !!remoteUrl && !!remoteAuthKey
1170
+ isConfigured: !!remoteUrl2 && !!remoteAuthKey
1162
1171
  };
1163
1172
  const resolved = {
1164
1173
  ...config,
@@ -1314,6 +1323,40 @@ function setPublicUrl(publicUrl) {
1314
1323
  console.warn("[config] failed to persist publicUrl:", err?.message || err);
1315
1324
  }
1316
1325
  }
1326
+ function setCfAccessConfig(input) {
1327
+ const applyToAuth = (auth) => {
1328
+ const curAuth = auth || {};
1329
+ const curCf = curAuth.cfAccess || {};
1330
+ const nextCf = { ...curCf };
1331
+ if (input.enabled !== void 0) nextCf.enabled = input.enabled;
1332
+ if (input.teamDomain !== void 0) nextCf.teamDomain = input.teamDomain;
1333
+ if (input.audTag !== void 0) nextCf.audTag = input.audTag;
1334
+ const nextAuth = { ...curAuth, cfAccess: nextCf };
1335
+ if (input.allowedEmails !== void 0) nextAuth.allowedEmails = input.allowedEmails;
1336
+ return nextAuth;
1337
+ };
1338
+ if (cachedConfig) {
1339
+ cachedConfig.auth = applyToAuth(cachedConfig.auth);
1340
+ }
1341
+ try {
1342
+ const cwdPath = resolve(process.cwd(), "sparkecoder.config.json");
1343
+ const target = existsSync(cwdPath) ? cwdPath : join(ensureAppDataDirectory(), "sparkecoder.config.json");
1344
+ let raw = {};
1345
+ if (existsSync(target)) {
1346
+ try {
1347
+ raw = JSON.parse(readFileSync(target, "utf-8"));
1348
+ } catch {
1349
+ raw = {};
1350
+ }
1351
+ } else {
1352
+ raw = createDefaultConfig();
1353
+ }
1354
+ raw.auth = applyToAuth(raw.auth);
1355
+ writeFileSync(target, JSON.stringify(raw, null, 2));
1356
+ } catch (err) {
1357
+ console.warn("[config] failed to persist cf-access config:", err?.message || err);
1358
+ }
1359
+ }
1317
1360
  function clearSlackConfig() {
1318
1361
  if (cachedConfig) cachedConfig.slack = {};
1319
1362
  try {
@@ -1355,7 +1398,7 @@ function autoApproveAllTools(sessionConfig) {
1355
1398
  }
1356
1399
  function createDefaultConfig() {
1357
1400
  return {
1358
- defaultModel: "anthropic/claude-opus-4.7",
1401
+ defaultModel: "gpt-5.5",
1359
1402
  // workingDirectory is intentionally not set - defaults to where CLI is run
1360
1403
  toolApprovals: {
1361
1404
  bash: true,
@@ -1377,6 +1420,10 @@ function createDefaultConfig() {
1377
1420
  port: 3141,
1378
1421
  host: "0.0.0.0"
1379
1422
  },
1423
+ autoUpdate: {
1424
+ enabled: true,
1425
+ intervalHours: 6
1426
+ },
1380
1427
  databasePath: "./sparkecoder.db"
1381
1428
  };
1382
1429
  }
@@ -1603,6 +1650,7 @@ var init_config = __esm({
1603
1650
  openai: "OPENAI_API_KEY",
1604
1651
  google: "GOOGLE_GENERATIVE_AI_API_KEY",
1605
1652
  xai: "XAI_API_KEY",
1653
+ litellm: "LITELLM_API_KEY",
1606
1654
  "ai-gateway": "AI_GATEWAY_API_KEY"
1607
1655
  };
1608
1656
  SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
@@ -4845,11 +4893,11 @@ async function getRepoNamespace(workingDirectory, configuredNamespace) {
4845
4893
  if (configuredNamespace) {
4846
4894
  return configuredNamespace;
4847
4895
  }
4848
- const remoteUrl = getGitRemoteUrl(workingDirectory);
4849
- if (!remoteUrl) {
4896
+ const remoteUrl2 = getGitRemoteUrl(workingDirectory);
4897
+ if (!remoteUrl2) {
4850
4898
  return null;
4851
4899
  }
4852
- const parsed = parseGitRemoteUrl(remoteUrl);
4900
+ const parsed = parseGitRemoteUrl(remoteUrl2);
4853
4901
  if (!parsed) {
4854
4902
  return null;
4855
4903
  }
@@ -6546,7 +6594,8 @@ async function buildSystemPrompt(options) {
6546
6594
  sessionId,
6547
6595
  discoveredSkills,
6548
6596
  activeFiles = [],
6549
- customInstructions
6597
+ customInstructions,
6598
+ taskScopedSkills
6550
6599
  } = options;
6551
6600
  let alwaysLoadedContent = "";
6552
6601
  let globMatchedContent = "";
@@ -6567,6 +6616,22 @@ async function buildSystemPrompt(options) {
6567
6616
  const skills2 = await loadAllSkills2(skillsDirectories);
6568
6617
  onDemandSkillsContext = formatSkillsForContext(skills2);
6569
6618
  }
6619
+ let taskScopedSkillsBlock = "";
6620
+ if (taskScopedSkills && (taskScopedSkills.always.length > 0 || taskScopedSkills.onDemand.length > 0)) {
6621
+ const parts = ["<task_provided_skills>"];
6622
+ parts.push("These skills were supplied with this task and are available for this run only.");
6623
+ if (taskScopedSkills.always.length > 0) {
6624
+ parts.push(formatAlwaysLoadedSkills(taskScopedSkills.always));
6625
+ }
6626
+ if (taskScopedSkills.onDemand.length > 0) {
6627
+ parts.push("Load any of these on demand with the load_skill tool:");
6628
+ for (const s of taskScopedSkills.onDemand) {
6629
+ parts.push(`- ${s.name}: ${s.description}`);
6630
+ }
6631
+ }
6632
+ parts.push("</task_provided_skills>");
6633
+ taskScopedSkillsBlock = parts.join("\n");
6634
+ }
6570
6635
  const todos = await todoQueries.getBySession(sessionId);
6571
6636
  const todosContext = formatTodosForContext(todos);
6572
6637
  const plans = await readSessionPlans(workingDirectory, sessionId);
@@ -6859,6 +6924,8 @@ ${globMatchedContent}
6859
6924
  ${onDemandSkillsContext}
6860
6925
  </on_demand_skills>
6861
6926
 
6927
+ ${taskScopedSkillsBlock}
6928
+
6862
6929
  <current_task_list>
6863
6930
  ${todosContext}
6864
6931
  </current_task_list>
@@ -7480,6 +7547,111 @@ var init_sanitize_messages = __esm({
7480
7547
  }
7481
7548
  });
7482
7549
 
7550
+ // src/utils/cap-image-count.ts
7551
+ function isImagePart(part) {
7552
+ if (!part || typeof part !== "object") return false;
7553
+ const t = part.type;
7554
+ if (t === "image") return true;
7555
+ if (t === "image-data") return true;
7556
+ if (t === "media") {
7557
+ const data = part.data;
7558
+ const mt = part.mediaType;
7559
+ if (typeof data === "string" && typeof mt === "string" && mt.startsWith("image/")) {
7560
+ return true;
7561
+ }
7562
+ }
7563
+ return false;
7564
+ }
7565
+ function makePlaceholder() {
7566
+ return { type: "text", text: IMAGE_TRUNCATED_PLACEHOLDER };
7567
+ }
7568
+ function countImages(messages) {
7569
+ let n = 0;
7570
+ for (const msg of messages) {
7571
+ if (!Array.isArray(msg.content)) continue;
7572
+ for (const part of msg.content) {
7573
+ if (isImagePart(part)) {
7574
+ n++;
7575
+ continue;
7576
+ }
7577
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
7578
+ for (const sub of part.output.value) {
7579
+ if (isImagePart(sub)) n++;
7580
+ }
7581
+ }
7582
+ }
7583
+ }
7584
+ return n;
7585
+ }
7586
+ function capImageCount(messages, max = MAX_IMAGES_IN_CONTEXT) {
7587
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
7588
+ if (max < 0) throw new Error("capImageCount: max must be >= 0");
7589
+ const total = countImages(messages);
7590
+ if (total <= max) return messages;
7591
+ let toDrop = total - max;
7592
+ let mutated = false;
7593
+ const out = messages.slice();
7594
+ for (let i = 0; i < out.length && toDrop > 0; i++) {
7595
+ const msg = out[i];
7596
+ if (!Array.isArray(msg.content)) continue;
7597
+ let contentCloned = false;
7598
+ const ensureContentCloned = () => {
7599
+ if (contentCloned) return;
7600
+ out[i] = { ...msg, content: [...msg.content] };
7601
+ contentCloned = true;
7602
+ };
7603
+ const content = () => out[i].content;
7604
+ for (let j = 0; j < content().length && toDrop > 0; j++) {
7605
+ const part = content()[j];
7606
+ if (isImagePart(part)) {
7607
+ ensureContentCloned();
7608
+ out[i].content[j] = makePlaceholder();
7609
+ toDrop--;
7610
+ mutated = true;
7611
+ continue;
7612
+ }
7613
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
7614
+ const innerImages = [];
7615
+ const innerValue = part.output.value;
7616
+ for (let k = 0; k < innerValue.length; k++) {
7617
+ if (isImagePart(innerValue[k])) innerImages.push(k);
7618
+ }
7619
+ if (innerImages.length === 0) continue;
7620
+ const dropHere = Math.min(innerImages.length, toDrop);
7621
+ ensureContentCloned();
7622
+ const newOutputValue = [...innerValue];
7623
+ for (let d = 0; d < dropHere; d++) {
7624
+ newOutputValue[innerImages[d]] = makePlaceholder();
7625
+ }
7626
+ const newPart = {
7627
+ ...part,
7628
+ output: {
7629
+ ...part.output,
7630
+ value: newOutputValue
7631
+ }
7632
+ };
7633
+ out[i].content[j] = newPart;
7634
+ toDrop -= dropHere;
7635
+ mutated = true;
7636
+ }
7637
+ }
7638
+ }
7639
+ if (mutated) {
7640
+ console.warn(
7641
+ `[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.`
7642
+ );
7643
+ }
7644
+ return mutated ? out : messages;
7645
+ }
7646
+ var MAX_IMAGES_IN_CONTEXT, IMAGE_TRUNCATED_PLACEHOLDER;
7647
+ var init_cap_image_count = __esm({
7648
+ "src/utils/cap-image-count.ts"() {
7649
+ "use strict";
7650
+ MAX_IMAGES_IN_CONTEXT = 11;
7651
+ IMAGE_TRUNCATED_PLACEHOLDER = "[image truncated due to length of conversation]";
7652
+ }
7653
+ });
7654
+
7483
7655
  // src/agent/model-limits.ts
7484
7656
  function getModelLimits(modelId) {
7485
7657
  const normalized = modelId.trim().toLowerCase();
@@ -7495,18 +7667,9 @@ var init_model_limits = __esm({
7495
7667
  "src/agent/model-limits.ts"() {
7496
7668
  "use strict";
7497
7669
  MODEL_LIMITS = {
7498
- "anthropic/claude-opus-4.7": { contextWindow: 2e5, rollingTarget: 15e4 },
7499
- "anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
7500
- "anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
7501
- "anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
7502
- "anthropic/claude-3-haiku": { contextWindow: 2e5, rollingTarget: 15e4 },
7503
- "google/gemini-3-flash-preview": { contextWindow: 1e6, rollingTarget: 15e4 },
7504
- "google/gemini-2.5-pro": { contextWindow: 1e6, rollingTarget: 15e4 },
7505
- "google/gemini-2.5-flash": { contextWindow: 1e6, rollingTarget: 15e4 },
7506
- "openai/gpt-4o": { contextWindow: 128e3, rollingTarget: 78e3 },
7507
- "openai/gpt-4.1": { contextWindow: 1e6, rollingTarget: 15e4 },
7508
- "openai/o3": { contextWindow: 2e5, rollingTarget: 15e4 },
7509
- "xai/grok-3": { contextWindow: 131072, rollingTarget: 8e4 }
7670
+ "claude-opus-4-8": { contextWindow: 2e5, rollingTarget: 15e4 },
7671
+ "gpt-5.5": { contextWindow: 35e4, rollingTarget: 15e4 },
7672
+ "claude-fable-5": { contextWindow: 2e5, rollingTarget: 15e4 }
7510
7673
  };
7511
7674
  DEFAULT_LIMITS = { contextWindow: 2e5, rollingTarget: 15e4 };
7512
7675
  PREFIX_DEFAULTS = {
@@ -7580,6 +7743,32 @@ var init_conversation_archive = __esm({
7580
7743
 
7581
7744
  // src/agent/context.ts
7582
7745
  import { generateText as generateText2 } from "ai";
7746
+ function stripBinaryContentForSummary(value) {
7747
+ if (Array.isArray(value)) return value.map(stripBinaryContentForSummary);
7748
+ if (!value || typeof value !== "object") return value;
7749
+ const record = value;
7750
+ const type = record.type;
7751
+ if ((type === "image-data" || type === "file-data" || type === "media") && typeof record.data === "string") {
7752
+ const mediaType = typeof record.mediaType === "string" ? record.mediaType : "unknown media type";
7753
+ const filename = typeof record.filename === "string" ? ` ${record.filename}` : "";
7754
+ return {
7755
+ ...record,
7756
+ data: `[${type}${filename}; ${mediaType}; ${record.data.length} base64 chars omitted for summary]`
7757
+ };
7758
+ }
7759
+ if (type === "image" && typeof record.image === "string") {
7760
+ const filename = typeof record.filename === "string" ? ` ${record.filename}` : "";
7761
+ return {
7762
+ ...record,
7763
+ image: `[image${filename}; ${record.image.length} base64 chars omitted for summary]`
7764
+ };
7765
+ }
7766
+ const out = {};
7767
+ for (const [key2, nested] of Object.entries(record)) {
7768
+ out[key2] = stripBinaryContentForSummary(nested);
7769
+ }
7770
+ return out;
7771
+ }
7583
7772
  function stripOrphanedToolResults(msg, removedIds) {
7584
7773
  if (!Array.isArray(msg.content)) return msg;
7585
7774
  const parts = msg.content.filter((part) => {
@@ -7740,6 +7929,7 @@ var init_context = __esm({
7740
7929
  init_tokens();
7741
7930
  init_prompts();
7742
7931
  init_sanitize_messages();
7932
+ init_cap_image_count();
7743
7933
  init_model_limits();
7744
7934
  TOOL_OUTPUT_TRIM_CHARS = 400;
7745
7935
  COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
@@ -7789,6 +7979,7 @@ ${summaryContent}`
7789
7979
  messages = repairToolPairing(messages);
7790
7980
  messages = ensureToolResultsFollowCalls(messages);
7791
7981
  messages = ensureEndsWithUserOrTool(messages);
7982
+ messages = capImageCount(messages);
7792
7983
  return messages;
7793
7984
  }
7794
7985
  // ---------------------------------------------------------------------------
@@ -7906,7 +8097,7 @@ ${summaryContent}`
7906
8097
  }
7907
8098
  async summarizeChunk(chunk) {
7908
8099
  const historyText = chunk.map((msg) => {
7909
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
8100
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(stripBinaryContentForSummary(msg.content));
7910
8101
  return `[${msg.role}]: ${content}`;
7911
8102
  }).join("\n\n");
7912
8103
  try {
@@ -8154,6 +8345,127 @@ var init_persistence = __esm({
8154
8345
  });
8155
8346
 
8156
8347
  // src/integrations/slack/client.ts
8348
+ var client_exports = {};
8349
+ __export(client_exports, {
8350
+ LOADING_REACTION: () => LOADING_REACTION,
8351
+ RESULT_REACTIONS: () => RESULT_REACTIONS,
8352
+ addLoadingReaction: () => addLoadingReaction,
8353
+ addResultReaction: () => addResultReaction,
8354
+ botParticipatedInThread: () => botParticipatedInThread,
8355
+ ensureSlackSelfIdentity: () => ensureSlackSelfIdentity,
8356
+ getCachedSlackSelfIdentity: () => getCachedSlackSelfIdentity,
8357
+ getDefaultOrchestratorName: () => getDefaultOrchestratorName,
8358
+ getSlackAdapter: () => getSlackAdapter,
8359
+ getSlackAllowlistPolicy: () => getSlackAllowlistPolicy,
8360
+ getSlackBotToken: () => getSlackBotToken,
8361
+ getSlackDeniedReplyPolicy: () => getSlackDeniedReplyPolicy,
8362
+ getSlackSigningSecret: () => getSlackSigningSecret,
8363
+ isSlackConfigured: () => isSlackConfigured,
8364
+ normalizeSlackMentions: () => normalizeSlackMentions,
8365
+ noteBotPostedInThread: () => noteBotPostedInThread,
8366
+ postThreadMessage: () => postThreadMessage,
8367
+ removeLoadingReaction: () => removeLoadingReaction,
8368
+ resolveSlackUserInfo: () => resolveSlackUserInfo,
8369
+ resolveSlackUserName: () => resolveSlackUserName
8370
+ });
8371
+ function slackBackoffMs(attempt) {
8372
+ const expo = SLACK_BACKOFF_BASE_MS * 2 ** attempt;
8373
+ const jitter = Math.floor(Math.random() * SLACK_BACKOFF_BASE_MS);
8374
+ return Math.min(expo + jitter, SLACK_BACKOFF_CAP_MS);
8375
+ }
8376
+ async function slackFetchWithRetry(url, init, attempts = SLACK_FETCH_ATTEMPTS) {
8377
+ let lastErr;
8378
+ for (let i = 0; i < attempts; i++) {
8379
+ const isLast = i === attempts - 1;
8380
+ try {
8381
+ const res = await fetch(url, init);
8382
+ if ((res.status === 429 || res.status >= 500) && !isLast) {
8383
+ const ra = Number(res.headers.get("retry-after"));
8384
+ const waitMs = Number.isFinite(ra) && ra > 0 ? Math.min(ra * 1e3, SLACK_BACKOFF_CAP_MS) : slackBackoffMs(i);
8385
+ await new Promise((r) => setTimeout(r, waitMs));
8386
+ continue;
8387
+ }
8388
+ return res;
8389
+ } catch (err) {
8390
+ lastErr = err;
8391
+ if (isLast) throw err;
8392
+ await new Promise((r) => setTimeout(r, slackBackoffMs(i)));
8393
+ }
8394
+ }
8395
+ throw lastErr ?? new Error("slack fetch failed");
8396
+ }
8397
+ function reactionKey(channel, ts) {
8398
+ return `${channel}\u241F${ts}`;
8399
+ }
8400
+ async function addLoadingReaction(channel, timestamp) {
8401
+ const adapter = getSlackAdapter();
8402
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
8403
+ const key2 = reactionKey(channel, timestamp);
8404
+ const inFlight = (async () => {
8405
+ try {
8406
+ const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
8407
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
8408
+ console.warn(`[slack] addReaction ${LOADING_REACTION} failed on ${channel}/${timestamp}: ${res.error}`);
8409
+ }
8410
+ return res;
8411
+ } catch (err) {
8412
+ console.warn(`[slack] addReaction threw on ${channel}/${timestamp}:`, err?.message || err);
8413
+ return { ok: false, error: err?.message || "unknown" };
8414
+ }
8415
+ })();
8416
+ pendingAdds.set(key2, inFlight);
8417
+ void inFlight.finally(() => {
8418
+ if (pendingAdds.get(key2) === inFlight) pendingAdds.delete(key2);
8419
+ });
8420
+ return inFlight;
8421
+ }
8422
+ async function removeLoadingReaction(channel, timestamp) {
8423
+ const adapter = getSlackAdapter();
8424
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
8425
+ const pending = pendingAdds.get(reactionKey(channel, timestamp));
8426
+ if (pending) {
8427
+ try {
8428
+ await pending;
8429
+ } catch {
8430
+ }
8431
+ }
8432
+ try {
8433
+ const res = await adapter.removeReaction({ channel, timestamp, name: LOADING_REACTION });
8434
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
8435
+ console.warn(`[slack] removeReaction ${LOADING_REACTION} failed on ${channel}/${timestamp}: ${res.error}`);
8436
+ }
8437
+ return res;
8438
+ } catch (err) {
8439
+ console.warn(`[slack] removeReaction threw on ${channel}/${timestamp}:`, err?.message || err);
8440
+ return { ok: false, error: err?.message || "unknown" };
8441
+ }
8442
+ }
8443
+ async function addResultReaction(channel, timestamp, state2) {
8444
+ const name = RESULT_REACTIONS[state2];
8445
+ if (!name) return { ok: false, error: `no_reaction_for_state:${state2}` };
8446
+ const adapter = getSlackAdapter();
8447
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
8448
+ try {
8449
+ const res = await adapter.addReaction({ channel, timestamp, name });
8450
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
8451
+ console.warn(`[slack] addReaction ${name} failed on ${channel}/${timestamp}: ${res.error}`);
8452
+ }
8453
+ return res;
8454
+ } catch (err) {
8455
+ console.warn(`[slack] addResultReaction threw on ${channel}/${timestamp}:`, err?.message || err);
8456
+ return { ok: false, error: err?.message || "unknown" };
8457
+ }
8458
+ }
8459
+ async function postThreadMessage(channel, threadTs, text) {
8460
+ const adapter = getSlackAdapter();
8461
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
8462
+ try {
8463
+ return await adapter.postMessage({ channel, text, threadTs });
8464
+ } catch (err) {
8465
+ console.warn(`[slack] postThreadMessage threw on ${channel}/${threadTs}:`, err?.message || err);
8466
+ return { ok: false, error: err?.message || "unknown" };
8467
+ }
8468
+ }
8157
8469
  function readSlackConfig() {
8158
8470
  try {
8159
8471
  const cfg = getConfig();
@@ -8171,9 +8483,25 @@ function readSlackConfig() {
8171
8483
  function getSlackAdapter() {
8172
8484
  const cfg = readSlackConfig();
8173
8485
  if (!cfg) return void 0;
8486
+ const slackForm = async (endpoint, params) => {
8487
+ const body = new URLSearchParams(params).toString();
8488
+ const res = await fetch(`https://slack.com/api/${endpoint}`, {
8489
+ method: "POST",
8490
+ headers: {
8491
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
8492
+ Authorization: `Bearer ${cfg.botToken}`
8493
+ },
8494
+ body
8495
+ });
8496
+ const data = await res.json().catch(() => ({}));
8497
+ if (!res.ok || data?.ok === false) {
8498
+ return { ok: false, error: data?.error || `HTTP ${res.status}` };
8499
+ }
8500
+ return { ok: true };
8501
+ };
8174
8502
  return {
8175
8503
  async postMessage({ channel, text, threadTs }) {
8176
- const res = await fetch("https://slack.com/api/chat.postMessage", {
8504
+ const res = await slackFetchWithRetry("https://slack.com/api/chat.postMessage", {
8177
8505
  method: "POST",
8178
8506
  headers: {
8179
8507
  "Content-Type": "application/json; charset=utf-8",
@@ -8186,6 +8514,12 @@ function getSlackAdapter() {
8186
8514
  return { ok: false, error: data?.error || `HTTP ${res.status}` };
8187
8515
  }
8188
8516
  return { ok: true, ts: data?.ts };
8517
+ },
8518
+ addReaction({ channel, timestamp, name }) {
8519
+ return slackForm("reactions.add", { channel, timestamp, name });
8520
+ },
8521
+ removeReaction({ channel, timestamp, name }) {
8522
+ return slackForm("reactions.remove", { channel, timestamp, name });
8189
8523
  }
8190
8524
  };
8191
8525
  }
@@ -8386,12 +8720,31 @@ function getSlackDeniedReplyPolicy() {
8386
8720
  return { enabled: true, template: DEFAULT_DENIED_TEMPLATE };
8387
8721
  }
8388
8722
  }
8389
- var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS, threadOwnedInflight, DEFAULT_DENIED_TEMPLATE;
8723
+ 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;
8390
8724
  var init_client3 = __esm({
8391
8725
  "src/integrations/slack/client.ts"() {
8392
8726
  "use strict";
8393
8727
  init_config();
8394
8728
  init_persistence();
8729
+ LOADING_REACTION = "hourglass_flowing_sand";
8730
+ RESULT_REACTIONS = {
8731
+ responded: "white_check_mark",
8732
+ skipped: "zzz",
8733
+ handed_off: "eyes",
8734
+ failed: "warning"
8735
+ };
8736
+ SLACK_FETCH_ATTEMPTS = 3;
8737
+ SLACK_BACKOFF_BASE_MS = 400;
8738
+ SLACK_BACKOFF_CAP_MS = 3e4;
8739
+ REACTION_SOFT_ERRORS = /* @__PURE__ */ new Set([
8740
+ "already_reacted",
8741
+ // add: someone (or we) already added this emoji
8742
+ "no_reaction",
8743
+ // remove: the emoji isn't on the message
8744
+ "message_not_found"
8745
+ // remove/add: original message deleted
8746
+ ]);
8747
+ pendingAdds = /* @__PURE__ */ new Map();
8395
8748
  cachedSelf = null;
8396
8749
  selfInflight = null;
8397
8750
  USER_TTL_MS = 60 * 60 * 1e3;
@@ -8404,83 +8757,541 @@ var init_client3 = __esm({
8404
8757
  }
8405
8758
  });
8406
8759
 
8407
- // src/integrations/channels/slack.ts
8408
- function threadKey(channel, threadTs) {
8409
- return `${channel}\u241F${threadTs}`;
8410
- }
8411
- function markThreadOwned(channel, threadTs) {
8412
- ownedThreads.add(threadKey(channel, threadTs));
8413
- }
8414
- function isThreadOwned(channel, threadTs) {
8415
- return ownedThreads.has(threadKey(channel, threadTs));
8760
+ // src/agent/session-lock.ts
8761
+ async function withSessionLock(sessionId, fn) {
8762
+ let state2 = locks.get(sessionId);
8763
+ if (!state2) {
8764
+ state2 = { tail: Promise.resolve(), pending: 0 };
8765
+ locks.set(sessionId, state2);
8766
+ }
8767
+ state2.pending++;
8768
+ const prev = state2.tail;
8769
+ let release;
8770
+ const next = new Promise((resolve13) => {
8771
+ release = resolve13;
8772
+ });
8773
+ state2.tail = prev.then(() => next);
8774
+ await prev;
8775
+ try {
8776
+ return await fn();
8777
+ } finally {
8778
+ release();
8779
+ state2.pending--;
8780
+ if (state2.pending === 0 && locks.get(sessionId) === state2) {
8781
+ locks.delete(sessionId);
8782
+ }
8783
+ }
8416
8784
  }
8417
- function isSelfAuthored(event, self) {
8418
- if (!self) return true;
8419
- if (self.botId && event.bot_id && event.bot_id === self.botId) return true;
8420
- if (self.botUserId && event.user && event.user === self.botUserId) return true;
8421
- return false;
8785
+ function isSessionLocked(sessionId) {
8786
+ const s = locks.get(sessionId);
8787
+ return !!s && s.pending > 0;
8422
8788
  }
8423
- function slackEventToInboundResult(event, opts = {}) {
8424
- if (!event) return { event: null, dropReason: "empty_text" };
8425
- const self = opts.self ?? getCachedSlackSelfIdentity();
8426
- const isBotAuthored = !!event.bot_id || event.type === "message" && event.subtype === "bot_message";
8427
- if (isBotAuthored && isSelfAuthored(event, self)) {
8428
- return { event: null, dropReason: "bot_message" };
8429
- }
8430
- if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
8431
- return { event: null, dropReason: "ignored_subtype" };
8789
+ var locks;
8790
+ var init_session_lock = __esm({
8791
+ "src/agent/session-lock.ts"() {
8792
+ "use strict";
8793
+ locks = /* @__PURE__ */ new Map();
8432
8794
  }
8433
- const isDm = event.type === "message" && event.channel_type === "im";
8434
- const isThreadReply = event.type === "message" && !isDm && typeof event.thread_ts === "string" && event.thread_ts !== event.ts;
8435
- 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.
8436
- typeof event.channel === "string");
8437
- if (event.type !== "app_mention" && !isDm && !isThreadReply) {
8438
- if (isNonThreadChannelMsg) {
8439
- return { event: null, dropReason: "non_thread_channel_msg" };
8795
+ });
8796
+
8797
+ // src/orchestrator/webhook-events.ts
8798
+ import { existsSync as existsSync17, readFileSync as readFileSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
8799
+ import { dirname as dirname7, join as join10 } from "path";
8800
+ import { nanoid as nanoid4 } from "nanoid";
8801
+ function logFilePath() {
8802
+ return join10(getAppDataDirectory(), "webhook-events.jsonl");
8803
+ }
8804
+ function ensureLoaded() {
8805
+ if (cache !== null) return cache;
8806
+ cache = [];
8807
+ try {
8808
+ const p = logFilePath();
8809
+ if (!existsSync17(p)) return cache;
8810
+ const lines = readFileSync8(p, "utf-8").split("\n").filter(Boolean);
8811
+ for (const line of lines) {
8812
+ try {
8813
+ cache.push(JSON.parse(line));
8814
+ } catch {
8815
+ }
8440
8816
  }
8441
- if (event.type !== "message") {
8442
- return { event: null, dropReason: "non_message_event" };
8817
+ if (cache.length > MAX_EVENTS) {
8818
+ cache = cache.slice(-MAX_EVENTS);
8819
+ try {
8820
+ writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
8821
+ } catch {
8822
+ }
8443
8823
  }
8444
- return { event: null, dropReason: "unsupported_type" };
8824
+ } catch {
8445
8825
  }
8446
- const text = (event.text ?? "").trim();
8447
- if (!text) return { event: null, dropReason: "empty_text" };
8448
- const policy = getSlackAllowlistPolicy();
8449
- const userAllowlistActive = policy.allowedUsers.length > 0;
8450
- const userOk = !userAllowlistActive || event.user && policy.allowedUsers.includes(event.user);
8451
- if (isDm) {
8452
- if (!policy.allowDmsFromAnyone && !(event.user && policy.allowedUsers.includes(event.user))) {
8453
- return { event: null, dropReason: "dm_blocked" };
8454
- }
8455
- } else {
8456
- const channelAllowlistActive = policy.allowedChannels.length > 0;
8457
- if (channelAllowlistActive && !policy.allowedChannels.includes(event.channel)) {
8458
- return { event: null, dropReason: "channel_not_allowed" };
8459
- }
8460
- if (!userOk) {
8461
- return { event: null, dropReason: "user_not_allowed" };
8462
- }
8826
+ return cache;
8827
+ }
8828
+ function appendEvent(ev) {
8829
+ const list = ensureLoaded();
8830
+ list.push(ev);
8831
+ if (list.length > MAX_EVENTS) list.shift();
8832
+ try {
8833
+ const p = logFilePath();
8834
+ mkdirSync7(dirname7(p), { recursive: true });
8835
+ appendFileSync3(p, JSON.stringify(ev) + "\n");
8836
+ } catch {
8463
8837
  }
8464
- const ref = {
8465
- channel: "slack",
8466
- slackChannel: event.channel,
8467
- threadTs: event.thread_ts || event.ts,
8468
- teamId: event.team,
8469
- user: event.user
8470
- };
8471
- const label = slackChannel.displayLabel(ref);
8472
- return {
8473
- event: {
8474
- ref,
8475
- content: `[${label}] ${text}`,
8476
- wake: "now",
8477
- enqueuedAt: /* @__PURE__ */ new Date()
8478
- }
8479
- };
8480
8838
  }
8481
- var ownedThreads, slackChannel, IGNORED_MESSAGE_SUBTYPES;
8482
- var init_slack = __esm({
8483
- "src/integrations/channels/slack.ts"() {
8839
+ function recordEvent(ev) {
8840
+ const full = {
8841
+ id: ev.id ?? nanoid4(),
8842
+ ts: ev.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
8843
+ source: ev.source,
8844
+ status: ev.status,
8845
+ subtype: ev.subtype,
8846
+ channel: ev.channel,
8847
+ user: ev.user,
8848
+ textSnippet: ev.textSnippet?.slice(0, 200),
8849
+ dropReason: ev.dropReason,
8850
+ error: ev.error,
8851
+ sessionId: ev.sessionId,
8852
+ durationMs: ev.durationMs,
8853
+ meta: ev.meta
8854
+ };
8855
+ appendEvent(full);
8856
+ return full.id;
8857
+ }
8858
+ function updateEvent(id, patch) {
8859
+ const list = ensureLoaded();
8860
+ const i = list.findIndex((e) => e.id === id);
8861
+ if (i < 0) return;
8862
+ list[i] = { ...list[i], ...patch };
8863
+ try {
8864
+ const p = logFilePath();
8865
+ mkdirSync7(dirname7(p), { recursive: true });
8866
+ writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
8867
+ } catch {
8868
+ }
8869
+ }
8870
+ function listEvents(filter = {}) {
8871
+ const list = ensureLoaded();
8872
+ const q = filter.q?.toLowerCase();
8873
+ const sinceTs = filter.since ? Date.parse(filter.since) : -Infinity;
8874
+ const beforeTs = filter.before ? Date.parse(filter.before) : Infinity;
8875
+ const matched = list.filter((e) => {
8876
+ if (filter.source && e.source !== filter.source) return false;
8877
+ if (filter.status && e.status !== filter.status) return false;
8878
+ const t = Date.parse(e.ts);
8879
+ if (t < sinceTs) return false;
8880
+ if (t >= beforeTs) return false;
8881
+ if (q) {
8882
+ const hay = `${e.channel ?? ""} ${e.user ?? ""} ${e.textSnippet ?? ""} ${e.dropReason ?? ""} ${e.error ?? ""} ${e.subtype ?? ""}`.toLowerCase();
8883
+ if (!hay.includes(q)) return false;
8884
+ }
8885
+ return true;
8886
+ });
8887
+ matched.reverse();
8888
+ const offset = Math.max(0, filter.offset ?? 0);
8889
+ const limit = Math.min(500, Math.max(1, filter.limit ?? 50));
8890
+ return {
8891
+ events: matched.slice(offset, offset + limit),
8892
+ total: matched.length
8893
+ };
8894
+ }
8895
+ function clearAllEvents() {
8896
+ cache = [];
8897
+ try {
8898
+ writeFileSync4(logFilePath(), "");
8899
+ } catch {
8900
+ }
8901
+ }
8902
+ var MAX_EVENTS, cache;
8903
+ var init_webhook_events = __esm({
8904
+ "src/orchestrator/webhook-events.ts"() {
8905
+ "use strict";
8906
+ init_config();
8907
+ MAX_EVENTS = 1e3;
8908
+ cache = null;
8909
+ }
8910
+ });
8911
+
8912
+ // src/orchestrator/inbox.ts
8913
+ var inbox_exports = {};
8914
+ __export(inbox_exports, {
8915
+ clearInbox: () => clearInbox,
8916
+ flush: () => flush,
8917
+ peekInbox: () => peekInbox,
8918
+ pushToInbox: () => pushToInbox,
8919
+ setFlushHandler: () => setFlushHandler
8920
+ });
8921
+ function setFlushHandler(fn) {
8922
+ flushHandler = fn;
8923
+ }
8924
+ function entryFor(sessionId) {
8925
+ let e = inboxes.get(sessionId);
8926
+ if (!e) {
8927
+ e = { pending: [] };
8928
+ inboxes.set(sessionId, e);
8929
+ }
8930
+ return e;
8931
+ }
8932
+ function pushToInbox(orchestratorSessionId, event) {
8933
+ const e = entryFor(orchestratorSessionId);
8934
+ e.pending.push(event);
8935
+ try {
8936
+ trackInbound(orchestratorSessionId, event);
8937
+ } catch {
8938
+ }
8939
+ if (event.wake === "now") {
8940
+ scheduleFlush(orchestratorSessionId);
8941
+ }
8942
+ }
8943
+ function scheduleFlush(sessionId) {
8944
+ const e = inboxes.get(sessionId);
8945
+ if (!e) return;
8946
+ if (e.timer) clearTimeout(e.timer);
8947
+ e.timer = setTimeout(() => {
8948
+ void flush(sessionId);
8949
+ }, FLUSH_DEBOUNCE_MS);
8950
+ }
8951
+ async function flush(sessionId) {
8952
+ const e = inboxes.get(sessionId);
8953
+ if (!e) return;
8954
+ if (e.timer) {
8955
+ clearTimeout(e.timer);
8956
+ e.timer = void 0;
8957
+ }
8958
+ const events = e.pending.splice(0);
8959
+ if (events.length === 0) return;
8960
+ if (!flushHandler) {
8961
+ console.warn("[orchestrator-inbox] flush called with no handler installed; dropping events");
8962
+ return;
8963
+ }
8964
+ try {
8965
+ await flushHandler(sessionId, events);
8966
+ } catch (err) {
8967
+ console.error("[orchestrator-inbox] flush handler threw:", err?.message || err);
8968
+ }
8969
+ }
8970
+ function peekInbox(sessionId) {
8971
+ return inboxes.get(sessionId)?.pending.slice() ?? [];
8972
+ }
8973
+ function clearInbox(sessionId) {
8974
+ const e = inboxes.get(sessionId);
8975
+ if (!e) return;
8976
+ if (e.timer) {
8977
+ clearTimeout(e.timer);
8978
+ e.timer = void 0;
8979
+ }
8980
+ e.pending.length = 0;
8981
+ }
8982
+ var inboxes, FLUSH_DEBOUNCE_MS, flushHandler;
8983
+ var init_inbox = __esm({
8984
+ "src/orchestrator/inbox.ts"() {
8985
+ "use strict";
8986
+ init_inbox_acks();
8987
+ inboxes = /* @__PURE__ */ new Map();
8988
+ FLUSH_DEBOUNCE_MS = 200;
8989
+ flushHandler = null;
8990
+ }
8991
+ });
8992
+
8993
+ // src/orchestrator/inbox-acks.ts
8994
+ var inbox_acks_exports = {};
8995
+ __export(inbox_acks_exports, {
8996
+ MAX_ATTEMPTS: () => MAX_ATTEMPTS,
8997
+ RECONCILE_EVERY_MS: () => RECONCILE_EVERY_MS,
8998
+ REPLAY_AFTER_MS: () => REPLAY_AFTER_MS,
8999
+ __getAck: () => __getAck,
9000
+ __listAcks: () => __listAcks,
9001
+ __resetAcks: () => __resetAcks,
9002
+ eventKey: () => eventKey,
9003
+ markRespondedForThread: () => markRespondedForThread,
9004
+ markState: () => markState,
9005
+ reconcileOnce: () => reconcileOnce,
9006
+ resolveBatchOnTurnEnd: () => resolveBatchOnTurnEnd,
9007
+ startReconciler: () => startReconciler,
9008
+ stopReconciler: () => stopReconciler,
9009
+ trackInbound: () => trackInbound
9010
+ });
9011
+ function eventKey(event) {
9012
+ const ref = event.ref;
9013
+ const ch = ref?.channel ?? "unknown";
9014
+ switch (ch) {
9015
+ case "slack":
9016
+ return `slack${SEP}${ref.slackChannel}${SEP}${ref.messageTs ?? ref.threadTs ?? ""}`;
9017
+ case "system":
9018
+ return `system${SEP}${ref.workerId}${SEP}${ref.kind}`;
9019
+ case "webhook":
9020
+ return `webhook${SEP}${ref.webhookId}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}`;
9021
+ case "schedule":
9022
+ return `schedule${SEP}${ref.scheduleId}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}`;
9023
+ default:
9024
+ return `${ch}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}${SEP}${event.content.slice(0, 40)}`;
9025
+ }
9026
+ }
9027
+ function trackInbound(sessionId, event) {
9028
+ if (event.wake === "never") return null;
9029
+ const key2 = eventKey(event);
9030
+ const existing = ledger.get(key2);
9031
+ if (existing) return key2;
9032
+ const ref = event.ref;
9033
+ const now = Date.now();
9034
+ const entry2 = {
9035
+ key: key2,
9036
+ sessionId,
9037
+ event,
9038
+ channel: ref?.channel ?? "unknown",
9039
+ state: "working",
9040
+ attempts: 0,
9041
+ trackedAt: now,
9042
+ updatedAt: now
9043
+ };
9044
+ if (ref?.channel === "slack") {
9045
+ entry2.slackChannel = ref.slackChannel;
9046
+ entry2.threadTs = ref.threadTs;
9047
+ entry2.messageTs = ref.messageTs;
9048
+ }
9049
+ ledger.set(key2, entry2);
9050
+ if (ledger.size > MAX_ENTRIES) pruneOldest();
9051
+ return key2;
9052
+ }
9053
+ function markState(key2, state2) {
9054
+ const entry2 = ledger.get(key2);
9055
+ if (!entry2) return;
9056
+ if (TERMINAL.has(entry2.state)) return;
9057
+ entry2.state = state2;
9058
+ entry2.updatedAt = Date.now();
9059
+ if (entry2.channel === "slack" && entry2.slackChannel && entry2.messageTs) {
9060
+ fireResultReaction(entry2.slackChannel, entry2.messageTs, state2);
9061
+ }
9062
+ }
9063
+ function markRespondedForThread(slackChannel2, threadTs) {
9064
+ if (!slackChannel2 || !threadTs) return;
9065
+ for (const entry2 of ledger.values()) {
9066
+ if (entry2.channel === "slack" && entry2.state === "working" && entry2.slackChannel === slackChannel2 && entry2.threadTs === threadTs) {
9067
+ markState(entry2.key, "responded");
9068
+ }
9069
+ }
9070
+ }
9071
+ function resolveBatchOnTurnEnd(events, ok) {
9072
+ if (!ok) return;
9073
+ for (const ev of events) {
9074
+ const key2 = eventKey(ev);
9075
+ const entry2 = ledger.get(key2);
9076
+ if (!entry2 || entry2.state !== "working") continue;
9077
+ if (entry2.channel === "slack") continue;
9078
+ markState(key2, "responded");
9079
+ }
9080
+ }
9081
+ async function reconcileOnce(now = Date.now()) {
9082
+ let pushToInbox2 = null;
9083
+ const toReplay = [];
9084
+ for (const entry2 of ledger.values()) {
9085
+ if (TERMINAL.has(entry2.state)) {
9086
+ if (now - entry2.updatedAt > PRUNE_AFTER_MS) ledger.delete(entry2.key);
9087
+ continue;
9088
+ }
9089
+ if (isSessionLocked(entry2.sessionId)) continue;
9090
+ if (now - entry2.updatedAt < REPLAY_AFTER_MS) continue;
9091
+ if (entry2.attempts >= MAX_ATTEMPTS) {
9092
+ failEntry(entry2);
9093
+ continue;
9094
+ }
9095
+ toReplay.push(entry2);
9096
+ }
9097
+ if (toReplay.length === 0) return;
9098
+ try {
9099
+ ({ pushToInbox: pushToInbox2 } = await Promise.resolve().then(() => (init_inbox(), inbox_exports)));
9100
+ } catch {
9101
+ return;
9102
+ }
9103
+ for (const entry2 of toReplay) {
9104
+ entry2.attempts += 1;
9105
+ entry2.updatedAt = Date.now();
9106
+ const nudged = {
9107
+ ...entry2.event,
9108
+ 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.]
9109
+ ${entry2.event.content}`,
9110
+ wake: "now"
9111
+ };
9112
+ try {
9113
+ pushToInbox2(entry2.sessionId, nudged);
9114
+ } catch {
9115
+ }
9116
+ }
9117
+ }
9118
+ function failEntry(entry2) {
9119
+ entry2.state = "failed";
9120
+ entry2.updatedAt = Date.now();
9121
+ if (entry2.channel === "slack" && entry2.slackChannel && entry2.messageTs) {
9122
+ fireResultReaction(entry2.slackChannel, entry2.messageTs, "failed");
9123
+ if (entry2.threadTs) {
9124
+ fireFallback(
9125
+ entry2.slackChannel,
9126
+ entry2.threadTs,
9127
+ `: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.`
9128
+ );
9129
+ }
9130
+ }
9131
+ recordEvent({
9132
+ source: "daemon",
9133
+ status: "failed",
9134
+ channel: entry2.channel,
9135
+ sessionId: entry2.sessionId,
9136
+ error: `unacknowledged after ${entry2.attempts} replay attempt(s)`,
9137
+ textSnippet: entry2.event.content.slice(0, 200),
9138
+ meta: { ackKey: entry2.key, ackState: "failed" }
9139
+ });
9140
+ }
9141
+ function pruneOldest() {
9142
+ const terminal = [];
9143
+ for (const e of ledger.values()) if (TERMINAL.has(e.state)) terminal.push(e);
9144
+ terminal.sort((a, b) => a.updatedAt - b.updatedAt);
9145
+ for (const e of terminal) {
9146
+ if (ledger.size <= MAX_ENTRIES) break;
9147
+ ledger.delete(e.key);
9148
+ }
9149
+ while (ledger.size > MAX_ENTRIES) {
9150
+ const oldest = ledger.keys().next().value;
9151
+ if (!oldest) break;
9152
+ ledger.delete(oldest);
9153
+ }
9154
+ }
9155
+ function fireResultReaction(channel, ts, state2) {
9156
+ if (typeof addResultReaction !== "function") return;
9157
+ try {
9158
+ void Promise.resolve(addResultReaction(channel, ts, state2)).catch(() => {
9159
+ });
9160
+ } catch {
9161
+ }
9162
+ }
9163
+ function fireFallback(channel, threadTs, text) {
9164
+ if (typeof postThreadMessage !== "function") return;
9165
+ try {
9166
+ void Promise.resolve(postThreadMessage(channel, threadTs, text)).catch(() => {
9167
+ });
9168
+ } catch {
9169
+ }
9170
+ }
9171
+ function startReconciler() {
9172
+ if (reconcileTimer) return;
9173
+ reconcileTimer = setInterval(() => {
9174
+ void reconcileOnce();
9175
+ }, RECONCILE_EVERY_MS);
9176
+ if (typeof reconcileTimer.unref === "function") reconcileTimer.unref();
9177
+ }
9178
+ function stopReconciler() {
9179
+ if (reconcileTimer) {
9180
+ clearInterval(reconcileTimer);
9181
+ reconcileTimer = null;
9182
+ }
9183
+ }
9184
+ function __getAck(key2) {
9185
+ return ledger.get(key2);
9186
+ }
9187
+ function __listAcks() {
9188
+ return [...ledger.values()];
9189
+ }
9190
+ function __resetAcks() {
9191
+ ledger.clear();
9192
+ }
9193
+ var REPLAY_AFTER_MS, RECONCILE_EVERY_MS, MAX_ATTEMPTS, PRUNE_AFTER_MS, MAX_ENTRIES, TERMINAL, SEP, ledger, reconcileTimer;
9194
+ var init_inbox_acks = __esm({
9195
+ "src/orchestrator/inbox-acks.ts"() {
9196
+ "use strict";
9197
+ init_session_lock();
9198
+ init_webhook_events();
9199
+ init_client3();
9200
+ REPLAY_AFTER_MS = 3 * 6e4;
9201
+ RECONCILE_EVERY_MS = 6e4;
9202
+ MAX_ATTEMPTS = 2;
9203
+ PRUNE_AFTER_MS = 60 * 6e4;
9204
+ MAX_ENTRIES = 5e3;
9205
+ TERMINAL = /* @__PURE__ */ new Set(["responded", "skipped", "handed_off", "failed"]);
9206
+ SEP = "\u241F";
9207
+ ledger = /* @__PURE__ */ new Map();
9208
+ reconcileTimer = null;
9209
+ }
9210
+ });
9211
+
9212
+ // src/integrations/channels/slack.ts
9213
+ function threadKey(channel, threadTs) {
9214
+ return `${channel}\u241F${threadTs}`;
9215
+ }
9216
+ function markThreadOwned(channel, threadTs) {
9217
+ ownedThreads.add(threadKey(channel, threadTs));
9218
+ }
9219
+ function isThreadOwned(channel, threadTs) {
9220
+ return ownedThreads.has(threadKey(channel, threadTs));
9221
+ }
9222
+ function isSelfAuthored(event, self) {
9223
+ if (!self) return true;
9224
+ if (self.botId && event.bot_id && event.bot_id === self.botId) return true;
9225
+ if (self.botUserId && event.user && event.user === self.botUserId) return true;
9226
+ return false;
9227
+ }
9228
+ function slackEventToInboundResult(event, opts = {}) {
9229
+ if (!event) return { event: null, dropReason: "empty_text" };
9230
+ const self = opts.self ?? getCachedSlackSelfIdentity();
9231
+ const isBotAuthored = !!event.bot_id || event.type === "message" && event.subtype === "bot_message";
9232
+ if (isBotAuthored && isSelfAuthored(event, self)) {
9233
+ return { event: null, dropReason: "bot_message" };
9234
+ }
9235
+ if (event.type === "message" && event.subtype && IGNORED_MESSAGE_SUBTYPES.has(event.subtype)) {
9236
+ return { event: null, dropReason: "ignored_subtype" };
9237
+ }
9238
+ const isDm = event.type === "message" && event.channel_type === "im";
9239
+ const isThreadReply = event.type === "message" && !isDm && typeof event.thread_ts === "string" && event.thread_ts !== event.ts;
9240
+ 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.
9241
+ typeof event.channel === "string");
9242
+ if (event.type !== "app_mention" && !isDm && !isThreadReply) {
9243
+ if (isNonThreadChannelMsg) {
9244
+ return { event: null, dropReason: "non_thread_channel_msg" };
9245
+ }
9246
+ if (event.type !== "message") {
9247
+ return { event: null, dropReason: "non_message_event" };
9248
+ }
9249
+ return { event: null, dropReason: "unsupported_type" };
9250
+ }
9251
+ const text = (event.text ?? "").trim();
9252
+ const hasFiles = Array.isArray(event.files) && event.files.length > 0;
9253
+ if (!text && !hasFiles) return { event: null, dropReason: "empty_text" };
9254
+ const policy = getSlackAllowlistPolicy();
9255
+ const userAllowlistActive = policy.allowedUsers.length > 0;
9256
+ const userOk = !userAllowlistActive || event.user && policy.allowedUsers.includes(event.user);
9257
+ if (isDm) {
9258
+ if (!policy.allowDmsFromAnyone && !(event.user && policy.allowedUsers.includes(event.user))) {
9259
+ return { event: null, dropReason: "dm_blocked" };
9260
+ }
9261
+ } else {
9262
+ const channelAllowlistActive = policy.allowedChannels.length > 0;
9263
+ if (channelAllowlistActive && !policy.allowedChannels.includes(event.channel)) {
9264
+ return { event: null, dropReason: "channel_not_allowed" };
9265
+ }
9266
+ if (!userOk) {
9267
+ return { event: null, dropReason: "user_not_allowed" };
9268
+ }
9269
+ }
9270
+ const ref = {
9271
+ channel: "slack",
9272
+ slackChannel: event.channel,
9273
+ // For thread replies, threadTs points at the parent (so our reply
9274
+ // continues the thread). messageTs is the inbound message's own ts —
9275
+ // used by reaction add/remove (which target the message itself, not
9276
+ // its parent) and any future "edit this message" operations.
9277
+ threadTs: event.thread_ts || event.ts,
9278
+ messageTs: event.ts,
9279
+ teamId: event.team,
9280
+ user: event.user
9281
+ };
9282
+ const label = slackChannel.displayLabel(ref);
9283
+ return {
9284
+ event: {
9285
+ ref,
9286
+ content: `[${label}] ${text}`,
9287
+ wake: "now",
9288
+ enqueuedAt: /* @__PURE__ */ new Date()
9289
+ }
9290
+ };
9291
+ }
9292
+ var ownedThreads, slackChannel, IGNORED_MESSAGE_SUBTYPES;
9293
+ var init_slack = __esm({
9294
+ "src/integrations/channels/slack.ts"() {
8484
9295
  "use strict";
8485
9296
  init_client3();
8486
9297
  ownedThreads = /* @__PURE__ */ new Set();
@@ -8500,6 +9311,8 @@ var init_slack = __esm({
8500
9311
  if (r.slackChannel && r.threadTs) {
8501
9312
  markThreadOwned(r.slackChannel, r.threadTs);
8502
9313
  noteBotPostedInThread(r.slackChannel, r.threadTs);
9314
+ void Promise.resolve().then(() => (init_inbox_acks(), inbox_acks_exports)).then((m) => m.markRespondedForThread(r.slackChannel, r.threadTs)).catch(() => {
9315
+ });
8503
9316
  }
8504
9317
  },
8505
9318
  displayLabel(ref) {
@@ -8532,8 +9345,14 @@ var init_slack = __esm({
8532
9345
  // also-broadcast-to-channel replies; the regular thread reply already fires
8533
9346
  "message_replied",
8534
9347
  // legacy parent-thread bump
8535
- "file_share",
8536
- // we'd handle these later; for now skip to avoid double-handling
9348
+ // NOTE: `file_share` is intentionally NOT ignored. It's the subtype Slack
9349
+ // attaches to a normal user message that includes file uploads — the
9350
+ // event still has `text`, `user`, `channel`, and `files: [...]`. The
9351
+ // route handler (src/server/routes/slack.ts) hands the `files[]` array
9352
+ // to `ingestSlackFiles` (src/integrations/slack/files.ts), which
9353
+ // downloads each file using the bot token, re-uploads it to GCS via
9354
+ // the remote-server's /storage/upload-url endpoint, and appends the
9355
+ // resulting short public URLs to the inbound message content.
8537
9356
  "reply_broadcast",
8538
9357
  "tombstone",
8539
9358
  "huddle_thread"
@@ -8742,7 +9561,7 @@ var init_messenger = __esm({
8742
9561
  });
8743
9562
 
8744
9563
  // src/orchestrator/schedules-store.ts
8745
- import { nanoid as nanoid4 } from "nanoid";
9564
+ import { nanoid as nanoid5 } from "nanoid";
8746
9565
  async function readOrch(orchestratorSessionId) {
8747
9566
  const s = await sessionQueries.getById(orchestratorSessionId);
8748
9567
  if (!s) return null;
@@ -8757,7 +9576,7 @@ async function createSchedule(orchestratorSessionId, input) {
8757
9576
  const data = await readOrch(orchestratorSessionId);
8758
9577
  if (!data) throw new Error("orchestrator session not found");
8759
9578
  const row = {
8760
- id: `sch_${nanoid4(10)}`,
9579
+ id: `sch_${nanoid5(10)}`,
8761
9580
  name: input.name,
8762
9581
  cron: input.cron,
8763
9582
  prompt: input.prompt,
@@ -8794,7 +9613,7 @@ var init_schedules_store = __esm({
8794
9613
 
8795
9614
  // src/orchestrator/webhooks-store.ts
8796
9615
  import { randomBytes } from "crypto";
8797
- import { nanoid as nanoid5 } from "nanoid";
9616
+ import { nanoid as nanoid6 } from "nanoid";
8798
9617
  function newToken() {
8799
9618
  return randomBytes(24).toString("base64url");
8800
9619
  }
@@ -8811,7 +9630,7 @@ async function createWebhook(orchestratorSessionId, input) {
8811
9630
  const data = await readOrch2(orchestratorSessionId);
8812
9631
  if (!data) throw new Error("orchestrator session not found");
8813
9632
  const row = {
8814
- id: `whk_${nanoid5(10)}`,
9633
+ id: `whk_${nanoid6(10)}`,
8815
9634
  name: input.name,
8816
9635
  token: newToken(),
8817
9636
  wake: input.wake ?? "now",
@@ -8956,7 +9775,9 @@ function buildAgentTool(opts) {
8956
9775
  workingDirectory: input.workingDirectory ?? opts.defaultWorkingDirectory,
8957
9776
  name: input.name,
8958
9777
  maxIterations: input.maxIterations ?? 100,
8959
- orchestratorSessionId: opts.orchestratorSessionId
9778
+ orchestratorSessionId: opts.orchestratorSessionId,
9779
+ ...input.mcpServers ? { mcpServers: input.mcpServers } : {},
9780
+ ...input.skills ? { skills: input.skills } : {}
8960
9781
  }
8961
9782
  });
8962
9783
  return {
@@ -9129,6 +9950,26 @@ var init_orchestrator_actions = __esm({
9129
9950
  model: z14.string().optional().describe("spawn only: model override."),
9130
9951
  workingDirectory: z14.string().optional().describe("spawn only: working directory override."),
9131
9952
  maxIterations: z14.number().int().min(1).max(500).optional().describe("spawn only."),
9953
+ mcpServers: z14.array(
9954
+ z14.object({
9955
+ name: z14.string(),
9956
+ transport: z14.enum(["http", "sse", "stdio"]),
9957
+ url: z14.string().optional(),
9958
+ headers: z14.record(z14.string(), z14.string()).optional(),
9959
+ command: z14.string().optional(),
9960
+ args: z14.array(z14.string()).optional(),
9961
+ env: z14.record(z14.string(), z14.string()).optional()
9962
+ })
9963
+ ).optional().describe("spawn only: task-scoped MCP servers (with auth headers) connected for this worker only, tools exposed as mcp_<name>_<tool>."),
9964
+ skills: z14.array(
9965
+ z14.object({
9966
+ name: z14.string(),
9967
+ description: z14.string().optional(),
9968
+ content: z14.string(),
9969
+ alwaysApply: z14.boolean().optional(),
9970
+ globs: z14.array(z14.string()).optional()
9971
+ })
9972
+ ).optional().describe("spawn only: task-scoped skills (inline markdown) available to this worker only."),
9132
9973
  // message
9133
9974
  text: z14.string().optional().describe("message only: the text to deliver to the worker."),
9134
9975
  force: z14.boolean().optional().describe("message only: soft-interrupt the current step."),
@@ -9168,9 +10009,9 @@ var init_orchestrator_actions = __esm({
9168
10009
  });
9169
10010
 
9170
10011
  // src/integrations/mcp/store.ts
9171
- import { nanoid as nanoid6 } from "nanoid";
9172
- import { existsSync as existsSync17, readFileSync as readFileSync8 } from "fs";
9173
- import { resolve as resolve10, join as join10 } from "path";
10012
+ import { nanoid as nanoid7 } from "nanoid";
10013
+ import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
10014
+ import { resolve as resolve10, join as join11 } from "path";
9174
10015
  function readServers() {
9175
10016
  try {
9176
10017
  const cfg = getConfig();
@@ -9182,12 +10023,12 @@ function readServers() {
9182
10023
  function refreshMcpServersFromDisk() {
9183
10024
  const candidates = [
9184
10025
  resolve10(process.cwd(), "sparkecoder.config.json"),
9185
- join10(ensureAppDataDirectory(), "sparkecoder.config.json")
10026
+ join11(ensureAppDataDirectory(), "sparkecoder.config.json")
9186
10027
  ];
9187
10028
  for (const path of candidates) {
9188
- if (!existsSync17(path)) continue;
10029
+ if (!existsSync18(path)) continue;
9189
10030
  try {
9190
- const raw = JSON.parse(readFileSync8(path, "utf-8"));
10031
+ const raw = JSON.parse(readFileSync9(path, "utf-8"));
9191
10032
  const servers2 = Array.isArray(raw?.mcp?.servers) ? raw.mcp.servers : [];
9192
10033
  setMcpServers(servers2);
9193
10034
  return servers2;
@@ -9206,7 +10047,7 @@ function createMcpServer(input) {
9206
10047
  const all = readServers();
9207
10048
  validateInput(input);
9208
10049
  const row = {
9209
- id: `mcp_${nanoid6(10)}`,
10050
+ id: `mcp_${nanoid7(10)}`,
9210
10051
  name: sanitizeName(input.name),
9211
10052
  transport: input.transport,
9212
10053
  url: input.url,
@@ -9377,6 +10218,159 @@ var init_pool = __esm({
9377
10218
  }
9378
10219
  });
9379
10220
 
10221
+ // src/integrations/mcp/task-scoped.ts
10222
+ import { createMCPClient as createMCPClient2 } from "@ai-sdk/mcp";
10223
+ function sanitizeName2(raw) {
10224
+ return raw.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/_+/g, "_");
10225
+ }
10226
+ function buildHttpLikeTransport(server) {
10227
+ if (!server.url) {
10228
+ throw new Error(`${server.transport} transport requires a url`);
10229
+ }
10230
+ return {
10231
+ type: server.transport,
10232
+ url: server.url,
10233
+ headers: server.headers
10234
+ };
10235
+ }
10236
+ async function buildStdioTransport2(server) {
10237
+ if (!server.command) {
10238
+ throw new Error("stdio transport requires a command");
10239
+ }
10240
+ const mod = await import("@ai-sdk/mcp/mcp-stdio");
10241
+ const Cls = mod.Experimental_StdioMCPTransport || mod.StdioClientTransport;
10242
+ if (!Cls) throw new Error("@ai-sdk/mcp/mcp-stdio is missing the stdio transport class");
10243
+ return new Cls({
10244
+ command: server.command,
10245
+ args: server.args ?? [],
10246
+ env: server.env
10247
+ });
10248
+ }
10249
+ async function buildTransport(server) {
10250
+ return server.transport === "stdio" ? await buildStdioTransport2(server) : buildHttpLikeTransport(server);
10251
+ }
10252
+ async function connectTaskMcpServers(servers2, opts = {}) {
10253
+ const tools = {};
10254
+ const connected = [];
10255
+ const errors = [];
10256
+ const clients2 = [];
10257
+ for (const raw of servers2 ?? []) {
10258
+ const name = sanitizeName2(raw.name || "");
10259
+ if (!name) {
10260
+ errors.push({ name: raw.name || "(unnamed)", error: "server name is required" });
10261
+ continue;
10262
+ }
10263
+ let client = null;
10264
+ try {
10265
+ const transport = await buildTransport(raw);
10266
+ client = await createMCPClient2({ transport });
10267
+ clients2.push(client);
10268
+ const serverTools = await client.tools();
10269
+ for (const [toolName, t] of Object.entries(serverTools)) {
10270
+ tools[`mcp_${name}_${toolName}`] = t;
10271
+ }
10272
+ connected.push(name);
10273
+ } catch (err) {
10274
+ const message = err?.message || String(err);
10275
+ errors.push({ name, error: message });
10276
+ if (!opts.quiet) {
10277
+ console.warn(`[mcp:task] connecting "${name}" failed: ${message}`);
10278
+ }
10279
+ if (client) {
10280
+ try {
10281
+ await client.close();
10282
+ } catch {
10283
+ }
10284
+ const idx = clients2.indexOf(client);
10285
+ if (idx >= 0) clients2.splice(idx, 1);
10286
+ }
10287
+ }
10288
+ }
10289
+ let closed = false;
10290
+ const close = async () => {
10291
+ if (closed) return;
10292
+ closed = true;
10293
+ await Promise.all(
10294
+ clients2.map(async (c) => {
10295
+ try {
10296
+ await c.close();
10297
+ } catch {
10298
+ }
10299
+ })
10300
+ );
10301
+ };
10302
+ return { tools, connected, errors, close };
10303
+ }
10304
+ var init_task_scoped = __esm({
10305
+ "src/integrations/mcp/task-scoped.ts"() {
10306
+ "use strict";
10307
+ }
10308
+ });
10309
+
10310
+ // src/skills/task-scoped.ts
10311
+ import { mkdtemp, writeFile as writeFile5, rm } from "fs/promises";
10312
+ import { tmpdir } from "os";
10313
+ import { join as join12 } from "path";
10314
+ function safeFileName(name, index) {
10315
+ const base = name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
10316
+ return `${base || `skill-${index + 1}`}.md`;
10317
+ }
10318
+ function escapeFrontmatterValue(value) {
10319
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
10320
+ }
10321
+ function buildSkillFile(skill) {
10322
+ const lines = ["---"];
10323
+ lines.push(`name: ${escapeFrontmatterValue(skill.name)}`);
10324
+ lines.push(`description: ${escapeFrontmatterValue(skill.description || skill.name)}`);
10325
+ if (skill.alwaysApply) lines.push("alwaysApply: true");
10326
+ if (skill.globs && skill.globs.length > 0) {
10327
+ lines.push(`globs: [${skill.globs.map((g) => escapeFrontmatterValue(g)).join(", ")}]`);
10328
+ }
10329
+ lines.push("---");
10330
+ lines.push("");
10331
+ lines.push(skill.content);
10332
+ return lines.join("\n");
10333
+ }
10334
+ async function materializeTaskSkills(skills2, taskId) {
10335
+ if (!skills2 || skills2.length === 0) return null;
10336
+ const safeTaskId = taskId.replace(/[^a-zA-Z0-9_-]+/g, "_");
10337
+ const dir = await mkdtemp(join12(tmpdir(), `sparkecoder-task-skills-${safeTaskId}-`));
10338
+ const seen = /* @__PURE__ */ new Set();
10339
+ await Promise.all(
10340
+ skills2.map(async (skill, i) => {
10341
+ let fileName = safeFileName(skill.name, i);
10342
+ while (seen.has(fileName)) fileName = `dup-${i}-${fileName}`;
10343
+ seen.add(fileName);
10344
+ await writeFile5(join12(dir, fileName), buildSkillFile(skill), "utf-8");
10345
+ })
10346
+ );
10347
+ const loaded2 = await loadSkillsFromDirectory(dir, { priority: 1, defaultLoadType: "on_demand" });
10348
+ const alwaysSkills = loaded2.filter((s) => s.alwaysApply || s.loadType === "always");
10349
+ const onDemand = loaded2.filter((s) => !(s.alwaysApply || s.loadType === "always"));
10350
+ const always = (await Promise.all(
10351
+ alwaysSkills.map(async (s) => {
10352
+ const withContent = await loadSkillContent(s.name, [dir]);
10353
+ return withContent ? { ...s, content: withContent.content } : null;
10354
+ })
10355
+ )).filter((s) => s !== null);
10356
+ let cleaned = false;
10357
+ const cleanup2 = async () => {
10358
+ if (cleaned) return;
10359
+ cleaned = true;
10360
+ try {
10361
+ await rm(dir, { recursive: true, force: true });
10362
+ } catch {
10363
+ }
10364
+ };
10365
+ return { dir, always, onDemand, cleanup: cleanup2 };
10366
+ }
10367
+ var init_task_scoped2 = __esm({
10368
+ "src/skills/task-scoped.ts"() {
10369
+ "use strict";
10370
+ init_skills();
10371
+ }
10372
+ });
10373
+
9380
10374
  // src/utils/webhook.ts
9381
10375
  var webhook_exports = {};
9382
10376
  __export(webhook_exports, {
@@ -9535,79 +10529,57 @@ var init_pending_input = __esm({
9535
10529
  }
9536
10530
  });
9537
10531
 
9538
- // src/orchestrator/inbox.ts
9539
- var inbox_exports = {};
9540
- __export(inbox_exports, {
9541
- clearInbox: () => clearInbox,
9542
- flush: () => flush,
9543
- peekInbox: () => peekInbox,
9544
- pushToInbox: () => pushToInbox,
9545
- setFlushHandler: () => setFlushHandler
9546
- });
9547
- function setFlushHandler(fn) {
9548
- flushHandler = fn;
9549
- }
9550
- function entryFor(sessionId) {
9551
- let e = inboxes.get(sessionId);
9552
- if (!e) {
9553
- e = { pending: [] };
9554
- inboxes.set(sessionId, e);
9555
- }
9556
- return e;
10532
+ // src/utils/local-device-time.ts
10533
+ function formatLocalDeviceTimeLine(now = /* @__PURE__ */ new Date()) {
10534
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
10535
+ const formatted = now.toLocaleString("en-US", {
10536
+ weekday: "long",
10537
+ year: "numeric",
10538
+ month: "long",
10539
+ day: "numeric",
10540
+ hour: "numeric",
10541
+ minute: "2-digit",
10542
+ second: "2-digit",
10543
+ timeZoneName: "short"
10544
+ });
10545
+ return `${LOCAL_TIME_MARKER} ${formatted} (${timeZone})]`;
9557
10546
  }
9558
- function pushToInbox(orchestratorSessionId, event) {
9559
- const e = entryFor(orchestratorSessionId);
9560
- e.pending.push(event);
9561
- if (event.wake === "now") {
9562
- scheduleFlush(orchestratorSessionId);
9563
- }
10547
+ function hasLocalDeviceTimeLine(text) {
10548
+ return text.includes(LOCAL_TIME_MARKER);
9564
10549
  }
9565
- function scheduleFlush(sessionId) {
9566
- const e = inboxes.get(sessionId);
9567
- if (!e) return;
9568
- if (e.timer) clearTimeout(e.timer);
9569
- e.timer = setTimeout(() => {
9570
- void flush(sessionId);
9571
- }, FLUSH_DEBOUNCE_MS);
10550
+ function prependLocalDeviceTimeToUserMessage(text, now) {
10551
+ const trimmed = text.trim();
10552
+ if (!trimmed || hasLocalDeviceTimeLine(text)) return text;
10553
+ return `${formatLocalDeviceTimeLine(now)}
10554
+ ${text}`;
9572
10555
  }
9573
- async function flush(sessionId) {
9574
- const e = inboxes.get(sessionId);
9575
- if (!e) return;
9576
- if (e.timer) {
9577
- clearTimeout(e.timer);
9578
- e.timer = void 0;
9579
- }
9580
- const events = e.pending.splice(0);
9581
- if (events.length === 0) return;
9582
- if (!flushHandler) {
9583
- console.warn("[orchestrator-inbox] flush called with no handler installed; dropping events");
9584
- return;
10556
+ function prependLocalDeviceTimeToUserContent(content, now) {
10557
+ if (typeof content === "string") {
10558
+ return prependLocalDeviceTimeToUserMessage(content, now);
9585
10559
  }
9586
- try {
9587
- await flushHandler(sessionId, events);
9588
- } catch (err) {
9589
- console.error("[orchestrator-inbox] flush handler threw:", err?.message || err);
10560
+ const line = formatLocalDeviceTimeLine(now);
10561
+ if (content.some((p) => p.type === "text" && p.text && hasLocalDeviceTimeLine(p.text))) {
10562
+ return content;
9590
10563
  }
9591
- }
9592
- function peekInbox(sessionId) {
9593
- return inboxes.get(sessionId)?.pending.slice() ?? [];
9594
- }
9595
- function clearInbox(sessionId) {
9596
- const e = inboxes.get(sessionId);
9597
- if (!e) return;
9598
- if (e.timer) {
9599
- clearTimeout(e.timer);
9600
- e.timer = void 0;
10564
+ const userIdx = content.findIndex(
10565
+ (p) => p.type === "text" && p.text?.includes("[USER MESSAGE]")
10566
+ );
10567
+ if (userIdx >= 0 && content[userIdx].text) {
10568
+ const copy = content.map((p) => ({ ...p }));
10569
+ copy[userIdx] = {
10570
+ ...copy[userIdx],
10571
+ text: `${line}
10572
+ ${copy[userIdx].text}`
10573
+ };
10574
+ return copy;
9601
10575
  }
9602
- e.pending.length = 0;
10576
+ return [{ type: "text", text: line }, ...content];
9603
10577
  }
9604
- var inboxes, FLUSH_DEBOUNCE_MS, flushHandler;
9605
- var init_inbox = __esm({
9606
- "src/orchestrator/inbox.ts"() {
10578
+ var LOCAL_TIME_MARKER;
10579
+ var init_local_device_time = __esm({
10580
+ "src/utils/local-device-time.ts"() {
9607
10581
  "use strict";
9608
- inboxes = /* @__PURE__ */ new Map();
9609
- FLUSH_DEBOUNCE_MS = 200;
9610
- flushHandler = null;
10582
+ LOCAL_TIME_MARKER = "[Local device time:";
9611
10583
  }
9612
10584
  });
9613
10585
 
@@ -9819,10 +10791,10 @@ __export(recorder_exports, {
9819
10791
  });
9820
10792
  import { exec as exec5 } from "child_process";
9821
10793
  import { promisify as promisify5 } from "util";
9822
- import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
9823
- import { join as join11 } from "path";
9824
- import { tmpdir } from "os";
9825
- import { nanoid as nanoid7 } from "nanoid";
10794
+ import { writeFile as writeFile6, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm as rm2 } from "fs/promises";
10795
+ import { join as join13 } from "path";
10796
+ import { tmpdir as tmpdir2 } from "os";
10797
+ import { nanoid as nanoid8 } from "nanoid";
9826
10798
  async function checkFfmpeg() {
9827
10799
  try {
9828
10800
  await execAsync5("ffmpeg -version", { timeout: 5e3 });
@@ -9833,7 +10805,7 @@ async function checkFfmpeg() {
9833
10805
  }
9834
10806
  async function cleanup(dir) {
9835
10807
  try {
9836
- await rm(dir, { recursive: true, force: true });
10808
+ await rm2(dir, { recursive: true, force: true });
9837
10809
  } catch {
9838
10810
  }
9839
10811
  }
@@ -9877,21 +10849,21 @@ var init_recorder = __esm({
9877
10849
  */
9878
10850
  async encode() {
9879
10851
  if (this.frames.length === 0) return null;
9880
- const workDir = join11(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
10852
+ const workDir = join13(tmpdir2(), `sparkecoder-recording-${nanoid8(8)}`);
9881
10853
  await mkdir4(workDir, { recursive: true });
9882
10854
  try {
9883
10855
  for (let i = 0; i < this.frames.length; i++) {
9884
- const framePath = join11(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
9885
- await writeFile5(framePath, this.frames[i].data);
10856
+ const framePath = join13(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
10857
+ await writeFile6(framePath, this.frames[i].data);
9886
10858
  }
9887
10859
  const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
9888
10860
  const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
9889
10861
  const clampedFps = Math.max(1, Math.min(fps, 30));
9890
- const outputPath = join11(workDir, `recording_${this.sessionId}.mp4`);
10862
+ const outputPath = join13(workDir, `recording_${this.sessionId}.mp4`);
9891
10863
  const hasFfmpeg = await checkFfmpeg();
9892
10864
  if (hasFfmpeg) {
9893
10865
  await execAsync5(
9894
- `ffmpeg -y -framerate ${clampedFps} -i "${join11(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
10866
+ `ffmpeg -y -framerate ${clampedFps} -i "${join13(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
9895
10867
  { timeout: 12e4 }
9896
10868
  );
9897
10869
  } else {
@@ -9903,7 +10875,7 @@ var init_recorder = __esm({
9903
10875
  const files = await readdir5(workDir);
9904
10876
  for (const f of files) {
9905
10877
  if (f.startsWith("frame_")) {
9906
- await unlink2(join11(workDir, f)).catch(() => {
10878
+ await unlink2(join13(workDir, f)).catch(() => {
9907
10879
  });
9908
10880
  }
9909
10881
  }
@@ -9932,7 +10904,7 @@ import {
9932
10904
  stepCountIs as stepCountIs2
9933
10905
  } from "ai";
9934
10906
  import { z as z15 } from "zod";
9935
- import { nanoid as nanoid8 } from "nanoid";
10907
+ import { nanoid as nanoid9 } from "nanoid";
9936
10908
  function anySignal(signals) {
9937
10909
  const ctrl = new AbortController();
9938
10910
  for (const s of signals) {
@@ -9976,10 +10948,13 @@ var init_agent = __esm({
9976
10948
  init_prompts();
9977
10949
  init_orchestrator_actions();
9978
10950
  init_pool();
10951
+ init_task_scoped();
10952
+ init_task_scoped2();
9979
10953
  init_webhook2();
9980
10954
  init_questions();
9981
10955
  init_pending_input();
9982
10956
  init_inbox();
10957
+ init_local_device_time();
9983
10958
  init_system();
9984
10959
  init_context();
9985
10960
  init_prompts();
@@ -10142,9 +11117,11 @@ ${prompt}` });
10142
11117
  */
10143
11118
  async stream(options) {
10144
11119
  const config = getConfig();
10145
- const userContent = this.buildUserMessageContent(options.prompt, options.attachments);
11120
+ const prompt = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserMessage(options.prompt) : options.prompt;
11121
+ const userContent = this.buildUserMessageContent(prompt, options.attachments);
11122
+ const persistedUserContent = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserContent(userContent) : userContent;
10146
11123
  if (!options.skipSaveUserMessage) {
10147
- await this.context.addUserMessage(userContent);
11124
+ await this.context.addUserMessage(persistedUserContent);
10148
11125
  }
10149
11126
  await sessionQueries.updateStatus(this.session.id, "active");
10150
11127
  let systemPrompt = await buildSystemPrompt({
@@ -10222,7 +11199,8 @@ ${personality.trim()}
10222
11199
  */
10223
11200
  async run(options) {
10224
11201
  const config = getConfig();
10225
- await this.context.addUserMessage(options.prompt);
11202
+ const prompt = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserMessage(options.prompt) : options.prompt;
11203
+ await this.context.addUserMessage(prompt);
10226
11204
  const systemPrompt = await buildSystemPrompt({
10227
11205
  workingDirectory: this.session.workingDirectory,
10228
11206
  skillsDirectories: config.resolvedSkillsDirectories,
@@ -10266,355 +11244,387 @@ ${personality.trim()}
10266
11244
  */
10267
11245
  async runTask(options) {
10268
11246
  const config = getConfig();
10269
- const maxIterations = options.taskConfig.maxIterations ?? 50;
10270
- const webhookUrl = options.taskConfig.webhookUrl;
10271
- const parentTaskId = options.taskConfig.parentTaskId;
10272
- const fireWebhook = (type, data) => {
10273
- if (!webhookUrl) return;
10274
- sendWebhook(webhookUrl, {
10275
- type,
10276
- taskId: this.session.id,
10277
- sessionId: this.session.id,
10278
- ...parentTaskId ? { parentTaskId } : {},
10279
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10280
- data
10281
- });
10282
- };
10283
- const completion = { signal: null };
10284
- const onComplete = (signal) => {
10285
- completion.signal = signal;
10286
- };
10287
- let taskRecorder = null;
10288
- const sessionId = this.session.id;
10289
- const emit = options.writeSSE;
10290
- const bashProgressHandler = (progress) => {
10291
- options.onToolProgress?.({ toolName: "bash", data: progress });
10292
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "bash", data: progress })).catch(() => {
10293
- });
10294
- const port = progress.browserStreamPort;
10295
- if (port && progress.status === "started") {
10296
- Promise.resolve().then(() => (init_stream_proxy(), stream_proxy_exports)).then(({ getOrCreateProxy: getOrCreateProxy2 }) => {
10297
- const proxy = getOrCreateProxy2(sessionId, port);
10298
- if (!taskRecorder) {
10299
- Promise.resolve().then(() => (init_recorder(), recorder_exports)).then(({ FrameRecorder: FrameRecorder2 }) => {
10300
- taskRecorder = new FrameRecorder2(sessionId);
10301
- taskRecorder.start();
10302
- });
10303
- }
10304
- if (proxy.listenerCount("frame") === 0) {
10305
- proxy.on("frame", (frame) => {
10306
- taskRecorder?.addFrame(frame);
10307
- if (emit) emit(JSON.stringify({ type: "browser-frame", data: frame.data, metadata: frame.metadata })).catch(() => {
10308
- });
10309
- });
10310
- proxy.on("status", (s) => {
10311
- if (emit) emit(JSON.stringify({ type: "browser-status", ...s })).catch(() => {
10312
- });
10313
- });
10314
- }
11247
+ const taskScopedCleanups = [];
11248
+ try {
11249
+ const maxIterations = options.taskConfig.maxIterations ?? 50;
11250
+ const webhookUrl = options.taskConfig.webhookUrl;
11251
+ const parentTaskId = options.taskConfig.parentTaskId;
11252
+ const fireWebhook = (type, data) => {
11253
+ if (!webhookUrl) return;
11254
+ sendWebhook(webhookUrl, {
11255
+ type,
11256
+ taskId: this.session.id,
11257
+ sessionId: this.session.id,
11258
+ ...parentTaskId ? { parentTaskId } : {},
11259
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11260
+ data
10315
11261
  });
11262
+ };
11263
+ const completion = { signal: null };
11264
+ const onComplete = (signal) => {
11265
+ completion.signal = signal;
11266
+ };
11267
+ let taskMcpTools = {};
11268
+ if (options.mcpServers && options.mcpServers.length > 0) {
11269
+ const mcpConnection = await connectTaskMcpServers(options.mcpServers, { quiet: true });
11270
+ taskScopedCleanups.push(mcpConnection.close);
11271
+ taskMcpTools = mcpConnection.tools;
11272
+ if (mcpConnection.connected.length > 0) {
11273
+ console.log(`[TASK] connected ${mcpConnection.connected.length} task-scoped MCP server(s): ${mcpConnection.connected.join(", ")}`);
11274
+ }
11275
+ for (const e of mcpConnection.errors) {
11276
+ console.warn(`[TASK] task-scoped MCP server "${e.name}" failed to connect: ${e.error}`);
11277
+ if (options.writeSSE) await options.writeSSE(JSON.stringify({ type: "task-mcp-error", data: { name: e.name, error: e.error } }));
11278
+ }
10316
11279
  }
10317
- };
10318
- const taskTools = await createTools({
10319
- sessionId: this.session.id,
10320
- workingDirectory: this.session.workingDirectory,
10321
- skillsDirectories: config.resolvedSkillsDirectories,
10322
- onBashProgress: bashProgressHandler,
10323
- onWriteFileProgress: (progress) => {
10324
- options.onToolProgress?.({ toolName: "write_file", data: progress });
10325
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "write_file", data: progress })).catch(() => {
10326
- });
10327
- },
10328
- onSearchProgress: (progress) => {
10329
- options.onToolProgress?.({ toolName: "explore_agent", data: progress });
10330
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "explore_agent", data: progress })).catch(() => {
11280
+ const materializedSkills = await materializeTaskSkills(options.skills, this.session.id);
11281
+ if (materializedSkills) taskScopedCleanups.push(materializedSkills.cleanup);
11282
+ const taskSkillsDir = materializedSkills?.dir;
11283
+ let taskRecorder = null;
11284
+ const sessionId = this.session.id;
11285
+ const emit = options.writeSSE;
11286
+ const bashProgressHandler = (progress) => {
11287
+ options.onToolProgress?.({ toolName: "bash", data: progress });
11288
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "bash", data: progress })).catch(() => {
10331
11289
  });
10332
- },
10333
- taskTools: {
10334
- outputSchema: options.taskConfig.outputSchema,
10335
- onComplete,
10336
- onQuestion: async (question) => {
10337
- const payload = {
10338
- questionId: question.questionId,
10339
- question: question.question,
10340
- context: question.context,
10341
- choices: question.choices,
10342
- status: "pending"
10343
- };
10344
- const answerPromise = waitForTaskQuestionAnswer({
10345
- taskId: this.session.id,
10346
- questionId: question.questionId,
10347
- question: question.question,
10348
- context: question.context,
10349
- choices: question.choices
11290
+ const port = progress.browserStreamPort;
11291
+ if (port && progress.status === "started") {
11292
+ Promise.resolve().then(() => (init_stream_proxy(), stream_proxy_exports)).then(({ getOrCreateProxy: getOrCreateProxy2 }) => {
11293
+ const proxy = getOrCreateProxy2(sessionId, port);
11294
+ if (!taskRecorder) {
11295
+ Promise.resolve().then(() => (init_recorder(), recorder_exports)).then(({ FrameRecorder: FrameRecorder2 }) => {
11296
+ taskRecorder = new FrameRecorder2(sessionId);
11297
+ taskRecorder.start();
11298
+ });
11299
+ }
11300
+ if (proxy.listenerCount("frame") === 0) {
11301
+ proxy.on("frame", (frame) => {
11302
+ taskRecorder?.addFrame(frame);
11303
+ if (emit) emit(JSON.stringify({ type: "browser-frame", data: frame.data, metadata: frame.metadata })).catch(() => {
11304
+ });
11305
+ });
11306
+ proxy.on("status", (s) => {
11307
+ if (emit) emit(JSON.stringify({ type: "browser-status", ...s })).catch(() => {
11308
+ });
11309
+ });
11310
+ }
10350
11311
  });
10351
- fireWebhook("task.question", payload);
10352
- if (emit) {
10353
- await emit(JSON.stringify({ type: "task-question", data: payload }));
10354
- }
10355
- const orchId = this.session.config?.orchestratorSessionId;
10356
- if (orchId) {
10357
- pushToInbox(orchId, workerQuestionEvent(
10358
- this.session.id,
10359
- this.session.name || "worker",
10360
- question.question,
10361
- question.questionId
10362
- ));
10363
- }
10364
- const answer = await answerPromise;
10365
- const answeredPayload = {
10366
- questionId: question.questionId,
10367
- answer: answer.answer,
10368
- answeredBy: answer.answeredBy
10369
- };
10370
- fireWebhook("task.question_answered", answeredPayload);
10371
- if (emit) {
10372
- await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
11312
+ }
11313
+ };
11314
+ const taskTools = await createTools({
11315
+ sessionId: this.session.id,
11316
+ workingDirectory: this.session.workingDirectory,
11317
+ onBashProgress: bashProgressHandler,
11318
+ onWriteFileProgress: (progress) => {
11319
+ options.onToolProgress?.({ toolName: "write_file", data: progress });
11320
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "write_file", data: progress })).catch(() => {
11321
+ });
11322
+ },
11323
+ onSearchProgress: (progress) => {
11324
+ options.onToolProgress?.({ toolName: "explore_agent", data: progress });
11325
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "explore_agent", data: progress })).catch(() => {
11326
+ });
11327
+ },
11328
+ // Add the task-scoped skills temp dir (if any) so load_skill can list
11329
+ // and load the inline skills supplied with this task.
11330
+ skillsDirectories: taskSkillsDir ? [...config.resolvedSkillsDirectories, taskSkillsDir] : config.resolvedSkillsDirectories,
11331
+ taskTools: {
11332
+ outputSchema: options.taskConfig.outputSchema,
11333
+ onComplete,
11334
+ onQuestion: async (question) => {
11335
+ const payload = {
11336
+ questionId: question.questionId,
11337
+ question: question.question,
11338
+ context: question.context,
11339
+ choices: question.choices,
11340
+ status: "pending"
11341
+ };
11342
+ const answerPromise = waitForTaskQuestionAnswer({
11343
+ taskId: this.session.id,
11344
+ questionId: question.questionId,
11345
+ question: question.question,
11346
+ context: question.context,
11347
+ choices: question.choices
11348
+ });
11349
+ fireWebhook("task.question", payload);
11350
+ if (emit) {
11351
+ await emit(JSON.stringify({ type: "task-question", data: payload }));
11352
+ }
11353
+ const orchId = this.session.config?.orchestratorSessionId;
11354
+ if (orchId) {
11355
+ pushToInbox(orchId, workerQuestionEvent(
11356
+ this.session.id,
11357
+ this.session.name || "worker",
11358
+ question.question,
11359
+ question.questionId
11360
+ ));
11361
+ }
11362
+ const answer = await answerPromise;
11363
+ const answeredPayload = {
11364
+ questionId: question.questionId,
11365
+ answer: answer.answer,
11366
+ answeredBy: answer.answeredBy
11367
+ };
11368
+ fireWebhook("task.question_answered", answeredPayload);
11369
+ if (emit) {
11370
+ await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
11371
+ }
11372
+ return answer;
10373
11373
  }
10374
- return answer;
10375
11374
  }
11375
+ });
11376
+ for (const [name, t] of Object.entries(taskMcpTools)) {
11377
+ taskTools[name] = t;
10376
11378
  }
10377
- });
10378
- const baseSystemPrompt = await buildSystemPrompt({
10379
- workingDirectory: this.session.workingDirectory,
10380
- skillsDirectories: config.resolvedSkillsDirectories,
10381
- sessionId: this.session.id,
10382
- discoveredSkills: config.discoveredSkills,
10383
- activeFiles: []
10384
- });
10385
- const taskAddendum = buildTaskPromptAddendum(options.taskConfig.outputSchema);
10386
- const systemPrompt = `${baseSystemPrompt}
11379
+ const baseSystemPrompt = await buildSystemPrompt({
11380
+ workingDirectory: this.session.workingDirectory,
11381
+ skillsDirectories: taskSkillsDir ? [...config.resolvedSkillsDirectories, taskSkillsDir] : config.resolvedSkillsDirectories,
11382
+ sessionId: this.session.id,
11383
+ discoveredSkills: config.discoveredSkills,
11384
+ activeFiles: [],
11385
+ taskScopedSkills: materializedSkills ? { always: materializedSkills.always, onDemand: materializedSkills.onDemand } : void 0
11386
+ });
11387
+ const taskAddendum = buildTaskPromptAddendum(options.taskConfig.outputSchema);
11388
+ const systemPrompt = `${baseSystemPrompt}
10387
11389
 
10388
11390
  ${taskAddendum}`;
10389
- fireWebhook("task.started", { prompt: options.prompt });
10390
- if (emit) {
10391
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: options.prompt } }));
10392
- }
10393
- await this.context.addUserMessage(options.prompt);
10394
- let iteration = 0;
10395
- while (iteration < maxIterations) {
10396
- iteration++;
10397
- if (options.abortSignal?.aborted) {
10398
- const cancelError = "Task was cancelled";
10399
- fireWebhook("task.failed", { status: "failed", error: cancelError, iterations: iteration });
10400
- clearInterruptController(this.session.id);
10401
- return { status: "failed", error: cancelError, iterations: iteration };
11391
+ fireWebhook("task.started", { prompt: options.prompt });
11392
+ if (emit) {
11393
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: options.prompt } }));
10402
11394
  }
10403
- const pending = drainInputs(this.session.id);
10404
- for (const p of pending) {
10405
- const labelled = p.source === "orchestrator" ? `[message from orchestrator]
11395
+ await this.context.addUserMessage(options.prompt);
11396
+ let iteration = 0;
11397
+ while (iteration < maxIterations) {
11398
+ iteration++;
11399
+ if (options.abortSignal?.aborted) {
11400
+ const cancelError = "Task was cancelled";
11401
+ fireWebhook("task.failed", { status: "failed", error: cancelError, iterations: iteration });
11402
+ clearInterruptController(this.session.id);
11403
+ return { status: "failed", error: cancelError, iterations: iteration };
11404
+ }
11405
+ const pending = drainInputs(this.session.id);
11406
+ for (const p of pending) {
11407
+ const labelled = p.source === "orchestrator" ? `[message from orchestrator]
10406
11408
  ${p.text}` : p.source === "system" ? `[system note]
10407
11409
  ${p.text}` : p.text;
10408
- if (emit) {
10409
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: labelled } }));
10410
- }
10411
- await this.context.addUserMessage(labelled);
10412
- }
10413
- const interruptController = new AbortController();
10414
- registerInterruptController(this.session.id, interruptController);
10415
- const combinedAbort = options.abortSignal ? anySignal([options.abortSignal, interruptController.signal]) : interruptController.signal;
10416
- const messages = await this.context.getMessages();
10417
- const useAnthropic = isAnthropicModel(this.session.model);
10418
- if (emit) {
10419
- await emit(JSON.stringify({ type: "start", messageId: `msg_${Date.now()}` }));
10420
- }
10421
- let textStarted = false;
10422
- let textId = `text_${Date.now()}`;
10423
- let reasoningId = `reasoning_${Date.now()}`;
10424
- let reasoningStarted = false;
10425
- const toolCallStarts = /* @__PURE__ */ new Set();
10426
- const iterStream = streamText2({
10427
- model: resolveModel(this.session.model),
10428
- system: systemPrompt,
10429
- messages,
10430
- tools: wrapToolsNeverThrow(taskTools),
10431
- stopWhen: stepCountIs2(500),
10432
- abortSignal: combinedAbort,
10433
- providerOptions: useAnthropic ? {
10434
- anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
10435
- } : void 0,
10436
- // See the matching note in `stream()` — repair tool pairing before
10437
- // every step so we never feed the model an orphan tool-call.
10438
- prepareStep: async ({ messages: stepMessages }) => {
10439
- const paired = repairToolPairing(stepMessages);
10440
- const ordered = ensureToolResultsFollowCalls(paired);
10441
- if (ordered === stepMessages) return {};
10442
- return { messages: ordered };
10443
- },
10444
- onStepFinish: async (step) => {
10445
- options.onStepFinish?.(step);
10446
- fireWebhook("task.step_finished", { iteration, text: step.text });
10447
11410
  if (emit) {
10448
- if (textStarted) {
10449
- await emit(JSON.stringify({ type: "text-end", id: textId }));
10450
- textStarted = false;
10451
- textId = `text_${Date.now()}`;
10452
- }
10453
- await emit(JSON.stringify({ type: "finish-step" }));
11411
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: labelled } }));
10454
11412
  }
11413
+ await this.context.addUserMessage(labelled);
10455
11414
  }
10456
- });
10457
- for await (const part of iterStream.fullStream) {
10458
- if (part.type === "text-delta") {
10459
- if (emit) {
10460
- if (!textStarted) {
10461
- await emit(JSON.stringify({ type: "text-start", id: textId }));
10462
- textStarted = true;
11415
+ const interruptController = new AbortController();
11416
+ registerInterruptController(this.session.id, interruptController);
11417
+ const combinedAbort = options.abortSignal ? anySignal([options.abortSignal, interruptController.signal]) : interruptController.signal;
11418
+ const messages = await this.context.getMessages();
11419
+ const useAnthropic = isAnthropicModel(this.session.model);
11420
+ if (emit) {
11421
+ await emit(JSON.stringify({ type: "start", messageId: `msg_${Date.now()}` }));
11422
+ }
11423
+ let textStarted = false;
11424
+ let textId = `text_${Date.now()}`;
11425
+ let reasoningId = `reasoning_${Date.now()}`;
11426
+ let reasoningStarted = false;
11427
+ const toolCallStarts = /* @__PURE__ */ new Set();
11428
+ const iterStream = streamText2({
11429
+ model: resolveModel(this.session.model),
11430
+ system: systemPrompt,
11431
+ messages,
11432
+ tools: wrapToolsNeverThrow(taskTools),
11433
+ stopWhen: stepCountIs2(500),
11434
+ abortSignal: combinedAbort,
11435
+ providerOptions: useAnthropic ? {
11436
+ anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
11437
+ } : void 0,
11438
+ // See the matching note in `stream()` — repair tool pairing before
11439
+ // every step so we never feed the model an orphan tool-call.
11440
+ prepareStep: async ({ messages: stepMessages }) => {
11441
+ const paired = repairToolPairing(stepMessages);
11442
+ const ordered = ensureToolResultsFollowCalls(paired);
11443
+ if (ordered === stepMessages) return {};
11444
+ return { messages: ordered };
11445
+ },
11446
+ onStepFinish: async (step) => {
11447
+ options.onStepFinish?.(step);
11448
+ fireWebhook("task.step_finished", { iteration, text: step.text });
11449
+ if (emit) {
11450
+ if (textStarted) {
11451
+ await emit(JSON.stringify({ type: "text-end", id: textId }));
11452
+ textStarted = false;
11453
+ textId = `text_${Date.now()}`;
11454
+ }
11455
+ await emit(JSON.stringify({ type: "finish-step" }));
10463
11456
  }
10464
- await emit(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
10465
- }
10466
- } else if (part.type === "reasoning-start") {
10467
- if (emit) {
10468
- await emit(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
10469
- reasoningStarted = true;
10470
- }
10471
- } else if (part.type === "reasoning-delta") {
10472
- if (emit) {
10473
- await emit(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
10474
- }
10475
- } else if (part.type === "reasoning-end") {
10476
- if (emit && reasoningStarted) {
10477
- await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
10478
- reasoningStarted = false;
10479
- reasoningId = `reasoning_${Date.now()}`;
10480
- }
10481
- } else if (part.type === "tool-call-streaming-start") {
10482
- if (emit) {
10483
- const p = part;
10484
- await emit(JSON.stringify({ type: "tool-input-start", toolCallId: p.toolCallId, toolName: p.toolName }));
10485
- toolCallStarts.add(p.toolCallId);
10486
- }
10487
- } else if (part.type === "tool-call-delta") {
10488
- if (emit) {
10489
- const p = part;
10490
- await emit(JSON.stringify({ type: "tool-input-delta", toolCallId: p.toolCallId, argsTextDelta: p.argsTextDelta }));
10491
11457
  }
10492
- } else if (part.type === "tool-call") {
10493
- if (emit) {
10494
- if (!toolCallStarts.has(part.toolCallId)) {
10495
- await emit(JSON.stringify({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName }));
10496
- toolCallStarts.add(part.toolCallId);
11458
+ });
11459
+ for await (const part of iterStream.fullStream) {
11460
+ if (part.type === "text-delta") {
11461
+ if (emit) {
11462
+ if (!textStarted) {
11463
+ await emit(JSON.stringify({ type: "text-start", id: textId }));
11464
+ textStarted = true;
11465
+ }
11466
+ await emit(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
11467
+ }
11468
+ } else if (part.type === "reasoning-start") {
11469
+ if (emit) {
11470
+ await emit(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
11471
+ reasoningStarted = true;
11472
+ }
11473
+ } else if (part.type === "reasoning-delta") {
11474
+ if (emit) {
11475
+ await emit(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
11476
+ }
11477
+ } else if (part.type === "reasoning-end") {
11478
+ if (emit && reasoningStarted) {
11479
+ await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
11480
+ reasoningStarted = false;
11481
+ reasoningId = `reasoning_${Date.now()}`;
11482
+ }
11483
+ } else if (part.type === "tool-call-streaming-start") {
11484
+ if (emit) {
11485
+ const p = part;
11486
+ await emit(JSON.stringify({ type: "tool-input-start", toolCallId: p.toolCallId, toolName: p.toolName }));
11487
+ toolCallStarts.add(p.toolCallId);
11488
+ }
11489
+ } else if (part.type === "tool-call-delta") {
11490
+ if (emit) {
11491
+ const p = part;
11492
+ await emit(JSON.stringify({ type: "tool-input-delta", toolCallId: p.toolCallId, argsTextDelta: p.argsTextDelta }));
11493
+ }
11494
+ } else if (part.type === "tool-call") {
11495
+ if (emit) {
11496
+ if (!toolCallStarts.has(part.toolCallId)) {
11497
+ await emit(JSON.stringify({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName }));
11498
+ toolCallStarts.add(part.toolCallId);
11499
+ }
11500
+ const safeInput = part.toolName === "write_file" && part.input && typeof part.input === "object" ? truncateWriteFileInput(part.input) : part.input;
11501
+ await emit(JSON.stringify({ type: "tool-input-available", toolCallId: part.toolCallId, toolName: part.toolName, input: safeInput }));
11502
+ }
11503
+ } else if (part.type === "tool-result") {
11504
+ if (emit) {
11505
+ await emit(JSON.stringify({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output }));
11506
+ }
11507
+ } else if (part.type === "error") {
11508
+ console.error("Task stream error:", part.error);
11509
+ if (emit) {
11510
+ await emit(JSON.stringify({ type: "error", errorText: String(part.error) }));
10497
11511
  }
10498
- const safeInput = part.toolName === "write_file" && part.input && typeof part.input === "object" ? truncateWriteFileInput(part.input) : part.input;
10499
- await emit(JSON.stringify({ type: "tool-input-available", toolCallId: part.toolCallId, toolName: part.toolName, input: safeInput }));
10500
11512
  }
10501
- } else if (part.type === "tool-result") {
10502
- if (emit) {
10503
- await emit(JSON.stringify({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output }));
11513
+ }
11514
+ if (emit && textStarted) {
11515
+ await emit(JSON.stringify({ type: "text-end", id: textId }));
11516
+ }
11517
+ if (emit && reasoningStarted) {
11518
+ await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
11519
+ }
11520
+ const interrupted = interruptController.signal.aborted;
11521
+ clearInterruptController(this.session.id);
11522
+ const iterResponse = await iterStream.response;
11523
+ const responseMessages = iterResponse.messages;
11524
+ await this.context.addResponseMessages(responseMessages);
11525
+ const resultText = await iterStream.text;
11526
+ const resultSteps = await iterStream.steps;
11527
+ if (resultText) {
11528
+ options.onText?.(resultText);
11529
+ fireWebhook("task.message", { iteration, text: resultText });
11530
+ }
11531
+ for (const step of resultSteps) {
11532
+ if (step.toolCalls) {
11533
+ for (const tc of step.toolCalls) {
11534
+ options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
11535
+ fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
11536
+ }
10504
11537
  }
10505
- } else if (part.type === "error") {
10506
- console.error("Task stream error:", part.error);
10507
- if (emit) {
10508
- await emit(JSON.stringify({ type: "error", errorText: String(part.error) }));
11538
+ if (step.toolResults) {
11539
+ for (const tr of step.toolResults) {
11540
+ options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
11541
+ fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
11542
+ }
10509
11543
  }
10510
11544
  }
10511
- }
10512
- if (emit && textStarted) {
10513
- await emit(JSON.stringify({ type: "text-end", id: textId }));
10514
- }
10515
- if (emit && reasoningStarted) {
10516
- await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
10517
- }
10518
- const interrupted = interruptController.signal.aborted;
10519
- clearInterruptController(this.session.id);
10520
- const iterResponse = await iterStream.response;
10521
- const responseMessages = iterResponse.messages;
10522
- await this.context.addResponseMessages(responseMessages);
10523
- const resultText = await iterStream.text;
10524
- const resultSteps = await iterStream.steps;
10525
- if (resultText) {
10526
- options.onText?.(resultText);
10527
- fireWebhook("task.message", { iteration, text: resultText });
10528
- }
10529
- for (const step of resultSteps) {
10530
- if (step.toolCalls) {
10531
- for (const tc of step.toolCalls) {
10532
- options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
10533
- fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
11545
+ if (completion.signal) {
11546
+ const sig = completion.signal;
11547
+ const finalStatus = sig.status;
11548
+ let fileUrls;
11549
+ if (finalStatus === "completed" && sig.result && typeof sig.result === "object") {
11550
+ const resultObj = sig.result;
11551
+ const filePaths = Array.isArray(resultObj.files) ? resultObj.files : [];
11552
+ if (filePaths.length > 0) {
11553
+ fileUrls = await this.uploadTaskFiles(filePaths);
11554
+ }
10534
11555
  }
10535
- }
10536
- if (step.toolResults) {
10537
- for (const tr of step.toolResults) {
10538
- options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
10539
- fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
11556
+ const recordingUrls = await this.finishTaskRecording(taskRecorder);
11557
+ const allFileUrls = [...fileUrls || [], ...recordingUrls];
11558
+ const eventType = finalStatus === "completed" ? "task.completed" : "task.failed";
11559
+ fireWebhook(eventType, {
11560
+ status: finalStatus,
11561
+ result: sig.result,
11562
+ error: sig.error,
11563
+ iterations: iteration,
11564
+ fileUrls: allFileUrls.length > 0 ? allFileUrls : void 0,
11565
+ browserRecordingUrls: recordingUrls.length > 0 ? recordingUrls : void 0
11566
+ });
11567
+ const updatedTask2 = {
11568
+ ...options.taskConfig,
11569
+ status: finalStatus,
11570
+ result: sig.result,
11571
+ error: sig.error,
11572
+ iterations: iteration
11573
+ };
11574
+ await sessionQueries.update(this.session.id, {
11575
+ config: { ...this.session.config, task: updatedTask2 }
11576
+ });
11577
+ const orchId = this.session.config?.orchestratorSessionId;
11578
+ if (orchId) {
11579
+ const summary = finalStatus === "completed" ? typeof sig.result?.summary === "string" ? sig.result.summary : JSON.stringify(sig.result) : sig.error || "unknown error";
11580
+ pushToInbox(orchId, finalStatus === "completed" ? workerCompletedEvent(this.session.id, this.session.name || "worker", summary) : workerFailedEvent(this.session.id, this.session.name || "worker", summary));
10540
11581
  }
11582
+ return {
11583
+ status: finalStatus,
11584
+ result: sig.result,
11585
+ error: sig.error,
11586
+ iterations: iteration
11587
+ };
10541
11588
  }
10542
- }
10543
- if (completion.signal) {
10544
- const sig = completion.signal;
10545
- const finalStatus = sig.status;
10546
- let fileUrls;
10547
- if (finalStatus === "completed" && sig.result && typeof sig.result === "object") {
10548
- const resultObj = sig.result;
10549
- const filePaths = Array.isArray(resultObj.files) ? resultObj.files : [];
10550
- if (filePaths.length > 0) {
10551
- fileUrls = await this.uploadTaskFiles(filePaths);
11589
+ if (!interrupted) {
11590
+ 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.";
11591
+ if (emit) {
11592
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: continuationPrompt } }));
10552
11593
  }
11594
+ await this.context.addUserMessage(continuationPrompt);
10553
11595
  }
10554
- const recordingUrls = await this.finishTaskRecording(taskRecorder);
10555
- const allFileUrls = [...fileUrls || [], ...recordingUrls];
10556
- const eventType = finalStatus === "completed" ? "task.completed" : "task.failed";
10557
- fireWebhook(eventType, {
10558
- status: finalStatus,
10559
- result: sig.result,
10560
- error: sig.error,
10561
- iterations: iteration,
10562
- fileUrls: allFileUrls.length > 0 ? allFileUrls : void 0,
10563
- browserRecordingUrls: recordingUrls.length > 0 ? recordingUrls : void 0
10564
- });
10565
- const updatedTask2 = {
10566
- ...options.taskConfig,
10567
- status: finalStatus,
10568
- result: sig.result,
10569
- error: sig.error,
10570
- iterations: iteration
10571
- };
10572
- await sessionQueries.update(this.session.id, {
10573
- config: { ...this.session.config, task: updatedTask2 }
10574
- });
10575
- const orchId = this.session.config?.orchestratorSessionId;
10576
- if (orchId) {
10577
- const summary = finalStatus === "completed" ? typeof sig.result?.summary === "string" ? sig.result.summary : JSON.stringify(sig.result) : sig.error || "unknown error";
10578
- pushToInbox(orchId, finalStatus === "completed" ? workerCompletedEvent(this.session.id, this.session.name || "worker", summary) : workerFailedEvent(this.session.id, this.session.name || "worker", summary));
10579
- }
10580
- return {
10581
- status: finalStatus,
10582
- result: sig.result,
10583
- error: sig.error,
10584
- iterations: iteration
10585
- };
10586
11596
  }
10587
- if (!interrupted) {
10588
- 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.";
10589
- if (emit) {
10590
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: continuationPrompt } }));
11597
+ clearInterruptController(this.session.id);
11598
+ const timeoutError = `Task did not complete within ${maxIterations} iterations`;
11599
+ const timeoutRecordingUrls = await this.finishTaskRecording(taskRecorder);
11600
+ fireWebhook("task.failed", {
11601
+ status: "failed",
11602
+ error: timeoutError,
11603
+ iterations: iteration,
11604
+ browserRecordingUrls: timeoutRecordingUrls.length > 0 ? timeoutRecordingUrls : void 0
11605
+ });
11606
+ const updatedTask = {
11607
+ ...options.taskConfig,
11608
+ status: "failed",
11609
+ error: timeoutError,
11610
+ iterations: iteration
11611
+ };
11612
+ await sessionQueries.update(this.session.id, {
11613
+ config: { ...this.session.config, task: updatedTask }
11614
+ });
11615
+ const orchIdTimeout = this.session.config?.orchestratorSessionId;
11616
+ if (orchIdTimeout) {
11617
+ pushToInbox(orchIdTimeout, workerFailedEvent(this.session.id, this.session.name || "worker", timeoutError));
11618
+ }
11619
+ return { status: "failed", error: timeoutError, iterations: iteration };
11620
+ } finally {
11621
+ for (const cleanup2 of taskScopedCleanups) {
11622
+ try {
11623
+ await cleanup2();
11624
+ } catch {
10591
11625
  }
10592
- await this.context.addUserMessage(continuationPrompt);
10593
11626
  }
10594
11627
  }
10595
- clearInterruptController(this.session.id);
10596
- const timeoutError = `Task did not complete within ${maxIterations} iterations`;
10597
- const timeoutRecordingUrls = await this.finishTaskRecording(taskRecorder);
10598
- fireWebhook("task.failed", {
10599
- status: "failed",
10600
- error: timeoutError,
10601
- iterations: iteration,
10602
- browserRecordingUrls: timeoutRecordingUrls.length > 0 ? timeoutRecordingUrls : void 0
10603
- });
10604
- const updatedTask = {
10605
- ...options.taskConfig,
10606
- status: "failed",
10607
- error: timeoutError,
10608
- iterations: iteration
10609
- };
10610
- await sessionQueries.update(this.session.id, {
10611
- config: { ...this.session.config, task: updatedTask }
10612
- });
10613
- const orchIdTimeout = this.session.config?.orchestratorSessionId;
10614
- if (orchIdTimeout) {
10615
- pushToInbox(orchIdTimeout, workerFailedEvent(this.session.id, this.session.name || "worker", timeoutError));
10616
- }
10617
- return { status: "failed", error: timeoutError, iterations: iteration };
10618
11628
  }
10619
11629
  /**
10620
11630
  * Stop a task-mode browser recording, encode to MP4, upload to GCS.
@@ -10674,11 +11684,11 @@ ${p.text}` : p.text;
10674
11684
  const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
10675
11685
  if (!isRemoteConfigured2()) return [];
10676
11686
  const { readFile: readFile13 } = await import("fs/promises");
10677
- const { join: join18, basename: basename7 } = await import("path");
11687
+ const { join: join20, basename: basename7 } = await import("path");
10678
11688
  const urls = [];
10679
11689
  for (const filePath of filePaths) {
10680
11690
  try {
10681
- const fullPath = filePath.startsWith("/") ? filePath : join18(this.session.workingDirectory, filePath);
11691
+ const fullPath = filePath.startsWith("/") ? filePath : join20(this.session.workingDirectory, filePath);
10682
11692
  const fileName = basename7(fullPath);
10683
11693
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
10684
11694
  const mimeMap = {
@@ -10740,7 +11750,7 @@ ${p.text}` : p.text;
10740
11750
  description: originalTool.description || "",
10741
11751
  inputSchema: originalTool.inputSchema || z15.object({}),
10742
11752
  execute: async (input, toolOptions) => {
10743
- const toolCallId = toolOptions.toolCallId || nanoid8();
11753
+ const toolCallId = toolOptions.toolCallId || nanoid9();
10744
11754
  const execution = toolExecutionQueries.create({
10745
11755
  sessionId: this.session.id,
10746
11756
  toolName: name,
@@ -10781,219 +11791,71 @@ ${p.text}` : p.text;
10781
11791
  await toolExecutionQueries.complete(exec7.id, null, error.message);
10782
11792
  throw error;
10783
11793
  }
10784
- }
10785
- });
10786
- }
10787
- return wrappedTools;
10788
- }
10789
- /**
10790
- * Wait for all pending approvals
10791
- */
10792
- async waitForApprovals() {
10793
- return Array.from(this.pendingApprovals.values());
10794
- }
10795
- /**
10796
- * Approve a pending tool execution
10797
- */
10798
- async approve(toolCallId) {
10799
- const resolver = approvalResolvers.get(toolCallId);
10800
- if (resolver) {
10801
- resolver.resolve(true);
10802
- return { approved: true };
10803
- }
10804
- const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
10805
- const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
10806
- if (!execution) {
10807
- throw new Error(`No pending approval for tool call: ${toolCallId}`);
10808
- }
10809
- await toolExecutionQueries.approve(execution.id);
10810
- return { approved: true };
10811
- }
10812
- /**
10813
- * Reject a pending tool execution
10814
- */
10815
- async reject(toolCallId, reason) {
10816
- const resolver = approvalResolvers.get(toolCallId);
10817
- if (resolver) {
10818
- resolver.reason = reason;
10819
- resolver.resolve(false);
10820
- return { rejected: true };
10821
- }
10822
- const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
10823
- const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
10824
- if (!execution) {
10825
- throw new Error(`No pending approval for tool call: ${toolCallId}`);
10826
- }
10827
- await toolExecutionQueries.reject(execution.id);
10828
- return { rejected: true };
10829
- }
10830
- /**
10831
- * Get pending approvals
10832
- */
10833
- async getPendingApprovals() {
10834
- return toolExecutionQueries.getPendingApprovals(this.session.id);
10835
- }
10836
- /**
10837
- * Get context statistics
10838
- */
10839
- getContextStats() {
10840
- return this.context.getStats();
10841
- }
10842
- /**
10843
- * Clear conversation context (start fresh)
10844
- */
10845
- clearContext() {
10846
- this.context.clear();
10847
- }
10848
- };
10849
- }
10850
- });
10851
-
10852
- // src/agent/session-lock.ts
10853
- async function withSessionLock(sessionId, fn) {
10854
- let state2 = locks.get(sessionId);
10855
- if (!state2) {
10856
- state2 = { tail: Promise.resolve(), pending: 0 };
10857
- locks.set(sessionId, state2);
10858
- }
10859
- state2.pending++;
10860
- const prev = state2.tail;
10861
- let release;
10862
- const next = new Promise((resolve13) => {
10863
- release = resolve13;
10864
- });
10865
- state2.tail = prev.then(() => next);
10866
- await prev;
10867
- try {
10868
- return await fn();
10869
- } finally {
10870
- release();
10871
- state2.pending--;
10872
- if (state2.pending === 0 && locks.get(sessionId) === state2) {
10873
- locks.delete(sessionId);
10874
- }
10875
- }
10876
- }
10877
- var locks;
10878
- var init_session_lock = __esm({
10879
- "src/agent/session-lock.ts"() {
10880
- "use strict";
10881
- locks = /* @__PURE__ */ new Map();
10882
- }
10883
- });
10884
-
10885
- // src/orchestrator/webhook-events.ts
10886
- import { existsSync as existsSync18, readFileSync as readFileSync9, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
10887
- import { dirname as dirname7, join as join12 } from "path";
10888
- import { nanoid as nanoid9 } from "nanoid";
10889
- function logFilePath() {
10890
- return join12(getAppDataDirectory(), "webhook-events.jsonl");
10891
- }
10892
- function ensureLoaded() {
10893
- if (cache !== null) return cache;
10894
- cache = [];
10895
- try {
10896
- const p = logFilePath();
10897
- if (!existsSync18(p)) return cache;
10898
- const lines = readFileSync9(p, "utf-8").split("\n").filter(Boolean);
10899
- for (const line of lines) {
10900
- try {
10901
- cache.push(JSON.parse(line));
10902
- } catch {
10903
- }
10904
- }
10905
- if (cache.length > MAX_EVENTS) {
10906
- cache = cache.slice(-MAX_EVENTS);
10907
- try {
10908
- writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
10909
- } catch {
10910
- }
10911
- }
10912
- } catch {
10913
- }
10914
- return cache;
10915
- }
10916
- function appendEvent(ev) {
10917
- const list = ensureLoaded();
10918
- list.push(ev);
10919
- if (list.length > MAX_EVENTS) list.shift();
10920
- try {
10921
- const p = logFilePath();
10922
- mkdirSync7(dirname7(p), { recursive: true });
10923
- appendFileSync3(p, JSON.stringify(ev) + "\n");
10924
- } catch {
10925
- }
10926
- }
10927
- function recordEvent(ev) {
10928
- const full = {
10929
- id: ev.id ?? nanoid9(),
10930
- ts: ev.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
10931
- source: ev.source,
10932
- status: ev.status,
10933
- subtype: ev.subtype,
10934
- channel: ev.channel,
10935
- user: ev.user,
10936
- textSnippet: ev.textSnippet?.slice(0, 200),
10937
- dropReason: ev.dropReason,
10938
- error: ev.error,
10939
- sessionId: ev.sessionId,
10940
- durationMs: ev.durationMs,
10941
- meta: ev.meta
10942
- };
10943
- appendEvent(full);
10944
- return full.id;
10945
- }
10946
- function updateEvent(id, patch) {
10947
- const list = ensureLoaded();
10948
- const i = list.findIndex((e) => e.id === id);
10949
- if (i < 0) return;
10950
- list[i] = { ...list[i], ...patch };
10951
- try {
10952
- const p = logFilePath();
10953
- mkdirSync7(dirname7(p), { recursive: true });
10954
- writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
10955
- } catch {
10956
- }
10957
- }
10958
- function listEvents(filter = {}) {
10959
- const list = ensureLoaded();
10960
- const q = filter.q?.toLowerCase();
10961
- const sinceTs = filter.since ? Date.parse(filter.since) : -Infinity;
10962
- const beforeTs = filter.before ? Date.parse(filter.before) : Infinity;
10963
- const matched = list.filter((e) => {
10964
- if (filter.source && e.source !== filter.source) return false;
10965
- if (filter.status && e.status !== filter.status) return false;
10966
- const t = Date.parse(e.ts);
10967
- if (t < sinceTs) return false;
10968
- if (t >= beforeTs) return false;
10969
- if (q) {
10970
- const hay = `${e.channel ?? ""} ${e.user ?? ""} ${e.textSnippet ?? ""} ${e.dropReason ?? ""} ${e.error ?? ""} ${e.subtype ?? ""}`.toLowerCase();
10971
- if (!hay.includes(q)) return false;
10972
- }
10973
- return true;
10974
- });
10975
- matched.reverse();
10976
- const offset = Math.max(0, filter.offset ?? 0);
10977
- const limit = Math.min(500, Math.max(1, filter.limit ?? 50));
10978
- return {
10979
- events: matched.slice(offset, offset + limit),
10980
- total: matched.length
10981
- };
10982
- }
10983
- function clearAllEvents() {
10984
- cache = [];
10985
- try {
10986
- writeFileSync4(logFilePath(), "");
10987
- } catch {
10988
- }
10989
- }
10990
- var MAX_EVENTS, cache;
10991
- var init_webhook_events = __esm({
10992
- "src/orchestrator/webhook-events.ts"() {
10993
- "use strict";
10994
- init_config();
10995
- MAX_EVENTS = 1e3;
10996
- cache = null;
11794
+ }
11795
+ });
11796
+ }
11797
+ return wrappedTools;
11798
+ }
11799
+ /**
11800
+ * Wait for all pending approvals
11801
+ */
11802
+ async waitForApprovals() {
11803
+ return Array.from(this.pendingApprovals.values());
11804
+ }
11805
+ /**
11806
+ * Approve a pending tool execution
11807
+ */
11808
+ async approve(toolCallId) {
11809
+ const resolver = approvalResolvers.get(toolCallId);
11810
+ if (resolver) {
11811
+ resolver.resolve(true);
11812
+ return { approved: true };
11813
+ }
11814
+ const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
11815
+ const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
11816
+ if (!execution) {
11817
+ throw new Error(`No pending approval for tool call: ${toolCallId}`);
11818
+ }
11819
+ await toolExecutionQueries.approve(execution.id);
11820
+ return { approved: true };
11821
+ }
11822
+ /**
11823
+ * Reject a pending tool execution
11824
+ */
11825
+ async reject(toolCallId, reason) {
11826
+ const resolver = approvalResolvers.get(toolCallId);
11827
+ if (resolver) {
11828
+ resolver.reason = reason;
11829
+ resolver.resolve(false);
11830
+ return { rejected: true };
11831
+ }
11832
+ const pendingFromDb = await toolExecutionQueries.getPendingApprovals(this.session.id);
11833
+ const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
11834
+ if (!execution) {
11835
+ throw new Error(`No pending approval for tool call: ${toolCallId}`);
11836
+ }
11837
+ await toolExecutionQueries.reject(execution.id);
11838
+ return { rejected: true };
11839
+ }
11840
+ /**
11841
+ * Get pending approvals
11842
+ */
11843
+ async getPendingApprovals() {
11844
+ return toolExecutionQueries.getPendingApprovals(this.session.id);
11845
+ }
11846
+ /**
11847
+ * Get context statistics
11848
+ */
11849
+ getContextStats() {
11850
+ return this.context.getStats();
11851
+ }
11852
+ /**
11853
+ * Clear conversation context (start fresh)
11854
+ */
11855
+ clearContext() {
11856
+ this.context.clear();
11857
+ }
11858
+ };
10997
11859
  }
10998
11860
  });
10999
11861
 
@@ -11069,7 +11931,24 @@ async function runDaemonTurn(sessionId, events) {
11069
11931
  durationMs: finishedAt.getTime() - startedAt.getTime(),
11070
11932
  meta: { triggeredBy: events.map((e) => e.content?.slice(0, 80)) }
11071
11933
  });
11934
+ try {
11935
+ resolveBatchOnTurnEnd(events, !error);
11936
+ } catch (err) {
11937
+ console.error("[daemon] ack bookkeeping threw:", err?.message || err);
11938
+ }
11072
11939
  broadcast({ sessionId, text: trimmed, triggeredBy: events, startedAt, finishedAt, error });
11940
+ const seen = /* @__PURE__ */ new Set();
11941
+ for (const ev of events) {
11942
+ if (ev.ref?.channel !== "slack") continue;
11943
+ const ref = ev.ref;
11944
+ const channel = ref.slackChannel;
11945
+ const ts = ref.messageTs;
11946
+ if (!channel || !ts) continue;
11947
+ const key2 = `${channel}\u241F${ts}`;
11948
+ if (seen.has(key2)) continue;
11949
+ seen.add(key2);
11950
+ void removeLoadingReaction(channel, ts);
11951
+ }
11073
11952
  }
11074
11953
  var listeners;
11075
11954
  var init_daemon = __esm({
@@ -11080,6 +11959,8 @@ var init_daemon = __esm({
11080
11959
  init_db();
11081
11960
  init_inbox();
11082
11961
  init_webhook_events();
11962
+ init_inbox_acks();
11963
+ init_client3();
11083
11964
  listeners = /* @__PURE__ */ new Map();
11084
11965
  }
11085
11966
  });
@@ -11178,6 +12059,233 @@ var init_ensure_orchestrator = __esm({
11178
12059
  }
11179
12060
  });
11180
12061
 
12062
+ // src/orchestrator/self-update.ts
12063
+ var self_update_exports = {};
12064
+ __export(self_update_exports, {
12065
+ __test: () => __test,
12066
+ startSelfUpdater: () => startSelfUpdater,
12067
+ stopSelfUpdater: () => stopSelfUpdater
12068
+ });
12069
+ import { spawn as spawn2, execFile } from "child_process";
12070
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync10 } from "fs";
12071
+ import { dirname as dirname10, join as join18 } from "path";
12072
+ import { fileURLToPath as fileURLToPath4 } from "url";
12073
+ function currentVersion2() {
12074
+ const here = dirname10(fileURLToPath4(import.meta.url));
12075
+ const candidates = [
12076
+ join18(here, "..", "..", "package.json"),
12077
+ join18(here, "..", "package.json"),
12078
+ join18(process.cwd(), "package.json")
12079
+ ];
12080
+ for (const p of candidates) {
12081
+ try {
12082
+ const pkg = JSON.parse(readFileSync11(p, "utf8"));
12083
+ if (pkg.name === "sparkecoder" && pkg.version) return pkg.version;
12084
+ } catch {
12085
+ }
12086
+ }
12087
+ return "0.0.0";
12088
+ }
12089
+ function isLikelyGlobalInstall() {
12090
+ const here = dirname10(fileURLToPath4(import.meta.url));
12091
+ return here.includes("/node_modules/sparkecoder/") || here.includes("\\node_modules\\sparkecoder\\");
12092
+ }
12093
+ function isEnabled() {
12094
+ if (process.env.SPARKECODER_AUTO_UPDATE === "false" || process.env.SPARKECODER_AUTO_UPDATE === "0") return false;
12095
+ try {
12096
+ const cfg = getConfig();
12097
+ if (cfg?.autoUpdate?.enabled === false) return false;
12098
+ } catch {
12099
+ }
12100
+ return true;
12101
+ }
12102
+ function remoteUrl() {
12103
+ try {
12104
+ const cfg = getConfig();
12105
+ const url = cfg?.remoteServer?.url;
12106
+ return typeof url === "string" && url.length > 0 ? url.replace(/\/+$/, "") : null;
12107
+ } catch {
12108
+ return null;
12109
+ }
12110
+ }
12111
+ function intervalMs() {
12112
+ try {
12113
+ const h = getConfig()?.autoUpdate?.intervalHours;
12114
+ if (typeof h === "number" && h > 0) return h * 60 * 6e4;
12115
+ } catch {
12116
+ }
12117
+ return DEFAULT_INTERVAL_HOURS * 60 * 6e4;
12118
+ }
12119
+ function semverGt(a, b) {
12120
+ const parse = (v) => v.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
12121
+ const pa = parse(a);
12122
+ const pb = parse(b);
12123
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
12124
+ const x = pa[i] ?? 0;
12125
+ const y = pb[i] ?? 0;
12126
+ if (x > y) return true;
12127
+ if (x < y) return false;
12128
+ }
12129
+ return false;
12130
+ }
12131
+ function statePath() {
12132
+ try {
12133
+ return join18(getAppDataDirectory(), "self-update-state.json");
12134
+ } catch {
12135
+ return null;
12136
+ }
12137
+ }
12138
+ function readState() {
12139
+ const p = statePath();
12140
+ if (!p) return {};
12141
+ try {
12142
+ return JSON.parse(readFileSync11(p, "utf8"));
12143
+ } catch {
12144
+ return {};
12145
+ }
12146
+ }
12147
+ function writeState(s) {
12148
+ const p = statePath();
12149
+ if (!p) return;
12150
+ try {
12151
+ mkdirSync10(dirname10(p), { recursive: true });
12152
+ writeFileSync7(p, JSON.stringify(s));
12153
+ } catch {
12154
+ }
12155
+ }
12156
+ function attemptedRecently(target, now) {
12157
+ const s = readState();
12158
+ return s.lastTarget === target && typeof s.lastAttemptAt === "number" && now - s.lastAttemptAt < RETRY_COOLDOWN_MS;
12159
+ }
12160
+ function latestPublishedVersion() {
12161
+ return new Promise((resolve13) => {
12162
+ execFile("npm", ["view", "sparkecoder", "version"], { timeout: 3e4 }, (err, stdout) => {
12163
+ if (err) {
12164
+ resolve13(null);
12165
+ return;
12166
+ }
12167
+ const v = String(stdout).trim();
12168
+ resolve13(/^\d+\.\d+\.\d+/.test(v) ? v : null);
12169
+ });
12170
+ });
12171
+ }
12172
+ function runInstaller(url) {
12173
+ const secret = process.env.SPARKECODER_SETUP_SECRET || process.env.SPARKECODER_TUNNEL_SECRET || "";
12174
+ const query = secret ? `?secret=${encodeURIComponent(secret)}` : "";
12175
+ const oneLiner = `bash -c "$(curl -fsSL '${url}/install.sh${query}')" >/tmp/sparkecoder-selfupdate.log 2>&1`;
12176
+ const child = spawn2("bash", ["-lc", oneLiner], {
12177
+ detached: true,
12178
+ stdio: "ignore"
12179
+ });
12180
+ child.unref();
12181
+ }
12182
+ async function checkAndUpdate() {
12183
+ if (upgrading || !isEnabled()) return;
12184
+ const url = remoteUrl();
12185
+ if (!url) return;
12186
+ const latest = await latestPublishedVersion();
12187
+ if (!latest) return;
12188
+ const current = currentVersion2();
12189
+ if (!semverGt(latest, current)) return;
12190
+ const now = Date.now();
12191
+ if (attemptedRecently(latest, now)) {
12192
+ console.log(`[self-update] v${latest} already attempted recently; skipping until cooldown elapses`);
12193
+ return;
12194
+ }
12195
+ upgrading = true;
12196
+ const announced = await announceUpdate(latest);
12197
+ const delay = announced ? ANNOUNCE_GRACE_MS : 0;
12198
+ if (announced) {
12199
+ console.log(`[self-update] announced v${latest} in Slack; updating in ${Math.round(delay / 6e4)}m`);
12200
+ }
12201
+ const t = setTimeout(() => doInstall(latest, url, current), delay);
12202
+ if (typeof t.unref === "function") t.unref();
12203
+ }
12204
+ function doInstall(latest, url, current) {
12205
+ const prev = readState();
12206
+ writeState({
12207
+ lastTarget: latest,
12208
+ lastAttemptAt: Date.now(),
12209
+ attempts: prev.lastTarget === latest ? (prev.attempts ?? 0) + 1 : 1
12210
+ });
12211
+ console.log(`[self-update] newer version available: v${current} \u2192 v${latest}; re-running installer`);
12212
+ try {
12213
+ runInstaller(url);
12214
+ } catch (err) {
12215
+ upgrading = false;
12216
+ console.warn("[self-update] failed to launch installer:", err?.message || err);
12217
+ }
12218
+ }
12219
+ async function findOrchestratorId() {
12220
+ try {
12221
+ const { sessionQueries: sessionQueries2 } = await Promise.resolve().then(() => (init_db(), db_exports));
12222
+ const all = await sessionQueries2.list(500, 0);
12223
+ const orch = all.find((s) => s?.config?.role === "orchestrator");
12224
+ return orch?.id ?? null;
12225
+ } catch {
12226
+ return null;
12227
+ }
12228
+ }
12229
+ async function announceUpdate(target) {
12230
+ try {
12231
+ const { isSlackConfigured: isSlackConfigured2 } = await Promise.resolve().then(() => (init_client3(), client_exports));
12232
+ if (!isSlackConfigured2()) return false;
12233
+ const orchId = await findOrchestratorId();
12234
+ if (!orchId) return false;
12235
+ const { pushToInbox: pushToInbox2 } = await Promise.resolve().then(() => (init_inbox(), inbox_exports));
12236
+ pushToInbox2(orchId, {
12237
+ ref: { channel: "system", kind: "worker.completed", workerId: "self-update", workerName: "self-update" },
12238
+ 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.`,
12239
+ wake: "now",
12240
+ enqueuedAt: /* @__PURE__ */ new Date()
12241
+ });
12242
+ return true;
12243
+ } catch {
12244
+ return false;
12245
+ }
12246
+ }
12247
+ function startSelfUpdater() {
12248
+ if (started) return;
12249
+ started = true;
12250
+ if (!isEnabled()) {
12251
+ console.log("[self-update] disabled");
12252
+ return;
12253
+ }
12254
+ if (!isLikelyGlobalInstall()) {
12255
+ console.log("[self-update] skipped (not a global install)");
12256
+ return;
12257
+ }
12258
+ const kickoff = setTimeout(() => {
12259
+ void checkAndUpdate();
12260
+ timer = setInterval(() => {
12261
+ void checkAndUpdate();
12262
+ }, intervalMs());
12263
+ if (typeof timer.unref === "function") timer.unref();
12264
+ }, INITIAL_DELAY_MS);
12265
+ if (typeof kickoff.unref === "function") kickoff.unref();
12266
+ }
12267
+ function stopSelfUpdater() {
12268
+ if (timer) {
12269
+ clearInterval(timer);
12270
+ timer = null;
12271
+ }
12272
+ }
12273
+ var INITIAL_DELAY_MS, DEFAULT_INTERVAL_HOURS, ANNOUNCE_GRACE_MS, RETRY_COOLDOWN_MS, timer, started, upgrading, __test;
12274
+ var init_self_update = __esm({
12275
+ "src/orchestrator/self-update.ts"() {
12276
+ "use strict";
12277
+ init_config();
12278
+ INITIAL_DELAY_MS = 5 * 6e4;
12279
+ DEFAULT_INTERVAL_HOURS = 6;
12280
+ ANNOUNCE_GRACE_MS = 5 * 6e4;
12281
+ RETRY_COOLDOWN_MS = 24 * 60 * 6e4;
12282
+ timer = null;
12283
+ started = false;
12284
+ upgrading = false;
12285
+ __test = { currentVersion: currentVersion2, semverGt, isLikelyGlobalInstall };
12286
+ }
12287
+ });
12288
+
11181
12289
  // src/tasks/scheduler.ts
11182
12290
  var scheduler_exports = {};
11183
12291
  __export(scheduler_exports, {
@@ -11261,11 +12369,11 @@ import { Hono as Hono10 } from "hono";
11261
12369
  import { serve } from "@hono/node-server";
11262
12370
  import { cors } from "hono/cors";
11263
12371
  import { logger } from "hono/logger";
11264
- import { existsSync as existsSync22, mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "fs";
11265
- import { resolve as resolve12, dirname as dirname10, join as join17 } from "path";
11266
- import { spawn as spawn2 } from "child_process";
12372
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, writeFileSync as writeFileSync8 } from "fs";
12373
+ import { resolve as resolve12, dirname as dirname11, join as join19 } from "path";
12374
+ import { spawn as spawn3 } from "child_process";
11267
12375
  import { createServer as createNetServer } from "net";
11268
- import { fileURLToPath as fileURLToPath4 } from "url";
12376
+ import { fileURLToPath as fileURLToPath5 } from "url";
11269
12377
 
11270
12378
  // src/server/routes/sessions.ts
11271
12379
  init_db();
@@ -11278,7 +12386,7 @@ import { zValidator } from "@hono/zod-validator";
11278
12386
  import { z as z16 } from "zod";
11279
12387
  import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
11280
12388
  import { readdir as readdir6 } from "fs/promises";
11281
- import { join as join13, basename as basename5, extname as extname8, relative as relative9 } from "path";
12389
+ import { join as join14, basename as basename5, extname as extname8, relative as relative9 } from "path";
11282
12390
  import { nanoid as nanoid10 } from "nanoid";
11283
12391
 
11284
12392
  // src/tasks/agent-status.ts
@@ -11919,7 +13027,7 @@ sessions2.get("/:id/diff/:filePath", async (c) => {
11919
13027
  });
11920
13028
  function getAttachmentsDir(sessionId) {
11921
13029
  const appDataDir = getAppDataDirectory();
11922
- return join13(appDataDir, "attachments", sessionId);
13030
+ return join14(appDataDir, "attachments", sessionId);
11923
13031
  }
11924
13032
  function ensureAttachmentsDir(sessionId) {
11925
13033
  const dir = getAttachmentsDir(sessionId);
@@ -11940,7 +13048,7 @@ sessions2.get("/:id/attachments", async (c) => {
11940
13048
  }
11941
13049
  const files = readdirSync3(dir);
11942
13050
  const attachments = files.map((filename) => {
11943
- const filePath = join13(dir, filename);
13051
+ const filePath = join14(dir, filename);
11944
13052
  const stats = statSync2(filePath);
11945
13053
  return {
11946
13054
  id: filename.split("_")[0],
@@ -11975,7 +13083,7 @@ sessions2.post("/:id/attachments", async (c) => {
11975
13083
  const id = nanoid10(10);
11976
13084
  const ext = extname8(file.name) || "";
11977
13085
  const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
11978
- const filePath = join13(dir, safeFilename);
13086
+ const filePath = join14(dir, safeFilename);
11979
13087
  const arrayBuffer = await file.arrayBuffer();
11980
13088
  writeFileSync5(filePath, Buffer.from(arrayBuffer));
11981
13089
  return c.json({
@@ -12001,7 +13109,7 @@ sessions2.post("/:id/attachments", async (c) => {
12001
13109
  const id = nanoid10(10);
12002
13110
  const ext = extname8(body.filename) || "";
12003
13111
  const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
12004
- const filePath = join13(dir, safeFilename);
13112
+ const filePath = join14(dir, safeFilename);
12005
13113
  let base64Data = body.data;
12006
13114
  if (base64Data.includes(",")) {
12007
13115
  base64Data = base64Data.split(",")[1];
@@ -12038,7 +13146,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
12038
13146
  if (!file) {
12039
13147
  return c.json({ error: "Attachment not found" }, 404);
12040
13148
  }
12041
- const filePath = join13(dir, file);
13149
+ const filePath = join14(dir, file);
12042
13150
  unlinkSync2(filePath);
12043
13151
  return c.json({ success: true, id: attachmentId });
12044
13152
  });
@@ -12121,7 +13229,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
12121
13229
  const entries = await readdir6(currentDir, { withFileTypes: true });
12122
13230
  for (const entry2 of entries) {
12123
13231
  if (results.length >= limit * 2) break;
12124
- const fullPath = join13(currentDir, entry2.name);
13232
+ const fullPath = join14(currentDir, entry2.name);
12125
13233
  const relativePath = relative9(baseDir, fullPath);
12126
13234
  if (entry2.isDirectory() && IGNORED_DIRECTORIES.has(entry2.name)) {
12127
13235
  continue;
@@ -12281,7 +13389,7 @@ import { Hono as Hono2 } from "hono";
12281
13389
  import { zValidator as zValidator2 } from "@hono/zod-validator";
12282
13390
  import { z as z17 } from "zod";
12283
13391
  import { existsSync as existsSync20, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
12284
- import { join as join14 } from "path";
13392
+ import { join as join15 } from "path";
12285
13393
 
12286
13394
  // src/agent/missing-tool-recovery.ts
12287
13395
  init_db();
@@ -12548,6 +13656,7 @@ init_stream_proxy();
12548
13656
  init_recorder();
12549
13657
  init_remote();
12550
13658
  init_resize_image();
13659
+ init_local_device_time();
12551
13660
  var sessionRecorders = /* @__PURE__ */ new Map();
12552
13661
  var MAX_TOOL_INPUT_LENGTH = 8 * 1024;
12553
13662
  var MAX_TOOL_INPUT_PREVIEW = 2 * 1024;
@@ -12663,7 +13772,7 @@ var rejectSchema = z17.object({
12663
13772
  var streamAbortControllers = /* @__PURE__ */ new Map();
12664
13773
  function getAttachmentsDirectory(sessionId) {
12665
13774
  const appDataDir = getAppDataDirectory();
12666
- return join14(appDataDir, "attachments", sessionId);
13775
+ return join15(appDataDir, "attachments", sessionId);
12667
13776
  }
12668
13777
  async function saveAttachmentToDisk(sessionId, attachment, index) {
12669
13778
  const attachmentsDir = getAttachmentsDirectory(sessionId);
@@ -12686,7 +13795,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
12686
13795
  attachment.mediaType = resized.mediaType;
12687
13796
  attachment.data = buffer.toString("base64");
12688
13797
  }
12689
- const filePath = join14(attachmentsDir, filename);
13798
+ const filePath = join15(attachmentsDir, filename);
12690
13799
  writeFileSync6(filePath, buffer);
12691
13800
  return filePath;
12692
13801
  }
@@ -13063,9 +14172,12 @@ agents.post(
13063
14172
  if (!session) {
13064
14173
  return c.json({ error: "Session not found" }, 404);
13065
14174
  }
13066
- if (session.config?.role === "orchestrator" && !/^\[\w+/.test(prompt)) {
14175
+ if (session.config?.role === "orchestrator" && !/^\[(WEB|SLACK|SYSTEM|SCHEDULE|WEBHOOK)\b/.test(prompt)) {
13067
14176
  prompt = `[WEB] ${prompt}`;
13068
14177
  }
14178
+ if (session.config?.role === "orchestrator") {
14179
+ prompt = prependLocalDeviceTimeToUserMessage(prompt);
14180
+ }
13069
14181
  const nextSequence = await messageQueries.getNextSequence(id);
13070
14182
  await createCheckpoint(id, session.workingDirectory, nextSequence);
13071
14183
  let userMessageContent;
@@ -13675,17 +14787,17 @@ import { zValidator as zValidator3 } from "@hono/zod-validator";
13675
14787
  import { z as z18 } from "zod";
13676
14788
  import { readFileSync as readFileSync10 } from "fs";
13677
14789
  import { fileURLToPath as fileURLToPath3 } from "url";
13678
- import { dirname as dirname8, join as join15 } from "path";
14790
+ import { dirname as dirname8, join as join16 } from "path";
13679
14791
  var __filename = fileURLToPath3(import.meta.url);
13680
14792
  var __dirname = dirname8(__filename);
13681
14793
  var possiblePaths = [
13682
- join15(__dirname, "../package.json"),
14794
+ join16(__dirname, "../package.json"),
13683
14795
  // From dist/server -> dist/../package.json
13684
- join15(__dirname, "../../package.json"),
14796
+ join16(__dirname, "../../package.json"),
13685
14797
  // From dist/server (if nested differently)
13686
- join15(__dirname, "../../../package.json"),
14798
+ join16(__dirname, "../../../package.json"),
13687
14799
  // From src/server/routes (development)
13688
- join15(process.cwd(), "package.json")
14800
+ join16(process.cwd(), "package.json")
13689
14801
  // From current working directory
13690
14802
  ];
13691
14803
  var currentVersion = "0.0.0";
@@ -14146,6 +15258,25 @@ import { nanoid as nanoid12 } from "nanoid";
14146
15258
  init_questions();
14147
15259
  var tasks = new Hono5();
14148
15260
  var taskAbortControllers = /* @__PURE__ */ new Map();
15261
+ var taskMcpServerSchema = z20.object({
15262
+ name: z20.string().min(1).describe("Tool prefix + display name."),
15263
+ transport: z20.enum(["http", "sse", "stdio"]),
15264
+ url: z20.string().url().optional().describe("http/sse transports."),
15265
+ headers: z20.record(z20.string(), z20.string()).optional().describe("Auth / custom headers for http/sse."),
15266
+ command: z20.string().optional().describe("stdio transport."),
15267
+ args: z20.array(z20.string()).optional(),
15268
+ env: z20.record(z20.string(), z20.string()).optional().describe("Env vars for stdio child process.")
15269
+ }).refine(
15270
+ (s) => s.transport === "stdio" ? !!s.command : !!s.url,
15271
+ { message: 'http/sse require "url"; stdio requires "command".' }
15272
+ );
15273
+ var taskSkillSchema = z20.object({
15274
+ name: z20.string().min(1),
15275
+ description: z20.string().optional(),
15276
+ content: z20.string().min(1).describe("Full markdown body of the skill."),
15277
+ alwaysApply: z20.boolean().optional().describe("Inject into the system prompt up-front (vs load on demand)."),
15278
+ globs: z20.array(z20.string()).optional()
15279
+ });
14149
15280
  var createTaskSchema = z20.object({
14150
15281
  prompt: z20.string().min(1),
14151
15282
  outputSchema: z20.record(z20.string(), z20.unknown()),
@@ -14157,8 +15288,30 @@ var createTaskSchema = z20.object({
14157
15288
  parentTaskId: z20.string().optional(),
14158
15289
  /** When set, the spawning orchestrator's session id. Stamped on the
14159
15290
  * worker's config so terminal events can wake the orchestrator. */
14160
- orchestratorSessionId: z20.string().optional()
15291
+ orchestratorSessionId: z20.string().optional(),
15292
+ /** Task-scoped MCP servers — auto-connected for this task only. */
15293
+ mcpServers: z20.array(taskMcpServerSchema).optional(),
15294
+ /** Task-scoped skills — available to this task only. */
15295
+ skills: z20.array(taskSkillSchema).optional()
14161
15296
  });
15297
+ function redactMcpServers(servers2) {
15298
+ if (!servers2 || servers2.length === 0) return void 0;
15299
+ return servers2.map((s) => ({
15300
+ name: s.name,
15301
+ transport: s.transport,
15302
+ url: s.url,
15303
+ hasHeaders: !!(s.headers && Object.keys(s.headers).length > 0),
15304
+ command: s.command
15305
+ }));
15306
+ }
15307
+ function redactSkills(skills2) {
15308
+ if (!skills2 || skills2.length === 0) return void 0;
15309
+ return skills2.map((s) => ({
15310
+ name: s.name,
15311
+ description: s.description,
15312
+ alwaysApply: s.alwaysApply
15313
+ }));
15314
+ }
14162
15315
  tasks.post(
14163
15316
  "/",
14164
15317
  zValidator5("json", createTaskSchema),
@@ -14171,7 +15324,9 @@ tasks.post(
14171
15324
  webhookUrl: body.webhookUrl,
14172
15325
  maxIterations: body.maxIterations ?? 50,
14173
15326
  status: "running",
14174
- parentTaskId: body.parentTaskId
15327
+ parentTaskId: body.parentTaskId,
15328
+ mcpServers: redactMcpServers(body.mcpServers),
15329
+ skills: redactSkills(body.skills)
14175
15330
  };
14176
15331
  let agent;
14177
15332
  if (body.parentTaskId) {
@@ -14248,7 +15403,9 @@ tasks.post(
14248
15403
  prompt: body.prompt,
14249
15404
  taskConfig,
14250
15405
  abortSignal: abortController.signal,
14251
- writeSSE
15406
+ writeSSE,
15407
+ mcpServers: body.mcpServers,
15408
+ skills: body.skills
14252
15409
  });
14253
15410
  await writeSSE(JSON.stringify({ type: "finish" }));
14254
15411
  } catch (err) {
@@ -14342,6 +15499,8 @@ tasks.get("/:id", async (c) => {
14342
15499
  model: session.model,
14343
15500
  name: session.name,
14344
15501
  parentTaskId: task.parentTaskId,
15502
+ mcpServers: task.mcpServers,
15503
+ skills: task.skills,
14345
15504
  createdAt: session.createdAt.toISOString(),
14346
15505
  updatedAt: session.updatedAt.toISOString(),
14347
15506
  browserRecordings: browserRecordings.length > 0 ? browserRecordings : void 0
@@ -14464,6 +15623,204 @@ function verifySlackSignature(opts) {
14464
15623
  // src/server/routes/slack.ts
14465
15624
  init_client3();
14466
15625
  init_slack();
15626
+
15627
+ // src/integrations/slack/files.ts
15628
+ init_client3();
15629
+ var MAX_BYTES = 100 * 1024 * 1024;
15630
+ var INGEST_TIMEOUT_MS = 2500;
15631
+ function inferFileName(file) {
15632
+ return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
15633
+ }
15634
+ function inferContentType(file) {
15635
+ if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
15636
+ return "application/octet-stream";
15637
+ }
15638
+ function formatBytes(n) {
15639
+ if (!Number.isFinite(n) || n <= 0) return "?";
15640
+ if (n < 1024) return `${n} B`;
15641
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
15642
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
15643
+ }
15644
+ function withTimeout(p, ms, label) {
15645
+ return new Promise((resolve13, reject) => {
15646
+ const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
15647
+ p.then(
15648
+ (v) => {
15649
+ clearTimeout(t);
15650
+ resolve13(v);
15651
+ },
15652
+ (e) => {
15653
+ clearTimeout(t);
15654
+ reject(e);
15655
+ }
15656
+ );
15657
+ });
15658
+ }
15659
+ async function ingestOne(file, sessionId, botToken) {
15660
+ const fileName = inferFileName(file);
15661
+ const contentType = inferContentType(file);
15662
+ const declaredSize = typeof file.size === "number" ? file.size : 0;
15663
+ const base = {
15664
+ slackFileId: file.id,
15665
+ fileName,
15666
+ contentType,
15667
+ sizeBytes: declaredSize
15668
+ };
15669
+ const sourceUrl = file.url_private_download || file.url_private;
15670
+ if (!sourceUrl || typeof sourceUrl !== "string") {
15671
+ return { ...base, shortUrl: null, error: "no_source_url" };
15672
+ }
15673
+ if (declaredSize > MAX_BYTES) {
15674
+ return { ...base, shortUrl: null, error: "size_exceeded" };
15675
+ }
15676
+ let bytes;
15677
+ try {
15678
+ const res = await fetch(sourceUrl, {
15679
+ headers: { Authorization: `Bearer ${botToken}` }
15680
+ });
15681
+ if (!res.ok) {
15682
+ return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
15683
+ }
15684
+ const ab = await res.arrayBuffer();
15685
+ if (ab.byteLength > MAX_BYTES) {
15686
+ return { ...base, shortUrl: null, error: "size_exceeded" };
15687
+ }
15688
+ bytes = Buffer.from(ab);
15689
+ } catch (err) {
15690
+ return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
15691
+ }
15692
+ const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
15693
+ let upload;
15694
+ try {
15695
+ upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
15696
+ } catch (err) {
15697
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
15698
+ }
15699
+ try {
15700
+ const putRes = await fetch(upload.uploadUrl, {
15701
+ method: "PUT",
15702
+ headers: { "Content-Type": contentType },
15703
+ body: bytes
15704
+ });
15705
+ if (!putRes.ok) {
15706
+ return {
15707
+ ...base,
15708
+ sizeBytes: bytes.length,
15709
+ shortUrl: null,
15710
+ error: `gcs_put_${putRes.status}`
15711
+ };
15712
+ }
15713
+ } catch (err) {
15714
+ return {
15715
+ ...base,
15716
+ sizeBytes: bytes.length,
15717
+ shortUrl: null,
15718
+ error: `gcs_put_error:${err?.message || "unknown"}`
15719
+ };
15720
+ }
15721
+ try {
15722
+ await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
15723
+ } catch (err) {
15724
+ console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
15725
+ }
15726
+ const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
15727
+ // server somehow forgot to return it (older remote-server versions).
15728
+ inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
15729
+ return {
15730
+ ...base,
15731
+ sizeBytes: bytes.length,
15732
+ shortUrl
15733
+ };
15734
+ }
15735
+ function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
15736
+ try {
15737
+ const u = new URL(uploadUrl);
15738
+ if (u.hostname.endsWith(".googleapis.com")) return null;
15739
+ return `${u.origin}/f/${fileId}`;
15740
+ } catch {
15741
+ return null;
15742
+ }
15743
+ }
15744
+ async function ingestSlackFiles(files, sessionId, options = {}) {
15745
+ if (!Array.isArray(files) || files.length === 0) return [];
15746
+ const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
15747
+ if (!isRemoteConfigured2()) {
15748
+ console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
15749
+ return files.map((f) => ({
15750
+ slackFileId: f.id,
15751
+ fileName: inferFileName(f),
15752
+ contentType: inferContentType(f),
15753
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
15754
+ shortUrl: null,
15755
+ error: "storage_unconfigured"
15756
+ }));
15757
+ }
15758
+ const botToken = getSlackBotToken();
15759
+ if (!botToken) {
15760
+ console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
15761
+ return files.map((f) => ({
15762
+ slackFileId: f.id,
15763
+ fileName: inferFileName(f),
15764
+ contentType: inferContentType(f),
15765
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
15766
+ shortUrl: null,
15767
+ error: "no_bot_token"
15768
+ }));
15769
+ }
15770
+ const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
15771
+ const startedAt = Date.now();
15772
+ const pipeline = Promise.allSettled(
15773
+ files.map((f) => ingestOne(f, sessionId, botToken))
15774
+ );
15775
+ let settled;
15776
+ try {
15777
+ settled = await withTimeout(pipeline, timeoutMs, "ingest");
15778
+ } catch (err) {
15779
+ console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
15780
+ return files.map((f) => ({
15781
+ slackFileId: f.id,
15782
+ fileName: inferFileName(f),
15783
+ contentType: inferContentType(f),
15784
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
15785
+ shortUrl: null,
15786
+ error: "timeout"
15787
+ }));
15788
+ }
15789
+ const results = settled.map((s, i) => {
15790
+ if (s.status === "fulfilled") return s.value;
15791
+ const f = files[i];
15792
+ return {
15793
+ slackFileId: f.id,
15794
+ fileName: inferFileName(f),
15795
+ contentType: inferContentType(f),
15796
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
15797
+ shortUrl: null,
15798
+ error: `unexpected:${s.reason?.message || String(s.reason)}`
15799
+ };
15800
+ });
15801
+ const okCount = results.filter((r) => r.shortUrl).length;
15802
+ console.log(
15803
+ `[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
15804
+ );
15805
+ return results;
15806
+ }
15807
+ function formatFileBlock(files) {
15808
+ if (!files || files.length === 0) return "";
15809
+ const lines = ["[files]"];
15810
+ for (const f of files) {
15811
+ const sizeLabel = formatBytes(f.sizeBytes);
15812
+ if (f.shortUrl) {
15813
+ lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
15814
+ } else {
15815
+ lines.push(
15816
+ ` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
15817
+ );
15818
+ }
15819
+ }
15820
+ return lines.join("\n");
15821
+ }
15822
+
15823
+ // src/server/routes/slack.ts
14467
15824
  init_webhook_events();
14468
15825
  init_inbox();
14469
15826
  var recentlyHandled = /* @__PURE__ */ new Map();
@@ -14550,9 +15907,43 @@ slack.post("/events", async (c) => {
14550
15907
  inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
14551
15908
  }
14552
15909
  }
14553
- pushToInbox(orchestratorId, inbound);
15910
+ const slackFiles = Array.isArray(ev.files) ? ev.files : [];
14554
15911
  markHandled(ev.channel, ev.ts);
14555
- updateEvent(auditId, { status: "routed", sessionId: orchestratorId });
15912
+ if (ev.channel && ev.ts) {
15913
+ void addLoadingReaction(String(ev.channel), String(ev.ts));
15914
+ }
15915
+ let ingestedCount = 0;
15916
+ if (slackFiles.length > 0) {
15917
+ try {
15918
+ const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
15919
+ const block = formatFileBlock(ingested);
15920
+ if (block) inbound.content = `${inbound.content}
15921
+ ${block}`;
15922
+ ingestedCount = ingested.filter((f) => f.shortUrl).length;
15923
+ } catch (err) {
15924
+ console.warn("[slack-files] ingestion threw:", err?.message || err);
15925
+ inbound.content = `${inbound.content}
15926
+ [files] (ingestion failed: ${err?.message || "unknown"})`;
15927
+ }
15928
+ }
15929
+ pushToInbox(orchestratorId, inbound);
15930
+ updateEvent(auditId, {
15931
+ status: "routed",
15932
+ sessionId: orchestratorId,
15933
+ ...slackFiles.length > 0 ? {
15934
+ // Preserve the original meta (ts, thread_ts, team,
15935
+ // event_subtype) from recordEvent above — updateEvent does a
15936
+ // shallow merge, so we have to re-include them.
15937
+ meta: {
15938
+ ts: ev.ts,
15939
+ thread_ts: ev.thread_ts,
15940
+ team: ev.team,
15941
+ event_subtype: ev.subtype,
15942
+ fileCount: slackFiles.length,
15943
+ ingestedCount
15944
+ }
15945
+ } : {}
15946
+ });
14556
15947
  } else {
14557
15948
  updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
14558
15949
  }
@@ -14772,10 +16163,35 @@ integrations.get("/", async (c) => {
14772
16163
  cfAccess: {
14773
16164
  enabled: !!cfg?.auth?.cfAccess?.enabled,
14774
16165
  teamDomain: cfg?.auth?.cfAccess?.teamDomain || null,
16166
+ audTag: cfg?.auth?.cfAccess?.audTag || null,
14775
16167
  allowedEmails: cfg?.auth?.allowedEmails || []
14776
16168
  }
14777
16169
  });
14778
16170
  });
16171
+ var cfAccessSchema = z21.object({
16172
+ enabled: z21.boolean().optional(),
16173
+ teamDomain: z21.string().optional(),
16174
+ audTag: z21.string().optional(),
16175
+ // Email allowlist for the public (cloudflared) surface. Empty array = allow
16176
+ // any email that passes the Cloudflare Access policy (no extra filtering).
16177
+ allowedEmails: z21.array(z21.string().trim().toLowerCase()).optional()
16178
+ });
16179
+ integrations.post("/cf-access", zValidator6("json", cfAccessSchema), async (c) => {
16180
+ const body = c.req.valid("json");
16181
+ if (body.enabled) {
16182
+ const cfg = getConfig();
16183
+ const teamDomain = body.teamDomain ?? cfg?.auth?.cfAccess?.teamDomain;
16184
+ const audTag = body.audTag ?? cfg?.auth?.cfAccess?.audTag;
16185
+ if (!teamDomain || !audTag) {
16186
+ return c.json(
16187
+ { error: "teamDomain and audTag are required to enable Cloudflare Access" },
16188
+ 400
16189
+ );
16190
+ }
16191
+ }
16192
+ setCfAccessConfig(body);
16193
+ return c.json({ ok: true });
16194
+ });
14779
16195
  var slackConfigSchema = z21.object({
14780
16196
  botToken: z21.string().optional(),
14781
16197
  signingSecret: z21.string().optional(),
@@ -14958,8 +16374,8 @@ import { Hono as Hono9 } from "hono";
14958
16374
  import { zValidator as zValidator7 } from "@hono/zod-validator";
14959
16375
  import { z as z22 } from "zod";
14960
16376
  import { existsSync as existsSync21, statSync as statSync3 } from "fs";
14961
- import { readFile as readFile12, writeFile as writeFile6, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
14962
- import { resolve as resolve11, join as join16, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
16377
+ import { readFile as readFile12, writeFile as writeFile7, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
16378
+ import { resolve as resolve11, join as join17, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
14963
16379
  var skills = new Hono9();
14964
16380
  function encodeId(filePath) {
14965
16381
  return Buffer.from(filePath, "utf-8").toString("base64url");
@@ -15107,13 +16523,13 @@ skills.post(
15107
16523
  const safeName = basename6(fileName).replace(/[^A-Za-z0-9._-]/g, "-");
15108
16524
  const ext = extname9(safeName).toLowerCase();
15109
16525
  const finalName = ext === ".md" || ext === ".mdc" ? safeName : `${safeName}.md`;
15110
- const filePath = join16(targetDir, finalName);
16526
+ const filePath = join17(targetDir, finalName);
15111
16527
  if (existsSync21(filePath)) {
15112
16528
  return c.json({ error: `file already exists: ${finalName}` }, 409);
15113
16529
  }
15114
16530
  try {
15115
16531
  await mkdir5(targetDir, { recursive: true });
15116
- await writeFile6(filePath, content, "utf-8");
16532
+ await writeFile7(filePath, content, "utf-8");
15117
16533
  } catch (err) {
15118
16534
  return c.json({ error: err?.message || "write failed" }, 500);
15119
16535
  }
@@ -15129,7 +16545,7 @@ skills.put(
15129
16545
  if (filePath.includes("/skills/default")) {
15130
16546
  return c.json({ error: "built-in skills are read-only" }, 400);
15131
16547
  }
15132
- await writeFile6(filePath, c.req.valid("json").content, "utf-8");
16548
+ await writeFile7(filePath, c.req.valid("json").content, "utf-8");
15133
16549
  return c.json({ ok: true });
15134
16550
  }
15135
16551
  );
@@ -15173,6 +16589,14 @@ skills.delete("/directories", (c) => {
15173
16589
  init_config();
15174
16590
  import { createRemoteJWKSet, jwtVerify } from "jose";
15175
16591
  var EXEMPT_PATH_PREFIXES = ["/health", "/api/slack/events", "/api/inbox/", "/w/"];
16592
+ function isExemptPath(path) {
16593
+ return EXEMPT_PATH_PREFIXES.some((p) => {
16594
+ if (p.endsWith("/")) {
16595
+ return path === p.slice(0, -1) || path === p || path.startsWith(p);
16596
+ }
16597
+ return path === p || path.startsWith(p + "/") || path.startsWith(p + "?");
16598
+ });
16599
+ }
15176
16600
  var cachedJWKS = null;
15177
16601
  var cachedJWKSUrl = null;
15178
16602
  function getOrCreateJWKS(teamDomain) {
@@ -15202,12 +16626,13 @@ function cfAccessMiddleware() {
15202
16626
  return next();
15203
16627
  }
15204
16628
  const path = c.req.path;
15205
- if (EXEMPT_PATH_PREFIXES.some((p) => path === p || path.startsWith(p + "/") || path.startsWith(p + "?"))) {
16629
+ if (isExemptPath(path)) {
15206
16630
  return next();
15207
16631
  }
15208
16632
  const host = c.req.header("host");
15209
16633
  const remote = c.req.raw?.socket?.remoteAddress;
15210
- if (isLoopback(host, remote)) {
16634
+ const hasCfJwt = !!c.req.header("cf-access-jwt-assertion");
16635
+ if (!hasCfJwt && isLoopback(host, remote)) {
15211
16636
  return next();
15212
16637
  }
15213
16638
  const teamDomain = cfg.teamDomain;
@@ -15226,8 +16651,10 @@ function cfAccessMiddleware() {
15226
16651
  audience: aud
15227
16652
  });
15228
16653
  const email = String(payload.email || "").toLowerCase();
15229
- const allowed = (auth?.allowedEmails || []).map((e) => e.toLowerCase());
15230
- if (allowed.length > 0 && !allowed.includes(email)) {
16654
+ const emailDomain = email.split("@")[1] || "";
16655
+ const allowed = (auth?.allowedEmails || []).map((e) => e.toLowerCase().trim().replace(/^@/, "")).filter(Boolean);
16656
+ const isAllowed = allowed.length === 0 || allowed.some((entry2) => entry2.includes("@") ? entry2 === email : entry2 === emailDomain);
16657
+ if (!isAllowed) {
15231
16658
  console.warn(`[cf-access] rejected ${email}: not in allowlist`);
15232
16659
  return c.json({ error: "Email not allowed" }, 403);
15233
16660
  }
@@ -15329,13 +16756,13 @@ var DEFAULT_WEB_PORT = 6969;
15329
16756
  var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
15330
16757
  function getWebDirectory() {
15331
16758
  try {
15332
- const currentDir = dirname10(fileURLToPath4(import.meta.url));
16759
+ const currentDir = dirname11(fileURLToPath5(import.meta.url));
15333
16760
  const webDir = resolve12(currentDir, "..", "web");
15334
- if (existsSync22(webDir) && existsSync22(join17(webDir, "package.json"))) {
16761
+ if (existsSync22(webDir) && existsSync22(join19(webDir, "package.json"))) {
15335
16762
  return webDir;
15336
16763
  }
15337
16764
  const altWebDir = resolve12(currentDir, "..", "..", "web");
15338
- if (existsSync22(altWebDir) && existsSync22(join17(altWebDir, "package.json"))) {
16765
+ if (existsSync22(altWebDir) && existsSync22(join19(altWebDir, "package.json"))) {
15339
16766
  return altWebDir;
15340
16767
  }
15341
16768
  return null;
@@ -15393,20 +16820,20 @@ async function findWebPort(preferredPort) {
15393
16820
  return { port: preferredPort, alreadyRunning: false };
15394
16821
  }
15395
16822
  function hasProductionBuild(webDir) {
15396
- const buildIdPath = join17(webDir, ".next", "BUILD_ID");
16823
+ const buildIdPath = join19(webDir, ".next", "BUILD_ID");
15397
16824
  return existsSync22(buildIdPath);
15398
16825
  }
15399
16826
  function hasSourceFiles(webDir) {
15400
- const appDir = join17(webDir, "src", "app");
15401
- const pagesDir = join17(webDir, "src", "pages");
15402
- const rootAppDir = join17(webDir, "app");
15403
- const rootPagesDir = join17(webDir, "pages");
16827
+ const appDir = join19(webDir, "src", "app");
16828
+ const pagesDir = join19(webDir, "src", "pages");
16829
+ const rootAppDir = join19(webDir, "app");
16830
+ const rootPagesDir = join19(webDir, "pages");
15404
16831
  return existsSync22(appDir) || existsSync22(pagesDir) || existsSync22(rootAppDir) || existsSync22(rootPagesDir);
15405
16832
  }
15406
16833
  function getStandaloneServerPath(webDir) {
15407
16834
  const possiblePaths2 = [
15408
- join17(webDir, ".next", "standalone", "server.js"),
15409
- join17(webDir, ".next", "standalone", "web", "server.js")
16835
+ join19(webDir, ".next", "standalone", "server.js"),
16836
+ join19(webDir, ".next", "standalone", "web", "server.js")
15410
16837
  ];
15411
16838
  for (const serverPath of possiblePaths2) {
15412
16839
  if (existsSync22(serverPath)) {
@@ -15417,7 +16844,7 @@ function getStandaloneServerPath(webDir) {
15417
16844
  }
15418
16845
  function runCommand(command, args, cwd, env) {
15419
16846
  return new Promise((resolve13) => {
15420
- const child = spawn2(command, args, {
16847
+ const child = spawn3(command, args, {
15421
16848
  cwd,
15422
16849
  stdio: ["ignore", "pipe", "pipe"],
15423
16850
  env,
@@ -15449,15 +16876,15 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15449
16876
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
15450
16877
  return { process: null, port: actualPort };
15451
16878
  }
15452
- const usePnpm = existsSync22(join17(webDir, "pnpm-lock.yaml"));
15453
- const useNpm = !usePnpm && existsSync22(join17(webDir, "package-lock.json"));
16879
+ const usePnpm = existsSync22(join19(webDir, "pnpm-lock.yaml"));
16880
+ const useNpm = !usePnpm && existsSync22(join19(webDir, "package-lock.json"));
15454
16881
  const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
15455
16882
  const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv } = process.env;
15456
16883
  const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
15457
- const runtimeConfig = { apiBaseUrl: apiUrl };
15458
- const runtimeConfigPath = join17(webDir, "runtime-config.json");
16884
+ const runtimeConfig = { apiBaseUrl: apiUrl, localApiBaseUrl: `http://127.0.0.1:${apiPort}` };
16885
+ const runtimeConfigPath = join19(webDir, "runtime-config.json");
15459
16886
  try {
15460
- writeFileSync7(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
16887
+ writeFileSync8(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
15461
16888
  if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
15462
16889
  } catch (err) {
15463
16890
  if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
@@ -15477,7 +16904,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15477
16904
  if (standaloneServerPath) {
15478
16905
  command = "node";
15479
16906
  args = ["server.js"];
15480
- cwd = dirname10(standaloneServerPath);
16907
+ cwd = dirname11(standaloneServerPath);
15481
16908
  webEnv.PORT = String(actualPort);
15482
16909
  webEnv.HOSTNAME = "0.0.0.0";
15483
16910
  if (!quiet) console.log(" \u{1F4E6} Starting Web UI from standalone build...");
@@ -15507,7 +16934,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15507
16934
  }
15508
16935
  return { process: null, port: actualPort };
15509
16936
  }
15510
- const child = spawn2(command, args, {
16937
+ const child = spawn3(command, args, {
15511
16938
  cwd,
15512
16939
  stdio: ["ignore", "pipe", "pipe"],
15513
16940
  env: webEnv,
@@ -15515,12 +16942,12 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15515
16942
  shell: true
15516
16943
  });
15517
16944
  const startupTimeout = 3e4;
15518
- let started = false;
16945
+ let started2 = false;
15519
16946
  let exited = false;
15520
16947
  let exitCode = null;
15521
16948
  const startedPromise = new Promise((resolve13) => {
15522
16949
  const timeout = setTimeout(() => {
15523
- if (!started && !exited) {
16950
+ if (!started2 && !exited) {
15524
16951
  resolve13(false);
15525
16952
  }
15526
16953
  }, startupTimeout);
@@ -15532,8 +16959,8 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15532
16959
  console.log(` Web UI: ${line}`);
15533
16960
  }
15534
16961
  }
15535
- if (!started && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
15536
- started = true;
16962
+ if (!started2 && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
16963
+ started2 = true;
15537
16964
  clearTimeout(timeout);
15538
16965
  resolve13(true);
15539
16966
  }
@@ -15552,7 +16979,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15552
16979
  child.on("exit", (code) => {
15553
16980
  exited = true;
15554
16981
  exitCode = code;
15555
- if (!started) {
16982
+ if (!started2) {
15556
16983
  clearTimeout(timeout);
15557
16984
  resolve13(false);
15558
16985
  }
@@ -15672,7 +17099,7 @@ async function startServer(options = {}) {
15672
17099
  config.resolvedWorkingDirectory = options.workingDirectory;
15673
17100
  }
15674
17101
  if (!existsSync22(config.resolvedWorkingDirectory)) {
15675
- mkdirSync10(config.resolvedWorkingDirectory, { recursive: true });
17102
+ mkdirSync11(config.resolvedWorkingDirectory, { recursive: true });
15676
17103
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
15677
17104
  }
15678
17105
  if (!config.resolvedRemoteServer.url) {
@@ -15703,9 +17130,17 @@ async function startServer(options = {}) {
15703
17130
  try {
15704
17131
  const { startOrchestratorDaemon: startOrchestratorDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
15705
17132
  startOrchestratorDaemon2();
17133
+ const { startReconciler: startReconciler2 } = await Promise.resolve().then(() => (init_inbox_acks(), inbox_acks_exports));
17134
+ startReconciler2();
15706
17135
  } catch (err) {
15707
17136
  if (!options.quiet) console.warn(`[daemon] start skipped: ${err.message}`);
15708
17137
  }
17138
+ try {
17139
+ const { startSelfUpdater: startSelfUpdater2 } = await Promise.resolve().then(() => (init_self_update(), self_update_exports));
17140
+ startSelfUpdater2();
17141
+ } catch (err) {
17142
+ if (!options.quiet) console.warn(`[self-update] start skipped: ${err.message}`);
17143
+ }
15709
17144
  try {
15710
17145
  const { startScheduler: startScheduler2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
15711
17146
  startScheduler2({ quiet: options.quiet });