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/main.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { ensurePersonalProject } from './team/projects.mjs';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
-
import { configDir, loadConfig } from './config.mjs';
|
|
14
|
+
import { configDir, loadConfig, saveConfig } from './config.mjs';
|
|
15
15
|
import { installMenu, applyZoom, requestFind } from './menu.mjs';
|
|
16
16
|
import { zoomDeltaFor } from './zoom-keys.mjs';
|
|
17
17
|
import { copySelection } from './copy-selection.mjs';
|
|
@@ -20,8 +20,14 @@ import { Store } from './store.mjs';
|
|
|
20
20
|
import { storeRootEnv } from './store/home.mjs';
|
|
21
21
|
import { Supervisor } from './supervisor.mjs';
|
|
22
22
|
import { startCodexWatch } from './codex-watch.mjs';
|
|
23
|
+
import { readCodexThreads } from './codex-threads.mjs';
|
|
24
|
+
import { appSessionFolders } from './agent-homes.mjs';
|
|
23
25
|
import { registerIpc } from './ipc.mjs';
|
|
24
|
-
import {
|
|
26
|
+
import { createPhoneLink } from './phone-link.mjs';
|
|
27
|
+
import { relayAddress } from './relay-address.mjs';
|
|
28
|
+
import { atTheApp } from '../shared/notify-rules.mjs';
|
|
29
|
+
import { answerWhatOnlyTheWindowAnswered } from './serve.mjs';
|
|
30
|
+
import { carryHerBriefsAcross, joinMessageRules, setAsideShippedMessageRules, unifyAgentInstructions } from './instruction-settings.mjs';
|
|
25
31
|
import * as approvals from './approvals.mjs';
|
|
26
32
|
import { recoveryToast } from '../shared/recovery.mjs';
|
|
27
33
|
import { dataFolderName, isNewUserBuild } from '../shared/side-build.mjs';
|
|
@@ -30,10 +36,14 @@ import { openFreshUser } from './fresh-user.mjs';
|
|
|
30
36
|
import { installCrashReports, reportFromRenderer, pending as pendingCrashes, setTransport } from './crash-report.mjs';
|
|
31
37
|
import { createAnalytics, createDailyCount } from './analytics.mjs';
|
|
32
38
|
import { createUpdater } from './updater.mjs';
|
|
39
|
+
import { createPlace } from '../shared/where-it-runs-from.mjs';
|
|
33
40
|
import { createSourceUpdater } from './source-updater.mjs';
|
|
41
|
+
import { learnShellDataDirs, recoverShellPath } from './shell-path.mjs';
|
|
34
42
|
import { installNotifier } from './notify.mjs';
|
|
43
|
+
import { createCornerTag } from './corner-tag.mjs';
|
|
35
44
|
import { DOC_SCHEMES, DocGrants, docPath } from './doc-scheme.mjs';
|
|
36
45
|
import { quietTheFramesScrollbars } from './frame-scrollbars.mjs';
|
|
46
|
+
import { letLocalAppsShowInThePane } from './local-app-frames.mjs';
|
|
37
47
|
import { IMG_SCHEMES, imgPath, mediaResponse, mediaType, servable } from './img-scheme.mjs';
|
|
38
48
|
import { hotWindowVerdict, storeHasWork } from './dev-window.mjs';
|
|
39
49
|
import { writeHeldThenReload } from './write-before-reload.mjs';
|
|
@@ -206,6 +216,9 @@ try {
|
|
|
206
216
|
// AND A BOX THAT AN OLDER JOIN FILLED WITH ONLY OUR TEXT IS EMPTIED, ONCE
|
|
207
217
|
// (w-3ec9f07978). Kept as a restore point; our rules ride from the checkout.
|
|
208
218
|
if (setAsideShippedMessageRules(appDir, userDir)) console.log('zero: the message rules box held only the shipped rules; set aside as a restore point');
|
|
219
|
+
// Both kinds of user rules now live in founder.md. Only user content is
|
|
220
|
+
// appended; the app's message defaults continue to come from the bundle.
|
|
221
|
+
if (unifyAgentInstructions(appDir, userDir)) console.log('zero: combined the general and writing instructions');
|
|
209
222
|
} catch (err) {
|
|
210
223
|
console.warn(`zero: could not carry her instructions into ${userDir}: ${err.message}`);
|
|
211
224
|
}
|
|
@@ -272,6 +285,13 @@ function reloadRenderer() {
|
|
|
272
285
|
}
|
|
273
286
|
|
|
274
287
|
async function createWindow() {
|
|
288
|
+
const shellPath = await recoverShellPath();
|
|
289
|
+
process.env.PATH = shellPath.path;
|
|
290
|
+
if (shellPath.error) console.warn('agentbox: could not load your shell PATH; using the inherited PATH:', shellPath.error);
|
|
291
|
+
// Where the shell says Claude Code and Codex keep their sessions, for the
|
|
292
|
+
// import only and never into this environment (main/shell-path.mjs). Not
|
|
293
|
+
// waited for: it lands long before setup reaches its last card.
|
|
294
|
+
void learnShellDataDirs();
|
|
275
295
|
const config = loadConfig(dataDir);
|
|
276
296
|
// The store modules read the app's own home variable at call time; setting it
|
|
277
297
|
// here points auth session files, sync, and every store path at its root.
|
|
@@ -396,7 +416,16 @@ async function createWindow() {
|
|
|
396
416
|
// HER CODEX CONVERSATIONS ASK TO COME IN. Once a minute: a new one in a
|
|
397
417
|
// folder a project points at gets a row asking yes or no, and the ones she
|
|
398
418
|
// said yes to follow Codex's latest answer.
|
|
399
|
-
|
|
419
|
+
// AND IT IS WHERE A CODEX AGENT IS COUNTED, because it is the one thing that
|
|
420
|
+
// already reads them (main/codex-watch.mjs). The switch is checked by
|
|
421
|
+
// `analytics.track` itself, so this hands it over unconditionally.
|
|
422
|
+
// Every Codex folder on the machine, read fresh each minute so a login made
|
|
423
|
+
// while the app is open is followed too (main/agent-homes.mjs).
|
|
424
|
+
const codexWatch = startCodexWatch({
|
|
425
|
+
store,
|
|
426
|
+
count: (name, props) => analytics.track(name, props),
|
|
427
|
+
readThreads: (opts) => readCodexThreads({ ...opts, codexDirs: appSessionFolders({ config, supervisor }).codexDirs }),
|
|
428
|
+
});
|
|
400
429
|
app.on('before-quit', () => codexWatch.stop());
|
|
401
430
|
// Whatever is still queued goes with the app rather than dying in memory.
|
|
402
431
|
app.on('will-quit', () => { analytics.flush(); });
|
|
@@ -603,6 +632,11 @@ async function createWindow() {
|
|
|
603
632
|
},
|
|
604
633
|
});
|
|
605
634
|
|
|
635
|
+
// THE MENU IS A BAR INSIDE THE WINDOW on Linux and Windows. On macOS it is
|
|
636
|
+
// the system menu, so it stays. The menu itself stays everywhere: the Ctrl
|
|
637
|
+
// chords are bound to it, and taking the menu out would take them with it.
|
|
638
|
+
if (process.platform !== 'darwin') window.setMenuBarVisibility(false);
|
|
639
|
+
|
|
606
640
|
// WHICH PICTURE SET THIS WINDOW IS ON, and a shout when it changes.
|
|
607
641
|
// main/screen-detail.mjs has the rule and the reasoning; this is the wiring.
|
|
608
642
|
// The initial answer rides bootInfo rather than being pushed, for the same
|
|
@@ -653,9 +687,16 @@ async function createWindow() {
|
|
|
653
687
|
// AND A COPY RUN FROM SOURCE watches its git branch instead of a release
|
|
654
688
|
// feed (main/source-updater.mjs), because that is how teammates run the team
|
|
655
689
|
// build and their updates are pushes to main, not releases.
|
|
690
|
+
//
|
|
691
|
+
// AND WHERE THIS COPY IS RUNNING FROM (shared/where-it-runs-from.mjs). An
|
|
692
|
+
// app run out of the disk image, or out of a read-only copy macOS made of
|
|
693
|
+
// it, can never update itself, and until 2026-10-12 nothing in the app said
|
|
694
|
+
// so: two installs sat on 0.1.11 for a week. The fact rides the updater's
|
|
695
|
+
// state so the Settings row, the sidebar card and ⌘K all read one answer.
|
|
656
696
|
let pushUpdate = () => {};
|
|
657
697
|
const updater = app.isPackaged ? createUpdater({
|
|
658
698
|
app,
|
|
699
|
+
place: createPlace({ app }),
|
|
659
700
|
onChanged: () => pushUpdate(),
|
|
660
701
|
}) : createSourceUpdater({
|
|
661
702
|
app,
|
|
@@ -665,9 +706,25 @@ async function createWindow() {
|
|
|
665
706
|
|
|
666
707
|
// The desktop door. `main/serve.mjs` is the other one and hands over plain
|
|
667
708
|
// Node stand-ins for these three; everything else about the call is the same.
|
|
709
|
+
// THE PHONE DOOR (main/phone-link.mjs) answers with these same handlers, so
|
|
710
|
+
// each one is filed in a Map as Electron gets it. And every push the window
|
|
711
|
+
// gets is handed to the phones' event streams too.
|
|
712
|
+
const phoneChannels = new Map();
|
|
713
|
+
const phoneListeners = new Set();
|
|
714
|
+
const filingIpcMain = {
|
|
715
|
+
handle(channel, fn) { ipcMain.handle(channel, fn); phoneChannels.set(channel, fn); },
|
|
716
|
+
};
|
|
717
|
+
const filingHost = { ipcMain: filingIpcMain, app, dialog };
|
|
718
|
+
const sendToWindow = window.webContents.send.bind(window.webContents);
|
|
719
|
+
window.webContents.send = (channel, ...args) => {
|
|
720
|
+
sendToWindow(channel, ...args);
|
|
721
|
+
for (const send of phoneListeners) {
|
|
722
|
+
try { send({ channel, args }); } catch { /* a closed phone is not an error */ }
|
|
723
|
+
}
|
|
724
|
+
};
|
|
668
725
|
const ipc = registerIpc({
|
|
669
726
|
store, supervisor, config, window, analytics, docGrants, updater,
|
|
670
|
-
host:
|
|
727
|
+
host: filingHost,
|
|
671
728
|
team,
|
|
672
729
|
});
|
|
673
730
|
pushUpdate = ipc.push;
|
|
@@ -675,6 +732,33 @@ async function createWindow() {
|
|
|
675
732
|
// See the `installMenu` call above for what writing the file instead did to a
|
|
676
733
|
// Codex card.
|
|
677
734
|
answerApproval = ipc.answerApproval;
|
|
735
|
+
// What the window answers itself (badge, boot info) a phone gets the plain
|
|
736
|
+
// stand-ins for, as a browser tab does.
|
|
737
|
+
answerWhatOnlyTheWindowAnswered({ handle: (channel, fn) => { if (!phoneChannels.has(channel)) phoneChannels.set(channel, fn); } });
|
|
738
|
+
const phone = createPhoneLink({
|
|
739
|
+
file: path.join(userDir, 'phone-link.json'),
|
|
740
|
+
channels: phoneChannels,
|
|
741
|
+
listeners: phoneListeners,
|
|
742
|
+
dist: path.join(appDir, 'renderer', 'dist'),
|
|
743
|
+
// Pictures and design pages, under the same two rules the window's own
|
|
744
|
+
// schemes use below: a page the pane was granted, and a picture inside a
|
|
745
|
+
// root the app owns (main/serve.mjs answerFile).
|
|
746
|
+
files: { allows: (kind, f) => (kind === 'doc' ? docGrants.allows(f) : servable(f, imageRoots())) },
|
|
747
|
+
// "Anywhere": the relay a phone reaches this Mac through from any network
|
|
748
|
+
// (main/relay-client.mjs). None until it is deployed, and then no choice.
|
|
749
|
+
relayUrl: relayAddress({ appDir }),
|
|
750
|
+
onChange: () => { if (!window.isDestroyed()) sendToWindow('zero:phone-changed'); },
|
|
751
|
+
// The phone stays quiet while she is at the desktop app, by the same rule
|
|
752
|
+
// the desktop's own notifications use (shared/notify-rules.mjs).
|
|
753
|
+
atDesk: () => {
|
|
754
|
+
try { return atTheApp({ focused: !window.isDestroyed() && window.isFocused(), idleMs: powerMonitor.getSystemIdleTime() * 1000 }); } catch { return false; }
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
window.on('focus', () => phone.seen());
|
|
758
|
+
// Settings' own channel, on the window only: a phone does not get to turn
|
|
759
|
+
// the phone door off or hand out its key.
|
|
760
|
+
ipcMain.handle('zero:phone-link', (_e, payload) => phone.act(payload ?? {}));
|
|
761
|
+
app.on('before-quit', () => phone.close());
|
|
678
762
|
updater.start();
|
|
679
763
|
// A task the app just shipped moved main: look now rather than in half an
|
|
680
764
|
// hour, so the restart is offered while she still remembers the task.
|
|
@@ -847,6 +931,7 @@ async function createWindow() {
|
|
|
847
931
|
// Unread badge + notification on new inbox arrivals are driven by the
|
|
848
932
|
// renderer (it owns the definition of "inbox"), through the badge API.
|
|
849
933
|
ipcMain.handle('zero:badge', (_e, count) => {
|
|
934
|
+
phone.count(count);
|
|
850
935
|
if (process.platform === 'darwin') app.dock.setBadge(count > 0 ? String(count) : '');
|
|
851
936
|
});
|
|
852
937
|
// WHETHER to speak, and what to say, is not the renderer's call: only this
|
|
@@ -855,7 +940,21 @@ async function createWindow() {
|
|
|
855
940
|
// a locked screen all read the same from in there). The renderer says what
|
|
856
941
|
// arrived; main/notify.mjs decides. tell me when I am in another app, never
|
|
857
942
|
// while I am in Agentbox.
|
|
858
|
-
installNotifier({ app, window, Notification, powerMonitor, ipcMain });
|
|
943
|
+
installNotifier({ app, window, Notification, powerMonitor, ipcMain, nativeImage, onArrivals: (arrivals) => phone.arrivals(arrivals) });
|
|
944
|
+
// THE CORNER TAG (w-dafae58a23): what is ready for her, floating over
|
|
945
|
+
// whatever app she is in. Its page is built beside the app's own, and comes
|
|
946
|
+
// from the dev server only when the app's own page does.
|
|
947
|
+
createCornerTag({
|
|
948
|
+
BrowserWindow, screen: electronScreen, ipcMain, Menu, powerMonitor, config, saveConfig, mainWindow: window,
|
|
949
|
+
preload: path.join(appDir, 'main', 'corner-tag-preload.cjs'),
|
|
950
|
+
// `part` is the tag itself or the list beside it, one page drawn twice.
|
|
951
|
+
load: (tagWindow, part) => {
|
|
952
|
+
const devTag = process.env.ZERO_DEV_URL;
|
|
953
|
+
const builtTag = path.join(appDir, 'renderer', 'dist', 'corner-tag.html');
|
|
954
|
+
if (devTag && !fs.existsSync(builtTag)) tagWindow.loadURL(`${devTag.replace(/\/$/, '')}/corner-tag.html?part=${part}`);
|
|
955
|
+
else tagWindow.loadFile(builtTag, { search: `part=${part}` });
|
|
956
|
+
},
|
|
957
|
+
});
|
|
859
958
|
// Asked for once by the renderer as it mounts, rather than pushed on
|
|
860
959
|
// did-finish-load, which races the first effects and would drop the very
|
|
861
960
|
// message it exists to deliver.
|
|
@@ -899,6 +998,11 @@ async function createWindow() {
|
|
|
899
998
|
// (frame-scrollbars.mjs). Without it, a mouse on macOS gets a white track.
|
|
900
999
|
quietTheFramesScrollbars(window.webContents);
|
|
901
1000
|
|
|
1001
|
+
// A local app a worker names (http://localhost:3006) draws in the pane even
|
|
1002
|
+
// when it refuses to be framed, and its sign-in cookie sticks there
|
|
1003
|
+
// (main/local-app-frames.mjs).
|
|
1004
|
+
letLocalAppsShowInThePane(window.webContents);
|
|
1005
|
+
|
|
902
1006
|
// Escape must ALWAYS leave the junk browser, even when the guest page has
|
|
903
1007
|
// focus and would otherwise swallow the key. Intercept it below the page.
|
|
904
1008
|
app.on('web-contents-created', (_event, contents) => {
|
|
@@ -1014,6 +1118,17 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
1014
1118
|
}
|
|
1015
1119
|
});
|
|
1016
1120
|
app.whenReady().then(() => {
|
|
1121
|
+
// A from-source Linux launch keeps the launcher command installed. The
|
|
1122
|
+
// entry runs that command; this only refreshes where it points. A packaged
|
|
1123
|
+
// build ships its own entry and must not write one from here.
|
|
1124
|
+
// LOADED HERE AND NOT AT THE TOP: scripts/ is not in the packaged app or the
|
|
1125
|
+
// npx package, and a static import of it stopped main.mjs loading at all,
|
|
1126
|
+
// on every Mac (tests/the-mac-app-ships-every-file-it-starts-with).
|
|
1127
|
+
if (process.platform === 'linux' && !app.isPackaged) {
|
|
1128
|
+
import('../scripts/linux-launcher.mjs')
|
|
1129
|
+
.then(({ installLinuxLauncher }) => installLinuxLauncher({ appDir, env: process.env }))
|
|
1130
|
+
.catch(() => { /* a launcher is never a reason to stop a launch */ });
|
|
1131
|
+
}
|
|
1017
1132
|
// THE ABOUT BOX SAYS AGENTBOX AND ITS OWN VERSION (w-db6f5e331e). Run from
|
|
1018
1133
|
// source it read the bundle's, which is Electron's: "Electron 43.0.0".
|
|
1019
1134
|
app.setAboutPanelOptions({ applicationName: NAME, applicationVersion: app.getVersion(), version: '' });
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
// measured using, here, the last few times it ran.
|
|
9
9
|
//
|
|
10
10
|
// THE ONE RULE THAT IS NOT NEGOTIABLE: missing evidence never makes a command
|
|
11
|
-
// light. Light means "
|
|
12
|
-
// heavy run is enough to make that exact command heavy. A program that has
|
|
13
|
-
// light every time across many different arguments lends
|
|
14
|
-
// it has not seen, and stops the moment one of its runs is
|
|
11
|
+
// light. Light means "measured light, more than once, and never heavy since".
|
|
12
|
+
// One heavy run is enough to make that exact command heavy. A program that has
|
|
13
|
+
// been light every time it was measured, across many different arguments, lends
|
|
14
|
+
// that to an argument it has not seen, and stops the moment one of its runs is
|
|
15
|
+
// measured as anything else.
|
|
16
|
+
//
|
|
17
|
+
// AND MISSING EVIDENCE DOES NOT MAKE A COMMAND HEAVY EITHER (w-b689e9fc0c). A
|
|
18
|
+
// run nothing measured is filed as `?` and read as nothing at all, rather than
|
|
19
|
+
// as a run that might have been big: see `measured` below for what that cost.
|
|
15
20
|
//
|
|
16
21
|
// A RUN THAT ENDED INSIDE 2 SECONDS AND WAS NEVER SEEN BIG COUNTS AS LIGHT.
|
|
17
22
|
// Measured on 2026-10-04 across 30 recent agent sessions (2006 commands): the
|
|
@@ -152,6 +157,26 @@ function basename(p) {
|
|
|
152
157
|
|
|
153
158
|
const programOf = (key) => key.split(' ')[0];
|
|
154
159
|
|
|
160
|
+
/**
|
|
161
|
+
* The observations that are evidence, which is all of them but `?`.
|
|
162
|
+
*
|
|
163
|
+
* `?` IS THE ABSENCE OF EVIDENCE AND IS READ AS NOTHING AT ALL (w-b689e9fc0c).
|
|
164
|
+
* It used to count against a command: `git branch` stood at "lllll?llll", nine
|
|
165
|
+
* light runs and one that nothing measured, and the single `?` inside the last
|
|
166
|
+
* five made every `git branch` command `unknown` — and because the command's own
|
|
167
|
+
* record had answered, the `git` program record, ten light runs with nothing else
|
|
168
|
+
* in it, was never reached. On a 16 GB M4 on 2026-10-07 that put
|
|
169
|
+
* `git fetch -q origin | tail -2; git branch -r --contains <sha> | head -3`
|
|
170
|
+
* behind one of two slots for 89 seconds to use 0 MB. One unmeasured run held
|
|
171
|
+
* that command back for its next nine, and a loaded Mac is precisely where runs
|
|
172
|
+
* go unmeasured, so the gate made itself hold more and more.
|
|
173
|
+
*
|
|
174
|
+
* The rule it must not break is the one above: missing evidence never makes a
|
|
175
|
+
* command light. It cannot, because a `?` is dropped rather than counted, and
|
|
176
|
+
* what is left still has to be light and still has to be more than one run.
|
|
177
|
+
*/
|
|
178
|
+
const measured = (list) => (list ?? []).filter((o) => o !== '?');
|
|
179
|
+
|
|
155
180
|
/**
|
|
156
181
|
* The learned record. Each key and each program keeps its last ten
|
|
157
182
|
* observations as letters: l light, m in between, h heavy, ? not measured.
|
|
@@ -167,27 +192,41 @@ export class CommandHistory {
|
|
|
167
192
|
this.programs = new Map();
|
|
168
193
|
}
|
|
169
194
|
|
|
170
|
-
/**
|
|
171
|
-
|
|
195
|
+
/**
|
|
196
|
+
* What one finished run tells us. `peakMb` null means it was never sampled.
|
|
197
|
+
* Answers whether anything was filed.
|
|
198
|
+
*
|
|
199
|
+
* `onlyIfLight` is for a run whose memory reading belonged to several
|
|
200
|
+
* commands at once: one `codex app-server` serves every Codex thread of a
|
|
201
|
+
* login, so two Codex commands running together are measured together
|
|
202
|
+
* (w-5601e99977). How long each took is still its own, so a quick one is
|
|
203
|
+
* light on that alone — and anything else from such a run is dropped rather
|
|
204
|
+
* than filed as "not measured", because that would un-learn a command this
|
|
205
|
+
* app has seen light before, and missing evidence must never move a command
|
|
206
|
+
* either way.
|
|
207
|
+
*/
|
|
208
|
+
record(command, { peakMb = null, durationMs = null, background = false, onlyIfLight = false } = {}) {
|
|
172
209
|
const { keys, opaque } = commandSegments(command);
|
|
173
|
-
if (!keys.length) return;
|
|
210
|
+
if (!keys.length) return false;
|
|
174
211
|
let obs;
|
|
175
212
|
if (typeof peakMb === 'number' && Number.isFinite(peakMb)) {
|
|
176
213
|
obs = peakMb >= this.heavyMb ? 'h' : peakMb < this.lightMb ? 'l' : 'm';
|
|
177
214
|
} else {
|
|
178
215
|
obs = !background && typeof durationMs === 'number' && durationMs < QUICK_MS ? 'l' : '?';
|
|
179
216
|
}
|
|
217
|
+
if (onlyIfLight && obs !== 'l') return false;
|
|
180
218
|
// A chain's memory belongs to the chain: `git status && npm test` measured
|
|
181
219
|
// heavy says nothing bad about `git status`. Only a light chain vouches for
|
|
182
220
|
// each of its parts.
|
|
183
221
|
if (keys.length > 1 || opaque) {
|
|
184
222
|
this._push(this.keys, chainKey(keys, opaque), obs);
|
|
185
|
-
if (obs !== 'l') return;
|
|
223
|
+
if (obs !== 'l') return true;
|
|
186
224
|
}
|
|
187
225
|
for (const key of keys) {
|
|
188
226
|
this._push(this.keys, key, obs);
|
|
189
227
|
this._push(this.programs, programOf(key), obs);
|
|
190
228
|
}
|
|
229
|
+
return true;
|
|
191
230
|
}
|
|
192
231
|
|
|
193
232
|
/** 'light' | 'unknown' | 'heavy' for a command about to run. */
|
|
@@ -210,12 +249,12 @@ export class CommandHistory {
|
|
|
210
249
|
|
|
211
250
|
_classifyKey(key) {
|
|
212
251
|
const own = this.keys.get(key);
|
|
213
|
-
const recent = own?.slice(-5)
|
|
252
|
+
const recent = measured(own?.slice(-5));
|
|
214
253
|
if (recent.includes('h')) return 'heavy';
|
|
215
|
-
if (
|
|
254
|
+
if (recent.length >= 2 && recent.every((o) => o === 'l')) return 'light';
|
|
216
255
|
if (recent.some((o) => o !== 'l')) return 'unknown';
|
|
217
|
-
const prog = this.programs.get(programOf(key));
|
|
218
|
-
if (prog
|
|
256
|
+
const prog = measured(this.programs.get(programOf(key))?.slice(-10));
|
|
257
|
+
if (prog.length >= 5 && prog.every((o) => o === 'l')) return 'light';
|
|
219
258
|
return 'unknown';
|
|
220
259
|
}
|
|
221
260
|
|
|
@@ -104,6 +104,10 @@ export class MemoryGateServer {
|
|
|
104
104
|
// the app-server's own `thread.id`. Answering null means "not this app's",
|
|
105
105
|
// and such a command is let straight through.
|
|
106
106
|
ownerOf = null,
|
|
107
|
+
// IS THIS ROW HER URGENT ONE (w-713aba0c89), asked live on every command so
|
|
108
|
+
// a row raised to Urgent counts at once. Only the app answers it: nothing in
|
|
109
|
+
// a worker's own request can make its command urgent.
|
|
110
|
+
urgentFor = null,
|
|
107
111
|
readPressure = readMacPressure,
|
|
108
112
|
sampleProcesses = listProcesses,
|
|
109
113
|
pollMs = 2_000,
|
|
@@ -113,6 +117,12 @@ export class MemoryGateServer {
|
|
|
113
117
|
lockPort = DEFAULT_LOCK_PORT,
|
|
114
118
|
idleReleaseMs = 30_000,
|
|
115
119
|
maxGrantMs = 3 * 60 * 60_000,
|
|
120
|
+
// How long a grant may hold a slot when its end can never be observed,
|
|
121
|
+
// because its memory reading is shared with another command on the same
|
|
122
|
+
// agent process. Null means the longest the gate will make anything wait:
|
|
123
|
+
// a grant nobody can see the end of must not outlive the wait it causes,
|
|
124
|
+
// or a queue is refused one by one while a phantom holds the slot.
|
|
125
|
+
sharedGrantMs = null,
|
|
116
126
|
clock = Date.now,
|
|
117
127
|
log = () => {},
|
|
118
128
|
} = {}) {
|
|
@@ -123,8 +133,10 @@ export class MemoryGateServer {
|
|
|
123
133
|
this.idleReleaseMs = idleReleaseMs;
|
|
124
134
|
this.maxGrantMs = maxGrantMs;
|
|
125
135
|
this.gate = new MemoryGate({ ...gate, history: this.history, clock });
|
|
136
|
+
this.sharedGrantMs = Number.isFinite(sharedGrantMs) ? sharedGrantMs : this.gate.maxWaitMs;
|
|
126
137
|
this.scoreFor = scoreFor;
|
|
127
138
|
this.ownerOf = ownerOf;
|
|
139
|
+
this.urgentFor = urgentFor;
|
|
128
140
|
this.readPressure = readPressure;
|
|
129
141
|
this.sampleProcesses = sampleProcesses;
|
|
130
142
|
this.pollMs = pollMs;
|
|
@@ -292,7 +304,24 @@ export class MemoryGateServer {
|
|
|
292
304
|
let owner;
|
|
293
305
|
try { owner = this.ownerOf(session); } catch { return null; }
|
|
294
306
|
if (!owner?.item) return null;
|
|
295
|
-
return { item: String(owner.item), product: owner.product ?? null };
|
|
307
|
+
return { item: String(owner.item), product: owner.product ?? null, session };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A CODEX THREAD'S TURN IS OVER: LET GO OF EVERY GRANT IT STILL HOLDS
|
|
312
|
+
* (w-713aba0c89). Codex sends no "after" report for a command that failed, so
|
|
313
|
+
* such a grant outlives its command, and on a shared app-server nothing else
|
|
314
|
+
* ever sees it end. A turn that has ended is waiting on none of its commands.
|
|
315
|
+
* Only grants that named this thread are touched: a Claude worker's names its
|
|
316
|
+
* row, and goes when its own process does.
|
|
317
|
+
*/
|
|
318
|
+
releaseSession(session) {
|
|
319
|
+
if (typeof session !== 'string' || !session) return;
|
|
320
|
+
for (const r of [...this.gate.running.values()]) {
|
|
321
|
+
if (r.session !== session) continue;
|
|
322
|
+
this.grants.delete(r.id);
|
|
323
|
+
this.gate.finish(r.id);
|
|
324
|
+
}
|
|
296
325
|
}
|
|
297
326
|
|
|
298
327
|
_pre(body, req, res) {
|
|
@@ -308,10 +337,12 @@ export class MemoryGateServer {
|
|
|
308
337
|
const id = String(hook.tool_use_id || `${body.ppid}-${Date.now()}-${Math.random()}`);
|
|
309
338
|
const live = typeof this.scoreFor === 'function' ? this.scoreFor(whose.product, whose.item) : null;
|
|
310
339
|
const score = Number.isFinite(live) ? live : Number(body.score) || 0;
|
|
340
|
+
let urgent = false;
|
|
341
|
+
try { urgent = typeof this.urgentFor === 'function' && this.urgentFor(whose.product, whose.item) === true; } catch { urgent = false; }
|
|
311
342
|
this.counts.asked++;
|
|
312
343
|
let answered = false;
|
|
313
344
|
this.gate.submit({
|
|
314
|
-
id, command, score, item: whose.item, product: whose.product,
|
|
345
|
+
id, command, score, urgent, item: whose.item, product: whose.product, session: whose.session ?? null,
|
|
315
346
|
background: !!hook?.tool_input?.run_in_background,
|
|
316
347
|
}, (decision) => {
|
|
317
348
|
answered = true;
|
|
@@ -341,13 +372,20 @@ export class MemoryGateServer {
|
|
|
341
372
|
this.grants.delete(id);
|
|
342
373
|
const was = this.gate.finish(id);
|
|
343
374
|
if (!was) return;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
375
|
+
// A SHARED READING IS NOT NO READING (w-5601e99977). Every Codex thread of
|
|
376
|
+
// a login runs under one `codex app-server`, so two Codex commands at once
|
|
377
|
+
// are measured together and neither may claim the figure. How long each
|
|
378
|
+
// took is its own, though, and a run that was over inside QUICK_MS counts
|
|
379
|
+
// as light on that alone, so `onlyIfLight` files that verdict and nothing
|
|
380
|
+
// else. Before this, Codex taught the history almost nothing and rode on
|
|
381
|
+
// what Claude workers taught it.
|
|
382
|
+
const filed = this.history.record(was.command, {
|
|
383
|
+
peakMb: grant?.ambiguous ? null : was.peakMb,
|
|
347
384
|
durationMs: Number(hook.duration_ms) || null,
|
|
348
385
|
background: was.background,
|
|
386
|
+
onlyIfLight: !!grant?.ambiguous,
|
|
349
387
|
});
|
|
350
|
-
this.dirty = true;
|
|
388
|
+
if (filed) this.dirty = true;
|
|
351
389
|
}
|
|
352
390
|
|
|
353
391
|
/* ------------------------------ measuring ------------------------------ */
|
|
@@ -374,7 +412,8 @@ export class MemoryGateServer {
|
|
|
374
412
|
* One look at every process. Each grant finds the agent process it belongs to
|
|
375
413
|
* (the nearest `claude` or `codex` above the hook), and is measured as the
|
|
376
414
|
* new processes under that agent since it was granted. Two grants on one
|
|
377
|
-
* agent at once share what they find
|
|
415
|
+
* agent at once share what they find, so neither claims that figure; each
|
|
416
|
+
* still teaches the history how long it took (`_post`).
|
|
378
417
|
*/
|
|
379
418
|
_sample(procs) {
|
|
380
419
|
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
@@ -387,9 +426,23 @@ export class MemoryGateServer {
|
|
|
387
426
|
const now = this.clock();
|
|
388
427
|
for (const grant of this.grants.values()) {
|
|
389
428
|
if (!this.gate.running.has(grant.id)) { this.grants.delete(grant.id); continue; }
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
|
|
429
|
+
// THE BACKSTOPS THAT DO NOT DEPEND ON WHAT IS RUNNING (w-5601e99977).
|
|
430
|
+
// Both of the other releases need a reading of the grant's own
|
|
431
|
+
// processes, and a Codex grant never gets one: its anchor is the shared
|
|
432
|
+
// app-server, where a sibling thread's work keeps the reading above zero
|
|
433
|
+
// for good. So a grant that was ever measured together with another
|
|
434
|
+
// (`ambiguous`, which nothing clears) is let go after `sharedGrantMs`,
|
|
435
|
+
// and no grant at all outlives `maxGrantMs`. This used to be checked
|
|
436
|
+
// only while no anchor had been found, so a Codex command whose "after"
|
|
437
|
+
// report was lost held one of two heavy slots until the app-server was
|
|
438
|
+
// replaced.
|
|
439
|
+
//
|
|
440
|
+
// LETTING A SLOT GO IS NOT LETTING GO OF THE MACHINE. Nothing is
|
|
441
|
+
// stopped, and if the command really is still running and really is big,
|
|
442
|
+
// the kernel's own pressure reading holds the next heavy command back
|
|
443
|
+
// anyway.
|
|
444
|
+
const held = now - grant.startedAt;
|
|
445
|
+
if (held >= this.maxGrantMs || (grant.ambiguous && held >= this.sharedGrantMs)) {
|
|
393
446
|
this.grants.delete(grant.id);
|
|
394
447
|
this.gate.finish(grant.id);
|
|
395
448
|
continue;
|
|
@@ -427,7 +480,10 @@ export class MemoryGateServer {
|
|
|
427
480
|
stack.push(...(children.get(p.pid) ?? []));
|
|
428
481
|
}
|
|
429
482
|
for (const g of grants) {
|
|
430
|
-
if (grants.length > 1)
|
|
483
|
+
if (grants.length > 1) {
|
|
484
|
+
g.ambiguous = true;
|
|
485
|
+
this.gate.share(g.id, anchorPid);
|
|
486
|
+
}
|
|
431
487
|
this.gate.observe(g.id, Math.round(mb));
|
|
432
488
|
// NOTHING RUNNING UNDER IT FOR A WHILE: its "after" report was lost
|
|
433
489
|
// (the hook was killed, the agent crashed mid-call). Every running
|
package/main/memory-gate.mjs
CHANGED
|
@@ -33,6 +33,15 @@
|
|
|
33
33
|
// trips its own time limits, and stays frozen forever if the app dies.
|
|
34
34
|
// - No command waits forever. After `maxWaitMs` the agent is told no, with a
|
|
35
35
|
// reason it can act on, and the hook's own time limit sits above that.
|
|
36
|
+
// - AN URGENT ROW DOES NOT QUEUE (w-713aba0c89). Her only Urgent row on
|
|
37
|
+
// 2026-10-07 was first in line for 34 minutes while no slot came free, so
|
|
38
|
+
// first in line bought nothing. Its commands now run whatever the slots and
|
|
39
|
+
// whatever "tight" says, and wait only while memory is critical. They still
|
|
40
|
+
// take a slot while they run, so everything else waits behind them.
|
|
41
|
+
// - COMMANDS MEASURED AS ONE FIGURE HOLD ONE SLOT (w-713aba0c89). Every Codex
|
|
42
|
+
// thread runs under one app-server, so two Codex commands at once read as
|
|
43
|
+
// the same number, and a `cat` beside a test run used to hold a slot of its
|
|
44
|
+
// own off the test run's gigabyte. A command known heavy still holds its own.
|
|
36
45
|
|
|
37
46
|
import { CommandHistory, HEAVY_MB } from './memory-gate-history.mjs';
|
|
38
47
|
import { NAME } from '../shared/product-name.mjs';
|
|
@@ -66,11 +75,23 @@ export const DEFAULTS = {
|
|
|
66
75
|
agingMs: 5 * 60_000,
|
|
67
76
|
};
|
|
68
77
|
|
|
69
|
-
|
|
78
|
+
/**
|
|
79
|
+
* IT SAYS WHICH OF THE TWO REASONS IT WAS (w-8386b3fd47). This sentence is the
|
|
80
|
+
* only account an agent ever gets of why its command did not run, and it used to
|
|
81
|
+
* blame memory either way. Two sessions were refused ordinary file reads while
|
|
82
|
+
* the coordinator itself reported normal pressure and 42% free, read the
|
|
83
|
+
* sentence, believed the machine was out of memory and spent their turns looking
|
|
84
|
+
* for a fault that was not there. A queue behind heavier work and a computer that
|
|
85
|
+
* is genuinely short are different things to be told.
|
|
86
|
+
*/
|
|
87
|
+
export function refusal(waitedMs, pressure = 'normal') {
|
|
70
88
|
const minutes = Math.max(1, Math.round(waitedMs / 60_000));
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
const held = `${NAME} held this command for ${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
90
|
+
const why = pressure === 'normal'
|
|
91
|
+
? `${held} behind heavier work that has not finished, and it still cannot start it.`
|
|
92
|
+
: `This computer is short of memory, so ${held} while heavier work ran, and it still cannot start it.`;
|
|
93
|
+
return `${why} Do not retry it straight away. Carry on with anything that does not need it, `
|
|
94
|
+
+ 'or end your turn and say what you are waiting for.';
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
export class MemoryGate {
|
|
@@ -109,6 +130,8 @@ export class MemoryGate {
|
|
|
109
130
|
product: req.product ?? null,
|
|
110
131
|
command: String(req.command ?? ''),
|
|
111
132
|
score: Number.isFinite(Number(req.score)) ? Number(req.score) : 0,
|
|
133
|
+
urgent: req.urgent === true,
|
|
134
|
+
session: req.session ?? null,
|
|
112
135
|
background: !!req.background,
|
|
113
136
|
cls: this.history.classify(req.command),
|
|
114
137
|
arrived: this.clock(),
|
|
@@ -133,6 +156,17 @@ export class MemoryGate {
|
|
|
133
156
|
r.peakMb = Math.max(r.peakMb ?? 0, mb);
|
|
134
157
|
}
|
|
135
158
|
|
|
159
|
+
/**
|
|
160
|
+
* This running command's memory reading is shared with others on the same
|
|
161
|
+
* agent process (`group`), so it is not its own figure. Nothing clears it.
|
|
162
|
+
*/
|
|
163
|
+
share(id, group) {
|
|
164
|
+
const r = this.running.get(String(id));
|
|
165
|
+
if (!r || r.shared === group) return;
|
|
166
|
+
r.shared = group;
|
|
167
|
+
this._pump();
|
|
168
|
+
}
|
|
169
|
+
|
|
136
170
|
tick() { this._pump(); }
|
|
137
171
|
|
|
138
172
|
snapshot() {
|
|
@@ -172,18 +206,25 @@ export class MemoryGate {
|
|
|
172
206
|
for (const w of [...this.waiting.values()]) {
|
|
173
207
|
if (now - w.arrived >= this.maxWaitMs) {
|
|
174
208
|
this.waiting.delete(w.id);
|
|
175
|
-
for (const r of w.replies) r({ allow: false, reason: refusal(now - w.arrived) });
|
|
209
|
+
for (const r of w.replies) r({ allow: false, reason: refusal(now - w.arrived, this.pressure) });
|
|
176
210
|
}
|
|
177
211
|
}
|
|
178
212
|
if (this.pressure === 'critical') return;
|
|
179
213
|
let slotsUsed = 0;
|
|
180
214
|
let lightUsed = 0;
|
|
215
|
+
const sharedGroups = new Set();
|
|
181
216
|
for (const r of this.running.values()) {
|
|
182
|
-
if (this._holdsSlot(r, now))
|
|
183
|
-
|
|
217
|
+
if (this._holdsSlot(r, now)) {
|
|
218
|
+
if (r.shared != null && r.cls !== 'heavy') {
|
|
219
|
+
if (sharedGroups.has(r.shared)) continue;
|
|
220
|
+
sharedGroups.add(r.shared);
|
|
221
|
+
}
|
|
222
|
+
slotsUsed++;
|
|
223
|
+
} else if (r.cls === 'light') lightUsed++;
|
|
184
224
|
}
|
|
185
225
|
let blocked = false;
|
|
186
226
|
for (const w of this._ordered(now)) {
|
|
227
|
+
if (w.urgent) { this._admit(w, now); slotsUsed++; continue; }
|
|
187
228
|
if (w.cls === 'light') {
|
|
188
229
|
if (lightUsed < this.lightLane) { this._admit(w, now); lightUsed++; }
|
|
189
230
|
continue;
|
|
@@ -199,7 +240,7 @@ export class MemoryGate {
|
|
|
199
240
|
this.waiting.delete(w.id);
|
|
200
241
|
this.running.set(w.id, {
|
|
201
242
|
id: w.id, item: w.item, product: w.product, command: w.command, cls: w.cls,
|
|
202
|
-
background: w.background, startedAt: now, peakMb: null,
|
|
243
|
+
urgent: w.urgent, session: w.session, background: w.background, startedAt: now, peakMb: null, shared: null,
|
|
203
244
|
});
|
|
204
245
|
for (const r of w.replies) r({ allow: true, cls: w.cls });
|
|
205
246
|
}
|