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
@@ -145,8 +145,8 @@ var init_types = __esm({
145
145
  authKey: z.string().optional()
146
146
  }).optional();
147
147
  SparkcoderConfigSchema = z.object({
148
- // Default model to use (Vercel AI Gateway format)
149
- defaultModel: z.string().default("anthropic/claude-opus-4.7"),
148
+ // Default model to use (LiteLLM model id)
149
+ defaultModel: z.string().default("gpt-5.5"),
150
150
  // Working directory for file operations
151
151
  workingDirectory: z.string().optional(),
152
152
  // Tool approval settings
@@ -185,6 +185,14 @@ var init_types = __esm({
185
185
  webhooks: z.object({
186
186
  token: z.string().optional()
187
187
  }).optional(),
188
+ // Self-update: when running as the managed service, periodically check
189
+ // npm for a newer published version and, if found, re-run the hosted
190
+ // installer (full upgrade + restart). Disabled automatically when not
191
+ // running from a global install (e.g. dev/source checkouts).
192
+ autoUpdate: z.object({
193
+ enabled: z.boolean().optional().default(true),
194
+ intervalHours: z.number().positive().optional().default(6)
195
+ }).optional().default({}),
188
196
  // Database path (used for local SQLite - ignored if remoteServer is configured)
189
197
  databasePath: z.string().optional().default("./sparkecoder.db"),
190
198
  // Remote server configuration (for centralized storage)
@@ -334,7 +342,7 @@ function requiresApproval(toolName, sessionConfig) {
334
342
  }
335
343
  function createDefaultConfig() {
336
344
  return {
337
- defaultModel: "anthropic/claude-opus-4.7",
345
+ defaultModel: "gpt-5.5",
338
346
  // workingDirectory is intentionally not set - defaults to where CLI is run
339
347
  toolApprovals: {
340
348
  bash: true,
@@ -356,6 +364,10 @@ function createDefaultConfig() {
356
364
  port: 3141,
357
365
  host: "0.0.0.0"
358
366
  },
367
+ autoUpdate: {
368
+ enabled: true,
369
+ intervalHours: 6
370
+ },
359
371
  databasePath: "./sparkecoder.db"
360
372
  };
361
373
  }
@@ -371,6 +383,7 @@ var init_config = __esm({
371
383
  openai: "OPENAI_API_KEY",
372
384
  google: "GOOGLE_GENERATIVE_AI_API_KEY",
373
385
  xai: "XAI_API_KEY",
386
+ litellm: "LITELLM_API_KEY",
374
387
  "ai-gateway": "AI_GATEWAY_API_KEY"
375
388
  };
376
389
  SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
@@ -1707,94 +1720,734 @@ var init_conversation_archive = __esm({
1707
1720
  }
1708
1721
  });
1709
1722
 
1710
- // src/browser/stream-proxy.ts
1711
- var stream_proxy_exports = {};
1712
- __export(stream_proxy_exports, {
1713
- BrowserStreamProxy: () => BrowserStreamProxy,
1714
- destroyProxy: () => destroyProxy,
1715
- getOrCreateProxy: () => getOrCreateProxy,
1716
- getProxy: () => getProxy
1717
- });
1718
- import WebSocket from "ws";
1719
- import { EventEmitter } from "events";
1720
- function getOrCreateProxy(sessionId, port) {
1721
- const existing = activeProxies.get(sessionId);
1722
- if (existing) {
1723
- console.log(`[BROWSER-WS] Reusing existing proxy for session ${sessionId} (connected=${existing.connected})`);
1724
- return existing;
1723
+ // src/integrations/slack/persistence.ts
1724
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync3, renameSync } from "fs";
1725
+ import { join as join9, dirname as dirname6 } from "path";
1726
+ function cachePath() {
1727
+ return join9(ensureAppDataDirectory(), FILENAME);
1728
+ }
1729
+ function load() {
1730
+ if (loaded) return;
1731
+ loaded = true;
1732
+ const path = cachePath();
1733
+ if (!existsSync16(path)) return;
1734
+ try {
1735
+ const raw = readFileSync7(path, "utf-8");
1736
+ const parsed = JSON.parse(raw);
1737
+ if (parsed?.version !== FILE_VERSION) return;
1738
+ const now = Date.now();
1739
+ for (const [id, e] of Object.entries(parsed.users || {})) {
1740
+ if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
1741
+ userMap.set(id, {
1742
+ name: e.name ?? null,
1743
+ realName: e.realName ?? null,
1744
+ email: e.email ?? null,
1745
+ expiresAt: e.expiresAt
1746
+ });
1747
+ }
1748
+ }
1749
+ for (const [key2, e] of Object.entries(parsed.threads || {})) {
1750
+ if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
1751
+ threadMap.set(key2, { owned: !!e.owned, expiresAt: e.expiresAt });
1752
+ }
1753
+ }
1754
+ } catch (err) {
1755
+ console.warn(`[slack] could not load ${FILENAME}:`, err?.message || err);
1725
1756
  }
1726
- console.log(`[BROWSER-WS] Creating new proxy for session ${sessionId} on port ${port} (active proxies: ${activeProxies.size})`);
1727
- const proxy = new BrowserStreamProxy(port);
1728
- activeProxies.set(sessionId, proxy);
1729
- proxy.on("close", () => {
1730
- console.log(`[BROWSER-WS] Proxy closed for session ${sessionId}, removing from registry`);
1731
- activeProxies.delete(sessionId);
1732
- });
1733
- proxy.connect();
1734
- return proxy;
1735
1757
  }
1736
- function getProxy(sessionId) {
1737
- return activeProxies.get(sessionId);
1758
+ function evictIfOversized(map, max) {
1759
+ if (map.size <= max) return;
1760
+ const sorted = [...map.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt);
1761
+ const toRemove = map.size - max;
1762
+ for (let i = 0; i < toRemove; i++) map.delete(sorted[i][0]);
1738
1763
  }
1739
- function destroyProxy(sessionId) {
1740
- const proxy = activeProxies.get(sessionId);
1741
- if (proxy) {
1742
- console.log(`[BROWSER-WS] destroyProxy() called for session ${sessionId}`);
1743
- proxy.destroy();
1744
- activeProxies.delete(sessionId);
1745
- } else {
1746
- console.log(`[BROWSER-WS] destroyProxy() called but no proxy exists for session ${sessionId}`);
1764
+ function scheduleSave() {
1765
+ dirty = true;
1766
+ if (saveTimer) return;
1767
+ saveTimer = setTimeout(() => {
1768
+ saveTimer = null;
1769
+ if (dirty) saveSync();
1770
+ }, SAVE_DEBOUNCE_MS);
1771
+ saveTimer.unref?.();
1772
+ }
1773
+ function saveSync() {
1774
+ dirty = false;
1775
+ try {
1776
+ const path = cachePath();
1777
+ const dir = dirname6(path);
1778
+ if (!existsSync16(dir)) mkdirSync6(dir, { recursive: true });
1779
+ const payload = {
1780
+ version: FILE_VERSION,
1781
+ users: Object.fromEntries(userMap),
1782
+ threads: Object.fromEntries(threadMap)
1783
+ };
1784
+ const tmp = `${path}.tmp`;
1785
+ writeFileSync3(tmp, JSON.stringify(payload), "utf-8");
1786
+ renameSync(tmp, path);
1787
+ } catch (err) {
1788
+ console.warn(`[slack] could not persist ${FILENAME}:`, err?.message || err);
1747
1789
  }
1748
1790
  }
1749
- var RECONNECT_DELAY_MS, MAX_RECONNECT_ATTEMPTS, FRAME_THROTTLE_MS, BrowserStreamProxy, activeProxies;
1750
- var init_stream_proxy = __esm({
1751
- "src/browser/stream-proxy.ts"() {
1791
+ function hookExit() {
1792
+ if (exitHooked) return;
1793
+ exitHooked = true;
1794
+ const flush2 = () => {
1795
+ if (dirty) saveSync();
1796
+ };
1797
+ process.once("beforeExit", flush2);
1798
+ process.once("SIGINT", flush2);
1799
+ process.once("SIGTERM", flush2);
1800
+ }
1801
+ function setCachedThreadOwnership(key2, entry2) {
1802
+ load();
1803
+ hookExit();
1804
+ threadMap.set(key2, entry2);
1805
+ evictIfOversized(threadMap, MAX_THREADS);
1806
+ scheduleSave();
1807
+ }
1808
+ var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
1809
+ var init_persistence = __esm({
1810
+ "src/integrations/slack/persistence.ts"() {
1752
1811
  "use strict";
1753
- RECONNECT_DELAY_MS = 1e3;
1754
- MAX_RECONNECT_ATTEMPTS = 20;
1755
- FRAME_THROTTLE_MS = 100;
1756
- BrowserStreamProxy = class extends EventEmitter {
1757
- ws = null;
1758
- port;
1759
- reconnectAttempts = 0;
1760
- reconnectTimer = null;
1761
- destroyed = false;
1762
- lastFrameTime = 0;
1763
- _latestFrame = null;
1764
- _connected = false;
1765
- constructor(port) {
1766
- super();
1767
- this.port = port;
1812
+ init_config();
1813
+ FILENAME = "slack-cache.json";
1814
+ FILE_VERSION = 1;
1815
+ SAVE_DEBOUNCE_MS = 500;
1816
+ MAX_THREADS = 5e3;
1817
+ loaded = false;
1818
+ userMap = /* @__PURE__ */ new Map();
1819
+ threadMap = /* @__PURE__ */ new Map();
1820
+ dirty = false;
1821
+ saveTimer = null;
1822
+ exitHooked = false;
1823
+ }
1824
+ });
1825
+
1826
+ // src/integrations/slack/client.ts
1827
+ function slackBackoffMs(attempt) {
1828
+ const expo = SLACK_BACKOFF_BASE_MS * 2 ** attempt;
1829
+ const jitter = Math.floor(Math.random() * SLACK_BACKOFF_BASE_MS);
1830
+ return Math.min(expo + jitter, SLACK_BACKOFF_CAP_MS);
1831
+ }
1832
+ async function slackFetchWithRetry(url, init, attempts = SLACK_FETCH_ATTEMPTS) {
1833
+ let lastErr;
1834
+ for (let i = 0; i < attempts; i++) {
1835
+ const isLast = i === attempts - 1;
1836
+ try {
1837
+ const res = await fetch(url, init);
1838
+ if ((res.status === 429 || res.status >= 500) && !isLast) {
1839
+ const ra = Number(res.headers.get("retry-after"));
1840
+ const waitMs = Number.isFinite(ra) && ra > 0 ? Math.min(ra * 1e3, SLACK_BACKOFF_CAP_MS) : slackBackoffMs(i);
1841
+ await new Promise((r) => setTimeout(r, waitMs));
1842
+ continue;
1768
1843
  }
1769
- get connected() {
1770
- return this._connected;
1844
+ return res;
1845
+ } catch (err) {
1846
+ lastErr = err;
1847
+ if (isLast) throw err;
1848
+ await new Promise((r) => setTimeout(r, slackBackoffMs(i)));
1849
+ }
1850
+ }
1851
+ throw lastErr ?? new Error("slack fetch failed");
1852
+ }
1853
+ async function addResultReaction(channel, timestamp, state2) {
1854
+ const name = RESULT_REACTIONS[state2];
1855
+ if (!name) return { ok: false, error: `no_reaction_for_state:${state2}` };
1856
+ const adapter = getSlackAdapter();
1857
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
1858
+ try {
1859
+ const res = await adapter.addReaction({ channel, timestamp, name });
1860
+ if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
1861
+ console.warn(`[slack] addReaction ${name} failed on ${channel}/${timestamp}: ${res.error}`);
1862
+ }
1863
+ return res;
1864
+ } catch (err) {
1865
+ console.warn(`[slack] addResultReaction threw on ${channel}/${timestamp}:`, err?.message || err);
1866
+ return { ok: false, error: err?.message || "unknown" };
1867
+ }
1868
+ }
1869
+ async function postThreadMessage(channel, threadTs, text) {
1870
+ const adapter = getSlackAdapter();
1871
+ if (!adapter) return { ok: false, error: "slack_not_configured" };
1872
+ try {
1873
+ return await adapter.postMessage({ channel, text, threadTs });
1874
+ } catch (err) {
1875
+ console.warn(`[slack] postThreadMessage threw on ${channel}/${threadTs}:`, err?.message || err);
1876
+ return { ok: false, error: err?.message || "unknown" };
1877
+ }
1878
+ }
1879
+ function readSlackConfig() {
1880
+ try {
1881
+ const cfg = getConfig();
1882
+ const slack = cfg?.slack;
1883
+ if (!slack?.botToken) return void 0;
1884
+ return {
1885
+ botToken: String(slack.botToken),
1886
+ signingSecret: slack.signingSecret ? String(slack.signingSecret) : void 0,
1887
+ defaultOrchestratorName: slack.defaultOrchestratorName ? String(slack.defaultOrchestratorName) : void 0
1888
+ };
1889
+ } catch {
1890
+ return void 0;
1891
+ }
1892
+ }
1893
+ function getSlackAdapter() {
1894
+ const cfg = readSlackConfig();
1895
+ if (!cfg) return void 0;
1896
+ const slackForm = async (endpoint, params) => {
1897
+ const body = new URLSearchParams(params).toString();
1898
+ const res = await fetch(`https://slack.com/api/${endpoint}`, {
1899
+ method: "POST",
1900
+ headers: {
1901
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
1902
+ Authorization: `Bearer ${cfg.botToken}`
1903
+ },
1904
+ body
1905
+ });
1906
+ const data = await res.json().catch(() => ({}));
1907
+ if (!res.ok || data?.ok === false) {
1908
+ return { ok: false, error: data?.error || `HTTP ${res.status}` };
1909
+ }
1910
+ return { ok: true };
1911
+ };
1912
+ return {
1913
+ async postMessage({ channel, text, threadTs }) {
1914
+ const res = await slackFetchWithRetry("https://slack.com/api/chat.postMessage", {
1915
+ method: "POST",
1916
+ headers: {
1917
+ "Content-Type": "application/json; charset=utf-8",
1918
+ "Authorization": `Bearer ${cfg.botToken}`
1919
+ },
1920
+ body: JSON.stringify({ channel, text, thread_ts: threadTs })
1921
+ });
1922
+ const data = await res.json().catch(() => ({}));
1923
+ if (!res.ok || data?.ok === false) {
1924
+ return { ok: false, error: data?.error || `HTTP ${res.status}` };
1771
1925
  }
1772
- get latestFrame() {
1773
- return this._latestFrame;
1926
+ return { ok: true, ts: data?.ts };
1927
+ },
1928
+ addReaction({ channel, timestamp, name }) {
1929
+ return slackForm("reactions.add", { channel, timestamp, name });
1930
+ },
1931
+ removeReaction({ channel, timestamp, name }) {
1932
+ return slackForm("reactions.remove", { channel, timestamp, name });
1933
+ }
1934
+ };
1935
+ }
1936
+ function isSlackConfigured() {
1937
+ return readSlackConfig() !== void 0;
1938
+ }
1939
+ function threadCacheKey(channel, threadTs) {
1940
+ return `${channel}\u241F${threadTs}`;
1941
+ }
1942
+ function noteBotPostedInThread(channel, threadTs) {
1943
+ if (!channel || !threadTs) return;
1944
+ setCachedThreadOwnership(threadCacheKey(channel, threadTs), {
1945
+ owned: true,
1946
+ expiresAt: Date.now() + THREAD_OWNED_TTL_MS
1947
+ });
1948
+ }
1949
+ var RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, USER_TTL_MS, USER_FAIL_TTL_MS, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS;
1950
+ var init_client2 = __esm({
1951
+ "src/integrations/slack/client.ts"() {
1952
+ "use strict";
1953
+ init_config();
1954
+ init_persistence();
1955
+ RESULT_REACTIONS = {
1956
+ responded: "white_check_mark",
1957
+ skipped: "zzz",
1958
+ handed_off: "eyes",
1959
+ failed: "warning"
1960
+ };
1961
+ SLACK_FETCH_ATTEMPTS = 3;
1962
+ SLACK_BACKOFF_BASE_MS = 400;
1963
+ SLACK_BACKOFF_CAP_MS = 3e4;
1964
+ REACTION_SOFT_ERRORS = /* @__PURE__ */ new Set([
1965
+ "already_reacted",
1966
+ // add: someone (or we) already added this emoji
1967
+ "no_reaction",
1968
+ // remove: the emoji isn't on the message
1969
+ "message_not_found"
1970
+ // remove/add: original message deleted
1971
+ ]);
1972
+ USER_TTL_MS = 60 * 60 * 1e3;
1973
+ USER_FAIL_TTL_MS = 5 * 60 * 1e3;
1974
+ THREAD_OWNED_TTL_MS = 60 * 60 * 1e3;
1975
+ THREAD_NEG_TTL_MS = 5 * 60 * 1e3;
1976
+ }
1977
+ });
1978
+
1979
+ // src/agent/session-lock.ts
1980
+ function isSessionLocked(sessionId) {
1981
+ const s = locks.get(sessionId);
1982
+ return !!s && s.pending > 0;
1983
+ }
1984
+ var locks;
1985
+ var init_session_lock = __esm({
1986
+ "src/agent/session-lock.ts"() {
1987
+ "use strict";
1988
+ locks = /* @__PURE__ */ new Map();
1989
+ }
1990
+ });
1991
+
1992
+ // src/orchestrator/webhook-events.ts
1993
+ import { existsSync as existsSync17, readFileSync as readFileSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
1994
+ import { dirname as dirname7, join as join10 } from "path";
1995
+ import { nanoid as nanoid4 } from "nanoid";
1996
+ function logFilePath() {
1997
+ return join10(getAppDataDirectory(), "webhook-events.jsonl");
1998
+ }
1999
+ function ensureLoaded() {
2000
+ if (cache !== null) return cache;
2001
+ cache = [];
2002
+ try {
2003
+ const p = logFilePath();
2004
+ if (!existsSync17(p)) return cache;
2005
+ const lines = readFileSync8(p, "utf-8").split("\n").filter(Boolean);
2006
+ for (const line of lines) {
2007
+ try {
2008
+ cache.push(JSON.parse(line));
2009
+ } catch {
1774
2010
  }
1775
- connect() {
1776
- if (this.destroyed) return;
1777
- console.log(`[BROWSER-WS] connect() called for port ${this.port}`);
1778
- this.doConnect();
2011
+ }
2012
+ if (cache.length > MAX_EVENTS) {
2013
+ cache = cache.slice(-MAX_EVENTS);
2014
+ try {
2015
+ writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
2016
+ } catch {
1779
2017
  }
1780
- doConnect() {
1781
- if (this.destroyed) return;
1782
- const url = `ws://localhost:${this.port}`;
1783
- console.log(`[BROWSER-WS] Attempting WebSocket connection to ${url} (attempt ${this.reconnectAttempts + 1}/${MAX_RECONNECT_ATTEMPTS})`);
1784
- try {
1785
- this.ws = new WebSocket(url);
1786
- } catch (err) {
1787
- console.warn(`[BROWSER-WS] WebSocket constructor threw for ${url}:`, err);
1788
- this.scheduleReconnect();
1789
- return;
1790
- }
1791
- this.ws.on("open", () => {
1792
- console.log(`[BROWSER-WS] Connected to ${url} (after ${this.reconnectAttempts} retries)`);
1793
- this.reconnectAttempts = 0;
1794
- this._connected = true;
1795
- });
1796
- this.ws.on("message", (raw) => {
1797
- try {
2018
+ }
2019
+ } catch {
2020
+ }
2021
+ return cache;
2022
+ }
2023
+ function appendEvent(ev) {
2024
+ const list = ensureLoaded();
2025
+ list.push(ev);
2026
+ if (list.length > MAX_EVENTS) list.shift();
2027
+ try {
2028
+ const p = logFilePath();
2029
+ mkdirSync7(dirname7(p), { recursive: true });
2030
+ appendFileSync3(p, JSON.stringify(ev) + "\n");
2031
+ } catch {
2032
+ }
2033
+ }
2034
+ function recordEvent(ev) {
2035
+ const full = {
2036
+ id: ev.id ?? nanoid4(),
2037
+ ts: ev.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
2038
+ source: ev.source,
2039
+ status: ev.status,
2040
+ subtype: ev.subtype,
2041
+ channel: ev.channel,
2042
+ user: ev.user,
2043
+ textSnippet: ev.textSnippet?.slice(0, 200),
2044
+ dropReason: ev.dropReason,
2045
+ error: ev.error,
2046
+ sessionId: ev.sessionId,
2047
+ durationMs: ev.durationMs,
2048
+ meta: ev.meta
2049
+ };
2050
+ appendEvent(full);
2051
+ return full.id;
2052
+ }
2053
+ var MAX_EVENTS, cache;
2054
+ var init_webhook_events = __esm({
2055
+ "src/orchestrator/webhook-events.ts"() {
2056
+ "use strict";
2057
+ init_config();
2058
+ MAX_EVENTS = 1e3;
2059
+ cache = null;
2060
+ }
2061
+ });
2062
+
2063
+ // src/orchestrator/inbox.ts
2064
+ var inbox_exports = {};
2065
+ __export(inbox_exports, {
2066
+ clearInbox: () => clearInbox,
2067
+ flush: () => flush,
2068
+ peekInbox: () => peekInbox,
2069
+ pushToInbox: () => pushToInbox,
2070
+ setFlushHandler: () => setFlushHandler
2071
+ });
2072
+ function setFlushHandler(fn) {
2073
+ flushHandler = fn;
2074
+ }
2075
+ function entryFor(sessionId) {
2076
+ let e = inboxes.get(sessionId);
2077
+ if (!e) {
2078
+ e = { pending: [] };
2079
+ inboxes.set(sessionId, e);
2080
+ }
2081
+ return e;
2082
+ }
2083
+ function pushToInbox(orchestratorSessionId, event) {
2084
+ const e = entryFor(orchestratorSessionId);
2085
+ e.pending.push(event);
2086
+ try {
2087
+ trackInbound(orchestratorSessionId, event);
2088
+ } catch {
2089
+ }
2090
+ if (event.wake === "now") {
2091
+ scheduleFlush(orchestratorSessionId);
2092
+ }
2093
+ }
2094
+ function scheduleFlush(sessionId) {
2095
+ const e = inboxes.get(sessionId);
2096
+ if (!e) return;
2097
+ if (e.timer) clearTimeout(e.timer);
2098
+ e.timer = setTimeout(() => {
2099
+ void flush(sessionId);
2100
+ }, FLUSH_DEBOUNCE_MS);
2101
+ }
2102
+ async function flush(sessionId) {
2103
+ const e = inboxes.get(sessionId);
2104
+ if (!e) return;
2105
+ if (e.timer) {
2106
+ clearTimeout(e.timer);
2107
+ e.timer = void 0;
2108
+ }
2109
+ const events = e.pending.splice(0);
2110
+ if (events.length === 0) return;
2111
+ if (!flushHandler) {
2112
+ console.warn("[orchestrator-inbox] flush called with no handler installed; dropping events");
2113
+ return;
2114
+ }
2115
+ try {
2116
+ await flushHandler(sessionId, events);
2117
+ } catch (err) {
2118
+ console.error("[orchestrator-inbox] flush handler threw:", err?.message || err);
2119
+ }
2120
+ }
2121
+ function peekInbox(sessionId) {
2122
+ return inboxes.get(sessionId)?.pending.slice() ?? [];
2123
+ }
2124
+ function clearInbox(sessionId) {
2125
+ const e = inboxes.get(sessionId);
2126
+ if (!e) return;
2127
+ if (e.timer) {
2128
+ clearTimeout(e.timer);
2129
+ e.timer = void 0;
2130
+ }
2131
+ e.pending.length = 0;
2132
+ }
2133
+ var inboxes, FLUSH_DEBOUNCE_MS, flushHandler;
2134
+ var init_inbox = __esm({
2135
+ "src/orchestrator/inbox.ts"() {
2136
+ "use strict";
2137
+ init_inbox_acks();
2138
+ inboxes = /* @__PURE__ */ new Map();
2139
+ FLUSH_DEBOUNCE_MS = 200;
2140
+ flushHandler = null;
2141
+ }
2142
+ });
2143
+
2144
+ // src/orchestrator/inbox-acks.ts
2145
+ var inbox_acks_exports = {};
2146
+ __export(inbox_acks_exports, {
2147
+ MAX_ATTEMPTS: () => MAX_ATTEMPTS,
2148
+ RECONCILE_EVERY_MS: () => RECONCILE_EVERY_MS,
2149
+ REPLAY_AFTER_MS: () => REPLAY_AFTER_MS,
2150
+ __getAck: () => __getAck,
2151
+ __listAcks: () => __listAcks,
2152
+ __resetAcks: () => __resetAcks,
2153
+ eventKey: () => eventKey,
2154
+ markRespondedForThread: () => markRespondedForThread,
2155
+ markState: () => markState,
2156
+ reconcileOnce: () => reconcileOnce,
2157
+ resolveBatchOnTurnEnd: () => resolveBatchOnTurnEnd,
2158
+ startReconciler: () => startReconciler,
2159
+ stopReconciler: () => stopReconciler,
2160
+ trackInbound: () => trackInbound
2161
+ });
2162
+ function eventKey(event) {
2163
+ const ref = event.ref;
2164
+ const ch = ref?.channel ?? "unknown";
2165
+ switch (ch) {
2166
+ case "slack":
2167
+ return `slack${SEP}${ref.slackChannel}${SEP}${ref.messageTs ?? ref.threadTs ?? ""}`;
2168
+ case "system":
2169
+ return `system${SEP}${ref.workerId}${SEP}${ref.kind}`;
2170
+ case "webhook":
2171
+ return `webhook${SEP}${ref.webhookId}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}`;
2172
+ case "schedule":
2173
+ return `schedule${SEP}${ref.scheduleId}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}`;
2174
+ default:
2175
+ return `${ch}${SEP}${event.enqueuedAt instanceof Date ? event.enqueuedAt.getTime() : Date.now()}${SEP}${event.content.slice(0, 40)}`;
2176
+ }
2177
+ }
2178
+ function trackInbound(sessionId, event) {
2179
+ if (event.wake === "never") return null;
2180
+ const key2 = eventKey(event);
2181
+ const existing = ledger.get(key2);
2182
+ if (existing) return key2;
2183
+ const ref = event.ref;
2184
+ const now = Date.now();
2185
+ const entry2 = {
2186
+ key: key2,
2187
+ sessionId,
2188
+ event,
2189
+ channel: ref?.channel ?? "unknown",
2190
+ state: "working",
2191
+ attempts: 0,
2192
+ trackedAt: now,
2193
+ updatedAt: now
2194
+ };
2195
+ if (ref?.channel === "slack") {
2196
+ entry2.slackChannel = ref.slackChannel;
2197
+ entry2.threadTs = ref.threadTs;
2198
+ entry2.messageTs = ref.messageTs;
2199
+ }
2200
+ ledger.set(key2, entry2);
2201
+ if (ledger.size > MAX_ENTRIES) pruneOldest();
2202
+ return key2;
2203
+ }
2204
+ function markState(key2, state2) {
2205
+ const entry2 = ledger.get(key2);
2206
+ if (!entry2) return;
2207
+ if (TERMINAL.has(entry2.state)) return;
2208
+ entry2.state = state2;
2209
+ entry2.updatedAt = Date.now();
2210
+ if (entry2.channel === "slack" && entry2.slackChannel && entry2.messageTs) {
2211
+ fireResultReaction(entry2.slackChannel, entry2.messageTs, state2);
2212
+ }
2213
+ }
2214
+ function markRespondedForThread(slackChannel2, threadTs) {
2215
+ if (!slackChannel2 || !threadTs) return;
2216
+ for (const entry2 of ledger.values()) {
2217
+ if (entry2.channel === "slack" && entry2.state === "working" && entry2.slackChannel === slackChannel2 && entry2.threadTs === threadTs) {
2218
+ markState(entry2.key, "responded");
2219
+ }
2220
+ }
2221
+ }
2222
+ function resolveBatchOnTurnEnd(events, ok) {
2223
+ if (!ok) return;
2224
+ for (const ev of events) {
2225
+ const key2 = eventKey(ev);
2226
+ const entry2 = ledger.get(key2);
2227
+ if (!entry2 || entry2.state !== "working") continue;
2228
+ if (entry2.channel === "slack") continue;
2229
+ markState(key2, "responded");
2230
+ }
2231
+ }
2232
+ async function reconcileOnce(now = Date.now()) {
2233
+ let pushToInbox2 = null;
2234
+ const toReplay = [];
2235
+ for (const entry2 of ledger.values()) {
2236
+ if (TERMINAL.has(entry2.state)) {
2237
+ if (now - entry2.updatedAt > PRUNE_AFTER_MS) ledger.delete(entry2.key);
2238
+ continue;
2239
+ }
2240
+ if (isSessionLocked(entry2.sessionId)) continue;
2241
+ if (now - entry2.updatedAt < REPLAY_AFTER_MS) continue;
2242
+ if (entry2.attempts >= MAX_ATTEMPTS) {
2243
+ failEntry(entry2);
2244
+ continue;
2245
+ }
2246
+ toReplay.push(entry2);
2247
+ }
2248
+ if (toReplay.length === 0) return;
2249
+ try {
2250
+ ({ pushToInbox: pushToInbox2 } = await Promise.resolve().then(() => (init_inbox(), inbox_exports)));
2251
+ } catch {
2252
+ return;
2253
+ }
2254
+ for (const entry2 of toReplay) {
2255
+ entry2.attempts += 1;
2256
+ entry2.updatedAt = Date.now();
2257
+ const nudged = {
2258
+ ...entry2.event,
2259
+ 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.]
2260
+ ${entry2.event.content}`,
2261
+ wake: "now"
2262
+ };
2263
+ try {
2264
+ pushToInbox2(entry2.sessionId, nudged);
2265
+ } catch {
2266
+ }
2267
+ }
2268
+ }
2269
+ function failEntry(entry2) {
2270
+ entry2.state = "failed";
2271
+ entry2.updatedAt = Date.now();
2272
+ if (entry2.channel === "slack" && entry2.slackChannel && entry2.messageTs) {
2273
+ fireResultReaction(entry2.slackChannel, entry2.messageTs, "failed");
2274
+ if (entry2.threadTs) {
2275
+ fireFallback(
2276
+ entry2.slackChannel,
2277
+ entry2.threadTs,
2278
+ `: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.`
2279
+ );
2280
+ }
2281
+ }
2282
+ recordEvent({
2283
+ source: "daemon",
2284
+ status: "failed",
2285
+ channel: entry2.channel,
2286
+ sessionId: entry2.sessionId,
2287
+ error: `unacknowledged after ${entry2.attempts} replay attempt(s)`,
2288
+ textSnippet: entry2.event.content.slice(0, 200),
2289
+ meta: { ackKey: entry2.key, ackState: "failed" }
2290
+ });
2291
+ }
2292
+ function pruneOldest() {
2293
+ const terminal = [];
2294
+ for (const e of ledger.values()) if (TERMINAL.has(e.state)) terminal.push(e);
2295
+ terminal.sort((a, b) => a.updatedAt - b.updatedAt);
2296
+ for (const e of terminal) {
2297
+ if (ledger.size <= MAX_ENTRIES) break;
2298
+ ledger.delete(e.key);
2299
+ }
2300
+ while (ledger.size > MAX_ENTRIES) {
2301
+ const oldest = ledger.keys().next().value;
2302
+ if (!oldest) break;
2303
+ ledger.delete(oldest);
2304
+ }
2305
+ }
2306
+ function fireResultReaction(channel, ts, state2) {
2307
+ if (typeof addResultReaction !== "function") return;
2308
+ try {
2309
+ void Promise.resolve(addResultReaction(channel, ts, state2)).catch(() => {
2310
+ });
2311
+ } catch {
2312
+ }
2313
+ }
2314
+ function fireFallback(channel, threadTs, text) {
2315
+ if (typeof postThreadMessage !== "function") return;
2316
+ try {
2317
+ void Promise.resolve(postThreadMessage(channel, threadTs, text)).catch(() => {
2318
+ });
2319
+ } catch {
2320
+ }
2321
+ }
2322
+ function startReconciler() {
2323
+ if (reconcileTimer) return;
2324
+ reconcileTimer = setInterval(() => {
2325
+ void reconcileOnce();
2326
+ }, RECONCILE_EVERY_MS);
2327
+ if (typeof reconcileTimer.unref === "function") reconcileTimer.unref();
2328
+ }
2329
+ function stopReconciler() {
2330
+ if (reconcileTimer) {
2331
+ clearInterval(reconcileTimer);
2332
+ reconcileTimer = null;
2333
+ }
2334
+ }
2335
+ function __getAck(key2) {
2336
+ return ledger.get(key2);
2337
+ }
2338
+ function __listAcks() {
2339
+ return [...ledger.values()];
2340
+ }
2341
+ function __resetAcks() {
2342
+ ledger.clear();
2343
+ }
2344
+ var REPLAY_AFTER_MS, RECONCILE_EVERY_MS, MAX_ATTEMPTS, PRUNE_AFTER_MS, MAX_ENTRIES, TERMINAL, SEP, ledger, reconcileTimer;
2345
+ var init_inbox_acks = __esm({
2346
+ "src/orchestrator/inbox-acks.ts"() {
2347
+ "use strict";
2348
+ init_session_lock();
2349
+ init_webhook_events();
2350
+ init_client2();
2351
+ REPLAY_AFTER_MS = 3 * 6e4;
2352
+ RECONCILE_EVERY_MS = 6e4;
2353
+ MAX_ATTEMPTS = 2;
2354
+ PRUNE_AFTER_MS = 60 * 6e4;
2355
+ MAX_ENTRIES = 5e3;
2356
+ TERMINAL = /* @__PURE__ */ new Set(["responded", "skipped", "handed_off", "failed"]);
2357
+ SEP = "\u241F";
2358
+ ledger = /* @__PURE__ */ new Map();
2359
+ reconcileTimer = null;
2360
+ }
2361
+ });
2362
+
2363
+ // src/browser/stream-proxy.ts
2364
+ var stream_proxy_exports = {};
2365
+ __export(stream_proxy_exports, {
2366
+ BrowserStreamProxy: () => BrowserStreamProxy,
2367
+ destroyProxy: () => destroyProxy,
2368
+ getOrCreateProxy: () => getOrCreateProxy,
2369
+ getProxy: () => getProxy
2370
+ });
2371
+ import WebSocket from "ws";
2372
+ import { EventEmitter } from "events";
2373
+ function getOrCreateProxy(sessionId, port) {
2374
+ const existing = activeProxies.get(sessionId);
2375
+ if (existing) {
2376
+ console.log(`[BROWSER-WS] Reusing existing proxy for session ${sessionId} (connected=${existing.connected})`);
2377
+ return existing;
2378
+ }
2379
+ console.log(`[BROWSER-WS] Creating new proxy for session ${sessionId} on port ${port} (active proxies: ${activeProxies.size})`);
2380
+ const proxy = new BrowserStreamProxy(port);
2381
+ activeProxies.set(sessionId, proxy);
2382
+ proxy.on("close", () => {
2383
+ console.log(`[BROWSER-WS] Proxy closed for session ${sessionId}, removing from registry`);
2384
+ activeProxies.delete(sessionId);
2385
+ });
2386
+ proxy.connect();
2387
+ return proxy;
2388
+ }
2389
+ function getProxy(sessionId) {
2390
+ return activeProxies.get(sessionId);
2391
+ }
2392
+ function destroyProxy(sessionId) {
2393
+ const proxy = activeProxies.get(sessionId);
2394
+ if (proxy) {
2395
+ console.log(`[BROWSER-WS] destroyProxy() called for session ${sessionId}`);
2396
+ proxy.destroy();
2397
+ activeProxies.delete(sessionId);
2398
+ } else {
2399
+ console.log(`[BROWSER-WS] destroyProxy() called but no proxy exists for session ${sessionId}`);
2400
+ }
2401
+ }
2402
+ var RECONNECT_DELAY_MS, MAX_RECONNECT_ATTEMPTS, FRAME_THROTTLE_MS, BrowserStreamProxy, activeProxies;
2403
+ var init_stream_proxy = __esm({
2404
+ "src/browser/stream-proxy.ts"() {
2405
+ "use strict";
2406
+ RECONNECT_DELAY_MS = 1e3;
2407
+ MAX_RECONNECT_ATTEMPTS = 20;
2408
+ FRAME_THROTTLE_MS = 100;
2409
+ BrowserStreamProxy = class extends EventEmitter {
2410
+ ws = null;
2411
+ port;
2412
+ reconnectAttempts = 0;
2413
+ reconnectTimer = null;
2414
+ destroyed = false;
2415
+ lastFrameTime = 0;
2416
+ _latestFrame = null;
2417
+ _connected = false;
2418
+ constructor(port) {
2419
+ super();
2420
+ this.port = port;
2421
+ }
2422
+ get connected() {
2423
+ return this._connected;
2424
+ }
2425
+ get latestFrame() {
2426
+ return this._latestFrame;
2427
+ }
2428
+ connect() {
2429
+ if (this.destroyed) return;
2430
+ console.log(`[BROWSER-WS] connect() called for port ${this.port}`);
2431
+ this.doConnect();
2432
+ }
2433
+ doConnect() {
2434
+ if (this.destroyed) return;
2435
+ const url = `ws://localhost:${this.port}`;
2436
+ console.log(`[BROWSER-WS] Attempting WebSocket connection to ${url} (attempt ${this.reconnectAttempts + 1}/${MAX_RECONNECT_ATTEMPTS})`);
2437
+ try {
2438
+ this.ws = new WebSocket(url);
2439
+ } catch (err) {
2440
+ console.warn(`[BROWSER-WS] WebSocket constructor threw for ${url}:`, err);
2441
+ this.scheduleReconnect();
2442
+ return;
2443
+ }
2444
+ this.ws.on("open", () => {
2445
+ console.log(`[BROWSER-WS] Connected to ${url} (after ${this.reconnectAttempts} retries)`);
2446
+ this.reconnectAttempts = 0;
2447
+ this._connected = true;
2448
+ });
2449
+ this.ws.on("message", (raw) => {
2450
+ try {
1798
2451
  const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
1799
2452
  this.handleMessage(msg);
1800
2453
  } catch (err) {
@@ -1915,10 +2568,10 @@ __export(recorder_exports, {
1915
2568
  });
1916
2569
  import { exec as exec5 } from "child_process";
1917
2570
  import { promisify as promisify5 } from "util";
1918
- import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
1919
- import { join as join11 } from "path";
1920
- import { tmpdir } from "os";
1921
- import { nanoid as nanoid7 } from "nanoid";
2571
+ import { writeFile as writeFile6, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm as rm2 } from "fs/promises";
2572
+ import { join as join13 } from "path";
2573
+ import { tmpdir as tmpdir2 } from "os";
2574
+ import { nanoid as nanoid8 } from "nanoid";
1922
2575
  async function checkFfmpeg() {
1923
2576
  try {
1924
2577
  await execAsync5("ffmpeg -version", { timeout: 5e3 });
@@ -1929,7 +2582,7 @@ async function checkFfmpeg() {
1929
2582
  }
1930
2583
  async function cleanup(dir) {
1931
2584
  try {
1932
- await rm(dir, { recursive: true, force: true });
2585
+ await rm2(dir, { recursive: true, force: true });
1933
2586
  } catch {
1934
2587
  }
1935
2588
  }
@@ -1973,21 +2626,21 @@ var init_recorder = __esm({
1973
2626
  */
1974
2627
  async encode() {
1975
2628
  if (this.frames.length === 0) return null;
1976
- const workDir = join11(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
2629
+ const workDir = join13(tmpdir2(), `sparkecoder-recording-${nanoid8(8)}`);
1977
2630
  await mkdir4(workDir, { recursive: true });
1978
2631
  try {
1979
2632
  for (let i = 0; i < this.frames.length; i++) {
1980
- const framePath = join11(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
1981
- await writeFile5(framePath, this.frames[i].data);
2633
+ const framePath = join13(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
2634
+ await writeFile6(framePath, this.frames[i].data);
1982
2635
  }
1983
2636
  const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
1984
2637
  const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
1985
2638
  const clampedFps = Math.max(1, Math.min(fps, 30));
1986
- const outputPath = join11(workDir, `recording_${this.sessionId}.mp4`);
2639
+ const outputPath = join13(workDir, `recording_${this.sessionId}.mp4`);
1987
2640
  const hasFfmpeg = await checkFfmpeg();
1988
2641
  if (hasFfmpeg) {
1989
2642
  await execAsync5(
1990
- `ffmpeg -y -framerate ${clampedFps} -i "${join11(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
2643
+ `ffmpeg -y -framerate ${clampedFps} -i "${join13(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
1991
2644
  { timeout: 12e4 }
1992
2645
  );
1993
2646
  } else {
@@ -1999,7 +2652,7 @@ var init_recorder = __esm({
1999
2652
  const files = await readdir5(workDir);
2000
2653
  for (const f of files) {
2001
2654
  if (f.startsWith("frame_")) {
2002
- await unlink2(join11(workDir, f)).catch(() => {
2655
+ await unlink2(join13(workDir, f)).catch(() => {
2003
2656
  });
2004
2657
  }
2005
2658
  }
@@ -2233,7 +2886,7 @@ var SUBAGENT_MODELS = {
2233
2886
  init_db();
2234
2887
  init_config();
2235
2888
  import { z as z15 } from "zod";
2236
- import { nanoid as nanoid8 } from "nanoid";
2889
+ import { nanoid as nanoid9 } from "nanoid";
2237
2890
 
2238
2891
  // src/tools/bash.ts
2239
2892
  import { tool } from "ai";
@@ -6011,7 +6664,8 @@ async function buildSystemPrompt(options) {
6011
6664
  sessionId,
6012
6665
  discoveredSkills,
6013
6666
  activeFiles = [],
6014
- customInstructions
6667
+ customInstructions,
6668
+ taskScopedSkills
6015
6669
  } = options;
6016
6670
  let alwaysLoadedContent = "";
6017
6671
  let globMatchedContent = "";
@@ -6032,6 +6686,22 @@ async function buildSystemPrompt(options) {
6032
6686
  const skills = await loadAllSkills2(skillsDirectories);
6033
6687
  onDemandSkillsContext = formatSkillsForContext(skills);
6034
6688
  }
6689
+ let taskScopedSkillsBlock = "";
6690
+ if (taskScopedSkills && (taskScopedSkills.always.length > 0 || taskScopedSkills.onDemand.length > 0)) {
6691
+ const parts = ["<task_provided_skills>"];
6692
+ parts.push("These skills were supplied with this task and are available for this run only.");
6693
+ if (taskScopedSkills.always.length > 0) {
6694
+ parts.push(formatAlwaysLoadedSkills(taskScopedSkills.always));
6695
+ }
6696
+ if (taskScopedSkills.onDemand.length > 0) {
6697
+ parts.push("Load any of these on demand with the load_skill tool:");
6698
+ for (const s of taskScopedSkills.onDemand) {
6699
+ parts.push(`- ${s.name}: ${s.description}`);
6700
+ }
6701
+ }
6702
+ parts.push("</task_provided_skills>");
6703
+ taskScopedSkillsBlock = parts.join("\n");
6704
+ }
6035
6705
  const todos = await todoQueries.getBySession(sessionId);
6036
6706
  const todosContext = formatTodosForContext(todos);
6037
6707
  const plans = await readSessionPlans(workingDirectory, sessionId);
@@ -6324,6 +6994,8 @@ ${globMatchedContent}
6324
6994
  ${onDemandSkillsContext}
6325
6995
  </on_demand_skills>
6326
6996
 
6997
+ ${taskScopedSkillsBlock}
6998
+
6327
6999
  <current_task_list>
6328
7000
  ${todosContext}
6329
7001
  </current_task_list>
@@ -6931,20 +7603,110 @@ function sanitizeModelMessages(messages) {
6931
7603
  return result;
6932
7604
  }
6933
7605
 
7606
+ // src/utils/cap-image-count.ts
7607
+ var MAX_IMAGES_IN_CONTEXT = 11;
7608
+ var IMAGE_TRUNCATED_PLACEHOLDER = "[image truncated due to length of conversation]";
7609
+ function isImagePart(part) {
7610
+ if (!part || typeof part !== "object") return false;
7611
+ const t = part.type;
7612
+ if (t === "image") return true;
7613
+ if (t === "image-data") return true;
7614
+ if (t === "media") {
7615
+ const data = part.data;
7616
+ const mt = part.mediaType;
7617
+ if (typeof data === "string" && typeof mt === "string" && mt.startsWith("image/")) {
7618
+ return true;
7619
+ }
7620
+ }
7621
+ return false;
7622
+ }
7623
+ function makePlaceholder() {
7624
+ return { type: "text", text: IMAGE_TRUNCATED_PLACEHOLDER };
7625
+ }
7626
+ function countImages(messages) {
7627
+ let n = 0;
7628
+ for (const msg of messages) {
7629
+ if (!Array.isArray(msg.content)) continue;
7630
+ for (const part of msg.content) {
7631
+ if (isImagePart(part)) {
7632
+ n++;
7633
+ continue;
7634
+ }
7635
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
7636
+ for (const sub of part.output.value) {
7637
+ if (isImagePart(sub)) n++;
7638
+ }
7639
+ }
7640
+ }
7641
+ }
7642
+ return n;
7643
+ }
7644
+ function capImageCount(messages, max = MAX_IMAGES_IN_CONTEXT) {
7645
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
7646
+ if (max < 0) throw new Error("capImageCount: max must be >= 0");
7647
+ const total = countImages(messages);
7648
+ if (total <= max) return messages;
7649
+ let toDrop = total - max;
7650
+ let mutated = false;
7651
+ const out = messages.slice();
7652
+ for (let i = 0; i < out.length && toDrop > 0; i++) {
7653
+ const msg = out[i];
7654
+ if (!Array.isArray(msg.content)) continue;
7655
+ let contentCloned = false;
7656
+ const ensureContentCloned = () => {
7657
+ if (contentCloned) return;
7658
+ out[i] = { ...msg, content: [...msg.content] };
7659
+ contentCloned = true;
7660
+ };
7661
+ const content = () => out[i].content;
7662
+ for (let j = 0; j < content().length && toDrop > 0; j++) {
7663
+ const part = content()[j];
7664
+ if (isImagePart(part)) {
7665
+ ensureContentCloned();
7666
+ out[i].content[j] = makePlaceholder();
7667
+ toDrop--;
7668
+ mutated = true;
7669
+ continue;
7670
+ }
7671
+ if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
7672
+ const innerImages = [];
7673
+ const innerValue = part.output.value;
7674
+ for (let k = 0; k < innerValue.length; k++) {
7675
+ if (isImagePart(innerValue[k])) innerImages.push(k);
7676
+ }
7677
+ if (innerImages.length === 0) continue;
7678
+ const dropHere = Math.min(innerImages.length, toDrop);
7679
+ ensureContentCloned();
7680
+ const newOutputValue = [...innerValue];
7681
+ for (let d = 0; d < dropHere; d++) {
7682
+ newOutputValue[innerImages[d]] = makePlaceholder();
7683
+ }
7684
+ const newPart = {
7685
+ ...part,
7686
+ output: {
7687
+ ...part.output,
7688
+ value: newOutputValue
7689
+ }
7690
+ };
7691
+ out[i].content[j] = newPart;
7692
+ toDrop -= dropHere;
7693
+ mutated = true;
7694
+ }
7695
+ }
7696
+ }
7697
+ if (mutated) {
7698
+ console.warn(
7699
+ `[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.`
7700
+ );
7701
+ }
7702
+ return mutated ? out : messages;
7703
+ }
7704
+
6934
7705
  // src/agent/model-limits.ts
6935
7706
  var MODEL_LIMITS = {
6936
- "anthropic/claude-opus-4.7": { contextWindow: 2e5, rollingTarget: 15e4 },
6937
- "anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
6938
- "anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
6939
- "anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
6940
- "anthropic/claude-3-haiku": { contextWindow: 2e5, rollingTarget: 15e4 },
6941
- "google/gemini-3-flash-preview": { contextWindow: 1e6, rollingTarget: 15e4 },
6942
- "google/gemini-2.5-pro": { contextWindow: 1e6, rollingTarget: 15e4 },
6943
- "google/gemini-2.5-flash": { contextWindow: 1e6, rollingTarget: 15e4 },
6944
- "openai/gpt-4o": { contextWindow: 128e3, rollingTarget: 78e3 },
6945
- "openai/gpt-4.1": { contextWindow: 1e6, rollingTarget: 15e4 },
6946
- "openai/o3": { contextWindow: 2e5, rollingTarget: 15e4 },
6947
- "xai/grok-3": { contextWindow: 131072, rollingTarget: 8e4 }
7707
+ "claude-opus-4-8": { contextWindow: 2e5, rollingTarget: 15e4 },
7708
+ "gpt-5.5": { contextWindow: 35e4, rollingTarget: 15e4 },
7709
+ "claude-fable-5": { contextWindow: 2e5, rollingTarget: 15e4 }
6948
7710
  };
6949
7711
  var DEFAULT_LIMITS = { contextWindow: 2e5, rollingTarget: 15e4 };
6950
7712
  var PREFIX_DEFAULTS = {
@@ -7015,6 +7777,7 @@ ${summaryContent}`
7015
7777
  messages = repairToolPairing(messages);
7016
7778
  messages = ensureToolResultsFollowCalls(messages);
7017
7779
  messages = ensureEndsWithUserOrTool(messages);
7780
+ messages = capImageCount(messages);
7018
7781
  return messages;
7019
7782
  }
7020
7783
  // ---------------------------------------------------------------------------
@@ -7132,7 +7895,7 @@ ${summaryContent}`
7132
7895
  }
7133
7896
  async summarizeChunk(chunk) {
7134
7897
  const historyText = chunk.map((msg) => {
7135
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
7898
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(stripBinaryContentForSummary(msg.content));
7136
7899
  return `[${msg.role}]: ${content}`;
7137
7900
  }).join("\n\n");
7138
7901
  try {
@@ -7243,9 +8006,35 @@ ${summaryContent}`
7243
8006
  this.summaries = [];
7244
8007
  }
7245
8008
  };
7246
- function stripOrphanedToolResults(msg, removedIds) {
7247
- if (!Array.isArray(msg.content)) return msg;
7248
- const parts = msg.content.filter((part) => {
8009
+ function stripBinaryContentForSummary(value) {
8010
+ if (Array.isArray(value)) return value.map(stripBinaryContentForSummary);
8011
+ if (!value || typeof value !== "object") return value;
8012
+ const record = value;
8013
+ const type = record.type;
8014
+ if ((type === "image-data" || type === "file-data" || type === "media") && typeof record.data === "string") {
8015
+ const mediaType = typeof record.mediaType === "string" ? record.mediaType : "unknown media type";
8016
+ const filename = typeof record.filename === "string" ? ` ${record.filename}` : "";
8017
+ return {
8018
+ ...record,
8019
+ data: `[${type}${filename}; ${mediaType}; ${record.data.length} base64 chars omitted for summary]`
8020
+ };
8021
+ }
8022
+ if (type === "image" && typeof record.image === "string") {
8023
+ const filename = typeof record.filename === "string" ? ` ${record.filename}` : "";
8024
+ return {
8025
+ ...record,
8026
+ image: `[image${filename}; ${record.image.length} base64 chars omitted for summary]`
8027
+ };
8028
+ }
8029
+ const out = {};
8030
+ for (const [key2, nested] of Object.entries(record)) {
8031
+ out[key2] = stripBinaryContentForSummary(nested);
8032
+ }
8033
+ return out;
8034
+ }
8035
+ function stripOrphanedToolResults(msg, removedIds) {
8036
+ if (!Array.isArray(msg.content)) return msg;
8037
+ const parts = msg.content.filter((part) => {
7249
8038
  if (part.type === "tool-result" && removedIds.has(part.toolCallId)) return false;
7250
8039
  if (part.type === "tool-call" && removedIds.has(part.toolCallId)) return false;
7251
8040
  return true;
@@ -7406,161 +8195,8 @@ var webChannel = {
7406
8195
  displayLabel: () => "WEB"
7407
8196
  };
7408
8197
 
7409
- // src/integrations/slack/client.ts
7410
- init_config();
7411
-
7412
- // src/integrations/slack/persistence.ts
7413
- init_config();
7414
- import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync3, renameSync } from "fs";
7415
- import { join as join9, dirname as dirname6 } from "path";
7416
- var FILENAME = "slack-cache.json";
7417
- var FILE_VERSION = 1;
7418
- var SAVE_DEBOUNCE_MS = 500;
7419
- var MAX_THREADS = 5e3;
7420
- var loaded = false;
7421
- var userMap = /* @__PURE__ */ new Map();
7422
- var threadMap = /* @__PURE__ */ new Map();
7423
- var dirty = false;
7424
- var saveTimer = null;
7425
- function cachePath() {
7426
- return join9(ensureAppDataDirectory(), FILENAME);
7427
- }
7428
- function load() {
7429
- if (loaded) return;
7430
- loaded = true;
7431
- const path = cachePath();
7432
- if (!existsSync16(path)) return;
7433
- try {
7434
- const raw = readFileSync7(path, "utf-8");
7435
- const parsed = JSON.parse(raw);
7436
- if (parsed?.version !== FILE_VERSION) return;
7437
- const now = Date.now();
7438
- for (const [id, e] of Object.entries(parsed.users || {})) {
7439
- if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
7440
- userMap.set(id, {
7441
- name: e.name ?? null,
7442
- realName: e.realName ?? null,
7443
- email: e.email ?? null,
7444
- expiresAt: e.expiresAt
7445
- });
7446
- }
7447
- }
7448
- for (const [key2, e] of Object.entries(parsed.threads || {})) {
7449
- if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
7450
- threadMap.set(key2, { owned: !!e.owned, expiresAt: e.expiresAt });
7451
- }
7452
- }
7453
- } catch (err) {
7454
- console.warn(`[slack] could not load ${FILENAME}:`, err?.message || err);
7455
- }
7456
- }
7457
- function evictIfOversized(map, max) {
7458
- if (map.size <= max) return;
7459
- const sorted = [...map.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt);
7460
- const toRemove = map.size - max;
7461
- for (let i = 0; i < toRemove; i++) map.delete(sorted[i][0]);
7462
- }
7463
- function scheduleSave() {
7464
- dirty = true;
7465
- if (saveTimer) return;
7466
- saveTimer = setTimeout(() => {
7467
- saveTimer = null;
7468
- if (dirty) saveSync();
7469
- }, SAVE_DEBOUNCE_MS);
7470
- saveTimer.unref?.();
7471
- }
7472
- function saveSync() {
7473
- dirty = false;
7474
- try {
7475
- const path = cachePath();
7476
- const dir = dirname6(path);
7477
- if (!existsSync16(dir)) mkdirSync6(dir, { recursive: true });
7478
- const payload = {
7479
- version: FILE_VERSION,
7480
- users: Object.fromEntries(userMap),
7481
- threads: Object.fromEntries(threadMap)
7482
- };
7483
- const tmp = `${path}.tmp`;
7484
- writeFileSync3(tmp, JSON.stringify(payload), "utf-8");
7485
- renameSync(tmp, path);
7486
- } catch (err) {
7487
- console.warn(`[slack] could not persist ${FILENAME}:`, err?.message || err);
7488
- }
7489
- }
7490
- var exitHooked = false;
7491
- function hookExit() {
7492
- if (exitHooked) return;
7493
- exitHooked = true;
7494
- const flush2 = () => {
7495
- if (dirty) saveSync();
7496
- };
7497
- process.once("beforeExit", flush2);
7498
- process.once("SIGINT", flush2);
7499
- process.once("SIGTERM", flush2);
7500
- }
7501
- function setCachedThreadOwnership(key2, entry2) {
7502
- load();
7503
- hookExit();
7504
- threadMap.set(key2, entry2);
7505
- evictIfOversized(threadMap, MAX_THREADS);
7506
- scheduleSave();
7507
- }
7508
-
7509
- // src/integrations/slack/client.ts
7510
- function readSlackConfig() {
7511
- try {
7512
- const cfg = getConfig();
7513
- const slack = cfg?.slack;
7514
- if (!slack?.botToken) return void 0;
7515
- return {
7516
- botToken: String(slack.botToken),
7517
- signingSecret: slack.signingSecret ? String(slack.signingSecret) : void 0,
7518
- defaultOrchestratorName: slack.defaultOrchestratorName ? String(slack.defaultOrchestratorName) : void 0
7519
- };
7520
- } catch {
7521
- return void 0;
7522
- }
7523
- }
7524
- function getSlackAdapter() {
7525
- const cfg = readSlackConfig();
7526
- if (!cfg) return void 0;
7527
- return {
7528
- async postMessage({ channel, text, threadTs }) {
7529
- const res = await fetch("https://slack.com/api/chat.postMessage", {
7530
- method: "POST",
7531
- headers: {
7532
- "Content-Type": "application/json; charset=utf-8",
7533
- "Authorization": `Bearer ${cfg.botToken}`
7534
- },
7535
- body: JSON.stringify({ channel, text, thread_ts: threadTs })
7536
- });
7537
- const data = await res.json().catch(() => ({}));
7538
- if (!res.ok || data?.ok === false) {
7539
- return { ok: false, error: data?.error || `HTTP ${res.status}` };
7540
- }
7541
- return { ok: true, ts: data?.ts };
7542
- }
7543
- };
7544
- }
7545
- function isSlackConfigured() {
7546
- return readSlackConfig() !== void 0;
7547
- }
7548
- var USER_TTL_MS = 60 * 60 * 1e3;
7549
- var USER_FAIL_TTL_MS = 5 * 60 * 1e3;
7550
- var THREAD_OWNED_TTL_MS = 60 * 60 * 1e3;
7551
- var THREAD_NEG_TTL_MS = 5 * 60 * 1e3;
7552
- function threadCacheKey(channel, threadTs) {
7553
- return `${channel}\u241F${threadTs}`;
7554
- }
7555
- function noteBotPostedInThread(channel, threadTs) {
7556
- if (!channel || !threadTs) return;
7557
- setCachedThreadOwnership(threadCacheKey(channel, threadTs), {
7558
- owned: true,
7559
- expiresAt: Date.now() + THREAD_OWNED_TTL_MS
7560
- });
7561
- }
7562
-
7563
8198
  // src/integrations/channels/slack.ts
8199
+ init_client2();
7564
8200
  var ownedThreads = /* @__PURE__ */ new Set();
7565
8201
  function threadKey(channel, threadTs) {
7566
8202
  return `${channel}\u241F${threadTs}`;
@@ -7584,6 +8220,8 @@ var slackChannel = {
7584
8220
  if (r.slackChannel && r.threadTs) {
7585
8221
  markThreadOwned(r.slackChannel, r.threadTs);
7586
8222
  noteBotPostedInThread(r.slackChannel, r.threadTs);
8223
+ void Promise.resolve().then(() => (init_inbox_acks(), inbox_acks_exports)).then((m) => m.markRespondedForThread(r.slackChannel, r.threadTs)).catch(() => {
8224
+ });
7587
8225
  }
7588
8226
  },
7589
8227
  displayLabel(ref) {
@@ -7710,7 +8348,7 @@ function describeConfiguredChannels() {
7710
8348
 
7711
8349
  // src/orchestrator/schedules-store.ts
7712
8350
  init_db();
7713
- import { nanoid as nanoid4 } from "nanoid";
8351
+ import { nanoid as nanoid5 } from "nanoid";
7714
8352
  async function readOrch(orchestratorSessionId) {
7715
8353
  const s = await sessionQueries.getById(orchestratorSessionId);
7716
8354
  if (!s) return null;
@@ -7725,7 +8363,7 @@ async function createSchedule(orchestratorSessionId, input) {
7725
8363
  const data = await readOrch(orchestratorSessionId);
7726
8364
  if (!data) throw new Error("orchestrator session not found");
7727
8365
  const row = {
7728
- id: `sch_${nanoid4(10)}`,
8366
+ id: `sch_${nanoid5(10)}`,
7729
8367
  name: input.name,
7730
8368
  cron: input.cron,
7731
8369
  prompt: input.prompt,
@@ -7760,7 +8398,7 @@ init_config();
7760
8398
  // src/orchestrator/webhooks-store.ts
7761
8399
  init_db();
7762
8400
  import { randomBytes } from "crypto";
7763
- import { nanoid as nanoid5 } from "nanoid";
8401
+ import { nanoid as nanoid6 } from "nanoid";
7764
8402
  function newToken() {
7765
8403
  return randomBytes(24).toString("base64url");
7766
8404
  }
@@ -7777,7 +8415,7 @@ async function createWebhook(orchestratorSessionId, input) {
7777
8415
  const data = await readOrch2(orchestratorSessionId);
7778
8416
  if (!data) throw new Error("orchestrator session not found");
7779
8417
  const row = {
7780
- id: `whk_${nanoid5(10)}`,
8418
+ id: `whk_${nanoid6(10)}`,
7781
8419
  name: input.name,
7782
8420
  token: newToken(),
7783
8421
  wake: input.wake ?? "now",
@@ -7853,6 +8491,26 @@ var agentInputSchema = z14.object({
7853
8491
  model: z14.string().optional().describe("spawn only: model override."),
7854
8492
  workingDirectory: z14.string().optional().describe("spawn only: working directory override."),
7855
8493
  maxIterations: z14.number().int().min(1).max(500).optional().describe("spawn only."),
8494
+ mcpServers: z14.array(
8495
+ z14.object({
8496
+ name: z14.string(),
8497
+ transport: z14.enum(["http", "sse", "stdio"]),
8498
+ url: z14.string().optional(),
8499
+ headers: z14.record(z14.string(), z14.string()).optional(),
8500
+ command: z14.string().optional(),
8501
+ args: z14.array(z14.string()).optional(),
8502
+ env: z14.record(z14.string(), z14.string()).optional()
8503
+ })
8504
+ ).optional().describe("spawn only: task-scoped MCP servers (with auth headers) connected for this worker only, tools exposed as mcp_<name>_<tool>."),
8505
+ skills: z14.array(
8506
+ z14.object({
8507
+ name: z14.string(),
8508
+ description: z14.string().optional(),
8509
+ content: z14.string(),
8510
+ alwaysApply: z14.boolean().optional(),
8511
+ globs: z14.array(z14.string()).optional()
8512
+ })
8513
+ ).optional().describe("spawn only: task-scoped skills (inline markdown) available to this worker only."),
7856
8514
  // message
7857
8515
  text: z14.string().optional().describe("message only: the text to deliver to the worker."),
7858
8516
  force: z14.boolean().optional().describe("message only: soft-interrupt the current step."),
@@ -7923,7 +8581,9 @@ function buildAgentTool(opts) {
7923
8581
  workingDirectory: input.workingDirectory ?? opts.defaultWorkingDirectory,
7924
8582
  name: input.name,
7925
8583
  maxIterations: input.maxIterations ?? 100,
7926
- orchestratorSessionId: opts.orchestratorSessionId
8584
+ orchestratorSessionId: opts.orchestratorSessionId,
8585
+ ...input.mcpServers ? { mcpServers: input.mcpServers } : {},
8586
+ ...input.skills ? { skills: input.skills } : {}
7927
8587
  }
7928
8588
  });
7929
8589
  return {
@@ -8104,9 +8764,9 @@ import { createMCPClient } from "@ai-sdk/mcp";
8104
8764
 
8105
8765
  // src/integrations/mcp/store.ts
8106
8766
  init_config();
8107
- import { nanoid as nanoid6 } from "nanoid";
8108
- import { existsSync as existsSync17, readFileSync as readFileSync8 } from "fs";
8109
- import { resolve as resolve10, join as join10 } from "path";
8767
+ import { nanoid as nanoid7 } from "nanoid";
8768
+ import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
8769
+ import { resolve as resolve10, join as join11 } from "path";
8110
8770
  function readServers() {
8111
8771
  try {
8112
8772
  const cfg = getConfig();
@@ -8118,12 +8778,12 @@ function readServers() {
8118
8778
  function refreshMcpServersFromDisk() {
8119
8779
  const candidates = [
8120
8780
  resolve10(process.cwd(), "sparkecoder.config.json"),
8121
- join10(ensureAppDataDirectory(), "sparkecoder.config.json")
8781
+ join11(ensureAppDataDirectory(), "sparkecoder.config.json")
8122
8782
  ];
8123
8783
  for (const path of candidates) {
8124
- if (!existsSync17(path)) continue;
8784
+ if (!existsSync18(path)) continue;
8125
8785
  try {
8126
- const raw = JSON.parse(readFileSync8(path, "utf-8"));
8786
+ const raw = JSON.parse(readFileSync9(path, "utf-8"));
8127
8787
  const servers2 = Array.isArray(raw?.mcp?.servers) ? raw.mcp.servers : [];
8128
8788
  setMcpServers(servers2);
8129
8789
  return servers2;
@@ -8207,6 +8867,149 @@ async function loadAllMcpTools(opts = {}) {
8207
8867
  return tools;
8208
8868
  }
8209
8869
 
8870
+ // src/integrations/mcp/task-scoped.ts
8871
+ import { createMCPClient as createMCPClient2 } from "@ai-sdk/mcp";
8872
+ function sanitizeName(raw) {
8873
+ return raw.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/_+/g, "_");
8874
+ }
8875
+ function buildHttpLikeTransport(server) {
8876
+ if (!server.url) {
8877
+ throw new Error(`${server.transport} transport requires a url`);
8878
+ }
8879
+ return {
8880
+ type: server.transport,
8881
+ url: server.url,
8882
+ headers: server.headers
8883
+ };
8884
+ }
8885
+ async function buildStdioTransport2(server) {
8886
+ if (!server.command) {
8887
+ throw new Error("stdio transport requires a command");
8888
+ }
8889
+ const mod = await import("@ai-sdk/mcp/mcp-stdio");
8890
+ const Cls = mod.Experimental_StdioMCPTransport || mod.StdioClientTransport;
8891
+ if (!Cls) throw new Error("@ai-sdk/mcp/mcp-stdio is missing the stdio transport class");
8892
+ return new Cls({
8893
+ command: server.command,
8894
+ args: server.args ?? [],
8895
+ env: server.env
8896
+ });
8897
+ }
8898
+ async function buildTransport(server) {
8899
+ return server.transport === "stdio" ? await buildStdioTransport2(server) : buildHttpLikeTransport(server);
8900
+ }
8901
+ async function connectTaskMcpServers(servers2, opts = {}) {
8902
+ const tools = {};
8903
+ const connected = [];
8904
+ const errors = [];
8905
+ const clients2 = [];
8906
+ for (const raw of servers2 ?? []) {
8907
+ const name = sanitizeName(raw.name || "");
8908
+ if (!name) {
8909
+ errors.push({ name: raw.name || "(unnamed)", error: "server name is required" });
8910
+ continue;
8911
+ }
8912
+ let client = null;
8913
+ try {
8914
+ const transport = await buildTransport(raw);
8915
+ client = await createMCPClient2({ transport });
8916
+ clients2.push(client);
8917
+ const serverTools = await client.tools();
8918
+ for (const [toolName, t] of Object.entries(serverTools)) {
8919
+ tools[`mcp_${name}_${toolName}`] = t;
8920
+ }
8921
+ connected.push(name);
8922
+ } catch (err) {
8923
+ const message = err?.message || String(err);
8924
+ errors.push({ name, error: message });
8925
+ if (!opts.quiet) {
8926
+ console.warn(`[mcp:task] connecting "${name}" failed: ${message}`);
8927
+ }
8928
+ if (client) {
8929
+ try {
8930
+ await client.close();
8931
+ } catch {
8932
+ }
8933
+ const idx = clients2.indexOf(client);
8934
+ if (idx >= 0) clients2.splice(idx, 1);
8935
+ }
8936
+ }
8937
+ }
8938
+ let closed = false;
8939
+ const close = async () => {
8940
+ if (closed) return;
8941
+ closed = true;
8942
+ await Promise.all(
8943
+ clients2.map(async (c) => {
8944
+ try {
8945
+ await c.close();
8946
+ } catch {
8947
+ }
8948
+ })
8949
+ );
8950
+ };
8951
+ return { tools, connected, errors, close };
8952
+ }
8953
+
8954
+ // src/skills/task-scoped.ts
8955
+ init_skills();
8956
+ import { mkdtemp, writeFile as writeFile5, rm } from "fs/promises";
8957
+ import { tmpdir } from "os";
8958
+ import { join as join12 } from "path";
8959
+ function safeFileName(name, index) {
8960
+ const base = name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
8961
+ return `${base || `skill-${index + 1}`}.md`;
8962
+ }
8963
+ function escapeFrontmatterValue(value) {
8964
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
8965
+ }
8966
+ function buildSkillFile(skill) {
8967
+ const lines = ["---"];
8968
+ lines.push(`name: ${escapeFrontmatterValue(skill.name)}`);
8969
+ lines.push(`description: ${escapeFrontmatterValue(skill.description || skill.name)}`);
8970
+ if (skill.alwaysApply) lines.push("alwaysApply: true");
8971
+ if (skill.globs && skill.globs.length > 0) {
8972
+ lines.push(`globs: [${skill.globs.map((g) => escapeFrontmatterValue(g)).join(", ")}]`);
8973
+ }
8974
+ lines.push("---");
8975
+ lines.push("");
8976
+ lines.push(skill.content);
8977
+ return lines.join("\n");
8978
+ }
8979
+ async function materializeTaskSkills(skills, taskId) {
8980
+ if (!skills || skills.length === 0) return null;
8981
+ const safeTaskId = taskId.replace(/[^a-zA-Z0-9_-]+/g, "_");
8982
+ const dir = await mkdtemp(join12(tmpdir(), `sparkecoder-task-skills-${safeTaskId}-`));
8983
+ const seen = /* @__PURE__ */ new Set();
8984
+ await Promise.all(
8985
+ skills.map(async (skill, i) => {
8986
+ let fileName = safeFileName(skill.name, i);
8987
+ while (seen.has(fileName)) fileName = `dup-${i}-${fileName}`;
8988
+ seen.add(fileName);
8989
+ await writeFile5(join12(dir, fileName), buildSkillFile(skill), "utf-8");
8990
+ })
8991
+ );
8992
+ const loaded2 = await loadSkillsFromDirectory(dir, { priority: 1, defaultLoadType: "on_demand" });
8993
+ const alwaysSkills = loaded2.filter((s) => s.alwaysApply || s.loadType === "always");
8994
+ const onDemand = loaded2.filter((s) => !(s.alwaysApply || s.loadType === "always"));
8995
+ const always = (await Promise.all(
8996
+ alwaysSkills.map(async (s) => {
8997
+ const withContent = await loadSkillContent(s.name, [dir]);
8998
+ return withContent ? { ...s, content: withContent.content } : null;
8999
+ })
9000
+ )).filter((s) => s !== null);
9001
+ let cleaned = false;
9002
+ const cleanup2 = async () => {
9003
+ if (cleaned) return;
9004
+ cleaned = true;
9005
+ try {
9006
+ await rm(dir, { recursive: true, force: true });
9007
+ } catch {
9008
+ }
9009
+ };
9010
+ return { dir, always, onDemand, cleanup: cleanup2 };
9011
+ }
9012
+
8210
9013
  // src/utils/webhook.ts
8211
9014
  var TERMINAL_EVENTS = /* @__PURE__ */ new Set([
8212
9015
  "task.started",
@@ -8295,51 +9098,55 @@ function clearInterruptController(sessionId) {
8295
9098
  if (e) e.interruptController = void 0;
8296
9099
  }
8297
9100
 
8298
- // src/orchestrator/inbox.ts
8299
- var inboxes = /* @__PURE__ */ new Map();
8300
- var FLUSH_DEBOUNCE_MS = 200;
8301
- var flushHandler = null;
8302
- function entryFor(sessionId) {
8303
- let e = inboxes.get(sessionId);
8304
- if (!e) {
8305
- e = { pending: [] };
8306
- inboxes.set(sessionId, e);
8307
- }
8308
- return e;
9101
+ // src/agent/index.ts
9102
+ init_inbox();
9103
+
9104
+ // src/utils/local-device-time.ts
9105
+ var LOCAL_TIME_MARKER = "[Local device time:";
9106
+ function formatLocalDeviceTimeLine(now = /* @__PURE__ */ new Date()) {
9107
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
9108
+ const formatted = now.toLocaleString("en-US", {
9109
+ weekday: "long",
9110
+ year: "numeric",
9111
+ month: "long",
9112
+ day: "numeric",
9113
+ hour: "numeric",
9114
+ minute: "2-digit",
9115
+ second: "2-digit",
9116
+ timeZoneName: "short"
9117
+ });
9118
+ return `${LOCAL_TIME_MARKER} ${formatted} (${timeZone})]`;
8309
9119
  }
8310
- function pushToInbox(orchestratorSessionId, event) {
8311
- const e = entryFor(orchestratorSessionId);
8312
- e.pending.push(event);
8313
- if (event.wake === "now") {
8314
- scheduleFlush(orchestratorSessionId);
8315
- }
9120
+ function hasLocalDeviceTimeLine(text) {
9121
+ return text.includes(LOCAL_TIME_MARKER);
8316
9122
  }
8317
- function scheduleFlush(sessionId) {
8318
- const e = inboxes.get(sessionId);
8319
- if (!e) return;
8320
- if (e.timer) clearTimeout(e.timer);
8321
- e.timer = setTimeout(() => {
8322
- void flush(sessionId);
8323
- }, FLUSH_DEBOUNCE_MS);
9123
+ function prependLocalDeviceTimeToUserMessage(text, now) {
9124
+ const trimmed = text.trim();
9125
+ if (!trimmed || hasLocalDeviceTimeLine(text)) return text;
9126
+ return `${formatLocalDeviceTimeLine(now)}
9127
+ ${text}`;
8324
9128
  }
8325
- async function flush(sessionId) {
8326
- const e = inboxes.get(sessionId);
8327
- if (!e) return;
8328
- if (e.timer) {
8329
- clearTimeout(e.timer);
8330
- e.timer = void 0;
9129
+ function prependLocalDeviceTimeToUserContent(content, now) {
9130
+ if (typeof content === "string") {
9131
+ return prependLocalDeviceTimeToUserMessage(content, now);
8331
9132
  }
8332
- const events = e.pending.splice(0);
8333
- if (events.length === 0) return;
8334
- if (!flushHandler) {
8335
- console.warn("[orchestrator-inbox] flush called with no handler installed; dropping events");
8336
- return;
9133
+ const line = formatLocalDeviceTimeLine(now);
9134
+ if (content.some((p) => p.type === "text" && p.text && hasLocalDeviceTimeLine(p.text))) {
9135
+ return content;
8337
9136
  }
8338
- try {
8339
- await flushHandler(sessionId, events);
8340
- } catch (err) {
8341
- console.error("[orchestrator-inbox] flush handler threw:", err?.message || err);
9137
+ const userIdx = content.findIndex(
9138
+ (p) => p.type === "text" && p.text?.includes("[USER MESSAGE]")
9139
+ );
9140
+ if (userIdx >= 0 && content[userIdx].text) {
9141
+ const copy = content.map((p) => ({ ...p }));
9142
+ copy[userIdx] = {
9143
+ ...copy[userIdx],
9144
+ text: `${line}
9145
+ ${copy[userIdx].text}`
9146
+ };
9147
+ return copy;
8342
9148
  }
9149
+ return [{ type: "text", text: line }, ...content];
8343
9150
  }
8344
9151
 
8345
9152
  // src/agent/index.ts
@@ -8533,9 +9340,11 @@ ${prompt}` });
8533
9340
  */
8534
9341
  async stream(options) {
8535
9342
  const config = getConfig();
8536
- const userContent = this.buildUserMessageContent(options.prompt, options.attachments);
9343
+ const prompt = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserMessage(options.prompt) : options.prompt;
9344
+ const userContent = this.buildUserMessageContent(prompt, options.attachments);
9345
+ const persistedUserContent = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserContent(userContent) : userContent;
8537
9346
  if (!options.skipSaveUserMessage) {
8538
- await this.context.addUserMessage(userContent);
9347
+ await this.context.addUserMessage(persistedUserContent);
8539
9348
  }
8540
9349
  await sessionQueries.updateStatus(this.session.id, "active");
8541
9350
  let systemPrompt = await buildSystemPrompt({
@@ -8613,7 +9422,8 @@ ${personality.trim()}
8613
9422
  */
8614
9423
  async run(options) {
8615
9424
  const config = getConfig();
8616
- await this.context.addUserMessage(options.prompt);
9425
+ const prompt = this.session.config?.role === "orchestrator" ? prependLocalDeviceTimeToUserMessage(options.prompt) : options.prompt;
9426
+ await this.context.addUserMessage(prompt);
8617
9427
  const systemPrompt = await buildSystemPrompt({
8618
9428
  workingDirectory: this.session.workingDirectory,
8619
9429
  skillsDirectories: config.resolvedSkillsDirectories,
@@ -8657,355 +9467,387 @@ ${personality.trim()}
8657
9467
  */
8658
9468
  async runTask(options) {
8659
9469
  const config = getConfig();
8660
- const maxIterations = options.taskConfig.maxIterations ?? 50;
8661
- const webhookUrl = options.taskConfig.webhookUrl;
8662
- const parentTaskId = options.taskConfig.parentTaskId;
8663
- const fireWebhook = (type, data) => {
8664
- if (!webhookUrl) return;
8665
- sendWebhook(webhookUrl, {
8666
- type,
8667
- taskId: this.session.id,
8668
- sessionId: this.session.id,
8669
- ...parentTaskId ? { parentTaskId } : {},
8670
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8671
- data
8672
- });
8673
- };
8674
- const completion = { signal: null };
8675
- const onComplete = (signal) => {
8676
- completion.signal = signal;
8677
- };
8678
- let taskRecorder = null;
8679
- const sessionId = this.session.id;
8680
- const emit = options.writeSSE;
8681
- const bashProgressHandler = (progress) => {
8682
- options.onToolProgress?.({ toolName: "bash", data: progress });
8683
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "bash", data: progress })).catch(() => {
8684
- });
8685
- const port = progress.browserStreamPort;
8686
- if (port && progress.status === "started") {
8687
- Promise.resolve().then(() => (init_stream_proxy(), stream_proxy_exports)).then(({ getOrCreateProxy: getOrCreateProxy2 }) => {
8688
- const proxy = getOrCreateProxy2(sessionId, port);
8689
- if (!taskRecorder) {
8690
- Promise.resolve().then(() => (init_recorder(), recorder_exports)).then(({ FrameRecorder: FrameRecorder2 }) => {
8691
- taskRecorder = new FrameRecorder2(sessionId);
8692
- taskRecorder.start();
8693
- });
8694
- }
8695
- if (proxy.listenerCount("frame") === 0) {
8696
- proxy.on("frame", (frame) => {
8697
- taskRecorder?.addFrame(frame);
8698
- if (emit) emit(JSON.stringify({ type: "browser-frame", data: frame.data, metadata: frame.metadata })).catch(() => {
8699
- });
8700
- });
8701
- proxy.on("status", (s) => {
8702
- if (emit) emit(JSON.stringify({ type: "browser-status", ...s })).catch(() => {
8703
- });
8704
- });
8705
- }
9470
+ const taskScopedCleanups = [];
9471
+ try {
9472
+ const maxIterations = options.taskConfig.maxIterations ?? 50;
9473
+ const webhookUrl = options.taskConfig.webhookUrl;
9474
+ const parentTaskId = options.taskConfig.parentTaskId;
9475
+ const fireWebhook = (type, data) => {
9476
+ if (!webhookUrl) return;
9477
+ sendWebhook(webhookUrl, {
9478
+ type,
9479
+ taskId: this.session.id,
9480
+ sessionId: this.session.id,
9481
+ ...parentTaskId ? { parentTaskId } : {},
9482
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9483
+ data
8706
9484
  });
9485
+ };
9486
+ const completion = { signal: null };
9487
+ const onComplete = (signal) => {
9488
+ completion.signal = signal;
9489
+ };
9490
+ let taskMcpTools = {};
9491
+ if (options.mcpServers && options.mcpServers.length > 0) {
9492
+ const mcpConnection = await connectTaskMcpServers(options.mcpServers, { quiet: true });
9493
+ taskScopedCleanups.push(mcpConnection.close);
9494
+ taskMcpTools = mcpConnection.tools;
9495
+ if (mcpConnection.connected.length > 0) {
9496
+ console.log(`[TASK] connected ${mcpConnection.connected.length} task-scoped MCP server(s): ${mcpConnection.connected.join(", ")}`);
9497
+ }
9498
+ for (const e of mcpConnection.errors) {
9499
+ console.warn(`[TASK] task-scoped MCP server "${e.name}" failed to connect: ${e.error}`);
9500
+ if (options.writeSSE) await options.writeSSE(JSON.stringify({ type: "task-mcp-error", data: { name: e.name, error: e.error } }));
9501
+ }
8707
9502
  }
8708
- };
8709
- const taskTools = await createTools({
8710
- sessionId: this.session.id,
8711
- workingDirectory: this.session.workingDirectory,
8712
- skillsDirectories: config.resolvedSkillsDirectories,
8713
- onBashProgress: bashProgressHandler,
8714
- onWriteFileProgress: (progress) => {
8715
- options.onToolProgress?.({ toolName: "write_file", data: progress });
8716
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "write_file", data: progress })).catch(() => {
8717
- });
8718
- },
8719
- onSearchProgress: (progress) => {
8720
- options.onToolProgress?.({ toolName: "explore_agent", data: progress });
8721
- if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "explore_agent", data: progress })).catch(() => {
9503
+ const materializedSkills = await materializeTaskSkills(options.skills, this.session.id);
9504
+ if (materializedSkills) taskScopedCleanups.push(materializedSkills.cleanup);
9505
+ const taskSkillsDir = materializedSkills?.dir;
9506
+ let taskRecorder = null;
9507
+ const sessionId = this.session.id;
9508
+ const emit = options.writeSSE;
9509
+ const bashProgressHandler = (progress) => {
9510
+ options.onToolProgress?.({ toolName: "bash", data: progress });
9511
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "bash", data: progress })).catch(() => {
8722
9512
  });
8723
- },
8724
- taskTools: {
8725
- outputSchema: options.taskConfig.outputSchema,
8726
- onComplete,
8727
- onQuestion: async (question) => {
8728
- const payload = {
8729
- questionId: question.questionId,
8730
- question: question.question,
8731
- context: question.context,
8732
- choices: question.choices,
8733
- status: "pending"
8734
- };
8735
- const answerPromise = waitForTaskQuestionAnswer({
8736
- taskId: this.session.id,
8737
- questionId: question.questionId,
8738
- question: question.question,
8739
- context: question.context,
8740
- choices: question.choices
9513
+ const port = progress.browserStreamPort;
9514
+ if (port && progress.status === "started") {
9515
+ Promise.resolve().then(() => (init_stream_proxy(), stream_proxy_exports)).then(({ getOrCreateProxy: getOrCreateProxy2 }) => {
9516
+ const proxy = getOrCreateProxy2(sessionId, port);
9517
+ if (!taskRecorder) {
9518
+ Promise.resolve().then(() => (init_recorder(), recorder_exports)).then(({ FrameRecorder: FrameRecorder2 }) => {
9519
+ taskRecorder = new FrameRecorder2(sessionId);
9520
+ taskRecorder.start();
9521
+ });
9522
+ }
9523
+ if (proxy.listenerCount("frame") === 0) {
9524
+ proxy.on("frame", (frame) => {
9525
+ taskRecorder?.addFrame(frame);
9526
+ if (emit) emit(JSON.stringify({ type: "browser-frame", data: frame.data, metadata: frame.metadata })).catch(() => {
9527
+ });
9528
+ });
9529
+ proxy.on("status", (s) => {
9530
+ if (emit) emit(JSON.stringify({ type: "browser-status", ...s })).catch(() => {
9531
+ });
9532
+ });
9533
+ }
8741
9534
  });
8742
- fireWebhook("task.question", payload);
8743
- if (emit) {
8744
- await emit(JSON.stringify({ type: "task-question", data: payload }));
8745
- }
8746
- const orchId = this.session.config?.orchestratorSessionId;
8747
- if (orchId) {
8748
- pushToInbox(orchId, workerQuestionEvent(
8749
- this.session.id,
8750
- this.session.name || "worker",
8751
- question.question,
8752
- question.questionId
8753
- ));
8754
- }
8755
- const answer = await answerPromise;
8756
- const answeredPayload = {
8757
- questionId: question.questionId,
8758
- answer: answer.answer,
8759
- answeredBy: answer.answeredBy
8760
- };
8761
- fireWebhook("task.question_answered", answeredPayload);
8762
- if (emit) {
8763
- await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
9535
+ }
9536
+ };
9537
+ const taskTools = await createTools({
9538
+ sessionId: this.session.id,
9539
+ workingDirectory: this.session.workingDirectory,
9540
+ onBashProgress: bashProgressHandler,
9541
+ onWriteFileProgress: (progress) => {
9542
+ options.onToolProgress?.({ toolName: "write_file", data: progress });
9543
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "write_file", data: progress })).catch(() => {
9544
+ });
9545
+ },
9546
+ onSearchProgress: (progress) => {
9547
+ options.onToolProgress?.({ toolName: "explore_agent", data: progress });
9548
+ if (emit) emit(JSON.stringify({ type: "tool-progress", toolName: "explore_agent", data: progress })).catch(() => {
9549
+ });
9550
+ },
9551
+ // Add the task-scoped skills temp dir (if any) so load_skill can list
9552
+ // and load the inline skills supplied with this task.
9553
+ skillsDirectories: taskSkillsDir ? [...config.resolvedSkillsDirectories, taskSkillsDir] : config.resolvedSkillsDirectories,
9554
+ taskTools: {
9555
+ outputSchema: options.taskConfig.outputSchema,
9556
+ onComplete,
9557
+ onQuestion: async (question) => {
9558
+ const payload = {
9559
+ questionId: question.questionId,
9560
+ question: question.question,
9561
+ context: question.context,
9562
+ choices: question.choices,
9563
+ status: "pending"
9564
+ };
9565
+ const answerPromise = waitForTaskQuestionAnswer({
9566
+ taskId: this.session.id,
9567
+ questionId: question.questionId,
9568
+ question: question.question,
9569
+ context: question.context,
9570
+ choices: question.choices
9571
+ });
9572
+ fireWebhook("task.question", payload);
9573
+ if (emit) {
9574
+ await emit(JSON.stringify({ type: "task-question", data: payload }));
9575
+ }
9576
+ const orchId = this.session.config?.orchestratorSessionId;
9577
+ if (orchId) {
9578
+ pushToInbox(orchId, workerQuestionEvent(
9579
+ this.session.id,
9580
+ this.session.name || "worker",
9581
+ question.question,
9582
+ question.questionId
9583
+ ));
9584
+ }
9585
+ const answer = await answerPromise;
9586
+ const answeredPayload = {
9587
+ questionId: question.questionId,
9588
+ answer: answer.answer,
9589
+ answeredBy: answer.answeredBy
9590
+ };
9591
+ fireWebhook("task.question_answered", answeredPayload);
9592
+ if (emit) {
9593
+ await emit(JSON.stringify({ type: "task-question-answered", data: answeredPayload }));
9594
+ }
9595
+ return answer;
8764
9596
  }
8765
- return answer;
8766
9597
  }
9598
+ });
9599
+ for (const [name, t] of Object.entries(taskMcpTools)) {
9600
+ taskTools[name] = t;
8767
9601
  }
8768
- });
8769
- const baseSystemPrompt = await buildSystemPrompt({
8770
- workingDirectory: this.session.workingDirectory,
8771
- skillsDirectories: config.resolvedSkillsDirectories,
8772
- sessionId: this.session.id,
8773
- discoveredSkills: config.discoveredSkills,
8774
- activeFiles: []
8775
- });
8776
- const taskAddendum = buildTaskPromptAddendum(options.taskConfig.outputSchema);
8777
- const systemPrompt = `${baseSystemPrompt}
9602
+ const baseSystemPrompt = await buildSystemPrompt({
9603
+ workingDirectory: this.session.workingDirectory,
9604
+ skillsDirectories: taskSkillsDir ? [...config.resolvedSkillsDirectories, taskSkillsDir] : config.resolvedSkillsDirectories,
9605
+ sessionId: this.session.id,
9606
+ discoveredSkills: config.discoveredSkills,
9607
+ activeFiles: [],
9608
+ taskScopedSkills: materializedSkills ? { always: materializedSkills.always, onDemand: materializedSkills.onDemand } : void 0
9609
+ });
9610
+ const taskAddendum = buildTaskPromptAddendum(options.taskConfig.outputSchema);
9611
+ const systemPrompt = `${baseSystemPrompt}
8778
9612
 
8779
9613
  ${taskAddendum}`;
8780
- fireWebhook("task.started", { prompt: options.prompt });
8781
- if (emit) {
8782
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: options.prompt } }));
8783
- }
8784
- await this.context.addUserMessage(options.prompt);
8785
- let iteration = 0;
8786
- while (iteration < maxIterations) {
8787
- iteration++;
8788
- if (options.abortSignal?.aborted) {
8789
- const cancelError = "Task was cancelled";
8790
- fireWebhook("task.failed", { status: "failed", error: cancelError, iterations: iteration });
8791
- clearInterruptController(this.session.id);
8792
- return { status: "failed", error: cancelError, iterations: iteration };
8793
- }
8794
- const pending = drainInputs(this.session.id);
8795
- for (const p of pending) {
8796
- const labelled = p.source === "orchestrator" ? `[message from orchestrator]
9614
+ fireWebhook("task.started", { prompt: options.prompt });
9615
+ if (emit) {
9616
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: options.prompt } }));
9617
+ }
9618
+ await this.context.addUserMessage(options.prompt);
9619
+ let iteration = 0;
9620
+ while (iteration < maxIterations) {
9621
+ iteration++;
9622
+ if (options.abortSignal?.aborted) {
9623
+ const cancelError = "Task was cancelled";
9624
+ fireWebhook("task.failed", { status: "failed", error: cancelError, iterations: iteration });
9625
+ clearInterruptController(this.session.id);
9626
+ return { status: "failed", error: cancelError, iterations: iteration };
9627
+ }
9628
+ const pending = drainInputs(this.session.id);
9629
+ for (const p of pending) {
9630
+ const labelled = p.source === "orchestrator" ? `[message from orchestrator]
8797
9631
  ${p.text}` : p.source === "system" ? `[system note]
8798
9632
  ${p.text}` : p.text;
8799
- if (emit) {
8800
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: labelled } }));
8801
- }
8802
- await this.context.addUserMessage(labelled);
8803
- }
8804
- const interruptController = new AbortController();
8805
- registerInterruptController(this.session.id, interruptController);
8806
- const combinedAbort = options.abortSignal ? anySignal([options.abortSignal, interruptController.signal]) : interruptController.signal;
8807
- const messages = await this.context.getMessages();
8808
- const useAnthropic = isAnthropicModel(this.session.model);
8809
- if (emit) {
8810
- await emit(JSON.stringify({ type: "start", messageId: `msg_${Date.now()}` }));
8811
- }
8812
- let textStarted = false;
8813
- let textId = `text_${Date.now()}`;
8814
- let reasoningId = `reasoning_${Date.now()}`;
8815
- let reasoningStarted = false;
8816
- const toolCallStarts = /* @__PURE__ */ new Set();
8817
- const iterStream = streamText2({
8818
- model: resolveModel(this.session.model),
8819
- system: systemPrompt,
8820
- messages,
8821
- tools: wrapToolsNeverThrow(taskTools),
8822
- stopWhen: stepCountIs2(500),
8823
- abortSignal: combinedAbort,
8824
- providerOptions: useAnthropic ? {
8825
- anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
8826
- } : void 0,
8827
- // See the matching note in `stream()` — repair tool pairing before
8828
- // every step so we never feed the model an orphan tool-call.
8829
- prepareStep: async ({ messages: stepMessages }) => {
8830
- const paired = repairToolPairing(stepMessages);
8831
- const ordered = ensureToolResultsFollowCalls(paired);
8832
- if (ordered === stepMessages) return {};
8833
- return { messages: ordered };
8834
- },
8835
- onStepFinish: async (step) => {
8836
- options.onStepFinish?.(step);
8837
- fireWebhook("task.step_finished", { iteration, text: step.text });
8838
9633
  if (emit) {
8839
- if (textStarted) {
8840
- await emit(JSON.stringify({ type: "text-end", id: textId }));
8841
- textStarted = false;
8842
- textId = `text_${Date.now()}`;
8843
- }
8844
- await emit(JSON.stringify({ type: "finish-step" }));
9634
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: labelled } }));
8845
9635
  }
9636
+ await this.context.addUserMessage(labelled);
8846
9637
  }
8847
- });
8848
- for await (const part of iterStream.fullStream) {
8849
- if (part.type === "text-delta") {
8850
- if (emit) {
8851
- if (!textStarted) {
8852
- await emit(JSON.stringify({ type: "text-start", id: textId }));
8853
- textStarted = true;
9638
+ const interruptController = new AbortController();
9639
+ registerInterruptController(this.session.id, interruptController);
9640
+ const combinedAbort = options.abortSignal ? anySignal([options.abortSignal, interruptController.signal]) : interruptController.signal;
9641
+ const messages = await this.context.getMessages();
9642
+ const useAnthropic = isAnthropicModel(this.session.model);
9643
+ if (emit) {
9644
+ await emit(JSON.stringify({ type: "start", messageId: `msg_${Date.now()}` }));
9645
+ }
9646
+ let textStarted = false;
9647
+ let textId = `text_${Date.now()}`;
9648
+ let reasoningId = `reasoning_${Date.now()}`;
9649
+ let reasoningStarted = false;
9650
+ const toolCallStarts = /* @__PURE__ */ new Set();
9651
+ const iterStream = streamText2({
9652
+ model: resolveModel(this.session.model),
9653
+ system: systemPrompt,
9654
+ messages,
9655
+ tools: wrapToolsNeverThrow(taskTools),
9656
+ stopWhen: stepCountIs2(500),
9657
+ abortSignal: combinedAbort,
9658
+ providerOptions: useAnthropic ? {
9659
+ anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
9660
+ } : void 0,
9661
+ // See the matching note in `stream()` — repair tool pairing before
9662
+ // every step so we never feed the model an orphan tool-call.
9663
+ prepareStep: async ({ messages: stepMessages }) => {
9664
+ const paired = repairToolPairing(stepMessages);
9665
+ const ordered = ensureToolResultsFollowCalls(paired);
9666
+ if (ordered === stepMessages) return {};
9667
+ return { messages: ordered };
9668
+ },
9669
+ onStepFinish: async (step) => {
9670
+ options.onStepFinish?.(step);
9671
+ fireWebhook("task.step_finished", { iteration, text: step.text });
9672
+ if (emit) {
9673
+ if (textStarted) {
9674
+ await emit(JSON.stringify({ type: "text-end", id: textId }));
9675
+ textStarted = false;
9676
+ textId = `text_${Date.now()}`;
9677
+ }
9678
+ await emit(JSON.stringify({ type: "finish-step" }));
8854
9679
  }
8855
- await emit(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
8856
9680
  }
8857
- } else if (part.type === "reasoning-start") {
8858
- if (emit) {
8859
- await emit(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
8860
- reasoningStarted = true;
8861
- }
8862
- } else if (part.type === "reasoning-delta") {
8863
- if (emit) {
8864
- await emit(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
8865
- }
8866
- } else if (part.type === "reasoning-end") {
8867
- if (emit && reasoningStarted) {
8868
- await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
8869
- reasoningStarted = false;
8870
- reasoningId = `reasoning_${Date.now()}`;
9681
+ });
9682
+ for await (const part of iterStream.fullStream) {
9683
+ if (part.type === "text-delta") {
9684
+ if (emit) {
9685
+ if (!textStarted) {
9686
+ await emit(JSON.stringify({ type: "text-start", id: textId }));
9687
+ textStarted = true;
9688
+ }
9689
+ await emit(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
9690
+ }
9691
+ } else if (part.type === "reasoning-start") {
9692
+ if (emit) {
9693
+ await emit(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
9694
+ reasoningStarted = true;
9695
+ }
9696
+ } else if (part.type === "reasoning-delta") {
9697
+ if (emit) {
9698
+ await emit(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
9699
+ }
9700
+ } else if (part.type === "reasoning-end") {
9701
+ if (emit && reasoningStarted) {
9702
+ await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
9703
+ reasoningStarted = false;
9704
+ reasoningId = `reasoning_${Date.now()}`;
9705
+ }
9706
+ } else if (part.type === "tool-call-streaming-start") {
9707
+ if (emit) {
9708
+ const p = part;
9709
+ await emit(JSON.stringify({ type: "tool-input-start", toolCallId: p.toolCallId, toolName: p.toolName }));
9710
+ toolCallStarts.add(p.toolCallId);
9711
+ }
9712
+ } else if (part.type === "tool-call-delta") {
9713
+ if (emit) {
9714
+ const p = part;
9715
+ await emit(JSON.stringify({ type: "tool-input-delta", toolCallId: p.toolCallId, argsTextDelta: p.argsTextDelta }));
9716
+ }
9717
+ } else if (part.type === "tool-call") {
9718
+ if (emit) {
9719
+ if (!toolCallStarts.has(part.toolCallId)) {
9720
+ await emit(JSON.stringify({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName }));
9721
+ toolCallStarts.add(part.toolCallId);
9722
+ }
9723
+ const safeInput = part.toolName === "write_file" && part.input && typeof part.input === "object" ? truncateWriteFileInput(part.input) : part.input;
9724
+ await emit(JSON.stringify({ type: "tool-input-available", toolCallId: part.toolCallId, toolName: part.toolName, input: safeInput }));
9725
+ }
9726
+ } else if (part.type === "tool-result") {
9727
+ if (emit) {
9728
+ await emit(JSON.stringify({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output }));
9729
+ }
9730
+ } else if (part.type === "error") {
9731
+ console.error("Task stream error:", part.error);
9732
+ if (emit) {
9733
+ await emit(JSON.stringify({ type: "error", errorText: String(part.error) }));
9734
+ }
8871
9735
  }
8872
- } else if (part.type === "tool-call-streaming-start") {
8873
- if (emit) {
8874
- const p = part;
8875
- await emit(JSON.stringify({ type: "tool-input-start", toolCallId: p.toolCallId, toolName: p.toolName }));
8876
- toolCallStarts.add(p.toolCallId);
9736
+ }
9737
+ if (emit && textStarted) {
9738
+ await emit(JSON.stringify({ type: "text-end", id: textId }));
9739
+ }
9740
+ if (emit && reasoningStarted) {
9741
+ await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
9742
+ }
9743
+ const interrupted = interruptController.signal.aborted;
9744
+ clearInterruptController(this.session.id);
9745
+ const iterResponse = await iterStream.response;
9746
+ const responseMessages = iterResponse.messages;
9747
+ await this.context.addResponseMessages(responseMessages);
9748
+ const resultText = await iterStream.text;
9749
+ const resultSteps = await iterStream.steps;
9750
+ if (resultText) {
9751
+ options.onText?.(resultText);
9752
+ fireWebhook("task.message", { iteration, text: resultText });
9753
+ }
9754
+ for (const step of resultSteps) {
9755
+ if (step.toolCalls) {
9756
+ for (const tc of step.toolCalls) {
9757
+ options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
9758
+ fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
9759
+ }
8877
9760
  }
8878
- } else if (part.type === "tool-call-delta") {
8879
- if (emit) {
8880
- const p = part;
8881
- await emit(JSON.stringify({ type: "tool-input-delta", toolCallId: p.toolCallId, argsTextDelta: p.argsTextDelta }));
9761
+ if (step.toolResults) {
9762
+ for (const tr of step.toolResults) {
9763
+ options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
9764
+ fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
9765
+ }
8882
9766
  }
8883
- } else if (part.type === "tool-call") {
8884
- if (emit) {
8885
- if (!toolCallStarts.has(part.toolCallId)) {
8886
- await emit(JSON.stringify({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName }));
8887
- toolCallStarts.add(part.toolCallId);
9767
+ }
9768
+ if (completion.signal) {
9769
+ const sig = completion.signal;
9770
+ const finalStatus = sig.status;
9771
+ let fileUrls;
9772
+ if (finalStatus === "completed" && sig.result && typeof sig.result === "object") {
9773
+ const resultObj = sig.result;
9774
+ const filePaths = Array.isArray(resultObj.files) ? resultObj.files : [];
9775
+ if (filePaths.length > 0) {
9776
+ fileUrls = await this.uploadTaskFiles(filePaths);
8888
9777
  }
8889
- const safeInput = part.toolName === "write_file" && part.input && typeof part.input === "object" ? truncateWriteFileInput(part.input) : part.input;
8890
- await emit(JSON.stringify({ type: "tool-input-available", toolCallId: part.toolCallId, toolName: part.toolName, input: safeInput }));
8891
9778
  }
8892
- } else if (part.type === "tool-result") {
8893
- if (emit) {
8894
- await emit(JSON.stringify({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output }));
9779
+ const recordingUrls = await this.finishTaskRecording(taskRecorder);
9780
+ const allFileUrls = [...fileUrls || [], ...recordingUrls];
9781
+ const eventType = finalStatus === "completed" ? "task.completed" : "task.failed";
9782
+ fireWebhook(eventType, {
9783
+ status: finalStatus,
9784
+ result: sig.result,
9785
+ error: sig.error,
9786
+ iterations: iteration,
9787
+ fileUrls: allFileUrls.length > 0 ? allFileUrls : void 0,
9788
+ browserRecordingUrls: recordingUrls.length > 0 ? recordingUrls : void 0
9789
+ });
9790
+ const updatedTask2 = {
9791
+ ...options.taskConfig,
9792
+ status: finalStatus,
9793
+ result: sig.result,
9794
+ error: sig.error,
9795
+ iterations: iteration
9796
+ };
9797
+ await sessionQueries.update(this.session.id, {
9798
+ config: { ...this.session.config, task: updatedTask2 }
9799
+ });
9800
+ const orchId = this.session.config?.orchestratorSessionId;
9801
+ if (orchId) {
9802
+ const summary = finalStatus === "completed" ? typeof sig.result?.summary === "string" ? sig.result.summary : JSON.stringify(sig.result) : sig.error || "unknown error";
9803
+ pushToInbox(orchId, finalStatus === "completed" ? workerCompletedEvent(this.session.id, this.session.name || "worker", summary) : workerFailedEvent(this.session.id, this.session.name || "worker", summary));
8895
9804
  }
8896
- } else if (part.type === "error") {
8897
- console.error("Task stream error:", part.error);
9805
+ return {
9806
+ status: finalStatus,
9807
+ result: sig.result,
9808
+ error: sig.error,
9809
+ iterations: iteration
9810
+ };
9811
+ }
9812
+ if (!interrupted) {
9813
+ 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.";
8898
9814
  if (emit) {
8899
- await emit(JSON.stringify({ type: "error", errorText: String(part.error) }));
9815
+ await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: continuationPrompt } }));
8900
9816
  }
9817
+ await this.context.addUserMessage(continuationPrompt);
8901
9818
  }
8902
9819
  }
8903
- if (emit && textStarted) {
8904
- await emit(JSON.stringify({ type: "text-end", id: textId }));
8905
- }
8906
- if (emit && reasoningStarted) {
8907
- await emit(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
8908
- }
8909
- const interrupted = interruptController.signal.aborted;
8910
9820
  clearInterruptController(this.session.id);
8911
- const iterResponse = await iterStream.response;
8912
- const responseMessages = iterResponse.messages;
8913
- await this.context.addResponseMessages(responseMessages);
8914
- const resultText = await iterStream.text;
8915
- const resultSteps = await iterStream.steps;
8916
- if (resultText) {
8917
- options.onText?.(resultText);
8918
- fireWebhook("task.message", { iteration, text: resultText });
8919
- }
8920
- for (const step of resultSteps) {
8921
- if (step.toolCalls) {
8922
- for (const tc of step.toolCalls) {
8923
- options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
8924
- fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
8925
- }
8926
- }
8927
- if (step.toolResults) {
8928
- for (const tr of step.toolResults) {
8929
- options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
8930
- fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
8931
- }
8932
- }
8933
- }
8934
- if (completion.signal) {
8935
- const sig = completion.signal;
8936
- const finalStatus = sig.status;
8937
- let fileUrls;
8938
- if (finalStatus === "completed" && sig.result && typeof sig.result === "object") {
8939
- const resultObj = sig.result;
8940
- const filePaths = Array.isArray(resultObj.files) ? resultObj.files : [];
8941
- if (filePaths.length > 0) {
8942
- fileUrls = await this.uploadTaskFiles(filePaths);
8943
- }
8944
- }
8945
- const recordingUrls = await this.finishTaskRecording(taskRecorder);
8946
- const allFileUrls = [...fileUrls || [], ...recordingUrls];
8947
- const eventType = finalStatus === "completed" ? "task.completed" : "task.failed";
8948
- fireWebhook(eventType, {
8949
- status: finalStatus,
8950
- result: sig.result,
8951
- error: sig.error,
8952
- iterations: iteration,
8953
- fileUrls: allFileUrls.length > 0 ? allFileUrls : void 0,
8954
- browserRecordingUrls: recordingUrls.length > 0 ? recordingUrls : void 0
8955
- });
8956
- const updatedTask2 = {
8957
- ...options.taskConfig,
8958
- status: finalStatus,
8959
- result: sig.result,
8960
- error: sig.error,
8961
- iterations: iteration
8962
- };
8963
- await sessionQueries.update(this.session.id, {
8964
- config: { ...this.session.config, task: updatedTask2 }
8965
- });
8966
- const orchId = this.session.config?.orchestratorSessionId;
8967
- if (orchId) {
8968
- const summary = finalStatus === "completed" ? typeof sig.result?.summary === "string" ? sig.result.summary : JSON.stringify(sig.result) : sig.error || "unknown error";
8969
- pushToInbox(orchId, finalStatus === "completed" ? workerCompletedEvent(this.session.id, this.session.name || "worker", summary) : workerFailedEvent(this.session.id, this.session.name || "worker", summary));
8970
- }
8971
- return {
8972
- status: finalStatus,
8973
- result: sig.result,
8974
- error: sig.error,
8975
- iterations: iteration
8976
- };
9821
+ const timeoutError = `Task did not complete within ${maxIterations} iterations`;
9822
+ const timeoutRecordingUrls = await this.finishTaskRecording(taskRecorder);
9823
+ fireWebhook("task.failed", {
9824
+ status: "failed",
9825
+ error: timeoutError,
9826
+ iterations: iteration,
9827
+ browserRecordingUrls: timeoutRecordingUrls.length > 0 ? timeoutRecordingUrls : void 0
9828
+ });
9829
+ const updatedTask = {
9830
+ ...options.taskConfig,
9831
+ status: "failed",
9832
+ error: timeoutError,
9833
+ iterations: iteration
9834
+ };
9835
+ await sessionQueries.update(this.session.id, {
9836
+ config: { ...this.session.config, task: updatedTask }
9837
+ });
9838
+ const orchIdTimeout = this.session.config?.orchestratorSessionId;
9839
+ if (orchIdTimeout) {
9840
+ pushToInbox(orchIdTimeout, workerFailedEvent(this.session.id, this.session.name || "worker", timeoutError));
8977
9841
  }
8978
- if (!interrupted) {
8979
- 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.";
8980
- if (emit) {
8981
- await emit(JSON.stringify({ type: "data-user-message", data: { id: `user_${Date.now()}`, content: continuationPrompt } }));
9842
+ return { status: "failed", error: timeoutError, iterations: iteration };
9843
+ } finally {
9844
+ for (const cleanup2 of taskScopedCleanups) {
9845
+ try {
9846
+ await cleanup2();
9847
+ } catch {
8982
9848
  }
8983
- await this.context.addUserMessage(continuationPrompt);
8984
9849
  }
8985
9850
  }
8986
- clearInterruptController(this.session.id);
8987
- const timeoutError = `Task did not complete within ${maxIterations} iterations`;
8988
- const timeoutRecordingUrls = await this.finishTaskRecording(taskRecorder);
8989
- fireWebhook("task.failed", {
8990
- status: "failed",
8991
- error: timeoutError,
8992
- iterations: iteration,
8993
- browserRecordingUrls: timeoutRecordingUrls.length > 0 ? timeoutRecordingUrls : void 0
8994
- });
8995
- const updatedTask = {
8996
- ...options.taskConfig,
8997
- status: "failed",
8998
- error: timeoutError,
8999
- iterations: iteration
9000
- };
9001
- await sessionQueries.update(this.session.id, {
9002
- config: { ...this.session.config, task: updatedTask }
9003
- });
9004
- const orchIdTimeout = this.session.config?.orchestratorSessionId;
9005
- if (orchIdTimeout) {
9006
- pushToInbox(orchIdTimeout, workerFailedEvent(this.session.id, this.session.name || "worker", timeoutError));
9007
- }
9008
- return { status: "failed", error: timeoutError, iterations: iteration };
9009
9851
  }
9010
9852
  /**
9011
9853
  * Stop a task-mode browser recording, encode to MP4, upload to GCS.
@@ -9065,11 +9907,11 @@ ${p.text}` : p.text;
9065
9907
  const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
9066
9908
  if (!isRemoteConfigured2()) return [];
9067
9909
  const { readFile: readFile12 } = await import("fs/promises");
9068
- const { join: join12, basename: basename5 } = await import("path");
9910
+ const { join: join14, basename: basename5 } = await import("path");
9069
9911
  const urls = [];
9070
9912
  for (const filePath of filePaths) {
9071
9913
  try {
9072
- const fullPath = filePath.startsWith("/") ? filePath : join12(this.session.workingDirectory, filePath);
9914
+ const fullPath = filePath.startsWith("/") ? filePath : join14(this.session.workingDirectory, filePath);
9073
9915
  const fileName = basename5(fullPath);
9074
9916
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
9075
9917
  const mimeMap = {
@@ -9131,7 +9973,7 @@ ${p.text}` : p.text;
9131
9973
  description: originalTool.description || "",
9132
9974
  inputSchema: originalTool.inputSchema || z15.object({}),
9133
9975
  execute: async (input, toolOptions) => {
9134
- const toolCallId = toolOptions.toolCallId || nanoid8();
9976
+ const toolCallId = toolOptions.toolCallId || nanoid9();
9135
9977
  const execution = toolExecutionQueries.create({
9136
9978
  sessionId: this.session.id,
9137
9979
  toolName: name,