@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
|
@@ -1,1497 +1,110 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { buildGuidance } from '../../memory/inline-ref-guidance.js';
|
|
11
|
-
import { clearSessionCache } from '../../substrate/session-cache.js';
|
|
12
|
-
import { captureSessionChatCapabilities } from '../command-surface.js';
|
|
13
|
-
import { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
|
|
14
|
-
export { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
|
|
15
|
-
import { configuredLadderProviders, isProviderPinnedModelToken, nextLadderModel, normalizeModel, } from '../launch.js';
|
|
16
|
-
import { clearFault, readFault } from '../fault.js';
|
|
17
|
-
import { isManagedProvider, readSubscriptionPool, } from '../../subscription-state.js';
|
|
18
|
-
import { SessionListCache } from '../session-list-cache.js';
|
|
19
|
-
import { sessionListCachePath } from '../../canvas/paths.js';
|
|
20
|
-
import { isReviewBoundary } from '../session-visibility.js';
|
|
21
|
-
import { isUnknownModel, parseModelSpec } from '../model-selection.js';
|
|
22
|
-
import { normalizeSnapshotTail } from '../broker-protocol.js';
|
|
23
|
-
import { buildCommandList, buildGetTreeData, buildListMemoryRefsData, buildListModelsData, buildScopedModelsData, buildSessionsData, buildSettingsData, modelKey, } from './read-ops.js';
|
|
24
|
-
export const REVIEW_BOUNDARY_NAVIGATION_CODE = 'review_boundary_navigation';
|
|
25
|
-
export class ReviewBoundaryNavigationError extends Error {
|
|
26
|
-
targetId;
|
|
27
|
-
code = REVIEW_BOUNDARY_NAVIGATION_CODE;
|
|
28
|
-
constructor(targetId) {
|
|
29
|
-
super(`review companion cannot navigate before the visible transcript boundary (target: ${targetId})`);
|
|
30
|
-
this.targetId = targetId;
|
|
31
|
-
this.name = 'ReviewBoundaryNavigationError';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Route a writable client's `prompt`/`follow_up` frame against the LIVE session
|
|
36
|
-
* state. The client picks its frame type off a possibly-STALE `isStreaming`
|
|
37
|
-
* snapshot, so the broker is authoritative and the client's choice is a HINT:
|
|
38
|
-
*
|
|
39
|
-
* - m-B (streaming-safe prompt): a `prompt` arriving mid-stream needs
|
|
40
|
-
* PromptOptions.streamingBehavior ("Required if streaming"; prompt() throws
|
|
41
|
-
* without it). "steer" mirrors pi interactive-mode's own
|
|
42
|
-
* Enter-while-streaming submit (`prompt(text, { streamingBehavior: "steer" })`).
|
|
43
|
-
* - m-C (idle-safe follow_up): pi's followUp() only enqueues into a RUNNING
|
|
44
|
-
* agent loop — on an idle session nothing ever drains the queue and the text
|
|
45
|
-
* is silently lost. An idle `follow_up` therefore runs as a prompt now.
|
|
46
|
-
*
|
|
47
|
-
* driveEngine calls this synchronously with `session.isStreaming`, so the
|
|
48
|
-
* routed state cannot go stale between the check and the engine call.
|
|
49
|
-
* C1: images ride as a field on PromptOptions (pi: `prompt(text, { images })`).
|
|
50
|
-
*/
|
|
51
|
-
export function resolveEngineRoute(frame, isStreaming) {
|
|
52
|
-
if (frame.type === 'follow_up' && isStreaming)
|
|
53
|
-
return { call: 'followUp' };
|
|
54
|
-
const options = {};
|
|
55
|
-
if (frame.images !== undefined)
|
|
56
|
-
options.images = frame.images;
|
|
57
|
-
if (isStreaming)
|
|
58
|
-
options.streamingBehavior = 'steer';
|
|
59
|
-
return { call: 'prompt', options };
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* True iff `text` would be consumed by pi's OWN leading-name preflight before
|
|
63
|
-
* `session.prompt/steer/followUp` ever sees it as prose — an extension command
|
|
64
|
-
* (`session.extensionRunner.getCommand`), a `/skill:name` skill, or a `/name`
|
|
65
|
-
* prompt template (mirrors `_tryExecuteExtensionCommand` / `_expandSkillCommand`
|
|
66
|
-
* / `expandPromptTemplate` in the pinned SDK verbatim, including the exact
|
|
67
|
-
* name-parsing substrings). Inline memory-reference guidance must NEVER fire
|
|
68
|
-
* for these — the leading token routes to the engine's own command surface,
|
|
69
|
-
* not prose referencing a memory doc. Each lookup is independently wrapped so
|
|
70
|
-
* an engine missing one accessor (the fake, or a degraded session) degrades
|
|
71
|
-
* that check to "not a match" rather than aborting the whole classification —
|
|
72
|
-
* matching `buildCommandList`'s existing "no live extensionRunner" tolerance.
|
|
73
|
-
*/
|
|
74
|
-
export function isLeadingEngineCommand(text, session) {
|
|
75
|
-
if (text.startsWith('/')) {
|
|
76
|
-
const spaceIndex = text.indexOf(' ');
|
|
77
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
78
|
-
try {
|
|
79
|
-
if (session.extensionRunner?.getCommand(commandName))
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
/* engine without a live extensionRunner (e.g. the fake) — not a match */
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (text.startsWith('/skill:')) {
|
|
87
|
-
const spaceIndex = text.indexOf(' ');
|
|
88
|
-
const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
|
|
89
|
-
try {
|
|
90
|
-
if (session.resourceLoader?.getSkills().skills.some((s) => s.name === skillName))
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
/* engine without a live resourceLoader — not a match */
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (text.startsWith('/')) {
|
|
98
|
-
const match = text.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/);
|
|
99
|
-
const templateName = match?.[1];
|
|
100
|
-
if (templateName !== undefined) {
|
|
101
|
-
try {
|
|
102
|
-
if (session.promptTemplates?.some((t) => t.name === templateName))
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
/* engine without a live promptTemplates getter — not a match */
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
/** A `steer` frame normally maps directly to pi's steer queue. Engine commands,
|
|
113
|
-
* skills, and templates must instead pass through prompt()'s leading-command
|
|
114
|
-
* preflight; with streamingBehavior:'steer', ordinary expanded text still lands
|
|
115
|
-
* in the same queue. */
|
|
116
|
-
export function resolveSteerCall(text, session) {
|
|
117
|
-
return isLeadingEngineCommand(text, session) ? 'prompt' : 'steer';
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Enumerate every leading-token STRING this session's engine would
|
|
121
|
-
* independently consume as a leading command/skill/template dispatch BEFORE
|
|
122
|
-
* inline memory-reference guidance ever applies — the read-op sibling of
|
|
123
|
-
* `isLeadingEngineCommand`'s point-lookup, gathered as a full enumeration for
|
|
124
|
-
* viewer-side suppression instead of a per-submission membership check. Same
|
|
125
|
-
* three sources, same shape of the leading match `isLeadingEngineCommand`
|
|
126
|
-
* checks (bare extension-command invocation names and prompt-template names,
|
|
127
|
-
* `skill:<name>` for skills), so `/${token}` round-trips through
|
|
128
|
-
* `isLeadingEngineCommand(text, session)` for every emitted token (locked by
|
|
129
|
-
* a test). Kept STRICTLY separate from `buildCommandList` (command
|
|
130
|
-
* completion) and the ref inventory (`RefMeta` rows) — this is neither.
|
|
131
|
-
*/
|
|
132
|
-
export function engineLeadingCommandTokens(session) {
|
|
133
|
-
const out = [];
|
|
134
|
-
try {
|
|
135
|
-
for (const c of session.extensionRunner?.getRegisteredCommands() ?? [])
|
|
136
|
-
out.push(c.invocationName);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
/* engine without a live extensionRunner (e.g. the fake) — none */
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
for (const s of session.resourceLoader?.getSkills().skills ?? [])
|
|
143
|
-
out.push(`skill:${s.name}`);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
/* engine without a live resourceLoader — none */
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
for (const t of session.promptTemplates ?? [])
|
|
150
|
-
out.push(t.name);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
/* engine without a live promptTemplates getter — none */
|
|
154
|
-
}
|
|
155
|
-
return out;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Choose how the hidden `crtr-memory-refs` guidance custom message rides
|
|
159
|
-
* alongside a human frame's own engine call (design Flow B). Pure over the
|
|
160
|
-
* frame's routing outcome — never reads live session state itself, so a
|
|
161
|
-
* caller can decide the delivery mode from the exact same `route`
|
|
162
|
-
* `resolveEngineRoute` already computed for the human call.
|
|
163
|
-
*
|
|
164
|
-
* - `steer` frame -> 'steer' (always; no route call exists).
|
|
165
|
-
* - a TRUE mid-stream follow_up (`routeCall === 'followUp'`) -> 'followUp'.
|
|
166
|
-
* - every other `prompt`/`follow_up` routed as `routeCall === 'prompt'`:
|
|
167
|
-
* idle -> 'nextTurn' (the ONLY case guidance precedes the human call —
|
|
168
|
-
* pi's `prompt()` reads+clears its pending-nextTurn queue synchronously
|
|
169
|
-
* into the SAME turn as the new user message); streaming (m-B
|
|
170
|
-
* steer-while-streaming) -> 'steer', never 'nextTurn' (that would attach
|
|
171
|
-
* to a later, unrelated turn).
|
|
172
|
-
*/
|
|
173
|
-
export function chooseGuidanceDeliveryMode(frameType, routeCall, isStreaming) {
|
|
174
|
-
if (frameType === 'steer')
|
|
175
|
-
return 'steer';
|
|
176
|
-
if (routeCall === 'followUp')
|
|
177
|
-
return 'followUp';
|
|
178
|
-
return isStreaming ? 'steer' : 'nextTurn';
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Deliver a `nextTurn` guidance custom message ATOMICALLY around the idle-
|
|
182
|
-
* prompt call it's meant to ride alongside. Pi's `_pendingNextTurnMessages`
|
|
183
|
-
* queue (the only public seam for `deliverAs:'nextTurn'`) is drained ONLY deep
|
|
184
|
-
* inside `session.prompt()`, well after several await points — extension-
|
|
185
|
-
* command interception, the `input` extension transform, a streaming reroute,
|
|
186
|
-
* model/auth validation, pre-turn compaction — that can return or throw BEFORE
|
|
187
|
-
* the drain ever runs. `sendCustomMessage`'s nextTurn branch pushes
|
|
188
|
-
* synchronously and unconditionally, so any of those early exits strands the
|
|
189
|
-
* pushed guidance in the queue to attach itself to whichever LATER, unrelated
|
|
190
|
-
* prompt happens to reach the drain first.
|
|
191
|
-
*
|
|
192
|
-
* There is no public SDK seam to observe or cancel a pending nextTurn message
|
|
193
|
-
* (`_pendingNextTurnMessages` is TS-`private` only — a plain runtime field, not
|
|
194
|
-
* `#private`), so this reaches into that one field and removes EXACTLY the
|
|
195
|
-
* entry this call pushed, identified by object identity via a locally-
|
|
196
|
-
* generated marker riding the message's own `details` field (never surfaced to
|
|
197
|
-
* the model, and irrelevant if the message WAS consumed normally — `details`
|
|
198
|
-
* is just carried through like any other field once folded into a real turn).
|
|
199
|
-
* Runs UNCONDITIONALLY after the human call settles, success or failure/early-
|
|
200
|
-
* return alike — a "successful" `prompt()` promise can just as easily be an
|
|
201
|
-
* early return that skipped the drain (an intercepted extension command, an
|
|
202
|
-
* `input` transform, a streaming reroute) as a genuine consumed turn, so only
|
|
203
|
-
* checking `.catch` is not enough.
|
|
204
|
-
*/
|
|
205
|
-
export async function promptWithNextTurnGuidance(session, guidance, runPrompt) {
|
|
206
|
-
// Must survive Pi's structured-clone step when the queued custom message is
|
|
207
|
-
// folded into the turn. A Symbol reaches that step in `details` and aborts
|
|
208
|
-
// the real prompt with "could not be cloned" before the provider runs.
|
|
209
|
-
const marker = `crtr-memory-refs-pending:${randomUUID()}`;
|
|
210
|
-
let queued = false;
|
|
211
|
-
try {
|
|
212
|
-
await session.sendCustomMessage({ customType: 'crtr-memory-refs', content: guidance, display: false, details: marker }, { deliverAs: 'nextTurn' });
|
|
213
|
-
queued = true;
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
emitEvent({
|
|
217
|
-
level: 'warn',
|
|
218
|
-
event: 'broker.memory_refs.guidance_send_failed',
|
|
219
|
-
error: err,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
await runPrompt();
|
|
224
|
-
}
|
|
225
|
-
finally {
|
|
226
|
-
if (queued)
|
|
227
|
-
clearOwnPendingNextTurnGuidance(session, marker);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
/** Splice out exactly the pending-nextTurn entry `marker` identifies (object-
|
|
231
|
-
* identity match on `details`), if it is still present — a no-op (and safe)
|
|
232
|
-
* if `session.prompt()` already drained it normally, or if the session/field
|
|
233
|
-
* shape doesn't match (defensive against an engine without the field, e.g. a
|
|
234
|
-
* future SDK bump or a structurally-different test double). */
|
|
235
|
-
function clearOwnPendingNextTurnGuidance(session, marker) {
|
|
236
|
-
const pending = session._pendingNextTurnMessages;
|
|
237
|
-
if (!Array.isArray(pending))
|
|
238
|
-
return;
|
|
239
|
-
const idx = pending.findIndex((m) => m?.details === marker);
|
|
240
|
-
if (idx !== -1)
|
|
241
|
-
pending.splice(idx, 1);
|
|
242
|
-
}
|
|
243
|
-
/** True iff a parsed JSONL branch-entry line is a hidden (`display:false`)
|
|
244
|
-
* custom message — the general display boundary crossed if left in an
|
|
245
|
-
* exported branch (not just `crtr-memory-refs` guidance; any hidden custom
|
|
246
|
-
* content). Covers both persisted shapes a branch entry can take: a
|
|
247
|
-
* `type:'custom_message'` entry (display at the entry's own top level) and a
|
|
248
|
-
* `type:'message'` entry wrapping a `role:'custom'` message (display nested
|
|
249
|
-
* under `.message`). */
|
|
250
|
-
export function isHiddenJsonlBranchEntry(entry) {
|
|
251
|
-
if (entry === null || typeof entry !== 'object')
|
|
252
|
-
return false;
|
|
253
|
-
const e = entry;
|
|
254
|
-
if (e.type === 'custom_message' && e.display === false)
|
|
255
|
-
return true;
|
|
256
|
-
if (e.type === 'message' && e.message?.role === 'custom' && e.message.display === false)
|
|
257
|
-
return true;
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
/** Rewrite an already-exported JSONL file in place, dropping every hidden
|
|
261
|
-
* (`display:false`) branch entry while re-chaining `parentId` so the
|
|
262
|
-
* remaining entries stay a single valid linear branch — applying the general
|
|
263
|
-
* display:false boundary to `session.exportToJsonl`'s own output, which
|
|
264
|
-
* serializes every entry verbatim regardless of display. The header line
|
|
265
|
-
* (line 1) is copied through untouched. */
|
|
266
|
-
export function stripHiddenEntriesFromJsonlExport(filePath) {
|
|
267
|
-
const raw = readFileSync(filePath, 'utf8');
|
|
268
|
-
const lines = raw.split('\n').filter((l) => l !== '');
|
|
269
|
-
if (lines.length === 0)
|
|
270
|
-
return;
|
|
271
|
-
const [headerLine, ...entryLines] = lines;
|
|
272
|
-
let prevId = null;
|
|
273
|
-
const kept = [];
|
|
274
|
-
for (const line of entryLines) {
|
|
275
|
-
const entry = JSON.parse(line);
|
|
276
|
-
if (isHiddenJsonlBranchEntry(entry))
|
|
277
|
-
continue;
|
|
278
|
-
kept.push(JSON.stringify({ ...entry, parentId: prevId }));
|
|
279
|
-
if (typeof entry.id === 'string')
|
|
280
|
-
prevId = entry.id;
|
|
281
|
-
}
|
|
282
|
-
writeFileSync(filePath, `${[headerLine, ...kept].join('\n')}\n`);
|
|
283
|
-
}
|
|
1
|
+
import { createFrameReplies } from './frame-replies.js';
|
|
2
|
+
import { createEngineDriver } from './engine-drive.js';
|
|
3
|
+
import { createClientHandlers } from './frame-client.js';
|
|
4
|
+
import { createMemoryRefGuidance } from './frame-memory-refs.js';
|
|
5
|
+
import { createAuthReloadGate } from './auth-reload.js';
|
|
6
|
+
import { createEngineControlHandler } from './frame-engine-control.js';
|
|
7
|
+
import { createReadHandler } from './frame-read.js';
|
|
8
|
+
import { createSessionHandler } from './frame-session.js';
|
|
9
|
+
import { createKickoffDriver, createPromptDispatchTracker, createTurnAcceptanceGate, } from './turn-ignition.js';
|
|
284
10
|
/** Owns frame-local input state while dispatching through explicit broker collaborators. */
|
|
285
11
|
export function createFrameDispatchContext(deps) {
|
|
286
12
|
const { nodeId, cfg, registry, toolGroups, projection, rebind, boundaryReviewId, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
|
|
287
13
|
const currentSession = () => rebind.session();
|
|
288
14
|
const currentServices = () => rebind.services();
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
waiter();
|
|
299
|
-
};
|
|
300
|
-
const sessionHasNoModel = () => currentSession().model === undefined ||
|
|
301
|
-
isUnknownModel(currentSession().model);
|
|
302
|
-
const drivePendingKickoff = () => {
|
|
303
|
-
if (kickoffPending === undefined || kickoffInFlight || sessionHasNoModel())
|
|
304
|
-
return;
|
|
305
|
-
const prompt = kickoffPending;
|
|
306
|
-
kickoffPending = undefined;
|
|
307
|
-
kickoffInFlight = true;
|
|
308
|
-
trackPromptDispatch(operationIdContext.fresh(() => currentSession()
|
|
309
|
-
.prompt(prompt)
|
|
310
|
-
.catch((err) => {
|
|
311
|
-
emitEvent({
|
|
312
|
-
level: 'error',
|
|
313
|
-
event: 'broker.prompt.first_failed',
|
|
314
|
-
error: err,
|
|
315
|
-
});
|
|
316
|
-
})
|
|
317
|
-
.finally(() => {
|
|
318
|
-
kickoffInFlight = false;
|
|
319
|
-
})));
|
|
320
|
-
};
|
|
321
|
-
// Drive the engine on behalf of a writable client.
|
|
322
|
-
// -------------------------------------------------------------------------
|
|
323
|
-
// ---------------------------------------------------------------------------
|
|
324
|
-
// Inline memory-reference inventory (design: inline memory references) — a
|
|
325
|
-
// single broker-level memo shared by BOTH the `list_memory_refs` read-op
|
|
326
|
-
// reply and per-submission guidance resolution below, so a burst of reads +
|
|
327
|
-
// submissions between reloads never re-walks the corpus per call. Cleared
|
|
328
|
-
// (work item 3) only on a SUCCESSFUL `currentSession().reload()` — a failed reload
|
|
329
|
-
// leaves the prior memo intact, since nothing about the corpus is known to
|
|
330
|
-
// have changed.
|
|
331
|
-
// ---------------------------------------------------------------------------
|
|
332
|
-
let memoizedRefInventory = null;
|
|
333
|
-
const getRefInventory = () => {
|
|
334
|
-
if (memoizedRefInventory === null)
|
|
335
|
-
memoizedRefInventory = buildRefInventory();
|
|
336
|
-
return memoizedRefInventory;
|
|
337
|
-
};
|
|
338
|
-
/** Resolve the hidden guidance text for a human frame's verbatim text, or
|
|
339
|
-
* `null` when no guidance should be sent (a leading engine command/skill/
|
|
340
|
-
* template, no resolvable refs, or a degraded inventory/guidance build —
|
|
341
|
-
* work item 6: any failure here degrades to "no guidance", logged once as a
|
|
342
|
-
* structured warning, and NEVER blocks/alters the human delivery below). */
|
|
343
|
-
const resolveFrameGuidance = (text) => {
|
|
344
|
-
try {
|
|
345
|
-
if (isLeadingEngineCommand(text, currentSession()))
|
|
346
|
-
return null;
|
|
347
|
-
return buildGuidance(text, getRefInventory().names);
|
|
348
|
-
}
|
|
349
|
-
catch (err) {
|
|
350
|
-
emitEvent({
|
|
351
|
-
level: 'warn',
|
|
352
|
-
event: 'broker.memory_refs.guidance_resolve_failed',
|
|
353
|
-
error: err,
|
|
354
|
-
});
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
// Prompt dispatches whose promise has not yet settled. `currentSession().prompt()`
|
|
359
|
-
// does async setup (input transforms, model/auth validation, compaction)
|
|
360
|
-
// BEFORE `_isAgentRunActive` flips, so for a short window after a routed
|
|
361
|
-
// deliver the engine reads idle and `currentSession().abort()` is a silent no-op — an
|
|
362
|
-
// id-bearing abort racing that window would miss the turn entirely (verified
|
|
363
|
-
// live). The counter lets it distinguish "genuinely idle" from "turn still
|
|
364
|
-
// igniting" and wait for the run to become abortable.
|
|
365
|
-
let pendingPromptDispatches = 0;
|
|
366
|
-
const trackPromptDispatch = (p) => {
|
|
367
|
-
pendingPromptDispatches++;
|
|
368
|
-
const tracked = Promise.resolve(p);
|
|
369
|
-
void tracked.finally(() => {
|
|
370
|
-
pendingPromptDispatches--;
|
|
371
|
-
});
|
|
372
|
-
return tracked;
|
|
373
|
-
};
|
|
374
|
-
/**
|
|
375
|
-
* Drive the engine for one client frame. For a prompt/follow_up frame this
|
|
376
|
-
* returns a promise that settles with the engine call's OUTCOME — `undefined`
|
|
377
|
-
* when the call succeeded, the rejection value when it did not — so a caller
|
|
378
|
-
* that must report success (the `deliver` ack) can read it instead of assuming
|
|
379
|
-
* the dispatch worked. Every other frame type keeps the fire-and-forget shape
|
|
380
|
-
* and returns `undefined`.
|
|
381
|
-
*/
|
|
382
|
-
const driveEngine = (client, frame) => {
|
|
383
|
-
// The engine call's failure, captured for the returned outcome. relayError
|
|
384
|
-
// still relays the error frame; recording it is what lets the caller ack
|
|
385
|
-
// from the outcome rather than from the fact that a call was made at all.
|
|
386
|
-
let failure;
|
|
387
|
-
const relayError = (err) => {
|
|
388
|
-
failure = err;
|
|
389
|
-
registry.sendFrame(client, {
|
|
390
|
-
type: 'error',
|
|
391
|
-
code: 'engine_error',
|
|
392
|
-
message: String(err),
|
|
393
|
-
});
|
|
394
|
-
};
|
|
395
|
-
// `prompt { interrupt: true }` is one atomic viewer intention: end whatever
|
|
396
|
-
// is producing now, then start this as a new turn. A normal mid-stream
|
|
397
|
-
// prompt deliberately steers into pi's queue; the whip must not.
|
|
398
|
-
if (frame.type === 'prompt' && frame.interrupt === true) {
|
|
399
|
-
return (async () => {
|
|
400
|
-
if (currentSession().isBashRunning) {
|
|
401
|
-
currentSession().abortBash();
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
// A prompt can be in pi's async setup before isStreaming flips. Match
|
|
405
|
-
// the correlated interrupt's guard so a whip cannot silently miss it.
|
|
406
|
-
if (!currentSession().isStreaming && pendingPromptDispatches > 0) {
|
|
407
|
-
const deadline = Date.now() + 5_000;
|
|
408
|
-
while (Date.now() < deadline &&
|
|
409
|
-
!currentSession().isStreaming &&
|
|
410
|
-
pendingPromptDispatches > 0) {
|
|
411
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
if (currentSession().isStreaming)
|
|
415
|
-
await currentSession().abort();
|
|
416
|
-
}
|
|
417
|
-
return driveEngine(client, {
|
|
418
|
-
type: 'prompt',
|
|
419
|
-
text: frame.text,
|
|
420
|
-
...(frame.images !== undefined ? { images: frame.images } : {}),
|
|
421
|
-
});
|
|
422
|
-
})().catch((err) => {
|
|
423
|
-
relayError(err);
|
|
424
|
-
return failure;
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
// Hidden `crtr-memory-refs` guidance rides ALONGSIDE the human frame's own
|
|
428
|
-
// engine call, never rewriting `frame.text` — see resolveFrameGuidance /
|
|
429
|
-
// chooseGuidanceDeliveryMode above. A failed SEND (as opposed to a failed
|
|
430
|
-
// resolve, handled above) is likewise logged-and-swallowed: it must never
|
|
431
|
-
// reach `relayError`, which is reserved for the human call's own outcome.
|
|
432
|
-
const sendGuidance = (guidance, deliverAs) => currentSession()
|
|
433
|
-
.sendCustomMessage({ customType: 'crtr-memory-refs', content: guidance, display: false }, { deliverAs })
|
|
434
|
-
.catch((err) => {
|
|
435
|
-
emitEvent({
|
|
436
|
-
level: 'warn',
|
|
437
|
-
event: 'broker.memory_refs.guidance_send_failed',
|
|
438
|
-
error: err,
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
switch (frame.type) {
|
|
442
|
-
case 'prompt':
|
|
443
|
-
case 'follow_up': {
|
|
444
|
-
// Settle on the engine call's outcome: `failure` is set by relayError
|
|
445
|
-
// before the caught promise resolves, so reading it after the chain is
|
|
446
|
-
// the call's verdict.
|
|
447
|
-
const settle = (p) => p.then(() => failure);
|
|
448
|
-
const dispatch = () => {
|
|
449
|
-
const route = resolveEngineRoute(frame, currentSession().isStreaming);
|
|
450
|
-
const guidance = resolveFrameGuidance(frame.text);
|
|
451
|
-
if (guidance === null) {
|
|
452
|
-
if (route.call === 'prompt') {
|
|
453
|
-
// A routed prompt STARTS a turn — including an idle follow_up
|
|
454
|
-
// escalated by m-C — so it gets a fresh operation id. A true
|
|
455
|
-
// (mid-stream) followUp rides the current turn's operation, like
|
|
456
|
-
// steer.
|
|
457
|
-
return settle(trackPromptDispatch(operationIdContext.fresh(() => promptWithAdvertisedCommandInvocation(currentSession(), frame.text, () => currentSession().prompt(frame.text, route.options)).catch(relayError))));
|
|
458
|
-
}
|
|
459
|
-
// C1: pi's followUp() takes images as a POSITIONAL 2nd arg (`followUp(text, images?)`).
|
|
460
|
-
return settle(currentSession()
|
|
461
|
-
.followUp(frame.text, frame.images)
|
|
462
|
-
.catch(relayError));
|
|
463
|
-
}
|
|
464
|
-
const deliverAs = chooseGuidanceDeliveryMode(frame.type, route.call, currentSession().isStreaming);
|
|
465
|
-
if (route.call === 'prompt') {
|
|
466
|
-
if (deliverAs === 'nextTurn') {
|
|
467
|
-
// The ONLY ordering where guidance precedes the human call — pi's
|
|
468
|
-
// prompt() reads+clears its pending-nextTurn queue synchronously
|
|
469
|
-
// at call time, injecting it into the SAME turn as the new user
|
|
470
|
-
// message. promptWithNextTurnGuidance queues the guidance and then
|
|
471
|
-
// ATOMICALLY clears its own pending entry if prompt() settles
|
|
472
|
-
// without ever reaching that drain (an early throw/return leaves
|
|
473
|
-
// nothing stranded to leak into a later, unrelated turn).
|
|
474
|
-
return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => promptWithAdvertisedCommandInvocation(currentSession(), frame.text, () => currentSession().prompt(frame.text, route.options)))).catch(relayError)));
|
|
475
|
-
}
|
|
476
|
-
return settle(trackPromptDispatch(operationIdContext.fresh(() => promptWithAdvertisedCommandInvocation(currentSession(), frame.text, () => currentSession().prompt(frame.text, route.options))
|
|
477
|
-
.catch(relayError)
|
|
478
|
-
.then(() => sendGuidance(guidance, deliverAs)))));
|
|
479
|
-
}
|
|
480
|
-
// deliverAs is always 'followUp' here per chooseGuidanceDeliveryMode's
|
|
481
|
-
// contract (a followUp routeCall never yields 'nextTurn').
|
|
482
|
-
return settle(currentSession()
|
|
483
|
-
.followUp(frame.text, frame.images)
|
|
484
|
-
.catch(relayError)
|
|
485
|
-
.then(() => sendGuidance(guidance, deliverAs)));
|
|
486
|
-
};
|
|
487
|
-
// /login acknowledges before its asynchronous local refresh completes.
|
|
488
|
-
// An idle turn must not overtake that refresh and launch on the stale
|
|
489
|
-
// route; true mid-stream follow-ups keep their immediate queue behavior.
|
|
490
|
-
const authReload = currentSession().isStreaming
|
|
491
|
-
? undefined
|
|
492
|
-
: reloadAuthInFlight?.promise;
|
|
493
|
-
if (authReload === undefined)
|
|
494
|
-
return dispatch();
|
|
495
|
-
const waitingGeneration = rebind.installedGeneration();
|
|
496
|
-
const waitingSession = currentSession();
|
|
497
|
-
return authReload.then(() => {
|
|
498
|
-
if (rebind.installedGeneration() !== waitingGeneration ||
|
|
499
|
-
currentSession() !== waitingSession) {
|
|
500
|
-
relayError(new Error('currentSession() changed while prompt waited for authentication reload'));
|
|
501
|
-
return failure;
|
|
502
|
-
}
|
|
503
|
-
return dispatch();
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
case 'steer': {
|
|
507
|
-
// A viewer submits ordinary text as `steer` while the agent is running,
|
|
508
|
-
// including a slash command injected by the tmux prefix menu. pi's
|
|
509
|
-
// steer() deliberately rejects registered extension commands because
|
|
510
|
-
// they cannot be queued; prompt() performs the command preflight first
|
|
511
|
-
// and executes it immediately even while streaming. It also expands a
|
|
512
|
-
// matching skill/template before applying the same steer behavior.
|
|
513
|
-
if (resolveSteerCall(frame.text, currentSession()) === 'prompt') {
|
|
514
|
-
void promptWithAdvertisedCommandInvocation(currentSession(), frame.text, () => currentSession().prompt(frame.text, {
|
|
515
|
-
images: frame.images,
|
|
516
|
-
streamingBehavior: 'steer',
|
|
517
|
-
})).catch(relayError);
|
|
518
|
-
break;
|
|
519
|
-
}
|
|
520
|
-
const guidance = resolveFrameGuidance(frame.text);
|
|
521
|
-
// C1: pi's steer() takes images as a POSITIONAL 2nd arg (`steer(text, images?)`).
|
|
522
|
-
if (guidance === null) {
|
|
523
|
-
void currentSession()
|
|
524
|
-
.steer(frame.text, frame.images)
|
|
525
|
-
.catch(relayError);
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
528
|
-
void currentSession()
|
|
529
|
-
.steer(frame.text, frame.images)
|
|
530
|
-
.catch(relayError)
|
|
531
|
-
.then(() => sendGuidance(guidance, 'steer'));
|
|
532
|
-
break;
|
|
533
|
-
}
|
|
534
|
-
case 'abort': {
|
|
535
|
-
// esc/abort cancels whatever is live: a running `!` command (abortBash)
|
|
536
|
-
// takes precedence over the agent turn, mirroring pi interactive mode.
|
|
537
|
-
if (currentSession().isBashRunning) {
|
|
538
|
-
currentSession().abortBash();
|
|
539
|
-
if (frame.id !== undefined)
|
|
540
|
-
ackTo(client, 'abort', true, 'bash', frame.id);
|
|
541
|
-
break;
|
|
542
|
-
}
|
|
543
|
-
if (frame.id === undefined) {
|
|
544
|
-
// A viewer's Esc — unchanged instant semantics, no ack.
|
|
545
|
-
void currentSession().abort().catch(relayError);
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
548
|
-
// An id-bearing abort (a one-shot daemon interrupt) must close the
|
|
549
|
-
// pre-turn window: a routed deliver whose prompt() is still in async
|
|
550
|
-
// setup reads as idle and abort() would silently miss it. If a prompt
|
|
551
|
-
// dispatch is pending, wait (bounded) for the run to become abortable
|
|
552
|
-
// — or for every dispatch to settle without one (an early return) —
|
|
553
|
-
// then abort and ack with what was actually interrupted.
|
|
554
|
-
void (async () => {
|
|
555
|
-
try {
|
|
556
|
-
if (!currentSession().isStreaming && pendingPromptDispatches > 0) {
|
|
557
|
-
const deadline = Date.now() + 5_000;
|
|
558
|
-
while (Date.now() < deadline &&
|
|
559
|
-
!currentSession().isStreaming &&
|
|
560
|
-
pendingPromptDispatches > 0) {
|
|
561
|
-
await new Promise((r) => setTimeout(r, 25));
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
const detail = currentSession().isStreaming ? 'turn' : 'idle';
|
|
565
|
-
await currentSession().abort();
|
|
566
|
-
ackTo(client, 'abort', true, detail, frame.id);
|
|
567
|
-
}
|
|
568
|
-
catch (err) {
|
|
569
|
-
engineErrorTo(client, frame.id)(err);
|
|
570
|
-
}
|
|
571
|
-
})();
|
|
572
|
-
break;
|
|
573
|
-
}
|
|
574
|
-
case 'bash': {
|
|
575
|
-
// `!`/`!!` bash: run via currentSession().executeBash (records a bashExecution
|
|
576
|
-
// context message, starts NO agent turn) and relay its lifecycle to ALL
|
|
577
|
-
// viewers as bash_start/bash_output/bash_end so each renders the live
|
|
578
|
-
// component. Serialized like pi's interactive mode — reject a second `!`
|
|
579
|
-
// while one is already running rather than racing _bashAbortController.
|
|
580
|
-
const command = frame.command;
|
|
581
|
-
if (command.trim() === '')
|
|
582
|
-
break;
|
|
583
|
-
if (currentSession().isBashRunning) {
|
|
584
|
-
registry.sendFrame(client, {
|
|
585
|
-
type: 'error',
|
|
586
|
-
code: 'bash_busy',
|
|
587
|
-
message: 'A bash command is already running',
|
|
588
|
-
});
|
|
589
|
-
break;
|
|
590
|
-
}
|
|
591
|
-
const exclude = frame.excludeFromContext === true;
|
|
592
|
-
// Order before any held assistant message_update, so a `!` run mid-stream
|
|
593
|
-
// never renders ahead of the update it follows.
|
|
594
|
-
projection.flush();
|
|
595
|
-
// The live bash component is a visible transcript boundary, even though
|
|
596
|
-
// its persisted bashExecution message arrives later via the currentSession().
|
|
597
|
-
toolGroups.finishOpenToolGroup();
|
|
598
|
-
registry.broadcast({
|
|
599
|
-
type: 'bash_start',
|
|
600
|
-
command,
|
|
601
|
-
excludeFromContext: exclude,
|
|
602
|
-
});
|
|
603
|
-
void currentSession()
|
|
604
|
-
.executeBash(command, (chunk) => registry.broadcast({ type: 'bash_output', chunk }), {
|
|
605
|
-
excludeFromContext: exclude,
|
|
606
|
-
})
|
|
607
|
-
.then((result) => {
|
|
608
|
-
registry.broadcast({
|
|
609
|
-
type: 'bash_end',
|
|
610
|
-
exitCode: result.exitCode,
|
|
611
|
-
cancelled: result.cancelled,
|
|
612
|
-
truncated: result.truncated,
|
|
613
|
-
fullOutputPath: result.fullOutputPath,
|
|
614
|
-
});
|
|
615
|
-
})
|
|
616
|
-
.catch(relayError);
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
// -------------------------------------------------------------------------
|
|
622
|
-
// Command-op helpers. The write guard is hoisted here and reused by all
|
|
623
|
-
// mutating ops; the ack/error replies
|
|
624
|
-
// and a few resolvers keep the per-op cases one-liners.
|
|
625
|
-
// -------------------------------------------------------------------------
|
|
626
|
-
/** Reject a read-only client for a mutating op. The decision reads this
|
|
627
|
-
* client's OWN fixed role — there is no shared writer slot, so any number of
|
|
628
|
-
* writable clients pass concurrently. Returns true when rejected (the caller
|
|
629
|
-
* should `break`). */
|
|
630
|
-
const notWritable = (client, what, id) => {
|
|
631
|
-
if (client.role === 'controller')
|
|
632
|
-
return false;
|
|
633
|
-
registry.sendFrame(client, {
|
|
634
|
-
type: 'error',
|
|
635
|
-
code: 'read_only',
|
|
636
|
-
message: `a read-only client may not ${what}`,
|
|
637
|
-
// M1: echo a correlated request's id so its pending-by-id promise rejects
|
|
638
|
-
// rather than hanging (e.g. a read-only `dequeue`).
|
|
639
|
-
...(id !== undefined ? { id } : {}),
|
|
640
|
-
});
|
|
641
|
-
return true;
|
|
642
|
-
};
|
|
643
|
-
const ackTo = (client, op, ok = true, detail, id) => registry.sendFrame(client, {
|
|
644
|
-
type: 'ack',
|
|
645
|
-
for: op,
|
|
646
|
-
ok,
|
|
647
|
-
...(detail !== undefined ? { detail } : {}),
|
|
648
|
-
...(id !== undefined ? { id } : {}),
|
|
15
|
+
// The reply vocabulary every handler below answers a client with.
|
|
16
|
+
const replies = createFrameReplies(registry);
|
|
17
|
+
const { notWritable } = replies;
|
|
18
|
+
const memoryRefs = createMemoryRefGuidance(currentSession);
|
|
19
|
+
const dispatches = createPromptDispatchTracker(currentSession);
|
|
20
|
+
const turnAcceptance = createTurnAcceptanceGate();
|
|
21
|
+
const kickoffDriver = createKickoffDriver({
|
|
22
|
+
session: currentSession,
|
|
23
|
+
dispatches,
|
|
649
24
|
});
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
25
|
+
const authReloadGate = createAuthReloadGate({
|
|
26
|
+
nodeId,
|
|
27
|
+
cfg,
|
|
28
|
+
replies,
|
|
29
|
+
rebind,
|
|
30
|
+
session: currentSession,
|
|
31
|
+
services: currentServices,
|
|
32
|
+
broadcastModelChanged,
|
|
33
|
+
formatModelSpec,
|
|
34
|
+
resolveLaunchModel,
|
|
35
|
+
driveKickoff: kickoffDriver.drivePending,
|
|
36
|
+
});
|
|
37
|
+
const driveEngine = createEngineDriver({
|
|
38
|
+
registry,
|
|
39
|
+
replies,
|
|
40
|
+
session: currentSession,
|
|
41
|
+
dispatches,
|
|
42
|
+
memoryRefs,
|
|
43
|
+
authReloadGate,
|
|
44
|
+
rebind,
|
|
45
|
+
projection,
|
|
46
|
+
toolGroups,
|
|
658
47
|
});
|
|
659
|
-
const isPreBoundaryReviewEntry = (entryId) => boundaryReviewId !== undefined &&
|
|
660
|
-
!currentSession()
|
|
661
|
-
.sessionManager.getBranch(entryId)
|
|
662
|
-
.some((entry) => isReviewBoundary(entry, boundaryReviewId));
|
|
663
|
-
const reviewBoundaryRefusalTo = (client, targetId) => {
|
|
664
|
-
const refusal = new ReviewBoundaryNavigationError(targetId);
|
|
665
|
-
registry.sendFrame(client, {
|
|
666
|
-
type: 'error',
|
|
667
|
-
code: refusal.code,
|
|
668
|
-
message: refusal.message,
|
|
669
|
-
});
|
|
670
|
-
};
|
|
671
|
-
/** Resolve a `provider/id` model spec against the LIVE currentServices() registry (which
|
|
672
|
-
* carries any extension-registered providers). Pure: returns undefined on a
|
|
673
|
-
* malformed spec or an unknown model. */
|
|
674
|
-
const findModelSpec = (spec) => {
|
|
675
|
-
const { modelSpec } = parseModelSpec(spec);
|
|
676
|
-
const slash = modelSpec.indexOf('/');
|
|
677
|
-
if (slash <= 0)
|
|
678
|
-
return undefined;
|
|
679
|
-
return registryOf(currentServices()).find(modelSpec.slice(0, slash), modelSpec.slice(slash + 1));
|
|
680
|
-
};
|
|
681
|
-
/** Resolve a model from a free-text query — `/model opus`, `/model fable`. Tries
|
|
682
|
-
* the exact `provider/id` spec first; failing that, searches the registry the
|
|
683
|
-
* way the picker's search box does (case-insensitive substring on the model id
|
|
684
|
-
* and the `provider/id` key) and returns the BEST match, preferring authed/
|
|
685
|
-
* available models. Returns undefined only when nothing matches. */
|
|
686
|
-
const resolveModelQuery = (query) => {
|
|
687
|
-
const exact = findModelSpec(query);
|
|
688
|
-
if (exact)
|
|
689
|
-
return exact;
|
|
690
|
-
const q = query.trim().toLowerCase();
|
|
691
|
-
if (q === '')
|
|
692
|
-
return undefined;
|
|
693
|
-
let all;
|
|
694
|
-
try {
|
|
695
|
-
all = registryOf(currentServices()).getAll();
|
|
696
|
-
}
|
|
697
|
-
catch {
|
|
698
|
-
return undefined;
|
|
699
|
-
}
|
|
700
|
-
let availableKeys;
|
|
701
|
-
try {
|
|
702
|
-
availableKeys = new Set(registryOf(currentServices()).getAvailable().map(modelKey));
|
|
703
|
-
}
|
|
704
|
-
catch {
|
|
705
|
-
availableKeys = new Set();
|
|
706
|
-
}
|
|
707
|
-
// Rank: exact id (100) > exact provider/id (90) > id prefix (80) > id
|
|
708
|
-
// substring (60) > provider/id substring (40); +5 if the model is available.
|
|
709
|
-
const rank = (m) => {
|
|
710
|
-
const id = m.id.toLowerCase();
|
|
711
|
-
const key = modelKey(m).toLowerCase();
|
|
712
|
-
let base = -1;
|
|
713
|
-
if (id === q)
|
|
714
|
-
base = 100;
|
|
715
|
-
else if (key === q)
|
|
716
|
-
base = 90;
|
|
717
|
-
else if (id.startsWith(q))
|
|
718
|
-
base = 80;
|
|
719
|
-
else if (id.includes(q))
|
|
720
|
-
base = 60;
|
|
721
|
-
else if (key.includes(q))
|
|
722
|
-
base = 40;
|
|
723
|
-
else
|
|
724
|
-
return -1;
|
|
725
|
-
return base + (availableKeys.has(modelKey(m)) ? 5 : 0);
|
|
726
|
-
};
|
|
727
|
-
let best;
|
|
728
|
-
let bestRank = 0;
|
|
729
|
-
for (const m of all) {
|
|
730
|
-
const r = rank(m);
|
|
731
|
-
if (r > bestRank) {
|
|
732
|
-
bestRank = r;
|
|
733
|
-
best = m;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
return best;
|
|
737
|
-
};
|
|
738
|
-
// Cache per-currentSession() wire info keyed by (path, mtime, size) so reopening the
|
|
739
|
-
// picker does not re-read the whole corpus (see currentSession()-list-cache.ts).
|
|
740
|
-
const sessionListCache = new SessionListCache(sessionListCachePath());
|
|
741
|
-
sessionListCache.prewarm(currentSession().sessionManager.getSessionDir());
|
|
742
|
-
const handleHelloFrame = (client, frame) => {
|
|
743
|
-
// Admit at the REQUESTED role — unconditionally. `controller` is a
|
|
744
|
-
// per-client write capability, not a singleton slot, so an arriving
|
|
745
|
-
// writable client never demotes (or is demoted by) an already-attached
|
|
746
|
-
// one: attached terminal panes are all writable at once.
|
|
747
|
-
//
|
|
748
|
-
// A repeated hello on an already-helloed socket must not RE-role it: the
|
|
749
|
-
// role is fixed for the socket's lifetime, so every downstream gate and
|
|
750
|
-
// every already-fanned-out dialog keep the authority the first welcome
|
|
751
|
-
// stated. A duplicate hello still re-sends the catch-up snapshot.
|
|
752
|
-
const firstHello = !client.helloed;
|
|
753
|
-
client.id = frame.client_id;
|
|
754
|
-
client.snapshotTail = normalizeSnapshotTail(frame.snapshot_tail);
|
|
755
|
-
if (firstHello) {
|
|
756
|
-
client.helloed = true;
|
|
757
|
-
client.role = frame.role === 'controller' ? 'controller' : 'observer';
|
|
758
|
-
client.attends = frame.attends !== false;
|
|
759
|
-
}
|
|
760
|
-
sendWelcome(client);
|
|
761
|
-
if (firstHello)
|
|
762
|
-
registry.persistAttachState(); // a helloed viewer arrived
|
|
763
|
-
// welcome carried the FIRST pending dialog; replay any extras so a writable
|
|
764
|
-
// client attaching mid-dialog can answer every in-flight dialog.
|
|
765
|
-
if (client.role === 'controller')
|
|
766
|
-
replayExtraPendingDialogsTo(client);
|
|
767
|
-
};
|
|
768
48
|
const handleDriveFrame = (client, frame) => {
|
|
769
49
|
if (notWritable(client, 'drive the engine'))
|
|
770
50
|
return;
|
|
771
51
|
driveEngine(client, frame);
|
|
772
52
|
};
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
};
|
|
812
|
-
if (via === 'prompt')
|
|
813
|
-
turnAcceptedWaiters.add(ackAccepted);
|
|
814
|
-
const outcome = driveEngine(client, {
|
|
815
|
-
type: 'prompt',
|
|
816
|
-
text: frame.text,
|
|
817
|
-
...(frame.images !== undefined ? { images: frame.images } : {}),
|
|
818
|
-
});
|
|
819
|
-
if (via === 'steer' || outcome === undefined) {
|
|
820
|
-
turnAcceptedWaiters.delete(ackAccepted);
|
|
821
|
-
ackAccepted();
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
void outcome.then((failure) => {
|
|
825
|
-
turnAcceptedWaiters.delete(ackAccepted);
|
|
826
|
-
if (failure === undefined) {
|
|
827
|
-
ackAccepted();
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
// The error frame already went to the client, but a one-shot deliver
|
|
831
|
-
// client is gone by then — record it where it survives.
|
|
832
|
-
emitEvent({
|
|
833
|
-
level: 'error',
|
|
834
|
-
event: 'broker.prompt.deliver_failed',
|
|
835
|
-
error: failure,
|
|
836
|
-
});
|
|
837
|
-
if (delivered)
|
|
838
|
-
return;
|
|
839
|
-
delivered = true;
|
|
840
|
-
ackTo(client, 'deliver', false, String(failure), frame.id);
|
|
841
|
-
});
|
|
842
|
-
};
|
|
843
|
-
const handleDialogFrame = (client, frame) => {
|
|
844
|
-
if (notWritable(client, 'answer dialogs'))
|
|
845
|
-
return;
|
|
846
|
-
// First response for this id wins: `resolve` settles the entry and deletes
|
|
847
|
-
// it, so every later response from a peer that was also fanned this dialog
|
|
848
|
-
// finds nothing here and is a silent no-op.
|
|
849
|
-
pendingDialogs.get(frame.id)?.resolve(frame);
|
|
850
|
-
};
|
|
851
|
-
const handleEngineControlFrame = (client, frame) => {
|
|
852
|
-
switch (frame.type) {
|
|
853
|
-
// --- extended engine-command ops -------------------------------------
|
|
854
|
-
case 'set_model': {
|
|
855
|
-
if (notWritable(client, 'set the model'))
|
|
856
|
-
break;
|
|
857
|
-
const requested = parseModelSpec(frame.model);
|
|
858
|
-
let model;
|
|
859
|
-
let normalized;
|
|
860
|
-
try {
|
|
861
|
-
// Tier/alias specs (`opus`, `strong`, `anthropic/strong`, …) MUST resolve
|
|
862
|
-
// through the config ladder before touching the registry: the fuzzy
|
|
863
|
-
// registry search rank-ties `opus` between claude-3-opus-20240229 and
|
|
864
|
-
// claude-opus-4-8, and the deprecated 2024 model wins on catalog order.
|
|
865
|
-
// Concrete `provider/id` specs and genuine substrings (`fable`) pass
|
|
866
|
-
// through normalizeModel unchanged and take the exact/search path below.
|
|
867
|
-
normalized = parseModelSpec(normalizeModel(requested.modelSpec));
|
|
868
|
-
model = resolveModelQuery(normalized.modelSpec);
|
|
869
|
-
}
|
|
870
|
-
catch (err) {
|
|
871
|
-
// N2: registry.find should never throw on the real SDK, but a degenerate
|
|
872
|
-
// engine must still get a reply rather than a silently-dropped frame.
|
|
873
|
-
engineErrorTo(client)(err);
|
|
874
|
-
break;
|
|
875
|
-
}
|
|
876
|
-
if (model === undefined) {
|
|
877
|
-
registry.sendFrame(client, {
|
|
878
|
-
type: 'error',
|
|
879
|
-
code: 'engine_error',
|
|
880
|
-
message: `no model matching '${frame.model}' in the registry`,
|
|
881
|
-
});
|
|
882
|
-
break;
|
|
883
|
-
}
|
|
884
|
-
void currentSession()
|
|
885
|
-
.setModel(model)
|
|
886
|
-
.then(async () => {
|
|
887
|
-
// The caller's explicit `:thinking` wins; else the ladder cell's.
|
|
888
|
-
const thinking = requested.thinkingLevel ?? normalized.thinkingLevel;
|
|
889
|
-
if (thinking !== undefined)
|
|
890
|
-
currentSession().setThinkingLevel(thinking);
|
|
891
|
-
// frame.pinned, when the sender pre-normalized `frame.model` (e.g.
|
|
892
|
-
// setModelLive), carries the RAW-token pin decision explicitly.
|
|
893
|
-
// Otherwise frame.model itself IS the raw/explicit token (a
|
|
894
|
-
// slash-command arg, or a concrete picker pick) and derives it.
|
|
895
|
-
await broadcastModelChanged(true, frame.pinned !== undefined
|
|
896
|
-
? frame.pinned
|
|
897
|
-
: isProviderPinnedModelToken(frame.model), true);
|
|
898
|
-
ackTo(client, 'set_model');
|
|
899
|
-
})
|
|
900
|
-
.catch(engineErrorTo(client));
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
903
|
-
case 'deliver_custom_message': {
|
|
904
|
-
if (notWritable(client, 'deliver a custom message'))
|
|
905
|
-
break;
|
|
906
|
-
// Never triggers a turn either way. `nextTurn` folds the message into
|
|
907
|
-
// whatever turn comes next (situational context); omitting it pushes
|
|
908
|
-
// straight onto the message list so it lands FIRST in a still-empty
|
|
909
|
-
// currentSession() (a freshly-claimed spare's bearings). The inbox watcher's own
|
|
910
|
-
// situational send uses `followUp` + triggerTurn because there its job
|
|
911
|
-
// IS to wake the node.
|
|
912
|
-
void currentSession()
|
|
913
|
-
.sendCustomMessage({
|
|
914
|
-
customType: frame.customType,
|
|
915
|
-
content: frame.content,
|
|
916
|
-
display: true,
|
|
917
|
-
...(frame.details !== undefined
|
|
918
|
-
? { details: frame.details }
|
|
919
|
-
: {}),
|
|
920
|
-
}, ...(frame.deliverAs !== undefined
|
|
921
|
-
? [{ deliverAs: frame.deliverAs }]
|
|
922
|
-
: []))
|
|
923
|
-
.then(() => ackTo(client, 'deliver_custom_message'))
|
|
924
|
-
.catch(engineErrorTo(client));
|
|
925
|
-
break;
|
|
926
|
-
}
|
|
927
|
-
case 'cycle_model': {
|
|
928
|
-
if (notWritable(client, 'cycle the model'))
|
|
929
|
-
break;
|
|
930
|
-
void currentSession()
|
|
931
|
-
.cycleModel(frame.direction)
|
|
932
|
-
.then(async () => {
|
|
933
|
-
await broadcastModelChanged(true, undefined, true);
|
|
934
|
-
ackTo(client, 'cycle_model');
|
|
935
|
-
})
|
|
936
|
-
.catch(engineErrorTo(client));
|
|
937
|
-
break;
|
|
938
|
-
}
|
|
939
|
-
case 'cycle_ladder': {
|
|
940
|
-
if (notWritable(client, 'cycle the model ladder'))
|
|
941
|
-
break;
|
|
942
|
-
// Resolve the next interleaved-ladder rung from the current model+thinking
|
|
943
|
-
// spec, then reuse the set_model path (registry resolve + thinking apply).
|
|
944
|
-
// The cycle only offers signed-in provider columns — same rule as the
|
|
945
|
-
// setup wizard's Models panel: pool occupancy for a managed provider
|
|
946
|
-
// (read live, so a login that landed mid-currentSession() counts immediately),
|
|
947
|
-
// else the registry's auth status. When nobody is signed in the helper
|
|
948
|
-
// returns every provider, so the cycle stays walkable pre-login.
|
|
949
|
-
const offeredProviders = (() => {
|
|
950
|
-
try {
|
|
951
|
-
const registry = registryOf(currentServices());
|
|
952
|
-
return configuredLadderProviders((providerId) => (isManagedProvider(providerId) &&
|
|
953
|
-
readSubscriptionPool(providerId).length > 0) ||
|
|
954
|
-
registry.getProviderAuthStatus(providerId).configured);
|
|
955
|
-
}
|
|
956
|
-
catch {
|
|
957
|
-
return undefined; // auth state unreadable — cycle the full ladder
|
|
958
|
-
}
|
|
959
|
-
})();
|
|
960
|
-
const currentSpec = formatModelSpec(currentSession().model, currentSession().thinkingLevel) ?? '';
|
|
961
|
-
const nextSpec = parseModelSpec(nextLadderModel(currentSpec, frame.direction === 'backward' ? -1 : 1, offeredProviders));
|
|
962
|
-
let ladderModel;
|
|
963
|
-
try {
|
|
964
|
-
ladderModel = resolveModelQuery(nextSpec.modelSpec);
|
|
965
|
-
}
|
|
966
|
-
catch (err) {
|
|
967
|
-
engineErrorTo(client)(err);
|
|
968
|
-
break;
|
|
969
|
-
}
|
|
970
|
-
if (ladderModel === undefined) {
|
|
971
|
-
registry.sendFrame(client, {
|
|
972
|
-
type: 'error',
|
|
973
|
-
code: 'engine_error',
|
|
974
|
-
message: `ladder model '${nextSpec.modelSpec}' not in the registry`,
|
|
975
|
-
});
|
|
976
|
-
break;
|
|
977
|
-
}
|
|
978
|
-
void currentSession()
|
|
979
|
-
.setModel(ladderModel)
|
|
980
|
-
.then(async () => {
|
|
981
|
-
if (nextSpec.thinkingLevel !== undefined)
|
|
982
|
-
currentSession().setThinkingLevel(nextSpec.thinkingLevel);
|
|
983
|
-
await broadcastModelChanged(true, undefined, true);
|
|
984
|
-
ackTo(client, 'cycle_ladder');
|
|
985
|
-
})
|
|
986
|
-
.catch(engineErrorTo(client));
|
|
987
|
-
break;
|
|
988
|
-
}
|
|
989
|
-
case 'cycle_thinking': {
|
|
990
|
-
if (notWritable(client, 'cycle the thinking level'))
|
|
991
|
-
break;
|
|
992
|
-
void (async () => {
|
|
993
|
-
currentSession().cycleThinkingLevel();
|
|
994
|
-
await persistModelChoice(undefined, true);
|
|
995
|
-
ackTo(client, 'cycle_thinking');
|
|
996
|
-
})().catch(engineErrorTo(client));
|
|
997
|
-
break;
|
|
998
|
-
}
|
|
999
|
-
case 'dequeue': {
|
|
1000
|
-
if (notWritable(client, 'dequeue messages', frame.id))
|
|
1001
|
-
break;
|
|
1002
|
-
try {
|
|
1003
|
-
const { steering, followUp } = currentSession().clearQueue();
|
|
1004
|
-
registry.sendFrame(client, {
|
|
1005
|
-
type: 'data',
|
|
1006
|
-
id: frame.id,
|
|
1007
|
-
kind: 'dequeue',
|
|
1008
|
-
steering,
|
|
1009
|
-
followUp,
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
catch (err) {
|
|
1013
|
-
engineErrorTo(client, frame.id)(err);
|
|
1014
|
-
}
|
|
1015
|
-
break;
|
|
1016
|
-
}
|
|
1017
|
-
case 'set_thinking_level': {
|
|
1018
|
-
if (notWritable(client, 'set the thinking level'))
|
|
1019
|
-
break;
|
|
1020
|
-
void (async () => {
|
|
1021
|
-
currentSession().setThinkingLevel(frame.level);
|
|
1022
|
-
await persistModelChoice(undefined, true);
|
|
1023
|
-
ackTo(client, 'set_thinking_level');
|
|
1024
|
-
})().catch(engineErrorTo(client));
|
|
1025
|
-
break;
|
|
1026
|
-
}
|
|
1027
|
-
case 'set_auto_retry': {
|
|
1028
|
-
if (notWritable(client, 'set auto-retry'))
|
|
1029
|
-
break;
|
|
1030
|
-
try {
|
|
1031
|
-
currentSession().setAutoRetryEnabled(frame.enabled);
|
|
1032
|
-
ackTo(client, 'set_auto_retry');
|
|
1033
|
-
}
|
|
1034
|
-
catch (err) {
|
|
1035
|
-
engineErrorTo(client)(err);
|
|
1036
|
-
}
|
|
1037
|
-
break;
|
|
1038
|
-
}
|
|
1039
|
-
case 'set_auto_compaction': {
|
|
1040
|
-
if (notWritable(client, 'set auto-compaction'))
|
|
1041
|
-
break;
|
|
1042
|
-
try {
|
|
1043
|
-
currentSession().setAutoCompactionEnabled(frame.enabled);
|
|
1044
|
-
ackTo(client, 'set_auto_compaction');
|
|
1045
|
-
}
|
|
1046
|
-
catch (err) {
|
|
1047
|
-
engineErrorTo(client)(err);
|
|
1048
|
-
}
|
|
1049
|
-
break;
|
|
1050
|
-
}
|
|
1051
|
-
case 'compact': {
|
|
1052
|
-
if (notWritable(client, 'compact the currentSession()'))
|
|
1053
|
-
break;
|
|
1054
|
-
void currentSession()
|
|
1055
|
-
.compact(frame.instructions)
|
|
1056
|
-
.then(() => ackTo(client, 'compact'))
|
|
1057
|
-
.catch(engineErrorTo(client));
|
|
1058
|
-
break;
|
|
1059
|
-
}
|
|
1060
|
-
case 'set_session_name': {
|
|
1061
|
-
if (notWritable(client, 'rename the currentSession()'))
|
|
1062
|
-
break;
|
|
1063
|
-
try {
|
|
1064
|
-
currentSession().setSessionName(frame.name);
|
|
1065
|
-
ackTo(client, 'set_session_name');
|
|
1066
|
-
}
|
|
1067
|
-
catch (err) {
|
|
1068
|
-
engineErrorTo(client)(err);
|
|
1069
|
-
}
|
|
1070
|
-
break;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
};
|
|
1074
|
-
const handleReadFrame = (client, frame) => {
|
|
1075
|
-
switch (frame.type) {
|
|
1076
|
-
case 'get_commands': {
|
|
1077
|
-
// OBSERVERS may call this: the merged command inventory is static
|
|
1078
|
-
// (extensions + templates + skills + builtins) and drives nothing in the
|
|
1079
|
-
// engine, so it is not a controller-gated op. Terminal observer
|
|
1080
|
-
// connections use it to populate the command palette.
|
|
1081
|
-
// The merged command list rides in ack.detail as JSON (the foundation's
|
|
1082
|
-
// AckFrame.detail field) — the viewer JSON.parses it when for ===
|
|
1083
|
-
// 'get_commands'. Keeps every command op a uniform ack reply.
|
|
1084
|
-
try {
|
|
1085
|
-
ackTo(client, 'get_commands', true, JSON.stringify(buildCommandList(currentSession())));
|
|
1086
|
-
}
|
|
1087
|
-
catch (err) {
|
|
1088
|
-
engineErrorTo(client)(err);
|
|
1089
|
-
}
|
|
1090
|
-
break;
|
|
1091
|
-
}
|
|
1092
|
-
// --- read-op picker data (§5 Unit A) ----------------------------------
|
|
1093
|
-
// NOT controller-gated: read-only, like get_commands. Terminal observer
|
|
1094
|
-
// connections populate pickers through these too.
|
|
1095
|
-
case 'list_models': {
|
|
1096
|
-
try {
|
|
1097
|
-
registry.sendFrame(client, buildListModelsData(frame.id, currentSession()));
|
|
1098
|
-
}
|
|
1099
|
-
catch (err) {
|
|
1100
|
-
engineErrorTo(client, frame.id)(err);
|
|
1101
|
-
}
|
|
1102
|
-
break;
|
|
1103
|
-
}
|
|
1104
|
-
case 'list_scoped_models': {
|
|
1105
|
-
try {
|
|
1106
|
-
registry.sendFrame(client, buildScopedModelsData(frame.id, currentSession()));
|
|
1107
|
-
}
|
|
1108
|
-
catch (err) {
|
|
1109
|
-
engineErrorTo(client, frame.id)(err);
|
|
1110
|
-
}
|
|
1111
|
-
break;
|
|
1112
|
-
}
|
|
1113
|
-
case 'list_memory_refs': {
|
|
1114
|
-
try {
|
|
1115
|
-
registry.sendFrame(client, buildListMemoryRefsData(frame.id, getRefInventory().refs, engineLeadingCommandTokens(currentSession())));
|
|
1116
|
-
}
|
|
1117
|
-
catch (err) {
|
|
1118
|
-
engineErrorTo(client, frame.id)(err);
|
|
1119
|
-
}
|
|
1120
|
-
break;
|
|
1121
|
-
}
|
|
1122
|
-
case 'get_tree': {
|
|
1123
|
-
try {
|
|
1124
|
-
registry.sendFrame(client, buildGetTreeData(frame.id, currentSession(), boundaryReviewId));
|
|
1125
|
-
}
|
|
1126
|
-
catch (err) {
|
|
1127
|
-
engineErrorTo(client, frame.id)(err);
|
|
1128
|
-
}
|
|
1129
|
-
break;
|
|
1130
|
-
}
|
|
1131
|
-
case 'get_settings': {
|
|
1132
|
-
try {
|
|
1133
|
-
registry.sendFrame(client, buildSettingsData(frame.id, currentSession()));
|
|
1134
|
-
}
|
|
1135
|
-
catch (err) {
|
|
1136
|
-
engineErrorTo(client, frame.id)(err);
|
|
1137
|
-
}
|
|
1138
|
-
break;
|
|
1139
|
-
}
|
|
1140
|
-
case 'list_sessions': {
|
|
1141
|
-
// Session listing reads the currentSession() dir off disk — async; reply when it
|
|
1142
|
-
// resolves (or relay the error, correlated by id so the viewer doesn't hang).
|
|
1143
|
-
void buildSessionsData(frame.id, frame.scope ?? 'cwd', currentSession(), sessionListCache)
|
|
1144
|
-
.then((d) => registry.sendFrame(client, d))
|
|
1145
|
-
.catch(engineErrorTo(client, frame.id));
|
|
1146
|
-
break;
|
|
1147
|
-
}
|
|
1148
|
-
case 'get_human_fork_coordinates': {
|
|
1149
|
-
// NOT controller-gated: this is a pure observer read. Branch creation is
|
|
1150
|
-
// intentionally delegated to a detached SDK manager in the ask publisher;
|
|
1151
|
-
// calling createBranchedSession here corrupts this live manager's identity.
|
|
1152
|
-
try {
|
|
1153
|
-
const sm = currentSession().sessionManager;
|
|
1154
|
-
const sourceFile = sm.getSessionFile();
|
|
1155
|
-
const leafId = sm.getLeafId();
|
|
1156
|
-
if (sourceFile === undefined ||
|
|
1157
|
-
sourceFile === '' ||
|
|
1158
|
-
leafId === null) {
|
|
1159
|
-
engineErrorTo(client, frame.id)(new Error('no persisted source currentSession() leaf'));
|
|
1160
|
-
break;
|
|
1161
|
-
}
|
|
1162
|
-
registry.sendFrame(client, {
|
|
1163
|
-
type: 'data',
|
|
1164
|
-
id: frame.id,
|
|
1165
|
-
kind: 'human_fork_coordinates',
|
|
1166
|
-
sourceFile,
|
|
1167
|
-
sourceSessionId: sm.getSessionId(),
|
|
1168
|
-
leafId,
|
|
1169
|
-
modelSpec: formatExactModelSpec(currentSession().model, currentSession().thinkingLevel),
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
catch (err) {
|
|
1173
|
-
engineErrorTo(client, frame.id)(err);
|
|
1174
|
-
}
|
|
1175
|
-
break;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
};
|
|
1179
|
-
const handleSessionFrame = (client, frame) => {
|
|
1180
|
-
switch (frame.type) {
|
|
1181
|
-
case 'navigate_tree': {
|
|
1182
|
-
if (notWritable(client, 'navigate the currentSession() tree'))
|
|
1183
|
-
break;
|
|
1184
|
-
if (isPreBoundaryReviewEntry(frame.targetId)) {
|
|
1185
|
-
reviewBoundaryRefusalTo(client, frame.targetId);
|
|
1186
|
-
break;
|
|
1187
|
-
}
|
|
1188
|
-
// navigateTree rewinds IN-PLACE (same currentSession() file, new leaf) and emits no
|
|
1189
|
-
// relayed event, so every viewer must be re-snapshotted onto the rewound
|
|
1190
|
-
// transcript — same reWelcomeAll the currentSession()-replacing ops use. The ack's
|
|
1191
|
-
// detail carries the navigated-to user message's text (pi parity: the
|
|
1192
|
-
// interactive tree navigator restores it to the editor for re-editing).
|
|
1193
|
-
void currentSession()
|
|
1194
|
-
.navigateTree(frame.targetId, frame.options)
|
|
1195
|
-
.then((r) => {
|
|
1196
|
-
if (!r.cancelled)
|
|
1197
|
-
reWelcomeAll();
|
|
1198
|
-
ackTo(client, 'navigate_tree', !r.cancelled, r.editorText);
|
|
1199
|
-
})
|
|
1200
|
-
.catch(engineErrorTo(client));
|
|
1201
|
-
break;
|
|
1202
|
-
}
|
|
1203
|
-
case 'reload': {
|
|
1204
|
-
if (notWritable(client, 'reload'))
|
|
1205
|
-
break;
|
|
1206
|
-
// A SUCCESSFUL reload invalidates the memoized ref inventory (work item
|
|
1207
|
-
// 3) so the next read-op/submission re-walks the corpus, AND clears the
|
|
1208
|
-
// shared substrate parse cache buildRefInventory reads through —
|
|
1209
|
-
// otherwise a fresh buildRefInventory() call would still see the stale
|
|
1210
|
-
// cached docs. A FAILED reload (`.catch`) leaves both untouched: nothing
|
|
1211
|
-
// about the corpus is known to have changed.
|
|
1212
|
-
void currentSession()
|
|
1213
|
-
.reload({ beforeSessionStart: () => captureSessionChatCapabilities(currentSession()) })
|
|
1214
|
-
.then(() => {
|
|
1215
|
-
// pi's reload() re-syncs queue modes from the shared settings.json
|
|
1216
|
-
// (its ONE post-bind syncQueueModesFromSettings call site) —
|
|
1217
|
-
// re-apply the broker's 'all' modes so a reload cannot silently
|
|
1218
|
-
// restore one-at-a-time draining (see rebind.enqueueRebind).
|
|
1219
|
-
const agent = currentSession().agent;
|
|
1220
|
-
if (agent) {
|
|
1221
|
-
agent.steeringMode = 'all';
|
|
1222
|
-
agent.followUpMode = 'all';
|
|
1223
|
-
}
|
|
1224
|
-
memoizedRefInventory = null;
|
|
1225
|
-
clearSessionCache();
|
|
1226
|
-
registry.forEachHelloed((observer) => {
|
|
1227
|
-
if (observer.role === 'observer')
|
|
1228
|
-
registry.sendFrame(observer, { type: 'resources_reloaded' });
|
|
1229
|
-
});
|
|
1230
|
-
ackTo(client, 'reload');
|
|
1231
|
-
})
|
|
1232
|
-
.catch(engineErrorTo(client));
|
|
1233
|
-
break;
|
|
1234
|
-
}
|
|
1235
|
-
case 'export': {
|
|
1236
|
-
if (notWritable(client, 'export the currentSession()'))
|
|
1237
|
-
break;
|
|
1238
|
-
if (frame.format === 'jsonl') {
|
|
1239
|
-
try {
|
|
1240
|
-
const outPath = currentSession().exportToJsonl(frame.path);
|
|
1241
|
-
stripHiddenEntriesFromJsonlExport(outPath);
|
|
1242
|
-
ackTo(client, 'export');
|
|
1243
|
-
}
|
|
1244
|
-
catch (err) {
|
|
1245
|
-
engineErrorTo(client)(err);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
else {
|
|
1249
|
-
void currentSession()
|
|
1250
|
-
.exportToHtml(frame.path)
|
|
1251
|
-
.then(() => ackTo(client, 'export'))
|
|
1252
|
-
.catch(engineErrorTo(client));
|
|
1253
|
-
}
|
|
1254
|
-
break;
|
|
1255
|
-
}
|
|
1256
|
-
case 'new_session': {
|
|
1257
|
-
if (notWritable(client, 'start a new currentSession()'))
|
|
1258
|
-
break;
|
|
1259
|
-
rebind.runReplacement(client, 'new_session', (rt) => rt.newSession());
|
|
1260
|
-
break;
|
|
1261
|
-
}
|
|
1262
|
-
case 'switch_session': {
|
|
1263
|
-
if (notWritable(client, 'switch sessions'))
|
|
1264
|
-
break;
|
|
1265
|
-
rebind.runReplacement(client, 'switch_session', (rt) => rt.switchSession(frame.path));
|
|
1266
|
-
break;
|
|
1267
|
-
}
|
|
1268
|
-
case 'fork': {
|
|
1269
|
-
if (notWritable(client, 'fork the currentSession()'))
|
|
1270
|
-
break;
|
|
1271
|
-
if (isPreBoundaryReviewEntry(frame.entryId)) {
|
|
1272
|
-
reviewBoundaryRefusalTo(client, frame.entryId);
|
|
1273
|
-
break;
|
|
1274
|
-
}
|
|
1275
|
-
rebind.runReplacement(client, 'fork', (rt) => rt.fork(frame.entryId));
|
|
1276
|
-
break;
|
|
1277
|
-
}
|
|
1278
|
-
case 'clone': {
|
|
1279
|
-
if (notWritable(client, 'clone the currentSession()'))
|
|
1280
|
-
break;
|
|
1281
|
-
rebind.runReplacement(client, 'clone', async (rt) => {
|
|
1282
|
-
const sm = currentSession().sessionManager;
|
|
1283
|
-
const leafId = sm.getLeafId();
|
|
1284
|
-
if (leafId === null) {
|
|
1285
|
-
return { cancelled: true, detail: 'no current leaf to clone from' };
|
|
1286
|
-
}
|
|
1287
|
-
const newPath = sm.createBranchedSession(leafId);
|
|
1288
|
-
if (newPath === undefined) {
|
|
1289
|
-
return {
|
|
1290
|
-
cancelled: true,
|
|
1291
|
-
detail: 'createBranchedSession returned no path',
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
return rt.switchSession(newPath);
|
|
1295
|
-
});
|
|
1296
|
-
break;
|
|
1297
|
-
}
|
|
1298
|
-
case 'share': {
|
|
1299
|
-
if (notWritable(client, 'share the currentSession()'))
|
|
1300
|
-
break;
|
|
1301
|
-
const tmpPath = join(tmpdir(), `pi-share-${Date.now()}.html`);
|
|
1302
|
-
void currentSession()
|
|
1303
|
-
.exportToHtml(tmpPath)
|
|
1304
|
-
.then(() => new Promise((resolve, reject) => {
|
|
1305
|
-
execFile('gh', ['gist', 'create', '--secret', tmpPath], { timeout: 30_000, maxBuffer: 1024 * 1024 }, (err, stdout) => {
|
|
1306
|
-
try {
|
|
1307
|
-
if (existsSync(tmpPath))
|
|
1308
|
-
unlinkSync(tmpPath);
|
|
1309
|
-
}
|
|
1310
|
-
catch {
|
|
1311
|
-
/* cleanup */
|
|
1312
|
-
}
|
|
1313
|
-
if (err) {
|
|
1314
|
-
reject(err);
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
resolve(stdout.trim());
|
|
1318
|
-
});
|
|
1319
|
-
}))
|
|
1320
|
-
.then((url) => {
|
|
1321
|
-
ackTo(client, 'share', true, url);
|
|
1322
|
-
})
|
|
1323
|
-
.catch((err) => {
|
|
1324
|
-
const msg = String(err?.message ?? err);
|
|
1325
|
-
ackTo(client, 'share', false, msg.includes('not found') ||
|
|
1326
|
-
msg.includes('gh: command not found') ||
|
|
1327
|
-
msg.includes('ENOENT')
|
|
1328
|
-
? '`gh` CLI not found — install GitHub CLI and authenticate with `gh auth login`'
|
|
1329
|
-
: `share failed: ${msg}`);
|
|
1330
|
-
});
|
|
1331
|
-
break;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
};
|
|
1335
|
-
const handleAuthenticationFrame = (client, frame) => {
|
|
1336
|
-
// Open to any client, observers included: reload_auth is an idempotent
|
|
1337
|
-
// local re-read of shared credentials + the runtime's availability
|
|
1338
|
-
// snapshot. It does not steer the conversation, so the daemon's
|
|
1339
|
-
// canvas-wide /login fan can ride an observer connection.
|
|
1340
|
-
try {
|
|
1341
|
-
ackTo(client, 'reload_auth');
|
|
1342
|
-
const authGeneration = rebind.installedGeneration();
|
|
1343
|
-
const existingAuthReload = reloadAuthInFlight;
|
|
1344
|
-
let authReloadPromise;
|
|
1345
|
-
if (existingAuthReload !== null &&
|
|
1346
|
-
existingAuthReload.generation === authGeneration) {
|
|
1347
|
-
authReloadPromise = existingAuthReload.promise;
|
|
1348
|
-
}
|
|
1349
|
-
if (authGeneration !== undefined && authReloadPromise === undefined) {
|
|
1350
|
-
const authSession = currentSession();
|
|
1351
|
-
const authServices = currentServices();
|
|
1352
|
-
authReloadPromise = (async () => {
|
|
1353
|
-
// Credentials are read fresh by PoolCredentialStore; only the
|
|
1354
|
-
// ModelRuntime snapshot needs rebuilding. Authentication reloads
|
|
1355
|
-
// never fetch provider catalogs over the network.
|
|
1356
|
-
await authServices.modelRuntime.refresh({ allowNetwork: false });
|
|
1357
|
-
if (rebind.installedGeneration() !== authGeneration ||
|
|
1358
|
-
currentSession() !== authSession)
|
|
1359
|
-
return;
|
|
1360
|
-
const modelLess = authSession.model === undefined ||
|
|
1361
|
-
isUnknownModel(authSession.model);
|
|
1362
|
-
const reconcilesLogicalRoute = cfg.modelExact !== true &&
|
|
1363
|
-
cfg.modelProviderPinned !== true &&
|
|
1364
|
-
(process.env['CRTR_MODEL_INTENT'] ?? '').trim() !== '';
|
|
1365
|
-
if (modelLess || reconcilesLogicalRoute) {
|
|
1366
|
-
const resolved = await resolveLaunchModel(authServices, cfg);
|
|
1367
|
-
if (rebind.installedGeneration() !== authGeneration ||
|
|
1368
|
-
currentSession() !== authSession)
|
|
1369
|
-
return;
|
|
1370
|
-
if (resolved !== undefined) {
|
|
1371
|
-
const currentSpec = formatModelSpec(authSession.model, authSession.thinkingLevel);
|
|
1372
|
-
const resolvedSpec = formatModelSpec(resolved.model, resolved.thinkingLevel);
|
|
1373
|
-
if (resolvedSpec !== null && resolvedSpec !== currentSpec) {
|
|
1374
|
-
await authSession.setModel(resolved.model);
|
|
1375
|
-
if (rebind.installedGeneration() !== authGeneration ||
|
|
1376
|
-
currentSession() !== authSession)
|
|
1377
|
-
return;
|
|
1378
|
-
authSession.setThinkingLevel((resolved.thinkingLevel ?? 'off'));
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
if (rebind.installedGeneration() !== authGeneration ||
|
|
1383
|
-
currentSession() !== authSession)
|
|
1384
|
-
return;
|
|
1385
|
-
// Persist an algorithmic route choice without clearing the logical
|
|
1386
|
-
// intent that makes future reconciliation possible.
|
|
1387
|
-
await broadcastModelChanged();
|
|
1388
|
-
drivePendingKickoff();
|
|
1389
|
-
})().catch((err) => {
|
|
1390
|
-
emitEvent({
|
|
1391
|
-
level: 'warn',
|
|
1392
|
-
event: 'broker.auth_reload.reconcile_failed',
|
|
1393
|
-
error: err,
|
|
1394
|
-
});
|
|
1395
|
-
});
|
|
1396
|
-
const work = {
|
|
1397
|
-
generation: authGeneration,
|
|
1398
|
-
promise: authReloadPromise,
|
|
1399
|
-
};
|
|
1400
|
-
reloadAuthInFlight = work;
|
|
1401
|
-
void authReloadPromise.finally(() => {
|
|
1402
|
-
if (reloadAuthInFlight === work)
|
|
1403
|
-
reloadAuthInFlight = null;
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
const authFault = readFault(nodeId);
|
|
1407
|
-
const authFaultGeneration = rebind.installedGeneration();
|
|
1408
|
-
const authFaultSession = currentSession();
|
|
1409
|
-
if (authFault?.link === 'pi→provider' &&
|
|
1410
|
-
authFault.kind === 'auth' &&
|
|
1411
|
-
!authFaultSession.isStreaming &&
|
|
1412
|
-
!authFaultRedriveInFlight) {
|
|
1413
|
-
authFaultRedriveInFlight = true;
|
|
1414
|
-
void (async () => {
|
|
1415
|
-
try {
|
|
1416
|
-
if (authReloadPromise !== undefined)
|
|
1417
|
-
await authReloadPromise;
|
|
1418
|
-
if (rebind.installedGeneration() !== authFaultGeneration ||
|
|
1419
|
-
currentSession() !== authFaultSession)
|
|
1420
|
-
return;
|
|
1421
|
-
const anchorEntryId = authFault.anchorEntryId;
|
|
1422
|
-
if (anchorEntryId !== undefined) {
|
|
1423
|
-
try {
|
|
1424
|
-
const leafId = authFaultSession.sessionManager.getLeafId?.() ?? null;
|
|
1425
|
-
if (leafId !== anchorEntryId)
|
|
1426
|
-
await authFaultSession.navigateTree(anchorEntryId);
|
|
1427
|
-
if (rebind.installedGeneration() !== authFaultGeneration ||
|
|
1428
|
-
currentSession() !== authFaultSession)
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1431
|
-
catch (err) {
|
|
1432
|
-
emitEvent({
|
|
1433
|
-
level: 'warn',
|
|
1434
|
-
event: 'broker.auth_fault.rewind_skipped',
|
|
1435
|
-
error: err,
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
const fresh = readFault(nodeId);
|
|
1440
|
-
if (rebind.installedGeneration() !== authFaultGeneration ||
|
|
1441
|
-
currentSession() !== authFaultSession ||
|
|
1442
|
-
authFaultSession.isStreaming ||
|
|
1443
|
-
fresh?.link !== 'pi→provider' ||
|
|
1444
|
-
fresh.kind !== 'auth' ||
|
|
1445
|
-
fresh.since !== authFault.since)
|
|
1446
|
-
return;
|
|
1447
|
-
clearFault(nodeId, { link: 'pi→provider' });
|
|
1448
|
-
await authFaultSession.prompt(formatCard('recovery', {}, AUTH_FAULT_RECOVERY_BODY));
|
|
1449
|
-
}
|
|
1450
|
-
catch (err) {
|
|
1451
|
-
emitEvent({
|
|
1452
|
-
level: 'error',
|
|
1453
|
-
event: 'broker.auth_fault.redrive_failed',
|
|
1454
|
-
error: err,
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
finally {
|
|
1458
|
-
authFaultRedriveInFlight = false;
|
|
1459
|
-
}
|
|
1460
|
-
})();
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
catch (err) {
|
|
1464
|
-
engineErrorTo(client)(err);
|
|
1465
|
-
}
|
|
1466
|
-
};
|
|
1467
|
-
const handleByeFrame = (client) => {
|
|
1468
|
-
client.socket.end();
|
|
1469
|
-
};
|
|
1470
|
-
const handleShutdownFrame = () => {
|
|
1471
|
-
disposeAndExit('shutdown-frame');
|
|
1472
|
-
};
|
|
53
|
+
const clientHandlers = createClientHandlers({
|
|
54
|
+
registry,
|
|
55
|
+
replies,
|
|
56
|
+
session: currentSession,
|
|
57
|
+
driveEngine,
|
|
58
|
+
turnAcceptance,
|
|
59
|
+
pendingDialogs,
|
|
60
|
+
sendWelcome,
|
|
61
|
+
replayExtraPendingDialogsTo,
|
|
62
|
+
disposeAndExit,
|
|
63
|
+
});
|
|
64
|
+
const handleEngineControlFrame = createEngineControlHandler({
|
|
65
|
+
registry,
|
|
66
|
+
replies,
|
|
67
|
+
session: currentSession,
|
|
68
|
+
services: currentServices,
|
|
69
|
+
registryOf,
|
|
70
|
+
persistModelChoice,
|
|
71
|
+
broadcastModelChanged,
|
|
72
|
+
formatModelSpec,
|
|
73
|
+
});
|
|
74
|
+
const handleReadFrame = createReadHandler({
|
|
75
|
+
registry,
|
|
76
|
+
replies,
|
|
77
|
+
session: currentSession,
|
|
78
|
+
boundaryReviewId,
|
|
79
|
+
memoryRefs,
|
|
80
|
+
formatExactModelSpec,
|
|
81
|
+
});
|
|
82
|
+
const handleSessionFrame = createSessionHandler({
|
|
83
|
+
registry,
|
|
84
|
+
replies,
|
|
85
|
+
rebind,
|
|
86
|
+
session: currentSession,
|
|
87
|
+
boundaryReviewId,
|
|
88
|
+
memoryRefs,
|
|
89
|
+
reWelcomeAll,
|
|
90
|
+
});
|
|
1473
91
|
return {
|
|
1474
92
|
handlers: {
|
|
1475
|
-
hello:
|
|
93
|
+
hello: clientHandlers.hello,
|
|
1476
94
|
drive: handleDriveFrame,
|
|
1477
|
-
deliver:
|
|
1478
|
-
dialog:
|
|
95
|
+
deliver: clientHandlers.deliver,
|
|
96
|
+
dialog: clientHandlers.dialog,
|
|
1479
97
|
engineControl: handleEngineControlFrame,
|
|
1480
98
|
read: handleReadFrame,
|
|
1481
99
|
session: handleSessionFrame,
|
|
1482
|
-
authentication:
|
|
1483
|
-
bye:
|
|
1484
|
-
shutdown:
|
|
1485
|
-
},
|
|
1486
|
-
kickoff(prompt) {
|
|
1487
|
-
kickoffPending = prompt;
|
|
1488
|
-
drivePendingKickoff();
|
|
1489
|
-
},
|
|
1490
|
-
notifyTurnAccepted,
|
|
1491
|
-
pendingPromptDispatches: () => pendingPromptDispatches,
|
|
1492
|
-
resetForSession: () => {
|
|
1493
|
-
reloadAuthInFlight = null;
|
|
100
|
+
authentication: (client) => authReloadGate.handle(client),
|
|
101
|
+
bye: clientHandlers.bye,
|
|
102
|
+
shutdown: clientHandlers.shutdown,
|
|
1494
103
|
},
|
|
104
|
+
kickoff: (prompt) => kickoffDriver.kickoff(prompt),
|
|
105
|
+
notifyTurnAccepted: turnAcceptance.notifyAccepted,
|
|
106
|
+
pendingPromptDispatches: dispatches.pending,
|
|
107
|
+
resetForSession: authReloadGate.resetForSession,
|
|
1495
108
|
};
|
|
1496
109
|
}
|
|
1497
110
|
/** The socket listener calls this explicit dispatcher for every decoded client frame. */
|
|
@@ -1547,7 +160,7 @@ export function handleFrame(ctx, client, frame) {
|
|
|
1547
160
|
ctx.handlers.session(client, frame);
|
|
1548
161
|
return;
|
|
1549
162
|
case 'reload_auth':
|
|
1550
|
-
ctx.handlers.authentication(client
|
|
163
|
+
ctx.handlers.authentication(client);
|
|
1551
164
|
return;
|
|
1552
165
|
case 'bye':
|
|
1553
166
|
ctx.handlers.bye(client);
|