agentbox-app 0.1.7 → 0.1.12

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 (255) hide show
  1. package/README.md +220 -58
  2. package/bin/agentbox.mjs +18 -3
  3. package/briefs/message-rules.md +24 -3
  4. package/briefs/worker.md +63 -49
  5. package/briefs/writing-rules.md +90 -131
  6. package/main/account-discovery.mjs +21 -20
  7. package/main/account-tooling.mjs +11 -11
  8. package/main/agent-activity.mjs +45 -5
  9. package/main/agent-schedule.mjs +14 -14
  10. package/main/agent-sessions.mjs +94 -10
  11. package/main/agent-updates.mjs +116 -0
  12. package/main/agents.mjs +52 -46
  13. package/main/analytics.mjs +27 -0
  14. package/main/approval-prompt-server.mjs +2 -2
  15. package/main/approvals.mjs +14 -5
  16. package/main/artifact-path.mjs +11 -0
  17. package/main/claude-bin.mjs +32 -5
  18. package/main/claude-input.mjs +45 -3
  19. package/main/claude-plan.mjs +25 -13
  20. package/main/code-change.mjs +3 -3
  21. package/main/codex-account.mjs +1 -1
  22. package/main/codex-bin.mjs +18 -0
  23. package/main/codex-launch-env.mjs +31 -0
  24. package/main/codex-memory-gate.mjs +260 -0
  25. package/main/codex-models.mjs +12 -7
  26. package/main/codex-threads.mjs +46 -23
  27. package/main/codex-usage-file.mjs +7 -7
  28. package/main/codex-usage.mjs +4 -4
  29. package/main/codex-watch.mjs +12 -12
  30. package/main/config.mjs +82 -17
  31. package/main/demo.mjs +17 -14
  32. package/main/dev-window.mjs +9 -9
  33. package/main/doc-scheme.mjs +2 -0
  34. package/main/engine-setup.mjs +204 -0
  35. package/main/feedback.mjs +55 -0
  36. package/main/folder-pictures.mjs +48 -0
  37. package/main/frame-scrollbars.mjs +90 -0
  38. package/main/fresh-user.mjs +7 -4
  39. package/main/git-change-offthread.mjs +70 -0
  40. package/main/git-change-thread.mjs +11 -0
  41. package/main/git-change.mjs +9 -3
  42. package/main/img-scheme.mjs +70 -1
  43. package/main/instruction-settings.mjs +134 -60
  44. package/main/ipc.mjs +335 -42
  45. package/main/leftovers.mjs +518 -0
  46. package/main/live-replies.mjs +43 -5
  47. package/main/machine.mjs +48 -1
  48. package/main/main.mjs +138 -15
  49. package/main/memory-gate-history.mjs +254 -0
  50. package/main/memory-gate-server.mjs +461 -0
  51. package/main/memory-gate.mjs +206 -0
  52. package/main/message-priority.mjs +87 -0
  53. package/main/notify.mjs +1 -1
  54. package/main/rail-note.mjs +2 -2
  55. package/main/refresh-agent-models.mjs +21 -0
  56. package/main/repeats.mjs +4 -4
  57. package/main/row-label.mjs +115 -51
  58. package/main/runs-on.mjs +93 -0
  59. package/main/serve.mjs +77 -5
  60. package/main/settings.mjs +245 -26
  61. package/main/ship-queue.mjs +279 -0
  62. package/main/sign-in-files.mjs +39 -0
  63. package/main/source-updater.mjs +298 -0
  64. package/main/store/home.mjs +84 -7
  65. package/main/store/project.mjs +15 -19
  66. package/main/store/work-items.mjs +150 -6
  67. package/main/store.mjs +245 -13
  68. package/main/supervisor.mjs +1634 -417
  69. package/main/task-commands.mjs +8 -10
  70. package/main/task-folders.mjs +94 -16
  71. package/main/task-remote-control.mjs +3 -3
  72. package/main/task-terminals.mjs +14 -2
  73. package/main/team/index.mjs +440 -0
  74. package/main/team/memory-cloud.mjs +247 -0
  75. package/main/team/projects.mjs +162 -0
  76. package/main/team/session.mjs +225 -0
  77. package/main/team/sign-in.mjs +106 -0
  78. package/main/team/supabase-backend.mjs +231 -0
  79. package/main/team/sync.mjs +163 -0
  80. package/main/updater.mjs +15 -5
  81. package/main/write-before-reload.mjs +50 -0
  82. package/mcp/core/page.mjs +10 -12
  83. package/mcp/core/work.mjs +123 -26
  84. package/mcp/tools.mjs +13 -6
  85. package/package.json +21 -9
  86. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  87. package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
  88. package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
  89. package/renderer/dist/index.html +2 -2
  90. package/scripts/after-install.mjs +14 -0
  91. package/scripts/approval-server.sh +7 -2
  92. package/scripts/memory-gate-hook.sh +59 -0
  93. package/shared/agent-import.mjs +39 -16
  94. package/shared/agents.d.mts +26 -8
  95. package/shared/agents.mjs +100 -108
  96. package/shared/analytics-events.mjs +19 -2
  97. package/shared/answers.mjs +12 -0
  98. package/shared/bridge-map.d.mts +12 -0
  99. package/shared/bridge-map.mjs +38 -1
  100. package/shared/claude-commands.generated.mjs +2 -2
  101. package/shared/claude-models.generated.mjs +6 -6
  102. package/shared/claude-usage.mjs +6 -6
  103. package/shared/codex-import.mjs +7 -7
  104. package/shared/codex-modes.d.mts +50 -0
  105. package/shared/codex-modes.mjs +1 -1
  106. package/shared/codex-usage.mjs +7 -8
  107. package/shared/demo-world.mjs +6 -6
  108. package/shared/engines.mjs +27 -10
  109. package/shared/feedback.d.mts +14 -0
  110. package/shared/feedback.mjs +44 -0
  111. package/shared/first-run-examples.mjs +4 -4
  112. package/shared/first-run-practice.d.mts +16 -0
  113. package/shared/first-run-practice.mjs +176 -70
  114. package/shared/fresh-user-home.mjs +34 -4
  115. package/shared/instruction-defaults.json +2 -2
  116. package/shared/product-name.mjs +4 -2
  117. package/shared/project-folder-check.mjs +9 -3
  118. package/shared/rank.d.mts +1 -0
  119. package/shared/rank.mjs +22 -0
  120. package/shared/referenced-files.mjs +1 -1
  121. package/shared/runs-on.d.mts +20 -0
  122. package/shared/runs-on.mjs +79 -0
  123. package/shared/schemes.d.mts +11 -0
  124. package/shared/schemes.mjs +1 -1
  125. package/shared/shortcuts.d.mts +41 -0
  126. package/shared/shortcuts.mjs +236 -0
  127. package/shared/spawn-trouble.d.mts +2 -0
  128. package/shared/spawn-trouble.mjs +40 -19
  129. package/shared/team-rules.d.mts +17 -0
  130. package/shared/team-rules.mjs +230 -0
  131. package/shared/team-status.d.mts +11 -0
  132. package/shared/team-status.mjs +90 -0
  133. package/shared/terminal-links.d.mts +2 -0
  134. package/shared/terminal-links.mjs +55 -0
  135. package/shared/terminal-state.d.mts +1 -0
  136. package/shared/terminal-state.mjs +16 -0
  137. package/shared/thread-cards.d.mts +19 -0
  138. package/shared/thread-cards.mjs +254 -0
  139. package/shared/usage.d.mts +1 -1
  140. package/shared/usage.mjs +6 -7
  141. package/shared/work-items.mjs +258 -28
  142. package/shared/work-lines.mjs +393 -16
  143. package/worker-permissions.json +1 -1
  144. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  145. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  146. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  147. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  148. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  149. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  150. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  151. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  152. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  153. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  154. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  155. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  156. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  157. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  158. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  159. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  160. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  161. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  162. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  163. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  164. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  165. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  166. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  167. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  168. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  169. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  170. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  171. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  172. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  173. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  174. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  175. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  176. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  177. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  178. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  179. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  180. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  181. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  182. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  183. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  184. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  185. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  186. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  187. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  188. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  189. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  190. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  191. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  192. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  193. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  194. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  195. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  196. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  197. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  198. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  199. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  200. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  201. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  202. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  203. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  204. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  205. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  206. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  207. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  208. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  209. package/renderer/dist/emulator/data/compression/README.md +0 -11
  210. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  211. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  212. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  213. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  214. package/renderer/dist/emulator/data/cores/README.md +0 -22
  215. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  216. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  217. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  218. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  219. package/renderer/dist/emulator/data/cores/package.json +0 -65
  220. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  221. package/renderer/dist/emulator/data/emulator.css +0 -1614
  222. package/renderer/dist/emulator/data/loader.js +0 -170
  223. package/renderer/dist/emulator/data/localization/README.md +0 -63
  224. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  225. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  226. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  227. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  228. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  229. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  230. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  231. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  232. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  233. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  234. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  235. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  236. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  237. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  238. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  239. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  240. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  241. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  242. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  243. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  244. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  245. package/renderer/dist/emulator/data/src/compression.js +0 -142
  246. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  247. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  248. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  249. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  250. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  251. package/renderer/dist/emulator/data/src/storage.js +0 -125
  252. package/renderer/dist/emulator/data/version.json +0 -5
  253. package/renderer/dist/emulator/host.js +0 -299
  254. package/renderer/dist/emulator/index.html +0 -28
  255. package/renderer/dist/emulator/provenance.json +0 -6
