agentbox-app 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -16
- package/bin/agentbox.mjs +18 -9
- package/briefs/message-rules.md +15 -4
- package/main/account-tooling.mjs +2 -1
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +57 -20
- package/main/agent-updates.mjs +9 -20
- package/main/analytics.mjs +30 -0
- package/main/approvals.mjs +14 -5
- package/main/claude-bin.mjs +45 -310
- package/main/claude-input.mjs +9 -3
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +14 -2
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -248
- package/main/codex-launch-env.mjs +31 -0
- package/main/codex-session.mjs +38 -3
- package/main/codex-threads.mjs +41 -8
- package/main/codex-watch.mjs +67 -3
- package/main/config.mjs +40 -13
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/demo.mjs +17 -14
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +27 -27
- package/main/feedback.mjs +11 -6
- package/main/fresh-user.mjs +1 -1
- package/main/git-change.mjs +8 -2
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +84 -0
- package/main/harnesses/codex.mjs +65 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +38 -0
- package/main/instruction-settings.mjs +47 -11
- package/main/ipc.mjs +175 -22
- package/main/local-app-frames.mjs +82 -0
- package/main/machine.mjs +1 -1
- package/main/main.mjs +120 -5
- package/main/memory-gate-history.mjs +51 -12
- package/main/memory-gate-server.mjs +67 -11
- package/main/memory-gate.mjs +49 -8
- package/main/memory-log.mjs +124 -0
- package/main/notify.mjs +48 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/refresh-agent-models.mjs +2 -1
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +8 -7
- package/main/runs-on.mjs +90 -0
- package/main/serve.mjs +124 -10
- package/main/settings.mjs +171 -11
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +25 -7
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +27 -1
- package/main/start-hold.mjs +73 -0
- package/main/store/home.mjs +37 -5
- package/main/store/project.mjs +15 -19
- package/main/store.mjs +10 -4
- package/main/supervisor.mjs +1030 -447
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +491 -82
- package/main/task-terminals.mjs +49 -8
- package/main/team/index.mjs +2 -2
- package/main/team/projects.mjs +1 -1
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/mcp/core/page.mjs +1 -1
- package/mcp/core/work.mjs +34 -3
- package/mcp/tools.mjs +2 -1
- package/package.json +10 -1
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
- package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
- package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
- package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
- package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
- package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
- package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +31 -7
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +3 -1
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +19 -0
- package/shared/claude-commands.generated.mjs +8 -6
- package/shared/claude-models.generated.mjs +10 -8
- package/shared/codex-modes.mjs +1 -1
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/demo-world.mjs +1 -1
- package/shared/engines.mjs +20 -5
- package/shared/fresh-user-home.mjs +12 -1
- package/shared/harness-definitions.mjs +103 -0
- package/shared/instruction-defaults.json +1 -1
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/project-folder-check.mjs +3 -3
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +2 -0
- package/shared/rank.mjs +50 -0
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/runs-on.d.mts +10 -0
- package/shared/runs-on.mjs +55 -0
- package/shared/shortcuts.mjs +11 -5
- package/shared/spawn-trouble.mjs +58 -7
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/usage.d.mts +3 -0
- package/shared/usage.mjs +7 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +28 -2
- package/shared/work-lines.mjs +1 -1
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-B-2Az952.css +0 -32
- package/renderer/dist/assets/index-D6qncgai.js +0 -1168
package/main/settings.mjs
CHANGED
|
@@ -13,18 +13,20 @@
|
|
|
13
13
|
// the project they belong to instead of in a text editor.
|
|
14
14
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
|
+
import { harnessFor } from './harnesses.mjs';
|
|
16
17
|
import os from 'node:os';
|
|
17
18
|
import path from 'node:path';
|
|
18
19
|
import { codexChoiceToOpen, saveConfig } from './config.mjs';
|
|
19
|
-
import {
|
|
20
|
+
import { forgetClaudeBin, INSTALL_URL } from './claude-bin.mjs';
|
|
20
21
|
import { AGENT_MODES } from '../shared/agents.mjs';
|
|
21
22
|
import { analyticsKey, diagnosticsOn } from './analytics.mjs';
|
|
22
23
|
import { CLAUDE_PERMISSION_MODES } from '../shared/work-items.mjs';
|
|
23
|
-
import { accountSentence, engineTroubleNote } from '../shared/spawn-trouble.mjs';
|
|
24
|
+
import { accountSentence, engineTroubleNote, refineCause } from '../shared/spawn-trouble.mjs';
|
|
24
25
|
import { readPlan } from './claude-plan.mjs';
|
|
25
26
|
import { effectiveProfiles } from './account-discovery.mjs';
|
|
26
27
|
import { machineSlots, machineNote, autoAgents, MAX_SLOTS } from './machine.mjs';
|
|
27
|
-
import {
|
|
28
|
+
import { shortPath } from './agent-files.mjs';
|
|
29
|
+
import { NAME, Name } from '../shared/product-name.mjs';
|
|
28
30
|
import { autoSlots } from './memory-gate.mjs';
|
|
29
31
|
import { accountIdentity, claudeLoginCommand, duplicateAccountNote, linkAccountTooling, makeClaudeHome } from './account-tooling.mjs';
|
|
30
32
|
import { isEngine } from '../shared/engines.mjs';
|
|
@@ -32,7 +34,7 @@ import { codexAccount, codexLoginCommand, makeCodexHome } from './codex-account.
|
|
|
32
34
|
import { codexDefaultModel, codexModels } from './codex-models.mjs';
|
|
33
35
|
import { CODEX_DEFAULT_MODE, isCodexMode } from '../shared/codex-modes.mjs';
|
|
34
36
|
import { claudeModels } from './claude-models.mjs';
|
|
35
|
-
import {
|
|
37
|
+
import { forgetCodexBin, INSTALL_URL as CODEX_INSTALL_URL } from './codex-bin.mjs';
|
|
36
38
|
|
|
37
39
|
// Her Claude Code sessions in her inbox: all of them, only the ones stopped on
|
|
38
40
|
// a question, or none. One reader, here, so the settings screen and the
|
|
@@ -248,13 +250,18 @@ const lineCount = (text) => (text.trim() ? text.trim().split('\n').length : 0);
|
|
|
248
250
|
// Claude Code: the path, whether it is really there, and where to get it when
|
|
249
251
|
// it is not. One reader, here, so the screen and the config cannot disagree.
|
|
250
252
|
export function claudeState(config) {
|
|
251
|
-
const found =
|
|
253
|
+
const found = harnessFor('claude').discover(config.claudeBinConfigured ?? null);
|
|
252
254
|
return {
|
|
253
255
|
claudeBin: found.path ?? config.claudeBin,
|
|
254
256
|
claudeFound: found.found,
|
|
255
257
|
// Whether the search got an answer at all.
|
|
256
258
|
claudeCertain: found.certain !== false,
|
|
257
259
|
claudeInstallUrl: INSTALL_URL,
|
|
260
|
+
// WHETHER SOMEBODY POINTED AT THIS FILE THEMSELVES, which is the one thing
|
|
261
|
+
// the card cannot work out from the path alone. A chosen path is never
|
|
262
|
+
// searched around, so the row that offers the search back has to know
|
|
263
|
+
// (`pointAtEngineBin` below).
|
|
264
|
+
claudeChosen: found.from === 'settings',
|
|
258
265
|
};
|
|
259
266
|
}
|
|
260
267
|
|
|
@@ -310,7 +317,7 @@ export function recheckClaude(config) {
|
|
|
310
317
|
* whose unsure state is not checked. Nothing in the app passes it.
|
|
311
318
|
*/
|
|
312
319
|
export function codexState(config, where = {}) {
|
|
313
|
-
const found =
|
|
320
|
+
const found = harnessFor('codex').discover(config.codexBinConfigured ?? null, where);
|
|
314
321
|
return {
|
|
315
322
|
// Empty rather than a path when there is nothing there, which is the same
|
|
316
323
|
// asymmetry `config.codexBin` already keeps (main/config.mjs): a configured
|
|
@@ -322,6 +329,8 @@ export function codexState(config, where = {}) {
|
|
|
322
329
|
// Whether the search got an answer at all.
|
|
323
330
|
certain: found.certain !== false,
|
|
324
331
|
url: CODEX_INSTALL_URL,
|
|
332
|
+
// Whether somebody pointed at this file themselves. See `claudeState`.
|
|
333
|
+
chosen: found.from === 'settings',
|
|
325
334
|
};
|
|
326
335
|
}
|
|
327
336
|
|
|
@@ -348,6 +357,87 @@ export function recheckCodex(config) {
|
|
|
348
357
|
return state;
|
|
349
358
|
}
|
|
350
359
|
|
|
360
|
+
/* ------------------- POINTING AT IT YOURSELF, AND BACK -------------------- */
|
|
361
|
+
/**
|
|
362
|
+
* THE WAY OUT OF A SEARCH THAT MISSED, AND IT IS NOT A RELEASE (w-ce8c1a9275).
|
|
363
|
+
*
|
|
364
|
+
* The note this came from: "It has a really hard time finding the install on the
|
|
365
|
+
* person's computer, and we keep having bug reports and then patching that
|
|
366
|
+
* specific case." Asking the login shell first (main/engine-bin.mjs) covers
|
|
367
|
+
* every installer a person's own PATH knows about, which is most of them. It
|
|
368
|
+
* cannot cover a wrapper script somewhere nobody would guess, a tool on a
|
|
369
|
+
* volume, or a machine whose login shell will not start at all -- and for those,
|
|
370
|
+
* every fix used to be a path WE ship and a release THEY wait for.
|
|
371
|
+
*
|
|
372
|
+
* `claudeBin` in zero.config.json already won over the search and always has.
|
|
373
|
+
* What was missing is that nobody is told about that file and the app gave no
|
|
374
|
+
* way to edit it. These two functions are that door, and the second one matters
|
|
375
|
+
* as much as the first: a wrong choice that could only be undone by hand-editing
|
|
376
|
+
* a file nobody has heard of is not a door, it is a trap.
|
|
377
|
+
*
|
|
378
|
+
* THE REFUSALS ARE MOST OF THE WORK. Each thing refused here was possible by
|
|
379
|
+
* hand before and each one becomes a spawn that fails once per row, naming a
|
|
380
|
+
* file nobody meant to name. The sentence comes back to the screen instead.
|
|
381
|
+
*
|
|
382
|
+
* NO PATH FROM THE WINDOW IS EVER TRUSTED: main/ipc.mjs opens the picker and
|
|
383
|
+
* calls this in the same trip, so the only path that reaches here is one the
|
|
384
|
+
* open panel itself produced. The sibling rule is
|
|
385
|
+
* tests/where-codex-runs-from-is-not-a-file-a-worker-can-write.test.mjs.
|
|
386
|
+
*/
|
|
387
|
+
const BIN_KEY = { claude: 'claudeBin', codex: 'codexBin' };
|
|
388
|
+
|
|
389
|
+
/** The sentence, or null when the file really is one we could run. */
|
|
390
|
+
function whyNotAnAgent(chosen) {
|
|
391
|
+
if (typeof chosen !== 'string' || !path.isAbsolute(chosen)) {
|
|
392
|
+
return `${Name} needs the file itself, picked from your computer.`;
|
|
393
|
+
}
|
|
394
|
+
let stat;
|
|
395
|
+
try { stat = fs.statSync(chosen); } catch { stat = null; }
|
|
396
|
+
if (!stat) return `${Name} could not find anything there. It may have moved since.`;
|
|
397
|
+
if (stat.isDirectory()) return 'That is a folder. Open it and pick the file inside.';
|
|
398
|
+
try { fs.accessSync(chosen, fs.constants.X_OK); } catch {
|
|
399
|
+
return 'That file cannot be run, so it is not the one.';
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Take a file somebody picked for one coding agent.
|
|
406
|
+
*
|
|
407
|
+
* Written to zero.config.json so it survives a restart AND set on the live
|
|
408
|
+
* config, because every consumer reads `config.*` per call and a setting that
|
|
409
|
+
* waits for a relaunch is the same failure as a fix that is not running. The
|
|
410
|
+
* recheck afterwards is what puts the path the fleet spawns from in step with
|
|
411
|
+
* it, the same way the Check again button does.
|
|
412
|
+
*/
|
|
413
|
+
export function pointAtEngineBin(config, engine, chosen) {
|
|
414
|
+
const key = BIN_KEY[engine];
|
|
415
|
+
if (!key) return { ok: false, say: `${Name} does not know a coding agent called that.` };
|
|
416
|
+
const why = whyNotAnAgent(chosen);
|
|
417
|
+
if (why) return { ok: false, say: why };
|
|
418
|
+
if (!config.appDir) return { ok: false, say: `${Name} has nowhere to save that.` };
|
|
419
|
+
saveConfig(config, { [key]: chosen });
|
|
420
|
+
config[`${engine}BinConfigured`] = chosen;
|
|
421
|
+
const state = engine === 'claude' ? recheckClaude(config) : recheckCodex(config);
|
|
422
|
+
return { ok: true, state };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Give the choice back, so the search is in charge again. The key is REMOVED
|
|
427
|
+
* from the file rather than emptied: `saveConfig` reads an explicit undefined as
|
|
428
|
+
* a deletion, and an empty string left behind would read as a setting that means
|
|
429
|
+
* something else (main/config.mjs only treats a non-empty string as chosen).
|
|
430
|
+
*/
|
|
431
|
+
export function searchForEngineBin(config, engine) {
|
|
432
|
+
const key = BIN_KEY[engine];
|
|
433
|
+
if (!key) return { ok: false, say: `${Name} does not know a coding agent called that.` };
|
|
434
|
+
if (!config.appDir) return { ok: false, say: `${Name} has nowhere to save that.` };
|
|
435
|
+
saveConfig(config, { [key]: undefined });
|
|
436
|
+
config[`${engine}BinConfigured`] = null;
|
|
437
|
+
const state = engine === 'claude' ? recheckClaude(config) : recheckCodex(config);
|
|
438
|
+
return { ok: true, state };
|
|
439
|
+
}
|
|
440
|
+
|
|
351
441
|
export function readSettings({ config, supervisor, store }) {
|
|
352
442
|
const products = store.listProducts();
|
|
353
443
|
const status = supervisor.status();
|
|
@@ -379,7 +469,11 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
379
469
|
// waiting, because a page she chose to open interrupts nobody.
|
|
380
470
|
const troubles = status.accountTrouble ?? {};
|
|
381
471
|
const accounts = (profiles.length ? profiles : ['default']).map((profile) => {
|
|
382
|
-
const
|
|
472
|
+
const plan = readPlan(profile);
|
|
473
|
+
// A reason filed before an ended subscription had its own name still reads
|
|
474
|
+
// as an admin's block; the login's file says which it is.
|
|
475
|
+
const filed = troubles[profile] ?? null;
|
|
476
|
+
const trouble = filed ? { ...filed, cause: refineCause(filed.cause, plan) } : null;
|
|
383
477
|
const resting = (supervisor._profileCooldown?.[profile] ?? 0) >= now;
|
|
384
478
|
// WHOSE SUBSCRIPTION THIS ROW IS, not merely which folder it lives in.
|
|
385
479
|
// The page named its rows after folders, so nothing on it could tell a
|
|
@@ -421,7 +515,7 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
421
515
|
// Read here rather than kept, because Claude Code refreshes its own
|
|
422
516
|
// profile as it runs and this page is opened by hand a few times a day.
|
|
423
517
|
// `null` when the file cannot be read, and then no screen says anything.
|
|
424
|
-
plan:
|
|
518
|
+
plan: plan.known ? { label: plan.label, max: plan.max } : null,
|
|
425
519
|
};
|
|
426
520
|
});
|
|
427
521
|
|
|
@@ -449,6 +543,10 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
449
543
|
permission: override ? permissionMode(override) : 'workspace',
|
|
450
544
|
permissionArgs: override ?? null,
|
|
451
545
|
codexMode: config.projectCodexMode?.[product.slug] ?? 'workspace',
|
|
546
|
+
// THE SUPERVISOR IS ASKED RATHER THAN THE CONFIG FILE, so the page shows
|
|
547
|
+
// what will actually happen: a tie naming a login that has been signed out
|
|
548
|
+
// is ignored when work is handed out, and reads 'any' here too.
|
|
549
|
+
account: supervisor._projectProfile?.(product.slug) ?? 'any',
|
|
452
550
|
instructions,
|
|
453
551
|
running: sessions.filter((s) => s.product === product.slug).length,
|
|
454
552
|
};
|
|
@@ -467,7 +565,9 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
467
565
|
archivedProjects,
|
|
468
566
|
workspace: {
|
|
469
567
|
agentsRunning: !supervisor.paused,
|
|
470
|
-
|
|
568
|
+
// The number picked on the menu, for the whole computer, or what is
|
|
569
|
+
// running as the limit now when nothing was picked.
|
|
570
|
+
sessionsAtOnce: typeof config.agentsAtOnce === 'number' ? config.agentsAtOnce : status.capacity,
|
|
471
571
|
// AUTOMATIC, AND WHAT IT WORKS OUT TO (w-e5225b62ba). `agentsAuto` is who
|
|
472
572
|
// is deciding; `agentsTotal` is what the whole Mac is running at once,
|
|
473
573
|
// which is the number the page shows, because the per-account one is
|
|
@@ -495,6 +595,7 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
495
595
|
machineNote: machineNote(slotsHere, config.maxConcurrentSessions),
|
|
496
596
|
memoryGate: memoryGateSettings({ config, supervisor }),
|
|
497
597
|
leftovers: leftoverSettings({ config, supervisor }),
|
|
598
|
+
strayFolders: strayFolderSettings({ supervisor }),
|
|
498
599
|
capacity: status.capacity,
|
|
499
600
|
running: sessions.length,
|
|
500
601
|
model: parseSessionArgs(workspaceArgs).model,
|
|
@@ -522,6 +623,9 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
522
623
|
// ADHD mode: whether the ADHD rules ride under hers (w-5737fe67cf). Off
|
|
523
624
|
// unless she turned it on, so only `true` means on.
|
|
524
625
|
adhdMode: config.adhdMode === true,
|
|
626
|
+
// The corner tag over other apps (w-dafae58a23). On unless she turned it
|
|
627
|
+
// off here, which is the only place it turns off for good.
|
|
628
|
+
cornerTag: config.cornerTag !== false,
|
|
525
629
|
// How much of her own Claude Code goes in her inbox: all / waiting / off.
|
|
526
630
|
// All by default, because the machine nobody has opened this page on is
|
|
527
631
|
// the one with thirteen forgotten sessions on it.
|
|
@@ -727,6 +831,24 @@ export function setProjectSetting({ config, supervisor }, { product, key, value
|
|
|
727
831
|
saveConfig(config, { projectCodexMode: Object.keys(map).length ? map : undefined });
|
|
728
832
|
break;
|
|
729
833
|
}
|
|
834
|
+
// WHICH CLAUDE LOGIN THIS PROJECT'S AGENTS RUN ON. 'any' is the absence of
|
|
835
|
+
// an opinion, like 'workspace' above, and it deletes the entry rather than
|
|
836
|
+
// writing a word: a config file with no `projectAccounts` is the file every
|
|
837
|
+
// Mac had before this existed.
|
|
838
|
+
//
|
|
839
|
+
// AND A NAME THIS MAC CANNOT HONOUR IS REFUSED AT THE DOOR rather than
|
|
840
|
+
// written and ignored later. The supervisor does ignore one (`_projectProfile`),
|
|
841
|
+
// because an account can be signed out after the fact, but a pick made
|
|
842
|
+
// against a list we just read has no excuse to be wrong.
|
|
843
|
+
case 'account': {
|
|
844
|
+
const known = effectiveProfiles(config.authProfiles, { home: config.home });
|
|
845
|
+
if (value !== 'any' && !known.includes(value)) throw new Error(`unknown Claude account: ${value}`);
|
|
846
|
+
const map = { ...(config.projectAccounts ?? {}) };
|
|
847
|
+
if (value === 'any') delete map[product];
|
|
848
|
+
else map[product] = value;
|
|
849
|
+
saveConfig(config, { projectAccounts: Object.keys(map).length ? map : undefined });
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
730
852
|
default:
|
|
731
853
|
throw new Error(`unknown project setting: ${key}`);
|
|
732
854
|
}
|
|
@@ -842,7 +964,7 @@ export function memoryGateSettings({ config, supervisor }) {
|
|
|
842
964
|
if (on) {
|
|
843
965
|
let s = null;
|
|
844
966
|
try { s = supervisor.memoryGateStatus?.() ?? null; } catch {}
|
|
845
|
-
if (s?.role === 'standby') now = `Another ${NAME} on this
|
|
967
|
+
if (s?.role === 'standby') now = `Another ${NAME} on this computer is coordinating.`;
|
|
846
968
|
else if (s) {
|
|
847
969
|
const heavy = (s.running ?? []).filter((r) => r.holdsSlot).length;
|
|
848
970
|
const waiting = (s.waiting ?? []).length;
|
|
@@ -869,6 +991,9 @@ export function memoryGateSettings({ config, supervisor }) {
|
|
|
869
991
|
export function memoryReading({ config, supervisor }) {
|
|
870
992
|
const out = { heavy: null, waiting: null, pressure: null, usedPct: null, asked: null, waited: null, refused: null };
|
|
871
993
|
if (!config.memoryGate) return out;
|
|
994
|
+
// The supervisor's own hold on new agents (main/start-hold.mjs), which does
|
|
995
|
+
// not depend on which copy of the app is coordinating the command queue.
|
|
996
|
+
try { out.holding = !!supervisor.startHoldStatus?.()?.holding; } catch { out.holding = false; }
|
|
872
997
|
let s = null;
|
|
873
998
|
try { s = supervisor.memoryGateStatus?.() ?? null; } catch { return out; }
|
|
874
999
|
if (!s || s.role === 'standby') return out;
|
|
@@ -924,6 +1049,34 @@ export function leftoverSettings({ config, supervisor }) {
|
|
|
924
1049
|
return { on, now };
|
|
925
1050
|
}
|
|
926
1051
|
|
|
1052
|
+
/**
|
|
1053
|
+
* COPIES OF A PROJECT THE APP DID NOT MAKE, SAID OUT LOUD (w-330eea6c66).
|
|
1054
|
+
*
|
|
1055
|
+
* MEASURED 2026-10-07: thirteen worktrees of one repository sat outside the
|
|
1056
|
+
* folder the app keeps its own in, about 740 MB, and nothing in the app could
|
|
1057
|
+
* list, show or sweep one. That is the same invisibility that stranded 26 GB on
|
|
1058
|
+
* 2026-09-22, and the half that fixes it for a person is being told.
|
|
1059
|
+
*
|
|
1060
|
+
* NO BUTTON, DELIBERATELY. A folder the app did not make is not the app's to
|
|
1061
|
+
* remove: it is the only copy of whatever is uncommitted inside it, and somebody
|
|
1062
|
+
* made it on purpose. So this is a sentence and nothing else, and it is absent
|
|
1063
|
+
* entirely when there is nothing to say, because a row that is usually quiet
|
|
1064
|
+
* trains the reaction it exists to prevent.
|
|
1065
|
+
*/
|
|
1066
|
+
export function strayFolderSettings({ supervisor }, home = os.homedir()) {
|
|
1067
|
+
let list = [];
|
|
1068
|
+
try { list = supervisor?.strayFolders?.() ?? []; } catch { list = []; }
|
|
1069
|
+
if (!list.length) return null;
|
|
1070
|
+
const count = list.length;
|
|
1071
|
+
const one = count === 1;
|
|
1072
|
+
const projects = [...new Set(list.map((w) => w.project).filter(Boolean))];
|
|
1073
|
+
const named = list.slice(0, 3).map((w) => shortPath(w.path, home));
|
|
1074
|
+
const rest = count - named.length;
|
|
1075
|
+
const of = projects.length ? ` of ${projects.slice(0, 2).join(' and ')}${projects.length > 2 ? ' and other projects' : ''}` : '';
|
|
1076
|
+
const now = `${count} ${one ? 'copy' : 'copies'}${of} ${one ? 'was' : 'were'} made by hand outside the folder ${NAME} keeps its own in, and ${one ? 'is' : 'are'} holding disk until you remove ${one ? 'it' : 'them'}: ${named.join(', ')}${rest > 0 ? ` and ${rest} more` : ''}. ${NAME} never removes a folder it did not make.`;
|
|
1077
|
+
return { count, now };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
927
1080
|
export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
|
|
928
1081
|
switch (key) {
|
|
929
1082
|
case 'agentsRunning':
|
|
@@ -957,7 +1110,9 @@ export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
|
|
|
957
1110
|
const n = Math.max(1, Math.min(MAX_SLOTS, Math.round(Number(value) || 1)));
|
|
958
1111
|
// Setting a number by hand IS taking the decision back, so the page never
|
|
959
1112
|
// shows a stepper that the supervisor is quietly ignoring.
|
|
960
|
-
|
|
1113
|
+
// FOR THE WHOLE COMPUTER (w-ec27ff1a51), under its own key, so a number
|
|
1114
|
+
// written per login before 5 Oct can never be read as this choice.
|
|
1115
|
+
saveConfig(config, { agentsAtOnce: n, agentsAuto: false });
|
|
961
1116
|
// An explicit choice replaces the startup plan suggestion for BOTH
|
|
962
1117
|
// engines. Leaving this marker set makes Codex ignore the new number
|
|
963
1118
|
// until loadConfig runs again at restart. It is derived, not persisted.
|
|
@@ -1007,6 +1162,11 @@ export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
|
|
|
1007
1162
|
case 'adhdMode':
|
|
1008
1163
|
saveConfig(config, { adhdMode: !!value });
|
|
1009
1164
|
break;
|
|
1165
|
+
// THE CORNER TAG (w-dafae58a23). Turning it back on also ends any hide, so
|
|
1166
|
+
// the switch does what it says the moment she flips it.
|
|
1167
|
+
case 'cornerTag':
|
|
1168
|
+
saveConfig(config, value ? { cornerTag: true, cornerTagHiddenUntil: 0 } : { cornerTag: false });
|
|
1169
|
+
break;
|
|
1010
1170
|
// WHICH ACCOUNT HER WORK RUNS ON.
|
|
1011
1171
|
//
|
|
1012
1172
|
// ONE KEY HOLDS BOTH ENGINES, because a profile word means different things
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Finder launches inherit launchd's PATH rather than the user's shell tools.
|
|
2
|
+
// Read only PATH: importing a profile's whole environment would import keys
|
|
3
|
+
// and login overrides too. Workers still pass through their billing scrub.
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const MARKER = '\0agentbox-path\0';
|
|
8
|
+
function runShell(file, args, options) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = execFile(file, args, options, (error, stdout) => error ? reject(error) : resolve(stdout));
|
|
11
|
+
child.stdin?.end();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* WHERE THE SHELL SAYS CLAUDE CODE AND CODEX KEEP THEIR SESSIONS (w-e46f2e9cea).
|
|
17
|
+
*
|
|
18
|
+
* An app opened from the Dock, or from a Linux launcher entry, never sees what
|
|
19
|
+
* a shell profile exports, so a CLAUDE_CONFIG_DIR or CODEX_HOME set there was
|
|
20
|
+
* invisible to the import unless the folder happened to be named
|
|
21
|
+
* ~/.claude-<name>. These two names and nothing else are read, the same way
|
|
22
|
+
* PATH is above, and they are handed to the session finder rather than put
|
|
23
|
+
* into the app's environment: CLAUDE_CONFIG_DIR there would change which
|
|
24
|
+
* account every worker runs on.
|
|
25
|
+
*
|
|
26
|
+
* Only an absolute path is kept. A quoted `~` the shell did not expand is
|
|
27
|
+
* expanded here; anything else, or no answer at all, is left out. */
|
|
28
|
+
const DIRS_MARKER = '\0agentbox-dirs\0';
|
|
29
|
+
export async function readShellDataDirs({ env = process.env, platform = process.platform, run = runShell } = {}) {
|
|
30
|
+
if (platform !== 'darwin' && platform !== 'linux') return {};
|
|
31
|
+
const shell = env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/sh');
|
|
32
|
+
try {
|
|
33
|
+
const stdout = String(await run(shell, ['-ilc', `printf '\\0agentbox-dirs\\0%s\\0%s\\0' "$CLAUDE_CONFIG_DIR" "$CODEX_HOME"`], {
|
|
34
|
+
env: { ...env }, encoding: 'utf8', timeout: 5000, maxBuffer: 64 * 1024,
|
|
35
|
+
}));
|
|
36
|
+
const start = stdout.lastIndexOf(DIRS_MARKER);
|
|
37
|
+
if (start < 0) return {};
|
|
38
|
+
const [claude = '', codex = ''] = stdout.slice(start + DIRS_MARKER.length).split('\0');
|
|
39
|
+
const out = {};
|
|
40
|
+
const keep = (name, raw) => {
|
|
41
|
+
let v = String(raw ?? '').trim();
|
|
42
|
+
if (!v || /[\r\n]/.test(v)) return;
|
|
43
|
+
if (v === '~' || v.startsWith('~/')) {
|
|
44
|
+
if (!env.HOME) return;
|
|
45
|
+
v = path.join(env.HOME, v.slice(1));
|
|
46
|
+
}
|
|
47
|
+
if (path.isAbsolute(v)) out[name] = v;
|
|
48
|
+
};
|
|
49
|
+
keep('CLAUDE_CONFIG_DIR', claude);
|
|
50
|
+
keep('CODEX_HOME', codex);
|
|
51
|
+
return out;
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What the shell answered, kept for the life of the app. Empty until it
|
|
59
|
+
* answers, which is long before setup reaches its last card. */
|
|
60
|
+
let shellDataEnv = {};
|
|
61
|
+
export const shellDataDirsNow = () => shellDataEnv;
|
|
62
|
+
export function learnShellDataDirs(opts) {
|
|
63
|
+
return readShellDataDirs(opts).then((found) => { shellDataEnv = found; return found; });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function recoverShellPath({ env = process.env, platform = process.platform, run = runShell } = {}) {
|
|
67
|
+
const original = env.PATH || '';
|
|
68
|
+
if (platform !== 'darwin') return { path: original, error: null };
|
|
69
|
+
const shell = env.SHELL || '/bin/zsh';
|
|
70
|
+
const value = path.basename(shell) === 'fish' ? '(string join : $PATH)' : '"$PATH"';
|
|
71
|
+
try {
|
|
72
|
+
// NUL framing keeps messages printed by .zshrc out of the returned PATH.
|
|
73
|
+
const stdout = String(await run(shell, ['-ilc', `printf '\\0agentbox-path\\0%s\\0' ${value}`], {
|
|
74
|
+
env: { ...env }, encoding: 'utf8', timeout: 5000, maxBuffer: 64 * 1024,
|
|
75
|
+
}));
|
|
76
|
+
const start = stdout.lastIndexOf(MARKER);
|
|
77
|
+
const end = start < 0 ? -1 : stdout.indexOf('\0', start + MARKER.length);
|
|
78
|
+
const recovered = end < 0 ? '' : stdout.slice(start + MARKER.length, end);
|
|
79
|
+
if (!recovered || /[\r\n]/.test(recovered)) throw Error('Your shell did not return its command search path.');
|
|
80
|
+
const entries = [...new Set([...recovered.split(':'), ...original.split(':')].filter(Boolean))];
|
|
81
|
+
if (!entries.length) throw Error('Your shell returned an empty command search path.');
|
|
82
|
+
return { path: entries.join(':'), error: null };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return { path: original, error: String(error?.message || error) };
|
|
85
|
+
}
|
|
86
|
+
}
|
package/main/ship-queue.mjs
CHANGED
|
@@ -77,15 +77,17 @@ export const wantsShipping = (item) => labelsOf(item).includes(SHIP_LABEL);
|
|
|
77
77
|
export const withoutShipLabel = (item) => labelsOf(item).filter((l) => l !== SHIP_LABEL);
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
80
|
+
* Every task this queue still owes a ship, whether or not a session is on it.
|
|
81
81
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
82
|
+
* A row of a project that ships, marked `ship`, not yet handled for that
|
|
83
|
+
* marking, and with a task folder to ship from. THE WINDOW READS THIS TOO
|
|
84
|
+
* (w-0c1ba766eb): a row on this list is waiting on the app, not on a person,
|
|
85
|
+
* so it is In progress and not Needs you until the queue has dealt with it.
|
|
84
86
|
*/
|
|
85
|
-
export function
|
|
87
|
+
export function waitingToShip(items, products, settings, { folderFor, handled = {} }) {
|
|
86
88
|
const bySlug = new Map(products.map((p) => [p.slug, p]));
|
|
87
|
-
|
|
88
|
-
.filter((i) => wantsShipping(i) && i.status !== 'done' && !
|
|
89
|
+
return items
|
|
90
|
+
.filter((i) => wantsShipping(i) && i.status !== 'done' && !(markedAt(i) <= (handled[keyOf(i)] ?? -1)))
|
|
89
91
|
.map((item) => {
|
|
90
92
|
const product = bySlug.get(item.product);
|
|
91
93
|
const script = product ? shipScriptFor(product, settings) : null;
|
|
@@ -93,6 +95,14 @@ export function nextToShip(items, products, settings, { isLive = () => false, fo
|
|
|
93
95
|
return script && cwd && fs.existsSync(cwd) ? { item, product, script, cwd } : null;
|
|
94
96
|
})
|
|
95
97
|
.filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The next task to ship, oldest ask first, or null: one the queue owes, with
|
|
102
|
+
* no session on it (an agent still working may yet commit).
|
|
103
|
+
*/
|
|
104
|
+
export function nextToShip(items, products, settings, { isLive = () => false, folderFor, handled = {} }) {
|
|
105
|
+
const ready = waitingToShip(items, products, settings, { folderFor, handled }).filter(({ item }) => !isLive(item));
|
|
96
106
|
ready.sort((a, b) => markedAt(a.item) - markedAt(b.item));
|
|
97
107
|
return ready[0] ?? null;
|
|
98
108
|
}
|
|
@@ -188,7 +198,7 @@ export function failureReply(output) {
|
|
|
188
198
|
...(died ? [
|
|
189
199
|
'',
|
|
190
200
|
'Its test run DIED rather than going red: nothing it printed names a failing',
|
|
191
|
-
'test. Look for what killed the run. This
|
|
201
|
+
'test. Look for what killed the run. This computer runs a dozen agents at once, so',
|
|
192
202
|
'a worker that was starved or timed out is likelier than a bug in your change.',
|
|
193
203
|
'Run the tests for your change again before you go looking for one.',
|
|
194
204
|
] : []),
|
|
@@ -255,6 +265,14 @@ export class ShipQueue {
|
|
|
255
265
|
return this.busy;
|
|
256
266
|
}
|
|
257
267
|
|
|
268
|
+
/** The ids of the rows still waiting on this queue, for the window. */
|
|
269
|
+
waitingIds(items, products) {
|
|
270
|
+
const settings = readShipSettings(this.userDir);
|
|
271
|
+
if (!Object.keys(settings).length) return [];
|
|
272
|
+
return waitingToShip(items, products, settings, { folderFor: this.folderFor, handled: readHandled(this.userDir) })
|
|
273
|
+
.map(({ item }) => item.id);
|
|
274
|
+
}
|
|
275
|
+
|
|
258
276
|
async ship({ item, product, script, cwd }) {
|
|
259
277
|
this.log.info?.(`zero: shipping ${item.id} from ${cwd}`);
|
|
260
278
|
const { code, out } = await this.run(script, cwd);
|
package/main/sign-in-files.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { harnessFor } from './harnesses.mjs';
|
|
1
2
|
// THE FILES A LOGIN WRITES, SO THE APP CAN SEE ONE LAND.
|
|
2
3
|
//
|
|
3
4
|
// A signed-out account used to sit out a fixed half hour, and nothing but a
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
// A moved time is a reason to TRY again, not proof; the try is the proof.
|
|
12
13
|
|
|
13
14
|
import fs from 'node:fs';
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Where one account's login is written.
|
|
@@ -21,12 +22,7 @@ import path from 'node:path';
|
|
|
21
22
|
* - Codex: `auth.json` inside its CODEX_HOME.
|
|
22
23
|
*/
|
|
23
24
|
export function signInFiles({ engine = 'claude', folder = null, home }) {
|
|
24
|
-
|
|
25
|
-
if (!folder) {
|
|
26
|
-
const dir = path.join(home, '.claude');
|
|
27
|
-
return [path.join(home, '.claude.json'), path.join(dir, '.claude.json'), path.join(dir, '.credentials.json')];
|
|
28
|
-
}
|
|
29
|
-
return [path.join(folder, '.claude.json'), path.join(folder, '.credentials.json')];
|
|
25
|
+
return harnessFor(engine).signInFiles({ folder, home });
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
/** The newest modification time among them, or 0 when none can be read. */
|
package/main/source-updater.mjs
CHANGED
|
@@ -55,7 +55,13 @@ const SAY = {
|
|
|
55
55
|
// on this Mac, and the restart below runs that build. Counted as her changes,
|
|
56
56
|
// they made every update after the first say "changes of your own" for good
|
|
57
57
|
// (tests/a-build-does-not-stop-the-next-update.test.mjs).
|
|
58
|
-
|
|
58
|
+
//
|
|
59
|
+
// THE LOCKFILE IS NPM'S, TOO (2026-10-07). The restart's `npm install` rewrote
|
|
60
|
+
// package-lock.json (this npm drops its `libc` lines), and the card never came
|
|
61
|
+
// back while main ran 21 commits ahead. An edit to the dependencies themselves
|
|
62
|
+
// is in package.json, which still blocks
|
|
63
|
+
// (tests/a-package-install-does-not-stop-the-next-update.test.mjs).
|
|
64
|
+
const isBuildOutput = (file) => file === 'package-lock.json' || /^shared\/[^/]+\.generated\.[cm]?[jt]s$/.test(file);
|
|
59
65
|
// The status code is one or two letters and `gitIn` trims the output, which
|
|
60
66
|
// takes the leading space off the first line, so it is matched, not sliced.
|
|
61
67
|
const changedPaths = (porcelain) => String(porcelain ?? '').split('\n').filter(Boolean).map((l) => l.replace(/^[ MADRCUT?!]{1,2} /, '').replace(/^"|"$/g, ''));
|
|
@@ -128,6 +134,18 @@ export function createSourceUpdater({
|
|
|
128
134
|
log = console,
|
|
129
135
|
onChanged = () => {},
|
|
130
136
|
npm = defaultNpm(appDir),
|
|
137
|
+
// THE STEP `npm start` DOES AND THIS DID NOT. A from-source run gets its
|
|
138
|
+
// name, icon and bundle id from scripts/brand-electron.mjs, which npm runs
|
|
139
|
+
// before Electron. An update does not go through npm: it builds and calls
|
|
140
|
+
// app.relaunch(), which re-executes the same binary. So the branding had
|
|
141
|
+
// never once run on a self-restart, and a fix to it could sit on disk for
|
|
142
|
+
// days without reaching the bundle (measured 2026-10-09, with the
|
|
143
|
+
// Info.plist four days older than the fix meant to change it).
|
|
144
|
+
// Imported here and not at the top of the file: scripts/ is deliberately not
|
|
145
|
+
// in the downloadable build (`build.files`, and a test holds it there), and
|
|
146
|
+
// a static import would stop that build booting at all. This path only ever
|
|
147
|
+
// runs from source, where the script is on disk.
|
|
148
|
+
brand = async () => (await import('../scripts/brand-electron.mjs')).brandElectron(),
|
|
131
149
|
relaunch = () => { app?.relaunch(); app?.quit(); },
|
|
132
150
|
} = {}) {
|
|
133
151
|
const git = gitIn(appDir);
|
|
@@ -251,12 +269,20 @@ export function createSourceUpdater({
|
|
|
251
269
|
set({ phase: 'ready', error: `The new packages would not install: ${failureLine(installed.out)}` });
|
|
252
270
|
return;
|
|
253
271
|
}
|
|
272
|
+
// What the install rewrote goes back, so the folder is left as main has
|
|
273
|
+
// it and anything else that fast-forwards it cannot trip on the file.
|
|
274
|
+
await git(['checkout', '--', 'package-lock.json']);
|
|
254
275
|
}
|
|
255
276
|
const built = await npm(['run', 'build']);
|
|
256
277
|
if (built.code !== 0) {
|
|
257
278
|
set({ phase: 'ready', error: `The new code would not build: ${failureLine(built.out)}` });
|
|
258
279
|
return;
|
|
259
280
|
}
|
|
281
|
+
// Before the relaunch, never after: the next launch is the one that reads
|
|
282
|
+
// the Info.plist. And never fatally, for the reason brand-electron.mjs
|
|
283
|
+
// opens with about itself — an app that does not come back up is worse
|
|
284
|
+
// than one wearing the wrong icon.
|
|
285
|
+
try { await brand(); } catch (e) { log.warn?.('[source-updater] could not rebrand the bundle:', e?.message ?? e); }
|
|
260
286
|
log.info?.('[source-updater] built', state.newVersion, '- relaunching');
|
|
261
287
|
relaunch();
|
|
262
288
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// NO NEW AGENT STARTS WHILE MEMORY IS SHORT (w-ec27ff1a51).
|
|
2
|
+
//
|
|
3
|
+
// 2026-10-11, a 16 GB Mac ran out of application memory with Agentbox at
|
|
4
|
+
// 22 GB and six agents running. The memory check (main/memory-gate.mjs) held
|
|
5
|
+
// tests and builds back, but nothing asked memory before STARTING an agent, so
|
|
6
|
+
// the fleet filled every slot it had into a Mac that was already short.
|
|
7
|
+
//
|
|
8
|
+
// So while the memory switch is on and the Mac reads tight or very short, a new
|
|
9
|
+
// agent waits. That is the whole rule. Everything else here is a way out of it,
|
|
10
|
+
// because the founder's condition was that urgent work still comes first and
|
|
11
|
+
// that nothing sits stuck for long on a misread:
|
|
12
|
+
//
|
|
13
|
+
// - NOTHING RUNNING, ONE STARTS. Whatever is filling memory then is not us,
|
|
14
|
+
// and an app with no agent running at all is broken rather than careful.
|
|
15
|
+
// - AN URGENT ROW, OR ONE SHE ASKED FOR BY NAME, starts while tight and waits
|
|
16
|
+
// only while very short: the command gate's rule for urgent commands.
|
|
17
|
+
// - A SWAP IS NOT A NEW AGENT. A row something was paused for (an Urgent row
|
|
18
|
+
// taking a lesser session's slot, or Run now) always starts, so urgent work
|
|
19
|
+
// still takes over while memory is very short, by replacing, not adding.
|
|
20
|
+
// - A READING THAT IS MISSING, UNREADABLE OR OLDER THAN A MINUTE IS NOT A
|
|
21
|
+
// REASON TO HOLD. The hold acts only on a reading it actually has.
|
|
22
|
+
// - TEN MINUTES HELD, ONE STARTS ANYWAY, and the ten minutes start again from
|
|
23
|
+
// it. A reading stuck on "tight" by mistake costs a slow queue, never a
|
|
24
|
+
// stopped one.
|
|
25
|
+
//
|
|
26
|
+
// Pure, so every one of those is a test (tests/no-new-agent-starts-while-memory-is-short).
|
|
27
|
+
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
import { pressureFrom } from './memory-gate.mjs';
|
|
30
|
+
|
|
31
|
+
/** One reading, synchronously, for the moment before a restart brings the
|
|
32
|
+
* fleet back. Nulls (so no hold) when it cannot be read. */
|
|
33
|
+
export function readMacPressureNow() {
|
|
34
|
+
let level = null;
|
|
35
|
+
let freePct = null;
|
|
36
|
+
try {
|
|
37
|
+
const out = execFileSync('/usr/sbin/sysctl', ['-n', 'kern.memorystatus_vm_pressure_level', 'kern.memorystatus_level'],
|
|
38
|
+
{ timeout: 2_000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
39
|
+
[level, freePct] = out.trim().split(/\s+/).map(Number);
|
|
40
|
+
} catch { /* not a Mac, or sysctl did not answer */ }
|
|
41
|
+
return { level: Number.isFinite(level) ? level : null, freePct: Number.isFinite(freePct) ? freePct : null, at: Date.now() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A reading older than this is not acted on. */
|
|
45
|
+
export const STALE_MS = 60_000;
|
|
46
|
+
/** The longest new work waits on memory before one agent starts anyway. */
|
|
47
|
+
export const MAX_HOLD_MS = 10 * 60_000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether one more agent may start now, and why. `reading` is
|
|
51
|
+
* `{ level, freePct, at }` from `readMacPressure`, stamped when it arrived.
|
|
52
|
+
* `heldSince` is when the current hold began (null when nothing is held);
|
|
53
|
+
* `lastStartAt` is when an agent last started.
|
|
54
|
+
*/
|
|
55
|
+
export function startDecision({
|
|
56
|
+
on, reading, now, running,
|
|
57
|
+
urgent = false, asked = false, swap = false,
|
|
58
|
+
lastStartAt = 0, heldSince = null,
|
|
59
|
+
staleMs = STALE_MS, maxHoldMs = MAX_HOLD_MS,
|
|
60
|
+
}) {
|
|
61
|
+
if (!on) return { start: true, why: 'off' };
|
|
62
|
+
if (!(running > 0)) return { start: true, why: 'nothing-running' };
|
|
63
|
+
if (swap) return { start: true, why: 'swap' };
|
|
64
|
+
const at = Number(reading?.at);
|
|
65
|
+
const known = reading && (Number.isFinite(reading.level) || Number.isFinite(reading.freePct));
|
|
66
|
+
if (!known || !Number.isFinite(at) || Math.abs(now - at) > staleMs) return { start: true, why: 'no-reading' };
|
|
67
|
+
const pressure = pressureFrom(reading);
|
|
68
|
+
if (pressure === 'normal') return { start: true, why: 'normal' };
|
|
69
|
+
if (pressure === 'tight' && (urgent || asked)) return { start: true, why: 'urgent' };
|
|
70
|
+
const waitingSince = Math.max(heldSince ?? now, Number(lastStartAt) || 0);
|
|
71
|
+
if (now - waitingSince >= maxHoldMs) return { start: true, why: 'overdue' };
|
|
72
|
+
return { start: false, why: pressure };
|
|
73
|
+
}
|