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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// THE WAY OUT, AND NOTHING ON IT MAY KILL THE APP (w-60c8ddbab6, 2026-10-15).
|
|
2
|
+
//
|
|
3
|
+
// Quitting used to be six bare `app.on('before-quit', ...)` listeners and one
|
|
4
|
+
// `will-quit` that started the analytics flush and did not wait for it. Both
|
|
5
|
+
// halves of that cost us the end of every session.
|
|
6
|
+
//
|
|
7
|
+
// ONE STEP THAT THROWS ENDED THE WHOLE QUIT. On 0.1.13 the phone step did:
|
|
8
|
+
// closing the main window nulls `window`, and `phone.close()` makes the relay
|
|
9
|
+
// report the change back to a handler that asked the window whether it was
|
|
10
|
+
// destroyed (main/main.mjs, the `zero:phone-changed` line). An EventEmitter
|
|
11
|
+
// stops calling listeners at the first one that throws, `main/crash-report.mjs`
|
|
12
|
+
// rethrows an uncaught main error on purpose, and the process then dies
|
|
13
|
+
// fatally. Measured on an Electron harness: `updater.stop()` never ran,
|
|
14
|
+
// `will-quit` never fired, `analytics.flush()` never ran, and `quit` never
|
|
15
|
+
// came. The window's own guard is fixed where it lives, but the shape was the
|
|
16
|
+
// real fault: a quit built so that any one step can swallow the rest is a quit
|
|
17
|
+
// that will do this again for some other reason.
|
|
18
|
+
//
|
|
19
|
+
// AND THE FLUSH WAS FIRE AND FORGET. `will-quit` without `preventDefault` means
|
|
20
|
+
// the process goes as soon as the listeners return, so a batch handed to
|
|
21
|
+
// `posthog-node` a moment earlier is a POST that never leaves. Measured the
|
|
22
|
+
// same way: the flush resolved AFTER `process.on('exit')` had already run, and
|
|
23
|
+
// it only resolved at all because the harness's destination was in the same
|
|
24
|
+
// process. Against us.i.posthog.com it is a race the app loses.
|
|
25
|
+
//
|
|
26
|
+
// WHICH MATTERS BECAUSE THE END OF A SESSION IS WHAT WE READ RETENTION FROM.
|
|
27
|
+
// Of the fourteen installs that arrived on 0.1.13 or 0.1.14, thirteen opened
|
|
28
|
+
// the app once and never opened it again (PostHog, 2026-10-15), so for thirteen
|
|
29
|
+
// of fourteen people the batch dropped here is the LAST thing we would ever
|
|
30
|
+
// have heard, and the crash report explaining it is still sitting on their disk
|
|
31
|
+
// waiting for a launch that is not coming.
|
|
32
|
+
|
|
33
|
+
/** How long the quit is held open for the last batch of counts, and no longer. */
|
|
34
|
+
export const FLUSH_DEADLINE_MS = 2000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* How long a quit we asked for is given before the process is ended outright.
|
|
38
|
+
* Longer than the flush above on purpose: a quit holding itself open for the
|
|
39
|
+
* last counts is a quit that is working. */
|
|
40
|
+
export const EXIT_DEADLINE_MS = 8000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How long the second ask gets, once the counts are out and there is nothing
|
|
44
|
+
* left to wait for. On Linux a quit asked for with a window still open — the
|
|
45
|
+
* menu's Quit, the tray's Quit — ignores it outright
|
|
46
|
+
* (tests/a-quit-held-for-the-last-counts-still-ends-the-app), and by then the
|
|
47
|
+
* window is gone and there is nothing left to show anybody. */
|
|
48
|
+
export const SECOND_QUIT_DEADLINE_MS = 2000;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A STEP ON THE WAY OUT, which runs once and cannot throw.
|
|
52
|
+
*
|
|
53
|
+
* `name` is in the warning rather than in a comment, because the one thing
|
|
54
|
+
* somebody reading a log needs to know is which step gave up. A swallow with
|
|
55
|
+
* nothing said is how this bug lived through three releases.
|
|
56
|
+
*
|
|
57
|
+
* ONCE, not once per `app.quit()`. Holding the quit for the flush below means
|
|
58
|
+
* calling `app.quit()` a second time, which emits `before-quit` again, and a
|
|
59
|
+
* `killAll` or a `close` run twice is at best wasted and at worst a second
|
|
60
|
+
* teardown of something already gone.
|
|
61
|
+
*/
|
|
62
|
+
export function quitStep(app, event, name, fn, { log = console } = {}) {
|
|
63
|
+
let ran = false;
|
|
64
|
+
app.on(event, () => {
|
|
65
|
+
if (ran) return;
|
|
66
|
+
ran = true;
|
|
67
|
+
try {
|
|
68
|
+
fn();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Never rethrown: the app is on its way out and every step after this one
|
|
71
|
+
// still has to happen.
|
|
72
|
+
log?.warn?.(`zero: ${name} failed while quitting: ${err?.message ?? err}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* HOLD THE QUIT OPEN FOR THE LAST BATCH OF COUNTS.
|
|
79
|
+
*
|
|
80
|
+
* Three things it will not do, each of them a way a quit becomes a hang:
|
|
81
|
+
*
|
|
82
|
+
* - It will not wait past `FLUSH_DEADLINE_MS`. A machine with no network, or
|
|
83
|
+
* a destination that accepts the connection and never answers, must cost
|
|
84
|
+
* somebody two seconds on the way out and not their afternoon.
|
|
85
|
+
* - It will not care whether the flush worked. There is nothing to be done
|
|
86
|
+
* about a flush that failed, and a count is never worth a quit.
|
|
87
|
+
* - It will not hold twice. `will-quit` comes round again when we ask again,
|
|
88
|
+
* and holding that one too is a quit that never happens.
|
|
89
|
+
* - It will not trust the second ask either. On Linux, a quit that began with
|
|
90
|
+
* a window open ignores it and the process stays for ever, which is what
|
|
91
|
+
* `quitThenExit` is for
|
|
92
|
+
* (tests/a-quit-held-for-the-last-counts-still-ends-the-app).
|
|
93
|
+
*/
|
|
94
|
+
export function flushBeforeQuit(app, flush, { deadlineMs = FLUSH_DEADLINE_MS, log = console } = {}) {
|
|
95
|
+
let held = false;
|
|
96
|
+
app.on('will-quit', (event) => {
|
|
97
|
+
if (held) return; // the second time round is the one we asked for
|
|
98
|
+
held = true;
|
|
99
|
+
event?.preventDefault?.();
|
|
100
|
+
let gone = false;
|
|
101
|
+
const go = () => {
|
|
102
|
+
if (gone) return;
|
|
103
|
+
gone = true;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
quitThenExit(app, { what: 'the last counts', deadlineMs: SECOND_QUIT_DEADLINE_MS, log });
|
|
106
|
+
};
|
|
107
|
+
const timer = setTimeout(go, deadlineMs);
|
|
108
|
+
// The deadline is armed before the flush is even called, so a `flush` that
|
|
109
|
+
// throws synchronously is covered by the same promise as one that rejects.
|
|
110
|
+
Promise.resolve()
|
|
111
|
+
.then(() => flush())
|
|
112
|
+
.catch((err) => { log?.warn?.(`zero: the last counts did not get out: ${err?.message ?? err}`); })
|
|
113
|
+
.then(go, go);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ASK TO QUIT, AND GO ANYWAY IF THE QUIT DOES NOT COME.
|
|
119
|
+
*
|
|
120
|
+
* `app.quit()` only asks, and on Linux an app that has already refused one
|
|
121
|
+
* `will-quit` ignores every ask after it: the measurement is in
|
|
122
|
+
* tests/a-quit-held-for-the-last-counts-still-ends-the-app. It is still asked,
|
|
123
|
+
* because that is what runs the quit steps, lets the workers go and fires
|
|
124
|
+
* `quit` for whoever is listening, and the deadline ends the process when the
|
|
125
|
+
* quit has not.
|
|
126
|
+
*
|
|
127
|
+
* Both callers pass a deadline that suits what they are waiting for: a
|
|
128
|
+
* screenshot run has a whole quit ahead of it, the flush above has nothing
|
|
129
|
+
* left but the `quit` event. The timer is unreffed: an app with nothing left
|
|
130
|
+
* to do must not be kept alive by the thing watching it. */
|
|
131
|
+
export function quitThenExit(app, { what = 'this run', deadlineMs = EXIT_DEADLINE_MS, log = console } = {}) {
|
|
132
|
+
let over = false;
|
|
133
|
+
const timer = setTimeout(() => {
|
|
134
|
+
if (over) return;
|
|
135
|
+
over = true;
|
|
136
|
+
log?.warn?.(`zero: ${what} asked the app to quit ${deadlineMs} ms ago and it is still here, so it is ending now`);
|
|
137
|
+
app.exit(0);
|
|
138
|
+
}, deadlineMs);
|
|
139
|
+
timer.unref?.();
|
|
140
|
+
app.once?.('quit', () => { over = true; clearTimeout(timer); });
|
|
141
|
+
app.quit();
|
|
142
|
+
return timer;
|
|
143
|
+
}
|
|
@@ -4,11 +4,24 @@ import {rememberCodexModels} from './codex-models.mjs';
|
|
|
4
4
|
import {forget,claudeModels} from './claude-models.mjs';
|
|
5
5
|
import {terminalEnv} from './task-terminals.mjs';
|
|
6
6
|
import {codexLaunchEnv} from './codex-launch-env.mjs';
|
|
7
|
-
|
|
7
|
+
import {ensureCodexHome} from './codex-account.mjs';
|
|
8
|
+
// `homes` is every Codex login to ask, one after another. A login that cannot
|
|
9
|
+
// answer keeps its last list and does not cost the others theirs; only all of
|
|
10
|
+
// them failing is an error.
|
|
11
|
+
export async function refreshAgentModels(engine,config,homes,{open=home=>openAppServer(config,home)}={}){
|
|
8
12
|
if(engine==='claude'){forget();claudeModels({bin:config.claudeBin});return;}
|
|
9
13
|
// Codex is optional and codexBin is null without it: nothing to ask, and spawn(null) only threw into the terminal.
|
|
10
14
|
if(!config.codexBin)return;
|
|
11
|
-
const
|
|
15
|
+
const list=[...new Set([homes].flat().filter(Boolean))];const errors=[];
|
|
16
|
+
for(const home of list){try{await refreshOne(open(home),home);}catch(error){errors.push(error);}}
|
|
17
|
+
if(list.length&&errors.length===list.length)throw errors[0];
|
|
18
|
+
}
|
|
19
|
+
function openAppServer(config,home){
|
|
20
|
+
// codex-cli refuses a CODEX_HOME that does not exist rather than making it.
|
|
21
|
+
ensureCodexHome(home);
|
|
22
|
+
return createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:codexLaunchEnv(config.codexBin,{...terminalEnv(),CODEX_HOME:home}),stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
|
|
23
|
+
}
|
|
24
|
+
async function refreshOne(client,home){
|
|
12
25
|
const deadline=setTimeout(()=>client.close('Model refresh timed out'),15000);
|
|
13
26
|
try{
|
|
14
27
|
await client.initialize();let cursor=null;const models=[];const seen=new Set();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// WHICH RELAY THIS MAC USES (tests/the-mac-finds-the-relay-the-deploy-wrote-down).
|
|
2
|
+
// AGENTBOX_RELAY_URL first, for trying one by hand; then the address
|
|
3
|
+
// `npm run relay:deploy` wrote to relay/relay.local.json; then the one built
|
|
4
|
+
// into the app, which is empty until a deployed address is committed.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { RELAY_URL } from '../shared/relay-address.mjs';
|
|
8
|
+
|
|
9
|
+
function usable(url) {
|
|
10
|
+
if (typeof url !== 'string' || !url) return null;
|
|
11
|
+
let u;
|
|
12
|
+
try { u = new URL(url); } catch { return null; }
|
|
13
|
+
const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1';
|
|
14
|
+
if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) return null;
|
|
15
|
+
return url.replace(/\/+$/, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function relayAddress({ env = process.env, appDir } = {}) {
|
|
19
|
+
const byHand = usable(env.AGENTBOX_RELAY_URL);
|
|
20
|
+
if (byHand) return byHand;
|
|
21
|
+
if (appDir) {
|
|
22
|
+
try {
|
|
23
|
+
const deployed = usable(JSON.parse(fs.readFileSync(path.join(appDir, 'relay', 'relay.local.json'), 'utf8'))?.url);
|
|
24
|
+
if (deployed) return deployed;
|
|
25
|
+
} catch { /* not deployed from this folder */ }
|
|
26
|
+
}
|
|
27
|
+
return usable(RELAY_URL) ?? RELAY_URL;
|
|
28
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// THE MAC'S END OF THE RELAY (relay/worker.mjs is the middle, relay/phone/ the
|
|
2
|
+
// far end). With phones let in from "Anywhere", the Mac holds one socket open
|
|
3
|
+
// out to the relay, so nothing has to reach in to it: no port opened, no
|
|
4
|
+
// router touched, nothing installed on the phone.
|
|
5
|
+
//
|
|
6
|
+
// Each request that arrives is opened with the key only the Mac and the phone
|
|
7
|
+
// have (shared/relay-crypto.mjs), checked for a replay, and then asked of this
|
|
8
|
+
// Mac's own phone server on 127.0.0.1, exactly as a phone on the Wi-Fi would
|
|
9
|
+
// ask it. So the token, the files and every channel are guarded by the same
|
|
10
|
+
// code either way. The answer goes back sealed, a frame at a time, so the
|
|
11
|
+
// live event stream and a large picture both work.
|
|
12
|
+
import {
|
|
13
|
+
relayIds, relayKeys, open, seal, unpackRequest, FRAME, frameAad, TO_MAC, FROM_MAC, ID_BYTES, tag,
|
|
14
|
+
} from '../shared/relay-crypto.mjs';
|
|
15
|
+
|
|
16
|
+
/** How far a phone's clock may be from this Mac's before a request is stale. */
|
|
17
|
+
const SKEW_MS = 5 * 60_000;
|
|
18
|
+
/** One sealed frame's most. Cloudflare takes up to 1MB per socket message. */
|
|
19
|
+
const CHUNK = 256 * 1024;
|
|
20
|
+
/** The most one request may carry, as the relay allows (relay/worker.mjs). */
|
|
21
|
+
const MAX_REQUEST = 32 * 1024 * 1024;
|
|
22
|
+
/** What a phone's request may carry on to the phone server. */
|
|
23
|
+
const PASS = ['accept', 'content-type', 'range', 'user-agent', 'if-none-match', 'x-agentbox-token', 'x-agentbox-device'];
|
|
24
|
+
/**
|
|
25
|
+
* How often a slow answer says "still working" (FRAME.WAIT), well inside the
|
|
26
|
+
* 30 s the room gives a Mac to start answering (relay/room.mjs).
|
|
27
|
+
*/
|
|
28
|
+
const WAIT_MS = 10_000;
|
|
29
|
+
/**
|
|
30
|
+
* THE LINK IS CHECKED, NOT TRUSTED (tests/the-computer-notices-a-dead-relay-link-and-reconnects).
|
|
31
|
+
* A socket that carries nothing cannot tell this end it has died: a laptop that
|
|
32
|
+
* slept or changed networks kept one it believed open while the relay had let
|
|
33
|
+
* it go, and phones were told the computer was not connected. So this end says
|
|
34
|
+
* "ping" this often and the relay answers "pong" without waking the room.
|
|
35
|
+
*/
|
|
36
|
+
export const PING_MS = 25_000;
|
|
37
|
+
/** How long past a ping's turn a missing pong is allowed before the link is called dead. */
|
|
38
|
+
export const PONG_GRACE_MS = 10_000;
|
|
39
|
+
/** What the platform sends as text on the socket. Never a request: those are bytes. */
|
|
40
|
+
const PING = 'ping';
|
|
41
|
+
const PONG = 'pong';
|
|
42
|
+
/** What the phone does not want back: the relay's stream is its own framing. */
|
|
43
|
+
const DROP = new Set(['connection', 'keep-alive', 'transfer-encoding', 'content-length', 'content-encoding']);
|
|
44
|
+
|
|
45
|
+
const enc = new TextEncoder();
|
|
46
|
+
const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
47
|
+
|
|
48
|
+
export function createRelayClient({
|
|
49
|
+
relayUrl, key, port, WebSocketImpl = globalThis.WebSocket, fetchImpl = (...a) => globalThis.fetch(...a),
|
|
50
|
+
log = console, onChange = () => {}, waitMs = WAIT_MS, pingMs = PING_MS, graceMs = PONG_GRACE_MS,
|
|
51
|
+
}) {
|
|
52
|
+
let ws = null;
|
|
53
|
+
let pinger = null;
|
|
54
|
+
/** When the relay last answered a ping on this link; null until it ever has. */
|
|
55
|
+
let ponged = null;
|
|
56
|
+
/** When the ping timer last ran, to tell a sleep from a slow tick. */
|
|
57
|
+
let ticked = 0;
|
|
58
|
+
let want = false;
|
|
59
|
+
let connected = false;
|
|
60
|
+
let error = null;
|
|
61
|
+
let retry = 1000;
|
|
62
|
+
let timer = null;
|
|
63
|
+
let gen = 0;
|
|
64
|
+
let forKey = null;
|
|
65
|
+
let ids = null;
|
|
66
|
+
let keys = null;
|
|
67
|
+
/** Requests answered lately, by nonce, so a recorded one sent again is refused. */
|
|
68
|
+
const answered = new Map();
|
|
69
|
+
/** Answers being sent, so a phone that gives up stops them. */
|
|
70
|
+
const live = new Map();
|
|
71
|
+
/** Pieces of requests too big for one message, until their last piece. */
|
|
72
|
+
const parts = new Map();
|
|
73
|
+
|
|
74
|
+
async function ready() {
|
|
75
|
+
const k = key();
|
|
76
|
+
if (k === forKey) return;
|
|
77
|
+
ids = await relayIds(k);
|
|
78
|
+
keys = await relayKeys(k);
|
|
79
|
+
forKey = k;
|
|
80
|
+
answered.clear();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fresh(head) {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
for (const [n, at] of answered) if (now - at > 2 * SKEW_MS) answered.delete(n);
|
|
86
|
+
if (typeof head?.nonce !== 'string' || head.nonce.length < 16) return false;
|
|
87
|
+
if (!Number.isFinite(head.ts) || Math.abs(now - head.ts) > SKEW_MS) return false;
|
|
88
|
+
if (answered.has(head.nonce)) return false;
|
|
89
|
+
answered.set(head.nonce, now);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const send = (sock, bytes) => { try { sock.send(bytes); } catch { /* the socket went; the phone hears from the relay */ } };
|
|
94
|
+
|
|
95
|
+
async function answer(sock, id, sealed) {
|
|
96
|
+
const k = keys;
|
|
97
|
+
let head;
|
|
98
|
+
let body;
|
|
99
|
+
try {
|
|
100
|
+
({ head, body } = unpackRequest(await open(k.up, sealed, 'req')));
|
|
101
|
+
} catch {
|
|
102
|
+
send(sock, tag(FROM_MAC.REFUSED, id));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!fresh(head) || typeof head.path !== 'string' || !head.path.startsWith('/')) {
|
|
106
|
+
send(sock, tag(FROM_MAC.REFUSED, id));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const ctl = new AbortController();
|
|
110
|
+
const idh = hex(id);
|
|
111
|
+
live.set(idh, ctl);
|
|
112
|
+
let seq = 0;
|
|
113
|
+
// One at a time, in order: each frame's number is sealed into it, and a
|
|
114
|
+
// "still working" frame from the timer must not overtake the head.
|
|
115
|
+
let queue = Promise.resolve();
|
|
116
|
+
const frame = (type, payload) => (queue = queue.then(async () => {
|
|
117
|
+
const plain = new Uint8Array(1 + payload.length);
|
|
118
|
+
plain[0] = type;
|
|
119
|
+
plain.set(payload, 1);
|
|
120
|
+
send(sock, tag(FROM_MAC.FRAME, id, await seal(k.down, plain, frameAad(head.nonce, seq++))));
|
|
121
|
+
}));
|
|
122
|
+
// Until the answer starts, a phone that asked for it hears "still working".
|
|
123
|
+
let waiting = head.wait ? setInterval(() => { frame(FRAME.WAIT, new Uint8Array(0)).catch(() => {}); }, waitMs) : null;
|
|
124
|
+
const answering = () => { if (waiting) clearInterval(waiting); waiting = null; };
|
|
125
|
+
let sentHead = false;
|
|
126
|
+
try {
|
|
127
|
+
const headers = { 'x-agentbox-relay': '1', 'x-forwarded-for': 'relay' };
|
|
128
|
+
for (const name of PASS) if (head.headers?.[name]) headers[name] = String(head.headers[name]);
|
|
129
|
+
const method = String(head.method || 'GET').toUpperCase();
|
|
130
|
+
const res = await fetchImpl(`http://127.0.0.1:${port()}${head.path}`, {
|
|
131
|
+
method, headers, body: method === 'GET' || method === 'HEAD' ? undefined : body, signal: ctl.signal,
|
|
132
|
+
});
|
|
133
|
+
answering();
|
|
134
|
+
const out = {};
|
|
135
|
+
res.headers.forEach((v, name) => { if (!DROP.has(name)) out[name] = v; });
|
|
136
|
+
sentHead = true;
|
|
137
|
+
await frame(FRAME.HEAD, enc.encode(JSON.stringify({ status: res.status, headers: out })));
|
|
138
|
+
if (res.body) {
|
|
139
|
+
const reader = res.body.getReader();
|
|
140
|
+
for (;;) {
|
|
141
|
+
const { value, done } = await reader.read();
|
|
142
|
+
if (done) break;
|
|
143
|
+
for (let i = 0; i < value.length; i += CHUNK) await frame(FRAME.DATA, value.subarray(i, i + CHUNK));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await frame(FRAME.END, new Uint8Array(0));
|
|
147
|
+
send(sock, tag(FROM_MAC.DONE, id));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
answering();
|
|
150
|
+
if (ctl.signal.aborted) return;
|
|
151
|
+
log.warn?.('[relay] answering failed:', err?.message ?? err);
|
|
152
|
+
try {
|
|
153
|
+
await queue.catch(() => {});
|
|
154
|
+
if (!sentHead) {
|
|
155
|
+
await frame(FRAME.HEAD, enc.encode(JSON.stringify({ status: 502, headers: { 'content-type': 'application/json' } })));
|
|
156
|
+
await frame(FRAME.DATA, enc.encode(JSON.stringify({ error: 'Your computer could not answer that.' })));
|
|
157
|
+
}
|
|
158
|
+
await frame(FRAME.END, new Uint8Array(0));
|
|
159
|
+
} catch { /* nothing more to say */ }
|
|
160
|
+
send(sock, tag(FROM_MAC.DONE, id));
|
|
161
|
+
} finally {
|
|
162
|
+
answering();
|
|
163
|
+
live.delete(idh);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function onMessage(sock, data) {
|
|
168
|
+
if (typeof data === 'string') {
|
|
169
|
+
if (data === PONG) ponged = Date.now();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data)
|
|
173
|
+
: data instanceof Uint8Array ? data
|
|
174
|
+
: new Uint8Array(await data.arrayBuffer());
|
|
175
|
+
if (bytes.length < 1 + ID_BYTES) return;
|
|
176
|
+
const id = bytes.slice(1, 1 + ID_BYTES);
|
|
177
|
+
const idh = hex(id);
|
|
178
|
+
if (bytes[0] === TO_MAC.CANCEL) {
|
|
179
|
+
live.get(idh)?.abort();
|
|
180
|
+
live.delete(idh);
|
|
181
|
+
parts.delete(idh);
|
|
182
|
+
} else if (bytes[0] === TO_MAC.PART) {
|
|
183
|
+
// A big request comes in pieces (relay/room.mjs); keep them until the
|
|
184
|
+
// last, within a limit, so a stream of pieces cannot fill memory.
|
|
185
|
+
const held = parts.get(idh) ?? { chunks: [], size: 0 };
|
|
186
|
+
held.chunks.push(bytes.slice(1 + ID_BYTES));
|
|
187
|
+
held.size += bytes.length - 1 - ID_BYTES;
|
|
188
|
+
if (held.size > MAX_REQUEST) { parts.delete(idh); send(sock, tag(FROM_MAC.REFUSED, id)); return; }
|
|
189
|
+
parts.set(idh, held);
|
|
190
|
+
} else if (bytes[0] === TO_MAC.REQUEST) {
|
|
191
|
+
const held = parts.get(idh);
|
|
192
|
+
parts.delete(idh);
|
|
193
|
+
const last = bytes.slice(1 + ID_BYTES);
|
|
194
|
+
if (!held) { await answer(sock, id, last); return; }
|
|
195
|
+
const whole = new Uint8Array(held.size + last.length);
|
|
196
|
+
let at = 0;
|
|
197
|
+
for (const c of held.chunks) { whole.set(c, at); at += c.length; }
|
|
198
|
+
whole.set(last, at);
|
|
199
|
+
await answer(sock, id, whole);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Two ways a link is known dead without waiting on the operating system: a
|
|
204
|
+
// relay that has answered pings stops answering, or this timer ran far too
|
|
205
|
+
// late, which is a laptop waking from sleep with every socket gone under it.
|
|
206
|
+
// A relay that never answers (one deployed before pings) is only ever judged
|
|
207
|
+
// by the second, so an old relay is not mistaken for a dead link.
|
|
208
|
+
function startPinging(sock) {
|
|
209
|
+
stopPinging();
|
|
210
|
+
ponged = null;
|
|
211
|
+
ticked = Date.now();
|
|
212
|
+
pinger = setInterval(() => {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const slept = now - ticked > pingMs * 2;
|
|
215
|
+
ticked = now;
|
|
216
|
+
const silent = ponged !== null && now - ponged > pingMs + graceMs;
|
|
217
|
+
if (slept || silent) {
|
|
218
|
+
log.info?.(`[relay] the link went quiet (${slept ? 'woke from sleep' : 'no answer to ping'}); reconnecting`);
|
|
219
|
+
replace(sock);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
send(sock, PING);
|
|
223
|
+
}, pingMs);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function stopPinging() {
|
|
227
|
+
if (pinger) clearInterval(pinger);
|
|
228
|
+
pinger = null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Let a dead link go and open a new one now, not after the backoff. */
|
|
232
|
+
function replace(sock) {
|
|
233
|
+
if (ws !== sock) return;
|
|
234
|
+
stopPinging();
|
|
235
|
+
ws = null;
|
|
236
|
+
connected = false;
|
|
237
|
+
for (const c of live.values()) c.abort();
|
|
238
|
+
live.clear();
|
|
239
|
+
try { sock.close(4002, 'No answer; reconnecting.'); } catch { /* already gone */ }
|
|
240
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
241
|
+
retry = 1000;
|
|
242
|
+
onChange();
|
|
243
|
+
void connect();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function schedule() {
|
|
247
|
+
if (!want || timer) return;
|
|
248
|
+
timer = setTimeout(() => { timer = null; void connect(); }, retry);
|
|
249
|
+
retry = Math.min(retry * 2, 30_000);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function connect() {
|
|
253
|
+
const mine = ++gen;
|
|
254
|
+
try { await ready(); } catch (err) { error = String(err?.message ?? err); onChange(); schedule(); return; }
|
|
255
|
+
if (!want || mine !== gen) return;
|
|
256
|
+
const url = `${relayUrl.replace(/^http/, 'ws').replace(/\/+$/, '')}/mac/${ids.room}?proof=${encodeURIComponent(ids.proof)}`;
|
|
257
|
+
let sock;
|
|
258
|
+
try { sock = new WebSocketImpl(url); } catch (err) { error = `Could not reach the relay: ${err?.message ?? err}`; onChange(); schedule(); return; }
|
|
259
|
+
ws = sock;
|
|
260
|
+
sock.binaryType = 'arraybuffer';
|
|
261
|
+
sock.onopen = () => {
|
|
262
|
+
if (ws !== sock) return;
|
|
263
|
+
connected = true;
|
|
264
|
+
error = null;
|
|
265
|
+
retry = 1000;
|
|
266
|
+
startPinging(sock);
|
|
267
|
+
onChange();
|
|
268
|
+
};
|
|
269
|
+
sock.onmessage = (event) => { if (ws === sock) void onMessage(sock, event.data); };
|
|
270
|
+
sock.onerror = () => { if (ws === sock && !connected) error = 'Could not reach the relay.'; };
|
|
271
|
+
sock.onclose = (event) => {
|
|
272
|
+
if (ws !== sock) return;
|
|
273
|
+
stopPinging();
|
|
274
|
+
ws = null;
|
|
275
|
+
const was = connected;
|
|
276
|
+
connected = false;
|
|
277
|
+
for (const c of live.values()) c.abort();
|
|
278
|
+
live.clear();
|
|
279
|
+
if (event?.code === 4001) error = 'The relay turned this computer away.';
|
|
280
|
+
else if (was) error = null;
|
|
281
|
+
onChange();
|
|
282
|
+
schedule();
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
start() {
|
|
288
|
+
if (want) return;
|
|
289
|
+
want = true;
|
|
290
|
+
retry = 1000;
|
|
291
|
+
void connect();
|
|
292
|
+
},
|
|
293
|
+
stop() {
|
|
294
|
+
want = false;
|
|
295
|
+
gen += 1;
|
|
296
|
+
stopPinging();
|
|
297
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
298
|
+
const sock = ws;
|
|
299
|
+
ws = null;
|
|
300
|
+
connected = false;
|
|
301
|
+
for (const c of live.values()) c.abort();
|
|
302
|
+
live.clear();
|
|
303
|
+
try { sock?.close(1000, 'Phones from anywhere turned off.'); } catch { /* already */ }
|
|
304
|
+
onChange();
|
|
305
|
+
},
|
|
306
|
+
/** The key was reset: the room is a new one. */
|
|
307
|
+
restart() {
|
|
308
|
+
if (!want) return;
|
|
309
|
+
this.stop();
|
|
310
|
+
this.start();
|
|
311
|
+
},
|
|
312
|
+
state: () => ({ connected, error, connecting: want && !connected && !error }),
|
|
313
|
+
};
|
|
314
|
+
}
|
package/main/repeats.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import {
|
|
18
18
|
DAY_NAMES, foldRepeats, isOwed, isRepeatRule, occurrenceId, periodKey, repeatLabel,
|
|
19
|
+
standingInstruction, PROTOCOL_OPENS, INSTRUCTION_MARK,
|
|
19
20
|
} from '../shared/repeats.mjs';
|
|
20
21
|
import { machineryPath } from './store/home.mjs';
|
|
21
22
|
import { NAME } from '../shared/product-name.mjs';
|
|
@@ -41,8 +42,8 @@ const MISSES_BEFORE_ALERT = 3;
|
|
|
41
42
|
// last can be truncated away entirely, and a worker that never saw it cannot
|
|
42
43
|
// know that a quiet finish has to be marked.
|
|
43
44
|
export const OCCURRENCE_PROTOCOL = [
|
|
44
|
-
|
|
45
|
-
'here, so this run does not have to carry the future.',
|
|
45
|
+
`${PROTOCOL_OPENS} It runs again at its next time whatever`,
|
|
46
|
+
'happens here, so this run does not have to carry the future.',
|
|
46
47
|
'',
|
|
47
48
|
'FINISH ONE OF TWO WAYS.',
|
|
48
49
|
'',
|
|
@@ -60,7 +61,12 @@ export const OCCURRENCE_PROTOCOL = [
|
|
|
60
61
|
`only place in ${NAME} where finishing quietly hides something, so the marker is`,
|
|
61
62
|
'required to hide, never to show.',
|
|
62
63
|
'',
|
|
63
|
-
'
|
|
64
|
+
'IF THEY TELL YOU HOW THIS TASK SHOULD GO FROM NOW ON, CHANGE IT YOURSELF with',
|
|
65
|
+
'the store tool set_repeating_task. Editing this run or writing a memory note',
|
|
66
|
+
'reaches no later run: this body is a copy, and every run gets a new task',
|
|
67
|
+
'folder. Never ask them to go and edit the task by hand.',
|
|
68
|
+
'',
|
|
69
|
+
INSTRUCTION_MARK,
|
|
64
70
|
].join('\n');
|
|
65
71
|
|
|
66
72
|
export class Repeats {
|
|
@@ -212,7 +218,7 @@ export class Repeats {
|
|
|
212
218
|
|
|
213
219
|
const { created } = workItemsDisk.createWorkItemIfAbsent(productDir, id, {
|
|
214
220
|
title: `${rule.title} · ${dayStamp(key)}`,
|
|
215
|
-
body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${rule
|
|
221
|
+
body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${standingInstruction(rule)}`,
|
|
216
222
|
kind: 'directive',
|
|
217
223
|
priority: rule.priority ?? 5,
|
|
218
224
|
labels: ['founder', repeatLabel(ruleId)],
|
package/main/row-label.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { harnessFor } from './harnesses.mjs';
|
|
2
|
+
import { teammateIdOf } from '../shared/teammates.mjs';
|
|
1
3
|
// THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM THE FIRST SENTENCE.
|
|
2
4
|
//
|
|
3
5
|
// A dictated message keeps its first line as its title, verbatim and forever
|
|
@@ -61,51 +63,18 @@ export const NAME_MAX = 64;
|
|
|
61
63
|
* archive; everything else is a row that can still appear in a list. */
|
|
62
64
|
const LIVE = new Set(['open', 'claimed', 'blocked']);
|
|
63
65
|
|
|
64
|
-
/**
|
|
65
|
-
* PURE. Is this title already a name somebody wrote?
|
|
66
|
-
*
|
|
67
|
-
* The one question that decides whether this module speaks at all, and it is
|
|
68
|
-
* answered off the words alone because that is all there is: nothing on a row
|
|
69
|
-
* records whether its title was typed or derived.
|
|
70
|
-
*
|
|
71
|
-
* Three marks, and together they separate the two shapes `splitMessage` makes
|
|
72
|
-
* (renderer/src/message-split.ts). A first line of 120 characters or less is
|
|
73
|
-
* kept VERBATIM and is the name its person wrote. A longer one is clipped into
|
|
74
|
-
* a title by the app, and every clip it can produce is either a whole sentence
|
|
75
|
-
* of at least 25 characters or a hard cut ending in an ellipsis.
|
|
76
|
-
*
|
|
77
|
-
* as short as a name we would write NAME_MAX, so a title already that tight
|
|
78
|
-
* has nothing for us to improve,
|
|
79
|
-
* as few words as a name has the same eight `cleanName` allows,
|
|
80
|
-
* not ending as a sentence does '.', '!', '?' and '…' are how a clipped
|
|
81
|
-
* title ends and not how a name does.
|
|
82
|
-
*
|
|
83
|
-
* It errs towards leaving the words alone, which is the right way to be wrong:
|
|
84
|
-
* the cost is a short typed sentence keeping its own first line, and the cost
|
|
85
|
-
* the other way is somebody losing the thread they named.
|
|
86
|
-
*/
|
|
87
|
-
export function isWrittenName(title) {
|
|
88
|
-
const t = String(title ?? '').trim();
|
|
89
|
-
if (!t) return false;
|
|
90
|
-
if (t.length > NAME_MAX) return false;
|
|
91
|
-
if (t.split(/\s+/).length > 8) return false;
|
|
92
|
-
return !/[.!?…]$/.test(t);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
66
|
/**
|
|
96
67
|
* PURE. Does this row want naming?
|
|
97
68
|
*
|
|
98
|
-
*
|
|
99
|
-
* own threads on 2026-10-01: "Add comment to cart.js" became "Improve cart.js
|
|
100
|
-
* header comment" and then "Update cart file comment" inside two minutes.
|
|
69
|
+
* Three ways to say no:
|
|
101
70
|
*
|
|
102
71
|
* - it is done, so nobody is scanning for it,
|
|
103
72
|
* - it has nothing to name from, which is a row with no words on it yet,
|
|
104
|
-
* - ITS PERSON ALREADY NAMED IT. A short clear title is the name they will
|
|
105
|
-
* look for, and on a team it is the name they will point someone else at.
|
|
106
|
-
* Improving it is not an improvement.
|
|
107
73
|
* - IT HAS BEEN NAMED. Once, when the thread was made, and then never again.
|
|
108
74
|
*
|
|
75
|
+
* A short typed title is named too: a list where some rows are typed lines and
|
|
76
|
+
* the rest are names reads as two styles. The prompt keeps such a line's words.
|
|
77
|
+
*
|
|
109
78
|
* That last line used to read `wroteAt < contentTouchedAt(item)`, so the name
|
|
110
79
|
* followed where the thread stood: an agent writes a note and a result on every
|
|
111
80
|
* run, so every run earned a new name. Following the thread was the intended
|
|
@@ -121,8 +90,8 @@ export function isWrittenName(title) {
|
|
|
121
90
|
*/
|
|
122
91
|
export function wantsName(item) {
|
|
123
92
|
if (!item || !LIVE.has(item.status)) return false;
|
|
93
|
+
if (teammateIdOf(item)) return false;
|
|
124
94
|
if (!String(item.title ?? '').trim() && !String(item.body ?? '').trim()) return false;
|
|
125
|
-
if (isWrittenName(item.title)) return false;
|
|
126
95
|
return !String(item.label ?? '').trim();
|
|
127
96
|
}
|
|
128
97
|
|
|
@@ -138,12 +107,15 @@ export function namePrompt(item) {
|
|
|
138
107
|
'Here is a work thread: the message that opened it, and the latest thing',
|
|
139
108
|
'written on it. Write a short name for the thread AS IT STANDS NOW: 3 to 7',
|
|
140
109
|
'words, sentence case, no quotes, no trailing punctuation, naming the thing',
|
|
141
|
-
'being worked on rather than restating the sentence.
|
|
142
|
-
'
|
|
110
|
+
'being worked on rather than restating the sentence. If the opening line is',
|
|
111
|
+
'already a short name, keep its words and only fix the case. Reply with the',
|
|
112
|
+
'name and nothing else.',
|
|
143
113
|
'',
|
|
144
114
|
'--- opened with ---',
|
|
145
115
|
cut(item?.title, 300),
|
|
146
|
-
|
|
116
|
+
// A teammate's run is titled with its own part of a message that may have
|
|
117
|
+
// woken several (shared/teammates.mjs `runTitle`); the whole would mix them.
|
|
118
|
+
(item?.labels ?? []).some((l) => String(l).startsWith('teammate:')) ? '' : cut(item?.body, 1200),
|
|
147
119
|
'--- latest ---',
|
|
148
120
|
latest || '(nothing written yet)',
|
|
149
121
|
'---',
|
|
@@ -188,10 +160,7 @@ export function codexNameArgs(item) {
|
|
|
188
160
|
}
|
|
189
161
|
|
|
190
162
|
function codexArgs(prompt) {
|
|
191
|
-
return
|
|
192
|
-
'exec', '--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules',
|
|
193
|
-
'-s', 'read-only', '-c', 'model_reasoning_effort=low', prompt,
|
|
194
|
-
];
|
|
163
|
+
return harnessFor('codex').smallModelArgs(prompt);
|
|
195
164
|
}
|
|
196
165
|
|
|
197
166
|
/**
|
|
@@ -218,11 +187,13 @@ export function nameRow(item, opts = {}) {
|
|
|
218
187
|
*/
|
|
219
188
|
export function askSmallModel(prompt, clean, { claudeBin, codexBin = null, engine = 'claude', model = NAME_MODEL, timeoutMs = NAME_TIMEOUT_MS, env } = {}) {
|
|
220
189
|
return new Promise((resolve) => {
|
|
221
|
-
|
|
190
|
+
let harness;
|
|
191
|
+
try { harness = harnessFor(engine); } catch { return resolve(''); }
|
|
192
|
+
const bin = harness.binary({ claudeBin, codexBin });
|
|
222
193
|
if (!bin) return resolve('');
|
|
223
194
|
let child;
|
|
224
195
|
try {
|
|
225
|
-
child = spawn(bin,
|
|
196
|
+
child = spawn(bin, harness.smallModelArgs(prompt, model), {
|
|
226
197
|
// Somewhere that is nobody's project. A naming call must not pick up a
|
|
227
198
|
// CLAUDE.md, a settings file or a hook from whatever folder it lands in.
|
|
228
199
|
cwd: '/tmp',
|