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