agentbox-app 0.1.13 → 0.1.16
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 +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// The two files behind teammates, per account: the teammates themselves and
|
|
2
|
+
// the one team chat. Append-only JSON lines, read whole and folded, the way
|
|
3
|
+
// repeats.jsonl is. The app and the store tools both write here, so this is
|
|
4
|
+
// the only code that knows where they live.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { foldTeammates, foldChat, foldPlaybooks } from '../../shared/teammates.mjs';
|
|
9
|
+
|
|
10
|
+
const MAX_READ = 4 * 1024 * 1024;
|
|
11
|
+
const MAX_LINE = 64 * 1024;
|
|
12
|
+
|
|
13
|
+
export function teamDir(storeRoot, accountId) {
|
|
14
|
+
if (!storeRoot || !accountId) throw new Error('teammates need a store root and an account');
|
|
15
|
+
return path.join(storeRoot, 'teammates', String(accountId).replace(/[^A-Za-z0-9_-]/g, '-'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const teammatesFile = (dir) => path.join(dir, 'teammates.jsonl');
|
|
19
|
+
export const chatFile = (dir) => path.join(dir, 'chat.jsonl');
|
|
20
|
+
export const playbooksFile = (dir) => path.join(dir, 'playbooks.jsonl');
|
|
21
|
+
|
|
22
|
+
// The screen asks for these on every refresh; a file only read again when it
|
|
23
|
+
// has changed keeps a long chat from costing a parse each time.
|
|
24
|
+
const READ = new Map();
|
|
25
|
+
|
|
26
|
+
function readLines(file) {
|
|
27
|
+
let text;
|
|
28
|
+
let key;
|
|
29
|
+
try {
|
|
30
|
+
const st = fs.statSync(file);
|
|
31
|
+
const size = st.size;
|
|
32
|
+
key = `${size}:${st.mtimeMs}`;
|
|
33
|
+
const had = READ.get(file);
|
|
34
|
+
if (had?.key === key) return had.lines.slice();
|
|
35
|
+
if (size > MAX_READ) {
|
|
36
|
+
const fd = fs.openSync(file, 'r');
|
|
37
|
+
try {
|
|
38
|
+
const buf = Buffer.allocUnsafe(MAX_READ);
|
|
39
|
+
const n = fs.readSync(fd, buf, 0, MAX_READ, size - MAX_READ);
|
|
40
|
+
text = buf.toString('utf8', 0, n);
|
|
41
|
+
text = text.slice(text.indexOf('\n') + 1);
|
|
42
|
+
} finally { fs.closeSync(fd); }
|
|
43
|
+
} else {
|
|
44
|
+
text = fs.readFileSync(file, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
if (e.code === 'ENOENT') return [];
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const raw of text.split('\n')) {
|
|
52
|
+
if (!raw.trim()) continue;
|
|
53
|
+
try { out.push(JSON.parse(raw)); } catch { /* a torn last line from a crash; the rest still reads */ }
|
|
54
|
+
}
|
|
55
|
+
READ.set(file, { key, lines: out });
|
|
56
|
+
return out.slice();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function append(file, obj) {
|
|
60
|
+
const buf = Buffer.from(JSON.stringify(obj) + '\n', 'utf8');
|
|
61
|
+
if (buf.length > MAX_LINE) throw new Error(`teammates line too large (${buf.length} bytes)`);
|
|
62
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
63
|
+
const fd = fs.openSync(file, 'a');
|
|
64
|
+
try { fs.writeSync(fd, buf, 0, buf.length); } finally { fs.closeSync(fd); }
|
|
65
|
+
return obj;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const newTeammateId = () => `t-${crypto.randomBytes(5).toString('hex')}`;
|
|
69
|
+
export const newMessageId = () => `m-${crypto.randomBytes(6).toString('hex')}`;
|
|
70
|
+
export const newMemoryId = () => `k-${crypto.randomBytes(5).toString('hex')}`;
|
|
71
|
+
export const newPlaybookId = () => `p-${crypto.randomBytes(5).toString('hex')}`;
|
|
72
|
+
export const newLessonId = () => `l-${crypto.randomBytes(5).toString('hex')}`;
|
|
73
|
+
export const newDutyId = () => `d-${crypto.randomBytes(4).toString('hex')}`;
|
|
74
|
+
|
|
75
|
+
export function readTeammates(dir, now = Date.now()) {
|
|
76
|
+
return foldTeammates(readLines(teammatesFile(dir)), now);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readChat(dir) {
|
|
80
|
+
return foldChat(readLines(chatFile(dir)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function writeTeammate(dir, id, patch, now = Date.now()) {
|
|
84
|
+
return append(teammatesFile(dir), { id, ts: now, patch });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function writeChat(dir, line, now = Date.now()) {
|
|
88
|
+
const full = { ...line, ts: line.ts ?? now };
|
|
89
|
+
if (full.kind !== 'seen' && !full.id) full.id = newMessageId();
|
|
90
|
+
return append(chatFile(dir), full);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readPlaybooks(dir) {
|
|
94
|
+
return foldPlaybooks(readLines(playbooksFile(dir)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function writePlaybook(dir, line, now = Date.now()) {
|
|
98
|
+
return append(playbooksFile(dir), { ...line, ts: line.ts ?? now });
|
|
99
|
+
}
|
|
@@ -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, normalizeLine, buildLine, buildClaimLine, buildHeartbeatLine, buildReleaseLine,
|
|
21
|
+
foldWorkItems, foldRawInto, finishItem, 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
|
|
|
@@ -140,32 +140,190 @@ function readLines(file) {
|
|
|
140
140
|
// themselves would let one caller's edit become every later caller's truth.
|
|
141
141
|
const folded = new Map();
|
|
142
142
|
const MAX_FOLDS_HELD = 64;
|
|
143
|
+
const REWINDOW_BYTES = 2 * 1024 * 1024;
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
// A STAMP OF SIZE AND TIME CANNOT TELL A REPLACED LEDGER FROM AN UNTOUCHED ONE,
|
|
146
|
+
// so the file that was folded is held open.
|
|
147
|
+
//
|
|
148
|
+
// What broke (2026-10-19, on GitHub's Linux runner): a ledger deleted and
|
|
149
|
+
// written again at the same length answered with the fold of the old one.
|
|
150
|
+
// Deleting it freed its inode number for the new file to take, and the clock
|
|
151
|
+
// that filesystem stamps a write with is coarse enough that both writes landed
|
|
152
|
+
// on one mtime, so all three parts of the stamp matched. On APFS the two writes
|
|
153
|
+
// get different nanoseconds, which is why no Mac ever saw it.
|
|
154
|
+
//
|
|
155
|
+
// An open descriptor settles it: while one is held, that inode number cannot be
|
|
156
|
+
// given to another file, so a number we do not recognise means a different
|
|
157
|
+
// file. The generation below carries that into the stamp, so a replaced ledger
|
|
158
|
+
// is read from the start by every cache keyed on it, not just the fold.
|
|
159
|
+
const pinned = new Map();
|
|
160
|
+
const MAX_PINS = 64;
|
|
161
|
+
let generations = 0;
|
|
162
|
+
|
|
163
|
+
function dropPin(file) {
|
|
164
|
+
const pin = pinned.get(file);
|
|
165
|
+
if (!pin) return;
|
|
166
|
+
pinned.delete(file);
|
|
167
|
+
try { fs.closeSync(pin.fd); } catch { /* already gone */ }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ledgerPin(file) {
|
|
171
|
+
const absent = { stamp: 'absent', st: null, fd: null, gen: 0 };
|
|
172
|
+
let st = null;
|
|
173
|
+
try { st = fs.statSync(file); } catch (err) { if (err.code !== 'ENOENT') throw err; }
|
|
174
|
+
if (!st) { dropPin(file); return absent; }
|
|
175
|
+
let pin = pinned.get(file);
|
|
176
|
+
if (!pin || pin.ino !== st.ino) {
|
|
177
|
+
dropPin(file);
|
|
178
|
+
let fd;
|
|
179
|
+
try { fd = fs.openSync(file, 'r'); } catch (err) {
|
|
180
|
+
if (err.code !== 'ENOENT') throw err;
|
|
181
|
+
return absent;
|
|
182
|
+
}
|
|
183
|
+
// The file that ended up held, which is a different one from the stat above
|
|
184
|
+
// if it was replaced between the two calls.
|
|
185
|
+
st = fs.fstatSync(fd);
|
|
186
|
+
generations += 1;
|
|
187
|
+
if (pinned.size >= MAX_PINS) dropPin(pinned.keys().next().value);
|
|
188
|
+
pin = { fd, ino: st.ino, gen: generations };
|
|
189
|
+
pinned.set(file, pin);
|
|
151
190
|
}
|
|
191
|
+
return { stamp: `${pin.gen}:${st.size}:${st.mtimeMs}`, st, fd: pin.fd, gen: pin.gen };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function ledgerStamp(file) {
|
|
195
|
+
return ledgerPin(file).stamp;
|
|
152
196
|
}
|
|
153
197
|
|
|
198
|
+
const usable = (l) => l && Buffer.byteLength(l, 'utf8') <= MAX_LINE_BYTES;
|
|
199
|
+
|
|
200
|
+
// A ledger that has only grown is folded on from where it was: every running
|
|
201
|
+
// agent's heartbeat changes the file, and folding all 8 MB again each time
|
|
202
|
+
// held up every request the window made. A file that shrank, was replaced, or
|
|
203
|
+
// changed without growing is read from the start.
|
|
154
204
|
function foldedLedger(file) {
|
|
155
|
-
const
|
|
205
|
+
const pin = ledgerPin(file);
|
|
206
|
+
const st = pin.st;
|
|
156
207
|
const held = folded.get(file);
|
|
157
|
-
if (held && held.stamp === stamp) return held.items;
|
|
208
|
+
if (held && held.stamp === pin.stamp) return held.items;
|
|
209
|
+
// While it still holds no more than the 8 MB a fresh read would, give or
|
|
210
|
+
// take REWINDOW_BYTES added since: a fold kept much longer would show
|
|
211
|
+
// threads a fresh read cuts off.
|
|
212
|
+
const inWindow = held && st && (held.from === 0 ? st.size <= MAX_LEDGER_BYTES : st.size - held.from <= MAX_LEDGER_BYTES + REWINDOW_BYTES);
|
|
213
|
+
if (held && st && held.gen === pin.gen && st.size > held.offset && held.raw && inWindow) {
|
|
214
|
+
const buf = Buffer.allocUnsafe(st.size - held.offset);
|
|
215
|
+
const n = fs.readSync(pin.fd, buf, 0, buf.length, held.offset);
|
|
216
|
+
// Whole lines only, by the byte: a line still being written waits for
|
|
217
|
+
// the next read rather than being cut through a character.
|
|
218
|
+
const end = n ? buf.lastIndexOf(0x0a, n - 1) + 1 : 0;
|
|
219
|
+
const chunk = buf.toString('utf8', 0, end);
|
|
220
|
+
held.offset += end;
|
|
221
|
+
const touched = new Set();
|
|
222
|
+
foldRawInto(held.raw, chunk.split('\n').filter(usable), touched);
|
|
223
|
+
// Only threads new to this fold or reopened in it: the rest were looked at
|
|
224
|
+
// already, and one with no title anywhere would be read for on every line.
|
|
225
|
+
if (held.from > 0) {
|
|
226
|
+
const was = (id) => held.items.get(id);
|
|
227
|
+
const arrived = [...touched].filter((id) => !was(id) || was(id).status === 'done');
|
|
228
|
+
restoreCutHeads(held.raw, arrived, pin.fd, held.from, held.offset);
|
|
229
|
+
}
|
|
230
|
+
for (const id of touched) held.items.set(id, finishItem(held.raw.get(id), 0));
|
|
231
|
+
held.stamp = pin.stamp;
|
|
232
|
+
return held.items;
|
|
233
|
+
}
|
|
158
234
|
// Time zero, so the lease pass inside the fold expires nothing: a lease is
|
|
159
235
|
// only ever lapsed against the clock of the call asking, and that is `aged`.
|
|
160
|
-
const
|
|
236
|
+
const raw = new Map();
|
|
237
|
+
let offset = 0;
|
|
238
|
+
let from = 0;
|
|
239
|
+
if (st) {
|
|
240
|
+
const read = readWindow(file, st.size, pin.fd);
|
|
241
|
+
foldRawInto(raw, read.text.split('\n').filter(usable));
|
|
242
|
+
if (read.from > 0) restoreCutHeads(raw, raw.keys(), pin.fd, read.from, read.end);
|
|
243
|
+
offset = read.end;
|
|
244
|
+
from = read.from;
|
|
245
|
+
}
|
|
246
|
+
const items = new Map();
|
|
247
|
+
for (const [id, item] of raw) items.set(id, finishItem(item, 0));
|
|
161
248
|
// One entry per ledger, so a real store has a few dozen. The cap is for the
|
|
162
249
|
// long-lived MCP server, which walks temporary project dirs that never come
|
|
163
250
|
// back; a Map iterates in insertion order, so the oldest key goes first.
|
|
164
251
|
if (folded.size >= MAX_FOLDS_HELD) folded.delete(folded.keys().next().value);
|
|
165
|
-
folded.set(file, { stamp, items });
|
|
252
|
+
folded.set(file, { stamp: pin.stamp, items, raw, gen: pin.gen, offset, from });
|
|
166
253
|
return items;
|
|
167
254
|
}
|
|
168
255
|
|
|
256
|
+
// The ledger's trailing window, as text, to its last whole line, and the byte
|
|
257
|
+
// that ends at, read through the descriptor the fold holds open.
|
|
258
|
+
function readWindow(file, size, fd) {
|
|
259
|
+
if (size > MAX_LEDGER_BYTES && !warnedOversize.has(file)) {
|
|
260
|
+
warnedOversize.add(file);
|
|
261
|
+
console.warn(`work-items: ${file} is ${size} bytes; reading only the last ${MAX_LEDGER_BYTES}`);
|
|
262
|
+
}
|
|
263
|
+
const from = Math.max(0, size - MAX_LEDGER_BYTES);
|
|
264
|
+
const buf = Buffer.allocUnsafe(size - from);
|
|
265
|
+
const n = fs.readSync(fd, buf, 0, buf.length, from);
|
|
266
|
+
const end = n ? buf.lastIndexOf(0x0a, n - 1) + 1 : 0;
|
|
267
|
+
return { text: buf.toString('utf8', 0, end), end: from + end, from };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// AN UNFINISHED THREAD WHOSE FIRST LINE IS BEFORE THE WINDOW is folded from its
|
|
271
|
+
// later lines alone: no title, so a blank row in the inbox. A title is written
|
|
272
|
+
// once, by the line that makes a thread, so no title means the cut went through
|
|
273
|
+
// it, and its own lines are read from the start of the file and folded again.
|
|
274
|
+
// A finished one is left to `readOlderWorkItems`, so a ledger without such a
|
|
275
|
+
// thread is read exactly as it always was.
|
|
276
|
+
//
|
|
277
|
+
// THE CLAIM STAYS THE WINDOW'S. `claimWorkItem` numbers a claim from the window,
|
|
278
|
+
// so one made after the cut can be lower than one before it, and the older one
|
|
279
|
+
// would fence off everything its agent wrote since.
|
|
280
|
+
function restoreCutHeads(raw, ids, fd, from, end) {
|
|
281
|
+
const cut = new Set();
|
|
282
|
+
for (const id of ids) {
|
|
283
|
+
const item = raw.get(id);
|
|
284
|
+
if (!item.wrote.title && item.status !== 'done') cut.add(id);
|
|
285
|
+
}
|
|
286
|
+
if (!cut.size) return;
|
|
287
|
+
const lines = linesUpTo(fd, end, cut);
|
|
288
|
+
const restored = new Map();
|
|
289
|
+
foldRawInto(restored, lines.filter((l) => l.at < from).map((l) => l.text));
|
|
290
|
+
const found = [...restored.keys()].filter((id) => cut.has(id));
|
|
291
|
+
for (const id of found) Object.assign(restored.get(id), { epoch: 0, claim: null });
|
|
292
|
+
foldRawInto(restored, lines.filter((l) => l.at >= from).map((l) => l.text));
|
|
293
|
+
for (const id of found) raw.set(id, restored.get(id));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// The whole lines before byte `end` that carry one of `ids`, with the byte each
|
|
297
|
+
// starts at, read a megabyte at a time. Searched as bytes, so a line that is
|
|
298
|
+
// not one of them is never decoded; the fold checks whose each line really is.
|
|
299
|
+
const PIECE_BYTES = 1024 * 1024;
|
|
300
|
+
const ID_KEY = Buffer.from('"id":"');
|
|
301
|
+
function linesUpTo(fd, end, ids) {
|
|
302
|
+
const out = [];
|
|
303
|
+
let rest = Buffer.alloc(0);
|
|
304
|
+
let restAt = 0;
|
|
305
|
+
for (let pos = 0; pos < end;) {
|
|
306
|
+
const buf = Buffer.allocUnsafe(Math.min(PIECE_BYTES, end - pos));
|
|
307
|
+
const n = fs.readSync(fd, buf, 0, buf.length, pos);
|
|
308
|
+
if (!n) break;
|
|
309
|
+
pos += n;
|
|
310
|
+
const bytes = rest.length ? Buffer.concat([rest, buf.subarray(0, n)]) : buf.subarray(0, n);
|
|
311
|
+
const whole = bytes.lastIndexOf(0x0a) + 1;
|
|
312
|
+
const starts = new Set();
|
|
313
|
+
for (let at = bytes.indexOf(ID_KEY); at !== -1 && at < whole; at = bytes.indexOf(ID_KEY, at + ID_KEY.length)) {
|
|
314
|
+
const from = at + ID_KEY.length;
|
|
315
|
+
if (ids.has(bytes.toString('latin1', from, bytes.indexOf(0x22, from)))) starts.add(bytes.lastIndexOf(0x0a, at) + 1);
|
|
316
|
+
}
|
|
317
|
+
for (const start of starts) {
|
|
318
|
+
const text = bytes.toString('utf8', start, bytes.indexOf(0x0a, start));
|
|
319
|
+
if (usable(text)) out.push({ text, at: restAt + start });
|
|
320
|
+
}
|
|
321
|
+
rest = bytes.subarray(whole);
|
|
322
|
+
restAt += whole;
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
169
327
|
// One item as of `now`: a copy, with the lease rule that `foldWorkItems` applies
|
|
170
328
|
// at the end of its own pass applied here instead. The two mutable fields a
|
|
171
329
|
// caller can reach through a spread are copied with it.
|
|
@@ -238,6 +396,56 @@ export function readWorkItem(projectDir, id, now = Date.now()) {
|
|
|
238
396
|
return item ? aged(item, now) : null;
|
|
239
397
|
}
|
|
240
398
|
|
|
399
|
+
/**
|
|
400
|
+
* ONE THREAD'S LINES, OLDEST FIRST, without the lease lines (heartbeats and
|
|
401
|
+
* releases), the last `max` of them.
|
|
402
|
+
*
|
|
403
|
+
* The phone pass (main/phone-inbox.mjs) asks this for every thread of a
|
|
404
|
+
* project, up to 300, and each ask used to read and parse the whole ledger:
|
|
405
|
+
* 20,929 ms of blocked main thread on the real 17,797-line agentbox-team
|
|
406
|
+
* ledger, against 101 ms for one read (2026-10-14, the endless beachball). So
|
|
407
|
+
* the ledger is read once per change of the file, grouped by thread, and kept,
|
|
408
|
+
* keyed on the file's identity exactly as the fold is. The lines handed out are
|
|
409
|
+
* the kept ones, frozen, so no caller can edit what the next one reads.
|
|
410
|
+
*/
|
|
411
|
+
const histories = new Map();
|
|
412
|
+
const HISTORIES_HELD = 8;
|
|
413
|
+
function deepFreeze(v) {
|
|
414
|
+
if (v && typeof v === 'object' && !Object.isFrozen(v)) {
|
|
415
|
+
Object.freeze(v);
|
|
416
|
+
for (const k of Object.keys(v)) deepFreeze(v[k]);
|
|
417
|
+
}
|
|
418
|
+
return v;
|
|
419
|
+
}
|
|
420
|
+
function historiesOf(file) {
|
|
421
|
+
const stamp = ledgerStamp(file);
|
|
422
|
+
const held = histories.get(file);
|
|
423
|
+
if (held && held.stamp === stamp) return held.byId;
|
|
424
|
+
const byId = new Map();
|
|
425
|
+
if (stamp !== 'absent') {
|
|
426
|
+
for (const raw of readLines(file)) {
|
|
427
|
+
let line;
|
|
428
|
+
try { line = JSON.parse(raw); } catch { continue; }
|
|
429
|
+
if (!line || typeof line.id !== 'string' || line.heartbeat || line.release) continue;
|
|
430
|
+
let list = byId.get(line.id);
|
|
431
|
+
if (!list) byId.set(line.id, (list = []));
|
|
432
|
+
list.push(deepFreeze(line));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
histories.delete(file);
|
|
436
|
+
if (histories.size >= HISTORIES_HELD) histories.delete(histories.keys().next().value);
|
|
437
|
+
histories.set(file, { stamp, byId });
|
|
438
|
+
return byId;
|
|
439
|
+
}
|
|
440
|
+
// `before` keeps the lines written at or before that moment, and the cap then
|
|
441
|
+
// counts back from there rather than from the newest line.
|
|
442
|
+
export function readItemHistory(projectDir, id, { max = 400, before } = {}) {
|
|
443
|
+
const lines = historiesOf(ledgerPath(projectDir)).get(id);
|
|
444
|
+
if (!lines) return [];
|
|
445
|
+
const upTo = Number.isFinite(before) ? lines.filter((l) => l.ts <= before) : lines;
|
|
446
|
+
return upTo.slice(-max);
|
|
447
|
+
}
|
|
448
|
+
|
|
241
449
|
/* -------------------------------- mutating ------------------------------- */
|
|
242
450
|
export function newWorkItemId() {
|
|
243
451
|
return `w-${crypto.randomBytes(5).toString('hex')}`;
|
|
@@ -440,4 +648,14 @@ export function readLinesFrom(projectDir, offset = 0) {
|
|
|
440
648
|
}
|
|
441
649
|
|
|
442
650
|
export { LEASE_MS };
|
|
443
|
-
export const _internals = {
|
|
651
|
+
export const _internals = {
|
|
652
|
+
appendLine,
|
|
653
|
+
readLines,
|
|
654
|
+
ledgerPath,
|
|
655
|
+
forgetFolds: () => {
|
|
656
|
+
folded.clear();
|
|
657
|
+
histories.clear();
|
|
658
|
+
wholeFolds.clear();
|
|
659
|
+
for (const file of [...pinned.keys()]) dropPin(file);
|
|
660
|
+
},
|
|
661
|
+
};
|