@north-light/crouter 0.3.312 → 0.3.314
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.d.ts +27 -30
- package/dist/api/client.js +80 -67
- package/dist/api/dto/broker-ops.d.ts +10 -20
- package/dist/api/dto/canvas.d.ts +1 -10
- package/dist/api/dto/health.d.ts +113 -0
- package/dist/api/dto/human-requests.d.ts +14 -3
- package/dist/api/dto/human.d.ts +2 -48
- package/dist/api/dto/human.js +3 -2
- package/dist/api/dto/inbox.d.ts +14 -0
- package/dist/api/dto/mail.d.ts +43 -0
- package/dist/api/dto/nodes.d.ts +11 -8
- package/dist/api/dto/recovery.d.ts +96 -0
- package/dist/api/dto/reviews.d.ts +2 -2
- package/dist/api/routes.d.ts +9 -6
- package/dist/api/routes.js +11 -11
- package/dist/build-root.d.ts +4 -0
- package/dist/build-root.js +11 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +35 -47
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +12 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +12 -5
- package/dist/cli.js +10 -1
- package/dist/clients/__tests__/helpers/human-daemon.d.ts +11 -0
- package/dist/clients/__tests__/helpers/human-daemon.js +60 -0
- package/dist/clients/attach/chrome/canvas-panels.js +1 -1
- package/dist/clients/attach/chrome/inbox-strip.d.ts +3 -3
- package/dist/clients/attach/chrome/inbox-strip.js +17 -9
- package/dist/clients/attach/chrome/ticket-panel.d.ts +2 -2
- package/dist/clients/attach/chrome/ticket-panel.js +77 -46
- package/dist/clients/attach/overlays/graph.js +6 -9
- package/dist/clients/attach/render/chat-view.d.ts +5 -4
- package/dist/clients/attach/render/chat-view.js +22 -13
- package/dist/clients/attach/render/page-block.d.ts +11 -7
- package/dist/clients/attach/render/page-block.js +64 -55
- package/dist/clients/attach/viewer.js +757 -746
- package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +56 -56
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.js +45 -32
- package/dist/clients/inbox/controller.d.ts +42 -14
- package/dist/clients/inbox/controller.js +240 -124
- package/dist/clients/inbox/dto-adapt.d.ts +28 -0
- package/dist/clients/inbox/dto-adapt.js +93 -0
- package/dist/clients/inbox/page-adapter.d.ts +5 -4
- package/dist/clients/inbox/page-adapter.js +12 -12
- package/dist/clients/inbox/resolve.d.ts +10 -5
- package/dist/clients/inbox/resolve.js +13 -12
- package/dist/clients/inbox/review/review-client.d.ts +0 -1
- package/dist/clients/inbox/review/review-client.js +0 -4
- package/dist/clients/inbox/review-adapter.d.ts +2 -1
- package/dist/clients/inbox/review-adapter.js +3 -5
- package/dist/clients/inbox/surface.d.ts +2 -2
- package/dist/clients/inbox/surface.js +19 -8
- package/dist/clients/inbox/tui.d.ts +3 -3
- package/dist/commands/__tests__/human.test.js +31 -15
- package/dist/commands/__tests__/integration/dashboard-snapshot.test.js +36 -11
- package/dist/commands/{sys/__tests__ → __tests__/integration}/migrate.test.js +142 -36
- package/dist/commands/__tests__/revive-now-gate.test.js +2 -0
- package/dist/commands/api-client.d.ts +5 -4
- package/dist/commands/api-client.js +13 -7
- package/dist/commands/canvas-history/shared.js +1 -1
- package/dist/commands/canvas-history.js +1 -1
- package/dist/commands/canvas-roster.js +1 -1
- package/dist/commands/canvas.js +1 -2
- package/dist/commands/human/feedback.js +20 -17
- package/dist/commands/human/prompts.js +45 -36
- package/dist/commands/human/queue.js +73 -123
- package/dist/commands/human/review.js +6 -16
- package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +2 -0
- package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.js +2 -0
- package/dist/commands/memory/__tests__/repository-root-lint.test.js +2 -0
- package/dist/commands/node-inspect-artifacts.js +1 -1
- package/dist/commands/node-lifecycle-revive.d.ts +2 -2
- package/dist/commands/node-lifecycle-revive.js +9 -19
- package/dist/commands/pkg/plugin-manage.js +0 -1
- package/dist/commands/profile/show.js +4 -12
- package/dist/commands/surface-tmux-spread.js +1 -5
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +23 -30
- package/dist/commands/sys/__tests__/sync-project-guidance.test.js +37 -11
- package/dist/commands/sys/branch.d.ts +2 -0
- package/dist/commands/sys/branch.js +19 -0
- package/dist/commands/sys/config.js +106 -33
- package/dist/commands/sys/daemon.d.ts +2 -0
- package/dist/commands/sys/daemon.js +130 -7
- package/dist/commands/sys/doctor.js +5 -15
- package/dist/commands/sys/migrate.js +21 -92
- package/dist/commands/sys/setup-settings-runner.js +4 -2
- package/dist/commands/sys.js +2 -15
- package/dist/core/__tests__/boot-fault-attempt.test.js +23 -1
- package/dist/core/__tests__/boot.test.js +32 -25
- package/dist/core/__tests__/broker-launch-candidates.test.js +49 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +149 -176
- package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +59 -10
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +76 -20
- package/dist/core/__tests__/canvas.test.js +18 -47
- package/dist/core/__tests__/child-death-wake.test.js +29 -7
- package/dist/core/__tests__/cli-help-contract.test.js +2 -0
- package/dist/core/__tests__/close.test.js +29 -10
- package/dist/core/__tests__/collectible.test.js +2 -1
- package/dist/core/__tests__/config-change-delta.test.js +2 -0
- package/dist/core/__tests__/context-intro.test.js +13 -1
- package/dist/core/__tests__/cron-armed-context-failure.test.js +17 -12
- package/dist/core/__tests__/cron-held-settlement.test.js +21 -3
- package/dist/core/__tests__/cron-node-sink-parked-root.test.js +29 -10
- package/dist/core/__tests__/cron-self-cancel-delivery.test.js +14 -2
- package/dist/core/__tests__/cron-timeout-descendants.test.js +13 -1
- package/dist/core/__tests__/daemon-boot.test.js +51 -14
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +1 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +8 -3
- package/dist/core/__tests__/focuses.test.js +10 -5
- package/dist/core/__tests__/helpers/harness.js +50 -18
- package/dist/core/__tests__/history-inbox.test.js +12 -16
- package/dist/core/__tests__/human-action-delivery.test.js +48 -28
- package/dist/core/__tests__/human-actions.test.js +7 -3
- package/dist/core/__tests__/inline-memory-refs.test.js +7 -2
- package/dist/core/__tests__/integration/broker-fork-seam.test.js +37 -10
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +42 -0
- package/dist/core/__tests__/integration/broker-snapshot-history.test.js +42 -29
- package/dist/core/__tests__/integration/broker-wedge-supervision.test.js +36 -35
- package/dist/core/__tests__/integration/command-plugins.test.js +15 -6
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +113 -92
- package/dist/core/__tests__/integration/fresh-home-commands.test.js +49 -0
- package/dist/core/__tests__/integration/host-teardown-process-group.test.js +9 -5
- package/dist/core/__tests__/integration/lifecycle-hooks.test.js +16 -3
- package/dist/core/__tests__/integration/plugin-revalidate.test.js +4 -0
- package/dist/core/__tests__/integration/profile-pause.test.js +4 -4
- package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +9 -12
- package/dist/core/__tests__/integration/revive.test.js +67 -21
- package/dist/core/__tests__/integration/spawn-root.test.js +51 -8
- package/dist/core/__tests__/integration/subscription-delivery.test.js +33 -21
- package/dist/core/__tests__/integration/worktree-land.test.js +5 -2
- package/dist/core/__tests__/integration/worktree-reap.test.js +15 -1
- package/dist/core/__tests__/kickoff.test.js +36 -39
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +2 -1
- package/dist/core/__tests__/lifecycle.test.js +2 -1
- package/dist/core/__tests__/live-lifecycle-prompt.test.js +13 -1
- package/dist/core/__tests__/memory-resolver-precedence.test.js +2 -0
- package/dist/core/__tests__/migration.test.js +145 -31
- package/dist/core/__tests__/model-pin-durability.test.js +4 -1
- package/dist/core/__tests__/model-routes-config.test.js +13 -5
- package/dist/core/__tests__/node-outcome.test.js +93 -26
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -1
- package/dist/core/__tests__/on-read-nested-store.test.js +2 -1
- package/dist/core/__tests__/passive-subscription.test.js +50 -157
- package/dist/core/__tests__/plugin-kinds.test.js +6 -4
- package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.js +6 -1
- package/dist/core/__tests__/plugin-page-components.test.js +6 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +2 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +4 -1
- package/dist/core/__tests__/prune.test.js +11 -5
- package/dist/core/__tests__/push-final-guard.test.js +14 -7
- package/dist/core/__tests__/relaunch-root.test.js +30 -2
- package/dist/core/__tests__/remote-canvas-target.test.js +7 -1
- package/dist/core/__tests__/repository-root-identity.test.js +4 -0
- package/dist/core/__tests__/review-model-floor.test.js +6 -3
- package/dist/core/__tests__/revive-all.test.js +13 -3
- package/dist/core/__tests__/revive-capacity.test.js +26 -4
- package/dist/core/__tests__/revive-parked-fresh.test.js +31 -8
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +1 -1
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +4 -3
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +11 -14
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +85 -71
- package/dist/core/__tests__/seam/dormancy-release.test.js +33 -19
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +7 -5
- package/dist/core/__tests__/seam/inbox-reference-guidance.test.js +20 -6
- package/dist/core/__tests__/seam/node-outcome-delivery.test.js +27 -3
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +2 -1
- package/dist/core/__tests__/stop-guard.test.js +2 -1
- package/dist/core/__tests__/subscription-pool-credential-guard.test.js +4 -3
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +18 -4
- package/dist/core/__tests__/yield-ensures-daemon.test.js +2 -1
- package/dist/core/bootstrap.js +2 -0
- package/dist/core/broker-client/__tests__/transport-socket.test.js +51 -6
- package/dist/core/broker-client/transport-socket.d.ts +1 -0
- package/dist/core/broker-client/transport-socket.js +13 -5
- package/dist/core/canvas/__tests__/attention.test.js +29 -35
- package/dist/core/canvas/__tests__/history-transcript.test.js +6 -6
- package/dist/core/canvas/__tests__/render-remote.test.js +25 -9
- package/dist/core/canvas/attention.d.ts +2 -5
- package/dist/core/canvas/attention.js +15 -64
- package/dist/core/canvas/boot.d.ts +6 -7
- package/dist/core/canvas/boot.js +5 -9
- package/dist/core/canvas/canvas.d.ts +13 -109
- package/dist/core/canvas/canvas.js +140 -401
- package/dist/core/canvas/crons.d.ts +2 -0
- package/dist/core/canvas/crons.js +16 -15
- package/dist/core/canvas/daemon-owner.js +10 -0
- package/dist/core/canvas/db.d.ts +2 -2
- package/dist/core/canvas/db.js +2 -18
- package/dist/core/canvas/deliveries.d.ts +104 -0
- package/dist/core/canvas/deliveries.js +354 -0
- package/dist/core/canvas/executions.d.ts +29 -0
- package/dist/core/canvas/executions.js +68 -0
- package/dist/core/canvas/history.js +35 -35
- package/dist/core/canvas/index.d.ts +3 -1
- package/dist/core/canvas/index.js +3 -1
- package/dist/core/canvas/migrations.js +20 -12
- package/dist/core/canvas/nav-model.js +1 -3
- package/dist/core/canvas/nav-render.d.ts +4 -4
- package/dist/core/canvas/nav-render.js +11 -21
- package/dist/core/canvas/node-sql-migration.d.ts +3 -0
- package/dist/core/canvas/node-sql-migration.js +261 -0
- package/dist/core/canvas/paths.d.ts +2 -16
- package/dist/core/canvas/paths.js +2 -31
- package/dist/core/canvas/recovery.d.ts +76 -0
- package/dist/core/canvas/recovery.js +588 -0
- package/dist/core/canvas/remote-canvas-source.js +4 -0
- package/dist/core/canvas/render-source.d.ts +16 -27
- package/dist/core/canvas/render-source.js +16 -45
- package/dist/core/canvas/sessions.d.ts +33 -0
- package/dist/core/canvas/sessions.js +78 -0
- package/dist/core/canvas/types.d.ts +33 -33
- package/dist/core/canvas/types.js +2 -5
- package/dist/core/canvas/waits.d.ts +18 -0
- package/dist/core/canvas/waits.js +70 -0
- package/dist/core/canvas/worktrees.d.ts +40 -0
- package/dist/core/canvas/worktrees.js +93 -0
- package/dist/core/command.js +11 -0
- package/dist/core/config.d.ts +17 -12
- package/dist/core/config.js +106 -155
- package/dist/core/feed/__tests__/inbox-migrate.test.js +27 -31
- package/dist/core/feed/feed.d.ts +0 -6
- package/dist/core/feed/feed.js +68 -56
- package/dist/core/feed/inbox.d.ts +7 -81
- package/dist/core/feed/inbox.js +46 -378
- package/dist/core/feed/messages.d.ts +58 -0
- package/dist/core/feed/messages.js +217 -0
- package/dist/core/fs-utils.d.ts +4 -0
- package/dist/core/fs-utils.js +21 -1
- package/dist/core/human/answer.d.ts +1 -1
- package/dist/core/human/answer.js +1 -1
- package/dist/core/human/claim.d.ts +1 -1
- package/dist/core/human/claim.js +2 -4
- package/dist/core/human/component-docs.js +1 -1
- package/dist/core/human/feedback-companion.js +3 -2
- package/dist/core/human/feedback.d.ts +5 -8
- package/dist/core/human/feedback.js +57 -85
- package/dist/core/human/page-authoring.d.ts +30 -0
- package/dist/core/human/page-authoring.js +19 -0
- package/dist/core/human/requests.d.ts +136 -0
- package/dist/core/human/requests.js +515 -0
- package/dist/core/human/root.d.ts +0 -7
- package/dist/core/human/root.js +0 -17
- package/dist/core/human/scan.d.ts +2 -7
- package/dist/core/human/scan.js +48 -132
- package/dist/core/human/summary.d.ts +1 -1
- package/dist/core/human/summary.js +1 -1
- package/dist/core/human/tickets.d.ts +23 -83
- package/dist/core/human/tickets.js +92 -243
- package/dist/core/human/types.d.ts +5 -1
- package/dist/core/installed-plugins.js +8 -3
- package/dist/core/keybindings/__tests__/resolve.test.js +19 -17
- package/dist/core/locked-file.d.ts +6 -3
- package/dist/core/locked-file.js +36 -9
- package/dist/core/memory/extensions.js +2 -0
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/profiles/__tests__/env-store.test.js +2 -0
- package/dist/core/profiles/default-binding.d.ts +0 -4
- package/dist/core/profiles/default-binding.js +12 -58
- package/dist/core/profiles/env-store.js +8 -3
- package/dist/core/profiles/manifest.d.ts +2 -37
- package/dist/core/profiles/manifest.js +35 -161
- package/dist/core/profiles/select.js +3 -0
- package/dist/core/review/__tests__/capture-origin.test.js +13 -2
- package/dist/core/review/__tests__/comments.test.js +28 -10
- package/dist/core/review/__tests__/remap.test.js +28 -10
- package/dist/core/review/__tests__/stage-identity.test.js +17 -7
- package/dist/core/review/birth.d.ts +1 -1
- package/dist/core/review/birth.js +34 -5
- package/dist/core/review/comments.js +1 -1
- package/dist/core/review/document.js +1 -1
- package/dist/core/review/project.d.ts +5 -1
- package/dist/core/review/project.js +14 -36
- package/dist/core/review/realize.d.ts +3 -6
- package/dist/core/review/realize.js +51 -168
- package/dist/core/review/remap.js +1 -1
- package/dist/core/review/stage.d.ts +3 -2
- package/dist/core/review/stage.js +2 -3
- package/dist/core/review/store.d.ts +3 -21
- package/dist/core/review/store.js +140 -186
- package/dist/core/review/types.d.ts +0 -1
- package/dist/core/runtime/__tests__/install-id-cache-scoping.test.js +9 -5
- package/dist/core/runtime/__tests__/spawn-env.test.js +8 -9
- package/dist/core/runtime/broker/auth-reload.js +9 -10
- package/dist/core/runtime/broker/daemon-ops.d.ts +25 -8
- package/dist/core/runtime/broker/daemon-ops.js +42 -9
- package/dist/core/runtime/broker/extension-abort.d.ts +3 -0
- package/dist/core/runtime/broker/extension-abort.js +11 -0
- package/dist/core/runtime/broker/fault-retry.d.ts +15 -6
- package/dist/core/runtime/broker/fault-retry.js +142 -95
- package/dist/core/runtime/broker/inbox.d.ts +4 -32
- package/dist/core/runtime/broker/inbox.js +4 -92
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +21 -12
- package/dist/core/runtime/broker-persona-guidance.d.ts +1 -1
- package/dist/core/runtime/broker-persona-guidance.js +2 -13
- package/dist/core/runtime/broker.js +28 -22
- package/dist/core/runtime/close.d.ts +3 -9
- package/dist/core/runtime/close.js +16 -27
- package/dist/core/runtime/fault.d.ts +3 -93
- package/dist/core/runtime/fault.js +9 -419
- package/dist/core/runtime/kickoff.d.ts +3 -6
- package/dist/core/runtime/kickoff.js +12 -18
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/lifecycle.js +40 -7
- package/dist/core/runtime/placement.js +2 -2
- package/dist/core/runtime/recycle.js +8 -2
- package/dist/core/runtime/reset.js +22 -6
- package/dist/core/runtime/revive.js +226 -218
- package/dist/core/runtime/spawn.js +14 -3
- package/dist/core/scoped-state/db.d.ts +18 -0
- package/dist/core/scoped-state/db.js +135 -0
- package/dist/core/scoped-state/migrate.d.ts +8 -0
- package/dist/core/scoped-state/migrate.js +181 -0
- package/dist/core/scoped-state/paths.d.ts +9 -0
- package/dist/core/scoped-state/paths.js +27 -0
- package/dist/core/scoped-state/profiles.d.ts +12 -0
- package/dist/core/scoped-state/profiles.js +62 -0
- package/dist/core/scoped-state/providers.d.ts +16 -0
- package/dist/core/scoped-state/providers.js +8 -0
- package/dist/core/scoped-state/schema.d.ts +3 -0
- package/dist/core/scoped-state/schema.js +26 -0
- package/dist/core/scoped-state/settings.d.ts +28 -0
- package/dist/core/scoped-state/settings.js +82 -0
- package/dist/core/subscription-state.d.ts +29 -48
- package/dist/core/subscription-state.js +176 -712
- package/dist/core/worktree-close.js +4 -4
- package/dist/core/worktree-mutation-async.js +2 -2
- package/dist/core/worktree-sweep.js +11 -16
- package/dist/core/worktree.js +6 -12
- package/dist/daemon/__tests__/autostart-storm.test.js +1 -39
- package/dist/daemon/__tests__/helpers/source-daemon.d.ts +2 -2
- package/dist/daemon/__tests__/helpers/source-daemon.js +7 -6
- package/dist/daemon/__tests__/integration/migration-startup.test.js +59 -26
- package/dist/daemon/__tests__/node-deadline-lane.test.js +2 -1
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +39 -15
- package/dist/daemon/__tests__/node-outcome-delivery.test.js +39 -10
- package/dist/daemon/__tests__/reconciler-signature.test.js +2 -2
- package/dist/daemon/__tests__/subtree-filter-freshness.test.js +2 -2
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +17 -12
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +27 -5
- package/dist/daemon/api/__tests__/model-config.test.js +8 -8
- package/dist/daemon/api/__tests__/node-create-description.test.js +22 -5
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -1
- package/dist/daemon/api/__tests__/nodes-activity-query.test.js +10 -12
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -1
- package/dist/daemon/api/__tests__/profile-metadata-route.test.js +4 -1
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +26 -8
- package/dist/daemon/api/__tests__/seam/api-server.test.js +24 -32
- package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +4 -1
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +5 -4
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +27 -41
- package/dist/daemon/api/__tests__/submit-result.test.js +3 -2
- package/dist/daemon/api/bridge.js +1 -1
- package/dist/daemon/api/handlers/bash-jobs.js +6 -7
- package/dist/daemon/api/handlers/broker-ops.js +47 -84
- package/dist/daemon/api/handlers/broker-recovery.d.ts +4 -0
- package/dist/daemon/api/handlers/broker-recovery.js +251 -0
- package/dist/daemon/api/handlers/canvas.js +2 -44
- package/dist/daemon/api/handlers/daemon.js +41 -0
- package/dist/daemon/api/handlers/feedback-comments.js +2 -2
- package/dist/daemon/api/handlers/health.js +10 -5
- package/dist/daemon/api/handlers/human-requests.js +168 -188
- package/dist/daemon/api/handlers/human.js +3 -151
- package/dist/daemon/api/handlers/inbox-page.d.ts +4 -0
- package/dist/daemon/api/handlers/inbox-page.js +89 -0
- package/dist/daemon/api/handlers/inbox.js +21 -120
- package/dist/daemon/api/handlers/messages.d.ts +1 -0
- package/dist/daemon/api/handlers/messages.js +73 -15
- package/dist/daemon/api/handlers/node-outcomes.js +58 -13
- package/dist/daemon/api/handlers/nodes.js +7 -13
- package/dist/daemon/api/handlers/reviews.js +4 -29
- package/dist/daemon/api/map.d.ts +25 -3
- package/dist/daemon/api/map.js +84 -14
- package/dist/daemon/api/server.js +68 -9
- package/dist/daemon/control.d.ts +18 -0
- package/dist/daemon/cron/disposition.d.ts +2 -0
- package/dist/daemon/cron/disposition.js +8 -5
- package/dist/daemon/cron/settle.js +37 -13
- package/dist/daemon/cron/sinks.d.ts +18 -1
- package/dist/daemon/cron/sinks.js +47 -93
- package/dist/daemon/crtrd-cli.js +4 -19
- package/dist/daemon/crtrd.d.ts +2 -0
- package/dist/daemon/crtrd.js +206 -81
- package/dist/daemon/fleet.js +5 -6
- package/dist/daemon/human/finish.d.ts +12 -24
- package/dist/daemon/human/finish.js +88 -285
- package/dist/daemon/human/settle.d.ts +40 -0
- package/dist/daemon/human/settle.js +89 -0
- package/dist/daemon/human/sweep.d.ts +20 -5
- package/dist/daemon/human/sweep.js +56 -60
- package/dist/daemon/manage.d.ts +21 -16
- package/dist/daemon/manage.js +85 -51
- package/dist/daemon/messaging/node-message.d.ts +1 -1
- package/dist/daemon/messaging/node-message.js +9 -12
- package/dist/daemon/park-activity.js +3 -3
- package/dist/daemon/reconcilers/bash-deadline.js +6 -7
- package/dist/daemon/reconcilers/broker-supervision.d.ts +1 -1
- package/dist/daemon/reconcilers/broker-supervision.js +41 -19
- package/dist/daemon/reconcilers/controller-death.js +6 -18
- package/dist/daemon/reconcilers/delivery-lane.d.ts +9 -0
- package/dist/daemon/reconcilers/delivery-lane.js +130 -0
- package/dist/daemon/reconcilers/live-obligation.js +3 -3
- package/dist/daemon/reconcilers/node-deadline.js +1 -1
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +1 -1
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +10 -12
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +5 -5
- package/dist/daemon/reconcilers/node-lifecycle/wants-execution.js +1 -1
- package/dist/daemon/reconcilers/pending-review-submit.js +3 -3
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -8
- package/dist/daemon/reconcilers/storage-maintenance.js +13 -43
- package/dist/daemon/review/companion.js +5 -11
- package/dist/daemon/review/deliver.d.ts +1 -5
- package/dist/daemon/review/deliver.js +3 -52
- package/dist/daemon/review/finish.d.ts +2 -27
- package/dist/daemon/review/finish.js +79 -263
- package/dist/daemon/review/sweep.d.ts +3 -10
- package/dist/daemon/review/sweep.js +13 -131
- package/dist/daemon/startup-policy.d.ts +0 -5
- package/dist/daemon/startup-policy.js +0 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/migrations/__tests__/activation-concurrency.test.js +3 -2
- package/dist/migrations/__tests__/activation.test.js +9 -6
- package/dist/migrations/activation.d.ts +3 -2
- package/dist/migrations/activation.js +2 -4
- package/dist/migrations/corpus.d.ts +2 -5
- package/dist/migrations/corpus.js +9 -20
- package/dist/migrations/runner.d.ts +3 -1
- package/dist/migrations/runner.js +6 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +21 -89
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +4 -2
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +51 -33
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +16 -2
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +13 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +98 -244
- package/dist/pi-extensions/canvas-passive-context.d.ts +4 -8
- package/dist/pi-extensions/canvas-passive-context.js +85 -37
- package/dist/pi-extensions/canvas-stophook.js +47 -31
- package/dist/shared/env.d.ts +1 -0
- package/dist/shared/env.js +3 -0
- package/package.json +1 -1
- package/runtime.lock.json +5 -5
- package/dist/clients/attach/__tests__/page-block.test.js +0 -54
- package/dist/clients/inbox/review-visibility.d.ts +0 -5
- package/dist/clients/inbox/review-visibility.js +0 -19
- package/dist/commands/canvas-rebuild-index.d.ts +0 -2
- package/dist/commands/canvas-rebuild-index.js +0 -46
- package/dist/core/__tests__/human-cancel-guard.test.js +0 -110
- package/dist/core/__tests__/human-deliver.test.js +0 -256
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +0 -1
- package/dist/core/__tests__/human-node-not-supervised.test.js +0 -89
- package/dist/core/__tests__/integration/human-deliver-e2e.test.d.ts +0 -1
- package/dist/core/__tests__/integration/human-deliver-e2e.test.js +0 -131
- package/dist/core/__tests__/phase4-review-store.test.d.ts +0 -1
- package/dist/core/__tests__/phase4-review-store.test.js +0 -144
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.d.ts +0 -1
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +0 -110
- package/dist/core/canvas/human-deliveries.d.ts +0 -39
- package/dist/core/canvas/human-deliveries.js +0 -75
- package/dist/core/canvas/node-outcome-deliveries.d.ts +0 -64
- package/dist/core/canvas/node-outcome-deliveries.js +0 -159
- package/dist/core/feed/passive.d.ts +0 -3
- package/dist/core/feed/passive.js +0 -53
- package/dist/core/human/__tests__/inbox-cancel-worker.d.ts +0 -1
- package/dist/core/human/__tests__/inbox-cancel-worker.js +0 -7
- package/dist/core/human/__tests__/inbox-claim-worker.d.ts +0 -1
- package/dist/core/human/__tests__/inbox-claim-worker.js +0 -17
- package/dist/core/human/__tests__/inbox-stale-claim-worker.d.ts +0 -1
- package/dist/core/human/__tests__/inbox-stale-claim-worker.js +0 -8
- package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +0 -1
- package/dist/core/human/__tests__/integration/inbox-core.test.js +0 -145
- package/dist/core/human/__tests__/page-tickets.test.d.ts +0 -1
- package/dist/core/human/__tests__/page-tickets.test.js +0 -161
- package/dist/core/review/ticket-filter.d.ts +0 -17
- package/dist/core/review/ticket-filter.js +0 -21
- package/dist/core/runtime/broker/passive.d.ts +0 -17
- package/dist/core/runtime/broker/passive.js +0 -44
- package/dist/core/runtime/busy.d.ts +0 -46
- package/dist/core/runtime/busy.js +0 -150
- package/dist/daemon/__tests__/startup-block-marker.test.d.ts +0 -1
- package/dist/daemon/__tests__/startup-block-marker.test.js +0 -86
- package/dist/daemon/api/__tests__/review-terminal-conflict.test.d.ts +0 -1
- package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +0 -98
- package/dist/daemon/human/deliver-action.d.ts +0 -3
- package/dist/daemon/human/deliver-action.js +0 -9
- package/dist/daemon/node-outcome/deliver-outcome.d.ts +0 -3
- package/dist/daemon/node-outcome/deliver-outcome.js +0 -9
- package/dist/daemon/reconcilers/human-delivery-lane.d.ts +0 -10
- package/dist/daemon/reconcilers/human-delivery-lane.js +0 -41
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +0 -10
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +0 -41
- package/dist/daemon/startup-block-marker.d.ts +0 -25
- package/dist/daemon/startup-block-marker.js +0 -99
- /package/dist/{clients/attach/__tests__/page-block.test.d.ts → api/dto/mail.js} +0 -0
- /package/dist/{commands/sys/__tests__/migrate.test.d.ts → api/dto/recovery.js} +0 -0
- /package/dist/{core/__tests__/human-cancel-guard.test.d.ts → commands/__tests__/integration/migrate.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-deliver.test.d.ts → integration/fresh-home-commands.test.d.ts} +0 -0
|
@@ -10,18 +10,21 @@ import { mkdirSync, rmSync } from 'node:fs';
|
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import registerCanvasInboxWatcher from '../../pi-extensions/canvas-inbox-watcher.js';
|
|
13
|
-
import {
|
|
13
|
+
import { acknowledgedNodeMailCount, resetStaleExecutionMail } from '../canvas/deliveries.js';
|
|
14
|
+
import { beginExecution } from '../canvas/executions.js';
|
|
15
|
+
import { enqueueNodeMail } from '../feed/messages.js';
|
|
14
16
|
import { drainBearings } from '../runtime/kickoff.js';
|
|
15
17
|
import { appendSituationalContext } from '../runtime/situational-context.js';
|
|
16
18
|
import { createEngineDriver } from '../runtime/broker/engine-drive.js';
|
|
17
19
|
import { createTurnAdmissionGate } from '../runtime/broker/turn-admission.js';
|
|
18
20
|
import { BROKER_IDLE_TURN_START, heldDeferredInboxPromptJoin, } from '../runtime/broker/held-deferred-inbox.js';
|
|
19
|
-
import { createNode, getNode,
|
|
21
|
+
import { createNode, getNode, setIntent } from '../canvas/canvas.js';
|
|
20
22
|
import { createProfile, pauseProfile, resumeProfile } from '../profiles/manifest.js';
|
|
21
|
-
import { closeDb } from '../canvas/db.js';
|
|
23
|
+
import { closeDb, migrate, openDb } from '../canvas/db.js';
|
|
24
|
+
import { closeScopedStores } from '../scoped-state/db.js';
|
|
25
|
+
import { initializeFreshScopedStore } from '../scoped-state/migrate.js';
|
|
26
|
+
import { resetScopeCache } from '../scope.js';
|
|
22
27
|
import { createApiServer } from '../../daemon/api/server.js';
|
|
23
|
-
import { CrtrClient } from '../../api/client.js';
|
|
24
|
-
import { ApiError } from '../../api/errors.js';
|
|
25
28
|
// Drive the watcher's injectable cadence seam (CRTR_WATCHER_TICK_MS /
|
|
26
29
|
// CRTR_WATCHER_DEBOUNCE_MS) at a fast tempo so the test sleeps milliseconds, not
|
|
27
30
|
// seconds. SETTLE_MS still allows a resolve+seed tick, a read tick, and the
|
|
@@ -30,17 +33,28 @@ const TICK_MS = 20;
|
|
|
30
33
|
const DEBOUNCE_MS = 25;
|
|
31
34
|
const SETTLE_MS = TICK_MS * 2 + DEBOUNCE_MS + 200;
|
|
32
35
|
let origHome;
|
|
36
|
+
let origUserHome;
|
|
33
37
|
let origNode;
|
|
38
|
+
let origExecution;
|
|
34
39
|
const homes = [];
|
|
35
40
|
const disposers = [];
|
|
36
41
|
let server;
|
|
37
|
-
/**
|
|
42
|
+
/** Bind each case to isolated canvas and user SQL state. */
|
|
38
43
|
function freshNode(nodeId) {
|
|
39
|
-
|
|
44
|
+
closeDb();
|
|
45
|
+
closeScopedStores();
|
|
46
|
+
resetScopeCache();
|
|
47
|
+
const root = join(tmpdir(), `crtr-w-${Math.random().toString(36).slice(2, 8)}`);
|
|
48
|
+
const home = join(root, 'home');
|
|
49
|
+
const canvas = join(root, 'canvas');
|
|
40
50
|
mkdirSync(home, { recursive: true });
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
mkdirSync(canvas, { recursive: true });
|
|
52
|
+
homes.push(root);
|
|
53
|
+
process.env['HOME'] = home;
|
|
54
|
+
process.env['CRTR_HOME'] = canvas;
|
|
43
55
|
process.env['CRTR_NODE_ID'] = nodeId;
|
|
56
|
+
initializeFreshScopedStore({ kind: 'user', root: join(home, '.crouter') });
|
|
57
|
+
migrate(openDb());
|
|
44
58
|
server = createApiServer();
|
|
45
59
|
}
|
|
46
60
|
function makeFakePi() {
|
|
@@ -70,7 +84,8 @@ function makeFakePi() {
|
|
|
70
84
|
// the one boundary where `hasPendingMessages()` still gates a commit.
|
|
71
85
|
async function settle(pi, opts = {}) {
|
|
72
86
|
const pending = opts.pending === true;
|
|
73
|
-
|
|
87
|
+
const idle = opts.idle ?? true;
|
|
88
|
+
await pi.fire('agent_settled', { type: 'agent_settled' }, { isIdle: () => idle, hasPendingMessages: () => pending });
|
|
74
89
|
}
|
|
75
90
|
// Fire session_shutdown — pi's graceful-teardown safety net for a settle that
|
|
76
91
|
// never fired before exit. The real handler takes NO event ctx of its own; it
|
|
@@ -110,27 +125,48 @@ function ensureWatcherNode() {
|
|
|
110
125
|
cwd: '/tmp',
|
|
111
126
|
kind: 'general',
|
|
112
127
|
mode: 'base',
|
|
113
|
-
// Resident: these cases are about the watcher's own cursor/handoff/hold
|
|
114
|
-
// contract at each tier as sent. A terminal node's mail is raised to the
|
|
115
|
-
// steering tier on append (feed/inbox.ts), which would route every case
|
|
116
|
-
// here as a steer.
|
|
117
128
|
lifecycle: 'resident',
|
|
118
129
|
status: 'active',
|
|
119
130
|
});
|
|
120
|
-
|
|
131
|
+
}
|
|
132
|
+
function publishMail(nodeId, input) {
|
|
133
|
+
const priorNode = process.env['CRTR_NODE_ID'];
|
|
134
|
+
process.env['CRTR_NODE_ID'] = nodeId;
|
|
135
|
+
ensureWatcherNode();
|
|
136
|
+
if (priorNode === undefined)
|
|
137
|
+
delete process.env['CRTR_NODE_ID'];
|
|
138
|
+
else
|
|
139
|
+
process.env['CRTR_NODE_ID'] = priorNode;
|
|
140
|
+
return enqueueNodeMail(nodeId, {
|
|
141
|
+
kind: input.kind,
|
|
142
|
+
label: input.label,
|
|
143
|
+
senderNodeId: input.from,
|
|
144
|
+
bodyRef: input.ref,
|
|
145
|
+
bodyJson: input.data,
|
|
146
|
+
disposition: input.disposition,
|
|
147
|
+
urgency: input.tier,
|
|
148
|
+
wake: 1,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function startWatcherExecution(nodeId) {
|
|
152
|
+
process.env['CRTR_EXECUTION_ID'] = beginExecution(nodeId, { daemonEpoch: 'test' });
|
|
153
|
+
resetStaleExecutionMail(Date.now());
|
|
121
154
|
}
|
|
122
155
|
function registerWatcher(pi) {
|
|
123
156
|
ensureWatcherNode();
|
|
124
|
-
|
|
157
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
158
|
+
startWatcherExecution(nodeId);
|
|
125
159
|
const dispose = registerCanvasInboxWatcher(pi);
|
|
126
|
-
void pi.fire('session_start', { type: 'session_start' }, undefined);
|
|
160
|
+
void server.ready.then(() => pi.fire('session_start', { type: 'session_start' }, undefined));
|
|
127
161
|
return dispose;
|
|
128
162
|
}
|
|
129
163
|
let origTick;
|
|
130
164
|
let origDebounce;
|
|
131
165
|
before(() => {
|
|
132
166
|
origHome = process.env['CRTR_HOME'];
|
|
167
|
+
origUserHome = process.env['HOME'];
|
|
133
168
|
origNode = process.env['CRTR_NODE_ID'];
|
|
169
|
+
origExecution = process.env['CRTR_EXECUTION_ID'];
|
|
134
170
|
origTick = process.env['CRTR_WATCHER_TICK_MS'];
|
|
135
171
|
origDebounce = process.env['CRTR_WATCHER_DEBOUNCE_MS'];
|
|
136
172
|
process.env['CRTR_WATCHER_TICK_MS'] = String(TICK_MS);
|
|
@@ -142,16 +178,26 @@ afterEach(async () => {
|
|
|
142
178
|
await server?.close();
|
|
143
179
|
server = undefined;
|
|
144
180
|
closeDb();
|
|
181
|
+
closeScopedStores();
|
|
182
|
+
resetScopeCache();
|
|
145
183
|
});
|
|
146
184
|
after(() => {
|
|
147
185
|
if (origHome === undefined)
|
|
148
186
|
delete process.env['CRTR_HOME'];
|
|
149
187
|
else
|
|
150
188
|
process.env['CRTR_HOME'] = origHome;
|
|
189
|
+
if (origUserHome === undefined)
|
|
190
|
+
delete process.env['HOME'];
|
|
191
|
+
else
|
|
192
|
+
process.env['HOME'] = origUserHome;
|
|
151
193
|
if (origNode === undefined)
|
|
152
194
|
delete process.env['CRTR_NODE_ID'];
|
|
153
195
|
else
|
|
154
196
|
process.env['CRTR_NODE_ID'] = origNode;
|
|
197
|
+
if (origExecution === undefined)
|
|
198
|
+
delete process.env['CRTR_EXECUTION_ID'];
|
|
199
|
+
else
|
|
200
|
+
process.env['CRTR_EXECUTION_ID'] = origExecution;
|
|
155
201
|
if (origTick === undefined)
|
|
156
202
|
delete process.env['CRTR_WATCHER_TICK_MS'];
|
|
157
203
|
else
|
|
@@ -160,6 +206,7 @@ after(() => {
|
|
|
160
206
|
delete process.env['CRTR_WATCHER_DEBOUNCE_MS'];
|
|
161
207
|
else
|
|
162
208
|
process.env['CRTR_WATCHER_DEBOUNCE_MS'] = origDebounce;
|
|
209
|
+
resetScopeCache();
|
|
163
210
|
for (const h of homes) {
|
|
164
211
|
try {
|
|
165
212
|
rmSync(h, { recursive: true, force: true });
|
|
@@ -174,7 +221,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
174
221
|
const meta = {
|
|
175
222
|
node_id: 'node-refresh', name: 'r', created: new Date().toISOString(),
|
|
176
223
|
cwd: '/tmp', kind: 'general', mode: 'base', lifecycle: 'resident',
|
|
177
|
-
status: 'active',
|
|
224
|
+
status: 'active',
|
|
178
225
|
};
|
|
179
226
|
createNode(meta);
|
|
180
227
|
const pi = makeFakePi();
|
|
@@ -182,69 +229,16 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
182
229
|
// Streaming (mid-turn) when a child finishes — normally this would steer.
|
|
183
230
|
pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
184
231
|
await wait(TICK_MS + 100);
|
|
185
|
-
|
|
232
|
+
publishMail('node-refresh', { from: 'child-x', tier: 'urgent', kind: 'final', label: 'done' });
|
|
186
233
|
await waitForFirstInjection(pi);
|
|
187
|
-
assert.equal(pi.injected.length, 1, 'the live broker receives the handoff before its
|
|
188
|
-
// The
|
|
189
|
-
//
|
|
234
|
+
assert.equal(pi.injected.length, 1, 'the live broker receives the handoff before its delivery receipt');
|
|
235
|
+
// The delivery receipt decides the refresh hold; clearing intent does not
|
|
236
|
+
// manufacture a second handoff.
|
|
237
|
+
setIntent('node-refresh', 'refresh');
|
|
190
238
|
setIntent('node-refresh', null);
|
|
191
239
|
await wait(SETTLE_MS);
|
|
192
240
|
assert.equal(pi.injected.length, 1, 'the same handoff is not delivered twice');
|
|
193
241
|
});
|
|
194
|
-
test('replacement session binding holds a late old-broker cursor commit', async () => {
|
|
195
|
-
freshNode('node-refresh-replacement');
|
|
196
|
-
closeDb();
|
|
197
|
-
const meta = {
|
|
198
|
-
node_id: 'node-refresh-replacement', name: 'r', created: new Date().toISOString(),
|
|
199
|
-
cwd: '/tmp', kind: 'general', mode: 'base', lifecycle: 'resident',
|
|
200
|
-
status: 'active', intent: 'refresh',
|
|
201
|
-
};
|
|
202
|
-
createNode(meta);
|
|
203
|
-
const entry = appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff before replacement' });
|
|
204
|
-
recordPid(meta.node_id, 101);
|
|
205
|
-
const client = CrtrClient.forLocalSocket({ autostart: false });
|
|
206
|
-
// The replacement binds first, consuming refresh and recording its own
|
|
207
|
-
// process generation. The old broker's delayed request must still hold.
|
|
208
|
-
await client.bindBrokerSession(meta.node_id, {
|
|
209
|
-
piSessionId: 'replacement-session', sessionFile: '/tmp/replacement.jsonl',
|
|
210
|
-
pid: 202, reason: 'startup', reviewBoundaryIds: [],
|
|
211
|
-
});
|
|
212
|
-
const oldResult = await client.advanceBrokerInboxCursor(meta.node_id, {
|
|
213
|
-
throughEntryId: entry.entry_id, brokerPid: 101,
|
|
214
|
-
});
|
|
215
|
-
assert.deepEqual(oldResult, { action: 'hold_refresh' });
|
|
216
|
-
assert.equal(readCursor(meta.node_id), undefined, 'the stale broker cannot consume the replacement replay');
|
|
217
|
-
const replacementResult = await client.advanceBrokerInboxCursor(meta.node_id, {
|
|
218
|
-
throughEntryId: entry.entry_id, brokerPid: 202,
|
|
219
|
-
});
|
|
220
|
-
assert.deepEqual(replacementResult, { action: 'advanced' });
|
|
221
|
-
assert.equal(readCursor(meta.node_id), entry.entry_id, 'only the replacement broker advances the durable cursor');
|
|
222
|
-
});
|
|
223
|
-
test('cursor rejects a nonexistent or backward physical entry', async () => {
|
|
224
|
-
freshNode('node-cursor-physical-boundary');
|
|
225
|
-
closeDb();
|
|
226
|
-
const meta = {
|
|
227
|
-
node_id: 'node-cursor-physical-boundary', name: 'r', created: new Date().toISOString(),
|
|
228
|
-
cwd: '/tmp', kind: 'general', mode: 'base', lifecycle: 'resident', status: 'active',
|
|
229
|
-
};
|
|
230
|
-
createNode(meta);
|
|
231
|
-
recordPid(meta.node_id, 303);
|
|
232
|
-
const first = appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'first' });
|
|
233
|
-
const second = appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'second' });
|
|
234
|
-
const client = CrtrClient.forLocalSocket({ autostart: false });
|
|
235
|
-
const rejected = (request) => client
|
|
236
|
-
.advanceBrokerInboxCursor(meta.node_id, request)
|
|
237
|
-
.then(() => assert.fail('cursor request should fail'))
|
|
238
|
-
.catch((error) => {
|
|
239
|
-
assert.ok(error instanceof ApiError);
|
|
240
|
-
assert.equal(error.status, 400);
|
|
241
|
-
});
|
|
242
|
-
await rejected({ throughEntryId: 'f'.repeat(32), brokerPid: 303 });
|
|
243
|
-
assert.equal(readCursor(meta.node_id), undefined, 'a well-shaped unknown id cannot poison the cursor');
|
|
244
|
-
await client.advanceBrokerInboxCursor(meta.node_id, { throughEntryId: second.entry_id, brokerPid: 303 });
|
|
245
|
-
await rejected({ throughEntryId: first.entry_id, brokerPid: 303 });
|
|
246
|
-
assert.equal(readCursor(meta.node_id), second.entry_id, 'a late commit cannot move the durable cursor backward');
|
|
247
|
-
});
|
|
248
242
|
test('refresh-yield replays a handoff that reached the discarded conversation', async () => {
|
|
249
243
|
freshNode('node-refresh-handoff');
|
|
250
244
|
closeDb();
|
|
@@ -258,15 +252,17 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
258
252
|
disposers.push(registerWatcher(pi));
|
|
259
253
|
pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
260
254
|
await wait(TICK_MS + 100);
|
|
261
|
-
|
|
255
|
+
publishMail(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at yield' });
|
|
262
256
|
await waitForFirstInjection(pi);
|
|
263
257
|
assert.equal(pi.injected.length, 1, 'the active conversation received the handoff before yield');
|
|
264
258
|
// Yield discards that conversation before it can act on the queued handoff.
|
|
265
259
|
// Its settle must leave the entry durable for the fresh revive's kickoff.
|
|
266
260
|
setIntent(meta.node_id, 'refresh');
|
|
267
261
|
await settle(pi);
|
|
268
|
-
assert.equal(
|
|
269
|
-
const
|
|
262
|
+
assert.equal(acknowledgedNodeMailCount(meta.node_id), 0, 'the discarded conversation cannot commit its inbox handoff');
|
|
263
|
+
const replacementExecution = beginExecution(meta.node_id, { daemonEpoch: 'test' });
|
|
264
|
+
resetStaleExecutionMail(Date.now());
|
|
265
|
+
const bearings = drainBearings(getNode(meta.node_id), replacementExecution);
|
|
270
266
|
assert.match(bearings.unreadDigest ?? '', /handoff at yield/, 'the fresh revive receives the uncommitted handoff');
|
|
271
267
|
});
|
|
272
268
|
test('idle: a finished node triggers a fresh turn (no deliverAs)', async () => {
|
|
@@ -275,7 +271,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
275
271
|
disposers.push(registerWatcher(pi));
|
|
276
272
|
// No agent_start fired → watcher treats the node as idle.
|
|
277
273
|
await wait(TICK_MS + 100);
|
|
278
|
-
|
|
274
|
+
publishMail('node-idle', { from: 'child-3', tier: 'normal', kind: 'final', label: 'done while idle' });
|
|
279
275
|
await waitForFirstInjection(pi);
|
|
280
276
|
assert.equal(pi.injected.length, 1);
|
|
281
277
|
assert.equal(pi.injected[0].deliverAs, undefined, 'idle → sendUserMessage triggers a turn, no deliverAs');
|
|
@@ -285,7 +281,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
285
281
|
const pi = makeFakePi();
|
|
286
282
|
disposers.push(registerWatcher(pi));
|
|
287
283
|
await wait(TICK_MS + 100);
|
|
288
|
-
const entry =
|
|
284
|
+
const entry = publishMail('node-deferred-engine-prompt', {
|
|
289
285
|
from: 'child-4',
|
|
290
286
|
tier: 'deferred',
|
|
291
287
|
kind: 'update',
|
|
@@ -317,11 +313,11 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
317
313
|
assert.equal(order.length, 2, 'one held card and the one human prompt share the admitted operation');
|
|
318
314
|
assert.match(order[0], /held update/, 'the held inbox card is first');
|
|
319
315
|
assert.equal(order[1], 'where are we?', 'the human body follows the held card');
|
|
320
|
-
assert.equal(
|
|
316
|
+
assert.equal(acknowledgedNodeMailCount('node-deferred-engine-prompt'), 0, 'the joined handoff waits for settlement');
|
|
321
317
|
await pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
322
318
|
await settle(pi);
|
|
323
|
-
assert.equal(
|
|
324
|
-
const staleFollowUpEntry =
|
|
319
|
+
assert.equal(acknowledgedNodeMailCount('node-deferred-engine-prompt'), 1, 'agent_settled commits the broker-joined handoff');
|
|
320
|
+
const staleFollowUpEntry = publishMail('node-deferred-engine-prompt', {
|
|
325
321
|
from: 'child-4',
|
|
326
322
|
tier: 'deferred',
|
|
327
323
|
kind: 'update',
|
|
@@ -335,14 +331,14 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
335
331
|
assert.equal(order.at(-1), 'continue from there');
|
|
336
332
|
await pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
337
333
|
await settle(pi);
|
|
338
|
-
assert.equal(
|
|
334
|
+
assert.equal(acknowledgedNodeMailCount('node-deferred-engine-prompt'), 2, 'the follow-up prompt settles its joined handoff too');
|
|
339
335
|
});
|
|
340
336
|
test('a held deferred batch is prepended to an admitted human prompt and replays if that prompt reloads before settlement', async () => {
|
|
341
337
|
freshNode('node-deferred-human-prompt');
|
|
342
338
|
const pi = makeFakePi();
|
|
343
339
|
disposers.push(registerWatcher(pi));
|
|
344
340
|
await wait(TICK_MS + 100);
|
|
345
|
-
const entry =
|
|
341
|
+
const entry = publishMail('node-deferred-human-prompt', {
|
|
346
342
|
from: 'child-4',
|
|
347
343
|
tier: 'deferred',
|
|
348
344
|
kind: 'update',
|
|
@@ -356,9 +352,9 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
356
352
|
});
|
|
357
353
|
assert.equal(firstPromptPrefix.length, 1, 'the held card is sent before the human prompt starts');
|
|
358
354
|
assert.match(firstPromptPrefix[0], /held update/);
|
|
359
|
-
assert.equal(
|
|
355
|
+
assert.equal(acknowledgedNodeMailCount('node-deferred-human-prompt'), 0, 'the pre-prompt handoff is not committed');
|
|
360
356
|
await shutdown(pi);
|
|
361
|
-
assert.equal(
|
|
357
|
+
assert.equal(acknowledgedNodeMailCount('node-deferred-human-prompt'), 0, 'a reload before agent_settled leaves the held entry replayable');
|
|
362
358
|
const replacement = makeFakePi();
|
|
363
359
|
disposers.push(registerWatcher(replacement));
|
|
364
360
|
await wait(SETTLE_MS);
|
|
@@ -370,20 +366,14 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
370
366
|
assert.match(replayedPrefix[0], /held update/);
|
|
371
367
|
await replacement.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
372
368
|
await settle(replacement);
|
|
373
|
-
assert.equal(
|
|
369
|
+
assert.equal(acknowledgedNodeMailCount('node-deferred-human-prompt'), 1, 'only agent_settled commits the prompt-joined handoff');
|
|
374
370
|
});
|
|
375
371
|
// Pause makes the profile inert while its broker is still alive: the entry
|
|
376
372
|
// must be RETAINED, not consumed. A critical entry is the sharpest case —
|
|
377
373
|
// its route aborts the live turn, exactly the punch-through pause forbids.
|
|
378
374
|
test('a paused profile holds queued entries in a live broker until resume', async () => {
|
|
379
375
|
freshNode('node-paused-profile');
|
|
380
|
-
closeDb();
|
|
381
|
-
const origUserHome = process.env.HOME;
|
|
382
376
|
const origProfile = process.env['CRTR_PROFILE_ID'];
|
|
383
|
-
const userHome = join(tmpdir(), `crtr-w-home-${Math.random().toString(36).slice(2, 8)}`);
|
|
384
|
-
mkdirSync(userHome, { recursive: true });
|
|
385
|
-
homes.push(userHome);
|
|
386
|
-
process.env.HOME = userHome;
|
|
387
377
|
try {
|
|
388
378
|
const profile = createProfile('paused watcher', [{ path: '/tmp', memory: 'content' }]);
|
|
389
379
|
pauseProfile(profile.profileId);
|
|
@@ -391,20 +381,16 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
391
381
|
const pi = makeFakePi();
|
|
392
382
|
disposers.push(registerWatcher(pi));
|
|
393
383
|
await wait(TICK_MS + 100);
|
|
394
|
-
|
|
384
|
+
publishMail('node-paused-profile', { from: 'child-1', tier: 'critical', kind: 'update', label: 'sent while paused' });
|
|
395
385
|
await wait(SETTLE_MS);
|
|
396
386
|
assert.equal(pi.injected.length, 0, 'a paused profile delivers nothing to its live broker');
|
|
397
|
-
assert.equal(
|
|
387
|
+
assert.equal(acknowledgedNodeMailCount('node-paused-profile'), 0, 'the held entry stays unconsumed');
|
|
398
388
|
resumeProfile(profile.profileId);
|
|
399
389
|
await waitForFirstInjection(pi);
|
|
400
390
|
assert.equal(pi.injected.length, 1, 'resume delivers the entry that waited');
|
|
401
391
|
assert.match(pi.injected[0].content, /sent while paused/);
|
|
402
392
|
}
|
|
403
393
|
finally {
|
|
404
|
-
if (origUserHome === undefined)
|
|
405
|
-
delete process.env.HOME;
|
|
406
|
-
else
|
|
407
|
-
process.env.HOME = origUserHome;
|
|
408
394
|
if (origProfile === undefined)
|
|
409
395
|
delete process.env['CRTR_PROFILE_ID'];
|
|
410
396
|
else
|
|
@@ -435,16 +421,17 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
|
|
|
435
421
|
};
|
|
436
422
|
// An unread entry already sits in the inbox before the watcher is even
|
|
437
423
|
// registered — the same shape as a revived broker with pre-existing work.
|
|
438
|
-
const entry =
|
|
424
|
+
const entry = publishMail('node-action-gate', { from: 'child-1', tier: 'normal', kind: 'update', label: 'pre-bind entry' });
|
|
439
425
|
// Registration alone (the extension factory, before Pi has bound actions)
|
|
440
426
|
// must be side-effect-free: no timer armed, no tick run, no action attempted
|
|
441
427
|
// — even across several tick+debounce windows.
|
|
442
428
|
ensureWatcherNode();
|
|
429
|
+
startWatcherExecution('node-action-gate');
|
|
443
430
|
disposers.push(registerCanvasInboxWatcher(pi));
|
|
444
431
|
await wait(TICK_MS * 5 + DEBOUNCE_MS + 50);
|
|
445
432
|
assert.equal(sendAttempts, 0, 'no action call is attempted before session_start arms the watcher');
|
|
446
433
|
assert.equal(pi.injected.length, 0, 'nothing handed off before session_start');
|
|
447
|
-
assert.equal(
|
|
434
|
+
assert.equal(acknowledgedNodeMailCount('node-action-gate'), 0, 'no delivery is acknowledged before session_start');
|
|
448
435
|
// session_start is pi's real bind signal: actions work from here on. Arming
|
|
449
436
|
// now must pick up the already-buffered pre-bind entry.
|
|
450
437
|
actionsReady = true;
|
|
@@ -452,9 +439,9 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
|
|
|
452
439
|
await waitForFirstInjection(pi);
|
|
453
440
|
assert.equal(sendAttempts, 1, 'exactly one action attempt once session_start arms the watcher');
|
|
454
441
|
assert.equal(pi.injected.length, 1, 'exactly one handoff once actions are bound');
|
|
455
|
-
assert.equal(
|
|
442
|
+
assert.equal(acknowledgedNodeMailCount('node-action-gate'), 0, 'still uncommitted until agent_settled');
|
|
456
443
|
await settle(pi);
|
|
457
|
-
assert.equal(
|
|
444
|
+
assert.equal(acknowledgedNodeMailCount('node-action-gate'), 1, 'agent_settled acknowledges the delivery');
|
|
458
445
|
// A repeated session_start (e.g. a resume) must not re-arm a second
|
|
459
446
|
// interval or re-run the initial tick — no duplicate delivery.
|
|
460
447
|
pi.fire('session_start', { type: 'session_start' }, undefined);
|
|
@@ -465,14 +452,15 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
|
|
|
465
452
|
test('disposal before any session_start is safe (no timer to leak, no action ever attempted)', async () => {
|
|
466
453
|
freshNode('node-dispose-before-start');
|
|
467
454
|
const pi = makeFakePi();
|
|
468
|
-
|
|
455
|
+
publishMail('node-dispose-before-start', { from: 'child-1', tier: 'normal', kind: 'update', label: 'never armed' });
|
|
456
|
+
startWatcherExecution('node-dispose-before-start');
|
|
469
457
|
const dispose = registerCanvasInboxWatcher(pi);
|
|
470
458
|
// Dispose immediately, before session_start ever fires — models a reload
|
|
471
459
|
// that discards this generation before Pi finishes binding.
|
|
472
460
|
dispose();
|
|
473
461
|
await wait(TICK_MS * 3 + DEBOUNCE_MS + 50);
|
|
474
462
|
assert.equal(pi.injected.length, 0, 'a watcher disposed before session_start never delivers');
|
|
475
|
-
assert.equal(
|
|
463
|
+
assert.equal(acknowledgedNodeMailCount('node-dispose-before-start'), 0, 'disposal before start commits nothing');
|
|
476
464
|
// A session_start firing after disposal (a stale handler on a discarded
|
|
477
465
|
// generation) must also be inert.
|
|
478
466
|
pi.fire('session_start', { type: 'session_start' }, undefined);
|
|
@@ -480,38 +468,26 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
|
|
|
480
468
|
assert.equal(pi.injected.length, 0, 'session_start after disposal does not resurrect the discarded generation');
|
|
481
469
|
});
|
|
482
470
|
});
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
// never fired before exit; THERE `ctx.hasPendingMessages()` still gates the
|
|
493
|
-
// commit, because an unconsumed queued send dies with the process. A watcher
|
|
494
|
-
// that dies (crash, SIGTERM, daemon kick) before a committing settle leaves the
|
|
495
|
-
// on-disk cursor behind, so a fresh watcher re-reads and re-delivers rather
|
|
496
|
-
// than silently losing the entry. See canvas-inbox-watcher.ts's header/inline
|
|
497
|
-
// comments on `handedOffWatermark`, `commitTarget`, `commitAtSettle`, and
|
|
498
|
-
// `commitAtShutdown` for the full rationale.
|
|
499
|
-
describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
500
|
-
test('mid-turn handoff, crash before settle: on-disk cursor NOT advanced, fresh watcher re-delivers', async () => {
|
|
501
|
-
freshNode('node-cursor-crash');
|
|
471
|
+
// SQL delivery receipts preserve at-least-once delivery. A claimed delivery is
|
|
472
|
+
// acknowledged only at agent_settled, never at the fire-and-forget send return
|
|
473
|
+
// or agent_end. A watcher that exits before acknowledgment leaves its claim for
|
|
474
|
+
// the replacement execution to inherit and deliver again. session_shutdown
|
|
475
|
+
// retains a claim when Pi still has pending messages, because that queued send
|
|
476
|
+
// would otherwise die with the process.
|
|
477
|
+
describe('canvas inbox watcher — SQL delivery receipts (at-least-once)', () => {
|
|
478
|
+
test('mid-turn handoff, crash before settle: delivery remains unacknowledged and a fresh watcher re-delivers', async () => {
|
|
479
|
+
freshNode('node-receipt-crash');
|
|
502
480
|
const pi1 = makeFakePi();
|
|
503
481
|
disposers.push(registerWatcher(pi1));
|
|
504
482
|
// Streaming (mid-turn) so this delivers via followUp, not the idle path.
|
|
505
483
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
506
484
|
await wait(TICK_MS + 100);
|
|
507
|
-
|
|
485
|
+
publishMail('node-receipt-crash', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still working' });
|
|
508
486
|
await waitForFirstInjection(pi1);
|
|
509
487
|
assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
|
|
510
488
|
assert.equal(pi1.injected[0].deliverAs, 'followUp');
|
|
511
|
-
// The
|
|
512
|
-
|
|
513
|
-
// commit-at-buffer-time / commit-on-send variant would have committed here).
|
|
514
|
-
assert.equal(readCursor('node-cursor-crash'), undefined, 'on-disk cursor not committed before a settle');
|
|
489
|
+
// The delivery remains handed off until settle; a send return alone must not acknowledge it.
|
|
490
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-crash'), 0, 'delivery is not acknowledged before a settle');
|
|
515
491
|
// Simulate the watcher dying without a graceful session_shutdown — never
|
|
516
492
|
// settle OR fire session_shutdown on pi1. The broker's SIGTERM handler DOES
|
|
517
493
|
// run a JS handler (disposeAndExit: session.dispose() + process.exit), but
|
|
@@ -525,44 +501,43 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
525
501
|
assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the same uncommitted entry');
|
|
526
502
|
assert.match(pi2.injected[0].content, /still working/);
|
|
527
503
|
});
|
|
528
|
-
test('mid-turn handoff + agent_settled:
|
|
529
|
-
freshNode('node-
|
|
504
|
+
test('mid-turn handoff + agent_settled: delivery acknowledged, fresh watcher does NOT redeliver', async () => {
|
|
505
|
+
freshNode('node-receipt-committed');
|
|
530
506
|
const pi1 = makeFakePi();
|
|
531
507
|
disposers.push(registerWatcher(pi1));
|
|
532
508
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
533
509
|
await wait(TICK_MS + 100);
|
|
534
|
-
const entry =
|
|
510
|
+
const entry = publishMail('node-receipt-committed', { from: 'child-2', tier: 'normal', kind: 'update', label: 'still working' });
|
|
535
511
|
await waitForFirstInjection(pi1);
|
|
536
512
|
assert.equal(pi1.injected.length, 1);
|
|
537
|
-
assert.equal(
|
|
513
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-committed'), 0, 'not yet committed before the settle');
|
|
538
514
|
// The turn settles with pi's queue drained (the followUp was consumed) —
|
|
539
|
-
// THAT
|
|
515
|
+
// THAT acknowledges the delivery.
|
|
540
516
|
await settle(pi1);
|
|
541
|
-
assert.equal(
|
|
517
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-committed'), 1, 'delivery acknowledged at agent_settled');
|
|
542
518
|
const pi2 = makeFakePi();
|
|
543
519
|
disposers.push(registerWatcher(pi2));
|
|
544
520
|
await wait(SETTLE_MS);
|
|
545
521
|
assert.equal(pi2.injected.length, 0, 'a fresh watcher does not redeliver an already-committed entry');
|
|
546
522
|
});
|
|
547
523
|
test('idle-path delivery: NOT committed on the void send, committed at agent_settled', async () => {
|
|
548
|
-
freshNode('node-
|
|
524
|
+
freshNode('node-receipt-idle');
|
|
549
525
|
const pi = makeFakePi();
|
|
550
526
|
disposers.push(registerWatcher(pi));
|
|
551
527
|
// No agent_start fired → watcher treats the node as idle.
|
|
552
528
|
await wait(TICK_MS + 100);
|
|
553
|
-
const entry =
|
|
529
|
+
const entry = publishMail('node-receipt-idle', { from: 'child-3', tier: 'normal', kind: 'update', label: 'idle update' });
|
|
554
530
|
await waitForFirstInjection(pi);
|
|
555
531
|
assert.equal(pi.injected.length, 1);
|
|
556
532
|
assert.equal(pi.injected[0].deliverAs, undefined, 'idle delivery has no deliverAs');
|
|
557
533
|
// The idle send triggers a turn, but its void return is a handoff, not proof
|
|
558
|
-
// of acceptance — the
|
|
559
|
-
|
|
560
|
-
assert.equal(readCursor('node-cursor-idle'), undefined, 'idle send does not commit on the fire-and-forget return');
|
|
534
|
+
// of acceptance — the delivery remains handed off until the turn settles.
|
|
535
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-idle'), 0, 'idle send does not commit on the fire-and-forget return');
|
|
561
536
|
await settle(pi);
|
|
562
|
-
assert.equal(
|
|
537
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-idle'), 1, 'idle-path delivery commits at the turn it started settling');
|
|
563
538
|
});
|
|
564
|
-
test('synchronous send throw then agent_settled:
|
|
565
|
-
freshNode('node-
|
|
539
|
+
test('synchronous send throw then agent_settled: re-buffered delivery remains unacknowledged and a fresh watcher re-delivers', async () => {
|
|
540
|
+
freshNode('node-receipt-failsend');
|
|
566
541
|
const pi1 = makeFakePi();
|
|
567
542
|
// Persistent synchronous send failure: the entry is read + buffered, flush
|
|
568
543
|
// throws, it is re-buffered and never handed off.
|
|
@@ -570,12 +545,12 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
570
545
|
disposers.push(registerWatcher(pi1));
|
|
571
546
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
572
547
|
await wait(TICK_MS + 100);
|
|
573
|
-
|
|
548
|
+
publishMail('node-receipt-failsend', { from: 'child-1', tier: 'normal', kind: 'update', label: 'never delivered' });
|
|
574
549
|
await wait(SETTLE_MS);
|
|
575
550
|
assert.equal(pi1.injected.length, 0, 'the failing send delivered nothing');
|
|
576
551
|
// agent_settled must NOT commit past the still-re-buffered (never-handed-off) entry.
|
|
577
552
|
await settle(pi1);
|
|
578
|
-
assert.equal(
|
|
553
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-failsend'), 0, 're-buffered, never-delivered mail is not acknowledged');
|
|
579
554
|
const pi2 = makeFakePi();
|
|
580
555
|
disposers.push(registerWatcher(pi2));
|
|
581
556
|
await waitForFirstInjection(pi2);
|
|
@@ -583,7 +558,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
583
558
|
assert.match(pi2.injected[0].content, /never delivered/);
|
|
584
559
|
});
|
|
585
560
|
test('critical abort then abort-generated settle: pending critical entry NOT committed, fresh watcher re-delivers', async () => {
|
|
586
|
-
freshNode('node-
|
|
561
|
+
freshNode('node-receipt-critical');
|
|
587
562
|
const pi1 = makeFakePi();
|
|
588
563
|
let aborted = 0;
|
|
589
564
|
disposers.push(registerWatcher(pi1));
|
|
@@ -591,15 +566,15 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
591
566
|
// preempts via ctx.abort()).
|
|
592
567
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false, abort: () => { aborted += 1; } });
|
|
593
568
|
await wait(TICK_MS + 100);
|
|
594
|
-
|
|
569
|
+
publishMail('node-receipt-critical', { from: 'child-1', tier: 'critical', kind: 'update', label: 'critical work' });
|
|
595
570
|
await wait(SETTLE_MS);
|
|
596
571
|
assert.ok(aborted >= 1, 'the critical entry aborted the live turn');
|
|
597
572
|
assert.equal(pi1.injected.length, 0, 'the critical path re-buffers, it does not deliver mid-stream');
|
|
598
573
|
// The abort-generated agent_end/settle must not commit the pending critical
|
|
599
574
|
// entry (it is re-buffered, never handed off).
|
|
600
|
-
pi1.fire('agent_end', { type: 'agent_end', messages: [] }, { isIdle: () =>
|
|
601
|
-
await settle(pi1);
|
|
602
|
-
assert.equal(
|
|
575
|
+
pi1.fire('agent_end', { type: 'agent_end', messages: [] }, { isIdle: () => false });
|
|
576
|
+
await settle(pi1, { idle: false });
|
|
577
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-critical'), 0, 'abort-generated settle does not commit the pending critical entry before its retry');
|
|
603
578
|
const pi2 = makeFakePi();
|
|
604
579
|
disposers.push(registerWatcher(pi2));
|
|
605
580
|
await waitForFirstInjection(pi2);
|
|
@@ -607,7 +582,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
607
582
|
assert.match(pi2.injected[0].content, /critical work/);
|
|
608
583
|
});
|
|
609
584
|
test('batch A settles while batch B still debounces: commit stops at A, fresh watcher re-delivers B', async () => {
|
|
610
|
-
freshNode('node-
|
|
585
|
+
freshNode('node-receipt-batchB');
|
|
611
586
|
// Wide debounce for THIS watcher so B stays buffered-but-unflushed when we
|
|
612
587
|
// settle A; the watcher captures the cadence at registration, so restore the
|
|
613
588
|
// file default immediately after for the fresh (pi2) watcher.
|
|
@@ -617,17 +592,16 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
617
592
|
process.env['CRTR_WATCHER_DEBOUNCE_MS'] = String(DEBOUNCE_MS);
|
|
618
593
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
619
594
|
await wait(TICK_MS + 60);
|
|
620
|
-
const a =
|
|
595
|
+
const a = publishMail('node-receipt-batchB', { from: 'child-a', tier: 'normal', kind: 'update', label: 'batch A' });
|
|
621
596
|
await waitForFirstInjection(pi1);
|
|
622
597
|
assert.equal(pi1.injected.length, 1, 'batch A delivered mid-turn');
|
|
623
598
|
assert.equal(pi1.injected[0].deliverAs, 'followUp');
|
|
624
|
-
const b =
|
|
599
|
+
const b = publishMail('node-receipt-batchB', { from: 'child-b', tier: 'normal', kind: 'update', label: 'batch B' });
|
|
625
600
|
await wait(70); // B read into the buffer but < 200ms debounce → not yet flushed
|
|
626
601
|
// A's turn settles while B is still debouncing in the buffer. The commit must
|
|
627
602
|
// clamp below the still-buffered B and land on A only.
|
|
628
603
|
await settle(pi1);
|
|
629
|
-
assert.equal(
|
|
630
|
-
assert.notEqual(readCursor('node-cursor-batchB'), b.entry_id, 'commit does not cross the still-buffered batch B');
|
|
604
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-batchB'), 1, 'commit lands on handed-off batch A');
|
|
631
605
|
const pi2 = makeFakePi();
|
|
632
606
|
disposers.push(registerWatcher(pi2));
|
|
633
607
|
await waitForFirstInjection(pi2);
|
|
@@ -640,26 +614,25 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
640
614
|
// — a context reporting pending sends (hasPendingMessages() = true) no longer
|
|
641
615
|
// vetoes it. This is the exact real-incident invariant behind #353/#362: a
|
|
642
616
|
// settle-time veto that declines the sole commit, followed by cooperative
|
|
643
|
-
// idle-release exit, permanently
|
|
644
|
-
// clear it.
|
|
617
|
+
// idle-release exit, permanently leaves the mail handed off with no later settle.
|
|
645
618
|
test('agent_settled commits unconditionally even when hasPendingMessages() reports pending', async () => {
|
|
646
|
-
freshNode('node-
|
|
619
|
+
freshNode('node-receipt-failhold');
|
|
647
620
|
const pi1 = makeFakePi();
|
|
648
621
|
disposers.push(registerWatcher(pi1));
|
|
649
622
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
650
623
|
await wait(TICK_MS + 100);
|
|
651
|
-
const entry =
|
|
624
|
+
const entry = publishMail('node-receipt-failhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'queued across a failure' });
|
|
652
625
|
await waitForFirstInjection(pi1);
|
|
653
626
|
assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
|
|
654
627
|
assert.equal(pi1.injected[0].deliverAs, 'followUp');
|
|
655
628
|
// A failure/aborted agent_end is never a commit point regardless of queue
|
|
656
629
|
// state, so it must NOT commit.
|
|
657
630
|
pi1.fire('agent_end', { type: 'agent_end', messages: [] }, { isIdle: () => false });
|
|
658
|
-
assert.equal(
|
|
631
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-failhold'), 0, 'a failure agent_end does not commit');
|
|
659
632
|
// The settle commits even though the context reports pending sends —
|
|
660
633
|
// agent_settled no longer declines on hasPendingMessages().
|
|
661
634
|
await settle(pi1, { pending: true });
|
|
662
|
-
assert.equal(
|
|
635
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-failhold'), 1, 'agent_settled commits unconditionally, even when hasPendingMessages() reports pending');
|
|
663
636
|
const pi2 = makeFakePi();
|
|
664
637
|
disposers.push(registerWatcher(pi2));
|
|
665
638
|
await wait(SETTLE_MS);
|
|
@@ -671,10 +644,10 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
671
644
|
// the process — committing past it would durably lose a message pi never
|
|
672
645
|
// delivered.
|
|
673
646
|
test('session_shutdown keeps a refresh handoff replayable when no messages are pending', async () => {
|
|
674
|
-
freshNode('node-
|
|
647
|
+
freshNode('node-receipt-refresh-shutdown');
|
|
675
648
|
closeDb();
|
|
676
649
|
const meta = {
|
|
677
|
-
node_id: 'node-
|
|
650
|
+
node_id: 'node-receipt-refresh-shutdown', name: 'r', created: new Date().toISOString(),
|
|
678
651
|
cwd: '/tmp', kind: 'general', mode: 'base', lifecycle: 'resident',
|
|
679
652
|
status: 'active',
|
|
680
653
|
};
|
|
@@ -683,26 +656,26 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
683
656
|
disposers.push(registerWatcher(pi));
|
|
684
657
|
pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
685
658
|
await wait(TICK_MS + 100);
|
|
686
|
-
|
|
659
|
+
publishMail(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at shutdown' });
|
|
687
660
|
await waitForFirstInjection(pi);
|
|
688
661
|
assert.equal(pi.injected.length, 1, 'the active conversation received the handoff before refresh');
|
|
689
662
|
setIntent(meta.node_id, 'refresh');
|
|
690
663
|
await shutdown(pi, { pending: false });
|
|
691
|
-
assert.equal(
|
|
664
|
+
assert.equal(acknowledgedNodeMailCount(meta.node_id), 0, 'a refresh keeps the shutdown handoff uncommitted for replay');
|
|
692
665
|
});
|
|
693
666
|
test('session_shutdown still holds while hasPendingMessages() reports pending (no agent_settled fired)', async () => {
|
|
694
|
-
freshNode('node-
|
|
667
|
+
freshNode('node-receipt-shutdownhold');
|
|
695
668
|
const pi1 = makeFakePi();
|
|
696
669
|
const dispose = registerWatcher(pi1);
|
|
697
670
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
698
671
|
await wait(TICK_MS + 100);
|
|
699
|
-
const entry =
|
|
672
|
+
const entry = publishMail('node-receipt-shutdownhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still queued at shutdown' });
|
|
700
673
|
await waitForFirstInjection(pi1);
|
|
701
674
|
assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
|
|
702
675
|
// session_shutdown fires with no prior agent_settled, and the context
|
|
703
676
|
// reports an unconsumed queued send — the hold declines the commit.
|
|
704
677
|
await shutdown(pi1, { pending: true });
|
|
705
|
-
assert.equal(
|
|
678
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-shutdownhold'), 0, 'session_shutdown declines to commit while pi reports pending sends');
|
|
706
679
|
dispose();
|
|
707
680
|
// A fresh watcher re-reads and re-delivers, then a drained settle commits.
|
|
708
681
|
const pi2 = makeFakePi();
|
|
@@ -711,27 +684,27 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
711
684
|
assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the never-committed entry');
|
|
712
685
|
assert.match(pi2.injected[0].content, /still queued at shutdown/);
|
|
713
686
|
await settle(pi2);
|
|
714
|
-
assert.equal(
|
|
687
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-shutdownhold'), 1, 'the redelivered entry commits once the fresh watcher settles cleanly');
|
|
715
688
|
});
|
|
716
689
|
// Situational custom sends do not enter pi's user steer/followUp queue, so
|
|
717
690
|
// hasPendingMessages() never reflects them: a situational handoff commits at
|
|
718
691
|
// the next drained settle (a situational send that async-rejected is the
|
|
719
692
|
// accepted silent-loss window, not held).
|
|
720
693
|
test('situational custom send: committed at the next drained agent_settled', async () => {
|
|
721
|
-
freshNode('node-
|
|
722
|
-
appendSituationalContext('node-
|
|
694
|
+
freshNode('node-receipt-situational');
|
|
695
|
+
appendSituationalContext('node-receipt-situational', 'ambient checkout state');
|
|
723
696
|
const pi1 = makeFakePi();
|
|
724
697
|
disposers.push(registerWatcher(pi1));
|
|
725
698
|
// Idle so the situational-only entry delivers as a hidden custom message.
|
|
726
699
|
await wait(TICK_MS + 100);
|
|
727
|
-
const entry =
|
|
700
|
+
const entry = publishMail('node-receipt-situational', {
|
|
728
701
|
from: null, tier: 'normal', kind: 'message', label: '(ambient context updated)',
|
|
729
702
|
data: { situational: true, situationalOnly: true },
|
|
730
703
|
});
|
|
731
704
|
await wait(SETTLE_MS);
|
|
732
705
|
assert.equal(pi1.sentMessages.length, 1, 'the hidden custom message was sent');
|
|
733
|
-
assert.equal(
|
|
706
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-situational'), 0, 'not committed before the settle');
|
|
734
707
|
await settle(pi1);
|
|
735
|
-
assert.equal(
|
|
708
|
+
assert.equal(acknowledgedNodeMailCount('node-receipt-situational'), 1, 'the situational handoff commits at the drained settle');
|
|
736
709
|
});
|
|
737
710
|
});
|