@north-light/crouter 0.3.173 → 0.3.174
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/bin/runtime-selector.mjs +8 -1
- package/dist/api/__tests__/{client.test.js → serial/client.test.js} +2 -135
- package/dist/api/client.d.ts +12 -21
- package/dist/api/client.js +22 -31
- package/dist/api/dto/broker-ops.d.ts +158 -0
- package/dist/api/dto/broker-ops.js +6 -0
- package/dist/api/dto/crons.d.ts +6 -0
- package/dist/api/dto/human.d.ts +7 -1
- package/dist/api/dto/inbox.d.ts +1 -71
- package/dist/api/dto/inbox.js +1 -9
- package/dist/api/dto/nodes.d.ts +6 -7
- package/dist/api/dto/reviews.d.ts +1 -11
- package/dist/api/routes.d.ts +7 -4
- package/dist/api/routes.js +8 -5
- package/dist/build-root.js +3 -3
- package/dist/builtin-memory/04-base-worker.md +1 -4
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +1 -5
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +2 -7
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -6
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -6
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -4
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -6
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +1 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -4
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +2 -3
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -2
- package/dist/builtin-memory/advisor/council.md +1 -6
- package/dist/builtin-memory/design.md +2 -7
- package/dist/builtin-memory/development.md +9 -14
- package/dist/builtin-memory/internal/marketplaces.md +2 -6
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/plugins.md +2 -8
- package/dist/builtin-memory/internal/storage-tiers.md +1 -2
- package/dist/builtin-memory/testing.md +41 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{provider-rotation.test.ts → serial/provider-rotation.test.ts} +27 -179
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +5 -15
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +1 -28
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +43 -88
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +9 -1
- package/dist/cli.js +2 -2
- package/dist/clients/attach/__tests__/attach-remote-readonly.test.js +37 -131
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +71 -152
- package/dist/clients/attach/__tests__/context-message.test.js +16 -174
- package/dist/clients/attach/__tests__/crtr-output.test.js +36 -2069
- package/dist/clients/attach/__tests__/diagram.test.js +2 -2
- package/dist/clients/attach/__tests__/edit-diff.test.js +0 -4
- package/dist/clients/attach/__tests__/file-review-focus.test.js +2 -1
- package/dist/clients/attach/__tests__/group-activity.test.js +40 -229
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +26 -76
- package/dist/clients/attach/__tests__/prompt-preview.test.js +4 -70
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +11 -125
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +14 -205
- package/dist/clients/attach/command.d.ts +2 -1
- package/dist/clients/attach/command.js +81 -76
- package/dist/clients/attach/input/capabilities.d.ts +27 -0
- package/dist/clients/attach/input/capabilities.js +96 -0
- package/dist/clients/attach/input/controller.d.ts +20 -182
- package/dist/clients/attach/input/controller.js +128 -537
- package/dist/clients/attach/input/overlay-owner.d.ts +41 -0
- package/dist/clients/attach/input/overlay-owner.js +199 -0
- package/dist/clients/attach/overlays/file-review.d.ts +0 -4
- package/dist/clients/attach/overlays/file-review.js +13 -5
- package/dist/clients/attach/overlays/graph.js +2 -10
- package/dist/clients/attach/render/chat-view.d.ts +4 -3
- package/dist/clients/attach/render/chat-view.js +39 -33
- package/dist/clients/attach/render/group-activity.d.ts +1 -0
- package/dist/clients/attach/render/group-activity.js +20 -13
- package/dist/clients/attach/render/group-recap.js +12 -3
- package/dist/clients/attach/session/connection.js +9 -15
- package/dist/clients/attach/session/editor-inventory.d.ts +6 -5
- package/dist/clients/attach/session/editor-inventory.js +7 -8
- package/dist/clients/attach/session/file-links.d.ts +13 -0
- package/dist/clients/attach/session/file-links.js +107 -0
- package/dist/clients/attach/session/frame.d.ts +3 -0
- package/dist/clients/attach/session/frame.js +10 -1
- package/dist/clients/attach/session/frames.js +2 -3
- package/dist/clients/attach/session/input-wiring.d.ts +4 -1
- package/dist/clients/attach/session/input-wiring.js +15 -14
- package/dist/clients/attach/session/mouse.d.ts +6 -1
- package/dist/clients/attach/session/mouse.js +8 -5
- package/dist/clients/attach/session/profile-files.js +18 -12
- package/dist/clients/attach/session/reconnect.js +1 -1
- package/dist/clients/attach/session/state-sync.js +1 -2
- package/dist/clients/attach/session/surface-host.d.ts +3 -2
- package/dist/clients/attach/session/surface-host.js +4 -4
- package/dist/clients/attach/slash/dispatch.d.ts +8 -31
- package/dist/clients/attach/slash/dispatch.js +34 -81
- package/dist/clients/attach/viewer.js +820 -860
- package/dist/clients/conversation/projection.d.ts +79 -0
- package/dist/clients/conversation/projection.js +341 -0
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +159 -0
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +151 -0
- package/dist/clients/inbox/controller.d.ts +0 -4
- package/dist/clients/inbox/controller.js +0 -2
- package/dist/clients/inbox/review/__tests__/review-core.test.js +4 -293
- package/dist/clients/inbox/review/__tests__/teardown-notice.test.js +100 -0
- package/dist/clients/inbox/review/comments-client.d.ts +3 -2
- package/dist/clients/inbox/review/comments-client.js +12 -12
- package/dist/clients/inbox/review/companion-pane.d.ts +30 -0
- package/dist/clients/inbox/review/companion-pane.js +86 -0
- package/dist/clients/inbox/review/document-surface.d.ts +182 -0
- package/dist/clients/inbox/review/{document-region.js → document-surface.js} +252 -48
- package/dist/clients/inbox/review/frame.js +5 -4
- package/dist/clients/inbox/review/launch.d.ts +8 -8
- package/dist/clients/inbox/review/launch.js +52 -92
- package/dist/clients/inbox/review/nvim.d.ts +2 -0
- package/dist/clients/inbox/review/nvim.js +14 -12
- package/dist/clients/inbox/review/review-client.d.ts +12 -8
- package/dist/clients/inbox/review/review-client.js +46 -35
- package/dist/clients/inbox/review-adapter.d.ts +3 -10
- package/dist/clients/inbox/review-adapter.js +27 -19
- package/dist/clients/inbox/surface.d.ts +0 -4
- package/dist/clients/inbox/surface.js +0 -2
- package/dist/clients/surfaces/host.d.ts +11 -3
- package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -34
- package/dist/commands/__tests__/human.test.js +11 -322
- package/dist/commands/api-client.d.ts +5 -5
- package/dist/commands/api-client.js +6 -85
- package/dist/commands/canvas-browse.js +3 -29
- package/dist/commands/canvas-config.js +1 -1
- package/dist/commands/canvas-use.js +3 -3
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/cron.js +5 -1
- package/dist/commands/human/queue.d.ts +0 -4
- package/dist/commands/human/queue.js +7 -40
- package/dist/commands/memory/list.d.ts +14 -0
- package/dist/commands/memory/list.js +35 -31
- package/dist/commands/memory/read.d.ts +7 -0
- package/dist/commands/memory/read.js +14 -8
- package/dist/commands/node/bash.d.ts +1 -0
- package/dist/commands/node/bash.js +198 -0
- package/dist/commands/node/create.d.ts +23 -0
- package/dist/commands/node/create.js +207 -0
- package/dist/commands/node/inspect.d.ts +2 -0
- package/dist/commands/node/inspect.js +302 -0
- package/dist/commands/node/lifecycle.d.ts +4 -0
- package/dist/commands/node/lifecycle.js +415 -0
- package/dist/commands/node/message.d.ts +1 -0
- package/dist/commands/node/message.js +164 -0
- package/dist/commands/node/pyramid.d.ts +2 -0
- package/dist/commands/node/pyramid.js +79 -0
- package/dist/commands/node/subscription.d.ts +1 -0
- package/dist/commands/node/subscription.js +82 -0
- package/dist/commands/node/wait.d.ts +1 -0
- package/dist/commands/node/wait.js +159 -0
- package/dist/commands/node-lifecycle-revive.js +10 -57
- package/dist/commands/node.d.ts +0 -42
- package/dist/commands/node.js +11 -2065
- package/dist/commands/search.js +1 -1
- package/dist/commands/surface/node/cycle.d.ts +1 -0
- package/dist/commands/surface/node/cycle.js +83 -0
- package/dist/commands/surface/node/focus.d.ts +1 -0
- package/dist/commands/surface/node/focus.js +67 -0
- package/dist/commands/surface/node/id.d.ts +1 -0
- package/dist/commands/surface/node/id.js +38 -0
- package/dist/commands/surface/node/pin.d.ts +1 -0
- package/dist/commands/surface/node/pin.js +42 -0
- package/dist/commands/surface/node/placement.d.ts +46 -0
- package/dist/commands/surface/node/placement.js +124 -0
- package/dist/commands/surface-node.js +4 -1
- package/dist/commands/surface-reload.js +1 -2
- package/dist/commands/surface-tmux-spread.js +1 -1
- package/dist/commands/surface.js +5 -20
- package/dist/commands/sys/__tests__/setup-core.test.js +18 -271
- package/dist/commands/sys/config.js +6 -6
- package/dist/commands/sys/logs.js +1 -1
- package/dist/commands/sys/panels/broker-limits-panel.d.ts +17 -0
- package/dist/commands/sys/panels/broker-limits-panel.js +104 -0
- package/dist/commands/sys/panels/dependencies-panel.d.ts +14 -0
- package/dist/commands/sys/panels/dependencies-panel.js +80 -0
- package/dist/commands/sys/panels/keybindings-panel.d.ts +56 -0
- package/dist/commands/sys/panels/keybindings-panel.js +703 -0
- package/dist/commands/sys/panels/keys-panel.d.ts +16 -0
- package/dist/commands/sys/panels/keys-panel.js +60 -0
- package/dist/commands/sys/panels/models-panel.d.ts +77 -0
- package/dist/commands/sys/panels/models-panel.js +647 -0
- package/dist/commands/sys/panels/panel.d.ts +45 -0
- package/dist/commands/sys/panels/panel.js +137 -0
- package/dist/commands/sys/panels/plugins-panel.d.ts +16 -0
- package/dist/commands/sys/panels/plugins-panel.js +80 -0
- package/dist/commands/sys/panels/prefs-panel.d.ts +21 -0
- package/dist/commands/sys/panels/prefs-panel.js +85 -0
- package/dist/commands/sys/panels/provider-panel.d.ts +62 -0
- package/dist/commands/sys/panels/provider-panel.js +538 -0
- package/dist/commands/sys/settings-shell.d.ts +55 -0
- package/dist/commands/sys/settings-shell.js +400 -0
- package/dist/commands/sys/settings.d.ts +2 -0
- package/dist/commands/sys/settings.js +31 -0
- package/dist/commands/sys/setup-core.d.ts +3 -33
- package/dist/commands/sys/setup-core.js +27 -78
- package/dist/commands/sys/setup-settings-runner.d.ts +24 -0
- package/dist/commands/sys/setup-settings-runner.js +63 -0
- package/dist/commands/sys/setup-wizard.d.ts +26 -125
- package/dist/commands/sys/setup-wizard.js +209 -2473
- package/dist/commands/sys/setup.js +5 -7
- package/dist/commands/sys.js +3 -2
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +50 -0
- package/dist/core/__tests__/broker-launch-candidates.test.js +20 -9
- package/dist/core/__tests__/broker-preflight.test.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +108 -27
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +14 -0
- package/dist/core/__tests__/canvas.test.js +2 -36
- package/dist/core/__tests__/close.test.js +14 -3
- package/dist/core/__tests__/context-intro.test.js +32 -143
- package/dist/core/__tests__/cron-broker-capacity.test.js +22 -3
- package/dist/core/__tests__/daemon-boot.test.js +2 -20
- package/dist/core/__tests__/daemon-wedge.test.js +1 -1
- package/dist/core/__tests__/dead-node-policy-table.test.js +1 -18
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +1 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +2 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +4 -98
- package/dist/core/__tests__/helpers/broker-clients.d.ts +0 -3
- package/dist/core/__tests__/helpers/broker-clients.js +0 -14
- package/dist/core/__tests__/helpers/harness.d.ts +0 -8
- package/dist/core/__tests__/helpers/harness.js +7 -87
- package/dist/core/__tests__/helpers/viewer-panes.js +1 -1
- package/dist/core/__tests__/inline-memory-refs.test.js +5 -134
- package/dist/core/__tests__/kickoff.test.js +1 -1
- package/dist/core/__tests__/memory-resolver-precedence.test.js +29 -1
- package/dist/core/__tests__/migration.test.js +35 -48
- package/dist/core/__tests__/on-read-dedup-resume.test.js +22 -12
- package/dist/core/__tests__/passive-subscription.test.js +26 -49
- package/dist/core/__tests__/preview-registry-sync.test.js +1 -2
- package/dist/core/__tests__/prune.test.js +94 -1
- package/dist/core/__tests__/review-model-floor.test.js +0 -32
- package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +255 -0
- package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.js +39 -7
- package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.js +30 -29
- package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.js +54 -5
- package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.js +134 -82
- package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.js +15 -4
- package/dist/core/__tests__/{full/idle-release-settlement.test.js → seam/dormancy-release.test.js} +69 -17
- package/dist/core/__tests__/{full/yield-message-transaction.test.js → seam/yield-refresh-transaction.test.js} +31 -9
- package/dist/core/__tests__/secrets.test.js +1 -7
- package/dist/core/__tests__/{broker-fork-seam.test.js → serial/broker-fork-seam.test.js} +58 -7
- package/dist/core/__tests__/{broker-sdk-wiring.test.js → serial/broker-sdk-wiring.test.js} +18 -162
- package/dist/core/__tests__/{broker-snapshot-history.test.js → serial/broker-snapshot-history.test.js} +9 -9
- package/dist/core/__tests__/{command-plugins.test.js → serial/command-plugins.test.js} +105 -15
- package/dist/core/__tests__/{deferred-no-wake.test.js → serial/deferred-no-wake.test.js} +23 -20
- package/dist/core/__tests__/{flagship-lifecycle.test.js → serial/flagship-lifecycle.test.js} +7 -7
- package/dist/core/__tests__/{host-teardown-process-group.test.js → serial/host-teardown-process-group.test.js} +6 -6
- package/dist/core/__tests__/{human-deliver-e2e.test.js → serial/human-deliver-e2e.test.js} +12 -12
- package/dist/core/__tests__/{live-mutation.test.js → serial/live-mutation.test.js} +5 -5
- package/dist/core/__tests__/{refresh-stall-recycle.test.js → serial/refresh-stall-recycle.test.js} +5 -14
- package/dist/core/__tests__/{revive.test.js → serial/revive.test.js} +15 -100
- package/dist/core/__tests__/{spawn-root.test.js → serial/spawn-root.test.js} +32 -15
- package/dist/core/__tests__/serial/subscription-delivery.test.js +126 -0
- package/dist/core/__tests__/serial/tmux-surface.test.js +256 -0
- package/dist/core/__tests__/{worktree.test.js → serial/worktree.test.js} +15 -15
- package/dist/core/__tests__/session-model.test.js +0 -18
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +9 -0
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +50 -0
- package/dist/core/broker-client/__tests__/transport-relay.test.js +15 -16
- package/dist/core/broker-client/__tests__/transport-socket.test.js +28 -0
- package/dist/core/broker-client/client.d.ts +2 -2
- package/dist/core/broker-client/client.js +4 -7
- package/dist/core/broker-client/transport-relay.d.ts +13 -19
- package/dist/core/broker-client/transport-relay.js +19 -34
- package/dist/core/broker-client/transport-socket.d.ts +2 -2
- package/dist/core/broker-client/transport-socket.js +9 -2
- package/dist/core/broker-client/transport.d.ts +7 -3
- package/dist/core/broker-client/transport.js +4 -4
- package/dist/core/canvas/__tests__/attention.test.js +1 -18
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +7 -64
- package/dist/core/canvas/__tests__/remote-transport.test.js +2 -3
- package/dist/core/canvas/__tests__/render-remote.test.js +7 -52
- package/dist/core/canvas/browse/app.d.ts +2 -17
- package/dist/core/canvas/browse/app.js +16 -88
- package/dist/core/canvas/browse/model.d.ts +0 -4
- package/dist/core/canvas/browse/model.js +1 -12
- package/dist/core/canvas/browse/render.js +6 -3
- package/dist/core/canvas/crons.d.ts +8 -5
- package/dist/core/canvas/crons.js +7 -6
- package/dist/core/canvas/extensions.d.ts +7 -3
- package/dist/core/canvas/extensions.js +16 -4
- package/dist/core/canvas/nav-model.d.ts +0 -4
- package/dist/core/canvas/nav-model.js +2 -14
- package/dist/core/canvas/node-order.d.ts +3 -0
- package/dist/core/canvas/node-order.js +12 -0
- package/dist/core/canvas/remote-canvas-source.d.ts +3 -6
- package/dist/core/canvas/remote-canvas-source.js +8 -15
- package/dist/core/command.js +11 -4
- package/dist/core/config.js +3 -1
- package/dist/core/events/envelope.js +1 -1
- package/dist/core/events/read.js +7 -21
- package/dist/core/events/source.d.ts +0 -2
- package/dist/core/events/source.js +0 -16
- package/dist/core/events/types.d.ts +1 -1
- package/dist/core/fault-classifier.d.ts +1 -1
- package/dist/core/feed/passive.d.ts +0 -11
- package/dist/core/feed/passive.js +1 -62
- package/dist/core/human/__tests__/{inbox-core.test.js → serial/inbox-core.test.js} +6 -6
- package/dist/core/keybindings/__tests__/resolve.test.js +1 -96
- package/dist/core/keybindings/background-bash.js +3 -4
- package/dist/core/keybindings/catalog.js +1 -1
- package/dist/core/memory/inline-ref-grammar.js +3 -3
- package/dist/core/memory-resolver.d.ts +11 -25
- package/dist/core/memory-resolver.js +51 -33
- package/dist/core/model-routes.d.ts +15 -0
- package/dist/core/model-routes.js +12 -0
- package/dist/core/pool-credential-store.js +1 -1
- package/dist/core/preview-registry.js +8 -9
- package/dist/core/profiles/__tests__/root-profile.test.js +4 -14
- package/dist/core/review/realize.d.ts +0 -13
- package/dist/core/review/realize.js +0 -40
- package/dist/core/review/types.d.ts +1 -1
- package/dist/core/runtime/__tests__/engine-route.test.js +2 -2
- package/dist/core/runtime/bearings.js +1 -11
- package/dist/core/runtime/broker/client-registry.d.ts +33 -0
- package/dist/core/runtime/broker/client-registry.js +164 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +19 -0
- package/dist/core/runtime/broker/daemon-ops.js +49 -0
- package/dist/core/runtime/broker/event-projection.d.ts +42 -0
- package/dist/core/runtime/broker/event-projection.js +178 -0
- package/dist/core/runtime/broker/fault-retry.d.ts +39 -0
- package/dist/core/runtime/broker/fault-retry.js +277 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +238 -0
- package/dist/core/runtime/broker/frame-dispatch.js +1537 -0
- package/dist/core/runtime/broker/inbox.d.ts +22 -0
- package/dist/core/runtime/broker/inbox.js +167 -0
- package/dist/core/runtime/broker/passive.d.ts +16 -0
- package/dist/core/runtime/broker/passive.js +44 -0
- package/dist/core/runtime/broker/read-ops.d.ts +23 -0
- package/dist/core/runtime/broker/read-ops.js +150 -0
- package/dist/core/runtime/broker/rebind.d.ts +74 -0
- package/dist/core/runtime/broker/rebind.js +211 -0
- package/dist/core/runtime/broker/tool-groups.d.ts +39 -0
- package/dist/core/runtime/broker/tool-groups.js +244 -0
- package/dist/core/runtime/broker-extension-render.d.ts +11 -0
- package/dist/core/runtime/broker-extension-render.js +181 -0
- package/dist/core/runtime/broker-persona-guidance.d.ts +13 -0
- package/dist/core/runtime/broker-persona-guidance.js +58 -0
- package/dist/core/runtime/broker-protocol.d.ts +1 -1
- package/dist/core/runtime/broker-protocol.js +1 -1
- package/dist/core/runtime/broker.d.ts +5 -196
- package/dist/core/runtime/broker.js +276 -3059
- package/dist/core/runtime/canvas-extensions.js +11 -2
- package/dist/core/runtime/fault.js +1 -1
- package/dist/core/runtime/model-selection.d.ts +9 -0
- package/dist/core/runtime/model-selection.js +12 -0
- package/dist/core/runtime/model-swap.d.ts +3 -3
- package/dist/core/runtime/model-swap.js +33 -25
- package/dist/core/runtime/placement-tmux.d.ts +3 -14
- package/dist/core/runtime/placement-tmux.js +11 -80
- package/dist/core/runtime/placement.d.ts +4 -2
- package/dist/core/runtime/placement.js +7 -12
- package/dist/core/runtime/promote.d.ts +14 -1
- package/dist/core/runtime/promote.js +1 -1
- package/dist/core/runtime/reset.d.ts +1 -1
- package/dist/core/runtime/roadmap.js +1 -1
- package/dist/core/runtime/spawn.d.ts +8 -0
- package/dist/core/runtime/spawn.js +36 -6
- package/dist/core/runtime/stop-guard.d.ts +4 -3
- package/dist/core/runtime/stop-guard.js +36 -18
- package/dist/core/runtime/stop-signals.d.ts +4 -1
- package/dist/core/runtime/stream-watchdog.js +4 -5
- package/dist/core/runtime/structured-output.d.ts +15 -2
- package/dist/core/runtime/structured-output.js +35 -12
- package/dist/core/runtime/tmux-bindings.d.ts +25 -0
- package/dist/core/runtime/tmux-bindings.js +682 -0
- package/dist/core/runtime/tmux-chrome.d.ts +2 -1
- package/dist/core/runtime/tmux-chrome.js +3 -2
- package/dist/core/runtime/{tmux.d.ts → tmux-driver.d.ts} +18 -26
- package/dist/core/runtime/tmux-driver.js +598 -0
- package/dist/core/runtime/view-socket.d.ts +14 -0
- package/dist/core/runtime/view-socket.js +83 -0
- package/dist/core/session-model/session-state.d.ts +1 -2
- package/dist/core/session-model/session-state.js +5 -8
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +2 -2
- package/dist/core/substrate/gate.d.ts +1 -1
- package/dist/core/substrate/on-read-pure.d.ts +7 -0
- package/dist/core/substrate/on-read-pure.js +272 -0
- package/dist/core/termrender/termrender.js +32 -2
- package/dist/core/user-settings.d.ts +23 -1
- package/dist/core/user-settings.js +18 -1
- package/dist/daemon/api/__tests__/node-model-validation.test.js +134 -0
- package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +98 -0
- package/dist/daemon/api/__tests__/{full → seam}/api-server.test.js +6 -48
- package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.js +17 -44
- package/dist/daemon/api/__tests__/{full/push-parity.test.js → seam/leaf-api-parity.test.js} +61 -42
- package/dist/daemon/api/bridge.js +40 -81
- package/dist/daemon/api/handlers/broker-ops.d.ts +2 -0
- package/dist/daemon/api/handlers/broker-ops.js +383 -0
- package/dist/daemon/api/handlers/crons.js +9 -4
- package/dist/daemon/api/handlers/files.js +6 -7
- package/dist/daemon/api/handlers/inbox.js +2 -340
- package/dist/daemon/api/handlers/nodes.d.ts +10 -1
- package/dist/daemon/api/handlers/nodes.js +74 -39
- package/dist/daemon/api/handlers/review-comments.js +5 -1
- package/dist/daemon/api/handlers/reviews.js +0 -19
- package/dist/daemon/api/map.js +25 -4
- package/dist/daemon/api/router.js +2 -3
- package/dist/daemon/api/server.js +5 -7
- package/dist/daemon/cron-run.d.ts +4 -0
- package/dist/daemon/cron-run.js +30 -13
- package/dist/daemon/crtrd.d.ts +3 -58
- package/dist/daemon/crtrd.js +28 -1244
- package/dist/daemon/manage.js +0 -1
- package/dist/daemon/reconcilers/broker-supervision.d.ts +50 -0
- package/dist/daemon/reconcilers/broker-supervision.js +549 -0
- package/dist/daemon/reconcilers/cron-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/cron-lane.js +32 -0
- package/dist/daemon/reconcilers/dormant-inbox.d.ts +18 -0
- package/dist/daemon/reconcilers/dormant-inbox.js +143 -0
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +22 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +213 -0
- package/dist/daemon/review/finish.js +2 -5
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +34 -53
- package/dist/pi-extensions/__tests__/canvas-recap.test.js +16 -113
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +72 -99
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +29 -12
- package/dist/pi-extensions/broker-local.d.ts +20 -0
- package/dist/pi-extensions/broker-local.js +89 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -3
- package/dist/pi-extensions/canvas-context-intro.js +22 -11
- package/dist/pi-extensions/canvas-doc-substrate.js +17 -34
- package/dist/pi-extensions/canvas-goal-capture.js +7 -12
- package/dist/pi-extensions/canvas-inbox-watcher.js +124 -139
- package/dist/pi-extensions/canvas-passive-context.d.ts +3 -3
- package/dist/pi-extensions/canvas-passive-context.js +22 -18
- package/dist/pi-extensions/canvas-recap.d.ts +2 -2
- package/dist/pi-extensions/canvas-recap.js +48 -21
- package/dist/pi-extensions/canvas-review-boundary.js +10 -4
- package/dist/pi-extensions/canvas-stophook.d.ts +0 -5
- package/dist/pi-extensions/canvas-stophook.js +149 -431
- package/dist/pi-extensions/canvas-structured-output.js +52 -30
- package/dist/pi-extensions/envelope-merge.d.ts +13 -0
- package/dist/pi-extensions/envelope-merge.js +29 -0
- package/dist/shared/working-activity.d.ts +3 -2
- package/dist/shared/working-activity.js +3 -2
- package/dist/types.d.ts +6 -5
- package/dist/types.js +2 -2
- package/docs/compat/hearth-crtr-v1.md +4 -12
- package/docs/compat/hearth-crtr-v2.md +3 -11
- package/docs/compat/hearth-crtr-v3.md +4 -12
- package/docs/compat/hearth-crtr-v4.md +3 -11
- package/docs/compat/hearth-crtr-v5.md +3 -11
- package/docs/public-api.md +3 -7
- package/package.json +7 -49
- package/runtime.lock.json +2766 -14533
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/claude-plugin-commands.test.ts +0 -49
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +0 -194
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/statusline.test.ts +0 -68
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/test.ts +0 -119
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +0 -439
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +0 -170
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +0 -57
- package/dist/clients/attach/__tests__/editor-frame-title.test.js +0 -19
- package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +0 -32
- package/dist/clients/attach/__tests__/markdown-headings.test.js +0 -35
- package/dist/clients/attach/__tests__/session-identity-refresh.test.js +0 -32
- package/dist/clients/conversation/conversation.d.ts +0 -65
- package/dist/clients/conversation/conversation.js +0 -318
- package/dist/clients/conversation/frames.d.ts +0 -30
- package/dist/clients/conversation/frames.js +0 -52
- package/dist/clients/conversation/region.d.ts +0 -51
- package/dist/clients/conversation/region.js +0 -265
- package/dist/clients/inbox/__tests__/inbox-controller.test.js +0 -247
- package/dist/clients/inbox/__tests__/mount-panel.test.js +0 -776
- package/dist/clients/inbox/review/__tests__/remap.test.js +0 -196
- package/dist/clients/inbox/review/columns.d.ts +0 -22
- package/dist/clients/inbox/review/columns.js +0 -40
- package/dist/clients/inbox/review/composition.d.ts +0 -60
- package/dist/clients/inbox/review/composition.js +0 -278
- package/dist/clients/inbox/review/document-region.d.ts +0 -120
- package/dist/clients/surfaces/region.d.ts +0 -16
- package/dist/clients/web/__tests__/push-engine.test.js +0 -158
- package/dist/clients/web/__tests__/push-registry.test.d.ts +0 -1
- package/dist/clients/web/__tests__/push-registry.test.js +0 -177
- package/dist/clients/web/dev-server.d.ts +0 -7
- package/dist/clients/web/dev-server.js +0 -46
- package/dist/clients/web/events.d.ts +0 -19
- package/dist/clients/web/events.js +0 -163
- package/dist/clients/web/push-engine.d.ts +0 -44
- package/dist/clients/web/push-engine.js +0 -125
- package/dist/clients/web/push-registry.d.ts +0 -85
- package/dist/clients/web/push-registry.js +0 -373
- package/dist/clients/web/server.d.ts +0 -19
- package/dist/clients/web/server.js +0 -661
- package/dist/clients/web/web-client/shared/protocol.d.ts +0 -214
- package/dist/clients/web/web-client/shared/protocol.js +0 -8
- package/dist/clients/web/web-cmd.d.ts +0 -2
- package/dist/clients/web/web-cmd.js +0 -141
- package/dist/commands/__tests__/api-canvas-source.test.d.ts +0 -1
- package/dist/commands/__tests__/api-canvas-source.test.js +0 -37
- package/dist/commands/__tests__/canvas-config.test.d.ts +0 -1
- package/dist/commands/__tests__/canvas-config.test.js +0 -107
- package/dist/commands/__tests__/node-no-kickoff-flag.test.d.ts +0 -1
- package/dist/commands/__tests__/node-no-kickoff-flag.test.js +0 -73
- package/dist/commands/__tests__/node-worktree-flag.test.d.ts +0 -1
- package/dist/commands/__tests__/node-worktree-flag.test.js +0 -49
- package/dist/commands/__tests__/search-contents.test.d.ts +0 -1
- package/dist/commands/__tests__/search-contents.test.js +0 -52
- package/dist/commands/memory/__tests__/lint.test.d.ts +0 -1
- package/dist/commands/memory/__tests__/lint.test.js +0 -15
- package/dist/commands/sys/__tests__/config-broker-thresholds.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/config-broker-thresholds.test.js +0 -95
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/config-keybindings.test.js +0 -55
- package/dist/commands/sys/__tests__/helpers/setup-runtime.d.ts +0 -5
- package/dist/commands/sys/__tests__/helpers/setup-runtime.js +0 -19
- package/dist/commands/sys/__tests__/setup-front-door.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/setup-front-door.test.js +0 -102
- package/dist/commands/sys/__tests__/setup-models.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/setup-models.test.js +0 -205
- package/dist/commands/sys/__tests__/setup-providers-viewport.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/setup-providers-viewport.test.js +0 -155
- package/dist/commands/sys/__tests__/sync-deps.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/sync-deps.test.js +0 -179
- package/dist/commands/sys/__tests__/sync-import.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/sync-import.test.js +0 -315
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +0 -1
- package/dist/core/__tests__/base-worker-prompt.test.js +0 -57
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +0 -1
- package/dist/core/__tests__/command-plugins-surfaces.test.js +0 -359
- package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +0 -1
- package/dist/core/__tests__/connection-reconnect-resume.test.js +0 -36
- package/dist/core/__tests__/finalization-migration.test.d.ts +0 -1
- package/dist/core/__tests__/finalization-migration.test.js +0 -38
- package/dist/core/__tests__/fixtures/extra-extension.d.ts +0 -1
- package/dist/core/__tests__/fixtures/extra-extension.js +0 -3
- package/dist/core/__tests__/fixtures/spawn-proof-target.test.d.ts +0 -1
- package/dist/core/__tests__/fixtures/spawn-proof-target.test.js +0 -15
- package/dist/core/__tests__/fork.test.d.ts +0 -1
- package/dist/core/__tests__/fork.test.js +0 -108
- package/dist/core/__tests__/full/broker-attach-limits.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-attach-limits.test.js +0 -147
- package/dist/core/__tests__/full/broker-deliver-ack.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-deliver-ack.test.js +0 -73
- package/dist/core/__tests__/full/broker-dialogs.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +0 -180
- package/dist/core/__tests__/full/broker-display-snapshot.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-display-snapshot.test.js +0 -75
- package/dist/core/__tests__/full/broker-dormant-wake.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-dormant-wake.test.js +0 -130
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +0 -57
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +0 -80
- package/dist/core/__tests__/full/cascade-close.test.d.ts +0 -1
- package/dist/core/__tests__/full/cascade-close.test.js +0 -240
- package/dist/core/__tests__/full/detach-focus.test.d.ts +0 -1
- package/dist/core/__tests__/full/detach-focus.test.js +0 -126
- package/dist/core/__tests__/full/idle-release-settlement.test.d.ts +0 -1
- package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.d.ts +0 -1
- package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.js +0 -107
- package/dist/core/__tests__/full/park-clock-release.test.d.ts +0 -1
- package/dist/core/__tests__/full/park-clock-release.test.js +0 -56
- package/dist/core/__tests__/full/yield-message-transaction.test.d.ts +0 -1
- package/dist/core/__tests__/memory-index-render.test.d.ts +0 -1
- package/dist/core/__tests__/memory-index-render.test.js +0 -60
- package/dist/core/__tests__/model-routes.test.d.ts +0 -1
- package/dist/core/__tests__/model-routes.test.js +0 -41
- package/dist/core/__tests__/on-read-identity.test.d.ts +0 -1
- package/dist/core/__tests__/on-read-identity.test.js +0 -61
- package/dist/core/__tests__/phase4-review-migration.test.d.ts +0 -1
- package/dist/core/__tests__/phase4-review-migration.test.js +0 -100
- package/dist/core/__tests__/prune-to-limit.test.d.ts +0 -1
- package/dist/core/__tests__/prune-to-limit.test.js +0 -126
- package/dist/core/__tests__/reset.test.d.ts +0 -1
- package/dist/core/__tests__/reset.test.js +0 -69
- package/dist/core/__tests__/spawn-no-kickoff.test.d.ts +0 -1
- package/dist/core/__tests__/spawn-no-kickoff.test.js +0 -115
- package/dist/core/__tests__/steer-note.test.d.ts +0 -1
- package/dist/core/__tests__/steer-note.test.js +0 -41
- package/dist/core/__tests__/subcommand-tier.test.d.ts +0 -1
- package/dist/core/__tests__/subcommand-tier.test.js +0 -99
- package/dist/core/__tests__/subscription-delivery.test.js +0 -211
- package/dist/core/__tests__/test-runner-cheap-sharding.test.d.ts +0 -1
- package/dist/core/__tests__/test-runner-cheap-sharding.test.js +0 -97
- package/dist/core/__tests__/tmux-surface.test.js +0 -484
- package/dist/core/__tests__/wake-bearings.test.d.ts +0 -1
- package/dist/core/__tests__/wake-bearings.test.js +0 -102
- package/dist/core/canvas/__tests__/remote-event-stream.test.d.ts +0 -1
- package/dist/core/canvas/__tests__/remote-event-stream.test.js +0 -146
- package/dist/core/canvas/__tests__/source-resolve.test.d.ts +0 -1
- package/dist/core/canvas/__tests__/source-resolve.test.js +0 -77
- package/dist/core/canvas/browse/__tests__/model.test.d.ts +0 -1
- package/dist/core/canvas/browse/__tests__/model.test.js +0 -290
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.d.ts +0 -1
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +0 -134
- package/dist/core/canvas/browse/__tests__/render.test.d.ts +0 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +0 -111
- package/dist/core/canvas/browse/rebuild-coalescer.d.ts +0 -7
- package/dist/core/canvas/browse/rebuild-coalescer.js +0 -61
- package/dist/core/canvas/remote-event-stream.d.ts +0 -24
- package/dist/core/canvas/remote-event-stream.js +0 -94
- package/dist/core/feed/__tests__/coalesce-human.test.d.ts +0 -1
- package/dist/core/feed/__tests__/coalesce-human.test.js +0 -36
- package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +0 -1
- package/dist/core/feed/__tests__/empty-node-guard.test.js +0 -45
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +0 -1
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +0 -84
- package/dist/core/human/__tests__/visible-paths.test.d.ts +0 -1
- package/dist/core/human/__tests__/visible-paths.test.js +0 -28
- package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +0 -1
- package/dist/core/keybindings/__tests__/inbox-affordance.test.js +0 -38
- package/dist/core/review/__tests__/project.test.d.ts +0 -1
- package/dist/core/review/__tests__/project.test.js +0 -71
- package/dist/core/runtime/__tests__/command-expansion.test.d.ts +0 -1
- package/dist/core/runtime/__tests__/command-expansion.test.js +0 -25
- package/dist/core/runtime/__tests__/session-visibility.test.d.ts +0 -1
- package/dist/core/runtime/__tests__/session-visibility.test.js +0 -119
- package/dist/core/runtime/__tests__/spawn-at-given-id.test.d.ts +0 -1
- package/dist/core/runtime/__tests__/spawn-at-given-id.test.js +0 -34
- package/dist/core/runtime/tmux.js +0 -1314
- package/dist/daemon/api/__tests__/full/push-parity.test.d.ts +0 -1
- package/dist/daemon/api/__tests__/full/worktree-parity.test.d.ts +0 -1
- package/dist/daemon/api/__tests__/full/worktree-parity.test.js +0 -160
- package/dist/daemon/api/__tests__/inbox.test.d.ts +0 -1
- package/dist/daemon/api/__tests__/inbox.test.js +0 -489
- package/dist/web-client/apple-touch-icon.png +0 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
- package/dist/web-client/assets/index-CCPaLeW7.css +0 -2
- package/dist/web-client/assets/index-kyOWsnZK.js +0 -86
- package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
- package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +0 -2
- package/dist/web-client/icon-192.png +0 -0
- package/dist/web-client/icon-512.png +0 -0
- package/dist/web-client/icon-maskable-512.png +0 -0
- package/dist/web-client/index.html +0 -17
- package/dist/web-client/manifest.webmanifest +0 -1
- package/dist/web-client/sw.js +0 -1
- /package/dist/api/__tests__/{client.test.d.ts → serial/client.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{inbox-controller.test.d.ts → serial/inbox-controller.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{mount-panel.test.d.ts → serial/mount-panel.test.d.ts} +0 -0
- /package/dist/clients/{attach/__tests__/attach-chrome-remote.test.d.ts → inbox/review/__tests__/teardown-notice.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/attach-keybindings.test.d.ts → core/__tests__/broker-extension-canvas-db-boundary.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/crtr-output-coverage.test.d.ts → core/__tests__/seam/broker-attach-multiclient.test.d.ts} +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.d.ts +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.d.ts +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.d.ts +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.d.ts +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.d.ts +0 -0
- /package/dist/{clients/attach/__tests__/editor-frame-title.test.d.ts → core/__tests__/seam/dormancy-release.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/input-controller-extension-command.test.d.ts → core/__tests__/seam/yield-refresh-transaction.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-fork-seam.test.d.ts → serial/broker-fork-seam.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-sdk-wiring.test.d.ts → serial/broker-sdk-wiring.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-snapshot-history.test.d.ts → serial/broker-snapshot-history.test.d.ts} +0 -0
- /package/dist/core/__tests__/{command-plugins.test.d.ts → serial/command-plugins.test.d.ts} +0 -0
- /package/dist/core/__tests__/{deferred-no-wake.test.d.ts → serial/deferred-no-wake.test.d.ts} +0 -0
- /package/dist/core/__tests__/{flagship-lifecycle.test.d.ts → serial/flagship-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{host-teardown-process-group.test.d.ts → serial/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-deliver-e2e.test.d.ts → serial/human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{live-mutation.test.d.ts → serial/live-mutation.test.d.ts} +0 -0
- /package/dist/core/__tests__/{refresh-stall-recycle.test.d.ts → serial/refresh-stall-recycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{revive.test.d.ts → serial/revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{spawn-root.test.d.ts → serial/spawn-root.test.d.ts} +0 -0
- /package/dist/core/__tests__/{subscription-delivery.test.d.ts → serial/subscription-delivery.test.d.ts} +0 -0
- /package/dist/core/__tests__/{tmux-surface.test.d.ts → serial/tmux-surface.test.d.ts} +0 -0
- /package/dist/core/__tests__/{worktree.test.d.ts → serial/worktree.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/markdown-headings.test.d.ts → core/__tests__/watchdog-abort-arms-retry.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/session-identity-refresh.test.d.ts → core/broker-client/__tests__/transport-socket.test.d.ts} +0 -0
- /package/dist/core/human/__tests__/{inbox-core.test.d.ts → serial/inbox-core.test.d.ts} +0 -0
- /package/dist/{clients/inbox/review/__tests__/remap.test.d.ts → daemon/api/__tests__/node-model-validation.test.d.ts} +0 -0
- /package/dist/{clients/surfaces/region.js → daemon/api/__tests__/review-terminal-conflict.test.d.ts} +0 -0
- /package/dist/daemon/api/__tests__/{full → seam}/api-server.test.d.ts +0 -0
- /package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.d.ts +0 -0
- /package/dist/{clients/web/__tests__/push-engine.test.d.ts → daemon/api/__tests__/seam/leaf-api-parity.test.d.ts} +0 -0
package/dist/daemon/crtrd.js
CHANGED
|
@@ -43,33 +43,22 @@
|
|
|
43
43
|
// scoped by CRTR_PIDFILE in managed deployments and crtrHome()/crtrd.pid
|
|
44
44
|
// otherwise. Shutdown quiesces admitted work, removes only this invocation's
|
|
45
45
|
// mirror, and then releases its exact claim.
|
|
46
|
-
import { writeFileSync, readFileSync, rmSync, mkdirSync,
|
|
47
|
-
import { dirname
|
|
48
|
-
import { homedir } from 'node:os';
|
|
49
|
-
import { execFile } from 'node:child_process';
|
|
46
|
+
import { writeFileSync, readFileSync, rmSync, mkdirSync, } from 'node:fs';
|
|
47
|
+
import { dirname } from 'node:path';
|
|
50
48
|
import { randomBytes } from 'node:crypto';
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import { listNodes, getRow, getNode, migrateLegacyPidIdentities, listFocuses, closeFocusRow, pruneToLimit, subscribersOf, } from '../core/canvas/index.js';
|
|
54
|
-
import { fullName } from '../core/canvas/labels.js';
|
|
55
|
-
import { transition } from '../core/runtime/lifecycle.js';
|
|
56
|
-
import { isBusy, busySince } from '../core/runtime/busy.js';
|
|
57
|
-
import { FAULT_QUIET_MS, readFault, recordFault } from '../core/runtime/fault.js';
|
|
58
|
-
import { fanDoctrineWake } from '../core/runtime/close.js';
|
|
49
|
+
import { crtrHome, isSafeNodeId } from '../core/canvas/paths.js';
|
|
50
|
+
import { listNodes, migrateLegacyPidIdentities, } from '../core/canvas/index.js';
|
|
59
51
|
import { createApiServer } from './api/server.js';
|
|
60
52
|
import { reconcileUndeliveredTickets } from './human/sweep.js';
|
|
61
53
|
import { sweepReviews } from './review/sweep.js';
|
|
62
|
-
// Lean pidfile/liveness helpers now live in `pidfile.ts` (db-free, so the CLI
|
|
63
|
-
// daemon front door can import them without reaching openDb). Imported here for
|
|
64
|
-
// internal use and re-exported below to preserve the historical crtrd.ts
|
|
65
|
-
// surface (tests, `commands/sys/daemon.ts`).
|
|
66
54
|
import { pidfilePath } from './pidfile.js';
|
|
67
|
-
export { readPidfile, isDaemonRunning } from './pidfile.js';
|
|
68
55
|
import { rendererWarning } from '../core/runtime/package-health.js';
|
|
69
56
|
import { DEFAULT_INTERVAL_MS } from './supervise-cadence.js';
|
|
70
|
-
import {
|
|
71
|
-
|
|
72
|
-
import {
|
|
57
|
+
import { BrokerSupervisionReconciler } from './reconcilers/broker-supervision.js';
|
|
58
|
+
import { ControllerDeathReconciler, DormantInboxReconciler } from './reconcilers/dormant-inbox.js';
|
|
59
|
+
import { CronLaneReconciler } from './reconcilers/cron-lane.js';
|
|
60
|
+
import { StorageMaintenanceReconciler } from './reconcilers/storage-maintenance.js';
|
|
61
|
+
import { captureLivenessSnapshot, capturePidCommand, captureTeardownSnapshot, isPidAlive, killProcessTreePids, } from '../core/canvas/pid.js';
|
|
73
62
|
import { bindFleet, boundFleet } from '../core/runtime/fleet.js';
|
|
74
63
|
import { bindDaemonControl, clearDaemonControl } from './control.js';
|
|
75
64
|
import { spawnDaemon } from './manage.js';
|
|
@@ -77,114 +66,12 @@ import { reconcileAndMigrateBoot } from '../core/canvas/boot.js';
|
|
|
77
66
|
import { resolveInstallId } from '../core/canvas/install-id.js';
|
|
78
67
|
import { setInstallId } from '../core/runtime/nodes.js';
|
|
79
68
|
import { claimDaemonOwnership, releaseDaemonOwnership } from '../core/canvas/daemon-owner.js';
|
|
80
|
-
import { listLivePanes, tearDownNode } from '../core/runtime/placement.js';
|
|
81
|
-
import { reviveNode } from '../core/runtime/revive.js';
|
|
82
69
|
import { prewarmRecentRecipes, reapStaleSpares } from '../core/runtime/warm-pool.js';
|
|
83
|
-
import {
|
|
84
|
-
import { reloadAuthLive } from '../core/runtime/auth-reload.js';
|
|
85
|
-
import { DaemonFleet, retryResumeMode } from './fleet.js';
|
|
86
|
-
export { retryResumeMode };
|
|
87
|
-
import { appendControllerDeathNotice, readInboxSince, readCursor, } from '../core/feed/inbox.js';
|
|
70
|
+
import { DaemonFleet } from './fleet.js';
|
|
88
71
|
import { migrateLegacyInboxIdentities } from '../core/feed/inbox-migrate.js';
|
|
89
72
|
import { emitEvent } from '../core/events/emit.js';
|
|
90
73
|
import { operationIdContext } from '../core/events/operation-id.js';
|
|
91
74
|
import { bindDaemonEventSource } from '../core/events/source.js';
|
|
92
|
-
import { recoverStaleCronLeases, runDueCrons } from './cron-run.js';
|
|
93
|
-
// Perf (freeze diagnosis, 2026-07-12): every `ps`/`tmux` shell-out on the
|
|
94
|
-
// reconcile tick used to go through `spawnSync`, which blocks the WHOLE event
|
|
95
|
-
// loop for the life of the child process. With a large fleet of brokers (100+
|
|
96
|
-
// observed), these synchronous spawns stacked up and froze the daemon solid —
|
|
97
|
-
// no other tick, inbox poll, or IPC could run while one was blocked on
|
|
98
|
-
// `posix_spawn`. `execFileAsync` (promisified `execFile`) runs the child via
|
|
99
|
-
// libuv without blocking the main thread; every call site below that used to
|
|
100
|
-
// `spawnSync` now awaits this instead, with identical inputs/outputs (same
|
|
101
|
-
// `encoding`/`timeout`, same decisions made from stdout) — only the blocking
|
|
102
|
-
// behavior changes.
|
|
103
|
-
const execFileAsync = promisify(execFile);
|
|
104
|
-
// Count-based auto-prune of the canvas (pruneToLimit). The canvas grows
|
|
105
|
-
// unbounded — the TTL `crtr canvas prune` is manual, so without this a long-lived
|
|
106
|
-
// daemon's canvas accumulates thousands of terminal nodes, making every scan +
|
|
107
|
-
// `canvas snapshot` expensive. Keep the AUTO_PRUNE_MAX most-recently-active nodes
|
|
108
|
-
// (live nodes are NEVER deleted, so the real floor is max(AUTO_PRUNE_MAX,
|
|
109
|
-
// liveCount)). It's a DB sweep, not per-tick work — throttled to PRUNE_INTERVAL_MS.
|
|
110
|
-
const AUTO_PRUNE_MAX = 2000;
|
|
111
|
-
const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
112
|
-
// Last auto-prune wall-clock. -Infinity so the FIRST tick after start runs it
|
|
113
|
-
// (the canvas may already be huge at boot); thereafter every PRUNE_INTERVAL_MS.
|
|
114
|
-
// In-memory only — a daemon restart re-runs it once, which is harmless.
|
|
115
|
-
let lastPruneAt = Number.NEGATIVE_INFINITY;
|
|
116
|
-
// Warm-pool GC (reapStaleSpares). Unclaimed spares are hidden from every
|
|
117
|
-
// listing, so NOTHING else collects them: the recovery sweep never resumes one
|
|
118
|
-
// (a daemon handover therefore leaves its broker dead and its row parked
|
|
119
|
-
// forever) and the count-based prune cannot even see it. Each carries a
|
|
120
|
-
// ~150-300 MB resident broker, so the pool must collect its own garbage. Not
|
|
121
|
-
// per-tick work — the sweep pid-probes each spare, so it is throttled.
|
|
122
|
-
const SPARE_SWEEP_INTERVAL_MS = 60 * 1000;
|
|
123
|
-
let lastSpareSweepAt = Number.NEGATIVE_INFINITY;
|
|
124
|
-
// Idle-release/inbox retry cap (2026-07-10 diagnosis, round 2): the SAME
|
|
125
|
-
// livelock shape as the stranded-relaunch case above, but on the SEPARATE
|
|
126
|
-
// pass-2 path that resumes a dormant idle-released node the instant its inbox
|
|
127
|
-
// gains an unseen entry. That path called reviveNode() unconditionally every
|
|
128
|
-
// poll with no grace, no backoff, and no attempt cap.
|
|
129
|
-
//
|
|
130
|
-
// ROUND 1 keyed the retry counter on row-state residency (pid liveness),
|
|
131
|
-
// cleared the instant the pid-alive branch observed a live engine. That
|
|
132
|
-
// caught a broker that never boots at all — but the RECURRING incident's
|
|
133
|
-
// actual failure is TURN-level, not launch-level: the revive succeeds (broker
|
|
134
|
-
// launches, records a pid — clearing the round-1 counter), the turn then
|
|
135
|
-
// faults against the provider, the engine exits with the inbox CURSOR
|
|
136
|
-
// UNADVANCED, the node re-enters idle/idle-release, pass-2 sees the same
|
|
137
|
-
// unseen entries again, and — because the counter was just cleared by the
|
|
138
|
-
// transiently-alive engine — starts over at attempt 1/6 every single cycle.
|
|
139
|
-
// crtrd.err showed exactly this: hundreds of `attempt 1/6` lines, never
|
|
140
|
-
// `2/6`, interleaved with `fatal-fault <id> (other, pi→provider)`.
|
|
141
|
-
//
|
|
142
|
-
// ROUND 2 fix: key the counter on INBOX-CURSOR PROGRESS, not row-state
|
|
143
|
-
// residency or pid liveness. `readCursor(id)` is recorded as the state's
|
|
144
|
-
// baseline the first time a stuck delivery is observed; a transiently-alive
|
|
145
|
-
// engine does NOT reset it (nothing clears this state on liveness — the
|
|
146
|
-
// fleet's exit-policy jobs never touch it). Only a different exact cursor identity on
|
|
147
|
-
// the NEXT observation — proof the wake delivered something — resets the
|
|
148
|
-
// counter (via `cursorChanged` below); an unchanged identity is the SAME stuck
|
|
149
|
-
// delivery and increments/backs off exactly like the stranded-relaunch path.
|
|
150
|
-
const IDLE_RELEASE_RELAUNCH_MAX_ATTEMPTS = 6;
|
|
151
|
-
const IDLE_RELEASE_RELAUNCH_MAX_BACKOFF_MS = 10 * 60_000; // 10 minutes
|
|
152
|
-
// Backoff base for the idle-release retry ladder (kept at the value the old
|
|
153
|
-
// boot-grace clock supplied, so the ladder's math is unchanged).
|
|
154
|
-
const IDLE_RELEASE_RELAUNCH_BASE_MS = 20_000;
|
|
155
|
-
const idleReleaseRelaunchState = new Map();
|
|
156
|
-
function idleReleaseRelaunchBackoffMs(attempts) {
|
|
157
|
-
return Math.min(IDLE_RELEASE_RELAUNCH_BASE_MS * 2 ** attempts, IDLE_RELEASE_RELAUNCH_MAX_BACKOFF_MS);
|
|
158
|
-
}
|
|
159
|
-
/** Cleared ONLY when the node row itself is gone, or once the counter gives
|
|
160
|
-
* up and terminalizes the node — NEVER on a transiently-alive engine (that
|
|
161
|
-
* was round 1's bug) and never on a bare idle-release status flap (the
|
|
162
|
-
* fault-recovery cycle flaps status EVERY episode, so clearing on that would
|
|
163
|
-
* reproduce the exact same livelock one level up). */
|
|
164
|
-
function clearIdleReleaseRelaunchState(id) {
|
|
165
|
-
idleReleaseRelaunchState.delete(id);
|
|
166
|
-
}
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
// Broker-pressure thresholds bound daemon-initiated revives only. Explicit
|
|
169
|
-
// `node lifecycle revive` and `surface node focus` calls bypass this daemon sweep.
|
|
170
|
-
// User config is the durable host policy; environment variables are explicit
|
|
171
|
-
// process-level overrides for managed deployments.
|
|
172
|
-
function configuredPositiveInteger(name, fallback) {
|
|
173
|
-
const raw = process.env[name];
|
|
174
|
-
if (raw === undefined || raw === '')
|
|
175
|
-
return fallback;
|
|
176
|
-
const n = Number(raw);
|
|
177
|
-
return Number.isSafeInteger(n) && n >= 1 ? n : fallback;
|
|
178
|
-
}
|
|
179
|
-
export function brokerThresholdsForDaemon() {
|
|
180
|
-
const configured = readConfig('user').brokerThresholds;
|
|
181
|
-
const automaticReviveCap = configuredPositiveInteger('CRTR_MAX_LIVE_BROKERS', configured.automaticReviveCap);
|
|
182
|
-
const warning = configuredPositiveInteger('CRTR_WARN_LIVE_BROKERS', configured.warning);
|
|
183
|
-
// Independent environment overrides can invert the configured pair. Clamp
|
|
184
|
-
// the warning to the cap so broker pressure is always visible before revive
|
|
185
|
-
// deferral begins.
|
|
186
|
-
return { warning: Math.min(warning, automaticReviveCap), automaticReviveCap };
|
|
187
|
-
}
|
|
188
75
|
// A broker DECLARES its owning canvas in argv at spawn (host.ts launch:
|
|
189
76
|
// `broker-cli.js --canvas-home <home> --epoch <id> <nodeId>`), so ownership is
|
|
190
77
|
// an exact match on this daemon's own canvas home — no path inference. An
|
|
@@ -355,18 +242,11 @@ export async function runStartupRecoverySweep(fleet) {
|
|
|
355
242
|
emitEvent({ level: 'error', event: 'daemon.recovery_sweep.failed', error: err });
|
|
356
243
|
return;
|
|
357
244
|
}
|
|
358
|
-
const thresholds = brokerThresholdsForDaemon();
|
|
359
245
|
for (const row of rows) {
|
|
360
246
|
if (row.kind === 'human')
|
|
361
247
|
continue; // human bridge rows — never broker-hosted
|
|
362
|
-
if (
|
|
363
|
-
emitEvent({
|
|
364
|
-
level: 'warn',
|
|
365
|
-
event: 'daemon.recovery_sweep.capacity_deferred',
|
|
366
|
-
fields: { live_count: fleet.size() },
|
|
367
|
-
});
|
|
248
|
+
if (!brokerSupervision.hasStartupRecoveryCapacity(fleet))
|
|
368
249
|
break;
|
|
369
|
-
}
|
|
370
250
|
await operationIdContext.fresh(async () => {
|
|
371
251
|
try {
|
|
372
252
|
await fleet.recoverDeadNode(row.node_id);
|
|
@@ -386,830 +266,6 @@ export async function runStartupRecoverySweep(fleet) {
|
|
|
386
266
|
});
|
|
387
267
|
}
|
|
388
268
|
}
|
|
389
|
-
// Broker-pressure warnings are surfaced at the lower threshold so a growing
|
|
390
|
-
// census is visible before automatic revives are deferred. A canonical event
|
|
391
|
-
// records the condition and a throttled tmux notice reaches attached clients.
|
|
392
|
-
const OVER_CAP_WARN_THROTTLE_MS = 5 * 60_000;
|
|
393
|
-
let lastOverCapWarnAt = Number.NEGATIVE_INFINITY;
|
|
394
|
-
async function warnOverCapInTmux(count, warning, max) {
|
|
395
|
-
const now = Date.now();
|
|
396
|
-
if (now - lastOverCapWarnAt < OVER_CAP_WARN_THROTTLE_MS)
|
|
397
|
-
return;
|
|
398
|
-
lastOverCapWarnAt = now;
|
|
399
|
-
emitEvent({
|
|
400
|
-
level: 'warn',
|
|
401
|
-
event: 'broker.capacity.warning',
|
|
402
|
-
fields: { live_count: count, warning_threshold: warning, hard_cap: max },
|
|
403
|
-
});
|
|
404
|
-
const msg = `⚠ crtrd: ${count} live brokers (warning ${warning}; hard cap ${max}) — possible broker leak. ps -ax | grep broker-cli.js`;
|
|
405
|
-
try {
|
|
406
|
-
const clients = await execFileAsync('tmux', ['list-clients', '-F', '#{client_name}'], {
|
|
407
|
-
encoding: 'utf8',
|
|
408
|
-
timeout: 2000,
|
|
409
|
-
});
|
|
410
|
-
if (typeof clients.stdout !== 'string')
|
|
411
|
-
return;
|
|
412
|
-
// Fan out per-client notification — each client's primary-then-fallback
|
|
413
|
-
// display-message sequence still runs in order, but N stale clients no
|
|
414
|
-
// longer serialize into ~4s × N of teardown-owned latency.
|
|
415
|
-
await Promise.allSettled(clients.stdout.split('\n').map(async (client) => {
|
|
416
|
-
if (client === '')
|
|
417
|
-
return;
|
|
418
|
-
// -d 10000 holds the message for 10s (tmux ≥3.2); if this tmux
|
|
419
|
-
// predates -d, fall back to the default display-time rather than not
|
|
420
|
-
// warning.
|
|
421
|
-
try {
|
|
422
|
-
await execFileAsync('tmux', ['display-message', '-d', '10000', '-c', client, msg], {
|
|
423
|
-
encoding: 'utf8',
|
|
424
|
-
timeout: 2000,
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
try {
|
|
429
|
-
await execFileAsync('tmux', ['display-message', '-c', client, msg], {
|
|
430
|
-
encoding: 'utf8',
|
|
431
|
-
timeout: 2000,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
catch {
|
|
435
|
-
/* both display-message variants failed for this client — skip it */
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}));
|
|
439
|
-
}
|
|
440
|
-
catch {
|
|
441
|
-
/* no tmux server — the canonical event above still records the condition */
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// Throttle for the canonical "at capacity" event: without it, every deferred
|
|
445
|
-
// revive on every 2s poll would flood the daemon event stream as badly as the
|
|
446
|
-
// livelock this cap exists to prevent.
|
|
447
|
-
const CAPACITY_LOG_THROTTLE_MS = 30_000;
|
|
448
|
-
let lastCapacityLogAt = Number.NEGATIVE_INFINITY;
|
|
449
|
-
/** Capacity for a TICK-initiated launch (pass-2 inbox wakes + due crons whose
|
|
450
|
-
* run launches or revives a node): the live count is EXACTLY the fleet size
|
|
451
|
-
* (design D-7) — every live broker is a fleet entry, no census or row inference. Checked BEFORE
|
|
452
|
-
* any attempt/backoff state is mutated at each call site, so a capacity-
|
|
453
|
-
* deferred revive costs a node NOTHING against its retry budget — it's
|
|
454
|
-
* retried plainly next poll once capacity frees up. Exit-driven 1:1 respawns
|
|
455
|
-
* are cap-exempt by construction: the dying broker freed its slot at exit. */
|
|
456
|
-
function hasBrokerCapacity(max) {
|
|
457
|
-
if (boundFleet().size() < max)
|
|
458
|
-
return true;
|
|
459
|
-
const now = Date.now();
|
|
460
|
-
if (now - lastCapacityLogAt >= CAPACITY_LOG_THROTTLE_MS) {
|
|
461
|
-
lastCapacityLogAt = now;
|
|
462
|
-
emitEvent({
|
|
463
|
-
level: 'warn',
|
|
464
|
-
event: 'broker.capacity.revive_deferred',
|
|
465
|
-
fields: { live_count: boundFleet().size(), hard_cap: max },
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
|
-
// §H refresh-authority grace: how long a node may sit with intent='refresh', its
|
|
471
|
-
// turn OVER (busy marker absent) and its engine still ALIVE before the daemon
|
|
472
|
-
// concludes the refresh stalled and force-kills the engine. The healthy window is
|
|
473
|
-
// seconds: a sound stophook calls shutdown() the instant the refresh turn ends;
|
|
474
|
-
// the resulting broker exit is classified as respawn-fresh. Minutes of this
|
|
475
|
-
// live-fleet state means the engine never exited — either shutdown hung
|
|
476
|
-
// (observed at ~200k-token contexts) OR a STALE in-process stophook (pi
|
|
477
|
-
// extensions never reload, so a days-old resident carries an old hook that
|
|
478
|
-
// ignores intent='refresh' at stop) never called shutdown at all. Nothing else
|
|
479
|
-
// will emit the exit event, so the daemon is the authority: kill the engine here
|
|
480
|
-
// and let its exit-policy job revive fresh.
|
|
481
|
-
export const YIELD_STALL_GRACE_MS = 3 * 60_000;
|
|
482
|
-
// After a yield-stall SIGTERM, how long the pi gets to die gracefully before
|
|
483
|
-
// escalating to SIGKILL (a pi hung at context overflow may not run handlers).
|
|
484
|
-
const KILL_ESCALATE_MS = 20_000;
|
|
485
|
-
// Per-node yield-stall clocks (audit 2026-06-09, Bug 1). In-memory — a daemon
|
|
486
|
-
// restart resets them (worst case: one extra grace).
|
|
487
|
-
const yieldStallSince = new Map();
|
|
488
|
-
const yieldTermAt = new Map();
|
|
489
|
-
/** Refresh-stall decision (§H refresh-authority; audit 2026-06-09, Bug 1 — node
|
|
490
|
-
* mq5v9hfa-74493d45 and siblings; stuck-refresh — parent mq7s5o93): a `node
|
|
491
|
-
* yield` / refresh records intent='refresh' and relies on the in-process
|
|
492
|
-
* stophook to call shutdown at stop, but the engine can be left ALIVE — either
|
|
493
|
-
* it HANGS instead of exiting (very large contexts) or a STALE stophook (pi
|
|
494
|
-
* extensions never reload) ignores intent='refresh' entirely and never calls
|
|
495
|
-
* shutdown. Either way: engine alive, turn over, intent pending forever. The
|
|
496
|
-
* daemon only revives DEAD engines, so without this verdict the node is
|
|
497
|
-
* permanently stuck (no revive, no wake). Pure core, mirroring wedgeVerdict;
|
|
498
|
-
* the kill side effects live in handleYieldStall.
|
|
499
|
-
*
|
|
500
|
-
* 'kill' ONLY when ALL of: engine alive, intent='refresh', the turn is over
|
|
501
|
-
* (busy marker absent — a node may legitimately keep working for many minutes
|
|
502
|
-
* after running `node yield` mid-turn, and a working engine must never be
|
|
503
|
-
* killed), and the state has persisted past YIELD_STALL_GRACE_MS. */
|
|
504
|
-
export function yieldStallVerdict(piPidAlive, intent, busy, stalledFor) {
|
|
505
|
-
if (piPidAlive !== true)
|
|
506
|
-
return 'leave';
|
|
507
|
-
if (intent !== 'refresh')
|
|
508
|
-
return 'leave';
|
|
509
|
-
if (busy)
|
|
510
|
-
return 'leave';
|
|
511
|
-
if (stalledFor === null || stalledFor < YIELD_STALL_GRACE_MS)
|
|
512
|
-
return 'pending';
|
|
513
|
-
return 'kill';
|
|
514
|
-
}
|
|
515
|
-
/** Enact a refresh-stall (§H refresh-authority): clock the stalled state, then
|
|
516
|
-
* SIGTERM the hung engine (escalating to SIGKILL if it survives
|
|
517
|
-
* KILL_ESCALATE_MS — an engine hung at context overflow, or a stale hook that
|
|
518
|
-
* never wired shutdown, may never run its handlers, so a graceful frame can't be
|
|
519
|
-
* trusted; the signal escalation is what guarantees the engine actually dies).
|
|
520
|
-
* Once the engine is dead the ORDINARY machinery finishes the job: the kill
|
|
521
|
-
* lands as a real exit event whose policy job classifies intent='refresh' as
|
|
522
|
-
* respawn-fresh, i.e. the fresh roadmap-revive the yield asked for. Safe
|
|
523
|
-
* against a graceful shutdown racing us: markCleanExitDone no-ops while intent
|
|
524
|
-
* is pending, so a dying engine can't mis-finalize the node. */
|
|
525
|
-
function handleYieldStall(row, pid, now) {
|
|
526
|
-
const id = row.node_id;
|
|
527
|
-
if (row.intent !== 'refresh' || isBusy(id)) {
|
|
528
|
-
yieldStallSince.delete(id);
|
|
529
|
-
yieldTermAt.delete(id);
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
const since = yieldStallSince.get(id);
|
|
533
|
-
if (since === undefined) {
|
|
534
|
-
yieldStallSince.set(id, now);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
if (yieldStallVerdict(true, row.intent, false, now - since) !== 'kill')
|
|
538
|
-
return;
|
|
539
|
-
// The tick snapshot is observation data, not signal authorization: recheck
|
|
540
|
-
// immediately before acting so PID reuse during a long tick cannot target a
|
|
541
|
-
// process that never belonged to this node.
|
|
542
|
-
if (row.pi_pid_identity == null || recordedPidLiveness(pid, row.pi_pid_identity) !== 'alive')
|
|
543
|
-
return;
|
|
544
|
-
const termed = yieldTermAt.get(id);
|
|
545
|
-
if (termed === undefined) {
|
|
546
|
-
emitEvent({
|
|
547
|
-
level: 'warn',
|
|
548
|
-
event: 'broker.yield_stall.sigterm',
|
|
549
|
-
...(isSafeNodeId(id) ? { node_id: id } : {}),
|
|
550
|
-
fields: {
|
|
551
|
-
...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
|
|
552
|
-
pid,
|
|
553
|
-
elapsed_ms: now - since,
|
|
554
|
-
grace_ms: YIELD_STALL_GRACE_MS,
|
|
555
|
-
},
|
|
556
|
-
});
|
|
557
|
-
try {
|
|
558
|
-
process.kill(pid, 'SIGTERM');
|
|
559
|
-
}
|
|
560
|
-
catch {
|
|
561
|
-
/* already gone */
|
|
562
|
-
}
|
|
563
|
-
yieldTermAt.set(id, now);
|
|
564
|
-
}
|
|
565
|
-
else if (now - termed >= KILL_ESCALATE_MS) {
|
|
566
|
-
emitEvent({
|
|
567
|
-
level: 'warn',
|
|
568
|
-
event: 'broker.yield_stall.sigkill',
|
|
569
|
-
...(isSafeNodeId(id) ? { node_id: id } : {}),
|
|
570
|
-
fields: {
|
|
571
|
-
...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
|
|
572
|
-
pid,
|
|
573
|
-
elapsed_ms: now - termed,
|
|
574
|
-
grace_ms: KILL_ESCALATE_MS,
|
|
575
|
-
stall_elapsed_ms: now - since,
|
|
576
|
-
},
|
|
577
|
-
});
|
|
578
|
-
try {
|
|
579
|
-
process.kill(pid, 'SIGKILL');
|
|
580
|
-
}
|
|
581
|
-
catch {
|
|
582
|
-
/* already gone */
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
// ---------------------------------------------------------------------------
|
|
587
|
-
// Wedge detection (issue #110): a broker whose pid is alive but whose engine is
|
|
588
|
-
// frozen MID-TURN (a runaway subprocess, a hung stream) is invisible to every
|
|
589
|
-
// other guarantee — the pid-alive branch above returns early, yield-stall only
|
|
590
|
-
// fires on intent='refresh', and provider-fault recovery needs a completed
|
|
591
|
-
// ERROR turn. A node in this state never pushes, so its parent waits forever
|
|
592
|
-
// with no wake and no recorded fault marker — there's just silence.
|
|
593
|
-
//
|
|
594
|
-
// Detection always NOTIFIES (fans a doctrine wake + records a `daemon→node`/
|
|
595
|
-
// 'wedged' fault so the node shows ⌘ “wedged · needs you” on canvas graph
|
|
596
|
-
// views). Recovery
|
|
597
|
-
// then branches on whether the broker's process TREE has a descendant:
|
|
598
|
-
// • descendants present → a runaway SUBPROCESS is the likely cause; the
|
|
599
|
-
// daemon does NOT touch the engine — kill the subprocess (not the node);
|
|
600
|
-
// see `crtr memory read wedged-child-on-runaway-bash`.
|
|
601
|
-
// • no descendants (the broker pid stands alone — e.g. the engine itself
|
|
602
|
-
// stalled after a model_change, with nothing to kill) → the daemon
|
|
603
|
-
// performs the ONLY remaining remediation itself: SIGTERM the broker, the
|
|
604
|
-
// same on-demand kick `node lifecycle revive --now` does — the SIGTERM
|
|
605
|
-
// gives Pi a bounded chance to persist an aborted turn, then the exit-policy
|
|
606
|
-
// job either resumes that certified session with a continuation or starts
|
|
607
|
-
// the existing dirty recovery cycle.
|
|
608
|
-
//
|
|
609
|
-
// Two corroborating signals, both generous, so a routine long-running bash/
|
|
610
|
-
// build/test tool call is never mistaken for a wedge:
|
|
611
|
-
// 1. quietForMs — ms since the busy marker's mtime last moved. canvas-
|
|
612
|
-
// stophook re-touches that marker (`touchBusy`) on every engine-level sign
|
|
613
|
-
// of life within a turn: a tool starting/streaming/ending, a streamed
|
|
614
|
-
// assistant token, or the turn ending. A tool that is actively streaming
|
|
615
|
-
// output, or a turn producing tokens, keeps this at ~0 no matter how long
|
|
616
|
-
// the OVERALL turn runs. Only a turn with NO observed engine event at all
|
|
617
|
-
// for WEDGE_QUIET_MS is even a candidate.
|
|
618
|
-
// 2. cpuPercent — the broker's WHOLE PROCESS TREE (the broker pid + every
|
|
619
|
-
// descendant), summed, sampled ONLY once a node is already a time-based
|
|
620
|
-
// candidate (never every tick — the `ps` shell-out is cheap but pointless
|
|
621
|
-
// on a healthy node). Sampling the tree, not just the broker pid, matters
|
|
622
|
-
// because the broker often sits idle awaiting a CHILD subprocess (bash,
|
|
623
|
-
// a build, a test run) that is the one actually burning CPU — sampling
|
|
624
|
-
// the broker alone would read that as 0% and false-fire on a perfectly
|
|
625
|
-
// healthy long-running tool call. A pi process (or any descendant)
|
|
626
|
-
// genuinely computing shows non-trivial tree CPU and is exonerated even
|
|
627
|
-
// past the quiet grace; a pi merely awaiting a subprocess or a stalled
|
|
628
|
-
// I/O call — wedged OR legitimately blocked on something equally silent —
|
|
629
|
-
// reads ~0% across the whole tree, matching the filed issue's own
|
|
630
|
-
// evidence (pid alive, 0% CPU, session file not advancing). A failed/
|
|
631
|
-
// unreadable CPU sample is treated as UNKNOWN, never as “low” — the
|
|
632
|
-
// verdict stays 'pending' rather than risk a false fire on an unreadable
|
|
633
|
-
// signal.
|
|
634
|
-
//
|
|
635
|
-
// ACCEPTED residual: a legitimate subprocess blocked on silent I/O (e.g. a
|
|
636
|
-
// 20-minute download emitting no output, a long `sleep`) reads ~0% tree
|
|
637
|
-
// CPU and is indistinguishable from a real wedge at the OS level — no CPU
|
|
638
|
-
// signal can tell a hung child from an I/O-blocked one. This is accepted:
|
|
639
|
-
// that case still HAS a descendant, so the daemon only notifies (never
|
|
640
|
-
// kills) — the cost of the rare false positive is one doctrine wake, and
|
|
641
|
-
// 20+ minutes of total silence with near-zero tree CPU is itself worth
|
|
642
|
-
// surfacing either way. Only the no-descendants branch ever acts, and
|
|
643
|
-
// SIGTERM preserves durable context: a settled abort resumes with a
|
|
644
|
-
// continuation, while an unconfirmed abort takes the existing fresh cycle.
|
|
645
|
-
// A false positive therefore costs at most one unnecessary recovery, not
|
|
646
|
-
// lost work.
|
|
647
|
-
// ---------------------------------------------------------------------------
|
|
648
|
-
// Generous: routine long tool calls settle well under this; only a turn with
|
|
649
|
-
// zero observed engine progress for 20+ minutes is even a wedge CANDIDATE.
|
|
650
|
-
export const WEDGE_QUIET_MS = 20 * 60_000;
|
|
651
|
-
// CPU% ceiling for the corroborating sample, taken once quietForMs already
|
|
652
|
-
// clears the grace above. Small but nonzero — tolerates measurement noise
|
|
653
|
-
// without accepting a process that's visibly still doing work.
|
|
654
|
-
export const WEDGE_CPU_MAX_PERCENT = 2;
|
|
655
|
-
/** Pure wedge decision — mirrors yieldStallVerdict: a total
|
|
656
|
-
* function of the observed signals, no clock or process access of its own.
|
|
657
|
-
* `busy` false — the turn already ended (however it routed) — is never a
|
|
658
|
-
* wedge, regardless of how stale the marker looks. `quietForMs` null means
|
|
659
|
-
* the heartbeat marker couldn't be read (treated as “not enough evidence”, not
|
|
660
|
-
* “wedged”). `cpuPercent` null (sample failed/unavailable) or above the
|
|
661
|
-
* ceiling — EITHER keeps the verdict at 'pending' (keep watching next tick)
|
|
662
|
-
* rather than firing on an unconfirmed or contradicted signal. */
|
|
663
|
-
export function wedgeVerdict(input) {
|
|
664
|
-
if (!input.busy)
|
|
665
|
-
return 'leave';
|
|
666
|
-
if (input.quietForMs === null || input.quietForMs < WEDGE_QUIET_MS)
|
|
667
|
-
return 'leave';
|
|
668
|
-
if (input.cpuPercent === null || input.cpuPercent > WEDGE_CPU_MAX_PERCENT)
|
|
669
|
-
return 'pending';
|
|
670
|
-
return 'wedged';
|
|
671
|
-
}
|
|
672
|
-
/** Notified-episode dedup (one notice per wedge, per §Detection-not-repeat-fire):
|
|
673
|
-
* keyed on node id → the busy-marker mtime we already notified for. The marker
|
|
674
|
-
* only advances on genuine engine progress (a heartbeat) or a fresh turn
|
|
675
|
-
* starting, so a stale wedge that later recovers (or a brand-new turn) always
|
|
676
|
-
* carries a DIFFERENT mtime and is free to notify again. In-memory only, like
|
|
677
|
-
* every other daemon dedup latch here — a restart re-arms it (worst case: one
|
|
678
|
-
* repeat notice), which is the same tradeoff as the daemon's other in-memory dedup latches. */
|
|
679
|
-
const wedgeNotifiedAt = new Map();
|
|
680
|
-
/** Fatal-provider-fault notify latch (issue #94), keyed on node id → the
|
|
681
|
-
* `fault.since` we already fanned a wake for. A fatal fault (dead OAuth grant,
|
|
682
|
-
* bad key, protocol) has NO retry consumer, so an errored turn just leaves the
|
|
683
|
-
* node `active` with a live-but-dormant broker and, until now, woke nobody —
|
|
684
|
-
* the manager waiting on it hung and later scheduled wakes silently re-failed.
|
|
685
|
-
* Deduped on `fault.since` so one episode fans exactly one wake; a later
|
|
686
|
-
* episode (fresh `since`) or a cleared fault re-arms. In-memory like every
|
|
687
|
-
* other latch here. */
|
|
688
|
-
const fatalFaultNotifiedAt = new Map();
|
|
689
|
-
/** Pure: sum CPU% across a process's entire tree (itself + every descendant).
|
|
690
|
-
* Parses `ps -ax -o pid=,ppid=,pcpu=` output, builds a ppid→children map, and
|
|
691
|
-
* DFS's from `rootPid` collecting root + all descendants; malformed/blank
|
|
692
|
-
* lines are skipped. Returns `null` if `rootPid` never appears in the output
|
|
693
|
-
* (can't measure at all — treated as UNKNOWN by wedgeVerdict, never as
|
|
694
|
-
* “low”) or the output has no parsable rows. Exported for direct unit testing
|
|
695
|
-
* — no process access of its own, mirroring wedgeVerdict's purity. */
|
|
696
|
-
function parsePsTree(psOutput) {
|
|
697
|
-
const byPid = new Map(); // pid → pcpu
|
|
698
|
-
const childrenOf = new Map(); // ppid → child pids
|
|
699
|
-
for (const line of psOutput.split('\n')) {
|
|
700
|
-
const trimmed = line.trim();
|
|
701
|
-
if (trimmed === '')
|
|
702
|
-
continue;
|
|
703
|
-
const parts = trimmed.split(/\s+/);
|
|
704
|
-
if (parts.length < 3)
|
|
705
|
-
continue;
|
|
706
|
-
const pid = Number(parts[0]);
|
|
707
|
-
const ppid = Number(parts[1]);
|
|
708
|
-
const pcpu = Number(parts[2]);
|
|
709
|
-
if (!Number.isFinite(pid) || !Number.isFinite(ppid) || !Number.isFinite(pcpu))
|
|
710
|
-
continue;
|
|
711
|
-
byPid.set(pid, pcpu);
|
|
712
|
-
const siblings = childrenOf.get(ppid);
|
|
713
|
-
if (siblings === undefined)
|
|
714
|
-
childrenOf.set(ppid, [pid]);
|
|
715
|
-
else
|
|
716
|
-
siblings.push(pid);
|
|
717
|
-
}
|
|
718
|
-
return { byPid, childrenOf };
|
|
719
|
-
}
|
|
720
|
-
export function sumTreeCpu(psOutput, rootPid) {
|
|
721
|
-
const { byPid, childrenOf } = parsePsTree(psOutput);
|
|
722
|
-
if (!byPid.has(rootPid))
|
|
723
|
-
return null;
|
|
724
|
-
let total = 0;
|
|
725
|
-
const stack = [rootPid];
|
|
726
|
-
const seen = new Set();
|
|
727
|
-
while (stack.length > 0) {
|
|
728
|
-
const pid = stack.pop();
|
|
729
|
-
if (seen.has(pid))
|
|
730
|
-
continue; // guard against a malformed/cyclic ps snapshot
|
|
731
|
-
seen.add(pid);
|
|
732
|
-
total += byPid.get(pid) ?? 0;
|
|
733
|
-
const kids = childrenOf.get(pid);
|
|
734
|
-
if (kids !== undefined)
|
|
735
|
-
stack.push(...kids);
|
|
736
|
-
}
|
|
737
|
-
return total;
|
|
738
|
-
}
|
|
739
|
-
/** Pure (issue #119): does `rootPid` have ANY descendant in this ps snapshot
|
|
740
|
-
* — a runaway subprocess to kill — or does the broker stand ALONE in its own
|
|
741
|
-
* tree (nothing to kill; the engine itself stalled)? Shares `parsePsTree`
|
|
742
|
-
* with `sumTreeCpu` so the two facts the wedge remediation needs come from
|
|
743
|
-
* one `ps` sample. Null when `rootPid` isn't in the sample at all (can't
|
|
744
|
-
* tell — same "unknown, never treated as a fact" stance `sumTreeCpu` takes on
|
|
745
|
-
* a missing root). Exported for direct unit testing. */
|
|
746
|
-
export function treeHasDescendants(psOutput, rootPid) {
|
|
747
|
-
const { byPid, childrenOf } = parsePsTree(psOutput);
|
|
748
|
-
if (!byPid.has(rootPid))
|
|
749
|
-
return null;
|
|
750
|
-
return (childrenOf.get(rootPid) ?? []).length > 0;
|
|
751
|
-
}
|
|
752
|
-
/** Sample the broker's WHOLE PROCESS TREE — the broker pid plus every
|
|
753
|
-
* descendant — via ONE `ps` shell-out, yielding both signals the wedge path
|
|
754
|
-
* needs: the summed tree CPU% (corroborates the heartbeat silence) and
|
|
755
|
-
* whether the tree has any descendant at all (issue #119 — decides whether
|
|
756
|
-
* remediation is "kill the subprocess" or "kick the broker"). Null on ANY
|
|
757
|
-
* failure (the shell-out itself failing, `ps` missing, or the root pid
|
|
758
|
-
* absent/output unparseable) — wedgeVerdict treats a null cpuPercent exactly
|
|
759
|
-
* like “too high”, so a broken probe can only ever suppress a false fire,
|
|
760
|
-
* never cause one; a null sample also means handleWedgeDetection never
|
|
761
|
-
* attempts the SIGTERM kick on unconfirmed information. */
|
|
762
|
-
async function readTreeSample(pid) {
|
|
763
|
-
try {
|
|
764
|
-
const { stdout } = await execFileAsync('ps', ['-ax', '-o', 'pid=,ppid=,pcpu='], {
|
|
765
|
-
encoding: 'utf8',
|
|
766
|
-
timeout: 2000,
|
|
767
|
-
});
|
|
768
|
-
if (typeof stdout !== 'string')
|
|
769
|
-
return null;
|
|
770
|
-
const cpuPercent = sumTreeCpu(stdout, pid);
|
|
771
|
-
const hasDescendants = treeHasDescendants(stdout, pid);
|
|
772
|
-
if (cpuPercent === null || hasDescendants === null)
|
|
773
|
-
return null;
|
|
774
|
-
return { cpuPercent, hasDescendants };
|
|
775
|
-
}
|
|
776
|
-
catch {
|
|
777
|
-
return null;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
/** Enact wedge detection for a LIVE, busy node (called from the tick's
|
|
781
|
-
* live-fleet pass, alongside handleYieldStall — the two never
|
|
782
|
-
* overlap in practice: yield-stall requires the turn OVER, wedge requires it
|
|
783
|
-
* mid-flight). Reads the heartbeat + samples CPU only when already past the
|
|
784
|
-
* time grace, fans one doctrine wake + records one `daemon→node` fault per
|
|
785
|
-
* episode, and never touches the engine itself. */
|
|
786
|
-
async function handleWedgeDetection(id, pid, expectedIdentity, now) {
|
|
787
|
-
if (!isBusy(id)) {
|
|
788
|
-
wedgeNotifiedAt.delete(id); // turn ended (however it routed) — next turn starts a fresh episode
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
const since = busySince(id);
|
|
792
|
-
const quietForMs = since === null ? null : now - since;
|
|
793
|
-
const sample = quietForMs !== null && quietForMs >= WEDGE_QUIET_MS ? await readTreeSample(pid) : null;
|
|
794
|
-
const cpuPercent = sample?.cpuPercent ?? null;
|
|
795
|
-
const verdict = wedgeVerdict({ busy: true, quietForMs, cpuPercent });
|
|
796
|
-
if (verdict !== 'wedged')
|
|
797
|
-
return;
|
|
798
|
-
if (since !== null && wedgeNotifiedAt.get(id) === since)
|
|
799
|
-
return; // already notified THIS episode
|
|
800
|
-
// readTreeSample is intentionally not signal authorization: confirm the
|
|
801
|
-
// launch identity again at the destructive boundary after its async probe.
|
|
802
|
-
if (sample?.hasDescendants === false
|
|
803
|
-
&& (expectedIdentity == null || recordedPidLiveness(pid, expectedIdentity) !== 'alive'))
|
|
804
|
-
return;
|
|
805
|
-
if (since !== null)
|
|
806
|
-
wedgeNotifiedAt.set(id, since);
|
|
807
|
-
const meta = getNode(id);
|
|
808
|
-
const name = meta !== null ? fullName(meta) : id;
|
|
809
|
-
const minutes = Math.round((quietForMs ?? WEDGE_QUIET_MS) / 60_000);
|
|
810
|
-
// A null sample can't reach here — wedgeVerdict only fires 'wedged' on a
|
|
811
|
-
// confirmed near-zero cpuPercent, which requires a non-null sample — so
|
|
812
|
-
// `sample.hasDescendants` is always defined at this point; the `?? true`
|
|
813
|
-
// is a type-level fallback only, biased toward the SAFER branch (notify,
|
|
814
|
-
// don't SIGTERM) if that invariant is ever violated.
|
|
815
|
-
const hasDescendants = sample?.hasDescendants ?? true;
|
|
816
|
-
let label;
|
|
817
|
-
if (hasDescendants) {
|
|
818
|
-
// A runaway subprocess is the likely cause — the daemon does not touch the
|
|
819
|
-
// engine; the sanctioned recovery is manual (issue #110).
|
|
820
|
-
label =
|
|
821
|
-
`Child wedged — ${name} (${id}) is alive and mid-turn but has shown NO engine progress for ` +
|
|
822
|
-
`${minutes}+ minutes and its process is near-idle. It will NOT recover on its own — read ` +
|
|
823
|
-
`\`crtr memory read wedged-child-on-runaway-bash\` and kill the runaway SUBPROCESS (not this ` +
|
|
824
|
-
`node) so its turn can resume with no lost context.`;
|
|
825
|
-
}
|
|
826
|
-
else {
|
|
827
|
-
// No descendant to kill — the engine itself stalled (e.g. mid model_change)
|
|
828
|
-
// with nothing a human could kill. The daemon performs the same kick
|
|
829
|
-
// `node lifecycle revive --now` does on demand: SIGTERM the broker. The
|
|
830
|
-
// exit-policy job resumes with a continuation if Pi certifies the abort;
|
|
831
|
-
// otherwise it starts the existing dirty recovery cycle (issue #119).
|
|
832
|
-
label =
|
|
833
|
-
`Child wedged — ${name} (${id}) is alive and mid-turn but has shown NO engine progress for ` +
|
|
834
|
-
`${minutes}+ minutes, its process is near-idle, AND it has no subprocess to kill (the engine ` +
|
|
835
|
-
`itself stalled). Kicking it now: SIGTERM to the broker — the daemon will resume a cleanly ` +
|
|
836
|
-
`aborted session with a continuation, or start a recovery cycle if the abort cannot settle.`;
|
|
837
|
-
try {
|
|
838
|
-
process.kill(pid, 'SIGTERM');
|
|
839
|
-
}
|
|
840
|
-
catch {
|
|
841
|
-
/* already gone — its ChildProcess exit event still owns recovery */
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
emitEvent({
|
|
845
|
-
level: 'error',
|
|
846
|
-
event: 'broker.wedge.detected',
|
|
847
|
-
...(isSafeNodeId(id) ? { node_id: id } : {}),
|
|
848
|
-
fields: {
|
|
849
|
-
...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
|
|
850
|
-
pid,
|
|
851
|
-
quiet_ms: quietForMs,
|
|
852
|
-
cpu_percent: cpuPercent,
|
|
853
|
-
has_descendants: hasDescendants,
|
|
854
|
-
kicked: !hasDescendants,
|
|
855
|
-
},
|
|
856
|
-
});
|
|
857
|
-
try {
|
|
858
|
-
recordFault(id, {
|
|
859
|
-
link: 'daemon→node',
|
|
860
|
-
op: 'engine wedge',
|
|
861
|
-
kind: 'wedged',
|
|
862
|
-
retry: { disposition: 'manual' },
|
|
863
|
-
message: label,
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
catch {
|
|
867
|
-
/* best-effort — the doctrine wake below is the load-bearing notice */
|
|
868
|
-
}
|
|
869
|
-
try {
|
|
870
|
-
fanDoctrineWake(id, subscribersOf(id), label, {
|
|
871
|
-
reason: 'child-wedged',
|
|
872
|
-
child: id,
|
|
873
|
-
quiet_ms: quietForMs,
|
|
874
|
-
cpu_percent: cpuPercent,
|
|
875
|
-
kicked: !hasDescendants,
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
catch {
|
|
879
|
-
/* best-effort, mirrors every other fan-out in this file */
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
/** Enact a fatal-provider-fault outcome for a LIVE node (issue #94; called
|
|
883
|
-
* from the tick's live-fleet pass). Fatal authentication and
|
|
884
|
-
* protocol faults remain parked because re-authentication or a correction can
|
|
885
|
-
* make the next wake viable. A context-window overflow cannot become viable
|
|
886
|
-
* while its oversized session remains selected, so it is crashed and its broker
|
|
887
|
-
* is torn down before the parent is notified. */
|
|
888
|
-
function handleFatalFault(id, now) {
|
|
889
|
-
void now;
|
|
890
|
-
const fault = readFault(id);
|
|
891
|
-
if (fault === null) {
|
|
892
|
-
fatalFaultNotifiedAt.delete(id); // fault cleared — next episode re-arms
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
if (fault.retry.disposition !== 'fatal' || fault.link !== 'pi→provider')
|
|
896
|
-
return;
|
|
897
|
-
if (fatalFaultNotifiedAt.get(id) === fault.since)
|
|
898
|
-
return; // already fanned THIS episode
|
|
899
|
-
fatalFaultNotifiedAt.set(id, fault.since);
|
|
900
|
-
const meta = getNode(id);
|
|
901
|
-
const name = meta !== null ? fullName(meta) : id;
|
|
902
|
-
const isAuth = fault.kind === 'auth';
|
|
903
|
-
const firstLine = (fault.message.split('\n')[0] ?? '').slice(0, 200);
|
|
904
|
-
const terminalOverflow = fault.kind === 'context-overflow';
|
|
905
|
-
const label = terminalOverflow
|
|
906
|
-
? `Child failed — ${name} (${id}) exceeded its model context window and is now dead. ` +
|
|
907
|
-
`Shrink the task or loaded context, or choose a larger-context model before spawning it again. Error: ${firstLine}`
|
|
908
|
-
: `Child hit a fatal provider fault — ${name} (${id}) got a non-retryable ${fault.kind} error and will NOT recover on its own; ` +
|
|
909
|
-
`it stays alive but every wake re-fails until the fault clears. ` +
|
|
910
|
-
(isAuth
|
|
911
|
-
? `Its provider credential is dead — re-authenticate (\`/login\`, or re-auth crtr); the child recovers on its next wake once the auth reloads. `
|
|
912
|
-
: ``) +
|
|
913
|
-
`Error: ${firstLine}`;
|
|
914
|
-
if (terminalOverflow) {
|
|
915
|
-
try {
|
|
916
|
-
transition(id, 'crash');
|
|
917
|
-
headlessBrokerHost.teardown(id);
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
/* the terminal notice remains useful if teardown raced the broker exit */
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
emitEvent({
|
|
924
|
-
level: 'error',
|
|
925
|
-
event: 'fault.fatal.fanout',
|
|
926
|
-
...(isSafeNodeId(id) ? { node_id: id } : {}),
|
|
927
|
-
fields: {
|
|
928
|
-
...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
|
|
929
|
-
kind: fault.kind,
|
|
930
|
-
link: fault.link,
|
|
931
|
-
terminal_overflow: terminalOverflow,
|
|
932
|
-
},
|
|
933
|
-
});
|
|
934
|
-
try {
|
|
935
|
-
fanDoctrineWake(id, subscribersOf(id), label, {
|
|
936
|
-
reason: terminalOverflow ? 'child-context-overflow' : 'child-fault-fatal',
|
|
937
|
-
child: id,
|
|
938
|
-
kind: fault.kind,
|
|
939
|
-
message: firstLine,
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
catch {
|
|
943
|
-
/* best-effort, mirrors every other fan-out in this file */
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
// ---------------------------------------------------------------------------
|
|
947
|
-
// Auth-reload fan (one /login → every live broker follows)
|
|
948
|
-
// ---------------------------------------------------------------------------
|
|
949
|
-
/** The credential files every broker reads: pi's shared `auth.json` plus the managed
|
|
950
|
-
* providers' subscription pools, which own the anthropic / openai-codex credentials
|
|
951
|
-
* outright (see `PoolCredentialStore`) — a managed login touches ONLY a pool file, so
|
|
952
|
-
* watching auth.json alone would miss it. VENDORED from pi `config.getAuthPath()` (=
|
|
953
|
-
* `<agentDir>/auth.json`) the same way spawn.ts vendors the sessions root: a ROOT import
|
|
954
|
-
* of pi's `getAgentDir` would eager-load the heavy pi SDK index, so we mirror its
|
|
955
|
-
* resolution — `PI_CODING_AGENT_DIR` env, else `~/.pi/agent`. Re-sync on a pi SDK bump
|
|
956
|
-
* that moves auth.json. */
|
|
957
|
-
function piCredentialPaths() {
|
|
958
|
-
const env = process.env['PI_CODING_AGENT_DIR'];
|
|
959
|
-
const agentDir = env !== undefined && env !== '' ? env : join(homedir(), '.pi', 'agent');
|
|
960
|
-
return [
|
|
961
|
-
join(agentDir, 'auth.json'),
|
|
962
|
-
join(agentDir, 'anthropic-subscriptions.json'),
|
|
963
|
-
join(agentDir, 'openai-codex-subscriptions.json'),
|
|
964
|
-
];
|
|
965
|
-
}
|
|
966
|
-
// Last-observed credential mtime, for the daemon's change-watch. Lazily seeded on
|
|
967
|
-
// the first poll that sees the file (so daemon start never fans a spurious
|
|
968
|
-
// reload), then updated on every detected change. In-memory only — a daemon
|
|
969
|
-
// restart re-seeds from the current file, so at most one missed change spans the
|
|
970
|
-
// restart gap (re-login during a ~1s restart), which a human just redoes.
|
|
971
|
-
let knownAuthMtimeMs = null;
|
|
972
|
-
/** Watch the shared auth.json mtime and, on a change, fan a live `reload_auth`
|
|
973
|
-
* to every live broker so one `/login` propagates across the whole canvas. The
|
|
974
|
-
* broker that performed the login already self-reloaded; re-reloading it is
|
|
975
|
-
* harmless (authStorage.reload() is idempotent), so we don't exclude it.
|
|
976
|
-
*
|
|
977
|
-
* Rides the existing ~2s poll, so several rapid writes from one login collapse
|
|
978
|
-
* to a single observed mtime change → a single fan. Each broker is reloaded
|
|
979
|
-
* under its own try/catch (Promise.allSettled): a broker mid-shutdown or whose
|
|
980
|
-
* socket refuses connect is logged and skipped, never aborting the others. */
|
|
981
|
-
async function handleAuthReload(now) {
|
|
982
|
-
// The newest mtime across the credential files: any one of them changing moves it forward,
|
|
983
|
-
// and a file that does not exist yet simply contributes nothing.
|
|
984
|
-
let mtimeMs = 0;
|
|
985
|
-
for (const path of piCredentialPaths()) {
|
|
986
|
-
try {
|
|
987
|
-
mtimeMs = Math.max(mtimeMs, statSync(path).mtimeMs);
|
|
988
|
-
}
|
|
989
|
-
catch {
|
|
990
|
-
/* not written yet — nothing to observe for this file */
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
if (mtimeMs === 0)
|
|
994
|
-
return; // never logged in — nothing to watch
|
|
995
|
-
if (knownAuthMtimeMs === null) {
|
|
996
|
-
knownAuthMtimeMs = mtimeMs; // seed on first sight; never fan on daemon start
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
if (mtimeMs === knownAuthMtimeMs)
|
|
1000
|
-
return;
|
|
1001
|
-
knownAuthMtimeMs = mtimeMs;
|
|
1002
|
-
await operationIdContext.fresh(async () => {
|
|
1003
|
-
try {
|
|
1004
|
-
// Every fleet entry IS a live broker (design D-1) — no row scan, no pid
|
|
1005
|
-
// probe. Enumerated only when auth actually changed (the rare path).
|
|
1006
|
-
const live = Array.from(boundFleet().entries(), ([id]) => id);
|
|
1007
|
-
if (live.length === 0)
|
|
1008
|
-
return;
|
|
1009
|
-
const results = await Promise.allSettled(live.map(async (id) => {
|
|
1010
|
-
try {
|
|
1011
|
-
await reloadAuthLive(id);
|
|
1012
|
-
}
|
|
1013
|
-
catch (err) {
|
|
1014
|
-
emitEvent({
|
|
1015
|
-
level: 'error',
|
|
1016
|
-
event: 'auth.reload.broker_failed',
|
|
1017
|
-
...(isSafeNodeId(id) ? { node_id: id } : { fields: { broker_id: id } }),
|
|
1018
|
-
error: err,
|
|
1019
|
-
});
|
|
1020
|
-
throw err;
|
|
1021
|
-
}
|
|
1022
|
-
}));
|
|
1023
|
-
const ok = results.filter((r) => r.status === 'fulfilled').length;
|
|
1024
|
-
emitEvent({
|
|
1025
|
-
level: 'info',
|
|
1026
|
-
event: 'auth.reload.completed',
|
|
1027
|
-
outcome: ok === live.length ? 'succeeded' : 'failed',
|
|
1028
|
-
fields: {
|
|
1029
|
-
broker_count: live.length,
|
|
1030
|
-
succeeded_count: ok,
|
|
1031
|
-
failed_count: live.length - ok,
|
|
1032
|
-
},
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
catch (err) {
|
|
1036
|
-
emitEvent({ level: 'error', event: 'auth.reload.failed', error: err });
|
|
1037
|
-
}
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
// retryResumeMode moved to ./fleet.js (the cycle-vs-resume arbiter inside
|
|
1041
|
-
// classifyDeadNode); re-exported above for the historical crtrd.ts surface.
|
|
1042
|
-
// How long a dead node's on-disk record must have been QUIET (no meta/telemetry
|
|
1043
|
-
// write) before its leftover placement is reaped. A fresh crash keeps its pane
|
|
1044
|
-
// around this long so a human can read the corpse; a long-dead node's orphaned
|
|
1045
|
-
// pane is torn down on sight.
|
|
1046
|
-
export const DEAD_REAP_GRACE_MS = 10 * 60_000;
|
|
1047
|
-
/** ms since the node's durable record was last written (meta.json / job/
|
|
1048
|
-
* telemetry.json mtimes — the files every live cycle touches). Infinity when
|
|
1049
|
-
* neither exists: no disk evidence reads as ancient, not fresh. Deliberately
|
|
1050
|
-
* NOT inbox.jsonl — children keep appending to a dead manager's inbox, which
|
|
1051
|
-
* must not hold its corpse un-reapable forever. */
|
|
1052
|
-
function msSinceDiskActivity(nodeId, now) {
|
|
1053
|
-
let latest = 0;
|
|
1054
|
-
for (const p of [join(nodeDir(nodeId), 'meta.json'), join(jobDir(nodeId), 'telemetry.json')]) {
|
|
1055
|
-
try {
|
|
1056
|
-
const m = statSync(p).mtimeMs;
|
|
1057
|
-
if (m > latest)
|
|
1058
|
-
latest = m;
|
|
1059
|
-
}
|
|
1060
|
-
catch {
|
|
1061
|
-
/* absent — contributes nothing */
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
return latest === 0 ? Number.POSITIVE_INFINITY : now - latest;
|
|
1065
|
-
}
|
|
1066
|
-
/** Reap the leftover PLACEMENT of long-dead nodes (audit 2026-06-09, Bug 3 —
|
|
1067
|
-
* node mq3l84it-40f6e8ac, a dead resident whose orphaned pane + focus row sat
|
|
1068
|
-
* untouched for ~36h). The daemon supervises only active|idle rows, so a node
|
|
1069
|
-
* that lands `dead` while still holding a pane is invisible to every pass and
|
|
1070
|
-
* its residue leaks forever.
|
|
1071
|
-
*
|
|
1072
|
-
* Deliberate REAP-not-REVIVE decision: a dead node is NOT auto-revived. `dead`
|
|
1073
|
-
* means crashed — auto-resurrecting a crashed RESIDENT (a human conversation)
|
|
1074
|
-
* would pop dead chats back open unbidden, and the daemon's contract is to
|
|
1075
|
-
* supervise live nodes, not to undo terminal states. A dead resident's wake
|
|
1076
|
-
* sources remain the human ones (`node message send --tier critical`, focus, `canvas
|
|
1077
|
-
* revive`), all of which still work after this reap. What we DO reclaim is the
|
|
1078
|
-
* physical residue: kill the orphaned pane, drop the focus row, null the
|
|
1079
|
-
* LOCATION (tearDownNode). The node row, its edges, and all on-disk state stay
|
|
1080
|
-
* intact, so a later manual revive keeps its full graph — the leak was the
|
|
1081
|
-
* pane/focus/presence, not the row. Grace: only after DEAD_REAP_GRACE_MS of
|
|
1082
|
-
* disk quiet, so a just-crashed pane survives long enough to be inspected. */
|
|
1083
|
-
function reapDeadResidue(now) {
|
|
1084
|
-
let rows;
|
|
1085
|
-
try {
|
|
1086
|
-
rows = listNodes({ status: ['dead'] });
|
|
1087
|
-
}
|
|
1088
|
-
catch (err) {
|
|
1089
|
-
operationIdContext.fresh(() => {
|
|
1090
|
-
emitEvent({ level: 'error', event: 'residue.reap_scan.failed', error: err });
|
|
1091
|
-
});
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
for (const row of rows) {
|
|
1095
|
-
if (row.pane == null && row.window == null && row.tmux_session == null)
|
|
1096
|
-
continue;
|
|
1097
|
-
try {
|
|
1098
|
-
if (msSinceDiskActivity(row.node_id, now) < DEAD_REAP_GRACE_MS)
|
|
1099
|
-
continue;
|
|
1100
|
-
}
|
|
1101
|
-
catch (err) {
|
|
1102
|
-
operationIdContext.fresh(() => {
|
|
1103
|
-
emitEvent({
|
|
1104
|
-
level: 'error',
|
|
1105
|
-
event: 'residue.reap.failed',
|
|
1106
|
-
...(isSafeNodeId(row.node_id) ? { node_id: row.node_id } : {}),
|
|
1107
|
-
error: err,
|
|
1108
|
-
fields: {
|
|
1109
|
-
...(isSafeNodeId(row.node_id) ? {} : { affected_node_id: row.node_id }),
|
|
1110
|
-
pane: row.pane,
|
|
1111
|
-
window: row.window,
|
|
1112
|
-
tmux_session: row.tmux_session,
|
|
1113
|
-
},
|
|
1114
|
-
});
|
|
1115
|
-
});
|
|
1116
|
-
continue;
|
|
1117
|
-
}
|
|
1118
|
-
operationIdContext.fresh(() => {
|
|
1119
|
-
try {
|
|
1120
|
-
emitEvent({
|
|
1121
|
-
level: 'info',
|
|
1122
|
-
event: 'residue.reaped',
|
|
1123
|
-
...(isSafeNodeId(row.node_id) ? { node_id: row.node_id } : {}),
|
|
1124
|
-
fields: {
|
|
1125
|
-
...(isSafeNodeId(row.node_id) ? {} : { affected_node_id: row.node_id }),
|
|
1126
|
-
pane: row.pane,
|
|
1127
|
-
window: row.window,
|
|
1128
|
-
tmux_session: row.tmux_session,
|
|
1129
|
-
},
|
|
1130
|
-
});
|
|
1131
|
-
tearDownNode(row.node_id);
|
|
1132
|
-
}
|
|
1133
|
-
catch (err) {
|
|
1134
|
-
emitEvent({
|
|
1135
|
-
level: 'error',
|
|
1136
|
-
event: 'residue.reap.failed',
|
|
1137
|
-
...(isSafeNodeId(row.node_id) ? { node_id: row.node_id } : {}),
|
|
1138
|
-
error: err,
|
|
1139
|
-
fields: {
|
|
1140
|
-
...(isSafeNodeId(row.node_id) ? {} : { affected_node_id: row.node_id }),
|
|
1141
|
-
pane: row.pane,
|
|
1142
|
-
window: row.window,
|
|
1143
|
-
tmux_session: row.tmux_session,
|
|
1144
|
-
},
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
/** GC stale focus rows (audit 2026-06-09, Bug 5): nothing else deletes a focuses
|
|
1151
|
-
* row when its pane dies outside the sanctioned teardown paths (a user
|
|
1152
|
-
* kill-pane, a tmux server restart, a crashed swap), so dead-pane rows
|
|
1153
|
-
* accumulate forever — the audit found 15 of 21 rows pointing at gone panes,
|
|
1154
|
-
* and a stale row both misreports the node's on-screen viewer and blocks its
|
|
1155
|
-
* node from being adopted into a fresh viewport (UNIQUE node_id). Sweep them here: one
|
|
1156
|
-
* batched `list-panes -a` probe (never a per-row display-message), deleting
|
|
1157
|
-
* rows whose recorded pane no longer exists. A pane-less row (a bridge/unplaced
|
|
1158
|
-
* viewport) has nothing to verify and is left alone. On a FAILED probe (tmux
|
|
1159
|
-
* unreachable — daemon racing a server restart) we skip the sweep entirely:
|
|
1160
|
-
* "can't tell" must never mass-delete every viewport. */
|
|
1161
|
-
function gcStaleFocuses() {
|
|
1162
|
-
let rows;
|
|
1163
|
-
try {
|
|
1164
|
-
rows = listFocuses();
|
|
1165
|
-
}
|
|
1166
|
-
catch (err) {
|
|
1167
|
-
operationIdContext.fresh(() => {
|
|
1168
|
-
emitEvent({ level: 'error', event: 'focus.gc.failed', error: err });
|
|
1169
|
-
});
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
if (rows.length === 0)
|
|
1173
|
-
return;
|
|
1174
|
-
let live;
|
|
1175
|
-
try {
|
|
1176
|
-
live = listLivePanes();
|
|
1177
|
-
}
|
|
1178
|
-
catch (err) {
|
|
1179
|
-
operationIdContext.fresh(() => {
|
|
1180
|
-
emitEvent({ level: 'error', event: 'focus.gc.failed', error: err });
|
|
1181
|
-
});
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
if (live === null)
|
|
1185
|
-
return; // probe failed — never GC on "can't tell"
|
|
1186
|
-
for (const f of rows) {
|
|
1187
|
-
if (f.pane === null || live.has(f.pane))
|
|
1188
|
-
continue;
|
|
1189
|
-
const removed = operationIdContext.fresh(() => {
|
|
1190
|
-
try {
|
|
1191
|
-
emitEvent({
|
|
1192
|
-
level: 'info',
|
|
1193
|
-
event: 'focus.gc.removed',
|
|
1194
|
-
...(isSafeNodeId(f.node_id) ? { node_id: f.node_id } : {}),
|
|
1195
|
-
fields: {
|
|
1196
|
-
focus_id: f.focus_id,
|
|
1197
|
-
focus_node_id: f.node_id,
|
|
1198
|
-
pane: f.pane,
|
|
1199
|
-
},
|
|
1200
|
-
});
|
|
1201
|
-
closeFocusRow(f.focus_id);
|
|
1202
|
-
return true;
|
|
1203
|
-
}
|
|
1204
|
-
catch (err) {
|
|
1205
|
-
emitEvent({ level: 'error', event: 'focus.gc.failed', error: err });
|
|
1206
|
-
return false;
|
|
1207
|
-
}
|
|
1208
|
-
});
|
|
1209
|
-
if (!removed)
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
269
|
// ---------------------------------------------------------------------------
|
|
1214
270
|
// Pidfile
|
|
1215
271
|
// ---------------------------------------------------------------------------
|
|
@@ -1245,25 +301,23 @@ function removeOwnedPidfileMirror(pid) {
|
|
|
1245
301
|
throw error;
|
|
1246
302
|
}
|
|
1247
303
|
}
|
|
1248
|
-
// isPidAlive now lives in canvas/pid.ts (the one shared signal-0 probe at the
|
|
1249
|
-
// lowest layer, so canvas/, runtime/, AND daemon/ all import it down). Re-
|
|
1250
|
-
// exported here to preserve the public surface consumed by commands/sys/daemon.ts
|
|
1251
|
-
// and the grace-clock test.
|
|
1252
|
-
export { isPidAlive };
|
|
1253
304
|
const directTickLifecycle = {
|
|
1254
305
|
acceptsDetachedWork: () => true,
|
|
1255
306
|
registerDetached: (work) => {
|
|
1256
307
|
void work.catch(() => { });
|
|
1257
308
|
},
|
|
1258
309
|
};
|
|
310
|
+
const brokerSupervision = new BrokerSupervisionReconciler();
|
|
311
|
+
const controllerDeath = new ControllerDeathReconciler();
|
|
312
|
+
const dormantInbox = new DormantInboxReconciler();
|
|
313
|
+
const cronLane = new CronLaneReconciler();
|
|
314
|
+
const storageMaintenance = new StorageMaintenanceReconciler();
|
|
1259
315
|
/** Supervision tick over the daemon's current live set. */
|
|
1260
316
|
export async function superviseTick(now = Date.now(), lifecycle = directTickLifecycle) {
|
|
1261
317
|
const fleet = boundFleet();
|
|
1262
|
-
// Auth
|
|
1263
|
-
//
|
|
1264
|
-
|
|
1265
|
-
// supervision passes never skips an auth fan (it does its own enumeration).
|
|
1266
|
-
await handleAuthReload(now);
|
|
318
|
+
// Auth reload must precede the row-list guard: it is fleet-wide and does not
|
|
319
|
+
// depend on a successful canvas scan.
|
|
320
|
+
const thresholds = await brokerSupervision.run(now, { fleet, lifecycle });
|
|
1267
321
|
let rows;
|
|
1268
322
|
try {
|
|
1269
323
|
rows = listNodes({ status: ['active', 'idle'] });
|
|
@@ -1272,285 +326,15 @@ export async function superviseTick(now = Date.now(), lifecycle = directTickLife
|
|
|
1272
326
|
emitEvent({ level: 'error', event: 'canvas.node_list.failed', error: err });
|
|
1273
327
|
return;
|
|
1274
328
|
}
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
rows = rows.filter((r) => r.kind !== 'human');
|
|
1285
|
-
// Broker-pressure thresholds gate this tick's daemon-initiated launches; the
|
|
1286
|
-
// live count is EXACTLY the fleet size (design D-7) — every live broker is a
|
|
1287
|
-
// fleet entry, no census, no row inference.
|
|
1288
|
-
const thresholds = brokerThresholdsForDaemon();
|
|
1289
|
-
// The warning stays detached so tmux cannot hold the supervision tick. Its
|
|
1290
|
-
// promise is registered synchronously for invocation teardown.
|
|
1291
|
-
if (fleet.size() >= thresholds.warning && lifecycle.acceptsDetachedWork()) {
|
|
1292
|
-
const warning = operationIdContext.fresh(() => warnOverCapInTmux(fleet.size(), thresholds.warning, thresholds.automaticReviveCap));
|
|
1293
|
-
lifecycle.registerDetached(warning);
|
|
1294
|
-
}
|
|
1295
|
-
// First pass: LIVE-fleet checks. Every fleet entry IS a live broker (design
|
|
1296
|
-
// D-1) — no pid probe, no grace clock. Dead-node recovery does NOT run here:
|
|
1297
|
-
// a broker's real exit event already enqueued an exit-policy job on the
|
|
1298
|
-
// serial work lane (fleet.onChildExit → classifyDeadNode), so by the time a
|
|
1299
|
-
// tick runs, every dead node has been classified. This pass inspects only
|
|
1300
|
-
// the conditions an exit event can never surface on a LIVE engine: a stalled
|
|
1301
|
-
// refresh (handleYieldStall), a wedged mid-turn engine (handleWedgeDetection),
|
|
1302
|
-
// and a parked fatal provider fault (handleFatalFault).
|
|
1303
|
-
for (const [id, entry] of Array.from(fleet.entries())) {
|
|
1304
|
-
await operationIdContext.fresh(async () => {
|
|
1305
|
-
try {
|
|
1306
|
-
const row = getRow(id);
|
|
1307
|
-
if (row === null || (row.status !== 'active' && row.status !== 'idle'))
|
|
1308
|
-
return;
|
|
1309
|
-
handleYieldStall(row, entry.pid, now);
|
|
1310
|
-
await handleWedgeDetection(id, entry.pid, row.pi_pid_identity, now);
|
|
1311
|
-
handleFatalFault(id, now);
|
|
1312
|
-
}
|
|
1313
|
-
catch (err) {
|
|
1314
|
-
// One bad node must never kill the loop.
|
|
1315
|
-
emitEvent({
|
|
1316
|
-
level: 'error',
|
|
1317
|
-
event: 'broker.supervision.failed',
|
|
1318
|
-
...(isSafeNodeId(id) ? { node_id: id } : { fields: { affected_node_id: id } }),
|
|
1319
|
-
error: err,
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
});
|
|
1323
|
-
}
|
|
1324
|
-
// Settle every wait whose controller has ended, independent of the waiter's
|
|
1325
|
-
// current status. Active and focused terminal nodes own a live inbox watcher;
|
|
1326
|
-
// idle-released nodes are revived by the second pass below for this same row.
|
|
1327
|
-
for (const row of rows) {
|
|
1328
|
-
const current = getRow(row.node_id);
|
|
1329
|
-
const controllerId = current?.waiting_for;
|
|
1330
|
-
if (controllerId === null || controllerId === undefined)
|
|
1331
|
-
continue;
|
|
1332
|
-
// Read-gate before the write boundary: only a dead controller warrants a
|
|
1333
|
-
// notice, and the vast majority of live waits would otherwise open a
|
|
1334
|
-
// write-lock transaction every tick just to no-op. settleDeadMessageWait
|
|
1335
|
-
// re-checks liveness inside the transaction, so this gate cannot misfire.
|
|
1336
|
-
const controller = getRow(controllerId);
|
|
1337
|
-
if (controller !== null && (controller.status === 'active' || controller.status === 'idle'))
|
|
1338
|
-
continue;
|
|
1339
|
-
operationIdContext.fresh(() => {
|
|
1340
|
-
appendControllerDeathNotice(row.node_id, controllerId);
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
// Second pass: revive idle-released nodes whose inbox has unseen entries.
|
|
1344
|
-
// The in-process inbox-watcher dies with pi, so the daemon owns wake-on-message
|
|
1345
|
-
// for dormant nodes. readCursor is the exact entry identity the watcher
|
|
1346
|
-
// persisted before exit; later physical entries are undelivered work.
|
|
1347
|
-
//
|
|
1348
|
-
// Backoff + attempt cap is keyed on CURSOR IDENTITY (2026-07-10 incident,
|
|
1349
|
-
// round 2 — see the idleReleaseRelaunchState doc above for the full failure-
|
|
1350
|
-
// mode writeup): a node whose delivery keeps failing — whether the broker
|
|
1351
|
-
// never boots, OR it boots and then faults its turn before changing the
|
|
1352
|
-
// cursor — used to be relaunched here every poll (round 1) or every fault-
|
|
1353
|
-
// recovery cycle (round 1's fix, still exploitable), forever. The counter now
|
|
1354
|
-
// resets only when readCursor(id) differs from the prior exact identity —
|
|
1355
|
-
// proof the wake accomplished something — never on a bare status flap or a
|
|
1356
|
-
// transiently-alive engine.
|
|
1357
|
-
// Capacity-checked too, so a burst of due idle-release wakes can't stampede
|
|
1358
|
-
// past the live-broker cap either.
|
|
1359
|
-
for (const row of rows) {
|
|
1360
|
-
const id = row.node_id;
|
|
1361
|
-
let r;
|
|
1362
|
-
try {
|
|
1363
|
-
// Re-read the ROW for fresh runtime (the first pass may have mutated it);
|
|
1364
|
-
// no meta needed — status/intent live in the row.
|
|
1365
|
-
r = getRow(id);
|
|
1366
|
-
if (r === null) {
|
|
1367
|
-
clearIdleReleaseRelaunchState(id); // node gone — nothing left to track
|
|
1368
|
-
continue;
|
|
1369
|
-
}
|
|
1370
|
-
// NOT idle-release right now (e.g. mid fault-recovery, briefly active) —
|
|
1371
|
-
// leave idleReleaseRelaunchState UNTOUCHED. Clearing here would let the
|
|
1372
|
-
// fault-recovery cycle (idle-release → briefly alive → fault → idle-
|
|
1373
|
-
// release again) reset the counter every episode — the exact round-2 bug.
|
|
1374
|
-
if (r.status !== 'idle' || r.intent !== 'idle-release')
|
|
1375
|
-
continue;
|
|
1376
|
-
// The in-process inbox-watcher only owns delivery while the engine is LIVE.
|
|
1377
|
-
// A released node's broker is DEAD (it freed itself while dormant), so it
|
|
1378
|
-
// has no watcher and the daemon must wake it. A fleet entry IS the live
|
|
1379
|
-
// engine (design D-1) — no pid probe, no identity check. Same no-clear
|
|
1380
|
-
// rule as above — a transiently-alive engine proves nothing about a
|
|
1381
|
-
// cursor identity change.
|
|
1382
|
-
if (fleet.has(id))
|
|
1383
|
-
continue;
|
|
1384
|
-
}
|
|
1385
|
-
catch (err) {
|
|
1386
|
-
operationIdContext.fresh(() => {
|
|
1387
|
-
emitEvent({
|
|
1388
|
-
level: 'error',
|
|
1389
|
-
event: 'inbox.poll.failed',
|
|
1390
|
-
...(isSafeNodeId(id) ? { node_id: id } : { fields: { affected_node_id: id } }),
|
|
1391
|
-
error: err,
|
|
1392
|
-
});
|
|
1393
|
-
});
|
|
1394
|
-
continue;
|
|
1395
|
-
}
|
|
1396
|
-
operationIdContext.fresh(() => {
|
|
1397
|
-
try {
|
|
1398
|
-
const cursorNow = readCursor(id);
|
|
1399
|
-
const entries = readInboxSince(id, cursorNow);
|
|
1400
|
-
// #341: deferred entries never wake an idle node — by contract they ride
|
|
1401
|
-
// the target's next NATURAL cycle (any other wake reason), not a revive
|
|
1402
|
-
// this pass triggers on their own account. A deferred-only unseen set is
|
|
1403
|
-
// therefore identical to "nothing unseen" for wake-worthiness: no revive,
|
|
1404
|
-
// and no stuck-relaunch episode to track (there is nothing this pass is
|
|
1405
|
-
// even attempting). The entries themselves are untouched — still past the
|
|
1406
|
-
// cursor, still on disk — so a later revive for any OTHER reason (a
|
|
1407
|
-
// non-deferred entry landing, or an explicit revive) delivers them via the
|
|
1408
|
-
// live watcher's normal flush, same as `entries.length === 0` below.
|
|
1409
|
-
const wakeWorthy = entries.filter((e) => e.tier !== 'deferred');
|
|
1410
|
-
if (wakeWorthy.length === 0) {
|
|
1411
|
-
clearIdleReleaseRelaunchState(id); // fully delivered, or only deferred entries remain — no stuck episode to track
|
|
1412
|
-
return;
|
|
1413
|
-
}
|
|
1414
|
-
const prior = idleReleaseRelaunchState.get(id);
|
|
1415
|
-
// An exact cursor identity change since the LAST observation (or no prior
|
|
1416
|
-
// episode at all) is the ONLY thing allowed to reset the counter.
|
|
1417
|
-
const cursorChanged = prior === undefined || prior.cursor !== cursorNow;
|
|
1418
|
-
const attempts = cursorChanged ? 0 : prior.attempts;
|
|
1419
|
-
if (attempts >= IDLE_RELEASE_RELAUNCH_MAX_ATTEMPTS) {
|
|
1420
|
-
clearIdleReleaseRelaunchState(id);
|
|
1421
|
-
const meta = getNode(id);
|
|
1422
|
-
emitEvent({
|
|
1423
|
-
level: 'warn',
|
|
1424
|
-
event: 'broker.idle_release.exhausted',
|
|
1425
|
-
...(isSafeNodeId(id) ? { node_id: id } : {}),
|
|
1426
|
-
fields: {
|
|
1427
|
-
...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
|
|
1428
|
-
attempts,
|
|
1429
|
-
max_attempts: IDLE_RELEASE_RELAUNCH_MAX_ATTEMPTS,
|
|
1430
|
-
},
|
|
1431
|
-
});
|
|
1432
|
-
transition(id, 'crash');
|
|
1433
|
-
if (meta !== null) {
|
|
1434
|
-
try {
|
|
1435
|
-
fanDoctrineWake(id, subscribersOf(id), `Child crashed — ${fullName(meta)} (${id}) failed to relaunch after ${attempts} attempts while trying to ` +
|
|
1436
|
-
`deliver a pending inbox message (no delivery progress across any attempt), and is now dead. It stays ` +
|
|
1437
|
-
`dead until you revive it — \`crtr node lifecycle revive ${id}\`.`, { reason: 'child-crashed', child: id });
|
|
1438
|
-
}
|
|
1439
|
-
catch {
|
|
1440
|
-
/* best-effort, mirrors every other fan-out in this file */
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
const lastAttempt = cursorChanged ? undefined : prior?.lastAttempt;
|
|
1446
|
-
const backoff = idleReleaseRelaunchBackoffMs(attempts);
|
|
1447
|
-
if (lastAttempt !== undefined && now - lastAttempt < backoff)
|
|
1448
|
-
return; // backing off before the next attempt
|
|
1449
|
-
if (!hasBrokerCapacity(thresholds.automaticReviveCap))
|
|
1450
|
-
return; // deferred, not attempted — costs no retry budget
|
|
1451
|
-
idleReleaseRelaunchState.set(id, { cursor: cursorNow, attempts: attempts + 1, lastAttempt: now });
|
|
1452
|
-
emitEvent({
|
|
1453
|
-
level: 'info',
|
|
1454
|
-
event: 'broker.revive.idle_release',
|
|
1455
|
-
...(isSafeNodeId(id) ? { node_id: id } : {}),
|
|
1456
|
-
attempt: attempts + 1,
|
|
1457
|
-
fields: {
|
|
1458
|
-
...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
|
|
1459
|
-
attempts: attempts + 1,
|
|
1460
|
-
max_attempts: IDLE_RELEASE_RELAUNCH_MAX_ATTEMPTS,
|
|
1461
|
-
},
|
|
1462
|
-
});
|
|
1463
|
-
reviveNode(id, { resume: true });
|
|
1464
|
-
}
|
|
1465
|
-
catch (err) {
|
|
1466
|
-
emitEvent({
|
|
1467
|
-
level: 'error',
|
|
1468
|
-
event: 'inbox.poll.failed',
|
|
1469
|
-
...(isSafeNodeId(id) ? { node_id: id } : { fields: { affected_node_id: id } }),
|
|
1470
|
-
error: err,
|
|
1471
|
-
});
|
|
1472
|
-
}
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
// Third pass: recover stale cron run leases stranded by a prior daemon
|
|
1476
|
-
// instance, then kick off every due cron. Runs stay detached from the tick
|
|
1477
|
-
// and registered for graceful-teardown drain.
|
|
1478
|
-
operationIdContext.fresh(() => {
|
|
1479
|
-
try {
|
|
1480
|
-
recoverStaleCronLeases(now, { register: (settlement) => lifecycle.registerDetached(settlement) });
|
|
1481
|
-
}
|
|
1482
|
-
catch (err) {
|
|
1483
|
-
emitEvent({ level: 'error', event: 'cron.lease_recovery.failed', error: err });
|
|
1484
|
-
}
|
|
1485
|
-
});
|
|
1486
|
-
if (lifecycle.acceptsDetachedWork()) {
|
|
1487
|
-
try {
|
|
1488
|
-
runDueCrons(now, {
|
|
1489
|
-
register: (settlement) => lifecycle.registerDetached(settlement),
|
|
1490
|
-
// Fleet safety: a due run that would launch or revive a node waits for
|
|
1491
|
-
// a free slot instead of stampeding the host; it stays due meanwhile.
|
|
1492
|
-
hasBrokerCapacity: () => hasBrokerCapacity(thresholds.automaticReviveCap),
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
catch (err) {
|
|
1496
|
-
emitEvent({ level: 'error', event: 'cron.run.failed', error: err });
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
// Fourth pass: reap the leftover placement of long-dead nodes (Bug 3), then
|
|
1500
|
-
// sweep focus rows whose pane died outside the sanctioned teardown paths
|
|
1501
|
-
// (Bug 5). Reap first so the GC also catches any focus row a reaped pane
|
|
1502
|
-
// strands; last overall so neither races a focus a same-tick revive just
|
|
1503
|
-
// re-anchored.
|
|
1504
|
-
reapDeadResidue(now);
|
|
1505
|
-
gcStaleFocuses();
|
|
1506
|
-
// Fifth pass: count-based auto-prune, throttled to once per PRUNE_INTERVAL_MS
|
|
1507
|
-
// (NOT every 2s tick — it's a full-canvas DB sweep). Keeps the AUTO_PRUNE_MAX
|
|
1508
|
-
// most-recently-active nodes; live nodes are never deleted (pruneToLimit's
|
|
1509
|
-
// floor is max(AUTO_PRUNE_MAX, liveCount)). Wrapped so a sweep failure never
|
|
1510
|
-
// crashes the tick; the tick is already serialized (serial work lane), so
|
|
1511
|
-
// this runs sequentially within it.
|
|
1512
|
-
if (now - lastPruneAt >= PRUNE_INTERVAL_MS) {
|
|
1513
|
-
lastPruneAt = now;
|
|
1514
|
-
operationIdContext.fresh(() => {
|
|
1515
|
-
try {
|
|
1516
|
-
const before = listNodes().length;
|
|
1517
|
-
const { pruned } = pruneToLimit({ maxNodes: AUTO_PRUNE_MAX });
|
|
1518
|
-
if (pruned.length > 0) {
|
|
1519
|
-
emitEvent({
|
|
1520
|
-
level: 'info',
|
|
1521
|
-
event: 'canvas.auto_prune.completed',
|
|
1522
|
-
outcome: 'succeeded',
|
|
1523
|
-
fields: {
|
|
1524
|
-
before_count: before,
|
|
1525
|
-
kept_count: before - pruned.length,
|
|
1526
|
-
pruned_count: pruned.length,
|
|
1527
|
-
},
|
|
1528
|
-
});
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
catch (err) {
|
|
1532
|
-
emitEvent({ level: 'error', event: 'canvas.auto_prune.failed', error: err });
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
// Sixth pass: warm-pool GC. Collects spares whose broker is gone (every
|
|
1537
|
-
// handover kills them and nothing resumes a spare) and spares nobody claimed
|
|
1538
|
-
// inside their TTL — including the ones stranded when a runtime install
|
|
1539
|
-
// re-keyed the pool. Throttled; never throws.
|
|
1540
|
-
if (now - lastSpareSweepAt >= SPARE_SWEEP_INTERVAL_MS) {
|
|
1541
|
-
lastSpareSweepAt = now;
|
|
1542
|
-
operationIdContext.fresh(() => {
|
|
1543
|
-
const reaped = reapStaleSpares();
|
|
1544
|
-
if (reaped.length > 0) {
|
|
1545
|
-
emitEvent({
|
|
1546
|
-
level: 'info',
|
|
1547
|
-
event: 'warm_pool.spares_reaped',
|
|
1548
|
-
outcome: 'succeeded',
|
|
1549
|
-
fields: { reaped_count: reaped.length },
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
329
|
+
// Human bridge rows have no broker engine and participate in none of the
|
|
330
|
+
// recurring row-driven reconciler passes. Startup recovery applies the same
|
|
331
|
+
// classification boundary.
|
|
332
|
+
rows = rows.filter((row) => row.kind !== 'human');
|
|
333
|
+
const hasBrokerCapacity = () => brokerSupervision.hasBrokerCapacity(thresholds.automaticReviveCap, fleet);
|
|
334
|
+
controllerDeath.run(now, { rows });
|
|
335
|
+
dormantInbox.run(now, { rows, fleet, hasBrokerCapacity });
|
|
336
|
+
cronLane.run(now, { lifecycle, hasBrokerCapacity });
|
|
337
|
+
storageMaintenance.run(now);
|
|
1554
338
|
}
|
|
1555
339
|
/** Start the supervisor loop after winning the authoritative canvas claim.
|
|
1556
340
|
*
|