@@ -15,7 +15,10 @@
15
15
  // and repeat rules all sat in it.
16
16
  //
17
17
  // So this is the same convention, one name over: a dot-folder in her home
18
- // named after this app.
18
+ // named after this app. On Linux a fresh install does not get that dot-folder,
19
+ // and it does not get a visible folder of the app's name in $HOME. Application
20
+ // data belongs in $XDG_DATA_HOME (see defaultStoreRoot). An existing dot-folder
21
+ // is still read, so a store already on disk is not orphaned.
19
22
  //
20
23
  // WHAT MOVES AND WHAT DOES NOT, because the line is not "everything of ours".
21
24
  // The app's own RECORDS move: the work item ledger, session traces, run
@@ -37,7 +40,7 @@ import fs from 'node:fs';
37
40
  import os from 'node:os';
38
41
  import path from 'node:path';
39
42
  import crypto from 'node:crypto';
40
- import { WAS, envNames, nameSlug, readEnv } from '../../shared/product-name.mjs';
43
+ import { Name, WAS, envNames, nameSlug, readEnv } from '../../shared/product-name.mjs';
41
44
 
42
45
  const isDir = (dir) => { try { return fs.statSync(dir).isDirectory(); } catch { return false; } };
43
46
 
@@ -61,6 +64,25 @@ export function storeRootEnv(storeRoot) {
61
64
  return env;
62
65
  }
