agentbox-app 0.1.6 → 0.1.11
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 +209 -55
- package/bin/agentbox.mjs +6 -0
- package/briefs/message-rules.md +13 -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 +9 -10
- 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 +114 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +31 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +11 -11
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +17 -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 +68 -8
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +193 -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 +6 -3
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +1 -1
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +92 -54
- package/main/ipc.mjs +316 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +47 -0
- package/main/main.mjs +119 -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/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +20 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +114 -51
- package/main/serve.mjs +67 -5
- package/main/settings.mjs +223 -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 +47 -2
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +235 -9
- package/main/supervisor.mjs +1444 -406
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +74 -13
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +2 -1
- 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 +9 -11
- package/mcp/core/work.mjs +91 -25
- package/mcp/tools.mjs +12 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-B-2Az952.css +32 -0
- package/renderer/dist/assets/index-D6qncgai.js +1168 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +38 -15
- 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 +37 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +4 -4
- 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-usage.mjs +7 -8
- package/shared/demo-world.mjs +5 -5
- 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 +22 -3
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +6 -0
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- 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 +39 -18
- 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 +255 -27
- 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-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/index-D9hCAVPl.js +0 -684
- 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/agent-sessions.mjs
CHANGED
|
@@ -58,8 +58,12 @@ import path from 'node:path';
|
|
|
58
58
|
* the last three days and 9 in the last seven, in two folders she works in.
|
|
59
59
|
* Three days is a card with tonight's test on it and almost nothing else; the
|
|
60
60
|
* filters have already done the work that "a few" was standing in for, so the
|
|
61
|
-
* window can afford to be the generous reading.
|
|
62
|
-
|
|
61
|
+
* window can afford to be the generous reading.
|
|
62
|
+
*
|
|
63
|
+
* Ten since the launch list (w-db6f5e331e, 2026-10-05): "showing your recently
|
|
64
|
+
* active agents over the last 10 days". Codex reads the same number
|
|
65
|
+
* (main/codex-threads.mjs), so the two halves of the card agree. */
|
|
66
|
+
export const RECENT_DAYS = 10;
|
|
63
67
|
|
|
64
68
|
/**
|
|
65
69
|
* How much of a transcript is read to find its first human turn. The first
|
|
@@ -67,6 +71,15 @@ export const RECENT_DAYS = 7;
|
|
|
67
71
|
* the largest in her store on 2026-08-29 is 13MB, and there are hundreds. */
|
|
68
72
|
const HEAD_BYTES = 96 * 1024;
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* How far past the head one cut-off first message may be followed
|
|
76
|
+
* (w-24edc04548, 2026-10-05). A screenshot pasted into the first message is
|
|
77
|
+
* written into that row as base64, so the row outgrows the head: three of her
|
|
78
|
+
* ten days' terminal threads opened with rows of 240KB, 294KB and 486KB, were
|
|
79
|
+
* cut, lost their name and fell off the card. Eight megabytes is the Codex
|
|
80
|
+
* reader's own ceiling (main/codex-threads.mjs) and several screenshots. */
|
|
81
|
+
const FIRST_TURN_BYTES = 8 * 1024 * 1024;
|
|
82
|
+
|
|
70
83
|
/**
|
|
71
84
|
* A stop on how many transcripts are OPENED, for the same reason the folder
|
|
72
85
|
* scan in agent-files.mjs has one: opening a card may not turn into a disk
|
|
@@ -80,6 +93,10 @@ const HEAD_BYTES = 96 * 1024;
|
|
|
80
93
|
* whether tonight's thread survived was down to where its folder sorted. A
|
|
81
94
|
* cap that can drop the newest thread on the machine is not a safety rail. */
|
|
82
95
|
const MAX_READS = 1200;
|
|
96
|
+
// How long the conversation read may take, in ms (w-ec62ab6b38). The card that
|
|
97
|
+
// uses it says "Looking" meanwhile; three seconds reaches two thirds of her
|
|
98
|
+
// week's 9,073 transcripts and all of a normal Mac's.
|
|
99
|
+
const READ_BUDGET_MS = 3000;
|
|
83
100
|
|
|
84
101
|
/* ------------------------- WHAT IS NOT HERS TO IMPORT ---------------------- */
|
|
85
102
|
|
|
@@ -106,6 +123,21 @@ export function isScratchFolder(folder, home = os.homedir()) {
|
|
|
106
123
|
|| /^\/var\/folders(\/|$)/.test(p);
|
|
107
124
|
}
|
|
108
125
|
|
|
126
|
+
/**
|
|
127
|
+
* THE SAME RULE, READ OFF THE FOLDER CLAUDE CODE FILED THE TRANSCRIPT IN, so
|
|
128
|
+
* it can be applied before the file is opened (w-24edc04548, 2026-10-05).
|
|
129
|
+
* Claude Code names that folder after the working folder with every other
|
|
130
|
+
* character turned into `-`, so `/private/tmp/w-1` is `-private-tmp-w-1`.
|
|
131
|
+
* Of her 12,272 transcripts touched in ten days about 12,000 were in temp
|
|
132
|
+
* folders, and opening each one only to refuse it spent the card's three
|
|
133
|
+
* seconds before it reached her oldest threads. */
|
|
134
|
+
export function isScratchProjectDir(name, home = os.homedir()) {
|
|
135
|
+
const n = String(name ?? '');
|
|
136
|
+
const here = path.resolve(String(home ?? '')).replace(/[^a-zA-Z0-9]/g, '-');
|
|
137
|
+
if (here.length > 1 && (n === here || n.startsWith(`${here}-`))) return false;
|
|
138
|
+
return /^-(private-)?(tmp|var-folders)(-|$)/.test(n);
|
|
139
|
+
}
|
|
140
|
+
|
|
109
141
|
/**
|
|
110
142
|
* THE ONES A PERSON STARTED, which is the whole filter.
|
|
111
143
|
*
|
|
@@ -171,7 +203,9 @@ export function threadTitle(texts, max = 72) {
|
|
|
171
203
|
function titleFromOne(text, max) {
|
|
172
204
|
const raw = String(text ?? '').replace(/\r/g, '');
|
|
173
205
|
for (const line of raw.split('\n')) {
|
|
174
|
-
|
|
206
|
+
// `[Image #1]` and `[Pasted text #1 +40 lines]` are Claude Code's stand-ins
|
|
207
|
+
// for what was pasted, not words she typed.
|
|
208
|
+
const t = line.replace(/\[(?:Image|Pasted text) #\d+[^\]]*\]/g, '').trim();
|
|
175
209
|
if (!t) continue;
|
|
176
210
|
if (NOT_TYPED.some((re) => re.test(t))) return '';
|
|
177
211
|
const clean = t
|
|
@@ -209,7 +243,7 @@ export function shortFolder(folder, home = os.homedir()) {
|
|
|
209
243
|
* every message row and the first human turn is the first message, so all
|
|
210
244
|
* three are at the top, and the alternative is reading gigabytes to fill in a
|
|
211
245
|
* list. Her largest transcript is 13MB and there are hundreds. */
|
|
212
|
-
export function readTranscriptHead(file, bytes = HEAD_BYTES) {
|
|
246
|
+
export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_BYTES) {
|
|
213
247
|
let fd = null;
|
|
214
248
|
try {
|
|
215
249
|
fd = fs.openSync(file, 'r');
|
|
@@ -218,22 +252,37 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES) {
|
|
|
218
252
|
const text = buf.slice(0, n).toString('utf8');
|
|
219
253
|
// The last line of a fixed read is usually cut in half. Dropping it costs
|
|
220
254
|
// nothing, because what is wanted is at the top.
|
|
221
|
-
const lines = text.split('\n')
|
|
255
|
+
const lines = text.split('\n');
|
|
256
|
+
const cut = lines.pop();
|
|
222
257
|
let cwd = null;
|
|
223
258
|
let entrypoint = null;
|
|
224
259
|
const said = [];
|
|
225
|
-
|
|
226
|
-
if (!line.trim()) continue;
|
|
260
|
+
const take = (line) => {
|
|
227
261
|
let row;
|
|
228
|
-
try { row = JSON.parse(line); } catch {
|
|
262
|
+
try { row = JSON.parse(line); } catch { return; }
|
|
229
263
|
if (!cwd && typeof row.cwd === 'string' && row.cwd) cwd = row.cwd;
|
|
230
264
|
if (!entrypoint && typeof row.entrypoint === 'string' && row.entrypoint) entrypoint = row.entrypoint;
|
|
231
265
|
if (row.type === 'user' && said.length < 5) {
|
|
232
266
|
const t = messageText(row);
|
|
233
267
|
if (t.trim()) said.push(t);
|
|
234
268
|
}
|
|
269
|
+
};
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
if (!line.trim()) continue;
|
|
272
|
+
take(line);
|
|
235
273
|
if (cwd && entrypoint && said.length >= 5) break;
|
|
236
274
|
}
|
|
275
|
+
// UNLESS THE HALF LINE IS HER FIRST MESSAGE, cut because a screenshot was
|
|
276
|
+
// pasted into it. Then it is read to its end, once, up to `most`. Only a
|
|
277
|
+
// cut row that names itself a user message near its start qualifies, and
|
|
278
|
+
// only when nothing she said was found and the session is not already
|
|
279
|
+
// known to be the app's own, so the thousands of worker transcripts are
|
|
280
|
+
// never read past their head.
|
|
281
|
+
if (!said.length && n === bytes && cut && /"type":"user"/.test(cut.slice(0, 2048))
|
|
282
|
+
&& (!entrypoint || startedByHand(entrypoint))) {
|
|
283
|
+
const rest = restOfLine(fd, n, most - n);
|
|
284
|
+
if (rest) take(Buffer.concat([buf.subarray(buf.lastIndexOf(0x0a, n - 1) + 1, n), rest]).toString('utf8'));
|
|
285
|
+
}
|
|
237
286
|
return { cwd, entrypoint, said };
|
|
238
287
|
} catch {
|
|
239
288
|
return { cwd: null, entrypoint: null, said: [] };
|
|
@@ -242,6 +291,25 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES) {
|
|
|
242
291
|
}
|
|
243
292
|
}
|
|
244
293
|
|
|
294
|
+
/**
|
|
295
|
+
* The bytes from `from` up to the next newline, or null if there is none
|
|
296
|
+
* within `budget` bytes: a line that long is given up on, not read whole. A
|
|
297
|
+
* file that ends without a newline ends the line too. */
|
|
298
|
+
function restOfLine(fd, from, budget) {
|
|
299
|
+
const parts = [];
|
|
300
|
+
let at = from;
|
|
301
|
+
const chunk = Buffer.alloc(256 * 1024);
|
|
302
|
+
while (at - from < budget) {
|
|
303
|
+
const n = fs.readSync(fd, chunk, 0, Math.min(chunk.length, budget - (at - from)), at);
|
|
304
|
+
if (n <= 0) return Buffer.concat(parts);
|
|
305
|
+
const nl = chunk.subarray(0, n).indexOf(0x0a);
|
|
306
|
+
if (nl >= 0) { parts.push(Buffer.from(chunk.subarray(0, nl))); return Buffer.concat(parts); }
|
|
307
|
+
parts.push(Buffer.from(chunk.subarray(0, n)));
|
|
308
|
+
at += n;
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
245
313
|
/**
|
|
246
314
|
* The words out of one transcript row, whichever of the two shapes it is in.
|
|
247
315
|
* Content is a string on the plain ones and a list of blocks on the rest. */
|
|
@@ -278,11 +346,27 @@ function readTerminalThreads({ home, cutoff, max, skipIds = new Set() }) {
|
|
|
278
346
|
try { when = fs.statSync(file).mtimeMs; } catch { continue; }
|
|
279
347
|
if (when < cutoff) { skipped.old += 1; continue; }
|
|
280
348
|
if (isSubagentPath(file.slice(root.length))) { skipped.subagent += 1; continue; }
|
|
349
|
+
if (isScratchProjectDir(path.relative(root, file).split(path.sep)[0], home)) { skipped.scratch += 1; continue; }
|
|
281
350
|
recent.push({ file, when });
|
|
282
351
|
}
|
|
283
352
|
recent.sort((a, b) => b.when - a.when);
|
|
284
|
-
|
|
285
|
-
|
|
353
|
+
// THE CAP COUNTS WHAT IS KEPT, NOT WHAT IS OPENED (w-ec62ab6b38, 2026-09-28).
|
|
354
|
+
// On a busy machine the last seven days held 9,073 transcripts, the newest
|
|
355
|
+
// 1,200 were opened, and every one of those was started by agentbox itself,
|
|
356
|
+
// so a conversation started by hand was never looked at and the import card
|
|
357
|
+
// said there was nothing, as if the app were not looking in Claude Code at
|
|
358
|
+
// all. Somebody running dozens of agents fills this folder with ours. So it
|
|
359
|
+
// opens newest first until it has `max` of the person's own, or until
|
|
360
|
+
// READ_BUDGET_MS has gone, whichever is first. Reading all 9,073 heads took
|
|
361
|
+
// 5.0s there.
|
|
362
|
+
const started = Date.now();
|
|
363
|
+
let opened = 0;
|
|
364
|
+
for (const { file, when } of recent) {
|
|
365
|
+
if (out.length >= max || Date.now() - started > READ_BUDGET_MS) {
|
|
366
|
+
skipped.unread = recent.length - opened;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
opened += 1;
|
|
286
370
|
const id = path.basename(file, '.jsonl');
|
|
287
371
|
// THE DESKTOP APP WRITES A THREAD TWICE, once as a transcript here and once
|
|
288
372
|
// as its own record, joined by `cliSessionId`. Measured 2026-08-29 on the
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Release discovery is read-only. An updater is a dedicated PTY command,
|
|
2
|
+
// started only by the app's Update now action, never by a poll or terminal open.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {execFile} from 'node:child_process';
|
|
7
|
+
import {promisify} from 'node:util';
|
|
8
|
+
import {TaskTerminals,terminalEnv} from './task-terminals.mjs';
|
|
9
|
+
const exec=promisify(execFile);
|
|
10
|
+
const valid=v=>typeof v==='string'&&/^\d+\.\d+\.\d+$/.test(v);
|
|
11
|
+
export function newerVersion(installed,latest){
|
|
12
|
+
if(!valid(installed)||!valid(latest))return false;
|
|
13
|
+
const a=installed.split('.').map(Number),b=latest.split('.').map(Number);
|
|
14
|
+
for(let i=0;i<3;i++){if(b[i]!==a[i])return b[i]>a[i];}return false;
|
|
15
|
+
}
|
|
16
|
+
export function updatePlan({engine,bin,real,home=os.homedir(),channel='latest'}){
|
|
17
|
+
if(!['claude','codex'].includes(engine)||!path.isAbsolute(bin||'')||!real)return null;
|
|
18
|
+
const name=engine==='claude'?'Claude Code':'Codex';
|
|
19
|
+
const brew=real.match(/^(\/opt\/homebrew|\/usr\/local)\/Caskroom\/(claude-code(?:@latest)?|codex)\//);
|
|
20
|
+
if(brew&&((engine==='claude'&&brew[2].startsWith('claude-code'))||(engine==='codex'&&brew[2]==='codex')))
|
|
21
|
+
return {name,file:`${brew[1]}/bin/brew`,args:['upgrade','--cask',brew[2]],feed:`https://formulae.brew.sh/api/cask/${brew[2]}.json`,format:'brew'};
|
|
22
|
+
const pkg=engine==='claude'?'@anthropic-ai/claude-code':'@openai/codex';
|
|
23
|
+
const tail=`/lib/node_modules/${pkg}/`;
|
|
24
|
+
const at=real.indexOf(tail);
|
|
25
|
+
if(at>0){const prefix=real.slice(0,at);return {name,file:path.join(prefix,'bin/npm'),args:['install','--global','--prefix',prefix,`${pkg}@${channel}`],env:{PATH:`${prefix}/bin:${process.env.PATH||''}`},feed:`https://registry.npmjs.org/${pkg}/${channel}`,format:'npm'};}
|
|
26
|
+
if(engine==='claude'&&real.startsWith(path.join(home,'.local/share/claude/versions')+'/'))return {name,file:bin,args:['update'],feed:`https://downloads.claude.ai/claude-code-releases/${channel}`,format:'text'};
|
|
27
|
+
const standalone=real.match(/^(.*)\/packages\/standalone\/(?:releases|current)\//);
|
|
28
|
+
if(engine==='codex'&&standalone)return {name,file:'/bin/bash',args:['-o','pipefail','-c','/usr/bin/curl -fsSL --connect-timeout 10 --max-time 60 https://chatgpt.com/codex/install.sh | /bin/sh'],env:{CODEX_HOME:standalone[1],CODEX_INSTALL_DIR:path.dirname(bin)},feed:'https://releases.openai.com/codex/channels/latest',format:'codex'};
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const json=file=>{try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return {}}};
|
|
32
|
+
export async function installedAgent(engine,config){
|
|
33
|
+
const bin=engine==='codex'?config.codexBin:config.claudeBin;
|
|
34
|
+
if(!bin)throw Error('This agent is not installed.');
|
|
35
|
+
const home=config.home||os.homedir();
|
|
36
|
+
const settings=engine==='claude'?json(path.join(process.env.CLAUDE_CONFIG_DIR||path.join(home,'.claude'),'settings.json')):{};
|
|
37
|
+
const env=terminalEnv();
|
|
38
|
+
// Version inspection must not itself auto-update anything.
|
|
39
|
+
const {stdout}=await exec(bin,['--version'],{timeout:8000,maxBuffer:16384,env:{...env,DISABLE_AUTOUPDATER:'1'}});
|
|
40
|
+
const version=stdout.match(/\b\d+\.\d+\.\d+(?:-[\w.-]+)?\b/)?.[0];
|
|
41
|
+
const channel=settings.autoUpdatesChannel==='stable'?'stable':'latest';
|
|
42
|
+
return {engine,bin,real:fs.realpathSync(bin),home,version,channel,disabled:settings.env?.DISABLE_UPDATES==='1'||process.env.DISABLE_UPDATES==='1'};
|
|
43
|
+
}
|
|
44
|
+
export async function latestRelease(plan){
|
|
45
|
+
const r=await fetch(plan.feed,{signal:AbortSignal.timeout(10000),headers:{Accept:plan.format==='text'?'text/plain':'application/json'}});
|
|
46
|
+
if(!r.ok)throw Error('Release check failed.');
|
|
47
|
+
const body=await r.text();if(body.length>2_000_000)throw Error('Release response too large.');
|
|
48
|
+
const data=plan.format==='text'?null:JSON.parse(body);
|
|
49
|
+
const version=plan.format==='text'?body.trim():plan.format==='codex'?String(data.version||data.tag_name||'').replace(/^(rust-v|v)/,''):data.version;
|
|
50
|
+
if(!valid(version))throw Error('Release version could not be read.');return version;
|
|
51
|
+
}
|
|
52
|
+
export class AgentUpdates{
|
|
53
|
+
constructor({installation,latest=latestRelease,refresh=async()=>{},terminals,now=()=>Date.now()}={}){
|
|
54
|
+
this.installation=installation;this.latest=latest;this.refresh=refresh;this.now=now;this.entries=new Map();this.checks=new Map();this.starts=new Map();this.finishes=new Map();this.plans=new Map();
|
|
55
|
+
this.terminals=terminals||new TaskTerminals({sweepMs:0});
|
|
56
|
+
this.terminals.resolve=engine=>{const p=this.plans.get(engine);if(!p)throw Error('Click Update now first.');return {cwd:p.home,command:{file:p.file,args:p.args,env:p.env},notice:`Updating ${p.name}.`}};
|
|
57
|
+
}
|
|
58
|
+
status(engine){return this.entries.get(engine)||{engine,state:'unknown'};}
|
|
59
|
+
check(engine,force=false){
|
|
60
|
+
if(!['claude','codex'].includes(engine))return Promise.reject(Error('Unknown agent.'));
|
|
61
|
+
if(this.starts.has(engine)||this.status(engine).state==='running')return Promise.resolve(this.status(engine));
|
|
62
|
+
if(this.checks.has(engine))return this.checks.get(engine);
|
|
63
|
+
const saved=this.status(engine);if(!force&&saved.checkedAt&&this.now()-saved.checkedAt<3600000)return Promise.resolve(saved);
|
|
64
|
+
const work=(async()=>{let result;try{
|
|
65
|
+
const install=await this.installation(engine);const plan=install.disabled?null:updatePlan(install);
|
|
66
|
+
if(!plan)result={engine,state:'unsupported',message:'Update this installation using its own installer.'};
|
|
67
|
+
else{const latest=await this.latest(plan);result={engine,state:newerVersion(install.version,latest)?'available':'current',installed:install.version,latest};if(!valid(install.version))throw Error('Installed version could not be read.');}
|
|
68
|
+
}catch{result={engine,state:'unknown',message:'Could not check for updates. Your current models are still available.'};}
|
|
69
|
+
result.checkedAt=this.now();if(this.starts.has(engine)||this.status(engine).state==='running')return this.status(engine);this.entries.set(engine,result);return result;})().finally(()=>this.checks.delete(engine));
|
|
70
|
+
this.checks.set(engine,work);return work;
|
|
71
|
+
}
|
|
72
|
+
start(engine){
|
|
73
|
+
if(this.starts.has(engine))return this.starts.get(engine);
|
|
74
|
+
if(this.status(engine).state==='running')return Promise.resolve(this.status(engine));
|
|
75
|
+
// Validate again at click time: an installation may have changed since
|
|
76
|
+
// the picker opened. Never trust a command or a release passed by the UI.
|
|
77
|
+
const work=(async()=>{
|
|
78
|
+
const install=await this.installation(engine);const plan=install.disabled?null:updatePlan(install);
|
|
79
|
+
if(!plan)throw Error('This installation must be updated using its own installer.');
|
|
80
|
+
const target=await this.latest(plan);
|
|
81
|
+
if(!valid(install.version))throw Error('Installed version could not be read.');
|
|
82
|
+
if(!newerVersion(install.version,target)){
|
|
83
|
+
await this.refresh(engine);const current={engine,state:'current',installed:install.version,latest:target,checkedAt:this.now(),message:'Up to date. Available models refreshed.'};this.entries.set(engine,current);return current;
|
|
84
|
+
}
|
|
85
|
+
this.plans.set(engine,{...plan,home:install.home});this.terminals.close(engine);
|
|
86
|
+
this.terminals.open(engine);
|
|
87
|
+
const running={engine,state:'running',installed:install.version,latest:target};this.entries.set(engine,running);
|
|
88
|
+
const finished=this.terminals.get(engine).exitPromise.then(async()=>{
|
|
89
|
+
try{
|
|
90
|
+
if(this.terminals.get(engine).exitCode!==0)throw Error('Update did not finish. You can retry.');
|
|
91
|
+
const after=await this.installation(engine);
|
|
92
|
+
if(!valid(after.version)||newerVersion(after.version,target))throw Error('The installed version is still older. Check the terminal output before retrying.');
|
|
93
|
+
await this.refresh(engine);
|
|
94
|
+
this.entries.set(engine,{engine,state:'current',installed:after.version,latest:target,checkedAt:this.now(),message:'Updated. Available models refreshed.'});
|
|
95
|
+
}catch(error){this.entries.set(engine,{...running,state:'failed',message:error.message});}
|
|
96
|
+
});this.finishes.set(engine,finished);return running;
|
|
97
|
+
})().catch(error=>{this.entries.set(engine,{engine,state:'failed',message:error.message});throw error;}).finally(()=>this.starts.delete(engine));
|
|
98
|
+
this.starts.set(engine,work);return work;
|
|
99
|
+
}
|
|
100
|
+
settled(engine){return this.finishes.get(engine)||Promise.resolve();}
|
|
101
|
+
terminal(engine,p){
|
|
102
|
+
// Opening the viewer is never an install action, including after it ended.
|
|
103
|
+
if(!this.plans.has(engine))throw Error('Click Update now first.');
|
|
104
|
+
switch(p.action){
|
|
105
|
+
case 'open':return this.terminals.read(engine,0);
|
|
106
|
+
case 'read':return this.terminals.read(engine,p.offset);
|
|
107
|
+
case 'write':return this.terminals.write(engine,p.data);
|
|
108
|
+
case 'resize':return this.terminals.resize(engine,p.cols,p.rows);
|
|
109
|
+
case 'close':return this.terminals.close(engine);
|
|
110
|
+
default:throw Error('Unknown terminal action.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
shutdown(){return this.terminals.shutdown();}
|
|
114
|
+
}
|
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/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) {
|