agentbox-app 0.1.7 → 0.1.11

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 (244) hide show
  1. package/README.md +209 -55
  2. package/bin/agentbox.mjs +6 -0
  3. package/briefs/message-rules.md +13 -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 +9 -10
  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 +114 -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/artifact-path.mjs +11 -0
  16. package/main/claude-bin.mjs +31 -5
  17. package/main/claude-input.mjs +45 -3
  18. package/main/claude-plan.mjs +11 -11
  19. package/main/code-change.mjs +3 -3
  20. package/main/codex-account.mjs +1 -1
  21. package/main/codex-bin.mjs +17 -0
  22. package/main/codex-memory-gate.mjs +260 -0
  23. package/main/codex-models.mjs +12 -7
  24. package/main/codex-threads.mjs +46 -23
  25. package/main/codex-usage-file.mjs +7 -7
  26. package/main/codex-usage.mjs +4 -4
  27. package/main/codex-watch.mjs +12 -12
  28. package/main/config.mjs +68 -8
  29. package/main/dev-window.mjs +9 -9
  30. package/main/doc-scheme.mjs +2 -0
  31. package/main/engine-setup.mjs +193 -0
  32. package/main/feedback.mjs +55 -0
  33. package/main/folder-pictures.mjs +48 -0
  34. package/main/frame-scrollbars.mjs +90 -0
  35. package/main/fresh-user.mjs +6 -3
  36. package/main/git-change-offthread.mjs +70 -0
  37. package/main/git-change-thread.mjs +11 -0
  38. package/main/git-change.mjs +1 -1
  39. package/main/img-scheme.mjs +70 -1
  40. package/main/instruction-settings.mjs +92 -54
  41. package/main/ipc.mjs +316 -42
  42. package/main/leftovers.mjs +518 -0
  43. package/main/live-replies.mjs +43 -5
  44. package/main/machine.mjs +47 -0
  45. package/main/main.mjs +119 -15
  46. package/main/memory-gate-history.mjs +254 -0
  47. package/main/memory-gate-server.mjs +461 -0
  48. package/main/memory-gate.mjs +206 -0
  49. package/main/message-priority.mjs +87 -0
  50. package/main/rail-note.mjs +2 -2
  51. package/main/refresh-agent-models.mjs +20 -0
  52. package/main/repeats.mjs +4 -4
  53. package/main/row-label.mjs +114 -51
  54. package/main/serve.mjs +67 -5
  55. package/main/settings.mjs +223 -26
  56. package/main/ship-queue.mjs +279 -0
  57. package/main/sign-in-files.mjs +39 -0
  58. package/main/source-updater.mjs +298 -0
  59. package/main/store/home.mjs +47 -2
  60. package/main/store/work-items.mjs +150 -6
  61. package/main/store.mjs +235 -9
  62. package/main/supervisor.mjs +1444 -406
  63. package/main/task-commands.mjs +8 -10
  64. package/main/task-folders.mjs +74 -13
  65. package/main/task-remote-control.mjs +3 -3
  66. package/main/task-terminals.mjs +2 -1
  67. package/main/team/index.mjs +440 -0
  68. package/main/team/memory-cloud.mjs +247 -0
  69. package/main/team/projects.mjs +162 -0
  70. package/main/team/session.mjs +225 -0
  71. package/main/team/sign-in.mjs +106 -0
  72. package/main/team/supabase-backend.mjs +231 -0
  73. package/main/team/sync.mjs +163 -0
  74. package/main/updater.mjs +15 -5
  75. package/main/write-before-reload.mjs +50 -0
  76. package/mcp/core/page.mjs +9 -11
  77. package/mcp/core/work.mjs +91 -25
  78. package/mcp/tools.mjs +12 -6
  79. package/package.json +21 -9
  80. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  81. package/renderer/dist/assets/index-B-2Az952.css +32 -0
  82. package/renderer/dist/assets/index-D6qncgai.js +1168 -0
  83. package/renderer/dist/index.html +2 -2
  84. package/scripts/memory-gate-hook.sh +59 -0
  85. package/shared/agent-import.mjs +38 -15
  86. package/shared/agents.d.mts +26 -8
  87. package/shared/agents.mjs +100 -108
  88. package/shared/analytics-events.mjs +19 -2
  89. package/shared/answers.mjs +12 -0
  90. package/shared/bridge-map.d.mts +12 -0
  91. package/shared/bridge-map.mjs +37 -1
  92. package/shared/claude-commands.generated.mjs +2 -2
  93. package/shared/claude-models.generated.mjs +4 -4
  94. package/shared/claude-usage.mjs +6 -6
  95. package/shared/codex-import.mjs +7 -7
  96. package/shared/codex-modes.d.mts +50 -0
  97. package/shared/codex-usage.mjs +7 -8
  98. package/shared/demo-world.mjs +5 -5
  99. package/shared/engines.mjs +27 -10
  100. package/shared/feedback.d.mts +14 -0
  101. package/shared/feedback.mjs +44 -0
  102. package/shared/first-run-examples.mjs +4 -4
  103. package/shared/first-run-practice.d.mts +16 -0
  104. package/shared/first-run-practice.mjs +176 -70
  105. package/shared/fresh-user-home.mjs +22 -3
  106. package/shared/instruction-defaults.json +2 -2
  107. package/shared/product-name.mjs +4 -2
  108. package/shared/project-folder-check.mjs +6 -0
  109. package/shared/rank.d.mts +1 -0
  110. package/shared/rank.mjs +22 -0
  111. package/shared/referenced-files.mjs +1 -1
  112. package/shared/schemes.d.mts +11 -0
  113. package/shared/schemes.mjs +1 -1
  114. package/shared/shortcuts.d.mts +41 -0
  115. package/shared/shortcuts.mjs +236 -0
  116. package/shared/spawn-trouble.d.mts +2 -0
  117. package/shared/spawn-trouble.mjs +39 -18
  118. package/shared/team-rules.d.mts +17 -0
  119. package/shared/team-rules.mjs +230 -0
  120. package/shared/team-status.d.mts +11 -0
  121. package/shared/team-status.mjs +90 -0
  122. package/shared/terminal-links.d.mts +2 -0
  123. package/shared/terminal-links.mjs +55 -0
  124. package/shared/terminal-state.d.mts +1 -0
  125. package/shared/terminal-state.mjs +16 -0
  126. package/shared/thread-cards.d.mts +19 -0
  127. package/shared/thread-cards.mjs +254 -0
  128. package/shared/usage.d.mts +1 -1
  129. package/shared/usage.mjs +6 -7
  130. package/shared/work-items.mjs +255 -27
  131. package/shared/work-lines.mjs +393 -16
  132. package/worker-permissions.json +1 -1
  133. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  134. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  135. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  136. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  137. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  138. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  139. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  140. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  141. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  142. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  143. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  144. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  145. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  146. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  147. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  148. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  149. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  150. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  151. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  152. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  153. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  154. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  155. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  156. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  157. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  158. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  159. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  160. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  161. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  162. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  163. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  164. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  165. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  166. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  167. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  168. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  169. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  170. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  171. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  172. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  173. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  174. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  175. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  176. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  177. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  178. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  179. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  180. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  181. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  182. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  183. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  184. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  185. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  186. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  187. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  188. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  189. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  190. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  191. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  192. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  193. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  194. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  195. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  196. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  197. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  198. package/renderer/dist/emulator/data/compression/README.md +0 -11
  199. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  200. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  201. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  202. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  203. package/renderer/dist/emulator/data/cores/README.md +0 -22
  204. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  205. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  206. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  207. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  208. package/renderer/dist/emulator/data/cores/package.json +0 -65
  209. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  210. package/renderer/dist/emulator/data/emulator.css +0 -1614
  211. package/renderer/dist/emulator/data/loader.js +0 -170
  212. package/renderer/dist/emulator/data/localization/README.md +0 -63
  213. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  214. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  215. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  216. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  217. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  218. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  219. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  220. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  221. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  222. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  223. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  224. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  225. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  226. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  227. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  228. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  229. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  230. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  231. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  232. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  233. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  234. package/renderer/dist/emulator/data/src/compression.js +0 -142
  235. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  236. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  237. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  238. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  239. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  240. package/renderer/dist/emulator/data/src/storage.js +0 -125
  241. package/renderer/dist/emulator/data/version.json +0 -5
  242. package/renderer/dist/emulator/host.js +0 -299
  243. package/renderer/dist/emulator/index.html +0 -28
  244. package/renderer/dist/emulator/provenance.json +0 -6