63
66
 
67
+ // Where a config that names no store keeps the inbox.
68
+ //
69
+ // macOS, and anything that is not Linux: a folder of the app's name in the
70
+ // home directory. That is the folder this app has used since 2026-09-22.
71
+ // Linux: $XDG_DATA_HOME/<slug>, or ~/.local/share/<slug> when the variable is
72
+ // unset, blank, or not an absolute path. A visible directory in $HOME is not
73
+ // where an application writes. Measured 2026-10-07: a config that named no
74
+ // store created one.
75
+ //
76
+ // The home and the environment are arguments so a test can ask about another
77
+ // machine. A caller that passes a stand-in home must pass an environment that
78
+ // belongs to it. This function does not decide that; it uses the env it is given.
79
+ export function defaultStoreRoot({ home = os.homedir(), platform = process.platform, env = process.env } = {}) {
80
+ if (platform !== 'linux') return path.join(home, Name);
81
+ const given = typeof env?.XDG_DATA_HOME === 'string' ? env.XDG_DATA_HOME.trim() : '';
82
+ const base = given && path.isAbsolute(given) ? given : path.join(home, '.local', 'share');
83
+ return path.join(base, nameSlug);
84
+ }
85
+
64
86
  // One env var, for tests and for anyone who keeps their home somewhere else.
