agentbox-app 0.1.7 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -58
- package/bin/agentbox.mjs +18 -3
- package/briefs/message-rules.md +24 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +11 -11
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +116 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/approvals.mjs +14 -5
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +32 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +25 -13
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +18 -0
- package/main/codex-launch-env.mjs +31 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +82 -17
- package/main/demo.mjs +17 -14
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +204 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +7 -4
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +9 -3
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +134 -60
- package/main/ipc.mjs +335 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +48 -1
- package/main/main.mjs +138 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/notify.mjs +1 -1
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +21 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +115 -51
- package/main/runs-on.mjs +93 -0
- package/main/serve.mjs +77 -5
- package/main/settings.mjs +245 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +84 -7
- package/main/store/project.mjs +15 -19
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +245 -13
- package/main/supervisor.mjs +1634 -417
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +94 -16
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +14 -2
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +10 -12
- package/mcp/core/work.mjs +123 -26
- package/mcp/tools.mjs +13 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
- package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +7 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +39 -16
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +38 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +6 -6
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-modes.mjs +1 -1
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +6 -6
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +34 -4
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +9 -3
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/runs-on.d.mts +20 -0
- package/shared/runs-on.mjs +79 -0
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +40 -19
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +258 -28
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
package/main/agents.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { execFile } from 'node:child_process';
|
|
|
18
18
|
import { promisify } from 'node:util';
|
|
19
19
|
import { NAME, Name, WAS } from '../shared/product-name.mjs';
|
|
20
20
|
import {
|
|
21
|
-
WORK_CHARS, agentKey, changedFile, fullSubject, saidCount, sameInstant, threadWindow, workSubject, workVerb,
|
|
21
|
+
WORK_CHARS, agentKey, changedFile, commandWork, fullSubject, isBookkeeping, saidCount, sameInstant, threadWindow, workSubject, workVerb,
|
|
22
22
|
} from '../shared/agents.mjs';
|
|
23
23
|
|
|
24
24
|
const run = promisify(execFile);
|
|
@@ -64,7 +64,7 @@ function stillTheSame(rec, proc) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/* ------------------------------- the sources ----------------------------- */
|
|
67
|
-
// TWO SOURCES, UNIONED, because
|
|
67
|
+
// TWO SOURCES, UNIONED, because when measured neither one alone saw the whole
|
|
68
68
|
// machine. `claude agents --json` (the public scripting flag) listed 3 sessions
|
|
69
69
|
// while 16 were live with sockets bound; the per-session state files listed
|
|
70
70
|
// those 16 and none of the 3. They disagree about which sessions they know, and
|
|
@@ -125,35 +125,29 @@ function scrubbed(env) {
|
|
|
125
125
|
// Claude Code already writes three facts into the same transcript this function
|
|
126
126
|
// is already reading, and they were simply being skipped:
|
|
127
127
|
//
|
|
128
|
-
// ai-title a one-line title the session wrote for ITSELF.
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// the
|
|
132
|
-
// last-prompt the last thing SHE typed into that session. "okay no worries.
|
|
133
|
-
// just make sure that auth persists across sessions…"
|
|
128
|
+
// ai-title a one-line title the session wrote for ITSELF. Every session
|
|
129
|
+
// measured had one inside the 96KB tail, and it is usually the
|
|
130
|
+
// sentence a person would use to recall the session.
|
|
131
|
+
// last-prompt the last thing the user typed into that session.
|
|
134
132
|
// tool_use the files it has been editing, reading and writing.
|
|
135
133
|
//
|
|
136
134
|
// So the reading is one pass over a tail that was already being read, and the
|
|
137
135
|
// card goes from three lines about a pid to what the work actually is.
|
|
138
136
|
//
|
|
139
|
-
// WHAT WAS MISSING WAS THE BEGINNING.
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// The conversation she needs to recall fits on a card. The agent's side is what
|
|
143
|
-
// is megabytes.
|
|
137
|
+
// WHAT WAS MISSING WAS THE BEGINNING. The human side of a conversation fits on
|
|
138
|
+
// a card. The agent's side is what is megabytes.
|
|
144
139
|
//
|
|
145
140
|
// AND THE TAIL CANNOT SEE IT. The opening turn lands between 11KB and 614KB in
|
|
146
|
-
// (
|
|
141
|
+
// (pasted images are single transcript lines of hundreds of KB), and 96KB
|
|
147
142
|
// off the end reaches none of it. Measured: a 1MB head plus a 512KB tail still
|
|
148
|
-
// saw only
|
|
149
|
-
// session-71 was the one she is complaining about.
|
|
143
|
+
// saw only 46% of the human turns.
|
|
150
144
|
//
|
|
151
145
|
// So the whole file is read, ONCE, and then only what has been appended since.
|
|
152
|
-
// Measured
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
146
|
+
// Measured: the first pass over 15 transcripts, 117MB, costs 353ms in total
|
|
147
|
+
// and never happens again for that session; after it, a refresh reads only the
|
|
148
|
+
// bytes that arrived. This is the same read the tail was already doing, moved
|
|
149
|
+
// from "the last 96KB" to "everything, once". No model sees any of it, by
|
|
150
|
+
// design.
|
|
157
151
|
const CHUNK_BYTES = 4 << 20;
|
|
158
152
|
const historyCache = new Map(); // sessionId -> { file, size, mtimeMs, offset, ...facts }
|
|
159
153
|
const dirCache = new Map(); // sessionId -> transcript path
|
|
@@ -186,11 +180,11 @@ function transcriptPath(sessionId, cwd) {
|
|
|
186
180
|
const NOTHING = { lastActiveAt: 0, lastSaid: '', saidAt: 0, saidOpen: false, about: '', lastAsked: '', touched: [], asked: [] };
|
|
187
181
|
|
|
188
182
|
// HOW MUCH OF THE LAST REPLY IS KEPT. The card quotes the end of it, so this is
|
|
189
|
-
// a bound on the accumulator and not on what
|
|
190
|
-
//
|
|
183
|
+
// a bound on the accumulator and not on what the user reads. A human reply is
|
|
184
|
+
// rarely anywhere near 6KB.
|
|
191
185
|
const SAID_CHARS = 8000;
|
|
192
186
|
|
|
193
|
-
// WHAT COUNTS AS SOMETHING
|
|
187
|
+
// WHAT COUNTS AS SOMETHING THE USER TYPED. A Claude Code transcript calls a great
|
|
194
188
|
// many things a `user` message that no human wrote: every tool result comes
|
|
195
189
|
// back as one, every system reminder is one, every slash command expands into
|
|
196
190
|
// one. Reading those onto the card would fill her recall with the machine
|
|
@@ -198,7 +192,7 @@ const SAID_CHARS = 8000;
|
|
|
198
192
|
//
|
|
199
193
|
// So a turn is hers only when it is a `user` line that is not flagged meta, is
|
|
200
194
|
// not a compaction summary, carries no tool result, and whose text does not
|
|
201
|
-
// open with a tag. Checked against
|
|
195
|
+
// open with a tag. Checked against 155 real human turns.
|
|
202
196
|
//
|
|
203
197
|
// THE CARD AND THE CONVERSATION READ THE SAME LINE DIFFERENTLY, which is why
|
|
204
198
|
// this is two functions and not one. A card quotes a sentence, so it flattens
|
|
@@ -210,9 +204,12 @@ const RELAY_MARK = `Relayed from ${NAME},`;
|
|
|
210
204
|
// WRITTEN UNDER THE CURRENT NAME, RECOGNISED UNDER EVERY NAME THIS APP HAS
|
|
211
205
|
// HAD. A transcript is a file on her disk and it outlives a rename, so a
|
|
212
206
|
// marker matched on the current name alone stops matching the day the name
|
|
213
|
-
// changes, and the app's own preamble starts reading as something
|
|
214
|
-
|
|
215
|
-
|
|
207
|
+
// changes, and the app's own preamble starts reading as something the user typed.
|
|
208
|
+
// The letter case is ignored as well: the name was written lowercase until
|
|
209
|
+
// 2026-10-02, and those transcripts say "Relayed from agentbox,".
|
|
210
|
+
const RELAY_MARKERS = [RELAY_MARK, ...WAS.map((was) => `Relayed from ${was},`)].map((m) => m.toLowerCase());
|
|
211
|
+
const wasRelayed = (line) => RELAY_MARKERS.some((m) => line.toLowerCase().startsWith(m));
|
|
212
|
+
export { wasRelayed as __wasRelayed };
|
|
216
213
|
|
|
217
214
|
function herTurnRaw(obj) {
|
|
218
215
|
if (obj.type !== 'user' || obj.isMeta || obj.isCompactSummary || obj.toolUseResult) return '';
|
|
@@ -225,10 +222,10 @@ function herTurnRaw(obj) {
|
|
|
225
222
|
const clean = String(text).trim();
|
|
226
223
|
const flat = clean.replace(/\s+/g, ' ');
|
|
227
224
|
// AND NEVER AGENTBOX'S OWN PREAMBLE. `fromTheApp` puts a sentence of context in
|
|
228
|
-
// front of what
|
|
225
|
+
// front of what the user typed, and today Claude Code stamps the result `isMeta`
|
|
229
226
|
// so this never sees it. If a future one records it as a plain user turn
|
|
230
|
-
// instead, the card would quote Agentbox talking to an agent back at
|
|
231
|
-
// own words. `spoke` already holds what
|
|
227
|
+
// instead, the card would quote Agentbox talking to an agent back at the user
|
|
228
|
+
// as their own words. `spoke` already holds what the user actually typed.
|
|
232
229
|
if (!flat || flat.startsWith('<') || flat.startsWith('[Request interrupted') || wasRelayed(flat)) return '';
|
|
233
230
|
return clean;
|
|
234
231
|
}
|
|
@@ -431,7 +428,7 @@ function fingerprint(agents) {
|
|
|
431
428
|
// a tick and pushing on it re-renders her window for nothing. The first moves
|
|
432
429
|
// only when the session's status moves, and since a row's LIST depends on it:
|
|
433
430
|
// a session that went busy and back to idle between two readings shows the
|
|
434
|
-
// same `status` both times, and without this the row
|
|
431
|
+
// same `status` both times, and without this the row the user replied to would sit
|
|
435
432
|
// in In progress until some other push happened to come. AND WHEN IT LAST
|
|
436
433
|
// SAID SOMETHING, which is the fact this list existed to notice and did
|
|
437
434
|
// not.An agent that answers her while she is looking at its card moves
|
|
@@ -591,12 +588,12 @@ function defaultSocket(pid) {
|
|
|
591
588
|
// Nothing here claims her authority.
|
|
592
589
|
//
|
|
593
590
|
// What it does say is the part the wrapper cannot: WHERE the message came from
|
|
594
|
-
// and WHAT it is about. MEASURED on session
|
|
595
|
-
//
|
|
596
|
-
// whole of the difference.
|
|
591
|
+
// and WHAT it is about. MEASURED on a session that had given no response
|
|
592
|
+
// without it; one sentence of context is the whole of the difference.
|
|
597
593
|
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
594
|
+
// THE USER'S OWN TEXT IS UNCHANGED AND IS WHAT THE CARD QUOTES: `spoke` records
|
|
595
|
+
// what the user typed, never this, so nothing the user never wrote is ever drawn
|
|
596
|
+
// as their words.
|
|
600
597
|
export function fromTheApp(message) {
|
|
601
598
|
return [
|
|
602
599
|
`${RELAY_MARK} the inbox your user reads. A person typed this, not an agent,`,
|
|
@@ -622,7 +619,7 @@ export async function reply(pid, text, { timeoutMs = 3000 } = {}) {
|
|
|
622
619
|
const before = await watermark(agent);
|
|
623
620
|
try {
|
|
624
621
|
// What goes on the wire says who is asking; what is matched and recorded
|
|
625
|
-
// below stays
|
|
622
|
+
// below stays the user's words, so `heldNotice` cannot start matching every
|
|
626
623
|
// message by its shared opening and the card cannot quote a preamble.
|
|
627
624
|
await writeToSocket(agent.socket, fromTheApp(message), tokenFor(pid));
|
|
628
625
|
} catch (err) {
|
|
@@ -787,7 +784,7 @@ function heldNotice(agent, sinceSize, message) {
|
|
|
787
784
|
const content = String(obj?.content ?? '');
|
|
788
785
|
if (!HOLD_LINE.test(content)) continue;
|
|
789
786
|
if (opening && !content.includes(opening.slice(0, 40))) continue;
|
|
790
|
-
// Said in
|
|
787
|
+
// Said in plain words, not the CLI's. What the user can DO about it is the point:
|
|
791
788
|
// the message is sitting in that session's own window waiting for a yes.
|
|
792
789
|
return `${agent.name} held your message for approval in its own window, so it has not read it yet.`;
|
|
793
790
|
}
|
|
@@ -846,8 +843,8 @@ export function reveal(pid) {
|
|
|
846
843
|
// the one surface built to make her trust the app. readline hands the lines
|
|
847
844
|
// back a chunk at a time and the app draws in between.
|
|
848
845
|
//
|
|
849
|
-
// NO MODEL SEES ANY OF IT, which
|
|
850
|
-
// nothing is written anywhere: this opens a file
|
|
846
|
+
// NO MODEL SEES ANY OF IT, which is a condition of the whole feature, and
|
|
847
|
+
// nothing is written anywhere: this opens a file the user already has and reads it.
|
|
851
848
|
|
|
852
849
|
// One turn is capped here rather than where it is drawn, so a pasted 600KB
|
|
853
850
|
// blob never becomes 600KB of React. It is far more than anybody reads and far
|
|
@@ -894,7 +891,13 @@ function workedOn(obj, events, byId, dir = '') {
|
|
|
894
891
|
const at = Date.parse(obj.timestamp ?? 0) || 0;
|
|
895
892
|
for (const c of obj.message.content) {
|
|
896
893
|
if (c.type !== 'tool_use') continue;
|
|
897
|
-
|
|
894
|
+
// The store's own records are how a session talks to the app, not work it
|
|
895
|
+
// did, and a thread that opens on them reads as noise (w-0bd0d8b2ef).
|
|
896
|
+
if (isBookkeeping(c.name)) continue;
|
|
897
|
+
// WHAT A COMMAND DID, IN WORDS, when the whole of it is recognised; null on
|
|
898
|
+
// anything else, and the line is then the command it always was.
|
|
899
|
+
const plain = c.name === 'Bash' ? commandWork(c.input?.command, dir, os.homedir()) : null;
|
|
900
|
+
const subject = plain ? plain.subject : workSubject(c.input, dir, os.homedir());
|
|
898
901
|
const whole = fullSubject(c.input);
|
|
899
902
|
// THE FILE IT CHANGED, WHOLE, so the thread can offer it as a chip into the
|
|
900
903
|
// code. Empty on everything that did not write a file, which is most calls:
|
|
@@ -903,10 +906,13 @@ function workedOn(obj, events, byId, dir = '') {
|
|
|
903
906
|
const event = {
|
|
904
907
|
kind: 'work',
|
|
905
908
|
at,
|
|
906
|
-
verb: workVerb(c.name),
|
|
909
|
+
verb: plain ? plain.verb : workVerb(c.name),
|
|
907
910
|
subject,
|
|
908
911
|
...(changed ? { file: changed } : {}),
|
|
909
|
-
|
|
912
|
+
// A line put in words always carries the command, even when the words name
|
|
913
|
+
// nothing ("Checked what changed"): the words replaced the only copy of it
|
|
914
|
+
// on the screen, so it has to stay one press behind them.
|
|
915
|
+
full: plain || (whole && whole.replace(/\s+/g, ' ').trim() !== subject) ? cappedHead(whole) : '',
|
|
910
916
|
output: '',
|
|
911
917
|
lines: 0,
|
|
912
918
|
failed: false,
|
|
@@ -1057,11 +1063,11 @@ export async function conversation({ pid, sessionId, cwd, spoke = [], whole = fa
|
|
|
1057
1063
|
const its = saidByIt(obj);
|
|
1058
1064
|
if (its) addTurn(turns, 'it', its, at);
|
|
1059
1065
|
// THE WORK GOES IN THE THREAD, IN THE ORDER IT HAPPENED, and it ends the
|
|
1060
|
-
// message above it.
|
|
1066
|
+
// message above it. The chosen shape says each thing it ran sits BETWEEN the
|
|
1061
1067
|
// messages, so a reply that stopped twice for a tool is three things on
|
|
1062
1068
|
// the screen in the order they occurred rather than one bubble with the
|
|
1063
1069
|
// work swept behind it. This is the one place the fold is deliberately
|
|
1064
|
-
// broken, and breaking it is the shape
|
|
1070
|
+
// broken, and breaking it is the shape that was chosen.
|
|
1065
1071
|
workedOn(obj, turns, byId, dir);
|
|
1066
1072
|
}
|
|
1067
1073
|
} catch {
|
package/main/analytics.mjs
CHANGED
|
@@ -204,3 +204,30 @@ export function createAnalytics({ config, version, dir, client, env = process.en
|
|
|
204
204
|
async shutdown() { try { await posthog?.shutdown?.(); } catch {} },
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
|
+
|
|
208
|
+
// "USED TODAY", ONCE PER CALENDAR DAY (2026-10-06). Retention is read from
|
|
209
|
+
// whether an install was used on a day, and `app_opened` cannot say that: a Mac
|
|
210
|
+
// app left open for a week sends it once. This is touched at launch and every
|
|
211
|
+
// time the window comes forward, and sends only on the first touch of each
|
|
212
|
+
// local day. Kept in memory: a relaunch on the same day may send it again,
|
|
213
|
+
// which PostHog's per-install, per-day counting absorbs.
|
|
214
|
+
export function createDailyCount(track) {
|
|
215
|
+
let last = null;
|
|
216
|
+
return (now) => {
|
|
217
|
+
const d = new Date(now);
|
|
218
|
+
const day = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
219
|
+
if (day === last) return;
|
|
220
|
+
last = day;
|
|
221
|
+
track('day_active');
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// WHAT AN AGENT'S RUN REPORTS WHEN IT ENDS: how long, whether it failed, and
|
|
226
|
+
// which engine. Nothing about the task it was on.
|
|
227
|
+
export function runEndedProps(session, now) {
|
|
228
|
+
return {
|
|
229
|
+
seconds: Math.round((now - (session.startedAt ?? now)) / 1000),
|
|
230
|
+
failed: !!session.exitFailed,
|
|
231
|
+
engine: session.engine,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -26,7 +26,7 @@ import { answerIsHers, appendSpoolLine, cardDigest, readSpoolJson, removeSpoolEn
|
|
|
26
26
|
|
|
27
27
|
const SPOOL = process.env.ZERO_APPROVALS_DIR;
|
|
28
28
|
|
|
29
|
-
// THE KEY THAT SAYS AN ANSWER IS
|
|
29
|
+
// THE KEY THAT SAYS AN ANSWER IS THE USER'S, AND IT IS SAFE TO HAND US.
|
|
30
30
|
//
|
|
31
31
|
// This process is the worker's own child, so its environment is the worker's to
|
|
32
32
|
// read and anything secret given here is a secret the forger has. This is the
|
|
@@ -93,7 +93,7 @@ async function decide(args) {
|
|
|
93
93
|
// against the command THIS PROCESS is still holding. `request` here is
|
|
94
94
|
// that memory: it was built from the tool call and has never been read
|
|
95
95
|
// back off the disk, so comparing it against the digest she signed is
|
|
96
|
-
// the whole check. A mismatch is a deny --
|
|
96
|
+
// the whole check. A mismatch is a deny -- the user said yes to something
|
|
97
97
|
// else -- and it is loud, because only a rewrite can produce it.
|
|
98
98
|
const mine = cardDigest(request);
|
|
99
99
|
if (!mine || answer.card !== mine) {
|
package/main/approvals.mjs
CHANGED
|
@@ -387,12 +387,20 @@ const REQUEST_SUFFIX = '.request.json';
|
|
|
387
387
|
* FORWARD to now. A worker can therefore make its own request look younger than
|
|
388
388
|
* it is, which costs it the front of the stack, and can never make it look
|
|
389
389
|
* older. Nanoseconds because APFS keeps them and two files written back to back
|
|
390
|
-
* differ by about 170 microseconds
|
|
391
|
-
*
|
|
390
|
+
* differ by about 170 microseconds.
|
|
391
|
+
*
|
|
392
|
+
* A coarse clock stamps both files the same. tmpfs does this: two writes in
|
|
393
|
+
* one poll share a ctime, and `readdir` then comes back in name order, which
|
|
394
|
+
* is the forgery the paragraph above is about. The inode breaks that tie.
|
|
395
|
+
* The kernel assigns it, a later create in a fresh directory gets a higher
|
|
396
|
+
* one, and a worker cannot move it backwards. Equal inode and equal ctime
|
|
397
|
+
* keep the order we already had.
|
|
392
398
|
*/
|
|
393
399
|
function bornAt(dir, name) {
|
|
394
|
-
try {
|
|
395
|
-
|
|
400
|
+
try {
|
|
401
|
+
const st = fs.lstatSync(path.join(dir, name), { bigint: true });
|
|
402
|
+
return { ctime: st.ctimeNs, ino: st.ino };
|
|
403
|
+
} catch { return { ctime: 2n ** 63n, ino: 2n ** 63n }; } // gone or unreadable: never in front of something real
|
|
396
404
|
}
|
|
397
405
|
|
|
398
406
|
export function listPending(storeRoot, { now = Date.now } = {}) {
|
|
@@ -409,7 +417,8 @@ export function listPending(storeRoot, { now = Date.now } = {}) {
|
|
|
409
417
|
for (const { f } of names
|
|
410
418
|
.filter((f) => !seenAt.has(prefix + f.slice(0, -REQUEST_SUFFIX.length)))
|
|
411
419
|
.map((f) => ({ f, born: bornAt(dir, f) }))
|
|
412
|
-
.sort((a, b) => (a.born < b.born ? -1 : a.born > b.born ? 1
|
|
420
|
+
.sort((a, b) => (a.born.ctime < b.born.ctime ? -1 : a.born.ctime > b.born.ctime ? 1
|
|
421
|
+
: a.born.ino < b.born.ino ? -1 : a.born.ino > b.born.ino ? 1 : 0))) {
|
|
413
422
|
seenAt.set(prefix + f.slice(0, -REQUEST_SUFFIX.length), { at, seq: sightings += 1 });
|
|
414
423
|
}
|
|
415
424
|
const present = new Set();
|
package/main/artifact-path.mjs
CHANGED
|
@@ -125,6 +125,17 @@ export function resolveArtifact({ src, product, products = [], accountRoot, stor
|
|
|
125
125
|
}
|
|
126
126
|
} else {
|
|
127
127
|
for (const root of roots) candidates.push(path.join(root, raw));
|
|
128
|
+
// AND THE WORKER'S OWN CHECKOUT, when the path names its task. Every run
|
|
129
|
+
// works in `<repo>/.claude/worktrees/<task id>` (./task-folders.mjs), so
|
|
130
|
+
// `designs/w-1b574413db/a1.png` is relative to that folder, and the hunt
|
|
131
|
+
// below never finds it because it skips every folder starting with a dot.
|
|
132
|
+
// Only a task the path itself names: a bare `shots/a.png` must not reach
|
|
133
|
+
// into some other task's checkout (2026-10-04, w-7fe215448b).
|
|
134
|
+
if (own?.repoPath) {
|
|
135
|
+
for (const segment of raw.split('/')) {
|
|
136
|
+
if (/^w-[0-9a-f]{6,}$/i.test(segment)) candidates.push(path.join(own.repoPath, '.claude', 'worktrees', segment, raw));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
for (const full of candidates) {
|
package/main/claude-bin.mjs
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// the app found nothing, said "missing" on a grey dot in Settings, and gave no
|
|
7
7
|
// sentence and no next step. It read as broken rather than as unfinished.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// THE RULE this file exists to keep: never tell someone Claude Code is
|
|
10
|
+
// missing unless we are certain it is.
|
|
10
11
|
//
|
|
11
12
|
// So there are two different questions here and they must never be answered by
|
|
12
13
|
// the same value. `found` is whether a runnable file is really there, and it
|
|
@@ -45,10 +46,10 @@
|
|
|
45
46
|
// the same way, and it is a real state: the installer points ~/.local/bin/claude
|
|
46
47
|
// at a versioned file and swaps that file on every update.
|
|
47
48
|
//
|
|
48
|
-
// Nothing here overrides
|
|
49
|
-
// wins untouched and is never searched around, because that
|
|
50
|
-
//
|
|
51
|
-
// a setting
|
|
49
|
+
// Nothing here overrides the config file. An explicit `claudeBin` in
|
|
50
|
+
// zero.config.json wins untouched and is never searched around, because that
|
|
51
|
+
// file promises explicit paths with no discovery, and a search that
|
|
52
|
+
// second-guesses a written setting would break that promise.
|
|
52
53
|
|
|
53
54
|
import fs from 'node:fs';
|
|
54
55
|
import os from 'node:os';
|
|
@@ -70,6 +71,7 @@ export function candidatePaths(home = os.homedir()) {
|
|
|
70
71
|
path.join(home, '.npm-packages/bin/claude'), // actually write in a guide
|
|
71
72
|
path.join(home, '.yarn/bin/claude'), // yarn global
|
|
72
73
|
path.join(home, 'Library/pnpm/claude'), // pnpm's own bin on macOS
|
|
74
|
+
path.join(home, '.local/share/pnpm/claude'), // pnpm's own bin on Linux
|
|
73
75
|
path.join(home, '.deno/bin/claude'), // deno
|
|
74
76
|
path.join(home, 'bin/claude'), // a hand-rolled ~/bin on PATH
|
|
75
77
|
'/opt/local/bin/claude', // MacPorts
|
|
@@ -96,6 +98,27 @@ function defaultReaddir(dir) {
|
|
|
96
98
|
try { return fs.readdirSync(dir); } catch { return []; }
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
// THE COPY THE CLAUDE APP KEEPS FOR ITSELF, one folder per version, newest
|
|
102
|
+
// first. Somebody who has only ever used Claude Code inside the Claude app has
|
|
103
|
+
// this and nothing else (w-9f6975906c, measured 2026-10-05: 2.1.209 there, and
|
|
104
|
+
// it shared the CLI's sign-in). It is searched LAST, after the shell, because it
|
|
105
|
+
// updates when the app does and a real install is fresher.
|
|
106
|
+
export function appCopyPaths(home = os.homedir(), readdir = defaultReaddir) {
|
|
107
|
+
const dir = path.join(home, 'Library/Application Support/Claude/claude-code');
|
|
108
|
+
return readdir(dir)
|
|
109
|
+
.filter((v) => /^\d+(\.\d+)*$/.test(v))
|
|
110
|
+
.sort(newestFirst)
|
|
111
|
+
.map((v) => path.join(dir, v, 'claude.app/Contents/MacOS/claude'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function newestFirst(a, b) {
|
|
115
|
+
const x = a.split('.').map(Number), y = b.split('.').map(Number);
|
|
116
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
117
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0)) return (y[i] ?? 0) - (x[i] ?? 0);
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
// WHAT CLAUDE CODE LEAVES BEHIND. Any one of these means it has run on this
|
|
100
123
|
// Mac. None of them is a binary and none of them is ever spawned; they exist
|
|
101
124
|
// only to stop the app announcing an absence it cannot support.
|
|
@@ -263,6 +286,10 @@ export function findClaudeBin({
|
|
|
263
286
|
const fromShell = shellSaid(shellLookup());
|
|
264
287
|
if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
|
|
265
288
|
|
|
289
|
+
for (const candidate of appCopyPaths(home, readdir)) {
|
|
290
|
+
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
|
|
291
|
+
}
|
|
292
|
+
|
|
266
293
|
// NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
|
|
267
294
|
// shell we could reach missed, so the last question is whether this Mac shows
|
|
268
295
|
// any sign of Claude Code anyway. If it does, we looked in the wrong places
|
package/main/claude-input.mjs
CHANGED
|
@@ -12,6 +12,10 @@ function turnClosed(message) {
|
|
|
12
12
|
return error;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// How long a finished background job may go without the agent waking for it
|
|
16
|
+
// before the session is closed anyway. Measured wakes arrive in about a second.
|
|
17
|
+
const WAKE_GRACE_MS = 2 * 60 * 1000;
|
|
18
|
+
|
|
15
19
|
export function claudeStreamArgs(args) {
|
|
16
20
|
const copy = [...args];
|
|
17
21
|
const index = copy.indexOf('-p');
|
|
@@ -27,7 +31,21 @@ export function attachClaudeInput(child) {
|
|
|
27
31
|
const controls = new Map();
|
|
28
32
|
const seenUsers = new Set();
|
|
29
33
|
const pending = new Map();
|
|
30
|
-
|
|
34
|
+
// WHAT THE AGENT STARTED AND HAS NOT HEARD BACK FROM: a background command,
|
|
35
|
+
// a command Claude Code moved to the background past its time limit, a
|
|
36
|
+
// background helper. Claude Code lists them in `background_tasks_changed`
|
|
37
|
+
// and wakes the agent itself when one ends, but only while stdin is open. A
|
|
38
|
+
// result with any of these out is "I'll report once it finishes", not the
|
|
39
|
+
// end of the run, and closing there killed the suite it was waiting on
|
|
40
|
+
// (tests/an-agent-waiting-on-its-own-tests-does-not-come-back-to-you).
|
|
41
|
+
let waiting = [], wakeTimer = null;
|
|
42
|
+
// Input only ever closes at a result with nothing out, so a closed input is
|
|
43
|
+
// the one proof the run reached its end. A session that exits with input
|
|
44
|
+
// still open was stopped part-way, whatever its last turn said
|
|
45
|
+
// (tests/a-worker-whose-helpers-are-still-working-does-not-come-back-to-you).
|
|
46
|
+
let ended = false;
|
|
47
|
+
const closeInput = () => { closed = true; ended = true; clearTimeout(wakeTimer); child.stdin.end(); };
|
|
48
|
+
const mayClose = () => !held && atResult && !pending.size && !waiting.length;
|
|
31
49
|
const fail = reason => {
|
|
32
50
|
closed = true;
|
|
33
51
|
for (const request of pending.values()) {
|
|
@@ -72,16 +90,29 @@ export function attachClaudeInput(child) {
|
|
|
72
90
|
atResult = false;
|
|
73
91
|
request.resolve({ accepted: true });
|
|
74
92
|
}
|
|
93
|
+
if (event.type === 'system' && event.subtype === 'background_tasks_changed' && Array.isArray(event.tasks)) {
|
|
94
|
+
waiting = event.tasks;
|
|
95
|
+
// The last one ended between turns. Claude Code wakes the agent for
|
|
96
|
+
// it, and that turn's result closes as usual. The timer is only for a
|
|
97
|
+
// wake that never comes, so a session cannot sit open forever.
|
|
98
|
+
clearTimeout(wakeTimer);
|
|
99
|
+
if (!waiting.length && atResult && !closed) {
|
|
100
|
+
wakeTimer = setTimeout(() => { if (mayClose() && !closed) closeInput(); }, WAKE_GRACE_MS);
|
|
101
|
+
wakeTimer.unref?.();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
75
104
|
if (event.type === 'result') {
|
|
76
105
|
atResult = true;
|
|
77
|
-
if (
|
|
106
|
+
if (mayClose()) closeInput();
|
|
78
107
|
}
|
|
79
108
|
}
|
|
80
109
|
});
|
|
110
|
+
child.waitingOn = () => [...waiting];
|
|
111
|
+
child.ranToTheEnd = () => ended;
|
|
81
112
|
child.holdInput = value => {
|
|
82
113
|
if (closed && value) throw Error('This session has ended. Open remote control again to resume it.');
|
|
83
114
|
held = !!value;
|
|
84
|
-
if (
|
|
115
|
+
if (mayClose()) closeInput();
|
|
85
116
|
};
|
|
86
117
|
child.control = (request, timeoutMs = 30000) => new Promise((resolve, reject) => {
|
|
87
118
|
if (closed || !child.stdin.writable) return reject(Error('The agent connection is closed.'));
|
|
@@ -92,6 +123,17 @@ export function attachClaudeInput(child) {
|
|
|
92
123
|
try { child.stdin.write(JSON.stringify({ type:'control_request', request_id, request }) + '\n'); }
|
|
93
124
|
catch(error) { clearTimeout(timer); controls.delete(request_id); reject(error); }
|
|
94
125
|
});
|
|
126
|
+
// SEND NOW: cut the step the agent is in so her waiting message is answered
|
|
127
|
+
// as a turn of its own, instead of after a command that may run for minutes
|
|
128
|
+
// (tests/send-now-cuts-the-current-step-so-the-agent-answers). Measured on a
|
|
129
|
+
// 45-second command: 43 s of waiting without it, 1.4 s to the answer with it.
|
|
130
|
+
// ONLY WHILE A MESSAGE OF HERS IS WAITING. The cut turn ends in a result,
|
|
131
|
+
// and with nothing pending that result closes the input and ends the run.
|
|
132
|
+
child.interrupt = () => {
|
|
133
|
+
if (closed || !pending.size) return Promise.resolve({ interrupted: false });
|
|
134
|
+
child.lastInterruptAt = Date.now();
|
|
135
|
+
return child.control({ subtype: 'interrupt' }).then(() => ({ interrupted: true }));
|
|
136
|
+
};
|
|
95
137
|
// NO CLOCK ON THE ACKNOWLEDGEMENT. Claude replays a queued message at its next
|
|
96
138
|
// break, and a break can be minutes away: a worker inside one long command
|
|
97
139
|
// (a render poll, `sleep 12` fifty times) holds it until the command returns.
|
package/main/claude-plan.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// THE PROBLEM. Agentbox runs several Claude Code sessions at once and the number
|
|
6
6
|
// is a flat default (`maxConcurrentSessions: 3`, main/config.mjs) that was
|
|
7
|
-
// chosen
|
|
7
|
+
// chosen with the biggest plan in mind and has never been anything else. Nothing
|
|
8
8
|
// anywhere in the app mentions a plan: not the walk, not the Accounts page, not
|
|
9
9
|
// a row that dies. So a person on a smaller plan gets three sessions started on
|
|
10
10
|
// their behalf, all three burn the same allowance, and what they see is agents
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
// founder was asking out loud while she watched a tester use it.
|
|
13
13
|
//
|
|
14
14
|
// THE ANSWER IS ALREADY ON THE DISK. Claude Code writes its own account profile
|
|
15
|
-
// into its config file and refreshes it as it runs.
|
|
16
|
-
//
|
|
15
|
+
// into its config file and refreshes it as it runs. On a machine with two
|
|
16
|
+
// accounts signed in, for example:
|
|
17
17
|
//
|
|
18
18
|
// ~/.claude.json oauthAccount.organizationType claude_max
|
|
19
19
|
// oauthAccount.organizationRateLimitTier default_claude_max_20x
|
|
20
|
-
// profileFetchedAt
|
|
21
|
-
// ~/.claude-second/.claude.json the same two
|
|
22
|
-
// profileFetchedAt
|
|
20
|
+
// profileFetchedAt (within the day)
|
|
21
|
+
// ~/.claude-second/.claude.json the same two fields
|
|
22
|
+
// profileFetchedAt (within the day)
|
|
23
23
|
//
|
|
24
|
-
//
|
|
24
|
+
// Each is refetched within the day, so the field is live rather than a one-time
|
|
25
25
|
// stamp from a login months ago, and it is the honest source: it is Claude
|
|
26
26
|
// Code's own answer about its own subscription, not a guess of ours.
|
|
27
27
|
//
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
// runs with nothing set, so its file is `~/.claude.json`; a second account runs
|
|
31
31
|
// with CLAUDE_CONFIG_DIR pointing at a folder, so its file is inside that
|
|
32
32
|
// folder (main/supervisor.mjs sets exactly that per spawn). Both are verified
|
|
33
|
-
// above. Note that `~/.claude/.claude.json` also
|
|
34
|
-
//
|
|
33
|
+
// above. Note that a `~/.claude/.claude.json` can also exist and be days
|
|
34
|
+
// STALE — it is what a run with CLAUDE_CONFIG_DIR=~/.claude left behind.
|
|
35
35
|
// It is not read for the default account, and that is deliberate: the rule here
|
|
36
36
|
// is Claude Code's own rule, not a search for the likeliest looking file.
|
|
37
37
|
//
|
|
38
38
|
// WHAT THIS DELIBERATELY DOES NOT DO. It does not guess the strings for plans
|
|
39
|
-
// nobody here has seen
|
|
40
|
-
//
|
|
39
|
+
// nobody here has seen in a real file, and inventing `default_claude_pro` in
|
|
40
|
+
// a table would be a number nobody
|
|
41
41
|
// measured. So `max` is decided by looking for the word max, the label is
|
|
42
42
|
// derived from the raw string mechanically, and anything unreadable comes back
|
|
43
43
|
// `known: false` and changes nothing anywhere.
|
|
@@ -95,13 +95,25 @@ export function isMaxTier(raw) {
|
|
|
95
95
|
* as it was". */
|
|
96
96
|
export function readPlan(profile, { home = os.homedir(), file = null } = {}) {
|
|
97
97
|
const at = file ?? planFile(profile, home);
|
|
98
|
-
const none = { known: false, max: false, label: null, tier: null, from: null, file: at };
|
|
99
98
|
let account = null;
|
|
100
99
|
try {
|
|
101
100
|
account = JSON.parse(fs.readFileSync(at, 'utf8'))?.oauthAccount ?? null;
|
|
102
101
|
} catch {
|
|
103
|
-
return
|
|
102
|
+
return planOfAccount(null, at);
|
|
104
103
|
}
|
|
104
|
+
return planOfAccount(account, at);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* THE SAME ANSWER OFF AN `oauthAccount` SOMEBODY ALREADY HAS IN HAND.
|
|
109
|
+
*
|
|
110
|
+
* Lifted out of `readPlan` so a caller that has read the file for its own
|
|
111
|
+
* reasons (main/runs-on.mjs, which also wants to know whether there is a login
|
|
112
|
+
* in there at all) does not carry a second copy of the tier ordering. One copy
|
|
113
|
+
* of that rule, and the file read stays in one place per caller.
|
|
114
|
+
*/
|
|
115
|
+
export function planOfAccount(account, at = null) {
|
|
116
|
+
const none = { known: false, max: false, label: null, tier: null, from: null, file: at };
|
|
105
117
|
if (!account || typeof account !== 'object') return none;
|
|
106
118
|
// THE ORDER IS MOST SPECIFIC FIRST. A rate limit tier names the plan AND its
|
|
107
119
|
// size (`max_20x` against `max_5x`), which is the thing worth showing; the
|
package/main/code-change.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// IT IS NOT ENOUGH ON ITS OWN, AND THAT WAS MEASURED, NOT ARGUED. An agent
|
|
15
15
|
// that rewrites a file with sed or a heredoc puts nothing in its transcript,
|
|
16
16
|
// and on her Mac 2026-08-24 that was 101 of the 105 runs that changed a file
|
|
17
|
-
// over two days. Every one of those left
|
|
17
|
+
// over two days. Every one of those left a tester an empty card. So the disk is
|
|
18
18
|
// read too, by main/git-change.mjs, and `mergeChange` below puts the two
|
|
19
19
|
// together: git's lines, the conversation's words, git winning where they
|
|
20
20
|
// disagree.
|
|
@@ -39,9 +39,9 @@ const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
|
|
|
39
39
|
// AND SCRATCH, WHICH IS NOT THE PRODUCT. A run writes throwaway scripts to
|
|
40
40
|
// /tmp constantly and one of them landed in the tree as a folder called `tmp`
|
|
41
41
|
// on the first real change we read (18 files, and one of them was
|
|
42
|
-
// /tmp/shelf-shots.mjs). Dropped rather than shown: what
|
|
42
|
+
// /tmp/shelf-shots.mjs). Dropped rather than shown: what a tester is being asked
|
|
43
43
|
// to review is the product, and a file nobody will ever open again reads as
|
|
44
|
-
// work
|
|
44
|
+
// work they have to account for.
|
|
45
45
|
const SCRATCH = /^\/(private\/)?(tmp|var\/folders)\//;
|
|
46
46
|
|
|
47
47
|
/**
|
package/main/codex-account.mjs
CHANGED
|
@@ -35,7 +35,7 @@ const AUTH_CLAIM = 'https://api.openai.com/auth';
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* The plan ids whose proper names are known. Everything else is printed exactly
|
|
38
|
-
* as Codex spells it, and that is deliberate:
|
|
38
|
+
* as Codex spells it, and that is deliberate: a real token can read "prolite",
|
|
39
39
|
* which title-cases into "Prolite", a product name nobody at OpenAI has ever
|
|
40
40
|
* used. Inventing a label is how a screen tells a confident lie about what
|
|
41
41
|
* somebody is paying for, so an id we do not know is shown as the id. */
|
package/main/codex-bin.mjs
CHANGED
|
@@ -114,6 +114,7 @@ export function candidatePaths(home = os.homedir()) {
|
|
|
114
114
|
path.join(home, '.npm-packages/bin/codex'), // actually write in a guide
|
|
115
115
|
path.join(home, '.yarn/bin/codex'),
|
|
116
116
|
path.join(home, 'Library/pnpm/codex'), // pnpm's own bin on macOS
|
|
117
|
+
path.join(home, '.local/share/pnpm/codex'), // pnpm's own bin on Linux
|
|
117
118
|
path.join(home, '.deno/bin/codex'),
|
|
118
119
|
path.join(home, 'bin/codex'), // a hand-rolled ~/bin on PATH
|
|
119
120
|
'/opt/local/bin/codex', // MacPorts
|
|
@@ -140,6 +141,19 @@ function defaultReaddir(dir) {
|
|
|
140
141
|
try { return fs.readdirSync(dir); } catch { return []; }
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
// THE COPY THE CHATGPT APP CARRIES INSIDE ITSELF. Somebody who has only ever
|
|
145
|
+
// used Codex in the ChatGPT app has this and nothing else (w-9f6975906c,
|
|
146
|
+
// measured 2026-10-05: codex-cli 0.158.0-alpha.2, sharing the CLI's sign-in).
|
|
147
|
+
// Searched LAST, after the shell, because it moves with the app's releases.
|
|
148
|
+
//
|
|
149
|
+
// AND THE ONE THE CODEX DESKTOP APP CARRIES (w-d5d632e503). Its reported place,
|
|
150
|
+
// not measured on a Mac here: none of the Macs this was written on had the app.
|
|
151
|
+
// A path that is not there costs one missed `exists`.
|
|
152
|
+
export function appCopyPaths(home = os.homedir()) {
|
|
153
|
+
const inside = ['ChatGPT.app/Contents/Resources/codex-cli/bin/codex', 'Codex.app/Contents/Resources/codex'];
|
|
154
|
+
return inside.flatMap((p) => [path.join('/Applications', p), path.join(home, 'Applications', p)]);
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
// WHAT CODEX LEAVES BEHIND. None of these is a binary and none is ever spawned.
|
|
144
158
|
// They exist only to stop the app claiming an absence it has not established.
|
|
145
159
|
//
|
|
@@ -214,6 +228,10 @@ export function findCodexBin({
|
|
|
214
228
|
const fromShell = shellSaid(shellLookup());
|
|
215
229
|
if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
|
|
216
230
|
|
|
231
|
+
for (const candidate of appCopyPaths(home)) {
|
|
232
|
+
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
|
|
233
|
+
}
|
|
234
|
+
|
|
217
235
|
// NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
|
|
218
236
|
// shell we could reach missed, so the last question is whether this Mac shows
|
|
219
237
|
// any sign of Codex anyway.
|