agentbox-app 0.1.12 → 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.
Files changed (214) hide show
  1. package/main/acp.mjs +296 -0
  2. package/main/agent-homes.mjs +158 -0
  3. package/main/agent-sessions.mjs +57 -20
  4. package/main/agent-updates.mjs +9 -22
  5. package/main/analytics.mjs +30 -0
  6. package/main/claude-bin.mjs +45 -311
  7. package/main/claude-input.mjs +9 -3
  8. package/main/claude-launch-env.mjs +54 -0
  9. package/main/claude-models.mjs +39 -7
  10. package/main/claude-plan.mjs +16 -16
  11. package/main/claude-usage.mjs +82 -25
  12. package/main/codex-bin.mjs +51 -249
  13. package/main/codex-session.mjs +38 -3
  14. package/main/codex-threads.mjs +41 -8
  15. package/main/codex-watch.mjs +67 -3
  16. package/main/config.mjs +26 -4
  17. package/main/corner-tag.mjs +489 -0
  18. package/main/crash-report.mjs +26 -3
  19. package/main/engine-bin.mjs +595 -0
  20. package/main/engine-setup.mjs +4 -15
  21. package/main/feedback.mjs +11 -6
  22. package/main/harnesses/claude-stream.mjs +208 -0
  23. package/main/harnesses/claude.mjs +84 -0
  24. package/main/harnesses/codex.mjs +65 -0
  25. package/main/harnesses/common.mjs +78 -0
  26. package/main/harnesses/instructions-file.mjs +116 -0
  27. package/main/harnesses/opencode.mjs +234 -0
  28. package/main/harnesses.mjs +65 -0
  29. package/main/importable-threads.mjs +38 -0
  30. package/main/ipc.mjs +165 -31
  31. package/main/local-app-frames.mjs +82 -0
  32. package/main/main.mjs +100 -4
  33. package/main/memory-gate-history.mjs +51 -12
  34. package/main/memory-gate-server.mjs +67 -11
  35. package/main/memory-gate.mjs +49 -8
  36. package/main/memory-log.mjs +124 -0
  37. package/main/notify.mjs +47 -4
  38. package/main/opencode-server.mjs +271 -0
  39. package/main/opencode.mjs +287 -0
  40. package/main/phone-link.mjs +551 -0
  41. package/main/relay-address.mjs +28 -0
  42. package/main/relay-client.mjs +246 -0
  43. package/main/row-label.mjs +6 -6
  44. package/main/runs-on.mjs +32 -35
  45. package/main/serve.mjs +113 -9
  46. package/main/settings.mjs +148 -10
  47. package/main/shell-path.mjs +86 -0
  48. package/main/ship-queue.mjs +24 -6
  49. package/main/sign-in-files.mjs +3 -7
  50. package/main/source-updater.mjs +27 -1
  51. package/main/start-hold.mjs +73 -0
  52. package/main/store.mjs +1 -1
  53. package/main/supervisor.mjs +832 -428
  54. package/main/task-commands.mjs +13 -17
  55. package/main/task-folders-offthread.mjs +6 -1
  56. package/main/task-folders.mjs +490 -98
  57. package/main/task-terminals.mjs +37 -7
  58. package/main/updater.mjs +71 -9
  59. package/main/worktree-include.mjs +494 -0
  60. package/package.json +10 -1
  61. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  62. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  63. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  64. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  65. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  66. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  67. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  68. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  69. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  70. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  71. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  72. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  73. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  74. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  75. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  76. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  77. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  78. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  79. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  80. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  81. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  82. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  83. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  84. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  85. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  86. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  87. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  88. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  89. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  90. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  91. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  92. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  93. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  94. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  95. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  96. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  97. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  98. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  99. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  101. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  102. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  103. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  104. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  105. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  106. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  107. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  108. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  109. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  111. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  112. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  113. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  114. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  115. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  116. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  117. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  118. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  119. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  120. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  121. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  122. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  123. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  124. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  125. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  126. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  127. package/renderer/dist/corner-tag.html +13 -0
  128. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  129. package/renderer/dist/emulator/data/compression/README.md +11 -0
  130. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  131. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  132. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  133. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  134. package/renderer/dist/emulator/data/cores/README.md +22 -0
  135. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  136. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  137. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  138. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  139. package/renderer/dist/emulator/data/cores/package.json +65 -0
  140. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  141. package/renderer/dist/emulator/data/emulator.css +1614 -0
  142. package/renderer/dist/emulator/data/loader.js +170 -0
  143. package/renderer/dist/emulator/data/localization/README.md +63 -0
  144. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  145. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  146. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  147. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  148. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  149. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  150. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  151. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  152. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  153. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  154. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  155. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  156. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  157. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  158. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  159. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  160. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  161. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  162. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  163. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  164. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  165. package/renderer/dist/emulator/data/src/compression.js +142 -0
  166. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  167. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  168. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  169. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  170. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  171. package/renderer/dist/emulator/data/src/storage.js +125 -0
  172. package/renderer/dist/emulator/data/version.json +5 -0
  173. package/renderer/dist/emulator/host.js +297 -0
  174. package/renderer/dist/emulator/index.html +28 -0
  175. package/renderer/dist/emulator/provenance.json +6 -0
  176. package/renderer/dist/index.html +16 -3
  177. package/scripts/approval-server.sh +24 -5
  178. package/scripts/before-publish.mjs +12 -10
  179. package/shared/agent-import.mjs +2 -0
  180. package/shared/agents.d.mts +1 -0
  181. package/shared/bridge-map.mjs +18 -0
  182. package/shared/claude-commands.generated.mjs +6 -4
  183. package/shared/claude-models.generated.mjs +6 -4
  184. package/shared/corner-tag.d.mts +14 -0
  185. package/shared/corner-tag.mjs +141 -0
  186. package/shared/engines.mjs +20 -5
  187. package/shared/harness-definitions.mjs +103 -0
  188. package/shared/notify-rules.d.mts +25 -0
  189. package/shared/notify-rules.mjs +19 -0
  190. package/shared/provider-commands.mjs +10 -2
  191. package/shared/rank.d.mts +2 -0
  192. package/shared/rank.mjs +50 -0
  193. package/shared/relay-address.mjs +6 -0
  194. package/shared/relay-crypto.mjs +165 -0
  195. package/shared/runs-on.d.mts +0 -10
  196. package/shared/runs-on.mjs +25 -49
  197. package/shared/shortcuts.mjs +10 -4
  198. package/shared/spawn-trouble.mjs +57 -6
  199. package/shared/terminal-input.d.mts +1 -0
  200. package/shared/terminal-input.mjs +30 -0
  201. package/shared/terminal-keys.d.mts +1 -0
  202. package/shared/terminal-keys.mjs +16 -0
  203. package/shared/terminal-size.d.mts +4 -0
  204. package/shared/terminal-size.mjs +29 -0
  205. package/shared/usage.d.mts +3 -0
  206. package/shared/usage.mjs +7 -0
  207. package/shared/where-it-runs-from.d.mts +43 -0
  208. package/shared/where-it-runs-from.mjs +155 -0
  209. package/shared/work-items.mjs +25 -1
  210. package/shared/work-lines.mjs +1 -1
  211. package/shared/xterm-invalid-range.d.mts +2 -0
  212. package/shared/xterm-invalid-range.mjs +58 -0
  213. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  214. 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 "seen light, more than once, and never heavy since". One
