agentbox-app 0.1.12 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +311 -260
- package/main/agent-updates.mjs +9 -22
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +42 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +57 -7
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +64 -16
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +94 -59
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-bin.mjs +51 -249
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +246 -506
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +112 -11
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +83 -5
- package/main/codex.mjs +95 -363
- package/main/config.mjs +37 -5
- package/main/corner-tag.mjs +810 -0
- package/main/crash-report.mjs +26 -3
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +545 -0
- package/main/engine-setup.mjs +25 -24
- package/main/feedback.mjs +14 -31
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +105 -0
- package/main/harnesses/codex.mjs +72 -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 +96 -0
- package/main/ipc.mjs +520 -135
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/local-app-frames.mjs +82 -0
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +248 -37
- package/main/memory-gate-history.mjs +85 -20
- package/main/memory-gate-server.mjs +129 -11
- package/main/memory-gate.mjs +62 -8
- package/main/memory-log.mjs +124 -0
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +64 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +571 -0
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +314 -0
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +18 -47
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/runs-on.mjs +32 -35
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +155 -9
- package/main/settings.mjs +364 -50
- package/main/shell-path.mjs +234 -0
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +134 -20
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +45 -11
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/start-hold.mjs +73 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +227 -53
- package/main/supervisor.mjs +3865 -699
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +897 -105
- package/main/task-terminals.mjs +147 -41
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/updater.mjs +71 -9
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +365 -0
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +30 -5
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
- package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
- package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.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 +30 -8
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +62 -34
- package/shared/agents.d.mts +1 -0
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +45 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +36 -0
- package/shared/corner-tag.mjs +396 -0
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +76 -300
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +111 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +31 -0
- package/shared/notify-rules.mjs +55 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +4 -1
- package/shared/rank.mjs +82 -19
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -13
- package/shared/runs-on.mjs +31 -52
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +24 -10
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +484 -30
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/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/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +12 -0
- package/shared/usage.mjs +56 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -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 +150 -29
- package/shared/work-lines.mjs +93 -245
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
|
@@ -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,9 +157,40 @@ 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
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Light, however it was learned: `l` measured on its own, `c` only as part of
|
|
182
|
+
* a chain. Read the same everywhere, so `c` changes no verdict; it is there to
|
|
183
|
+
* be counted. Pinned by
|
|
184
|
+
* tests/the-record-says-whether-it-learned-light-from-a-chain.test.mjs.
|
|
185
|
+
*/
|
|
186
|
+
const isLight = (o) => o === 'l' || o === 'c';
|
|
187
|
+
|
|
155
188
|
/**
|
|
156
189
|
* The learned record. Each key and each program keeps its last ten
|
|
157
|
-
* observations as letters: l light,
|
|
190
|
+
* observations as letters: l light, c light but learned only inside a chain,
|
|
191
|
+
* m in between, h heavy, ? not measured. A build that predates `c` drops it on
|
|
192
|
+
* load, which costs those runs as evidence and holds a little more than it
|
|
193
|
+
* needs to; it cannot call anything light that is not.
|
|
158
194
|
* Both maps are kept in least-recently-seen-first order so the oldest falls off
|
|
159
195
|
* first when either passes `maxKeys`.
|
|
160
196
|
*/
|
|
@@ -167,27 +203,50 @@ export class CommandHistory {
|
|
|
167
203
|
this.programs = new Map();
|
|
168
204
|
}
|
|
169
205
|
|
|
170
|
-
/**
|
|
171
|
-
|
|
206
|
+
/**
|
|
207
|
+
* What one finished run tells us. `peakMb` null means it was never sampled.
|
|
208
|
+
* Answers whether anything was filed.
|
|
209
|
+
*
|
|
210
|
+
* `onlyIfLight` is for a run whose memory reading belonged to several
|
|
211
|
+
* commands at once: one `codex app-server` serves every Codex thread of a
|
|
212
|
+
* login, so two Codex commands running together are measured together
|
|
213
|
+
* (w-5601e99977). How long each took is still its own, so a quick one is
|
|
214
|
+
* light on that alone — and anything else from such a run is dropped rather
|
|
215
|
+
* than filed as "not measured", because that would un-learn a command this
|
|
216
|
+
* app has seen light before, and missing evidence must never move a command
|
|
217
|
+
* either way.
|
|
218
|
+
*/
|
|
219
|
+
record(command, { peakMb = null, durationMs = null, background = false, onlyIfLight = false } = {}) {
|
|
172
220
|
const { keys, opaque } = commandSegments(command);
|
|
173
|
-
if (!keys.length) return;
|
|
221
|
+
if (!keys.length) return false;
|
|
174
222
|
let obs;
|
|
175
223
|
if (typeof peakMb === 'number' && Number.isFinite(peakMb)) {
|
|
176
224
|
obs = peakMb >= this.heavyMb ? 'h' : peakMb < this.lightMb ? 'l' : 'm';
|
|
177
225
|
} else {
|
|
178
226
|
obs = !background && typeof durationMs === 'number' && durationMs < QUICK_MS ? 'l' : '?';
|
|
179
227
|
}
|
|
228
|
+
if (onlyIfLight && obs !== 'l') return false;
|
|
180
229
|
// A chain's memory belongs to the chain: `git status && npm test` measured
|
|
181
230
|
// heavy says nothing bad about `git status`. Only a light chain vouches for
|
|
182
231
|
// each of its parts.
|
|
183
232
|
if (keys.length > 1 || opaque) {
|
|
184
233
|
this._push(this.keys, chainKey(keys, opaque), obs);
|
|
185
|
-
if (obs !== 'l') return;
|
|
234
|
+
if (obs !== 'l') return true;
|
|
235
|
+
// And only as a chain: the early return above means these records only
|
|
236
|
+
// ever hear about the chains that came out light, so they drift light.
|
|
237
|
+
// `c` marks that weaker evidence so it can be counted. It is read as `l`
|
|
238
|
+
// everywhere, so nothing is classified differently yet.
|
|
239
|
+
for (const key of keys) {
|
|
240
|
+
this._push(this.keys, key, 'c');
|
|
241
|
+
this._push(this.programs, programOf(key), 'c');
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
186
244
|
}
|
|
187
245
|
for (const key of keys) {
|
|
188
246
|
this._push(this.keys, key, obs);
|
|
189
247
|
this._push(this.programs, programOf(key), obs);
|
|
190
248
|
}
|
|
249
|
+
return true;
|
|
191
250
|
}
|
|
192
251
|
|
|
193
252
|
/** 'light' | 'unknown' | 'heavy' for a command about to run. */
|
|
@@ -199,23 +258,29 @@ export class CommandHistory {
|
|
|
199
258
|
if (v === 'heavy' || verdict === 'heavy') verdict = 'heavy';
|
|
200
259
|
else if (v === 'unknown') verdict = 'unknown';
|
|
201
260
|
};
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
261
|
+
// A command's own record outranks the records of its parts: it is the only
|
|
262
|
+
// reading that covered everything the command starts, and the parts drift
|
|
263
|
+
// light (see `record`). The key is coarser than the command, so an old
|
|
264
|
+
// heavy run also holds back cheap variants that share it; that is the
|
|
265
|
+
// price. A middling run only keeps the command out of the light lane, where
|
|
266
|
+
// `lightLane` of them would start together, and `_holdsSlot` hands the slot
|
|
267
|
+
// back once a run measures small.
|
|
268
|
+
const own = measured(this.keys.get(keys.length > 1 || opaque ? chainKey(keys, opaque) : keys[0]));
|
|
269
|
+
if (own.includes('h')) return 'heavy';
|
|
270
|
+
if (own.includes('m')) worse('unknown');
|
|
271
|
+
if (opaque) worse('unknown');
|
|
207
272
|
for (const key of keys) worse(this._classifyKey(key));
|
|
208
273
|
return verdict;
|
|
209
274
|
}
|
|
210
275
|
|
|
211
276
|
_classifyKey(key) {
|
|
212
277
|
const own = this.keys.get(key);
|
|
213
|
-
const recent = own?.slice(-5)
|
|
278
|
+
const recent = measured(own?.slice(-5));
|
|
214
279
|
if (recent.includes('h')) return 'heavy';
|
|
215
|
-
if (
|
|
216
|
-
if (recent.some((o) => o
|
|
217
|
-
const prog = this.programs.get(programOf(key));
|
|
218
|
-
if (prog
|
|
280
|
+
if (recent.length >= 2 && recent.every(isLight)) return 'light';
|
|
281
|
+
if (recent.some((o) => !isLight(o))) return 'unknown';
|
|
282
|
+
const prog = measured(this.programs.get(programOf(key))?.slice(-10));
|
|
283
|
+
if (prog.length >= 5 && prog.every(isLight)) return 'light';
|
|
219
284
|
return 'unknown';
|
|
220
285
|
}
|
|
221
286
|
|
|
@@ -239,7 +304,7 @@ export class CommandHistory {
|
|
|
239
304
|
if (!Array.isArray(entries)) return;
|
|
240
305
|
for (const e of entries) {
|
|
241
306
|
if (Array.isArray(e) && typeof e[0] === 'string' && Array.isArray(e[1])) {
|
|
242
|
-
map.set(e[0], e[1].filter((o) => ['l', 'm', 'h', '?'].includes(o)).slice(-10));
|
|
307
|
+
map.set(e[0], e[1].filter((o) => ['l', 'c', 'm', 'h', '?'].includes(o)).slice(-10));
|
|
243
308
|
}
|
|
244
309
|
}
|
|
245
310
|
};
|
|
@@ -95,6 +95,11 @@ export class MemoryGateServer {
|
|
|
95
95
|
socketPath = defaultSocketPath(),
|
|
96
96
|
historyFile = null,
|
|
97
97
|
history = null,
|
|
98
|
+
// One appended line per grant, so what the slots cost can be read later.
|
|
99
|
+
// Without it the only duration anywhere is a transcript's, which is waiting
|
|
100
|
+
// and running added together and cannot be separated.
|
|
101
|
+
grantLogFile = null,
|
|
102
|
+
grantLogMaxBytes = 4 * 1024 * 1024,
|
|
98
103
|
gate = {},
|
|
99
104
|
scoreFor = null,
|
|
100
105
|
// WHOSE CODEX THREAD IS THIS (w-e5225b62ba). A Claude worker carries its row
|
|
@@ -104,6 +109,10 @@ export class MemoryGateServer {
|
|
|
104
109
|
// the app-server's own `thread.id`. Answering null means "not this app's",
|
|
105
110
|
// and such a command is let straight through.
|
|
106
111
|
ownerOf = null,
|
|
112
|
+
// IS THIS ROW HER URGENT ONE (w-713aba0c89), asked live on every command so
|
|
113
|
+
// a row raised to Urgent counts at once. Only the app answers it: nothing in
|
|
114
|
+
// a worker's own request can make its command urgent.
|
|
115
|
+
urgentFor = null,
|
|
107
116
|
readPressure = readMacPressure,
|
|
108
117
|
sampleProcesses = listProcesses,
|
|
109
118
|
pollMs = 2_000,
|
|
@@ -113,18 +122,28 @@ export class MemoryGateServer {
|
|
|
113
122
|
lockPort = DEFAULT_LOCK_PORT,
|
|
114
123
|
idleReleaseMs = 30_000,
|
|
115
124
|
maxGrantMs = 3 * 60 * 60_000,
|
|
125
|
+
// How long a grant may hold a slot when its end can never be observed,
|
|
126
|
+
// because its memory reading is shared with another command on the same
|
|
127
|
+
// agent process. Null means the longest the gate will make anything wait:
|
|
128
|
+
// a grant nobody can see the end of must not outlive the wait it causes,
|
|
129
|
+
// or a queue is refused one by one while a phantom holds the slot.
|
|
130
|
+
sharedGrantMs = null,
|
|
116
131
|
clock = Date.now,
|
|
117
132
|
log = () => {},
|
|
118
133
|
} = {}) {
|
|
119
134
|
this.socketPath = socketPath;
|
|
120
135
|
this.historyFile = historyFile;
|
|
136
|
+
this.grantLogFile = grantLogFile;
|
|
137
|
+
this.grantLogMaxBytes = grantLogMaxBytes;
|
|
121
138
|
this.history = history ?? loadHistory(historyFile);
|
|
122
139
|
this.clock = clock;
|
|
123
140
|
this.idleReleaseMs = idleReleaseMs;
|
|
124
141
|
this.maxGrantMs = maxGrantMs;
|
|
125
142
|
this.gate = new MemoryGate({ ...gate, history: this.history, clock });
|
|
143
|
+
this.sharedGrantMs = Number.isFinite(sharedGrantMs) ? sharedGrantMs : this.gate.maxWaitMs;
|
|
126
144
|
this.scoreFor = scoreFor;
|
|
127
145
|
this.ownerOf = ownerOf;
|
|
146
|
+
this.urgentFor = urgentFor;
|
|
128
147
|
this.readPressure = readPressure;
|
|
129
148
|
this.sampleProcesses = sampleProcesses;
|
|
130
149
|
this.pollMs = pollMs;
|
|
@@ -292,7 +311,24 @@ export class MemoryGateServer {
|
|
|
292
311
|
let owner;
|
|
293
312
|
try { owner = this.ownerOf(session); } catch { return null; }
|
|
294
313
|
if (!owner?.item) return null;
|
|
295
|
-
return { item: String(owner.item), product: owner.product ?? null };
|
|
314
|
+
return { item: String(owner.item), product: owner.product ?? null, session };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A CODEX THREAD'S TURN IS OVER: LET GO OF EVERY GRANT IT STILL HOLDS
|
|
319
|
+
* (w-713aba0c89). Codex sends no "after" report for a command that failed, so
|
|
320
|
+
* such a grant outlives its command, and on a shared app-server nothing else
|
|
321
|
+
* ever sees it end. A turn that has ended is waiting on none of its commands.
|
|
322
|
+
* Only grants that named this thread are touched: a Claude worker's names its
|
|
323
|
+
* row, and goes when its own process does.
|
|
324
|
+
*/
|
|
325
|
+
releaseSession(session) {
|
|
326
|
+
if (typeof session !== 'string' || !session) return;
|
|
327
|
+
for (const r of [...this.gate.running.values()]) {
|
|
328
|
+
if (r.session !== session) continue;
|
|
329
|
+
this.grants.delete(r.id);
|
|
330
|
+
this.gate.finish(r.id);
|
|
331
|
+
}
|
|
296
332
|
}
|
|
297
333
|
|
|
298
334
|
_pre(body, req, res) {
|
|
@@ -308,10 +344,12 @@ export class MemoryGateServer {
|
|
|
308
344
|
const id = String(hook.tool_use_id || `${body.ppid}-${Date.now()}-${Math.random()}`);
|
|
309
345
|
const live = typeof this.scoreFor === 'function' ? this.scoreFor(whose.product, whose.item) : null;
|
|
310
346
|
const score = Number.isFinite(live) ? live : Number(body.score) || 0;
|
|
347
|
+
let urgent = false;
|
|
348
|
+
try { urgent = typeof this.urgentFor === 'function' && this.urgentFor(whose.product, whose.item) === true; } catch { urgent = false; }
|
|
311
349
|
this.counts.asked++;
|
|
312
350
|
let answered = false;
|
|
313
351
|
this.gate.submit({
|
|
314
|
-
id, command, score, item: whose.item, product: whose.product,
|
|
352
|
+
id, command, score, urgent, item: whose.item, product: whose.product, session: whose.session ?? null,
|
|
315
353
|
background: !!hook?.tool_input?.run_in_background,
|
|
316
354
|
}, (decision) => {
|
|
317
355
|
answered = true;
|
|
@@ -324,6 +362,10 @@ export class MemoryGateServer {
|
|
|
324
362
|
res.end('');
|
|
325
363
|
} else {
|
|
326
364
|
this.counts.refused++;
|
|
365
|
+
this._writeGrant({
|
|
366
|
+
refused: true, cls: decision.cls, waitMs: decision.waitedMs, ranMs: null, peakMb: null,
|
|
367
|
+
pressure: decision.pressure, command,
|
|
368
|
+
});
|
|
327
369
|
res.end(denyOutput(decision.reason));
|
|
328
370
|
}
|
|
329
371
|
});
|
|
@@ -341,13 +383,30 @@ export class MemoryGateServer {
|
|
|
341
383
|
this.grants.delete(id);
|
|
342
384
|
const was = this.gate.finish(id);
|
|
343
385
|
if (!was) return;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
386
|
+
this._writeGrant({
|
|
387
|
+
refused: false,
|
|
388
|
+
cls: was.cls,
|
|
389
|
+
waitMs: was.waitedMs ?? 0,
|
|
390
|
+
ranMs: this.clock() - was.startedAt,
|
|
391
|
+
peakMb: grant?.ambiguous ? null : was.peakMb,
|
|
392
|
+
shared: !!grant?.ambiguous,
|
|
393
|
+
pressure: was.pressureAt,
|
|
394
|
+
command: was.command,
|
|
395
|
+
});
|
|
396
|
+
// A SHARED READING IS NOT NO READING (w-5601e99977). Every Codex thread of
|
|
397
|
+
// a login runs under one `codex app-server`, so two Codex commands at once
|
|
398
|
+
// are measured together and neither may claim the figure. How long each
|
|
399
|
+
// took is its own, though, and a run that was over inside QUICK_MS counts
|
|
400
|
+
// as light on that alone, so `onlyIfLight` files that verdict and nothing
|
|
401
|
+
// else. Before this, Codex taught the history almost nothing and rode on
|
|
402
|
+
// what Claude workers taught it.
|
|
403
|
+
const filed = this.history.record(was.command, {
|
|
404
|
+
peakMb: grant?.ambiguous ? null : was.peakMb,
|
|
347
405
|
durationMs: Number(hook.duration_ms) || null,
|
|
348
406
|
background: was.background,
|
|
407
|
+
onlyIfLight: !!grant?.ambiguous,
|
|
349
408
|
});
|
|
350
|
-
this.dirty = true;
|
|
409
|
+
if (filed) this.dirty = true;
|
|
351
410
|
}
|
|
352
411
|
|
|
353
412
|
/* ------------------------------ measuring ------------------------------ */
|
|
@@ -374,7 +433,8 @@ export class MemoryGateServer {
|
|
|
374
433
|
* One look at every process. Each grant finds the agent process it belongs to
|
|
375
434
|
* (the nearest `claude` or `codex` above the hook), and is measured as the
|
|
376
435
|
* new processes under that agent since it was granted. Two grants on one
|
|
377
|
-
* agent at once share what they find
|
|
436
|
+
* agent at once share what they find, so neither claims that figure; each
|
|
437
|
+
* still teaches the history how long it took (`_post`).
|
|
378
438
|
*/
|
|
379
439
|
_sample(procs) {
|
|
380
440
|
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
@@ -387,9 +447,23 @@ export class MemoryGateServer {
|
|
|
387
447
|
const now = this.clock();
|
|
388
448
|
for (const grant of this.grants.values()) {
|
|
389
449
|
if (!this.gate.running.has(grant.id)) { this.grants.delete(grant.id); continue; }
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
|
|
450
|
+
// THE BACKSTOPS THAT DO NOT DEPEND ON WHAT IS RUNNING (w-5601e99977).
|
|
451
|
+
// Both of the other releases need a reading of the grant's own
|
|
452
|
+
// processes, and a Codex grant never gets one: its anchor is the shared
|
|
453
|
+
// app-server, where a sibling thread's work keeps the reading above zero
|
|
454
|
+
// for good. So a grant that was ever measured together with another
|
|
455
|
+
// (`ambiguous`, which nothing clears) is let go after `sharedGrantMs`,
|
|
456
|
+
// and no grant at all outlives `maxGrantMs`. This used to be checked
|
|
457
|
+
// only while no anchor had been found, so a Codex command whose "after"
|
|
458
|
+
// report was lost held one of two heavy slots until the app-server was
|
|
459
|
+
// replaced.
|
|
460
|
+
//
|
|
461
|
+
// LETTING A SLOT GO IS NOT LETTING GO OF THE MACHINE. Nothing is
|
|
462
|
+
// stopped, and if the command really is still running and really is big,
|
|
463
|
+
// the kernel's own pressure reading holds the next heavy command back
|
|
464
|
+
// anyway.
|
|
465
|
+
const held = now - grant.startedAt;
|
|
466
|
+
if (held >= this.maxGrantMs || (grant.ambiguous && held >= this.sharedGrantMs)) {
|
|
393
467
|
this.grants.delete(grant.id);
|
|
394
468
|
this.gate.finish(grant.id);
|
|
395
469
|
continue;
|
|
@@ -427,7 +501,10 @@ export class MemoryGateServer {
|
|
|
427
501
|
stack.push(...(children.get(p.pid) ?? []));
|
|
428
502
|
}
|
|
429
503
|
for (const g of grants) {
|
|
430
|
-
if (grants.length > 1)
|
|
504
|
+
if (grants.length > 1) {
|
|
505
|
+
g.ambiguous = true;
|
|
506
|
+
this.gate.share(g.id, anchorPid);
|
|
507
|
+
}
|
|
431
508
|
this.gate.observe(g.id, Math.round(mb));
|
|
432
509
|
// NOTHING RUNNING UNDER IT FOR A WHILE: its "after" report was lost
|
|
433
510
|
// (the hook was killed, the agent crashed mid-call). Every running
|
|
@@ -444,6 +521,47 @@ export class MemoryGateServer {
|
|
|
444
521
|
this.lastPids = new Set(procs.map((p) => p.pid));
|
|
445
522
|
}
|
|
446
523
|
|
|
524
|
+
/**
|
|
525
|
+
* One line per grant: what it was, how long it queued, how long it ran and
|
|
526
|
+
* its measured peak. `shared` says the peak is null because two commands on
|
|
527
|
+
* one agent process were measured together, without which a week of this
|
|
528
|
+
* reads as two commands where there was one.
|
|
529
|
+
*
|
|
530
|
+
* Every shell command on the Mac waits on this path, so a write that fails
|
|
531
|
+
* is logged and swallowed rather than returned to the agent.
|
|
532
|
+
*/
|
|
533
|
+
_writeGrant({ refused, cls, waitMs, ranMs, peakMb, shared = false, pressure = null, command }) {
|
|
534
|
+
if (!this.grantLogFile) return;
|
|
535
|
+
try {
|
|
536
|
+
if (this.grantBytes === undefined) {
|
|
537
|
+
try { this.grantBytes = fs.statSync(this.grantLogFile).size; } catch { this.grantBytes = 0; }
|
|
538
|
+
}
|
|
539
|
+
// Bounded, and the one before it is kept, so a week is still readable
|
|
540
|
+
// across the two even when a busy day fills one.
|
|
541
|
+
if (this.grantBytes >= this.grantLogMaxBytes) {
|
|
542
|
+
fs.renameSync(this.grantLogFile, `${this.grantLogFile}.1`);
|
|
543
|
+
this.grantBytes = 0;
|
|
544
|
+
}
|
|
545
|
+
const line = `${JSON.stringify({
|
|
546
|
+
t: this.clock(),
|
|
547
|
+
cls: cls ?? null,
|
|
548
|
+
waitMs: Number.isFinite(waitMs) ? waitMs : null,
|
|
549
|
+
ranMs: Number.isFinite(ranMs) ? ranMs : null,
|
|
550
|
+
peakMb: Number.isFinite(peakMb) ? peakMb : null,
|
|
551
|
+
// Only a wait that happened while this was `normal` is evidence about
|
|
552
|
+
// the slot count; under `tight` nothing heavy starts whatever it is.
|
|
553
|
+
pressure: pressure ?? null,
|
|
554
|
+
...(shared ? { shared: true } : {}),
|
|
555
|
+
...(refused ? { refused: true } : {}),
|
|
556
|
+
cmd: String(command ?? '').slice(0, 300),
|
|
557
|
+
})}\n`;
|
|
558
|
+
fs.appendFileSync(this.grantLogFile, line);
|
|
559
|
+
this.grantBytes += Buffer.byteLength(line);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
this.log(`memory gate: could not write the grant log: ${err.message}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
447
565
|
_saveHistory() {
|
|
448
566
|
if (!this.historyFile || !this.dirty) return;
|
|
449
567
|
try {
|
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,33 @@ 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)
|
|
209
|
+
for (const r of w.replies) {
|
|
210
|
+
r({
|
|
211
|
+
reason: refusal(now - w.arrived, this.pressure),
|
|
212
|
+
allow: false,
|
|
213
|
+
cls: w.cls,
|
|
214
|
+
waitedMs: now - w.arrived,
|
|
215
|
+
pressure: this.pressure,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
176
218
|
}
|
|
177
219
|
}
|
|
178
220
|
if (this.pressure === 'critical') return;
|
|
179
221
|
let slotsUsed = 0;
|
|
180
222
|
let lightUsed = 0;
|
|
223
|
+
const sharedGroups = new Set();
|
|
181
224
|
for (const r of this.running.values()) {
|
|
182
|
-
if (this._holdsSlot(r, now))
|
|
183
|
-
|
|
225
|
+
if (this._holdsSlot(r, now)) {
|
|
226
|
+
if (r.shared != null && r.cls !== 'heavy') {
|
|
227
|
+
if (sharedGroups.has(r.shared)) continue;
|
|
228
|
+
sharedGroups.add(r.shared);
|
|
229
|
+
}
|
|
230
|
+
slotsUsed++;
|
|
231
|
+
} else if (r.cls === 'light') lightUsed++;
|
|
184
232
|
}
|
|
185
233
|
let blocked = false;
|
|
186
234
|
for (const w of this._ordered(now)) {
|
|
235
|
+
if (w.urgent) { this._admit(w, now); slotsUsed++; continue; }
|
|
187
236
|
if (w.cls === 'light') {
|
|
188
237
|
if (lightUsed < this.lightLane) { this._admit(w, now); lightUsed++; }
|
|
189
238
|
continue;
|
|
@@ -199,7 +248,12 @@ export class MemoryGate {
|
|
|
199
248
|
this.waiting.delete(w.id);
|
|
200
249
|
this.running.set(w.id, {
|
|
201
250
|
id: w.id, item: w.item, product: w.product, command: w.command, cls: w.cls,
|
|
202
|
-
background: w.background, startedAt: now, peakMb: null,
|
|
251
|
+
urgent: w.urgent, session: w.session, background: w.background, startedAt: now, peakMb: null, shared: null,
|
|
252
|
+
// Only the coordinator reads these, to write down what the grant cost.
|
|
253
|
+
// The pressure is what makes the wait mean anything: under `normal` only
|
|
254
|
+
// full slots can hold a command back, so the wait is evidence about
|
|
255
|
+
// `slots`; under `tight` nothing heavy starts whatever the count is.
|
|
256
|
+
waitedMs: now - w.arrived, pressureAt: this.pressure,
|
|
203
257
|
});
|
|
204
258
|
for (const r of w.replies) r({ allow: true, cls: w.cls });
|
|
205
259
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// A LINE A MINUTE: HOW SHORT MEMORY IS, AND WHICH TASK IS HOLDING IT (w-ec27ff1a51).
|
|
2
|
+
//
|
|
3
|
+
// 2026-10-11 the Mac ran out of memory with Agentbox at 22 GB. By the time
|
|
4
|
+
// anyone looked it had calmed down, and nothing had been written down, so which
|
|
5
|
+
// task had been holding the memory could only be guessed. This writes one JSON
|
|
6
|
+
// line a minute to `memory-<day>.jsonl` in the app's own folder, kept a week:
|
|
7
|
+
// the kernel's reading, the agents running and the limit, whether new agents
|
|
8
|
+
// were being held (main/start-hold.mjs), the app's own processes, every task's
|
|
9
|
+
// programs with what they hold, and the biggest programs that are not ours.
|
|
10
|
+
//
|
|
11
|
+
// It stays on this computer. Nothing here is sent anywhere.
|
|
12
|
+
//
|
|
13
|
+
// A program belongs to the task of the nearest process at or above it that
|
|
14
|
+
// carries ZERO_ITEM (main/leftovers.mjs reads the tag), which is what catches a
|
|
15
|
+
// dev server that renamed itself and hid its environment.
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { listProcesses } from './leftovers.mjs';
|
|
20
|
+
import { readMacPressure } from './memory-gate-server.mjs';
|
|
21
|
+
import { pressureFrom } from './memory-gate.mjs';
|
|
22
|
+
|
|
23
|
+
/** Days of files kept, today included. */
|
|
24
|
+
export const KEEP_DAYS = 7;
|
|
25
|
+
const FILE = /^memory-(\d{4}-\d{2}-\d{2})\.jsonl$/;
|
|
26
|
+
|
|
27
|
+
const name = (exe) => String(exe ?? '').split('/').filter(Boolean).pop() || String(exe ?? '');
|
|
28
|
+
const top = (list, n) => [...list].sort((a, b) => b.mb - a.mb).slice(0, n).map((p) => ({ name: name(p.exe), mb: Math.round(p.mb) }));
|
|
29
|
+
|
|
30
|
+
/** One minute's line, from one listing. Pure. */
|
|
31
|
+
export function summarizeMemory(procs, { appPid = process.pid, at = Date.now(), reading = {} } = {}) {
|
|
32
|
+
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
33
|
+
const owner = new Map();
|
|
34
|
+
const ownerOf = (p) => {
|
|
35
|
+
if (owner.has(p.pid)) return owner.get(p.pid);
|
|
36
|
+
let found = null;
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (let q = p; q && !seen.has(q.pid); q = byPid.get(q.ppid)) {
|
|
39
|
+
seen.add(q.pid);
|
|
40
|
+
if (q.item) { found = { item: q.item, product: q.product ?? null }; break; }
|
|
41
|
+
if (q.pid === appPid) { found = 'app'; break; }
|
|
42
|
+
}
|
|
43
|
+
owner.set(p.pid, found);
|
|
44
|
+
return found;
|
|
45
|
+
};
|
|
46
|
+
const tasks = new Map();
|
|
47
|
+
const app = [];
|
|
48
|
+
const others = [];
|
|
49
|
+
for (const p of procs) {
|
|
50
|
+
const o = ownerOf(p);
|
|
51
|
+
if (o === 'app') app.push(p);
|
|
52
|
+
else if (o) {
|
|
53
|
+
const t = tasks.get(o.item) ?? { item: o.item, product: o.product, list: [] };
|
|
54
|
+
t.product ??= o.product;
|
|
55
|
+
t.list.push(p);
|
|
56
|
+
tasks.set(o.item, t);
|
|
57
|
+
} else others.push(p);
|
|
58
|
+
}
|
|
59
|
+
const sum = (list) => Math.round(list.reduce((n, p) => n + (Number(p.mb) || 0), 0));
|
|
60
|
+
const level = Number.isFinite(reading?.level) ? reading.level : null;
|
|
61
|
+
const freePct = Number.isFinite(reading?.freePct) ? reading.freePct : null;
|
|
62
|
+
return {
|
|
63
|
+
at: new Date(at).toISOString(),
|
|
64
|
+
pressure: level === null && freePct === null ? null : pressureFrom({ level, freePct }),
|
|
65
|
+
level,
|
|
66
|
+
freePct,
|
|
67
|
+
totalMb: sum(procs),
|
|
68
|
+
app: { mb: sum(app), procs: app.length },
|
|
69
|
+
tasks: [...tasks.values()]
|
|
70
|
+
.map((t) => ({ item: t.item, product: t.product, mb: sum(t.list), procs: t.list.length, top: top(t.list, 3) }))
|
|
71
|
+
.sort((a, b) => b.mb - a.mb),
|
|
72
|
+
others: top(others, 5),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const dayOf = (ms) => {
|
|
77
|
+
const d = new Date(ms);
|
|
78
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export class MemoryLog {
|
|
82
|
+
constructor({
|
|
83
|
+
dir, appPid = process.pid, list = listProcesses, readPressure = readMacPressure,
|
|
84
|
+
now = Date.now, facts = () => ({}), keepDays = KEEP_DAYS,
|
|
85
|
+
}) {
|
|
86
|
+
Object.assign(this, { dir, appPid, list, readPressure, now, facts, keepDays });
|
|
87
|
+
this._pruned = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Writes this minute's line. Never throws: a log that breaks the app is worse than a gap in the log. */
|
|
91
|
+
async tick() {
|
|
92
|
+
if (this._busy) return;
|
|
93
|
+
this._busy = true;
|
|
94
|
+
try {
|
|
95
|
+
const [procs, reading] = await Promise.all([
|
|
96
|
+
Promise.resolve().then(() => this.list()).catch(() => []),
|
|
97
|
+
Promise.resolve().then(() => this.readPressure()).catch(() => ({})),
|
|
98
|
+
]);
|
|
99
|
+
const at = this.now();
|
|
100
|
+
let facts = {};
|
|
101
|
+
try { facts = this.facts() ?? {}; } catch { /* the line goes without them */ }
|
|
102
|
+
const line = { ...summarizeMemory(procs, { appPid: this.appPid, at, reading }), ...facts };
|
|
103
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
104
|
+
fs.appendFileSync(path.join(this.dir, `memory-${dayOf(at)}.jsonl`), `${JSON.stringify(line)}\n`);
|
|
105
|
+
this._prune(at);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn('zero: memory log:', err.message);
|
|
108
|
+
} finally {
|
|
109
|
+
this._busy = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Once a day: removes this log's own files older than `keepDays`, and nothing else. */
|
|
114
|
+
_prune(at) {
|
|
115
|
+
const today = dayOf(at);
|
|
116
|
+
if (this._pruned === today) return;
|
|
117
|
+
this._pruned = today;
|
|
118
|
+
const oldest = dayOf(new Date(at).setDate(new Date(at).getDate() - (this.keepDays - 1)));
|
|
119
|
+
for (const f of fs.readdirSync(this.dir)) {
|
|
120
|
+
const m = f.match(FILE);
|
|
121
|
+
if (m && m[1] < oldest) fs.rmSync(path.join(this.dir, f), { force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/main/menu.mjs
CHANGED
|
@@ -159,8 +159,10 @@ export function installMenu({ onApproval, onReload, onZoom } = {}) {
|
|
|
159
159
|
{
|
|
160
160
|
label: 'Agents',
|
|
161
161
|
submenu: [
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// CommandOrControl like every other accelerator here: Command alone
|
|
163
|
+
// left the approval card's hint naming two dead keys on Linux.
|
|
164
|
+
{ label: 'Allow Pending Approval', accelerator: 'CommandOrControl+Y', click: () => onApproval?.(true) },
|
|
165
|
+
{ label: 'Deny Pending Approval', accelerator: 'CommandOrControl+N', click: () => onApproval?.(false) },
|
|
164
166
|
],
|
|
165
167
|
},
|
|
166
168
|
{
|