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
package/main/agent-sessions.mjs
CHANGED
|
@@ -1,123 +1,60 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Claude Code conversation threads for the import card. These are separate from
|
|
2
|
+
// the `.md` agent definitions that agent-files.mjs reads, and the card shows both.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
// module reads `.md` agent DEFINITIONS, which is a real thing Claude Code has
|
|
5
|
-
// and is not what she is looking at when she says "my agents". the four
|
|
6
|
-
// definition files on this Mac were last edited on 23 and 30 March, and she had
|
|
7
|
-
// four threads going the evening she complained.
|
|
4
|
+
// A thread lives in one of two places:
|
|
8
5
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// <claude data folder>/projects/<folder>/<id>.jsonl a terminal or editor
|
|
7
|
+
// session, one JSON object per line (data folders: main/agent-homes.mjs)
|
|
8
|
+
// ~/Library/Application Support/Claude/claude-code-sessions/**/local_<id>.json
|
|
9
|
+
// the desktop app's small record about a session, with a real `title`
|
|
12
10
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// same under every other Claude
|
|
17
|
-
// data folder: main/agent-homes.mjs)
|
|
18
|
-
// ~/Library/Application Support/Claude/
|
|
19
|
-
// claude-code-sessions/**/local_<id>.json the desktop app
|
|
20
|
-
//
|
|
21
|
-
// The terminal store is the transcript itself, one JSON object per line. The
|
|
22
|
-
// desktop store is a small record ABOUT the session, and it is the richer of
|
|
23
|
-
// the two because the app writes a real `title` into it. Measured 2026-08-29:
|
|
24
|
-
// 2,444 transcripts in 240 folders, and 38 desktop records whose newest is
|
|
25
|
-
// 7 July, so on her Mac today the terminal side is all of it.
|
|
26
|
-
//
|
|
27
|
-
// THE HARD PART IS NOT FINDING THEM, IT IS THROWING THEM AWAY. hundreds exist
|
|
28
|
-
// and a handful are relevant.
|
|
29
|
-
//
|
|
30
|
-
// The line that separates them is not a heuristic, which is the good news. Every
|
|
31
|
-
// transcript row carries an `entrypoint`, and it says who started the session.
|
|
32
|
-
// Counted over all 2,212 transcripts in her store on 2026-08-29:
|
|
33
|
-
//
|
|
34
|
-
// 2,158 sdk-cli something started it: Agentbox's own workers, Agentbox's
|
|
35
|
-
// chats, and our measurement scripts running twenty
|
|
36
|
-
// agents at a time in temp folders
|
|
37
|
-
// 53 cli she opened a terminal and typed
|
|
38
|
-
// 1 claude-desktop she used the desktop app
|
|
39
|
-
//
|
|
40
|
-
// So 54 of 2,212 were started by a person, and those 54 are the handful she
|
|
41
|
-
// means. Her own test is one of them: `~/Desktop/dev/zero`, 8:38 PM, "this is a
|
|
42
|
-
// test", `entrypoint: "cli"`.
|
|
43
|
-
//
|
|
44
|
-
// A FIRST DRAFT OF THIS GUESSED INSTEAD, and the guess is worth keeping written
|
|
45
|
-
// down because it looked like it worked. It dropped anything opening with the
|
|
46
|
-
// work item brief and anything running in a temp folder, which on that evening
|
|
47
|
-
// removed 126 and 171 rows and left a believable list. It would have kept every
|
|
48
|
-
// chat Agentbox itself runs in a product folder, because those open with her own
|
|
49
|
-
// question and run somewhere real. `entrypoint` was on the row the whole time.
|
|
11
|
+
// Most transcripts on a machine running Agentbox were started by Agentbox. The
|
|
12
|
+
// `entrypoint` on every transcript row says who started the session, so the
|
|
13
|
+
// filter reads that rather than guessing from the prompt or the folder.
|
|
50
14
|
|
|
51
15
|
import fs from 'node:fs';
|
|
52
16
|
import os from 'node:os';
|
|
53
17
|
import path from 'node:path';
|
|
54
18
|
import { claudeDataDirs } from './agent-homes.mjs';
|
|
19
|
+
import { howTheChatEnded } from '../shared/agent-import.mjs';
|
|
55
20
|
|
|
56
|
-
/**
|
|
57
|
-
* "Recent = last few days", in days.
|
|
58
|
-
*
|
|
59
|
-
* Seven rather than three, and the number is measured rather than chosen.
|
|
60
|
-
* Counting only the threads a person started, her Mac on 2026-08-29 has 2 in
|
|
61
|
-
* the last three days and 9 in the last seven, in two folders she works in.
|
|
62
|
-
* Three days is a card with tonight's test on it and almost nothing else; the
|
|
63
|
-
* filters have already done the work that "a few" was standing in for, so the
|
|
64
|
-
* window can afford to be the generous reading.
|
|
65
|
-
*
|
|
66
|
-
* Ten since the launch list (w-db6f5e331e, 2026-10-05): "showing your recently
|
|
67
|
-
* active agents over the last 10 days". Codex reads the same number
|
|
21
|
+
/** How many days back counts as recent. Codex reads the same number
|
|
68
22
|
* (main/codex-threads.mjs), so the two halves of the card agree. */
|
|
69
23
|
export const RECENT_DAYS = 10;
|
|
70
24
|
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
* turn is at the top of the file by construction, and the files are not small:
|
|
74
|
-
* the largest in her store on 2026-08-29 is 13MB, and there are hundreds. */
|
|
25
|
+
/** How much of a transcript is read to find its first human turn, which is at
|
|
26
|
+
* the top of the file. Transcripts run to many megabytes. */
|
|
75
27
|
const HEAD_BYTES = 96 * 1024;
|
|
76
28
|
|
|
77
|
-
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* ten days' terminal threads opened with rows of 240KB, 294KB and 486KB, were
|
|
82
|
-
* cut, lost their name and fell off the card. Eight megabytes is the Codex
|
|
83
|
-
* reader's own ceiling (main/codex-threads.mjs) and several screenshots. */
|
|
29
|
+
/** How far past the head one cut-off first message may be followed. A
|
|
30
|
+
* screenshot pasted into the first message is written into that row as
|
|
31
|
+
* base64, so the row can be hundreds of KB. Eight MB matches the Codex
|
|
32
|
+
* reader's ceiling (main/codex-threads.mjs). */
|
|
84
33
|
const FIRST_TURN_BYTES = 8 * 1024 * 1024;
|
|
85
34
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* cap that can drop the newest thread on the machine is not a safety rail. */
|
|
35
|
+
/** How much of a transcript's end is read for its title and last reply. */
|
|
36
|
+
const TAIL_BYTES = 256 * 1024;
|
|
37
|
+
/** The longest last reply kept, from its end, which is where the answer is. */
|
|
38
|
+
const SAID_CHARS = 8000;
|
|
39
|
+
/** The longest first prompt kept for an imported row's opening message. */
|
|
40
|
+
const FIRST_CHARS = 4000;
|
|
41
|
+
|
|
42
|
+
/** A cap on how many threads are kept, so opening the card is not a disk
|
|
43
|
+
* crawl. It caps reading, never looking: the terminal walk stats every file
|
|
44
|
+
* and sorts newest first before it opens any, so the cap only drops older
|
|
45
|
+
* threads. */
|
|
98
46
|
const MAX_READS = 1200;
|
|
99
|
-
//
|
|
100
|
-
// uses it says "Looking" meanwhile; three seconds reaches two thirds of her
|
|
101
|
-
// week's 9,073 transcripts and all of a normal Mac's.
|
|
47
|
+
// The card shows "Looking" while this runs.
|
|
102
48
|
const READ_BUDGET_MS = 3000;
|
|
103
49
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* A FOLDER NOBODY WORKS IN.
|
|
108
|
-
*
|
|
109
|
-
* `/private/var/folders/19/.../T/speedsize-6AGMVw-run-12` is a real cwd of a
|
|
110
|
-
* real Claude Code session, and it is one of twenty a measurement script of
|
|
111
|
-
* ours started in a loop. 258 of her 448 recent threads are that. Nobody opens
|
|
112
|
-
* a temp directory to work, so a thread that ran in one is not a thread she
|
|
113
|
-
* would recognise, and offering it is how a list of hundreds happens. */
|
|
50
|
+
/** A temp folder, where nobody works. Agentbox's own scripts and workers run
|
|
51
|
+
* Claude Code sessions there, so a thread from one is not the person's. */
|
|
114
52
|
export function isScratchFolder(folder, home = os.homedir()) {
|
|
115
53
|
const p = String(folder ?? '');
|
|
116
54
|
if (!p) return true;
|
|
117
|
-
// A
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// one under `/var/folders` on purpose.
|
|
55
|
+
// A folder inside the home folder is the person's, whatever the path. This
|
|
56
|
+
// check comes first because a Mac can put a home folder anywhere, and the
|
|
57
|
+
// tests put one under `/var/folders`.
|
|
121
58
|
const here = path.resolve(String(home ?? ''));
|
|
122
59
|
if (here && (p === here || p.startsWith(`${here}/`))) return false;
|
|
123
60
|
return /^\/private\/tmp(\/|$)/.test(p)
|
|
@@ -126,14 +63,10 @@ export function isScratchFolder(folder, home = os.homedir()) {
|
|
|
126
63
|
|| /^\/var\/folders(\/|$)/.test(p);
|
|
127
64
|
}
|
|
128
65
|
|
|
129
|
-
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* character turned into `-`, so `/private/tmp/w-1` is `-private-tmp-w-1`.
|
|
134
|
-
* Of her 12,272 transcripts touched in ten days about 12,000 were in temp
|
|
135
|
-
* folders, and opening each one only to refuse it spent the card's three
|
|
136
|
-
* seconds before it reached her oldest threads. */
|
|
66
|
+
/** The same rule read off the folder Claude Code filed the transcript in, so
|
|
67
|
+
* a temp-folder transcript is refused without being opened. Claude Code names
|
|
68
|
+
* that folder after the working folder with every character that is not a
|
|
69
|
+
* letter or digit turned into `-`, so `/private/tmp/w-1` is `-private-tmp-w-1`. */
|
|
137
70
|
export function isScratchProjectDir(name, home = os.homedir()) {
|
|
138
71
|
const n = String(name ?? '');
|
|
139
72
|
const here = path.resolve(String(home ?? '')).replace(/[^a-zA-Z0-9]/g, '-');
|
|
@@ -141,24 +74,12 @@ export function isScratchProjectDir(name, home = os.homedir()) {
|
|
|
141
74
|
return /^-(private-)?(tmp|var-folders)(-|$)/.test(n);
|
|
142
75
|
}
|
|
143
76
|
|
|
144
|
-
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* to measure it. Offering those on an import card would be offering to bring
|
|
151
|
-
* the inbox into the inbox.
|
|
152
|
-
*
|
|
153
|
-
* An entrypoint we have never seen is treated as not hers. The cost of being
|
|
154
|
-
* wrong that way is one missing row on a card; the other way is the hundreds
|
|
155
|
-
* she is complaining about.
|
|
156
|
-
*
|
|
157
|
-
* `claude-vscode` IS A PERSON TOO (w-e46f2e9cea, 2026-10-11). It is what the
|
|
158
|
-
* Claude Code extension writes in VS Code, Cursor and Windsurf, and leaving it
|
|
159
|
-
* out threw away every session of everybody who runs Claude Code in an editor.
|
|
160
|
-
* Read out of the 2.1.293 binary, which maps it to `claude_code_vscode`. The
|
|
161
|
-
* `remote*` family stays out: those run in the cloud, not on this disk. */
|
|
77
|
+
/** Entrypoints a person starts. `cli` is a terminal, `claude-desktop` the
|
|
78
|
+
* desktop app, and `claude-vscode` the Claude Code extension in VS Code,
|
|
79
|
+
* Cursor and Windsurf. Everything else, mostly `sdk-cli`, was started by
|
|
80
|
+
* software, usually Agentbox's own workers and chats. An unknown entrypoint
|
|
81
|
+
* counts as not the person's: a missing row costs less than hundreds of
|
|
82
|
+
* wrong ones. `remote*` sessions run in the cloud, not on this disk. */
|
|
162
83
|
const BY_HAND = new Set(['cli', 'claude-desktop', 'claude-vscode']);
|
|
163
84
|
export function startedByHand(entrypoint) {
|
|
164
85
|
return BY_HAND.has(entrypoint);
|
|
@@ -170,44 +91,25 @@ export function sourceOf(entrypoint) {
|
|
|
170
91
|
return entrypoint === 'claude-vscode' ? 'editor' : 'terminal';
|
|
171
92
|
}
|
|
172
93
|
|
|
173
|
-
/**
|
|
174
|
-
*
|
|
175
|
-
* `<session>/subagents/`, one per fan-out, so the parent already stands for
|
|
176
|
-
* them and listing both says the same work twice. */
|
|
94
|
+
/** Claude Code files subagent transcripts under `<session>/subagents/`. The
|
|
95
|
+
* parent session already stands for that work. */
|
|
177
96
|
export function isSubagentPath(file) {
|
|
178
97
|
return /(^|\/)subagents\//.test(String(file ?? ''));
|
|
179
98
|
}
|
|
180
99
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Lines a person did not type, which a transcript's first turn is often made
|
|
185
|
-
* of: the slash command wrapper, the resume caveat, and anything the harness
|
|
186
|
-
* injected. A thread named `<command-name>/clear</command-name>` is a thread
|
|
187
|
-
* she cannot recognise. */
|
|
100
|
+
/** Lines Claude Code writes into a first turn that a person did not type: the
|
|
101
|
+
* slash command wrapper, the resume caveat and injected reminders. */
|
|
188
102
|
const NOT_TYPED = [
|
|
189
103
|
/^<command-name>/, /^<command-message>/, /^<command-args>/, /^<local-command/,
|
|
190
104
|
/^<system-reminder>/, /^<user-memory/, /^Caveat: The messages below/,
|
|
191
105
|
/^This session is being continued from/, /^\[Request interrupted/,
|
|
192
106
|
];
|
|
193
107
|
|
|
194
|
-
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* conversation. The first human sentence is what the desktop app titles from
|
|
200
|
-
* too, and it is the line she would know the thread by.
|
|
201
|
-
*
|
|
202
|
-
* SEVERAL MESSAGES ARE OFFERED, NOT ONE, and that is a fix rather than a
|
|
203
|
-
* flourish. A session that opens with a slash command has the wrapper as its
|
|
204
|
-
* first user message and her actual words in the second, so reading only the
|
|
205
|
-
* first left four of her own threads in an outside repo nameless and
|
|
206
|
-
* therefore off the card. The first message that is something a person typed
|
|
207
|
-
* wins; a thread with no such message has no name and is dropped, which is the
|
|
208
|
-
* right outcome for a session that was never spoken to.
|
|
209
|
-
*
|
|
210
|
-
* Markdown is flattened rather than rendered, because this lands in a list. */
|
|
108
|
+
/** The row's name, from the first line a person typed. Terminal transcripts
|
|
109
|
+
* carry no title, so the conversation is all there is. Takes several messages
|
|
110
|
+
* because a session that opens with a slash command has the wrapper first and
|
|
111
|
+
* the person's words second. Returns '' when nobody typed anything, and the
|
|
112
|
+
* caller drops that thread. */
|
|
211
113
|
export function threadTitle(texts, max = 72) {
|
|
212
114
|
for (const text of Array.isArray(texts) ? texts : [texts]) {
|
|
213
115
|
const got = titleFromOne(text, max);
|
|
@@ -220,7 +122,7 @@ function titleFromOne(text, max) {
|
|
|
220
122
|
const raw = String(text ?? '').replace(/\r/g, '');
|
|
221
123
|
for (const line of raw.split('\n')) {
|
|
222
124
|
// `[Image #1]` and `[Pasted text #1 +40 lines]` are Claude Code's stand-ins
|
|
223
|
-
// for what was pasted
|
|
125
|
+
// for what was pasted.
|
|
224
126
|
const t = line.replace(/\[(?:Image|Pasted text) #\d+[^\]]*\]/g, '').trim();
|
|
225
127
|
if (!t) continue;
|
|
226
128
|
if (NOT_TYPED.some((re) => re.test(t))) return '';
|
|
@@ -238,27 +140,17 @@ function titleFromOne(text, max) {
|
|
|
238
140
|
return '';
|
|
239
141
|
}
|
|
240
142
|
|
|
241
|
-
/**
|
|
242
|
-
* `/Users/you/Desktop/dev/zero` → `~/Desktop/dev/zero`. Same reason as the
|
|
243
|
-
* one in agent-files.mjs: a path carrying her account name reads like a
|
|
244
|
-
* system string rather than a folder she knows. */
|
|
143
|
+
/** Writes a folder under home as `~/...`. */
|
|
245
144
|
export function shortFolder(folder, home = os.homedir()) {
|
|
246
145
|
const here = path.resolve(home);
|
|
247
146
|
const full = path.resolve(String(folder ?? '') || here);
|
|
248
147
|
return full === here || full.startsWith(`${here}/`) ? `~${full.slice(here.length)}` : full;
|
|
249
148
|
}
|
|
250
149
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
*
|
|
255
|
-
* started the session, which folder it ran in, and the first thing the person
|
|
256
|
-
* typed.
|
|
257
|
-
*
|
|
258
|
-
* Read as a fixed head rather than a whole file. `entrypoint` and `cwd` are on
|
|
259
|
-
* every message row and the first human turn is the first message, so all
|
|
260
|
-
* three are at the top, and the alternative is reading gigabytes to fill in a
|
|
261
|
-
* list. Her largest transcript is 13MB and there are hundreds. */
|
|
150
|
+
/** Reads a transcript's head for who started the session, which folder it ran
|
|
151
|
+
* in and the first messages the person typed. `entrypoint` and `cwd` are on
|
|
152
|
+
* every message row and the first human turn comes first, so a fixed head is
|
|
153
|
+
* enough. */
|
|
262
154
|
export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_BYTES) {
|
|
263
155
|
let fd = null;
|
|
264
156
|
try {
|
|
@@ -266,18 +158,18 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_B
|
|
|
266
158
|
const buf = Buffer.alloc(bytes);
|
|
267
159
|
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
268
160
|
const text = buf.slice(0, n).toString('utf8');
|
|
269
|
-
// The last line of a
|
|
270
|
-
//
|
|
271
|
-
// then the last line is complete, and a file whose one row has no newline
|
|
272
|
-
// after it lost that row (found by Codex's review, 2026-10-11).
|
|
161
|
+
// The last line of a full-size read is usually cut in half, so it is set
|
|
162
|
+
// aside. When the whole file fit, the last line is complete and is kept.
|
|
273
163
|
const lines = text.split('\n');
|
|
274
164
|
const cut = n < bytes ? null : lines.pop();
|
|
275
165
|
let cwd = null;
|
|
276
166
|
let entrypoint = null;
|
|
167
|
+
let firstAt = 0;
|
|
277
168
|
const said = [];
|
|
278
169
|
const take = (line) => {
|
|
279
170
|
let row;
|
|
280
171
|
try { row = JSON.parse(line); } catch { return; }
|
|
172
|
+
if (!firstAt && row.timestamp) firstAt = Date.parse(row.timestamp) || 0;
|
|
281
173
|
if (!cwd && typeof row.cwd === 'string' && row.cwd) cwd = row.cwd;
|
|
282
174
|
if (!entrypoint && typeof row.entrypoint === 'string' && row.entrypoint) entrypoint = row.entrypoint;
|
|
283
175
|
if (row.type === 'user' && said.length < 5) {
|
|
@@ -290,29 +182,81 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_B
|
|
|
290
182
|
take(line);
|
|
291
183
|
if (cwd && entrypoint && said.length >= 5) break;
|
|
292
184
|
}
|
|
293
|
-
//
|
|
294
|
-
// pasted into it
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// known to be the app's own, so the thousands of worker transcripts are
|
|
298
|
-
// never read past their head.
|
|
185
|
+
// If the cut line is the first user message, cut because a screenshot was
|
|
186
|
+
// pasted into it, read it to its end, up to `most`. Only when nothing typed
|
|
187
|
+
// was found and the session is not known to be the app's own, so worker
|
|
188
|
+
// transcripts are never read past their head.
|
|
299
189
|
if (!said.length && n === bytes && cut && /"type"\s*:\s*"user"/.test(cut.slice(0, 2048))
|
|
300
190
|
&& (!entrypoint || startedByHand(entrypoint))) {
|
|
301
191
|
const rest = restOfLine(fd, n, most - n);
|
|
302
192
|
if (rest) take(Buffer.concat([buf.subarray(buf.lastIndexOf(0x0a, n - 1) + 1, n), rest]).toString('utf8'));
|
|
303
193
|
}
|
|
304
|
-
return { cwd, entrypoint, said };
|
|
194
|
+
return { cwd, entrypoint, said, firstAt };
|
|
195
|
+
} catch {
|
|
196
|
+
return { cwd: null, entrypoint: null, said: [], firstAt: 0 };
|
|
197
|
+
} finally {
|
|
198
|
+
if (fd !== null) { try { fs.closeSync(fd); } catch { /* nothing to do */ } }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A `user` line a person typed, as opposed to a tool result, a reminder or a
|
|
203
|
+
* command's wrapper. Only these end the agent's reply. */
|
|
204
|
+
function typedTurn(row) {
|
|
205
|
+
if (row?.type !== 'user' || row.toolUseResult || row.isMeta || row.isCompactSummary) return false;
|
|
206
|
+
return threadTitle(messageText(row)) !== '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Reads a transcript's end for Claude Code's own title (`ai-title`, the last
|
|
210
|
+
* one wins) and the agent's last reply, every text block of it since the last
|
|
211
|
+
* thing a person typed. */
|
|
212
|
+
export function readTranscriptTail(file, bytes = TAIL_BYTES) {
|
|
213
|
+
let fd = null;
|
|
214
|
+
try {
|
|
215
|
+
fd = fs.openSync(file, 'r');
|
|
216
|
+
const size = fs.fstatSync(fd).size;
|
|
217
|
+
const from = Math.max(0, size - bytes);
|
|
218
|
+
const buf = Buffer.alloc(size - from);
|
|
219
|
+
const n = fs.readSync(fd, buf, 0, buf.length, from);
|
|
220
|
+
const lines = buf.slice(0, n).toString('utf8').split('\n');
|
|
221
|
+
if (from > 0) lines.shift();
|
|
222
|
+
let aiTitle = '';
|
|
223
|
+
let reply = '';
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
if (!line.trim()) continue;
|
|
226
|
+
let row;
|
|
227
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
228
|
+
if (row.type === 'ai-title' && row.aiTitle) aiTitle = String(row.aiTitle);
|
|
229
|
+
else if (typedTurn(row)) reply = '';
|
|
230
|
+
else if (row.type === 'assistant') {
|
|
231
|
+
const text = messageText(row).trim();
|
|
232
|
+
if (text) reply = reply ? `${reply}\n\n${text}` : text;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { aiTitle, lastSaid: reply.slice(-SAID_CHARS) };
|
|
305
236
|
} catch {
|
|
306
|
-
return {
|
|
237
|
+
return { aiTitle: '', lastSaid: '' };
|
|
307
238
|
} finally {
|
|
308
239
|
if (fd !== null) { try { fs.closeSync(fd); } catch { /* nothing to do */ } }
|
|
309
240
|
}
|
|
310
241
|
}
|
|
311
242
|
|
|
312
|
-
/**
|
|
313
|
-
*
|
|
314
|
-
|
|
315
|
-
|
|
243
|
+
/** What a finished read knows about how a chat ended, for its row and the
|
|
244
|
+
* setup card. */
|
|
245
|
+
function endOf(file) {
|
|
246
|
+
const { aiTitle, lastSaid } = file ? readTranscriptTail(file) : { aiTitle: '', lastSaid: '' };
|
|
247
|
+
return { aiTitle, lastSaid, ...howTheChatEnded(lastSaid) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The first message a person typed, whole, for an imported row's opening. */
|
|
251
|
+
function firstTyped(said) {
|
|
252
|
+
for (const text of said) {
|
|
253
|
+
if (threadTitle(text)) return String(text).trim().slice(0, FIRST_CHARS);
|
|
254
|
+
}
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The bytes from `from` up to the next newline or the end of the file, or
|
|
259
|
+
* null if neither comes within `budget` bytes. */
|
|
316
260
|
function restOfLine(fd, from, budget) {
|
|
317
261
|
const parts = [];
|
|
318
262
|
let at = from;
|
|
@@ -328,9 +272,7 @@ function restOfLine(fd, from, budget) {
|
|
|
328
272
|
return null;
|
|
329
273
|
}
|
|
330
274
|
|
|
331
|
-
/**
|
|
332
|
-
* The words out of one transcript row, whichever of the two shapes it is in.
|
|
333
|
-
* Content is a string on the plain ones and a list of blocks on the rest. */
|
|
275
|
+
/** A row's content is either a string or a list of blocks. */
|
|
334
276
|
function messageText(row) {
|
|
335
277
|
const c = row?.message?.content;
|
|
336
278
|
if (typeof c === 'string') return c;
|
|
@@ -343,22 +285,13 @@ function messageText(row) {
|
|
|
343
285
|
return '';
|
|
344
286
|
}
|
|
345
287
|
|
|
346
|
-
/**
|
|
347
|
-
*
|
|
348
|
-
|
|
349
|
-
* `skipped` comes back with it because the counts are the argument for the
|
|
350
|
-
* filters, and a session that changes one of them should be able to see what
|
|
351
|
-
* it changed rather than take this comment's word for it. */
|
|
352
|
-
function readTerminalThreads({ home, dirs, cutoff, max, skipIds = new Set() }) {
|
|
288
|
+
/** `skipped` counts why each transcript was left out, so a change to a filter
|
|
289
|
+
* can be measured. */
|
|
290
|
+
function readTerminalThreads({ home, dirs, cutoff, max, budgetMs = READ_BUDGET_MS, skipIds = new Set() }) {
|
|
353
291
|
const skipped = { old: 0, scratch: 0, started: 0, subagent: 0, empty: 0, unread: 0, desktopDupe: 0, copy: 0 };
|
|
354
292
|
const out = [];
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// older than everything it kept.
|
|
358
|
-
//
|
|
359
|
-
// EVERY DATA FOLDER, NOT ONLY ~/.claude (main/agent-homes.mjs). They are
|
|
360
|
-
// pooled before the sort, so the cap and the time budget are spent on the
|
|
361
|
-
// newest sessions on the machine, whichever folder they are in.
|
|
293
|
+
// Every data folder is pooled and sorted newest first before any file is
|
|
294
|
+
// opened, so `max` and the time budget only ever cut off older sessions.
|
|
362
295
|
const recent = [];
|
|
363
296
|
for (const dir of dirs) {
|
|
364
297
|
const root = path.join(dir, 'projects');
|
|
@@ -374,37 +307,42 @@ function readTerminalThreads({ home, dirs, cutoff, max, skipIds = new Set() }) {
|
|
|
374
307
|
}
|
|
375
308
|
}
|
|
376
309
|
recent.sort((a, b) => b.when - a.when);
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
// one still being written to.
|
|
310
|
+
// One session is one row even when two data folders hold a copy. Newest
|
|
311
|
+
// first, so the copy kept is the one still being written to.
|
|
380
312
|
const seenIds = new Set();
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
// 1,200 were opened, and every one of those was started by agentbox itself,
|
|
384
|
-
// so a conversation started by hand was never looked at and the import card
|
|
385
|
-
// said there was nothing, as if the app were not looking in Claude Code at
|
|
386
|
-
// all. Somebody running dozens of agents fills this folder with ours. So it
|
|
387
|
-
// opens newest first until it has `max` of the person's own, or until
|
|
388
|
-
// READ_BUDGET_MS has gone, whichever is first. Reading all 9,073 heads took
|
|
389
|
-
// 5.0s there.
|
|
313
|
+
// The cap counts threads kept, not files opened: on a machine running many
|
|
314
|
+
// workers, the newest thousand transcripts can all be Agentbox's own.
|
|
390
315
|
const started = Date.now();
|
|
391
316
|
let opened = 0;
|
|
317
|
+
// The moment the read gave up at, zero when it read the whole window. A
|
|
318
|
+
// name is only known once the file is open, so a search that stopped in
|
|
319
|
+
// August must not answer "no such chat" as if it had looked at September.
|
|
320
|
+
let reached = 0;
|
|
321
|
+
let lastOpened = 0;
|
|
322
|
+
// The desktop app's own record wins over its transcript, but how the chat
|
|
323
|
+
// ended is only in the transcript.
|
|
324
|
+
const desktopFiles = new Map();
|
|
392
325
|
for (const { file, when } of recent) {
|
|
393
|
-
if (out.length >= max || Date.now() - started >
|
|
326
|
+
if (out.length >= max || Date.now() - started > budgetMs) {
|
|
394
327
|
skipped.unread = recent.length - opened;
|
|
328
|
+
reached = lastOpened;
|
|
395
329
|
break;
|
|
396
330
|
}
|
|
397
331
|
opened += 1;
|
|
332
|
+
lastOpened = when;
|
|
398
333
|
const id = path.basename(file, '.jsonl');
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
334
|
+
// The desktop app writes a thread twice, as a transcript here and as its
|
|
335
|
+
// own record joined by `cliSessionId`. Its record wins because it has a
|
|
336
|
+
// real title.
|
|
337
|
+
if (skipIds.has(id)) {
|
|
338
|
+
skipped.desktopDupe += 1;
|
|
339
|
+
if (!desktopFiles.has(id)) desktopFiles.set(id, file);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
404
342
|
if (seenIds.has(id)) { skipped.copy += 1; continue; }
|
|
405
|
-
const { cwd, entrypoint, said } = readTranscriptHead(file);
|
|
406
|
-
//
|
|
407
|
-
//
|
|
343
|
+
const { cwd, entrypoint, said, firstAt } = readTranscriptHead(file);
|
|
344
|
+
// Only a readable copy counts, so a newer copy cut off mid-write does not
|
|
345
|
+
// hide a good older one.
|
|
408
346
|
if (cwd || entrypoint) seenIds.add(id);
|
|
409
347
|
if (!startedByHand(entrypoint)) { skipped.started += 1; continue; }
|
|
410
348
|
if (isScratchFolder(cwd, home)) { skipped.scratch += 1; continue; }
|
|
@@ -419,15 +357,53 @@ function readTerminalThreads({ home, dirs, cutoff, max, skipIds = new Set() }) {
|
|
|
419
357
|
title,
|
|
420
358
|
when,
|
|
421
359
|
path: file,
|
|
360
|
+
firstSaid: firstTyped(said),
|
|
361
|
+
startedAt: firstAt || when,
|
|
422
362
|
});
|
|
423
363
|
}
|
|
424
|
-
return { threads: out, skipped };
|
|
364
|
+
return { threads: out, skipped, reached, desktopFiles };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** How many of the newest threads get their end read, and for how long. The
|
|
368
|
+
* head walk above has its own budget, so this one only ever adds detail to
|
|
369
|
+
* threads already listed. */
|
|
370
|
+
const ENDS_MOST = 120;
|
|
371
|
+
const ENDS_BUDGET_MS = 2000;
|
|
372
|
+
|
|
373
|
+
function withEnds(threads, desktopFiles) {
|
|
374
|
+
const started = Date.now();
|
|
375
|
+
return threads.map((t, i) => {
|
|
376
|
+
const file = t.source === 'desktop' ? desktopFiles.get(t.id) ?? null : t.path;
|
|
377
|
+
if (i >= ENDS_MOST || Date.now() - started > ENDS_BUDGET_MS || !file) {
|
|
378
|
+
// Not "finished": nothing is known of how it ended until it is picked.
|
|
379
|
+
return { ...t, waiting: false, ask: '', lastSaid: '', endUnknown: true };
|
|
380
|
+
}
|
|
381
|
+
const end = endOf(file);
|
|
382
|
+
const firstSaid = t.firstSaid ?? firstTyped(readTranscriptHead(file).said);
|
|
383
|
+
return {
|
|
384
|
+
...t,
|
|
385
|
+
title: t.source === 'desktop' ? t.title : threadTitle(end.aiTitle) || t.title,
|
|
386
|
+
waiting: end.waiting,
|
|
387
|
+
ask: end.ask,
|
|
388
|
+
lastSaid: end.lastSaid,
|
|
389
|
+
firstSaid,
|
|
390
|
+
};
|
|
391
|
+
});
|
|
425
392
|
}
|
|
426
393
|
|
|
427
|
-
/**
|
|
428
|
-
*
|
|
429
|
-
|
|
430
|
-
|
|
394
|
+
/** The chats somebody picked, with the end read for any the list left unread
|
|
395
|
+
* (past its cap, or found by name). Codex and desktop chats come read. */
|
|
396
|
+
export function readChatEnds(threads) {
|
|
397
|
+
return threads.map((t) => {
|
|
398
|
+
if (t.source === 'codex' || t.source === 'desktop' || !t.path) return t;
|
|
399
|
+
if (t.lastSaid !== undefined && !t.endUnknown) return t;
|
|
400
|
+
const { endUnknown, ...rest } = t;
|
|
401
|
+
return withEnds([rest], new Map())[0];
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Lists every transcript, subagents included so the caller can count them.
|
|
406
|
+
* Not capped: listing names is cheap, and the cap is on opening. */
|
|
431
407
|
function walkTranscripts(dir, into, depth = 4) {
|
|
432
408
|
if (depth < 0) return;
|
|
433
409
|
let entries = [];
|
|
@@ -439,30 +415,17 @@ function walkTranscripts(dir, into, depth = 4) {
|
|
|
439
415
|
}
|
|
440
416
|
}
|
|
441
417
|
|
|
442
|
-
/* ------------------------------- THE DESKTOP SIDE -------------------------- */
|
|
443
|
-
|
|
444
|
-
/** Where the desktop app keeps what it knows about a session. */
|
|
445
418
|
export function desktopSessionsDir(home = os.homedir()) {
|
|
446
419
|
return path.join(home, 'Library', 'Application Support', 'Claude', 'claude-code-sessions');
|
|
447
420
|
}
|
|
448
421
|
|
|
449
|
-
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
* Its record is small and it carries a real `title` the app wrote, plus the
|
|
453
|
-
* folder, the branch and the moment of the last turn. So there is no
|
|
454
|
-
* transcript to read and no name to guess: measured on her Mac 2026-08-29, 38
|
|
455
|
-
* records, every one of them titled, for example "Fix duplicate cowork task
|
|
456
|
-
* display issue".
|
|
457
|
-
*
|
|
458
|
-
* Archived ones are left out. She archived them, which is the desktop app's
|
|
459
|
-
* word for done with. */
|
|
422
|
+
/** The desktop app's record carries a real `title`, the folder and the time of
|
|
423
|
+
* the last turn, so no transcript is read. Archived sessions are left out. */
|
|
460
424
|
function readDesktopThreads({ home, cutoff, max }) {
|
|
461
425
|
const root = desktopSessionsDir(home);
|
|
462
426
|
const skipped = { old: 0, scratch: 0, archived: 0, empty: 0 };
|
|
463
427
|
const out = [];
|
|
464
|
-
// Every id this side owns, recent or not, so the transcript walk
|
|
465
|
-
// them alone rather than draw the same thread under a worse name.
|
|
428
|
+
// Every id this side owns, recent or not, so the transcript walk skips them.
|
|
466
429
|
const owned = new Set();
|
|
467
430
|
const files = [];
|
|
468
431
|
walkDesktop(root, files, max);
|
|
@@ -486,6 +449,7 @@ function readDesktopThreads({ home, cutoff, max }) {
|
|
|
486
449
|
title,
|
|
487
450
|
when,
|
|
488
451
|
path: file,
|
|
452
|
+
startedAt: Number(rec?.createdAt || when),
|
|
489
453
|
});
|
|
490
454
|
}
|
|
491
455
|
return { threads: out, skipped, owned };
|
|
@@ -503,46 +467,96 @@ function walkDesktop(dir, into, max, depth = 3) {
|
|
|
503
467
|
}
|
|
504
468
|
}
|
|
505
469
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* BOTH SOURCES, NEWEST FIRST, THE HANDFUL SHE MEANT.
|
|
510
|
-
*
|
|
511
|
-
* `days` is her "last few days" and it is a parameter rather than a constant
|
|
512
|
-
* because the number is the one thing on this card somebody may want to argue
|
|
513
|
-
* about. Everything else here is a fact about the file.
|
|
514
|
-
*
|
|
515
|
-
* One row per thread and no folding by folder: the folder is on the row and
|
|
516
|
-
* the card groups by it, the same way it already groups agent files. */
|
|
470
|
+
/** Terminal, editor and desktop threads a person started, newest first, one
|
|
471
|
+
* row per thread. */
|
|
517
472
|
export function readSessionThreads({
|
|
518
473
|
home = os.homedir(),
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
// the
|
|
522
|
-
// caller's process happens to carry.
|
|
474
|
+
// The app passes the data folders it found with its own environment and
|
|
475
|
+
// settings. The default is the folders under `home`, ignoring whatever
|
|
476
|
+
// CLAUDE_CONFIG_DIR the calling process has.
|
|
523
477
|
dirs = claudeDataDirs({ home }),
|
|
524
478
|
days = RECENT_DAYS,
|
|
525
479
|
now = Date.now(),
|
|
526
480
|
max = MAX_READS,
|
|
481
|
+
// The card's list keeps the three seconds it was measured for; a search is
|
|
482
|
+
// allowed longer, reaching back a year with somebody waiting on it.
|
|
483
|
+
budgetMs = READ_BUDGET_MS,
|
|
484
|
+
// Whether to read each listed chat's end too: its own title, its last reply
|
|
485
|
+
// and whether it is waiting on you. The year-long search does not need it.
|
|
486
|
+
ends = true,
|
|
527
487
|
} = {}) {
|
|
528
488
|
const cutoff = now - Math.max(0, Number(days) || 0) * 86400 * 1000;
|
|
529
|
-
// The desktop side goes first so its ids are known before the transcripts
|
|
530
|
-
// walked, which
|
|
489
|
+
// The desktop side goes first so its ids are known before the transcripts
|
|
490
|
+
// are walked, which stops one thread appearing twice.
|
|
531
491
|
const desk = readDesktopThreads({ home, cutoff, max });
|
|
532
|
-
const term = readTerminalThreads({ home, dirs, cutoff, max, skipIds: desk.owned });
|
|
533
|
-
const
|
|
492
|
+
const term = readTerminalThreads({ home, dirs, cutoff, max, budgetMs, skipIds: desk.owned });
|
|
493
|
+
const listed = [...term.threads, ...desk.threads].sort((a, b) => b.when - a.when);
|
|
494
|
+
const threads = ends ? withEnds(listed, term.desktopFiles) : listed;
|
|
534
495
|
return {
|
|
535
496
|
threads,
|
|
536
497
|
skipped: { ...term.skipped, desktopOld: desk.skipped.old, archived: desk.skipped.archived },
|
|
498
|
+
/** Zero when the whole window was read. See `reached` above. */
|
|
499
|
+
reached: term.reached,
|
|
537
500
|
};
|
|
538
501
|
}
|
|
539
502
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
503
|
+
/* -------------------- AND THE ONE THE NEW-CHATS WATCH USES ------------------ */
|
|
504
|
+
// The watch (main/new-chats-watch.mjs) runs every minute on the main process,
|
|
505
|
+
// so it reads only the folders the projects point at, which also leaves out
|
|
506
|
+
// Agentbox's own runs in their task folders, only files changed since the
|
|
507
|
+
// switch went on, and each file once until it changes.
|
|
508
|
+
|
|
509
|
+
const PROJECT_CHATS_KEPT = 2000;
|
|
510
|
+
const projectChats = new Map();
|
|
511
|
+
const slugOf = (folder) => String(folder ?? '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
512
|
+
const sameFolder = (a, b) => String(a ?? '').replace(/\/+$/, '') === String(b ?? '').replace(/\/+$/, '');
|
|
513
|
+
|
|
514
|
+
function readOneChat(file, { id, folder, home, when }) {
|
|
515
|
+
const { cwd, entrypoint, said, firstAt } = readTranscriptHead(file);
|
|
516
|
+
if (!startedByHand(entrypoint) || !sameFolder(cwd, folder)) return null;
|
|
517
|
+
const title = threadTitle(said);
|
|
518
|
+
if (!title) return null;
|
|
519
|
+
const end = endOf(file);
|
|
520
|
+
return {
|
|
521
|
+
id, source: sourceOf(entrypoint), folder: cwd, folderName: path.basename(cwd), short: shortFolder(cwd, home),
|
|
522
|
+
title: threadTitle(end.aiTitle) || title, when, path: file,
|
|
523
|
+
waiting: end.waiting, ask: end.ask, lastSaid: end.lastSaid, firstSaid: firstTyped(said), startedAt: firstAt || when,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function readProjectChats({ home = os.homedir(), dirs = claudeDataDirs({ home }), folders = [], since = 0 } = {}) {
|
|
528
|
+
const out = [];
|
|
529
|
+
const seen = new Set();
|
|
530
|
+
for (const dir of dirs) {
|
|
531
|
+
for (const folder of folders) {
|
|
532
|
+
const at = path.join(dir, 'projects', slugOf(folder));
|
|
533
|
+
let names = [];
|
|
534
|
+
try { names = fs.readdirSync(at); } catch { continue; }
|
|
535
|
+
for (const name of names) {
|
|
536
|
+
if (!name.endsWith('.jsonl')) continue;
|
|
537
|
+
const id = name.slice(0, -'.jsonl'.length);
|
|
538
|
+
if (seen.has(id)) continue;
|
|
539
|
+
const file = path.join(at, name);
|
|
540
|
+
let st;
|
|
541
|
+
try { st = fs.statSync(file); } catch { continue; }
|
|
542
|
+
if (st.mtimeMs < since) continue;
|
|
543
|
+
const kept = projectChats.get(file);
|
|
544
|
+
let thread = kept?.thread ?? null;
|
|
545
|
+
if (!kept || kept.size !== st.size || kept.mtimeMs !== st.mtimeMs) {
|
|
546
|
+
thread = readOneChat(file, { id, folder, home, when: st.mtimeMs });
|
|
547
|
+
projectChats.delete(file);
|
|
548
|
+
projectChats.set(file, { size: st.size, mtimeMs: st.mtimeMs, thread });
|
|
549
|
+
if (projectChats.size > PROJECT_CHATS_KEPT) projectChats.delete(projectChats.keys().next().value);
|
|
550
|
+
}
|
|
551
|
+
if (thread) { seen.add(id); out.push(thread); }
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return out.sort((a, b) => b.when - a.when);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Groups threads by folder, each folder sorted by its newest thread. Expects
|
|
559
|
+
* the threads newest first, as readSessionThreads returns them. */
|
|
546
560
|
export function threadsByFolder(threads = []) {
|
|
547
561
|
const byFolder = new Map();
|
|
548
562
|
for (const t of threads) {
|