12
- // heavy run is enough to make that exact command heavy. A program that has been
13
- // light every time across many different arguments lends that to an argument
14
- // it has not seen, and stops the moment one of its runs is not light.
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
- /** What one finished run tells us. `peakMb` null means it was never sampled. */
171
- record(command, { peakMb = null, durationMs = null, background = false } = {}) {
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 (own && own.length >= 2 && recent.every((o) => o === 'l')) return 'light';
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 && prog.length >= 5 && prog.slice(-10).every((o) => o === 'l')) return 'light';
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
- if (grant?.ambiguous) return;
345
- this.history.record(was.command, {
346
- peakMb: was.peakMb,
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 and teach the history nothing.
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
- // The last resort, for a grant whose agent could never be found: no
391
- // report back and nothing to measure for three hours is over.
392
- if (!grant.anchor && now - grant.startedAt >= this.maxGrantMs) {
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) g.ambiguous = true;
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
@@ -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
- export function refusal(waitedMs) {
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
- return `This computer is short of memory, so ${NAME} held this command for ${minutes} minute${minutes === 1 ? '' : 's'} `
72
- + 'while heavier work ran, and it still cannot start it. Do not retry it straight away. Carry on with anything '
73
- + 'that does not need it, or end your turn and say you are waiting for memory.';
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)) slotsUsed++;
183
- else if (r.cls === 'light') lightUsed++;
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
  }
@@ -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/notify.mjs CHANGED
@@ -13,16 +13,46 @@
13
13
  // AND ONCE, NOT TWENTY TIMES. One at a time is not once. `spoke` below is the
14
14
  // fix, and shared/notify-rules.mjs carries the rule.
15
15
 
16
+ import path from 'node:path';
16
17
  import { atTheApp, banner, shouldSpeak } from '../shared/notify-rules.mjs';
17
18
  import { NAME } from '../shared/product-name.mjs';
18
19
 
