@north-light/crouter 0.3.232 → 0.3.234
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/dist/api/client.js +16 -15
- package/dist/builtin-memory/internal/memory-loading.md +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +14 -17
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -11
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +8 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +10 -11
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +7 -7
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +8 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +6 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +1 -1
- package/dist/clients/attach/chrome/ticket-panel.js +15 -2
- package/dist/clients/attach/overlays/graph.js +0 -3
- package/dist/clients/attach/overlays/mcp.js +1 -2
- package/dist/clients/attach/render/chat-view.js +0 -12
- package/dist/clients/attach/render/crtr-output.js +0 -2
- package/dist/clients/attach/render/tool-calls.js +0 -6
- package/dist/clients/attach/slash/dispatch.js +8 -11
- package/dist/clients/attach/viewer.js +532 -534
- package/dist/clients/conversation/projection.js +0 -2
- package/dist/clients/inbox/controller.js +22 -3
- package/dist/clients/inbox/review/companion-pane.js +0 -4
- package/dist/clients/inbox/review-adapter.js +0 -7
- package/dist/clients/inbox/tui/slots.d.ts +3 -3
- package/dist/clients/inbox/tui/types.d.ts +2 -2
- package/dist/commands/api-client.js +0 -8
- package/dist/commands/attention.js +0 -8
- package/dist/commands/canvas-browse.js +2 -1
- package/dist/commands/canvas-config.js +0 -10
- package/dist/commands/canvas-history.js +2 -1
- package/dist/commands/cron.js +5 -18
- package/dist/commands/dashboard.js +0 -2
- package/dist/commands/human/feedback.js +2 -1
- package/dist/commands/human/prompts.js +2 -5
- package/dist/commands/human/queue.js +2 -7
- package/dist/commands/human/request.js +0 -14
- package/dist/commands/human/review.js +2 -1
- package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +4 -4
- package/dist/commands/memory/find.js +2 -2
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/read.d.ts +2 -7
- package/dist/commands/memory/read.js +5 -16
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +7 -10
- package/dist/commands/memory/write.js +4 -3
- package/dist/commands/node/bash.js +2 -1
- package/dist/commands/node/create.js +2 -1
- package/dist/commands/node/inspect.js +0 -6
- package/dist/commands/node/lifecycle.js +4 -13
- package/dist/commands/node/message.js +3 -2
- package/dist/commands/node/subscription.js +2 -1
- package/dist/commands/node/wait.js +4 -3
- package/dist/commands/node-context.js +2 -1
- package/dist/commands/node-lifecycle-revive.js +0 -2
- package/dist/commands/node-worktree.js +2 -1
- package/dist/commands/pkg/browse/actions.d.ts +1 -1
- package/dist/commands/pkg/browse/actions.js +4 -4
- package/dist/commands/pkg/browse/catalog.d.ts +1 -1
- package/dist/commands/pkg/browse/catalog.js +15 -15
- package/dist/commands/pkg/browse/command-view.d.ts +1 -1
- package/dist/commands/pkg/browse/command-view.js +3 -3
- package/dist/commands/pkg/browse/detail-panel.d.ts +1 -1
- package/dist/commands/pkg/browse/detail-panel.js +8 -8
- package/dist/commands/pkg/browse/doc-view.d.ts +1 -1
- package/dist/commands/pkg/browse/doc-view.js +3 -3
- package/dist/commands/pkg/browse/list-panel.d.ts +1 -1
- package/dist/commands/pkg/browse/list-panel.js +3 -3
- package/dist/commands/pkg/browse/model.d.ts +3 -3
- package/dist/commands/pkg/browse/shell.d.ts +2 -2
- package/dist/commands/pkg/browse/shell.js +13 -13
- package/dist/commands/pkg/plugin-manage.js +2 -1
- package/dist/commands/pkg/shared.js +0 -2
- package/dist/commands/profile/delete.js +2 -1
- package/dist/commands/push.js +2 -3
- package/dist/commands/revive.js +0 -2
- package/dist/commands/surface/node/placement.js +2 -1
- package/dist/commands/surface-edit.js +2 -1
- package/dist/commands/sys/config.js +0 -4
- package/dist/commands/sys/daemon.js +0 -10
- package/dist/commands/sys/doctor.js +3 -2
- package/dist/commands/sys/panels/broker-limits-panel.d.ts +2 -2
- package/dist/commands/sys/panels/broker-limits-panel.js +4 -4
- package/dist/commands/sys/panels/dependencies-panel.d.ts +2 -2
- package/dist/commands/sys/panels/dependencies-panel.js +2 -2
- package/dist/commands/sys/panels/keybindings-panel.d.ts +1 -1
- package/dist/commands/sys/panels/keybindings-panel.js +5 -5
- package/dist/commands/sys/panels/models-panel.d.ts +4 -4
- package/dist/commands/sys/panels/models-panel.js +11 -10
- package/dist/commands/sys/panels/plugins-panel.d.ts +2 -2
- package/dist/commands/sys/panels/plugins-panel.js +2 -2
- package/dist/commands/sys/panels/prefs-panel.d.ts +2 -2
- package/dist/commands/sys/panels/prefs-panel.js +4 -4
- package/dist/commands/sys/panels/profiles-panel.d.ts +1 -1
- package/dist/commands/sys/panels/profiles-panel.js +14 -13
- package/dist/commands/sys/panels/provider-panel.d.ts +3 -3
- package/dist/commands/sys/panels/provider-panel.js +8 -8
- package/dist/commands/sys/settings-shell.d.ts +3 -3
- package/dist/commands/sys/settings-shell.js +15 -15
- package/dist/commands/sys/setup-settings-runner.d.ts +3 -3
- package/dist/commands/sys/setup-settings-runner.js +5 -5
- package/dist/commands/sys/setup-wizard.d.ts +4 -4
- package/dist/commands/sys/setup-wizard.js +8 -8
- package/dist/commands/sys/support/prepare.js +4 -4
- package/dist/commands/sys/support/submit.js +2 -2
- package/dist/commands/sys/sync-deps.js +0 -2
- package/dist/commands/sys/sync-project-guidance.js +2 -1
- package/dist/commands/sys/sysprompt.js +2 -1
- package/dist/core/__tests__/boot.test.js +0 -8
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +23 -1
- package/dist/core/__tests__/canvas-db-boundary.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-db-boundary.test.js +46 -0
- package/dist/core/__tests__/canvas.test.js +1 -1
- package/dist/core/__tests__/child-death-wake.test.js +0 -2
- package/dist/core/__tests__/cron-armed-context-failure.test.d.ts +1 -0
- package/dist/core/__tests__/cron-armed-context-failure.test.js +145 -0
- package/dist/core/__tests__/daemon-wedge.test.js +0 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +0 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +4 -14
- package/dist/core/__tests__/fixtures/fake-pi-host.js +5 -5
- package/dist/core/__tests__/focuses.test.js +0 -6
- package/dist/core/__tests__/fs-utils.test.d.ts +1 -0
- package/dist/core/__tests__/fs-utils.test.js +50 -0
- package/dist/core/__tests__/helpers/harness.js +1 -1
- package/dist/core/__tests__/inline-memory-refs.test.js +0 -2
- package/dist/core/__tests__/integration/broker-fork-seam.test.js +0 -2
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +3 -16
- package/dist/core/__tests__/integration/command-plugins.test.js +0 -24
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -6
- package/dist/core/__tests__/integration/flagship-lifecycle.test.js +0 -26
- package/dist/core/__tests__/integration/live-mutation-verbs.test.js +0 -4
- package/dist/core/__tests__/integration/live-mutation.test.js +1 -5
- package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +1 -5
- package/dist/core/__tests__/integration/revive.test.js +0 -12
- package/dist/core/__tests__/integration/subscription-delivery.test.js +0 -6
- package/dist/core/__tests__/integration/tmux-surface.test.js +24 -8
- package/dist/core/__tests__/integration/worktree-reap.test.js +0 -6
- package/dist/core/__tests__/lifecycle.test.js +0 -18
- package/dist/core/__tests__/memory-resolver-precedence.test.js +1 -1
- package/dist/core/__tests__/migration.test.js +2 -2
- package/dist/core/__tests__/on-read-dedup-resume.test.js +3 -3
- package/dist/core/__tests__/pid-identity-match.test.js +1 -1
- package/dist/core/__tests__/plugin-kinds.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +1 -1
- package/dist/core/__tests__/preview-registry-sync.test.js +42 -1
- package/dist/core/__tests__/respawn-throttle.test.js +0 -4
- package/dist/core/__tests__/revive-all.test.js +5 -5
- package/dist/core/__tests__/revive-parked-fresh.test.js +62 -11
- package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +0 -8
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +0 -8
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +0 -2
- package/dist/core/__tests__/tmux-driver-boundary.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-driver-boundary.test.js +50 -0
- package/dist/core/activity-signal.d.ts +7 -0
- package/dist/core/activity-signal.js +38 -0
- package/dist/core/auto-update.js +2 -1
- package/dist/core/bootstrap.js +2 -1
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
- package/dist/core/canvas/__tests__/remote-transport.test.js +1 -1
- package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
- package/dist/core/canvas/browse/app.js +3 -2
- package/dist/core/canvas/browse/model.d.ts +1 -1
- package/dist/core/canvas/browse/model.js +0 -4
- package/dist/core/canvas/canvas.d.ts +22 -0
- package/dist/core/canvas/canvas.js +50 -18
- package/dist/core/canvas/crons.js +0 -6
- package/dist/core/canvas/db.d.ts +14 -20
- package/dist/core/canvas/db.js +30 -1332
- package/dist/core/canvas/focuses.js +0 -4
- package/dist/core/canvas/history.js +2 -1
- package/dist/core/canvas/migrations.d.ts +20 -0
- package/dist/core/canvas/migrations.js +1339 -0
- package/dist/core/canvas/nav-model.js +1 -11
- package/dist/core/canvas/nav-render.js +0 -8
- package/dist/core/canvas/remote-canvas-source.js +0 -2
- package/dist/core/canvas/render-source.js +31 -13
- package/dist/core/command-manifests/manifest.js +0 -10
- package/dist/core/command-manifests/schema.js +0 -4
- package/dist/core/command-plugins/transport/http-invoke.js +0 -14
- package/dist/core/command.js +0 -4
- package/dist/core/config.js +2 -1
- package/dist/core/feed/inbox.d.ts +10 -0
- package/dist/core/feed/inbox.js +18 -12
- package/dist/core/fs-utils.d.ts +9 -2
- package/dist/core/fs-utils.js +7 -15
- package/dist/core/help.js +0 -8
- package/dist/core/host-exports/export.d.ts +2 -2
- package/dist/core/host-exports/export.js +9 -12
- package/dist/core/human/answer.d.ts +2 -2
- package/dist/core/human/convention.d.ts +2 -5
- package/dist/core/human/convention.js +3 -13
- package/dist/core/human/page-schema.d.ts +2 -2
- package/dist/core/human/page-schema.js +2 -2
- package/dist/core/human/root.js +6 -23
- package/dist/core/human/scan.js +24 -7
- package/dist/core/human/tickets.d.ts +4 -1
- package/dist/core/human/tickets.js +20 -6
- package/dist/core/io.js +0 -12
- package/dist/core/memory/identity.js +0 -4
- package/dist/core/memory/inline-ref-inventory.js +3 -2
- package/dist/core/memory/lint.js +3 -6
- package/dist/core/memory-resolver.d.ts +5 -0
- package/dist/core/memory-resolver.js +14 -12
- package/dist/core/pagination.js +0 -2
- package/dist/core/preview-registry.d.ts +14 -0
- package/dist/core/preview-registry.js +2 -4
- package/dist/core/preview-result-path.js +2 -1
- package/dist/core/profiles/manifest.d.ts +37 -5
- package/dist/core/profiles/manifest.js +44 -24
- package/dist/core/profiles/select.js +0 -4
- package/dist/core/review/signal.js +4 -23
- package/dist/core/review/ticket-filter.d.ts +3 -3
- package/dist/core/review/ticket-filter.js +4 -9
- package/dist/core/runtime/__tests__/engine-route.test.js +1 -1
- package/dist/core/runtime/__tests__/spawn-env.test.js +0 -8
- package/dist/core/runtime/bearings-render.d.ts +56 -0
- package/dist/core/runtime/bearings-render.js +241 -0
- package/dist/core/runtime/bearings.d.ts +2 -17
- package/dist/core/runtime/bearings.js +12 -198
- package/dist/core/runtime/broker/auth-reload.d.ts +39 -0
- package/dist/core/runtime/broker/auth-reload.js +149 -0
- package/dist/core/runtime/broker/engine-drive.d.ts +26 -0
- package/dist/core/runtime/broker/engine-drive.js +257 -0
- package/dist/core/runtime/broker/engine-routing.d.ts +143 -0
- package/dist/core/runtime/broker/engine-routing.js +211 -0
- package/dist/core/runtime/broker/event-projection.js +3 -2
- package/dist/core/runtime/broker/fault-retry.js +5 -4
- package/dist/core/runtime/broker/frame-client.d.ts +38 -0
- package/dist/core/runtime/broker/frame-client.js +129 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +12 -189
- package/dist/core/runtime/broker/frame-dispatch.js +89 -1476
- package/dist/core/runtime/broker/frame-engine-control.d.ts +20 -0
- package/dist/core/runtime/broker/frame-engine-control.js +231 -0
- package/dist/core/runtime/broker/frame-memory-refs.d.ts +19 -0
- package/dist/core/runtime/broker/frame-memory-refs.js +41 -0
- package/dist/core/runtime/broker/frame-model-query.d.ts +13 -0
- package/dist/core/runtime/broker/frame-model-query.js +69 -0
- package/dist/core/runtime/broker/frame-read.d.ts +19 -0
- package/dist/core/runtime/broker/frame-read.js +116 -0
- package/dist/core/runtime/broker/frame-replies.d.ts +24 -0
- package/dist/core/runtime/broker/frame-replies.js +64 -0
- package/dist/core/runtime/broker/frame-session.d.ts +34 -0
- package/dist/core/runtime/broker/frame-session.js +203 -0
- package/dist/core/runtime/broker/turn-ignition.d.ts +42 -0
- package/dist/core/runtime/broker/turn-ignition.js +74 -0
- package/dist/core/runtime/broker-extension-render.js +9 -85
- package/dist/core/runtime/broker-protocol.js +0 -4
- package/dist/core/runtime/broker-sdk.js +2 -1
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +5 -23
- package/dist/core/runtime/fault.js +3 -2
- package/dist/core/runtime/headless-pi.js +3 -2
- package/dist/core/runtime/host.js +2 -3
- package/dist/core/runtime/kickoff.js +0 -4
- package/dist/core/runtime/launch-target.js +2 -1
- package/dist/core/runtime/launch.js +3 -6
- package/dist/core/runtime/lifecycle.js +17 -26
- package/dist/core/runtime/managed-provider-cooling.d.ts +1 -1
- package/dist/core/runtime/naming.js +0 -2
- package/dist/core/runtime/nodes.js +2 -1
- package/dist/core/runtime/persona.js +0 -6
- package/dist/core/runtime/placement-tmux.js +2 -5
- package/dist/core/runtime/placement.js +4 -11
- package/dist/core/runtime/recap.js +4 -3
- package/dist/core/runtime/reopen.js +4 -7
- package/dist/core/runtime/reset.js +0 -4
- package/dist/core/runtime/resume-root.js +0 -2
- package/dist/core/runtime/revive.js +17 -13
- package/dist/core/runtime/session-list-cache.js +2 -2
- package/dist/core/runtime/shell-expansion.js +0 -2
- package/dist/core/runtime/spawn.js +0 -2
- package/dist/core/runtime/stream-watchdog.d.ts +0 -1
- package/dist/core/runtime/stream-watchdog.js +13 -18
- package/dist/core/runtime/tmux-bindings.js +0 -2
- package/dist/core/runtime/tmux-driver.d.ts +12 -0
- package/dist/core/runtime/tmux-driver.js +18 -13
- package/dist/core/runtime/warm-pool.js +0 -4
- package/dist/core/scope.js +4 -3
- package/dist/core/spawn.js +2 -3
- package/dist/core/subscription-state.js +10 -17
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +92 -0
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +6 -0
- package/dist/core/substrate/injected-store.js +24 -0
- package/dist/core/substrate/on-read.d.ts +17 -1
- package/dist/core/substrate/on-read.js +36 -2
- package/dist/core/substrate/render.js +0 -12
- package/dist/core/substrate/schema.d.ts +3 -3
- package/dist/core/substrate/schema.js +4 -16
- package/dist/core/substrate/surface-match.d.ts +19 -0
- package/dist/core/substrate/surface-match.js +60 -2
- package/dist/core/termrender/display.js +2 -1
- package/dist/core/tui/page-host.d.ts +3 -3
- package/dist/core/tui/page-host.js +4 -4
- package/dist/core/tui/panel.js +3 -3
- package/dist/core/wake.js +0 -12
- package/dist/core/worktree.js +0 -2
- package/dist/daemon/__tests__/reconciler-signature.test.d.ts +1 -0
- package/dist/daemon/__tests__/reconciler-signature.test.js +36 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +2 -16
- package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +3 -11
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +0 -6
- package/dist/daemon/api/handlers/attach.js +2 -1
- package/dist/daemon/api/handlers/broker-ops.js +3 -6
- package/dist/daemon/api/handlers/canvas.js +12 -18
- package/dist/daemon/api/handlers/feedback-comments.js +0 -8
- package/dist/daemon/api/handlers/health.js +3 -2
- package/dist/daemon/api/handlers/human-requests.js +0 -10
- package/dist/daemon/api/handlers/human.js +1 -5
- package/dist/daemon/api/handlers/inbox.js +0 -18
- package/dist/daemon/api/handlers/memory.js +1 -1
- package/dist/daemon/api/handlers/messages.js +4 -8
- package/dist/daemon/api/handlers/nodes.js +10 -21
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/reviews.js +1 -6
- package/dist/daemon/api/map.js +0 -6
- package/dist/daemon/api/router.js +0 -4
- package/dist/daemon/api/server.js +2 -5
- package/dist/daemon/boot-migrations.d.ts +14 -0
- package/dist/daemon/boot-migrations.js +31 -0
- package/dist/daemon/cron/armed-context.d.ts +21 -0
- package/dist/daemon/cron/armed-context.js +100 -0
- package/dist/daemon/cron/capture.d.ts +20 -0
- package/dist/daemon/cron/capture.js +45 -0
- package/dist/daemon/cron/disposition.d.ts +19 -0
- package/dist/daemon/cron/disposition.js +52 -0
- package/dist/daemon/cron/escalation.d.ts +12 -0
- package/dist/daemon/cron/escalation.js +124 -0
- package/dist/daemon/cron/held.d.ts +4 -0
- package/dist/daemon/cron/held.js +20 -0
- package/dist/daemon/cron/lease-recovery.d.ts +3 -0
- package/dist/daemon/cron/lease-recovery.js +119 -0
- package/dist/daemon/cron/live-runs.d.ts +11 -0
- package/dist/daemon/cron/live-runs.js +23 -0
- package/dist/daemon/cron/passes.d.ts +11 -0
- package/dist/daemon/cron/passes.js +78 -0
- package/dist/daemon/cron/settle.d.ts +28 -0
- package/dist/daemon/cron/settle.js +37 -0
- package/dist/daemon/cron/sinks.d.ts +2 -0
- package/dist/daemon/cron/sinks.js +100 -0
- package/dist/daemon/cron-run.d.ts +5 -41
- package/dist/daemon/cron-run.js +193 -882
- package/dist/daemon/crtrd-cli.js +2 -1
- package/dist/daemon/crtrd.js +22 -22
- package/dist/daemon/fleet.js +0 -4
- package/dist/daemon/human/deliver-action.js +2 -1
- package/dist/daemon/human/sweep.js +15 -4
- package/dist/daemon/manage.d.ts +2 -2
- package/dist/daemon/manage.js +4 -13
- package/dist/daemon/pidfile.js +2 -2
- package/dist/daemon/reconcilers/broker-supervision.js +26 -29
- package/dist/daemon/reconcilers/dormant-inbox.d.ts +1 -1
- package/dist/daemon/reconcilers/dormant-inbox.js +2 -1
- package/dist/daemon/reconcilers/live-obligation.js +2 -2
- package/dist/daemon/reconcilers/pending-review-submit.d.ts +1 -1
- package/dist/daemon/reconcilers/pending-review-submit.js +2 -1
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -1
- package/dist/daemon/reconcilers/storage-maintenance.js +2 -1
- package/dist/daemon/review/companion.js +2 -2
- package/dist/daemon/review/sweep.js +8 -3
- package/dist/migrations/001-surfaces-frontmatter.js +0 -6
- package/dist/migrations/corpus.js +2 -5
- package/dist/migrations/registry.js +15 -2
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +0 -4
- package/dist/pi-extensions/__tests__/pre-command-gate.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +220 -0
- package/dist/pi-extensions/broker-local.js +2 -1
- package/dist/pi-extensions/canvas-bash-valve.js +3 -17
- package/dist/pi-extensions/canvas-context-intro.js +2 -7
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +14 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +77 -5
- package/dist/pi-extensions/canvas-goal-capture.js +2 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +5 -12
- package/dist/pi-extensions/canvas-passive-context.js +2 -5
- package/dist/pi-extensions/canvas-preview-result.js +2 -1
- package/dist/pi-extensions/canvas-prompt-scrub.js +2 -1
- package/dist/pi-extensions/canvas-recap.js +3 -16
- package/dist/pi-extensions/canvas-review-boundary.js +2 -5
- package/dist/pi-extensions/canvas-stophook.js +2 -26
- package/dist/pi-extensions/canvas-structured-output.js +2 -1
- package/dist/pi-extensions/canvas-tool-guide.js +2 -1
- package/dist/shared/__tests__/env-boundary.test.d.ts +1 -0
- package/dist/shared/__tests__/env-boundary.test.js +93 -0
- package/dist/shared/env.d.ts +68 -0
- package/dist/shared/env.js +171 -0
- package/package.json +3 -1
- package/runtime.lock.json +2 -2
- package/dist/core/canvas/render.d.ts +0 -47
- package/dist/core/canvas/render.js +0 -245
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The ordered boot-migration list — the THIRD of this repo's three migration
|
|
2
|
+
// homes, and the one for filesystem repairs that must run unconditionally at
|
|
3
|
+
// every daemon boot, before supervision can revive anything. No version gate
|
|
4
|
+
// (there is no single database to hold a `user_version`, and no `crtr sys
|
|
5
|
+
// migrate` invocation to drive it): each entry converges on its own, the same
|
|
6
|
+
// way `migrateLegacyInboxIdentities` already does — a re-run over
|
|
7
|
+
// already-migrated state rewrites nothing.
|
|
8
|
+
//
|
|
9
|
+
// The other two homes:
|
|
10
|
+
// - `../core/canvas/migrations.ts` — canvas.db's versioned schema chain,
|
|
11
|
+
// gated by `PRAGMA user_version`, applied once per database inside
|
|
12
|
+
// `migrate()`.
|
|
13
|
+
// - `../migrations/registry.ts` — the `STATE_MIGRATIONS` registry for
|
|
14
|
+
// on-disk memory documents, profile manifests, and the corpus lane,
|
|
15
|
+
// driven by `crtr sys migrate`.
|
|
16
|
+
//
|
|
17
|
+
// Authoring contract: one entry per boot-time filesystem repair, appended
|
|
18
|
+
// here in order. `crtrd.ts` iterates this list once at boot and emits one
|
|
19
|
+
// `daemon.boot_migration.applied` event per entry that reports a change —
|
|
20
|
+
// add a step here instead of a bespoke call in the boot sequence.
|
|
21
|
+
import { migrateLegacyInboxIdentities } from '../core/feed/inbox-migrate.js';
|
|
22
|
+
export const BOOT_MIGRATIONS = [
|
|
23
|
+
{
|
|
24
|
+
id: 'legacy-inbox-identities',
|
|
25
|
+
description: 'normalize pre-cutover inbox entry/operation ids (UUID-spelled or missing) and cursors (ISO-timestamp boundaries) into canonical 32-lowercase-hex identities the strict inbox reader requires.',
|
|
26
|
+
apply: () => {
|
|
27
|
+
const summary = migrateLegacyInboxIdentities();
|
|
28
|
+
return { changed: summary.nodes > 0 || summary.failures > 0, summary: { ...summary } };
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Cron } from '../../core/canvas/crons.js';
|
|
2
|
+
/** The cron's profile, re-validated now and returned in canonical id form.
|
|
3
|
+
*
|
|
4
|
+
* A DELETED profile is the spec's root fallback: null. An UNREADABLE one is
|
|
5
|
+
* not — the profile is still there, so running as root would execute the
|
|
6
|
+
* command under an identity nobody armed, with the profile's own env store and
|
|
7
|
+
* purview silently replaced. That throws, and the run fails loudly once
|
|
8
|
+
* instead of running wrong on every tick. */
|
|
9
|
+
export declare function resolveLiveProfile(c: Cron): string | null;
|
|
10
|
+
export declare function resolveLiveProfileOrRoot(c: Cron): string | null;
|
|
11
|
+
export declare function resolveLiveProfileName(c: Cron): string | undefined;
|
|
12
|
+
/** A stored context cannot be rebuilt; the run fails rather than running under a context nobody armed. */
|
|
13
|
+
export declare class ArmedContextError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export interface ArmedRunContext {
|
|
17
|
+
profileId: string | null;
|
|
18
|
+
env: NodeJS.ProcessEnv;
|
|
19
|
+
}
|
|
20
|
+
/** Rebuild the armed context. Corrupt snapshots and unreadable profiles fail rather than silently changing its identity or environment. */
|
|
21
|
+
export declare function buildArmedRunContext(c: Cron, runId: string): ArmedRunContext;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { envHomeOverride } from '../../shared/env.js';
|
|
2
|
+
import { loadProfileManifest, lookupProfile } from '../../core/profiles/manifest.js';
|
|
3
|
+
import { readProfileEnvVars } from '../../core/profiles/env-store.js';
|
|
4
|
+
import { ADMITTED_HOST_PATH_ENV, buildOperationalEnvBase } from '../../core/runtime/spawn-env.js';
|
|
5
|
+
/** The cron's profile, re-validated now and returned in canonical id form.
|
|
6
|
+
*
|
|
7
|
+
* A DELETED profile is the spec's root fallback: null. An UNREADABLE one is
|
|
8
|
+
* not — the profile is still there, so running as root would execute the
|
|
9
|
+
* command under an identity nobody armed, with the profile's own env store and
|
|
10
|
+
* purview silently replaced. That throws, and the run fails loudly once
|
|
11
|
+
* instead of running wrong on every tick. */
|
|
12
|
+
export function resolveLiveProfile(c) {
|
|
13
|
+
if (c.profile == null)
|
|
14
|
+
return null;
|
|
15
|
+
let lookup;
|
|
16
|
+
try {
|
|
17
|
+
lookup = lookupProfile(c.profile);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null; // an ambiguous NAME resolves no better than a deleted profile
|
|
21
|
+
}
|
|
22
|
+
if (lookup.kind === 'resolved')
|
|
23
|
+
return lookup.entry.profileId;
|
|
24
|
+
if (lookup.kind === 'absent')
|
|
25
|
+
return null;
|
|
26
|
+
throw new ArmedContextError(`the armed profile "${c.profile}" is still on this host but its manifest could not be read (${lookup.manifestPath}) — running as root would silently give this cron a different identity, env store, and purview. Repair the manifest, or re-arm the cron under a profile that resolves.`);
|
|
27
|
+
}
|
|
28
|
+
// The escalation node must still report an unreadable manifest, so its own spawn falls back to root.
|
|
29
|
+
export function resolveLiveProfileOrRoot(c) {
|
|
30
|
+
try {
|
|
31
|
+
return resolveLiveProfile(c);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function resolveLiveProfileName(c) {
|
|
38
|
+
if (c.profile == null)
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
return loadProfileManifest(c.profile).manifest.name;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** A stored context cannot be rebuilt; the run fails rather than running under a context nobody armed. */
|
|
48
|
+
export class ArmedContextError extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'ArmedContextError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Invalid snapshots are corruption: dropping `--env` would run under an environment nobody armed, so fail instead.
|
|
55
|
+
function parseArmedEnv(envJson) {
|
|
56
|
+
if (envJson == null)
|
|
57
|
+
return {};
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(envJson);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
throw new ArmedContextError(`the armed env snapshot is not valid JSON (${err.message}) — this cron's stored env_json is corrupt, so the run would silently drop every armed variable. Re-arm the cron with a valid --env.`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
66
|
+
throw new ArmedContextError("the armed env snapshot is not a JSON object — this cron's stored env_json is corrupt. Re-arm the cron with a valid --env.");
|
|
67
|
+
}
|
|
68
|
+
const extraEnv = {};
|
|
69
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
70
|
+
if (typeof v !== 'string') {
|
|
71
|
+
throw new ArmedContextError(`the armed env snapshot's value for "${k}" is not a string — this cron's stored env_json is corrupt, so the run would silently drop that variable. Re-arm the cron with a valid --env.`);
|
|
72
|
+
}
|
|
73
|
+
extraEnv[k] = v;
|
|
74
|
+
}
|
|
75
|
+
return extraEnv;
|
|
76
|
+
}
|
|
77
|
+
/** Rebuild the armed context. Corrupt snapshots and unreadable profiles fail rather than silently changing its identity or environment. */
|
|
78
|
+
export function buildArmedRunContext(c, runId) {
|
|
79
|
+
const extraEnv = parseArmedEnv(c.env_json);
|
|
80
|
+
const profileId = resolveLiveProfile(c);
|
|
81
|
+
const operationalEnv = buildOperationalEnvBase({ targetCwd: c.cwd, targetProfileId: profileId });
|
|
82
|
+
const env = {
|
|
83
|
+
...operationalEnv,
|
|
84
|
+
...readProfileEnvVars(profileId),
|
|
85
|
+
...extraEnv,
|
|
86
|
+
...(profileId !== null ? { CRTR_PROFILE_ID: profileId } : {}),
|
|
87
|
+
CRTR_CRON_ID: c.cron_id,
|
|
88
|
+
CRTR_CRON_NAME: c.name,
|
|
89
|
+
CRTR_RUN_ID: runId,
|
|
90
|
+
};
|
|
91
|
+
// Preserve the daemon identity: otherwise child `crtr` resolves the default canvas.
|
|
92
|
+
const canvasHomeOverride = envHomeOverride();
|
|
93
|
+
if (canvasHomeOverride !== undefined && canvasHomeOverride !== '') {
|
|
94
|
+
env['CRTR_HOME'] = canvasHomeOverride;
|
|
95
|
+
}
|
|
96
|
+
if (c.anchor_node != null)
|
|
97
|
+
env['CRTR_ANCHOR'] = c.anchor_node;
|
|
98
|
+
env[ADMITTED_HOST_PATH_ENV] = operationalEnv[ADMITTED_HOST_PATH_ENV];
|
|
99
|
+
return { profileId, env };
|
|
100
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Output head caps for the run log (spec: "capped, e.g. 4KB"). */
|
|
2
|
+
export declare const CRON_OUTPUT_HEAD_BYTES: number;
|
|
3
|
+
/** Bound between the run-timeout SIGTERM and a forced SIGKILL escalation, so
|
|
4
|
+
* a signal-trapping command cannot stall teardown's detached drain. */
|
|
5
|
+
export declare const CRON_KILL_GRACE_MS = 2000;
|
|
6
|
+
/** Poll interval while waiting for a SIGTERMed stale-lease run to actually die
|
|
7
|
+
* (each probe is a `ps` call, so this is deliberately coarse). */
|
|
8
|
+
export declare const CRON_DEATH_POLL_MS = 250;
|
|
9
|
+
/** sha256 of empty stdout — the change-detection hash a run that produced no
|
|
10
|
+
* output carries (a failed spawn, a recovered stale lease). */
|
|
11
|
+
export declare const EMPTY_STDOUT_SHA256: string;
|
|
12
|
+
/** A byte-capped capture of one output stream's head. Bytes are buffered and
|
|
13
|
+
* decoded exactly once, at the end — decoding per chunk both mis-measured the
|
|
14
|
+
* cap (UTF-16 code units, not bytes) and corrupted any multi-byte sequence
|
|
15
|
+
* that straddled a chunk boundary. Truncation is MARKED, never silent: a
|
|
16
|
+
* clipped body reads as complete output to whoever receives the delivery. */
|
|
17
|
+
export declare function newHeadCapture(): {
|
|
18
|
+
take(chunk: Buffer): void;
|
|
19
|
+
decode(): string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/daemon/cron/capture.ts — output capture for a cron run's child process,
|
|
2
|
+
// plus the timing/hash constants the capture and kill mechanics share.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
/** Output head caps for the run log (spec: "capped, e.g. 4KB"). */
|
|
5
|
+
export const CRON_OUTPUT_HEAD_BYTES = 4 * 1024;
|
|
6
|
+
/** Bound between the run-timeout SIGTERM and a forced SIGKILL escalation, so
|
|
7
|
+
* a signal-trapping command cannot stall teardown's detached drain. */
|
|
8
|
+
export const CRON_KILL_GRACE_MS = 2000;
|
|
9
|
+
/** Poll interval while waiting for a SIGTERMed stale-lease run to actually die
|
|
10
|
+
* (each probe is a `ps` call, so this is deliberately coarse). */
|
|
11
|
+
export const CRON_DEATH_POLL_MS = 250;
|
|
12
|
+
/** sha256 of empty stdout — the change-detection hash a run that produced no
|
|
13
|
+
* output carries (a failed spawn, a recovered stale lease). */
|
|
14
|
+
export const EMPTY_STDOUT_SHA256 = createHash('sha256').update('').digest('hex');
|
|
15
|
+
/** A byte-capped capture of one output stream's head. Bytes are buffered and
|
|
16
|
+
* decoded exactly once, at the end — decoding per chunk both mis-measured the
|
|
17
|
+
* cap (UTF-16 code units, not bytes) and corrupted any multi-byte sequence
|
|
18
|
+
* that straddled a chunk boundary. Truncation is MARKED, never silent: a
|
|
19
|
+
* clipped body reads as complete output to whoever receives the delivery. */
|
|
20
|
+
export function newHeadCapture() {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
let truncated = false;
|
|
24
|
+
return {
|
|
25
|
+
take(chunk) {
|
|
26
|
+
const room = CRON_OUTPUT_HEAD_BYTES - bytes;
|
|
27
|
+
if (room <= 0) {
|
|
28
|
+
truncated = true;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (chunk.length > room) {
|
|
32
|
+
chunks.push(chunk.subarray(0, room));
|
|
33
|
+
bytes = CRON_OUTPUT_HEAD_BYTES;
|
|
34
|
+
truncated = true;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
bytes += chunk.length;
|
|
39
|
+
},
|
|
40
|
+
decode() {
|
|
41
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
42
|
+
return truncated ? `${text}\n[… output truncated at ${CRON_OUTPUT_HEAD_BYTES} bytes]` : text;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Cron } from '../../core/canvas/crons.js';
|
|
2
|
+
export interface RunSettleContext {
|
|
3
|
+
runId: string;
|
|
4
|
+
startedAtIso: string;
|
|
5
|
+
exitCode: number | null;
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
runTimeoutS: number;
|
|
8
|
+
processErrorMessage: string | null;
|
|
9
|
+
stdoutHead: string;
|
|
10
|
+
/** Hash the full output: a capped head can hide a change. */
|
|
11
|
+
stdoutSha256: string;
|
|
12
|
+
stderrHead: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Disposition {
|
|
15
|
+
delivered: string;
|
|
16
|
+
/** A paused row is retained so `cron resume` can re-arm it. */
|
|
17
|
+
paused: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function disposeSettledRun(c: Cron, ctx: RunSettleContext, outOfBand: boolean, selfCanceled?: boolean): Promise<Disposition>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getCron, setCronLastOutputHash } from '../../core/canvas/crons.js';
|
|
2
|
+
import { escalateFailure, failureSummary } from './escalation.js';
|
|
3
|
+
import { deliverToSink } from './sinks.js';
|
|
4
|
+
export async function disposeSettledRun(c, ctx, outOfBand, selfCanceled = false) {
|
|
5
|
+
const prefix = outOfBand ? 'out-of-band: ' : '';
|
|
6
|
+
// A self-cancel still delivers its already-produced output; an external cancellation does not.
|
|
7
|
+
const fresh = getCron(c.cron_id);
|
|
8
|
+
if (fresh === null && !selfCanceled)
|
|
9
|
+
return { delivered: `${prefix}cron canceled mid-run — nothing delivered`, paused: false };
|
|
10
|
+
const effective = fresh ?? c;
|
|
11
|
+
const failed = ctx.timedOut || ctx.processErrorMessage !== null || ctx.exitCode !== 0;
|
|
12
|
+
// `cron run` compares but never rewrites the scheduled baseline.
|
|
13
|
+
let changed = true;
|
|
14
|
+
if (effective.on_output === 'on-change') {
|
|
15
|
+
changed = effective.last_output_hash !== ctx.stdoutSha256;
|
|
16
|
+
if (!outOfBand && fresh !== null)
|
|
17
|
+
setCronLastOutputHash(c.cron_id, ctx.stdoutSha256);
|
|
18
|
+
}
|
|
19
|
+
if (failed) {
|
|
20
|
+
if (effective.on_output === 'silent')
|
|
21
|
+
return { delivered: `${prefix}silent`, paused: false };
|
|
22
|
+
if (outOfBand)
|
|
23
|
+
return { delivered: `out-of-band: ${failureSummary(ctx, null)} — not escalated`, paused: false };
|
|
24
|
+
return { delivered: await escalateFailure(effective, ctx, null), paused: true };
|
|
25
|
+
}
|
|
26
|
+
switch (effective.on_output) {
|
|
27
|
+
case 'silent':
|
|
28
|
+
return { delivered: `${prefix}silent`, paused: false };
|
|
29
|
+
case 'on-failure':
|
|
30
|
+
return { delivered: `${prefix}ok — nothing to deliver (on-failure)`, paused: false };
|
|
31
|
+
case 'on-change':
|
|
32
|
+
if (!changed)
|
|
33
|
+
return { delivered: `${prefix}unchanged — not delivered (on-change)`, paused: false };
|
|
34
|
+
return deliverOrEscalate(effective, ctx, outOfBand, prefix);
|
|
35
|
+
case 'always':
|
|
36
|
+
return deliverOrEscalate(effective, ctx, outOfBand, prefix);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function deliverOrEscalate(c, ctx, outOfBand, prefix) {
|
|
40
|
+
if (ctx.stdoutHead.trim() === '') {
|
|
41
|
+
return { delivered: `${prefix}empty stdout — nothing delivered`, paused: false };
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return { delivered: `${prefix}${await deliverToSink(c, ctx.stdoutHead)}`, paused: false };
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const why = `delivery failed: ${err.message}`;
|
|
48
|
+
if (outOfBand)
|
|
49
|
+
return { delivered: `out-of-band: ${why} — not escalated`, paused: false };
|
|
50
|
+
return { delivered: await escalateFailure(c, ctx, why), paused: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Cron } from '../../core/canvas/crons.js';
|
|
2
|
+
import type { RunSettleContext } from './disposition.js';
|
|
3
|
+
export declare function failureSummary(ctx: RunSettleContext, deliveryFailure: string | null): string;
|
|
4
|
+
/** Pause the cron and spawn the escalation node in the cron's originating
|
|
5
|
+
* profile (root fallback). Returns the run-log `delivered` string. Never
|
|
6
|
+
* throws: an escalation spawn failure is recorded and logged, the pause
|
|
7
|
+
* stands (the row must not keep refiring a broken command). */
|
|
8
|
+
export declare function escalateFailure(c: Cron, ctx: RunSettleContext, deliveryFailure: string | null): Promise<string>;
|
|
9
|
+
/** The delivery-failure line an escalated crashed ONE-SHOT carries: its side
|
|
10
|
+
* effects may already have landed in full, so a human/agent decides whether to
|
|
11
|
+
* redo it rather than the daemon re-firing it. */
|
|
12
|
+
export declare function crashedOneShotSummary(why: string): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/daemon/cron/escalation.ts — the cron failure path: pause the row and
|
|
2
|
+
// spawn a terminal node holding everything it needs to deal with the failure.
|
|
3
|
+
// Pausing IS the dedup — a paused row is never due, so nothing refires while
|
|
4
|
+
// the escalation node works.
|
|
5
|
+
import { getNode } from '../../core/canvas/canvas.js';
|
|
6
|
+
import { fullName } from '../../core/canvas/labels.js';
|
|
7
|
+
import { listCronRuns, setCronState } from '../../core/canvas/crons.js';
|
|
8
|
+
import { emitEvent } from '../../core/events/emit.js';
|
|
9
|
+
import { spawnChild } from '../../core/runtime/spawn.js';
|
|
10
|
+
import { cronWakeOrigin } from '../../core/runtime/bearings.js';
|
|
11
|
+
import { cadenceDisplay } from '../../core/wake.js';
|
|
12
|
+
import { resolveLiveProfileOrRoot } from './armed-context.js';
|
|
13
|
+
export function failureSummary(ctx, deliveryFailure) {
|
|
14
|
+
if (deliveryFailure !== null)
|
|
15
|
+
return deliveryFailure;
|
|
16
|
+
if (ctx.timedOut)
|
|
17
|
+
return `run timed out after ${ctx.runTimeoutS}s (killed; recorded exit -1)`;
|
|
18
|
+
if (ctx.processErrorMessage !== null)
|
|
19
|
+
return `run process error: ${ctx.processErrorMessage}`;
|
|
20
|
+
return `command exited ${ctx.exitCode ?? '(no exit observed)'}`;
|
|
21
|
+
}
|
|
22
|
+
/** The escalation node's kickoff: the cron's identity/config, this failure,
|
|
23
|
+
* the run-log ring, the creating-node reference, and a mandate that names its
|
|
24
|
+
* actual options (resume after verifying / cancel / ask the human). */
|
|
25
|
+
function buildEscalationKickoff(c, ctx, deliveryFailure) {
|
|
26
|
+
const creator = c.created_by != null ? getNode(c.created_by) : null;
|
|
27
|
+
const creatorLine = c.created_by == null
|
|
28
|
+
? 'created by: (no creating node recorded)'
|
|
29
|
+
: creator !== null
|
|
30
|
+
? `created by: node ${c.created_by} ("${fullName(creator)}") — still on the canvas; consider telling it what happened`
|
|
31
|
+
: `created by: node ${c.created_by} (now gone)`;
|
|
32
|
+
const schedule = c.recur != null ? `every ${cadenceDisplay(c.recur)}` : `one-shot at ${c.fire_at}`;
|
|
33
|
+
const priorRuns = listCronRuns(c.cron_id)
|
|
34
|
+
.slice(0, 10)
|
|
35
|
+
.map((r) => {
|
|
36
|
+
const exit = r.exit_code === null ? 'no exit' : `exit ${r.exit_code}`;
|
|
37
|
+
const head = (r.stderr_head ?? r.stdout_head ?? '').split('\n')[0]?.slice(0, 120) ?? '';
|
|
38
|
+
return `- ${r.started} — ${exit}, ${r.duration_ms}ms — ${r.delivered ?? ''}${head !== '' ? ` — ${head}` : ''}`;
|
|
39
|
+
});
|
|
40
|
+
return [
|
|
41
|
+
`A scheduled cron FAILED and has paused itself. You were spawned to deal with it.`,
|
|
42
|
+
'',
|
|
43
|
+
`- cron: "${c.name}" (${c.cron_id})`,
|
|
44
|
+
`- schedule: ${schedule}${c.tz != null ? ` (tz ${c.tz})` : ''}`,
|
|
45
|
+
`- context: cwd ${c.cwd}${c.profile != null ? `, profile ${c.profile}` : ''}`,
|
|
46
|
+
`- ${creatorLine}`,
|
|
47
|
+
`- this failure: ${failureSummary(ctx, deliveryFailure)}`,
|
|
48
|
+
'',
|
|
49
|
+
'## The command',
|
|
50
|
+
'',
|
|
51
|
+
'```bash',
|
|
52
|
+
c.command,
|
|
53
|
+
'```',
|
|
54
|
+
'',
|
|
55
|
+
'## This run',
|
|
56
|
+
'',
|
|
57
|
+
`- exit: ${ctx.timedOut ? '-1 (timeout)' : String(ctx.exitCode)}`,
|
|
58
|
+
`- stdout (head): ${ctx.stdoutHead === '' ? '(empty)' : ''}`,
|
|
59
|
+
...(ctx.stdoutHead !== '' ? ['```', ctx.stdoutHead, '```'] : []),
|
|
60
|
+
`- stderr (head): ${ctx.stderrHead === '' ? '(empty)' : ''}`,
|
|
61
|
+
...(ctx.stderrHead !== '' ? ['```', ctx.stderrHead, '```'] : []),
|
|
62
|
+
'',
|
|
63
|
+
'## Recent runs (most recent first, this failure not yet recorded)',
|
|
64
|
+
'',
|
|
65
|
+
...(priorRuns.length > 0 ? priorRuns : ['(no prior runs recorded)']),
|
|
66
|
+
'',
|
|
67
|
+
'## Your mandate',
|
|
68
|
+
'',
|
|
69
|
+
`Diagnose why it failed and decide what to do:`,
|
|
70
|
+
`- Transient or fixable: fix the environment/command context, verify with \`crtr cron run ${c.cron_id}\` (fires now, out of band, shows you the result), then \`crtr cron resume ${c.cron_id}\` to re-arm it.`,
|
|
71
|
+
`- Obsolete: \`crtr cron cancel ${c.cron_id}\`.`,
|
|
72
|
+
`- The user's call (or the fix needs a change you cannot make): \`crtr human send\`.`,
|
|
73
|
+
'',
|
|
74
|
+
'The cron stays paused until someone resumes it — nothing refires while you work.',
|
|
75
|
+
].join('\n');
|
|
76
|
+
}
|
|
77
|
+
/** Pause the cron and spawn the escalation node in the cron's originating
|
|
78
|
+
* profile (root fallback). Returns the run-log `delivered` string. Never
|
|
79
|
+
* throws: an escalation spawn failure is recorded and logged, the pause
|
|
80
|
+
* stands (the row must not keep refiring a broken command). */
|
|
81
|
+
export async function escalateFailure(c, ctx, deliveryFailure) {
|
|
82
|
+
setCronState(c.cron_id, 'paused');
|
|
83
|
+
const creatorAlive = c.created_by != null && getNode(c.created_by) !== null;
|
|
84
|
+
try {
|
|
85
|
+
const res = await spawnChild({
|
|
86
|
+
kind: 'general',
|
|
87
|
+
cwd: c.cwd,
|
|
88
|
+
prompt: buildEscalationKickoff(c, ctx, deliveryFailure),
|
|
89
|
+
// The spec's word is TERMINAL: a node that deals with this failure and
|
|
90
|
+
// reaps. With the creator gone there is nobody to parent it, but
|
|
91
|
+
// parentless must not mean resident-and-forefront — see deliverSpawnSink.
|
|
92
|
+
...(creatorAlive ? { parent: c.created_by } : { root: true, rootLifecycle: 'terminal' }),
|
|
93
|
+
readiness: 'launch',
|
|
94
|
+
profile: resolveLiveProfileOrRoot(c),
|
|
95
|
+
wakeOrigin: cronWakeOrigin(c, 'node_birth'),
|
|
96
|
+
});
|
|
97
|
+
emitEvent({
|
|
98
|
+
level: 'info',
|
|
99
|
+
event: 'cron.failure.escalated',
|
|
100
|
+
fields: { cron_id: c.cron_id, name: c.name, escalation_node: res.node.node_id },
|
|
101
|
+
});
|
|
102
|
+
return `failure: cron paused, escalation node ${res.node.node_id} spawned`;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
emitEvent({
|
|
106
|
+
level: 'error',
|
|
107
|
+
event: 'cron.escalation.spawn_failed',
|
|
108
|
+
error: err,
|
|
109
|
+
fields: { cron_id: c.cron_id, name: c.name },
|
|
110
|
+
});
|
|
111
|
+
return `failure: cron paused; escalation spawn failed: ${err.message}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** The delivery-failure line an escalated crashed ONE-SHOT carries: its side
|
|
115
|
+
* effects may already have landed in full, so a human/agent decides whether to
|
|
116
|
+
* redo it rather than the daemon re-firing it. */
|
|
117
|
+
export function crashedOneShotSummary(why) {
|
|
118
|
+
return [
|
|
119
|
+
`${why}.`,
|
|
120
|
+
'This is a ONE-SHOT, so it has been PAUSED rather than released: its side effects may have completed in full,',
|
|
121
|
+
'and re-firing would enact them a second time. Determine whether the work actually landed, then either',
|
|
122
|
+
'`crtr cron run` it to redo it deliberately, or `crtr cron cancel` it.',
|
|
123
|
+
].join(' ');
|
|
124
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Cron, type HeldSettle } from '../../core/canvas/crons.js';
|
|
2
|
+
export declare const EXIT_HELD = 75;
|
|
3
|
+
export declare const HELD_DELIVERED = "deferred (exit 75) \u2014 held for poke";
|
|
4
|
+
export declare function decideHeldSettle(c: Cron, startedAtIso: string, finishedAtMs: number): HeldSettle | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getCron } from '../../core/canvas/crons.js';
|
|
2
|
+
// Exit 75 parks an owed occurrence; the daemon honors the gate but does not implement it.
|
|
3
|
+
export const EXIT_HELD = 75;
|
|
4
|
+
export const HELD_DELIVERED = 'deferred (exit 75) — held for poke';
|
|
5
|
+
// A held one-shot without expiry is poke-only; leaving it past due would re-fire every tick.
|
|
6
|
+
const HELD_ONE_SHOT_FAR_FUTURE = '9999-12-31T23:59:59.999Z';
|
|
7
|
+
// Re-read `last_poke_at`: a poke can land while the gate check runs.
|
|
8
|
+
export function decideHeldSettle(c, startedAtIso, finishedAtMs) {
|
|
9
|
+
const fresh = getCron(c.cron_id);
|
|
10
|
+
if (fresh === null)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (fresh.state === 'active' && fresh.last_poke_at !== null && startedAtIso < fresh.last_poke_at) {
|
|
13
|
+
// A mid-run poke cannot unpark a lease, so re-due an active row; paused rows must remain parked.
|
|
14
|
+
return { kind: 'redue', nowIso: new Date(finishedAtMs).toISOString() };
|
|
15
|
+
}
|
|
16
|
+
if (c.recur != null) {
|
|
17
|
+
return { kind: 'park' };
|
|
18
|
+
}
|
|
19
|
+
return { kind: 'park', fireAt: fresh.expires_at ?? HELD_ONE_SHOT_FAR_FUTURE };
|
|
20
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RunDueCronOptions } from './passes.js';
|
|
2
|
+
/** Recover leases from prior daemon instances. Verify launch identity, not pid liveness: a reboot can recycle the pid, and killing its new owner would signal a stranger. */
|
|
3
|
+
export declare function recoverStaleCronLeases(now: number, options?: RunDueCronOptions): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { CRON_DEATH_POLL_MS, CRON_KILL_GRACE_MS, EMPTY_STDOUT_SHA256 } from './capture.js';
|
|
4
|
+
import { cronDaemonInstanceId } from './live-runs.js';
|
|
5
|
+
import { crashedOneShotSummary, escalateFailure } from './escalation.js';
|
|
6
|
+
import { settleRun } from './settle.js';
|
|
7
|
+
import { killLeasedCronRun, runningCrons, setCronState, verifyLeasedCronRun, } from '../../core/canvas/crons.js';
|
|
8
|
+
import { emitEvent } from '../../core/events/emit.js';
|
|
9
|
+
/** Recover leases from prior daemon instances. Verify launch identity, not pid liveness: a reboot can recycle the pid, and killing its new owner would signal a stranger. */
|
|
10
|
+
export function recoverStaleCronLeases(now, options = {}) {
|
|
11
|
+
for (const c of runningCrons()) {
|
|
12
|
+
if (c.run_lease_owner === cronDaemonInstanceId)
|
|
13
|
+
continue;
|
|
14
|
+
if (!verifyLeasedCronRun(c)) {
|
|
15
|
+
settleCrashedRun(c, now, options);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const startedAtMs = c.run_started_at != null ? new Date(c.run_started_at).getTime() : now;
|
|
19
|
+
if (now - startedAtMs < c.run_timeout_s * 1000)
|
|
20
|
+
continue;
|
|
21
|
+
if (!killLeasedCronRun(c)) {
|
|
22
|
+
settleCrashedRun(c, now, options);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const ctx = {
|
|
26
|
+
runId: randomUUID(),
|
|
27
|
+
startedAtIso: c.run_started_at ?? new Date(now).toISOString(),
|
|
28
|
+
exitCode: -1,
|
|
29
|
+
timedOut: true,
|
|
30
|
+
runTimeoutS: c.run_timeout_s,
|
|
31
|
+
processErrorMessage: null,
|
|
32
|
+
stdoutHead: '',
|
|
33
|
+
stdoutSha256: EMPTY_STDOUT_SHA256,
|
|
34
|
+
stderrHead: `run timed out after ${c.run_timeout_s}s (recovered stale lease from a prior daemon instance)`,
|
|
35
|
+
};
|
|
36
|
+
const settlement = (async () => {
|
|
37
|
+
await awaitLeasedRunDeath(c);
|
|
38
|
+
await settleRun(c, {
|
|
39
|
+
outOfBand: false,
|
|
40
|
+
outcome: { kind: 'disposed', ctx },
|
|
41
|
+
record: {
|
|
42
|
+
runId: ctx.runId,
|
|
43
|
+
startedAtIso: ctx.startedAtIso,
|
|
44
|
+
finishedAtMs: now,
|
|
45
|
+
durationMs: now - startedAtMs,
|
|
46
|
+
exitCode: -1,
|
|
47
|
+
stdoutHead: null,
|
|
48
|
+
stderrHead: ctx.stderrHead,
|
|
49
|
+
},
|
|
50
|
+
lease: { kind: 'clear' },
|
|
51
|
+
});
|
|
52
|
+
})();
|
|
53
|
+
options.register?.(settlement);
|
|
54
|
+
void settlement.catch((err) => {
|
|
55
|
+
emitEvent({ level: 'error', event: 'cron.stale_lease.settle_failed', error: err, fields: { cron_id: c.cron_id } });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Wait after SIGTERM: a signal-trapping command can still produce side effects.
|
|
60
|
+
async function awaitLeasedRunDeath(c) {
|
|
61
|
+
const deadline = Date.now() + CRON_KILL_GRACE_MS;
|
|
62
|
+
while (Date.now() < deadline) {
|
|
63
|
+
await delay(CRON_DEATH_POLL_MS);
|
|
64
|
+
if (!verifyLeasedCronRun(c))
|
|
65
|
+
return; // gone
|
|
66
|
+
}
|
|
67
|
+
killLeasedCronRun(c, 'SIGKILL');
|
|
68
|
+
}
|
|
69
|
+
/** Record an unobserved crashed run. Pause one-shots rather than re-fire them: their side effects may already have landed. */
|
|
70
|
+
function settleCrashedRun(c, now, options) {
|
|
71
|
+
const oneShot = c.recur == null;
|
|
72
|
+
const startedAtMs = c.run_started_at != null ? new Date(c.run_started_at).getTime() : now;
|
|
73
|
+
const why = 'the daemon life that owned this run ended before it settled — the process is gone and its outcome was never observed';
|
|
74
|
+
const ctx = {
|
|
75
|
+
runId: randomUUID(),
|
|
76
|
+
startedAtIso: c.run_started_at ?? new Date(now).toISOString(),
|
|
77
|
+
exitCode: null,
|
|
78
|
+
timedOut: false,
|
|
79
|
+
runTimeoutS: c.run_timeout_s,
|
|
80
|
+
processErrorMessage: why,
|
|
81
|
+
stdoutHead: '',
|
|
82
|
+
stdoutSha256: EMPTY_STDOUT_SHA256,
|
|
83
|
+
stderrHead: `run outcome unobserved: ${why}`,
|
|
84
|
+
};
|
|
85
|
+
const settlement = (async () => {
|
|
86
|
+
let delivered = `crashed: ${why}${oneShot ? '' : ' — the next occurrence is unaffected'}`;
|
|
87
|
+
if (oneShot) {
|
|
88
|
+
// Pause before escalation so a throw cannot re-fire the past-due one-shot.
|
|
89
|
+
setCronState(c.cron_id, 'paused');
|
|
90
|
+
delivered =
|
|
91
|
+
c.on_output === 'silent'
|
|
92
|
+
? `crashed: ${why} — one-shot paused, not re-fired (silent: not escalated)`
|
|
93
|
+
: `crashed one-shot: ${await escalateFailure(c, ctx, crashedOneShotSummary(why))}`;
|
|
94
|
+
}
|
|
95
|
+
await settleRun(c, {
|
|
96
|
+
outOfBand: false,
|
|
97
|
+
outcome: { kind: 'stated', delivered, paused: oneShot },
|
|
98
|
+
record: {
|
|
99
|
+
runId: ctx.runId,
|
|
100
|
+
startedAtIso: ctx.startedAtIso,
|
|
101
|
+
finishedAtMs: now,
|
|
102
|
+
durationMs: Math.max(0, now - startedAtMs),
|
|
103
|
+
exitCode: null,
|
|
104
|
+
stdoutHead: null,
|
|
105
|
+
stderrHead: ctx.stderrHead,
|
|
106
|
+
},
|
|
107
|
+
lease: 'release',
|
|
108
|
+
});
|
|
109
|
+
emitEvent({
|
|
110
|
+
level: 'warn',
|
|
111
|
+
event: 'cron.run.unobserved',
|
|
112
|
+
fields: { cron_id: c.cron_id, name: c.name, one_shot: oneShot },
|
|
113
|
+
});
|
|
114
|
+
})();
|
|
115
|
+
options.register?.(settlement);
|
|
116
|
+
void settlement.catch((err) => {
|
|
117
|
+
emitEvent({ level: 'error', event: 'cron.crashed_run.settle_failed', error: err, fields: { cron_id: c.cron_id } });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const cronDaemonInstanceId: `${string}-${string}-${string}-${string}-${string}`;
|
|
2
|
+
export interface LiveRun {
|
|
3
|
+
runId: string;
|
|
4
|
+
replaced: boolean;
|
|
5
|
+
selfCanceled: boolean;
|
|
6
|
+
kill(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function liveRunFor(cronId: string): LiveRun | undefined;
|
|
9
|
+
export declare function registerLiveRun(cronId: string, run: LiveRun): void;
|
|
10
|
+
export declare function clearLiveRun(cronId: string, runId: string): void;
|
|
11
|
+
export declare function markSelfCanceledCronRun(cronId: string, runId: string): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export const cronDaemonInstanceId = randomUUID();
|
|
3
|
+
const liveRuns = new Map();
|
|
4
|
+
export function liveRunFor(cronId) {
|
|
5
|
+
return liveRuns.get(cronId);
|
|
6
|
+
}
|
|
7
|
+
export function registerLiveRun(cronId, run) {
|
|
8
|
+
liveRuns.set(cronId, run);
|
|
9
|
+
}
|
|
10
|
+
// An earlier close handler must not evict a later run.
|
|
11
|
+
export function clearLiveRun(cronId, runId) {
|
|
12
|
+
const live = liveRuns.get(cronId);
|
|
13
|
+
if (live !== undefined && live.runId === runId)
|
|
14
|
+
liveRuns.delete(cronId);
|
|
15
|
+
}
|
|
16
|
+
// Only that exact process may finish after its row is removed.
|
|
17
|
+
export function markSelfCanceledCronRun(cronId, runId) {
|
|
18
|
+
const live = liveRuns.get(cronId);
|
|
19
|
+
if (live === undefined || live.runId !== runId)
|
|
20
|
+
return false;
|
|
21
|
+
live.selfCanceled = true;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Cron } from '../../core/canvas/crons.js';
|
|
2
|
+
export interface RunDueCronOptions {
|
|
3
|
+
/** Registers settlement work for graceful-teardown draining. */
|
|
4
|
+
register?: (work: Promise<void>) => void;
|
|
5
|
+
/** Node-launching runs need capacity; absent means all runs are admitted. */
|
|
6
|
+
hasBrokerCapacity?: () => boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function cronLaunchesNode(c: Cron): boolean;
|
|
9
|
+
export declare function expireDueCrons(nowIso: string): void;
|
|
10
|
+
export declare function applyOverlapPolicies(now: number, nowIso: string): void;
|
|
11
|
+
export declare function emitCapacityDeferral(now: number, deferred: number): void;
|