65
87
  // Read at call time rather than captured, because a test sets it per case.
66
88
  //
@@ -74,9 +96,10 @@ export function storeRootEnv(storeRoot) {
74
96
  // store goes missing on the release that renames the app.
75
97
  // `homeDir` is injected so a test can ask what this answers on a machine with
76
98
  // an old dot-folder on it, without one having to exist on the machine running
77
- // the test. Production never passes it.
78
- export function appHome(homeDir = os.homedir()) {
79
- const override = readEnv('HOME');
99
+ // the test. `platform` and `env` are the same kind of injection: this suite
100
+ // runs on a Mac and still has to answer a Linux home. Production passes neither.
101
+ export function appHome(homeDir = os.homedir(), { platform = process.platform, env } = {}) {
102
+ const override = readEnv('HOME', env);
80
103
  if (override) return override;
81
104
  const here = path.join(homeDir, `.${nameSlug}`);
82
105
  if (isDir(here)) return here;
@@ -84,12 +107,21 @@ export function appHome(homeDir = os.homedir()) {
84
107
  const older = path.join(homeDir, `.${was.toLowerCase()}`);
85
108
  if (isDir(older)) return older;
86
109
  }
110
+ // A fresh Linux directory has nothing of ours on disk yet. The answer has to
111
+ // be the same path loadConfig opens, or the desktop and this lookup write two
112
+ // stores. The machine's own XDG_DATA_HOME is an absolute path in the real
113
+ // home; applying it to a stand-in home writes that stand-in's store there.
114
+ // A caller that passes `env` is asking about that environment, not this one.
115
+ if (platform === 'linux') {
116
+ const real = env === undefined && path.resolve(homeDir) === path.resolve(os.homedir());
117
+ return defaultStoreRoot({ home: homeDir, platform: 'linux', env: real ? process.env : (env ?? {}) });
118
+ }
87
119
  return here;
88
120
  }
89
121
 
90
122
  // Claude Code's own encoding, checked against a real folder on her Mac:
91
- // `/Users/you/Zero/accounts/b482f.../agentbox` becomes
92
- // `-Users-you-Zero-accounts-b482f...-agentbox`. Every character that is not
123
+ // `/Users/you/Zero/accounts/00000.../agentbox` becomes
124
+ // `-Users-you-Zero-accounts-00000...-agentbox`. Every character that is not
93
125
  // a letter or a digit becomes a dash, which is readable at a glance and safe on
94
126
  // every filesystem.
95
127
  export function encodeProjectPath(dir) {
@@ -303,6 +335,51 @@ export function machineryPath(projectDir, rel, home = appHome()) {
303
335
  return target;
304
336
  }
305
337
 
338
+ /**
339
+ * RECORDS A COPY WROTE UNDER THE WRONG HOME, CARRIED TO ITS OWN.
340
+ *
341
+ * The headless door (main/serve.mjs) used to open the store without pointing
342
+ * the app's home at `storeRoot`, so it wrote its ledgers under whatever home it
343
+ * inherited: `~/.agentbox` for `npx agentbox-app`, and on the test Mac
344
+ * 2026-10-01 the founder's own store, because the copies were started from an
345
+ * agent session. Its workers were handed `storeRoot`, found nothing there, and
346
+ * answered "no work item" to every store call. Pointing the home at `storeRoot`
347
+ * fixes the next write; this is what keeps the rows already written from
348
+ * vanishing on the first boot after the fix.
349
+ *
350
+ * ONLY WHAT IS PROVABLY THIS STORE'S MOVES. A machinery directory names the
351
+ * product it belongs to in `.origin`; one is carried only when that product is
352
+ * inside `storeRoot`. Everything else under `fromHome` is somebody else's
353
+ * store and is not touched. Each record goes through `foldStray`, so a ledger
354
+ * is appended rather than overwritten and nothing is ever destroyed; the old
355
+ * directory is removed only once nothing but its marker is left in it.
356
+ *
357
+ * Returns the product folders whose records were carried.
358
+ */
359
+ export function carryMisplacedMachinery(fromHome, storeRoot, toHome = storeRoot) {
360
+ if (!fromHome || !storeRoot) return [];
361
+ const from = path.resolve(fromHome);
362
+ if (from === path.resolve(toHome)) return [];
363
+ const inside = path.resolve(storeRoot) + path.sep;
364
+ let names = [];
365
+ try { names = fs.readdirSync(path.join(from, 'projects')); } catch { return []; }
366
+ const carried = [];
367
+ for (const name of names) {
368
+ const dir = path.join(from, 'projects', name);
369
+ const origin = readOrigin(dir);
370
+ if (!origin || !path.resolve(origin).startsWith(inside)) continue;
371
+ const target = ensureMachineryDir(origin, toHome);
372
+ for (const rel of MACHINERY) foldStray(path.join(dir, rel), path.join(target, rel), rel);
373
+ let left = [];
374
+ try { left = fs.readdirSync(dir); } catch { /* gone already */ }
375
+ if (left.length === 1 && left[0] === ORIGIN) {
376
+ try { fs.rmSync(path.join(dir, ORIGIN)); fs.rmdirSync(dir); } catch { /* best effort */ }
377
+ }
378
+ carried.push(origin);
379
+ }
380
+ return carried;
381
+ }
382
+
306
383
  /**
307
384
  * EVERY RECORD OF OURS FOR ONE PROJECT, GONE.
308
385
  *
@@ -4,34 +4,30 @@
4
4
 
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import os from 'node:os';
8
7
  import crypto from 'node:crypto';
9
8
  import { SCHEMA_VERSION, validateCreation, creationText, TEXT_CREATION_KINDS } from '../../shared/contracts.mjs';
10
9
  import { assertInside, assertCreationDir, deniesGitOrEnvPath } from './paths.mjs';
11
10
  import { migrateIndex } from './migrate.mjs';
12
11
  import { withProjectLockSync } from './project-lock.mjs';
13
- import { Name, readEnv } from '../../shared/product-name.mjs';
14
-
15
- // The machine-wide root: ASTRAL_HOME, else the folder named below. This is
16
- // where device-local files live (settings.json, ui-state.json, usage.json,
17
- // .runtime-pids, ...) and where a founder's PRE-account projects sit until they
18
- // are migrated into an account space. It is NEVER account-scoped: those files
19
- // stay global (Phase 1).
12
+ import { readEnv } from '../../shared/product-name.mjs';
13
+ import { defaultStoreRoot } from './home.mjs';
14
+
15
+ // The machine-wide root: the store-home variable, else the same default
16
+ // loadConfig uses when a config names no store. This is where device-local
17
+ // files live (settings.json, ui-state.json, usage.json, .runtime-pids, ...)
18
+ // and where a founder's PRE-account projects sit until they are migrated into
19
+ // an account space. It is NEVER account-scoped: those files stay global
20
+ // (Phase 1).
20
21
  //
21
- // THE FALLBACK IS THE APP'S OWN NAME NOW, where it used to be an older
22
- // product's folder.
22
+ // THE FALLBACK IS defaultStoreRoot. On macOS that is a folder of the app's
23
+ // name. On Linux it is the XDG data directory. A visible folder in $HOME was
24
+ // the fallback until 2026-10-07, and a config that named no store created one.
23
25
  //
24
- // NOTHING WAS DELETED AND NOTHING MOVED. That folder is still on disk exactly
25
- // as it was. What changed is that this app stops reaching for it by default.
26
26
  // Her live store is named in zero.config.json and handed to every worker as
27
- // the app's own home variable, so this line was never the path she runs on; it
28
- // was only ever
29
- // the answer for an install carrying no config at all.
30
- //
31
- // An older product's HOME env var used to sit between these two. It is gone:
32
- // nothing sets it any more now that the store server carries the app's own name.
27
+ // the app's own home variable, so this line is only ever the answer for an
28
+ // install carrying no config and no store-home variable.
33
29
  export function legacyRoot() {
34
- return readEnv('HOME') || path.join(os.homedir(), Name);
30
+ return readEnv('HOME') || defaultStoreRoot();
35
31
  }
36
32
 
37
33
  // The account currently signed in, if any. Set once at boot and re-resolved on
@@ -18,7 +18,7 @@ import crypto from 'node:crypto';
18
18
  import { machineryPath } from './home.mjs';
19
19
  import { withProjectLock } from './project-lock.mjs';
20
20
  import {
21
- foldWorkItems, buildLine, buildClaimLine, buildHeartbeatLine, buildReleaseLine,
21
+ foldWorkItems, normalizeLine, buildLine, buildClaimLine, buildHeartbeatLine, buildReleaseLine,
22
22
  isClaimable, sortForPull, matchesFilter, isWorkItemId, MAX_LINE_BYTES, LEASE_MS,
23
23
  } from '../../shared/work-items.mjs';
24
24
 
@@ -47,7 +47,29 @@ function ledgerPath(projectDir) {
47
47
  // and the result is two corrupted records rather than one. A short write means
48
48
  // the guarantee did not hold, so it throws rather than leaving a half line for
49
49
  // the fold to skip and the founder to never hear about.
50
+ // WHO IS WRITING, on a Mac someone has signed in on (the team version).
51
+ //
52
+ // Main sets it on sign-in. The store's MCP server is another process, so it
53
+ // reads AGENTBOX_PERSON_ID, which the supervisor hands it. With neither, no line
54
+ // is stamped and the single-person app writes exactly what it always wrote.
55
+ let lineAuthor = null;
56
+ export function setLineAuthor(personId) {
57
+ lineAuthor = typeof personId === 'string' && personId ? personId : null;
58
+ }
59
+ function currentAuthor() {
60
+ return lineAuthor ?? (process.env.AGENTBOX_PERSON_ID || null);
61
+ }
62
+ const newLineUid = () => `l-${crypto.randomBytes(8).toString('hex')}`;
63
+
50
64
  function appendLine(file, obj) {
65
+ const author = currentAuthor();
66
+ // `by` and `uid` are added here, the one place every line passes, so no
67
+ // writer can forget them. A line that already carries them keeps its own.
68
+ const line = author ? { ...obj, by: obj.by ?? author, uid: obj.uid ?? newLineUid() } : obj;
69
+ return writeLine(file, line);
70
+ }
71
+
72
+ function writeLine(file, obj) {
51
73
  const buf = Buffer.from(JSON.stringify(obj) + '\n', 'utf8');
52
74
  if (buf.length > MAX_LINE_BYTES) throw new Error(`work item line too large (${buf.length} bytes, max ${MAX_LINE_BYTES})`);
53
75
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -136,7 +158,7 @@ function foldedLedger(file) {
136
158
  // Time zero, so the lease pass inside the fold expires nothing: a lease is
137
159
  // only ever lapsed against the clock of the call asking, and that is `aged`.
138
160
  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
161
+ // One entry per ledger, so a real store has a few dozen. The cap is for the
140
162
  // long-lived MCP server, which walks temporary project dirs that never come
141
163
  // back; a Map iterates in insertion order, so the oldest key goes first.
142
164
  if (folded.size >= MAX_FOLDS_HELD) folded.delete(folded.keys().next().value);
@@ -167,6 +189,48 @@ export function readWorkItems(projectDir, now = Date.now()) {
167
189
  return out;
168
190
  }
169
191
 
192
+ /**
193
+ * THE FINISHED THREADS THE 8 MB READ CUT OFF, read only when somebody asks.
194
+ *
195
+ * `readLines` keeps a ledger's trailing 8 MB, so a thread whose lines all came
196
+ * before that is in no fold, and one whose first lines did is folded without
197
+ * them (a blank title, the wrong `createdAt`). Measured 2026-10-07
198
+ * (w-fda2165ec6): 155 finished threads gone and 21 blank on one 12.8 MB
199
+ * ledger. Her call was that these load as she scrolls rather than up front, so
200
+ * the snapshot keeps its window and this reads the whole file, once per
201
+ * change of the file, for the Done and All tabs' next page.
202
+ *
203
+ * Finished threads only: this is history, and it is where the tabs that ask
204
+ * for it go. A ledger that fits in the window has nothing to give.
205
+ */
206
+ const wholeFolds = new Map();
207
+ export function readOlderWorkItems(projectDir, now = Date.now()) {
208
+ const file = ledgerPath(projectDir);
209
+ let size;
210
+ try { size = fs.statSync(file).size; } catch { return []; }
211
+ if (size <= MAX_LEDGER_BYTES) return [];
212
+ const stamp = ledgerStamp(file);
213
+ let whole = wholeFolds.get(file);
214
+ if (!whole || whole.stamp !== stamp) {
215
+ let lines;
216
+ try { lines = fs.readFileSync(file, 'utf8').split('\n').filter((l) => l && Buffer.byteLength(l, 'utf8') <= MAX_LINE_BYTES); } catch { return []; }
217
+ // Few ledgers are ever this big; hold the latest fold of each, no more.
218
+ if (wholeFolds.size >= 8) wholeFolds.delete(wholeFolds.keys().next().value);
219
+ whole = { stamp, items: foldWorkItems(lines, 0) };
220
+ wholeFolds.set(file, whole);
221
+ }
222
+ const windowed = foldedLedger(file);
223
+ const out = [];
224
+ for (const item of whole.items.values()) {
225
+ if (item.status !== 'done') continue;
226
+ const seen = windowed.get(item.id);
227
+ // In the window whole: the snapshot already has it right.
228
+ if (seen && seen.createdAt === item.createdAt) continue;
229
+ out.push(aged(item, now));
230
+ }
231
+ return out;
232
+ }
233
+
170
234
  /** One work item, or null. */
171
235
  export function readWorkItem(projectDir, id, now = Date.now()) {
172
236
  if (!isWorkItemId(id)) return null;
@@ -261,10 +325,10 @@ export async function claimWorkItem(projectDir, { id = null, filter = null, hold
261
325
  // A LIVE CLAIM OF YOUR OWN IS NOT SOMEBODY ELSE'S. The holder was never
262
326
  // compared here, so a session asking for the row it already holds was
263
327
  // 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.
328
+ // only way through was to sit out its own five minute lease: after a
329
+ // reply, the session was refused by its own claim, waited out the lease,
330
+ // and the answer the user was waiting for sat in a parked file.
331
+ // Re-claiming what you hold is a renewal.
268
332
  if (item.claim && !item.claimExpired && item.claim.holder !== holder) {
269
333
  return { claimed: false, reason: 'held by another worker', heldBy: item.claim.holder };
270
334
  }
@@ -295,5 +359,85 @@ export function releaseWorkItem(projectDir, id, { epoch, now = Date.now() } = {}
295
359
  return readWorkItem(projectDir, id, now);
296
360
  }
297
361
 
362
+ // Release only the uninterrupted claim acquired by this run. A tool server
363
+ // may outlive its turn, so process lifetime is not run lifetime. The first
364
+ // claim after the spawn snapshot identifies the holder; same-holder renewals
365
+ // are allowed, but a release or another holder ends this run's ownership.
366
+ export function releaseRunClaim(projectDir, id, { afterEpoch, startedAt, now = Date.now() } = {}) {
367
+ if (!Number.isFinite(afterEpoch) || !Number.isFinite(startedAt)) return null;
368
+ const lines = readLines(ledgerPath(projectDir)).map(normalizeLine).filter(line => line?.id === id);
369
+ const item = foldWorkItems(lines, now).get(id);
370
+ if (!item?.claim) return item;
371
+ const claims = lines.filter(line => line.claim && !line.heartbeat && line.epoch > afterEpoch);
372
+ const first = claims[0];
373
+ if (!first || first.epoch !== afterEpoch + 1 || first.ts < startedAt) return item;
374
+ if (claims.some(line => line.claim.holder !== first.claim.holder)) return item;
375
+ if (lines.some(line => line.release && line.epoch >= first.epoch)) return item;
376
+ if (item.claim.holder !== first.claim.holder) return item;
377
+ // A concurrent new claim fences this append out through its newer epoch.
378
+ return releaseWorkItem(projectDir, id, { epoch: item.epoch, now });
379
+ }
380
+
381
+ /* ------------------------------ the team sync ----------------------------- */
382
+ // A TEAMMATE'S LINES, PULLED FROM THE CLOUD, ENTER THE LEDGER HERE AND ONLY
383
+ // HERE, which keeps this file the one write path even with several writers on
384
+ // several Macs. Each line goes in verbatim, still its author's, and a line
385
+ // whose uid the ledger already holds is skipped, so pulling the same page twice
386
+ // stores it once. A line with no uid or no writer is refused: nothing could tell
387
+ // it apart on the next pull. So is one too big to write, which used to throw
388
+ // and leave the pull cursor in front of it forever (review, 2026-10-01): a bad
389
+ // line is skipped and the pull moves past it. Returns how many were appended.
390
+ export function appendForeignLines(projectDir, lines) {
391
+ const file = ledgerPath(projectDir);
392
+ const have = new Set();
393
+ for (const text of readLines(file)) {
394
+ const uid = /"uid":"([^"]+)"/.exec(text)?.[1];
395
+ if (uid) have.add(uid);
396
+ }
397
+ let added = 0;
398
+ for (const raw of Array.isArray(lines) ? lines : []) {
399
+ const line = normalizeLine(raw);
400
+ if (!line || !line.uid || !line.by || have.has(line.uid)) continue;
401
+ if (Buffer.byteLength(JSON.stringify(raw) + '\n', 'utf8') > MAX_LINE_BYTES) continue;
402
+ writeLine(file, raw);
403
+ have.add(line.uid);
404
+ added += 1;
405
+ }
406
+ return added;
407
+ }
408
+
409
+ // Every line after a byte offset, as written, and where the file ends. The
410
+ // ledger only grows, so an offset is a cursor: sync keeps the one it pushed to
411
+ // and asks from there next time.
412
+ export function readLinesFrom(projectDir, offset = 0) {
413
+ const file = ledgerPath(projectDir);
414
+ let size;
415
+ try { size = fs.statSync(file).size; } catch (err) {
416
+ if (err.code === 'ENOENT') return { lines: [], end: 0 };
417
+ throw err;
418
+ }
419
+ const start = Math.max(0, Math.min(Number(offset) || 0, size));
420
+ if (start >= size) return { lines: [], end: size };
421
+ const fd = fs.openSync(file, 'r');
422
+ let text;
423
+ try {
424
+ const buf = Buffer.allocUnsafe(size - start);
425
+ const read = fs.readSync(fd, buf, 0, size - start, start);
426
+ text = buf.toString('utf8', 0, read);
427
+ } finally {
428
+ fs.closeSync(fd);
429
+ }
430
+ // Stop at the last whole line: a line still being written is read next time.
431
+ const lastNewline = text.lastIndexOf('\n');
432
+ if (lastNewline < 0) return { lines: [], end: start };
433
+ const whole = text.slice(0, lastNewline + 1);
434
+ const lines = [];
435
+ for (const piece of whole.split('\n')) {
436
+ if (!piece || Buffer.byteLength(piece, 'utf8') > MAX_LINE_BYTES) continue;
437
+ try { lines.push(JSON.parse(piece)); } catch { /* a torn line reads as nothing, as in the fold */ }
438
+ }
439
+ return { lines, end: start + Buffer.byteLength(whole, 'utf8') };
440
+ }
441
+
298
442
  export { LEASE_MS };
299
443
  export const _internals = { appendLine, readLines, ledgerPath, forgetFolds: () => folded.clear() };