agentbox-app 0.1.6

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 (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
@@ -0,0 +1,299 @@
1
+ // Work items on disk: append-only jsonl, one file per product, folded on read.
2
+ //
3
+ // The rules live in shared/work-items.mjs (pure, browser-safe, imported by the
4
+ // UI too). This file is the disk half, and it is the ONLY write path: the MCP
5
+ // server and the app's main process both come through here rather than each
6
+ // inventing their own append. That is deliberate. Two writers that agree about
7
+ // the fold but disagree about how a line reaches the file is exactly how a
8
+ // substrate stops being trustworthy.
9
+ //
10
+ // Every answer is derived from the file, because at any moment there may be a
11
+ // dozen server processes reading it. The one thing held between calls is a fold
12
+ // of a file that has not moved, keyed on the file's own identity, and the note
13
+ // beside `foldedLedger` says why that is still the same answer.
14
+
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import crypto from 'node:crypto';
18
+ import { machineryPath } from './home.mjs';
19
+ import { withProjectLock } from './project-lock.mjs';
20
+ import {
21
+ foldWorkItems, buildLine, buildClaimLine, buildHeartbeatLine, buildReleaseLine,
22
+ isClaimable, sortForPull, matchesFilter, isWorkItemId, MAX_LINE_BYTES, LEASE_MS,
23
+ } from '../../shared/work-items.mjs';
24
+
25
+ const LEDGER = 'work-items.jsonl';
26
+ // Never load an unbounded jsonl into memory: past this, read only the trailing
27
+ // window. The fold tolerates the torn leading line that produces, exactly as
28
+ // records.mjs and the chat log already do.
29
+ const MAX_LEDGER_BYTES = 8 * 1024 * 1024;
30
+ const warnedOversize = new Set();
31
+
32
+ // THE LEDGER LIVES IN THE APP'S HOME, NOT IN THE FOLDER SHE UPLOADS FILES TO.
33
+ // It used to be `<product>/work-items.jsonl`, and on a product with no code repo
34
+ // registered that folder is the worker's own working directory. main/store/home.mjs
35
+ // says why it moved and moves the old file in the first time this is called.
36
+ function ledgerPath(projectDir) {
37
+ return machineryPath(projectDir, LEDGER);
38
+ }
39
+
40
+ /* --------------------------------- writing ------------------------------- */
41
+ // ONE writeSync of ONE buffer onto an O_APPEND fd, with the byte count checked.
42
+ //
43
+ // This is the whole reason torn lines are rare in practice and would not be if
44
+ // written casually: O_APPEND makes the offset-seek and the write a single atomic
45
+ // step against other appenders, but only for one write call. A line assembled
46
+ // from several writes can interleave with another process's line between them,
47
+ // and the result is two corrupted records rather than one. A short write means
48
+ // the guarantee did not hold, so it throws rather than leaving a half line for
49
+ // the fold to skip and the founder to never hear about.
50
+ function appendLine(file, obj) {
51
+ const buf = Buffer.from(JSON.stringify(obj) + '\n', 'utf8');
52
+ if (buf.length > MAX_LINE_BYTES) throw new Error(`work item line too large (${buf.length} bytes, max ${MAX_LINE_BYTES})`);
53
+ fs.mkdirSync(path.dirname(file), { recursive: true });
54
+ const fd = fs.openSync(file, 'a');
55
+ try {
56
+ const written = fs.writeSync(fd, buf, 0, buf.length);
57
+ if (written !== buf.length) throw new Error(`short append (${written} of ${buf.length} bytes) writing ${file}`);
58
+ } finally {
59
+ fs.closeSync(fd);
60
+ }
61
+ return obj;
62
+ }
63
+
64
+ /* --------------------------------- reading ------------------------------- */
65
+ function readLines(file) {
66
+ let text;
67
+ try {
68
+ const size = fs.statSync(file).size;
69
+ if (size > MAX_LEDGER_BYTES) {
70
+ if (!warnedOversize.has(file)) {
71
+ warnedOversize.add(file);
72
+ console.warn(`work-items: ${file} is ${size} bytes; reading only the last ${MAX_LEDGER_BYTES}`);
73
+ }
74
+ const fd = fs.openSync(file, 'r');
75
+ try {
76
+ const buf = Buffer.allocUnsafe(MAX_LEDGER_BYTES);
77
+ const read = fs.readSync(fd, buf, 0, MAX_LEDGER_BYTES, size - MAX_LEDGER_BYTES);
78
+ text = buf.toString('utf8', 0, read);
79
+ } finally {
80
+ fs.closeSync(fd);
81
+ }
82
+ } else {
83
+ text = fs.readFileSync(file, 'utf8');
84
+ }
85
+ } catch (err) {
86
+ if (err.code === 'ENOENT') return [];
87
+ throw err;
88
+ }
89
+ return text.split('\n').filter((l) => l && Buffer.byteLength(l, 'utf8') <= MAX_LINE_BYTES);
90
+ }
91
+
92
+ /* --------------------------- the fold, kept warm -------------------------- */
93
+ // THE INBOX REFOLDS EVERY LEDGER SHE OWNS SEVERAL TIMES A SECOND, and that was
94
+ // the lag. Measured on her Mac, 2026-09-19: the app's snapshot calls
95
+ // `readWorkItems` once per project, 33 of them, over 21 MB of jsonl, and the
96
+ // renderer refetches that snapshot on every push while a worker streams, which
97
+ // is one push every 400 ms. Median cost of the fold alone: 232 ms of
98
+ // synchronous work on the thread that also draws her window. The main process
99
+ // sat at 69% of a core with nobody touching the app, and every click had to
100
+ // wait its turn behind a 232 ms block.
101
+ //
102
+ // Nothing in that work is new. A streaming worker writes to the ledger once a
103
+ // minute at most, so the same 22,990 lines were being read off disk and parsed
104
+ // again for an answer that had not changed.
105
+ //
106
+ // SO THE FOLD IS KEPT, KEYED ON THE FILE'S OWN IDENTITY: inode, byte length and
107
+ // modification time. The ledger is append-only through `appendLine` above, so
108
+ // any write at all moves the length, and a write from ANOTHER process moves it
109
+ // just the same. That is what makes this safe across the dozen readers the
110
+ // header talks about: the key is a property of the file on disk, not a belief
111
+ // this process holds about it. A miss costs exactly what every call used to.
112
+ //
113
+ // THE CACHED FOLD IS UN-AGED, and that is the subtle part. `foldWorkItems`
114
+ // finishes by expiring leases against `now`, which is the one thing in the
115
+ // answer that changes while the file does not. So the cache is folded at time
116
+ // zero, where no lease has lapsed yet, and every caller gets a copy aged
117
+ // against its own `now` (`aged` below). Handing out the cached objects
118
+ // themselves would let one caller's edit become every later caller's truth.
119
+ const folded = new Map();
120
+ const MAX_FOLDS_HELD = 64;
121
+
122
+ function ledgerStamp(file) {
123
+ try {
124
+ const st = fs.statSync(file);
125
+ return `${st.ino}:${st.size}:${st.mtimeMs}`;
126
+ } catch (err) {
127
+ if (err.code === 'ENOENT') return 'absent';
128
+ throw err;
129
+ }
130
+ }
131
+
132
+ function foldedLedger(file) {
133
+ const stamp = ledgerStamp(file);
134
+ const held = folded.get(file);
135
+ if (held && held.stamp === stamp) return held.items;
136
+ // Time zero, so the lease pass inside the fold expires nothing: a lease is
137
+ // only ever lapsed against the clock of the call asking, and that is `aged`.
138
+ const items = stamp === 'absent' ? new Map() : foldWorkItems(readLines(file), 0);
139
+ // One entry per ledger, so on her Mac this is 33 of them. The cap is for the
140
+ // long-lived MCP server, which walks temporary project dirs that never come
141
+ // back; a Map iterates in insertion order, so the oldest key goes first.
142
+ if (folded.size >= MAX_FOLDS_HELD) folded.delete(folded.keys().next().value);
143
+ folded.set(file, { stamp, items });
144
+ return items;
145
+ }
146
+
147
+ // One item as of `now`: a copy, with the lease rule that `foldWorkItems` applies
148
+ // at the end of its own pass applied here instead. The two mutable fields a
149
+ // caller can reach through a spread are copied with it.
150
+ function aged(item, now) {
151
+ const claimExpired = !!(item.claim && item.claim.leaseUntil < now);
152
+ const out = {
153
+ ...item,
154
+ labels: Array.isArray(item.labels) ? [...item.labels] : item.labels,
155
+ wrote: { ...item.wrote },
156
+ claim: item.claim ? { ...item.claim } : item.claim,
157
+ claimExpired,
158
+ };
159
+ if (out.status === 'claimed' && (!out.claim || claimExpired)) out.status = 'open';
160
+ return out;
161
+ }
162
+
163
+ /** Every work item in one product, folded. */
164
+ export function readWorkItems(projectDir, now = Date.now()) {
165
+ const out = [];
166
+ for (const item of foldedLedger(ledgerPath(projectDir)).values()) out.push(aged(item, now));
167
+ return out;
168
+ }
169
+
170
+ /** One work item, or null. */
171
+ export function readWorkItem(projectDir, id, now = Date.now()) {
172
+ if (!isWorkItemId(id)) return null;
173
+ const item = foldedLedger(ledgerPath(projectDir)).get(id);
174
+ return item ? aged(item, now) : null;
175
+ }
176
+
177
+ /* -------------------------------- mutating ------------------------------- */
178
+ export function newWorkItemId() {
179
+ return `w-${crypto.randomBytes(5).toString('hex')}`;
180
+ }
181
+
182
+ /**
183
+ * Create a work item. A plain append: no lock, because there is no read to
184
+ * modify and the id is fresh, so there is nothing to race against.
185
+ */
186
+ export function createWorkItem(projectDir, { title, ...rest } = {}, { source = 'agent', now = Date.now() } = {}) {
187
+ if (!title || typeof title !== 'string' || !title.trim()) throw new Error('a work item needs a title');
188
+ const id = newWorkItemId();
189
+ appendLine(ledgerPath(projectDir), buildLine({ id, patch: { title, status: 'open', ...rest }, source, now }));
190
+ return readWorkItem(projectDir, id, now);
191
+ }
192
+
193
+ /**
194
+ * Create a work item under a CALLER-CHOSEN id, only if it does not exist yet.
195
+ *
196
+ * The caller is the app serving a repeating task's period, where the id is derived
197
+ * from the rule and the date so that two writers racing describe the same item
198
+ * rather than two rows nothing can merge.
199
+ *
200
+ * Why the absence check and not simply a create with a fixed id: a create append
201
+ * writes `status: 'open'`, and the fold hands a field to the later line between
202
+ * equal authorities. Replaying a create after that run had finished would reopen
203
+ * a completed item and strip the labels that kept it out of the founder's inbox.
204
+ *
205
+ * TAKE THE PROJECT LOCK AROUND THIS. The read and the append are not atomic
206
+ * together. It does not lock itself because its caller has more to write in the
207
+ * same critical section, and this lock is not reentrant.
208
+ */
209
+ export function createWorkItemIfAbsent(projectDir, id, { title, ...rest } = {}, { source = 'agent', now = Date.now() } = {}) {
210
+ if (!isWorkItemId(id)) throw new Error(`invalid work item id: ${id}`);
211
+ if (!title || typeof title !== 'string' || !title.trim()) throw new Error('a work item needs a title');
212
+ const file = ledgerPath(projectDir);
213
+ const existing = foldWorkItems(readLines(file), now).get(id);
214
+ if (existing) return { created: false, item: existing };
215
+ appendLine(file, buildLine({ id, patch: { title, status: 'open', ...rest }, source, now }));
216
+ return { created: true, item: readWorkItem(projectDir, id, now) };
217
+ }
218
+
219
+ /**
220
+ * Patch a work item. Also a plain append: the fold resolves conflicts per field,
221
+ * so concurrent patches from different processes cannot lose each other.
222
+ *
223
+ * `epoch` fences an agent write. Pass the epoch the caller's claim was granted
224
+ * under; the fold discards the line if that claim has since been superseded.
225
+ * A founder write passes source 'founder' and no epoch, and is never fenced.
226
+ */
227
+ export function updateWorkItem(projectDir, id, patch, { epoch = null, source = 'agent', now = Date.now() } = {}) {
228
+ appendLine(ledgerPath(projectDir), buildLine({ id, patch, source, epoch, now }));
229
+ return readWorkItem(projectDir, id, now);
230
+ }
231
+
232
+ /**
233
+ * Claim a work item, or pull the next one matching a filter.
234
+ *
235
+ * THE ONE OPERATION THAT NEEDS THE LOCK. Everything else here is an append that
236
+ * the fold reconciles, but claiming is read-decide-write: fold the ledger, see
237
+ * an item unclaimed, append a claim. Two processes doing that concurrently both
238
+ * see it unclaimed and both claim it, which is the exact failure the whole
239
+ * design exists to prevent. So the fold and the append happen together, inside
240
+ * the lock, and the section stays tiny.
241
+ *
242
+ * Returns { claimed: true, item, epoch } or { claimed: false, reason, heldBy },
243
+ * never a throw for the ordinary case of losing a race: a worker that loses
244
+ * should ask for the next item, not die.
245
+ */
246
+ export async function claimWorkItem(projectDir, { id = null, filter = null, holder, leaseMs = LEASE_MS, now = Date.now() } = {}) {
247
+ if (!holder) throw new Error('a claim needs a holder id');
248
+ if (!id && !filter) throw new Error('claim needs either an id or a filter');
249
+ const file = ledgerPath(projectDir);
250
+
251
+ return withProjectLock(projectDir, async () => {
252
+ const items = foldWorkItems(readLines(file), now);
253
+
254
+ let target = null;
255
+ if (id) {
256
+ const item = items.get(id);
257
+ if (!item) return { claimed: false, reason: 'no such work item' };
258
+ // By id, a blocked item IS claimable: a worker sent specifically to
259
+ // unblock something must not be refused by its own queue.
260
+ if (item.status === 'done') return { claimed: false, reason: 'already done' };
261
+ // A LIVE CLAIM OF YOUR OWN IS NOT SOMEBODY ELSE'S. The holder was never
262
+ // compared here, so a session asking for the row it already holds was
263
+ // told "held by another worker" and handed back its own name, and the
264
+ // only way through was to sit out its own five minute lease. Measured
265
+ // on, 2026-09-21: she replied at 20:26:52, the session was refused by
266
+ // its own claim, waited out the lease, and the answer she was waiting
267
+ // for sat in a parked file. Re-claiming what you hold is a renewal.
268
+ if (item.claim && !item.claimExpired && item.claim.holder !== holder) {
269
+ return { claimed: false, reason: 'held by another worker', heldBy: item.claim.holder };
270
+ }
271
+ target = item;
272
+ } else {
273
+ const candidates = sortForPull([...items.values()].filter((i) => matchesFilter(i, filter) && isClaimable(i, now)));
274
+ if (!candidates.length) return { claimed: false, reason: 'nothing available' };
275
+ target = candidates[0];
276
+ }
277
+
278
+ // The epoch always moves forward, including when breaking an expired lease.
279
+ // That is what makes the previous holder's later writes identifiable as
280
+ // stale rather than merely old.
281
+ const epoch = (target.epoch ?? 0) + 1;
282
+ appendLine(file, buildClaimLine({ id: target.id, epoch, holder, now, leaseMs }));
283
+ return { claimed: true, epoch, item: foldWorkItems(readLines(file), now).get(target.id) };
284
+ }, { label: `claim ${id ?? 'next'}` });
285
+ }
286
+
287
+ /** Extend a lease. Written by the server on a timer, never by a model. */
288
+ export function heartbeatWorkItem(projectDir, id, { epoch, holder, leaseMs = LEASE_MS, now = Date.now() } = {}) {
289
+ appendLine(ledgerPath(projectDir), buildHeartbeatLine({ id, epoch, holder, now, leaseMs }));
290
+ }
291
+
292
+ /** Hand an item back without finishing it. */
293
+ export function releaseWorkItem(projectDir, id, { epoch, now = Date.now() } = {}) {
294
+ appendLine(ledgerPath(projectDir), buildReleaseLine({ id, epoch, now }));
295
+ return readWorkItem(projectDir, id, now);
296
+ }
297
+
298
+ export { LEASE_MS };
299
+ export const _internals = { appendLine, readLines, ledgerPath, forgetFolds: () => folded.clear() };
@@ -0,0 +1,14 @@
1
+ // Per-key async serialization: catalog mutations and record appends for one
2
+ // project run one at a time, so concurrent chats can never interleave a
3
+ // read-modify-write. File edits are deliberately NOT queued (spec: LWW + git).
4
+ export function makeWriteQueue() {
5
+ const tails = new Map(); // key -> promise tail
6
+ return {
7
+ run(key, fn) {
8
+ const prev = tails.get(key) ?? Promise.resolve();
9
+ const next = prev.then(fn, fn); // run regardless of prior outcome
10
+ tails.set(key, next.catch(() => {})); // a throw never poisons the chain
11
+ return next;
12
+ },
13
+ };
14
+ }
@@ -0,0 +1,23 @@
1
+ // Agentbox's own store modules, imported normally because they now live here.
2
+ //
3
+ // These files used to be loaded out of another repo by absolute path, so
4
+ // the app could not start on a machine that did not have that private repo
5
+ // checked out. They are Agentbox's now: main/store/* and shared/work-items.mjs,
6
+ // shared/dashboard.mjs, shared/contracts.mjs. The fold rules, the lock and the
7
+ // disk write-path still exist in exactly one place, that place is just inside
8
+ // this repo. Nothing here reaches outside it.
9
+
10
+ import * as workItemsDisk from './store/work-items.mjs';
11
+ import * as workItemsCore from '../shared/work-items.mjs';
12
+ import * as dashboard from '../shared/dashboard.mjs';
13
+ import * as project from './store/project.mjs';
14
+ import * as lock from './store/project-lock.mjs';
15
+
16
+ const mods = { workItemsDisk, workItemsCore, dashboard, project, lock };
17
+
18
+ // Kept as a function, and async, because every caller already awaits it and
19
+ // the shape of the returned object is what store.mjs and repeats.mjs destructure.
20
+ // There is nothing left to fail, so there is nothing left to throw.
21
+ export async function loadStore() {
22
+ return mods;
23
+ }