@@ -0,0 +1,39 @@
1
+ // THE FILES A LOGIN WRITES, SO THE APP CAN SEE ONE LAND.
2
+ //
3
+ // A signed-out account used to sit out a fixed half hour, and nothing but a
4
+ // session surviving on it could bring it back, which no session could do while
5
+ // it sat out. So typing /login fixed nothing a person could see: their agents
6
+ // stayed "Queued" until the half hour ran out on its own, and a /login and any
7
+ // "continue" replies in between did nothing.
8
+ //
9
+ // A login rewrites these. Only their modification times are read, never their
10
+ // contents: what is in them is her credentials and is none of our business.
11
+ // A moved time is a reason to TRY again, not proof; the try is the proof.
12
+
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+
16
+ /**
17
+ * Where one account's login is written.
18
+ * - Claude Code, its default login: `~/.claude.json` (where the CLI keeps the
19
+ * account it is signed in as) and the same files inside `~/.claude`.
20
+ * - Claude Code, a CLAUDE_CONFIG_DIR: those files inside that folder.
21
+ * - Codex: `auth.json` inside its CODEX_HOME.
22
+ */
23
+ export function signInFiles({ engine = 'claude', folder = null, home }) {
24
+ if (engine === 'codex') return folder ? [path.join(folder, 'auth.json')] : [];
25
+ if (!folder) {
26
+ const dir = path.join(home, '.claude');
27
+ return [path.join(home, '.claude.json'), path.join(dir, '.claude.json'), path.join(dir, '.credentials.json')];
28
+ }
29
+ return [path.join(folder, '.claude.json'), path.join(folder, '.credentials.json')];
30
+ }
31
+
32
+ /** The newest modification time among them, or 0 when none can be read. */
33
+ export function signInStamp(files, stat = fs.statSync) {
34
+ let newest = 0;
35
+ for (const f of files ?? []) {
36
+ try { newest = Math.max(newest, stat(f).mtimeMs); } catch {}
37
+ }
38
+ return newest;
39
+ }
@@ -0,0 +1,298 @@
1
+ // A COPY RUN FROM SOURCE KEEPS ITSELF CURRENT TOO (w-7a39dace23, 2026-10-01).
2
+ //
3
+ // Teammates run the team build from a git checkout (docs/team/PLAN.md), and
4
+ // main/updater.mjs cannot help them: electron-updater reads published releases
5
+ // and does nothing at all unpackaged. Their updates are pushes to main, several
6
+ // a day, so this watches the branch instead of a release feed.
7
+ //
8
+ // WHAT IT DOES. A few seconds after launch, and then every half hour, it runs
9
+ // `git fetch` and asks whether the branch's upstream is ahead of the code this
10
+ // process booted from. If it is, the state goes to `ready` and the sidebar
11
+ // shows the "New version ready" card (renderer/src/components/SidebarUpdate.tsx),
12
+ // which names what changed when pointed at.
13
+ // Pressing Restart fast-forwards the folder, reinstalls packages only when
14
+ // package.json or the lockfile changed, rebuilds the screens, and relaunches.
15
+ // Nothing happens until that button is pressed.
16
+ //
17
+ // IT NEVER TOUCHES WORK SOMEBODY HAS NOT PUSHED. Four cases get no offer at
18
+ // all, each stated in `error` so Settings can say why: a folder that is not a
19
+ // checkout, a branch that follows nothing, tracked files with edits in them,
20
+ // and a branch with commits of its own. The founder's own app runs from a
21
+ // local branch with no upstream, so it is untouched by this. Untracked files
22
+ // (zero.config.json, cloud/team.config.json) do not count as edits: they are
23
+ // ignored or new, and a fast-forward cannot overwrite them without failing.
24
+ //
25
+ // "AHEAD OF WHAT IS RUNNING", NOT "AHEAD OF THE FOLDER". The code in memory is
26
+ // what booted, so the folder brought up to date by hand without a restart is
27
+ // still an update waiting, and the row says so.
28
+ //
29
+ // IF THE BUILD FAILS IT DOES NOT RESTART. The row comes back with what went
30
+ // wrong, and pressing Restart again retries from where it stopped. Restarting
31
+ // onto screens that did not build would be the one outcome worse than waiting.
32
+
33
+ import { execFile, execFileSync, spawn } from 'node:child_process';
34
+ import fs from 'node:fs';
35
+ import { describe } from './updater.mjs';
36
+
37
+ const FIRST_CHECK_MS = 20_000;
38
+ const EVERY_MS = 30 * 60 * 1000;
39
+ const FETCH_MS = 60_000;
40
+ const INSTALL_MS = 10 * 60 * 1000;
41
+ const BUILD_MS = 5 * 60 * 1000;
42
+ // How many change titles the row lists. The rest are counted, not named.
43
+ const LISTED = 5;
44
+
45
+ const SAY = {
46
+ notRepo: 'This folder is not a git checkout, so it does not update itself.',
47
+ noUpstream: 'This copy\'s branch does not follow a branch on GitHub, so it does not update itself.',
48
+ dirty: 'This folder has changes of your own in it, so it does not update itself.',
49
+ diverged: 'This copy has commits of its own, so it does not update itself.',
50
+ offline: 'Could not reach GitHub.',
51
+ };
52
+
53
+ // A FILE THE BUILD WRITES IS NOT AN EDIT OF HERS (2026-10-02). `npm run build`
54
+ // rewrites shared/*.generated.mjs with the version and date of the Claude Code
55
+ // on this Mac, and the restart below runs that build. Counted as her changes,
56
+ // they made every update after the first say "changes of your own" for good
57
+ // (tests/a-build-does-not-stop-the-next-update.test.mjs).
58
+ const isBuildOutput = (file) => /^shared\/[^/]+\.generated\.[cm]?[jt]s$/.test(file);
59
+ // The status code is one or two letters and `gitIn` trims the output, which
60
+ // takes the leading space off the first line, so it is matched, not sliced.
61
+ const changedPaths = (porcelain) => String(porcelain ?? '').split('\n').filter(Boolean).map((l) => l.replace(/^[ MADRCUT?!]{1,2} /, '').replace(/^"|"$/g, ''));
62
+ const theirEdits = (porcelain) => changedPaths(porcelain).filter((f) => !isBuildOutput(f));
63
+
64
+ function gitIn(appDir) {
65
+ return (args, { timeout = 30_000 } = {}) => new Promise((resolve) => {
66
+ execFile('git', args, {
67
+ cwd: appDir,
68
+ timeout,
69
+ // Never a password prompt from a background check. The repository is
70
+ // public; anything that asks for credentials is a failure to report.
71
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
72
+ maxBuffer: 4 * 1024 * 1024,
73
+ }, (err, stdout, stderr) => {
74
+ resolve({ code: err ? (typeof err.code === 'number' ? err.code : 1) : 0, out: String(stdout).trim(), err: String(stderr).trim() });
75
+ });
76
+ });
77
+ }
78
+
79
+ function collect(bin, args, { cwd, timeout }) {
80
+ return new Promise((resolve) => {
81
+ let out = '';
82
+ let child;
83
+ try {
84
+ child = spawn(bin, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
85
+ } catch (e) {
86
+ resolve({ code: 1, out: String(e?.message ?? e) });
87
+ return;
88
+ }
89
+ const keep = (b) => { out = (out + b).slice(-64 * 1024); };
90
+ child.stdout.on('data', keep);
91
+ child.stderr.on('data', keep);
92
+ const timer = setTimeout(() => { keep('\nerror: timed out'); child.kill('SIGTERM'); }, timeout);
93
+ child.on('error', (e) => { keep(`\nerror: ${e.message}`); });
94
+ child.on('close', (code) => { clearTimeout(timer); resolve({ code: code ?? 1, out }); });
95
+ });
96
+ }
97
+
98
+ // WHICH npm. Started with `npm start`, the process carries npm's own path in
99
+ // its environment, so that one is used and nvm or a version manager never
100
+ // matters. Started any other way, her login shell is asked, the way
101
+ // main/claude-bin.mjs finds Claude Code. The arguments are fixed strings from
102
+ // this file, never anything read from disk.
103
+ export function defaultNpm(appDir, env = process.env) {
104
+ return (args) => {
105
+ const timeout = args[0] === 'install' ? INSTALL_MS : BUILD_MS;
106
+ const node = env.npm_node_execpath;
107
+ const cli = env.npm_execpath;
108
+ if (node && cli && fs.existsSync(node) && fs.existsSync(cli)) {
109
+ return collect(node, [cli, ...args], { cwd: appDir, timeout });
110
+ }
111
+ const shell = env.SHELL || '/bin/zsh';
112
+ return collect(shell, ['-lic', `npm ${args.join(' ')}`], { cwd: appDir, timeout });
113
+ };
114
+ }
115
+
116
+ // The line of a failed build worth showing a person: the first that says
117
+ // error, else the last thing it printed.
118
+ export function failureLine(out) {
119
+ const lines = String(out ?? '').split('\n').map((l) => l.trim()).filter(Boolean);
120
+ const named = lines.find((l) => /error/i.test(l));
121
+ const line = named ?? lines[lines.length - 1] ?? 'it stopped without saying why';
122
+ return line.length > 160 ? `${line.slice(0, 157)}…` : line;
123
+ }
124
+
125
+ export function createSourceUpdater({
126
+ appDir,
127
+ app = null,
128
+ log = console,
129
+ onChanged = () => {},
130
+ npm = defaultNpm(appDir),
131
+ relaunch = () => { app?.relaunch(); app?.quit(); },
132
+ } = {}) {
133
+ const git = gitIn(appDir);
134
+ // The commit this process booted from. Read once, now, because it is the
135
+ // code in memory and nothing on disk can change that.
136
+ let bootHead = null;
137
+ try {
138
+ bootHead = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: appDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
139
+ } catch { /* not a checkout; check() says so */ }
140
+
141
+ let state = {
142
+ phase: 'idle',
143
+ source: true,
144
+ currentVersion: bootHead ? bootHead.slice(0, 7) : null,
145
+ newVersion: null,
146
+ percent: null,
147
+ error: null,
148
+ checkedAt: null,
149
+ readyAt: null,
150
+ changes: [],
151
+ behind: null,
152
+ };
153
+ let timer = null;
154
+ let stopped = false;
155
+ let pending = Promise.resolve();
156
+
157
+ const set = (next) => {
158
+ state = { ...state, ...next };
159
+ try { onChanged(describe(state)); } catch {}
160
+ };
161
+
162
+ // Everything that has to be true before an update is offered or applied.
163
+ // Returns the upstream commit, or the sentence saying why not.
164
+ async function preconditions() {
165
+ if (!bootHead) return { why: SAY.notRepo };
166
+ const upstream = await git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
167
+ if (upstream.code !== 0) return { why: SAY.noUpstream };
168
+ const dirty = await git(['status', '--porcelain', '--untracked-files=no']);
169
+ if (dirty.code !== 0 || theirEdits(dirty.out).length) return { why: SAY.dirty };
170
+ const ahead = await git(['merge-base', '--is-ancestor', 'HEAD', '@{u}']);
171
+ if (ahead.code !== 0) return { why: SAY.diverged };
172
+ const up = await git(['rev-parse', '@{u}']);
173
+ if (up.code !== 0 || !up.out) return { why: SAY.noUpstream };
174
+ return { up: up.out };
175
+ }
176
+
177
+ async function check() {
178
+ if (stopped) return describe(state);
179
+ if (state.phase === 'checking' || state.phase === 'installing') return describe(state);
180
+ if (!bootHead) {
181
+ set({ phase: 'unsupported', error: SAY.notRepo, checkedAt: Date.now() });
182
+ return describe(state);
183
+ }
184
+ const was = state;
185
+ set({ phase: 'checking' });
186
+ // A failed fetch is not the end of the check: what was fetched before is
187
+ // still on disk, and an update already found stays offered on a plane.
188
+ const fetched = await git(['fetch', '--quiet'], { timeout: FETCH_MS });
189
+ if (fetched.code !== 0) log.warn?.('[source-updater] fetch failed:', fetched.err.split('\n')[0]);
190
+ const ok = await preconditions();
191
+ if (ok.why) {
192
+ set({ phase: 'unsupported', error: ok.why, newVersion: null, changes: [], behind: null, readyAt: null, checkedAt: Date.now() });
193
+ return describe(state);
194
+ }
195
+ if (ok.up === bootHead) {
196
+ set(fetched.code === 0
197
+ ? { phase: 'current', error: null, newVersion: null, changes: [], behind: null, readyAt: null, checkedAt: Date.now() }
198
+ : { phase: 'error', error: SAY.offline, checkedAt: Date.now() });
199
+ return describe(state);
200
+ }
201
+ const range = `${bootHead}..${ok.up}`;
202
+ const titles = await git(['log', '--no-merges', '--format=%s', `-n${LISTED}`, range]);
203
+ const count = await git(['rev-list', '--no-merges', '--count', range]);
204
+ const newVersion = ok.up.slice(0, 7);
205
+ set({
206
+ phase: 'ready',
207
+ newVersion,
208
+ changes: titles.code === 0 && titles.out ? titles.out.split('\n') : [],
209
+ behind: count.code === 0 ? Number(count.out) || 0 : null,
210
+ // Stamped when THIS version was first seen, so a recheck does not make
211
+ // old news young again, and a newer push does.
212
+ readyAt: was.phase === 'ready' && was.newVersion === newVersion && was.readyAt ? was.readyAt : Date.now(),
213
+ // A failed earlier restart keeps saying so until the next press.
214
+ error: was.phase === 'ready' && was.newVersion === newVersion ? was.error : null,
215
+ checkedAt: Date.now(),
216
+ });
217
+ return describe(state);
218
+ }
219
+
220
+ async function apply() {
221
+ const ok = await preconditions();
222
+ if (ok.why) {
223
+ set({ phase: 'unsupported', error: ok.why, newVersion: null, changes: [], behind: null, readyAt: null });
224
+ return;
225
+ }
226
+ const head = await git(['rev-parse', 'HEAD']);
227
+ // What the last build stamped goes back first, so the fast-forward can
228
+ // never trip on it. The build below writes it again.
229
+ const stamped = await git(['status', '--porcelain', '--untracked-files=no']);
230
+ const buildOutput = stamped.code === 0 ? changedPaths(stamped.out).filter(isBuildOutput) : [];
231
+ if (buildOutput.length) await git(['checkout', '--', ...buildOutput]);
232
+ if (head.out !== ok.up) {
233
+ const merged = await git(['merge', '--ff-only', '--quiet', '@{u}']);
234
+ if (merged.code !== 0) {
235
+ set({ phase: 'ready', error: `Could not bring the new code in: ${failureLine(merged.err || merged.out)}` });
236
+ return;
237
+ }
238
+ }
239
+ // Packages changed when either file's object id moved. Compared by id
240
+ // rather than by asking git for a diff, which this process does in exactly
241
+ // one file (tests/the-old-git-diff-screen-stays-gone.test.mjs).
242
+ let depsMoved = false;
243
+ for (const file of ['package.json', 'package-lock.json']) {
244
+ const was = await git(['rev-parse', '--verify', '--quiet', `${bootHead}:${file}`]);
245
+ const now = await git(['rev-parse', '--verify', '--quiet', `HEAD:${file}`]);
246
+ if (was.out !== now.out) depsMoved = true;
247
+ }
248
+ if (depsMoved) {
249
+ const installed = await npm(['install', '--no-audit', '--no-fund']);
250
+ if (installed.code !== 0) {
251
+ set({ phase: 'ready', error: `The new packages would not install: ${failureLine(installed.out)}` });
252
+ return;
253
+ }
254
+ }
255
+ const built = await npm(['run', 'build']);
256
+ if (built.code !== 0) {
257
+ set({ phase: 'ready', error: `The new code would not build: ${failureLine(built.out)}` });
258
+ return;
259
+ }
260
+ log.info?.('[source-updater] built', state.newVersion, '- relaunching');
261
+ relaunch();
262
+ }
263
+
264
+ // THE BUTTON. Returns at once; the work takes about a minute and the row
265
+ // says "Updating" while it runs.
266
+ function install() {
267
+ if (stopped || state.phase !== 'ready') return false;
268
+ set({ phase: 'installing', error: null });
269
+ pending = apply().catch((e) => {
270
+ set({ phase: 'ready', error: `The update stopped: ${failureLine(e?.message ?? e)}` });
271
+ });
272
+ return true;
273
+ }
274
+
275
+ function start() {
276
+ if (stopped || timer) return;
277
+ const tick = () => { void check(); };
278
+ setTimeout(tick, FIRST_CHECK_MS).unref?.();
279
+ timer = setInterval(tick, EVERY_MS);
280
+ timer.unref?.();
281
+ }
282
+
283
+ function stop() {
284
+ stopped = true;
285
+ if (timer) clearInterval(timer);
286
+ timer = null;
287
+ }
288
+
289
+ return {
290
+ start,
291
+ stop,
292
+ check: () => check(),
293
+ install,
294
+ state: () => describe(state),
295
+ // For tests: resolves when a started install has finished, either way.
296
+ settled: () => pending,
297
+ };
298
+ }
@@ -88,8 +88,8 @@ export function appHome(homeDir = os.homedir()) {
88
88
  }
89
89
 
90
90
  // 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
91
+ // `/Users/you/Zero/accounts/00000.../agentbox` becomes
92
+ // `-Users-you-Zero-accounts-00000...-agentbox`. Every character that is not
93
93
  // a letter or a digit becomes a dash, which is readable at a glance and safe on
94
94
  // every filesystem.
95
95
  export function encodeProjectPath(dir) {
@@ -303,6 +303,51 @@ export function machineryPath(projectDir, rel, home = appHome()) {
303
303
  return target;
304
304
  }
305
305
 
306
+ /**
307
+ * RECORDS A COPY WROTE UNDER THE WRONG HOME, CARRIED TO ITS OWN.
308
+ *
309
+ * The headless door (main/serve.mjs) used to open the store without pointing
310
+ * the app's home at `storeRoot`, so it wrote its ledgers under whatever home it
311
+ * inherited: `~/.agentbox` for `npx agentbox-app`, and on the test Mac
312
+ * 2026-10-01 the founder's own store, because the copies were started from an
313
+ * agent session. Its workers were handed `storeRoot`, found nothing there, and
314
+ * answered "no work item" to every store call. Pointing the home at `storeRoot`
315
+ * fixes the next write; this is what keeps the rows already written from
316
+ * vanishing on the first boot after the fix.
317
+ *
318
+ * ONLY WHAT IS PROVABLY THIS STORE'S MOVES. A machinery directory names the
319
+ * product it belongs to in `.origin`; one is carried only when that product is
320
+ * inside `storeRoot`. Everything else under `fromHome` is somebody else's
321
+ * store and is not touched. Each record goes through `foldStray`, so a ledger
322
+ * is appended rather than overwritten and nothing is ever destroyed; the old
323
+ * directory is removed only once nothing but its marker is left in it.
324
+ *
325
+ * Returns the product folders whose records were carried.
326
+ */
327
+ export function carryMisplacedMachinery(fromHome, storeRoot, toHome = storeRoot) {
328
+ if (!fromHome || !storeRoot) return [];
329
+ const from = path.resolve(fromHome);
330
+ if (from === path.resolve(toHome)) return [];
331
+ const inside = path.resolve(storeRoot) + path.sep;
332
+ let names = [];
333
+ try { names = fs.readdirSync(path.join(from, 'projects')); } catch { return []; }
334
+ const carried = [];
335
+ for (const name of names) {
336
+ const dir = path.join(from, 'projects', name);
337
+ const origin = readOrigin(dir);
338
+ if (!origin || !path.resolve(origin).startsWith(inside)) continue;
339
+ const target = ensureMachineryDir(origin, toHome);
340
+ for (const rel of MACHINERY) foldStray(path.join(dir, rel), path.join(target, rel), rel);
341
+ let left = [];
342
+ try { left = fs.readdirSync(dir); } catch { /* gone already */ }
343
+ if (left.length === 1 && left[0] === ORIGIN) {
344
+ try { fs.rmSync(path.join(dir, ORIGIN)); fs.rmdirSync(dir); } catch { /* best effort */ }
345
+ }
346
+ carried.push(origin);
347
+ }
348
+ return carried;
349
+ }
350
+
306
351
  /**
307
352
  * EVERY RECORD OF OURS FOR ONE PROJECT, GONE.
308
353
  *
@@ -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() };