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.
- package/README.md +220 -58
- package/bin/agentbox.mjs +18 -3
- package/briefs/message-rules.md +24 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +11 -11
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +116 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/approvals.mjs +14 -5
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +32 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +25 -13
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +18 -0
- package/main/codex-launch-env.mjs +31 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +82 -17
- package/main/demo.mjs +17 -14
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +204 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +7 -4
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +9 -3
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +134 -60
- package/main/ipc.mjs +335 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +48 -1
- package/main/main.mjs +138 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/notify.mjs +1 -1
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +21 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +115 -51
- package/main/runs-on.mjs +93 -0
- package/main/serve.mjs +77 -5
- package/main/settings.mjs +245 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +84 -7
- package/main/store/project.mjs +15 -19
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +245 -13
- package/main/supervisor.mjs +1634 -417
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +94 -16
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +14 -2
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +10 -12
- package/mcp/core/work.mjs +123 -26
- package/mcp/tools.mjs +13 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
- package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +7 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +39 -16
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +38 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +6 -6
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-modes.mjs +1 -1
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +6 -6
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +34 -4
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +9 -3
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/runs-on.d.mts +20 -0
- package/shared/runs-on.mjs +79 -0
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +40 -19
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +258 -28
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
package/main/store/home.mjs
CHANGED
|
@@ -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.
|
|
78
|
-
|
|
79
|
-
|
|
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/
|
|
92
|
-
// `-Users-you-Zero-accounts-
|
|
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
|
*
|
package/main/store/project.mjs
CHANGED
|
@@ -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 {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
|
22
|
-
//
|
|
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
|
|
28
|
-
//
|
|
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') ||
|
|
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
|
|
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
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
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() };
|