20
+ // THE MARK THE BANNER WEARS, and why it is a png and not the .icns every other
21
+ // part of this repo uses.
22
+ //
23
+ // macOS draws the app's OWN bundle icon down the left of a banner, and no app
24
+ // can override that. From a source checkout the bundle being asked is the
25
+ // Electron.app in node_modules, which scripts/brand-electron.mjs renames and
26
+ // re-icons, and macOS still drew a generic grey square: "it shows a plain
27
+ // placeholder", looking at a real one, 2026-10-07.
28
+ //
29
+ // An image handed to the notification is the half we DO control; macOS attaches
30
+ // it to the banner. Measured the same day in the Electron the app runs from:
31
+ // nativeImage.createFromPath on build/icon.icns comes back empty, 0x0, so the
32
+ // icns is not a candidate however convenient it would be. The png beside it,
33
+ // drawn from the same art, reads 256x256.
34
+ //
35
+ // AN EMPTY IMAGE IS NOT A MARK. createFromPath answers a missing or unreadable
36
+ // file with an empty image rather than an error, so "it returned something" is
37
+ // the wrong question, and handing that something to macOS is how this goes
38
+ // quietly back to a grey square. Returns null when there is nothing to show,
39
+ // and the banner then looks exactly as it did before any of this.
40
+ export const MARK_FILE = path.join('build', 'notification-icon.png');
41
+
42
+ export function markFor({ root, createFromPath }) {
43
+ try {
44
+ const img = createFromPath(path.join(root, MARK_FILE));
45
+ return img && !img.isEmpty() ? img : null;
46
+ } catch { return null; }
47
+ }
48
+
19
49
  // A BURST IS ONE PIECE OF NEWS. Four workers finishing inside the same
20
50
  // supervisor tick is the ordinary case, and it arrives as four pushes over a
21
51
  // second or two. Waiting a beat before speaking turns that into one banner
22
52
  // with one sound instead of four rewrites and four chimes.
23
53
  const COALESCE_MS = 1500;
24
54
 
25
- export function createNotifier({ window, Notification, powerMonitor, coalesceMs = COALESCE_MS }) {
55
+ export function createNotifier({ window, Notification, powerMonitor, icon = null, coalesceMs = COALESCE_MS }) {
26
56
  // Everything she has not seen since she last had the window, keyed by id so
27
57
  // a repeated push is idempotent. Insertion order is arrival order.
28
58
  const unseen = new Map();
@@ -81,6 +111,9 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
81
111
  title: say.title,
82
112
  body: say.body,
83
113
  silent: false,
114
+ // Omitted rather than passed empty when there is nothing to show, so a
115
+ // banner with no mark is byte for byte the banner that shipped before.
116
+ ...(icon ? { icon } : {}),
84
117
  });
85
118
  } catch { return; }
86
119
  // A banner she can act on. Clicking it brings Agentbox forward and opens the
@@ -138,18 +171,28 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
138
171
  };
139
172
  }
140
173
 
141
- export function installNotifier({ app, window, Notification, powerMonitor, ipcMain }) {
174
+ // `onArrivals` hears every arrival too, for the phone's alerts
175
+ // (main/phone-link.mjs), which make their own call about speaking.
176
+ export function installNotifier({ app, window, Notification, powerMonitor, ipcMain, nativeImage, onArrivals = () => {} }) {
142
177
  if (Notification.isSupported && !Notification.isSupported()) {
178
+ ipcMain.handle('zero:notify', (_e, payload = {}) => { try { onArrivals(Array.isArray(payload.arrivals) ? payload.arrivals : []); } catch {} });
143
179
  return { add() {}, seen() {} };
144
180
  }
145
- const notifier = createNotifier({ window, Notification, powerMonitor });
181
+ // app.getAppPath() is the repo from source and the asar inside the bundle
182
+ // when it is packaged, and the mark sits at the same place in both.
183
+ const icon = nativeImage
184
+ ? markFor({ root: app.getAppPath(), createFromPath: (p) => nativeImage.createFromPath(p) })
185
+ : null;
186
+ const notifier = createNotifier({ window, Notification, powerMonitor, icon });
146
187
  window.on('focus', () => notifier.seen());
147
188
  // Unlocking with Agentbox already the front app never fires 'focus' (it never
148
189
  // lost it), so without this the banner that fired while the screen was
149
190
  // locked would sit in Notification Center after she is back at the row.
150
191
  try { powerMonitor.on('unlock-screen', () => { if (window.isFocused()) notifier.seen(); }); } catch {}
151
192
  ipcMain.handle('zero:notify', (_e, payload = {}) => {
152
- notifier.add(Array.isArray(payload.arrivals) ? payload.arrivals : []);
193
+ const arrivals = Array.isArray(payload.arrivals) ? payload.arrivals : [];
194
+ try { onArrivals(arrivals); } catch {}
195
+ notifier.add(arrivals);
153
196
  });
154
197
  return notifier;
155
198
  }