@crouton-kit/crouter 0.3.27 → 0.3.29
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/build-root.js +4 -2
- package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +20 -18
- package/dist/builtin-memory/design.md +1 -1
- package/dist/builtin-memory/development.md +1 -1
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/planning.md +1 -1
- package/dist/builtin-memory/spec.md +6 -4
- package/dist/builtin-personas/orchestration-kernel.md +4 -5
- package/dist/builtin-personas/runtime-base.md +8 -4
- package/dist/builtin-personas/spec/PERSONA.md +8 -4
- package/dist/builtin-personas/spec/orchestrator.md +5 -3
- package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
- package/dist/builtin-views/canvas/core.mjs +586 -0
- package/dist/builtin-views/canvas/text.mjs +58 -0
- package/dist/builtin-views/canvas/tui.mjs +165 -0
- package/dist/builtin-views/canvas/web.jsx +120 -0
- package/dist/builtin-views/git-pr/core.mjs +673 -0
- package/dist/builtin-views/git-pr/text.mjs +84 -0
- package/dist/builtin-views/git-pr/tui.mjs +302 -0
- package/dist/builtin-views/git-pr/web.jsx +216 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
- package/dist/builtin-views/inbox/core.mjs +1273 -0
- package/dist/builtin-views/inbox/text.mjs +73 -0
- package/dist/builtin-views/inbox/tui.mjs +312 -0
- package/dist/builtin-views/inbox/web.jsx +188 -0
- package/dist/builtin-views/linkedin/core.mjs +906 -0
- package/dist/builtin-views/linkedin/text.mjs +69 -0
- package/dist/builtin-views/linkedin/tui.mjs +427 -0
- package/dist/builtin-views/linkedin/web.jsx +206 -0
- package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
- package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
- package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
- package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
- package/dist/cli.js +3 -3
- package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
- package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
- package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
- package/dist/clients/attach/__tests__/git-info.test.js +31 -0
- package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
- package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
- package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +445 -41
- package/dist/clients/attach/auth-pickers.d.ts +9 -0
- package/dist/clients/attach/auth-pickers.js +194 -0
- package/dist/clients/attach/canvas-panels.d.ts +12 -0
- package/dist/clients/attach/canvas-panels.js +78 -0
- package/dist/clients/attach/chat-view.d.ts +27 -2
- package/dist/clients/attach/chat-view.js +98 -9
- package/dist/clients/attach/clipboard-image.d.ts +47 -8
- package/dist/clients/attach/clipboard-image.js +150 -26
- package/dist/clients/attach/clipboard-text.d.ts +3 -0
- package/dist/clients/attach/clipboard-text.js +39 -0
- package/dist/clients/attach/config-load.d.ts +54 -7
- package/dist/clients/attach/config-load.js +99 -3
- package/dist/clients/attach/context-message.d.ts +12 -0
- package/dist/clients/attach/context-message.js +72 -0
- package/dist/clients/attach/git-info.d.ts +20 -0
- package/dist/clients/attach/git-info.js +44 -0
- package/dist/clients/attach/graph-overlay.d.ts +43 -0
- package/dist/clients/attach/graph-overlay.js +306 -0
- package/dist/clients/attach/input-controller.d.ts +81 -13
- package/dist/clients/attach/input-controller.js +307 -67
- package/dist/clients/attach/pickers.d.ts +58 -0
- package/dist/clients/attach/pickers.js +171 -0
- package/dist/clients/attach/slash-commands.d.ts +44 -1
- package/dist/clients/attach/slash-commands.js +250 -27
- package/dist/clients/attach/titled-editor.d.ts +33 -0
- package/dist/clients/attach/titled-editor.js +84 -0
- package/dist/clients/attach/view-socket.d.ts +37 -2
- package/dist/clients/attach/view-socket.js +131 -7
- package/dist/clients/web/dev-server.d.ts +7 -0
- package/dist/clients/web/dev-server.js +59 -0
- package/dist/clients/web/events.d.ts +14 -0
- package/dist/clients/web/events.js +151 -0
- package/dist/clients/web/server.d.ts +18 -0
- package/dist/clients/web/server.js +450 -0
- package/dist/clients/web/web-cmd.d.ts +2 -0
- package/dist/clients/web/web-cmd.js +120 -0
- package/dist/commands/canvas.js +1 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/dashboard.js +5 -1
- package/dist/commands/human/prompts.js +3 -3
- package/dist/commands/human/queue.d.ts +17 -0
- package/dist/commands/human/queue.js +111 -4
- package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
- package/dist/commands/memory/lint.d.ts +5 -4
- package/dist/commands/memory/lint.js +23 -7
- package/dist/commands/memory/read.js +2 -2
- package/dist/commands/memory/shared.d.ts +7 -1
- package/dist/commands/memory/shared.js +18 -3
- package/dist/commands/memory/write.js +20 -7
- package/dist/commands/memory.js +4 -4
- package/dist/commands/node.d.ts +1 -1
- package/dist/commands/node.js +64 -65
- package/dist/commands/pkg.js +1 -2
- package/dist/commands/revive.js +11 -3
- package/dist/commands/sys/feedback.d.ts +1 -0
- package/dist/commands/sys/feedback.js +163 -0
- package/dist/commands/sys/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +4 -2
- package/dist/commands/view-cycle.js +2 -2
- package/dist/commands/view-list.js +8 -8
- package/dist/commands/view-new.js +21 -17
- package/dist/commands/view-pick.js +1 -1
- package/dist/commands/view-run.js +35 -14
- package/dist/commands/view.js +8 -6
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +161 -0
- package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
- package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
- package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
- package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
- package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
- package/dist/core/__tests__/child-death-wake.test.js +56 -111
- package/dist/core/__tests__/close.test.js +14 -20
- package/dist/core/__tests__/context-intro.test.js +19 -19
- package/dist/core/__tests__/daemon-boot.test.js +30 -23
- package/dist/core/__tests__/draw-style.test.js +1 -1
- package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +25 -13
- package/dist/core/__tests__/fork.test.js +22 -5
- package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
- package/dist/core/__tests__/full/cascade-close.test.js +22 -8
- package/dist/core/__tests__/full/detach-focus.test.js +66 -169
- package/dist/core/__tests__/helpers/harness.d.ts +6 -9
- package/dist/core/__tests__/helpers/harness.js +19 -36
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
- package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
- package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
- package/dist/core/__tests__/lifecycle.test.js +1 -19
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
- package/dist/core/__tests__/memory-resolver.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
- package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch-root.test.js +114 -0
- package/dist/core/__tests__/reset.test.js +19 -79
- package/dist/core/__tests__/steer-note.test.js +9 -7
- package/dist/core/__tests__/tmux-surface.test.js +13 -7
- package/dist/core/bootstrap.d.ts +0 -3
- package/dist/core/bootstrap.js +1 -143
- package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
- package/dist/core/canvas/browse/app.js +131 -8
- package/dist/core/canvas/browse/model.d.ts +23 -5
- package/dist/core/canvas/browse/model.js +68 -13
- package/dist/core/canvas/browse/render.d.ts +7 -1
- package/dist/core/canvas/browse/render.js +178 -47
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/focuses.d.ts +4 -4
- package/dist/core/canvas/focuses.js +7 -7
- package/dist/core/canvas/nav-model.d.ts +162 -0
- package/dist/core/canvas/nav-model.js +487 -0
- package/dist/core/canvas/paths.d.ts +7 -0
- package/dist/core/canvas/paths.js +9 -0
- package/dist/core/canvas/render.d.ts +68 -9
- package/dist/core/canvas/render.js +195 -61
- package/dist/core/canvas/types.d.ts +16 -23
- package/dist/core/config.js +1 -4
- package/dist/core/memory-resolver.js +43 -5
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +8 -8
- package/dist/core/runtime/branded-host.d.ts +16 -0
- package/dist/core/runtime/branded-host.js +127 -0
- package/dist/core/runtime/broker-protocol.d.ts +212 -6
- package/dist/core/runtime/broker-sdk.d.ts +0 -12
- package/dist/core/runtime/broker-sdk.js +77 -6
- package/dist/core/runtime/broker.d.ts +5 -1
- package/dist/core/runtime/broker.js +520 -59
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +14 -7
- package/dist/core/runtime/host.d.ts +14 -34
- package/dist/core/runtime/host.js +14 -50
- package/dist/core/runtime/launch.d.ts +12 -9
- package/dist/core/runtime/launch.js +27 -15
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +15 -19
- package/dist/core/runtime/nodes.d.ts +0 -55
- package/dist/core/runtime/nodes.js +6 -74
- package/dist/core/runtime/placement.d.ts +91 -334
- package/dist/core/runtime/placement.js +262 -828
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +24 -6
- package/dist/core/runtime/recap.d.ts +8 -0
- package/dist/core/runtime/recap.js +107 -0
- package/dist/core/runtime/recycle.js +25 -61
- package/dist/core/runtime/reset.d.ts +43 -37
- package/dist/core/runtime/reset.js +131 -218
- package/dist/core/runtime/revive.d.ts +9 -29
- package/dist/core/runtime/revive.js +45 -135
- package/dist/core/runtime/spawn.d.ts +15 -11
- package/dist/core/runtime/spawn.js +168 -136
- package/dist/core/runtime/surface-bg.d.ts +11 -0
- package/dist/core/runtime/surface-bg.js +68 -0
- package/dist/core/runtime/tmux.d.ts +15 -43
- package/dist/core/runtime/tmux.js +38 -80
- package/dist/core/scope.d.ts +1 -2
- package/dist/core/scope.js +1 -5
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
- package/dist/core/skill-sync/builtins.d.ts +42 -0
- package/dist/core/skill-sync/builtins.js +112 -0
- package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
- package/dist/core/skill-sync/claude-plugins.js +71 -0
- package/dist/core/skill-sync/engine.d.ts +42 -0
- package/dist/core/skill-sync/engine.js +633 -0
- package/dist/core/skill-sync/export.d.ts +23 -0
- package/dist/core/skill-sync/export.js +86 -0
- package/dist/core/skill-sync/manifest.d.ts +64 -0
- package/dist/core/skill-sync/manifest.js +181 -0
- package/dist/core/skill-sync/profile.d.ts +76 -0
- package/dist/core/skill-sync/profile.js +173 -0
- package/dist/core/skill-sync/snapshot.d.ts +57 -0
- package/dist/core/skill-sync/snapshot.js +120 -0
- package/dist/core/spawn.d.ts +14 -0
- package/dist/core/spawn.js +29 -9
- package/dist/core/substrate/index.d.ts +2 -2
- package/dist/core/substrate/index.js +7 -7
- package/dist/core/substrate/injected-store.d.ts +10 -0
- package/dist/core/substrate/injected-store.js +55 -0
- package/dist/core/substrate/on-read.js +68 -6
- package/dist/core/substrate/render.d.ts +8 -11
- package/dist/core/substrate/render.js +29 -43
- package/dist/core/substrate/schema.d.ts +16 -11
- package/dist/core/substrate/schema.js +32 -31
- package/dist/core/tui/host.d.ts +12 -4
- package/dist/core/tui/host.js +280 -149
- package/dist/core/view/bridge.d.ts +10 -0
- package/dist/core/view/bridge.js +31 -0
- package/dist/core/view/chrome.d.ts +9 -0
- package/dist/core/view/chrome.js +22 -0
- package/dist/core/view/contract.d.ts +171 -0
- package/dist/core/view/contract.js +23 -0
- package/dist/core/view/loader.d.ts +31 -0
- package/dist/core/view/loader.js +188 -0
- package/dist/core/view/transport-local.d.ts +7 -0
- package/dist/core/view/transport-local.js +70 -0
- package/dist/core/view/transport.d.ts +4 -0
- package/dist/core/view/transport.js +15 -0
- package/dist/daemon/crtrd.d.ts +38 -27
- package/dist/daemon/crtrd.js +303 -376
- package/dist/daemon/manage.js +6 -1
- package/dist/index.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
- package/dist/pi-extensions/canvas-context-intro.js +4 -4
- package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
- package/dist/pi-extensions/canvas-nav.js +35 -385
- package/dist/pi-extensions/canvas-recap.d.ts +37 -0
- package/dist/pi-extensions/canvas-recap.js +236 -0
- package/dist/pi-extensions/canvas-resume.js +2 -1
- package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
- package/dist/pi-extensions/canvas-stophook.js +119 -103
- package/dist/pi-extensions/canvas-view.js +2 -1
- package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
- package/dist/pi-extensions/widget-order-bus.js +34 -0
- package/dist/prompts/view.d.ts +2 -2
- package/dist/prompts/view.js +145 -73
- package/dist/types.d.ts +1 -6
- package/dist/types.js +1 -3
- package/dist/web/ViewChrome.d.ts +7 -0
- package/dist/web/ViewChrome.js +28 -0
- package/dist/web/ViewPane.d.ts +39 -0
- package/dist/web/ViewPane.js +48 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +16 -0
- package/dist/web/runtime.d.ts +39 -0
- package/dist/web/runtime.js +133 -0
- package/dist/web/states.d.ts +24 -0
- package/dist/web/states.js +24 -0
- package/dist/web/transport-http.d.ts +5 -0
- package/dist/web/transport-http.js +28 -0
- package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
- package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
- package/dist/web-client/index.html +13 -0
- package/package.json +20 -6
- package/dist/builtin-views/canvas/client.mjs +0 -303
- package/dist/builtin-views/canvas/view.mjs +0 -576
- package/dist/builtin-views/git-pr/client.mjs +0 -440
- package/dist/builtin-views/git-pr/view.mjs +0 -675
- package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
- package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
- package/dist/builtin-views/inbox/view.mjs +0 -889
- package/dist/builtin-views/linkedin/client.mjs +0 -610
- package/dist/builtin-views/linkedin/view.mjs +0 -1171
- package/dist/commands/pkg/bridge.d.ts +0 -1
- package/dist/commands/pkg/bridge.js +0 -137
- package/dist/commands/skill/author.d.ts +0 -3
- package/dist/commands/skill/author.js +0 -140
- package/dist/commands/skill/shared.d.ts +0 -3
- package/dist/commands/skill/shared.js +0 -19
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.js +0 -21
- package/dist/commands/tmux-spread.d.ts +0 -2
- package/dist/commands/tmux-spread.js +0 -144
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
- package/dist/core/__tests__/full/placement-focus.test.js +0 -309
- package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
- package/dist/core/__tests__/full/placement-revive.test.js +0 -238
- package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
- package/dist/core/__tests__/home-session.test.js +0 -186
- package/dist/core/__tests__/relaunch.test.js +0 -335
- package/dist/core/bridge-map.d.ts +0 -19
- package/dist/core/bridge-map.js +0 -73
- package/dist/core/tui/contract.d.ts +0 -83
- package/dist/core/tui/contract.js +0 -8
- package/dist/core/tui/loader.d.ts +0 -16
- package/dist/core/tui/loader.js +0 -94
- package/dist/prompts/skill.d.ts +0 -2
- package/dist/prompts/skill.js +0 -650
- /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
- /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// broker.ts — the headless node broker (design §4, §5; plan T4).
|
|
2
2
|
//
|
|
3
|
-
// One broker process per
|
|
3
|
+
// One broker process per node — the SOLE host. It hosts ONE pi engine IN-PROCESS
|
|
4
4
|
// via the SDK (createAgentSession), is the SOLE writer of the node's session
|
|
5
5
|
// `.jsonl`, listens on `nodeDir(id)/view.sock`, fans the single engine event
|
|
6
6
|
// stream out to N viewers, serializes a single controller's drive commands, and
|
|
7
|
-
// routes blocking extension dialogs.
|
|
8
|
-
//
|
|
7
|
+
// routes blocking extension dialogs. The engine has no terminal of its own; a
|
|
8
|
+
// tmux pane (or web tab) is only a viewer of this socket. It
|
|
9
|
+
// runs one turn-cycle and, when the engine settles (the stophook calls
|
|
9
10
|
// ctx.shutdown(), or the engine goes idle), disposes the engine and exits 0 —
|
|
10
11
|
// the daemon then routes revival per the intent the bound stophook set.
|
|
11
12
|
//
|
|
@@ -16,10 +17,13 @@
|
|
|
16
17
|
// the real canvas-stophook fires session_start).
|
|
17
18
|
import { createServer } from 'node:net';
|
|
18
19
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
19
22
|
import { join } from 'node:path';
|
|
20
23
|
import { randomUUID } from 'node:crypto';
|
|
21
24
|
import { getAgentDir, initTheme, } from '@earendil-works/pi-coding-agent';
|
|
22
25
|
import { jobDir, nodeDir } from '../canvas/paths.js';
|
|
26
|
+
import { getNode, updateNode } from '../canvas/index.js';
|
|
23
27
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
24
28
|
import { piInvocationToSdkConfig } from './launch.js';
|
|
25
29
|
import { assertEngineVersion, loadBrokerEngine } from './broker-sdk.js';
|
|
@@ -83,14 +87,13 @@ export async function runBroker(nodeId) {
|
|
|
83
87
|
// front-door recursion guard (runtime/CLAUDE.md fork-bomb invariant). The host
|
|
84
88
|
// (T6) also sets this; we set it again so the broker is self-sufficient.
|
|
85
89
|
process.env[FRONT_DOOR_ENV] = '1';
|
|
86
|
-
// The broker is PANELESS
|
|
87
|
-
// parent inherits the parent's TMUX_PANE/TMUX in its env (
|
|
88
|
-
// {...process.env})
|
|
89
|
-
// (
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// the daemon then respawns the node as a broker).
|
|
90
|
+
// The broker is PANELESS and detached, but a broker spawned by a tmux-resident
|
|
91
|
+
// parent inherits the parent's TMUX_PANE/TMUX in its env (the launcher spawns
|
|
92
|
+
// with {...process.env}). Left in place, every `crtr` command the engine runs
|
|
93
|
+
// (bash tool, goal-capture naming, a child spawn) would resolve `currentTmux()`
|
|
94
|
+
// to the PARENT's pane/session and act there — e.g. open viewers beside, or
|
|
95
|
+
// close, the parent's pane. Strip both so this broker's children see no tmux
|
|
96
|
+
// context and take their paneless path.
|
|
94
97
|
delete process.env['TMUX_PANE'];
|
|
95
98
|
delete process.env['TMUX'];
|
|
96
99
|
// 1. Version tripwire + load the launch recipe the host serialized.
|
|
@@ -110,12 +113,6 @@ export async function runBroker(nodeId) {
|
|
|
110
113
|
const launchFile = join(dir, 'broker-launch.json');
|
|
111
114
|
const inv = JSON.parse(readFileSync(launchFile, 'utf8'));
|
|
112
115
|
const cfg = piInvocationToSdkConfig(inv); // also merges inv.env
|
|
113
|
-
// Fork-on-spawn is the runtime.fork() path (createAgentSessionRuntime), not
|
|
114
|
-
// wired headless in Phase 3. Fail loud rather than silently mis-resume.
|
|
115
|
-
if (cfg.forkFrom !== undefined && cfg.forkFrom !== '') {
|
|
116
|
-
throw new Error(`[broker] --fork is not supported by the headless broker in Phase 3 ` +
|
|
117
|
-
`(forkFrom=${cfg.forkFrom}); the SDK fork path is createAgentSessionRuntime.fork()`);
|
|
118
|
-
}
|
|
119
116
|
// 2–4. Build the engine session via the SERVICES path (C3) — see
|
|
120
117
|
// buildBrokerSession below. Register it so the FATAL exit path (M3) can
|
|
121
118
|
// dispose it and reap detached bash children.
|
|
@@ -157,12 +154,25 @@ export async function runBroker(nodeId) {
|
|
|
157
154
|
let controllerId = null;
|
|
158
155
|
let disposed = false;
|
|
159
156
|
let server;
|
|
157
|
+
// Liveness-aware: a controllerId whose client's transport is already gone counts
|
|
158
|
+
// as NO controller, so control self-heals the instant a controller's peer departs
|
|
159
|
+
// — WITHOUT waiting for the socket 'close' event. On a unix socket a peer
|
|
160
|
+
// destroy() delivers EOF (readableEnded) promptly, but the matching 'close' can
|
|
161
|
+
// lag arbitrarily while undrainable pending writes to the gone peer flush; under
|
|
162
|
+
// load that lag stranded control on a dead client and froze admission (a fresh
|
|
163
|
+
// controller hello was denied for the whole window — the one-writer reattach
|
|
164
|
+
// deadlock the G9 gate locks). Treating an ended/destroyed/unwritable holder as
|
|
165
|
+
// free closes that window at every read of the controller (admission included).
|
|
160
166
|
const controllerClient = () => {
|
|
161
167
|
if (controllerId === null)
|
|
162
168
|
return null;
|
|
163
|
-
for (const c of clients)
|
|
164
|
-
if (c.id
|
|
165
|
-
|
|
169
|
+
for (const c of clients) {
|
|
170
|
+
if (c.id !== controllerId)
|
|
171
|
+
continue;
|
|
172
|
+
if (c.socket.destroyed || c.socket.readableEnded || !c.socket.writable)
|
|
173
|
+
return null;
|
|
174
|
+
return c;
|
|
175
|
+
}
|
|
166
176
|
return null;
|
|
167
177
|
};
|
|
168
178
|
// Persist viewer presence to job/attach.json on every viewer state change
|
|
@@ -309,8 +319,42 @@ export async function runBroker(nodeId) {
|
|
|
309
319
|
persistAttachState(); // every control change is a viewer-state change
|
|
310
320
|
broadcast({ type: 'control_changed', controller_id: controllerId });
|
|
311
321
|
};
|
|
322
|
+
// Persist a live model switch into the node's durable launch recipe so it
|
|
323
|
+
// survives a yield/revive. pi's `/model` (→ set_model/cycle_model) only
|
|
324
|
+
// mutates the in-memory engine; without this the node reverts to its
|
|
325
|
+
// persona/spawn model on the next revive. We write the full `provider/id`
|
|
326
|
+
// (which normalizeModel passes through unchanged) to BOTH model_override (so
|
|
327
|
+
// polymorphs preserve it via buildLaunchSpec) and launch.model (the recipe
|
|
328
|
+
// buildPiArgv replays on revive). Best-effort: a degenerate/ephemeral node
|
|
329
|
+
// with no canvas row just skips persistence. CRTR_NODE_ID is set in the
|
|
330
|
+
// broker's own env (merged by piInvocationToSdkConfig at boot).
|
|
331
|
+
const persistModelChoice = () => {
|
|
332
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
333
|
+
const m = session.model;
|
|
334
|
+
if (nodeId === undefined || nodeId === '' || m === null || m === undefined)
|
|
335
|
+
return;
|
|
336
|
+
const spec = `${m.provider}/${m.id}`;
|
|
337
|
+
try {
|
|
338
|
+
const meta = getNode(nodeId);
|
|
339
|
+
if (meta === null)
|
|
340
|
+
return;
|
|
341
|
+
const launch = meta.launch !== undefined ? { ...meta.launch, model: spec } : undefined;
|
|
342
|
+
updateNode(nodeId, { model_override: spec, ...(launch !== undefined ? { launch } : {}) });
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Persistence is best-effort; the live switch already succeeded.
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
// pi emits no AgentSessionEvent for a model switch, so after a successful
|
|
349
|
+
// set_model/cycle_model the broker announces it itself — otherwise the new
|
|
350
|
+
// model reaches no viewer (the requester gets only a bare ack) and every
|
|
351
|
+
// footer shows the stale model until the next unrelated event.
|
|
352
|
+
const broadcastModelChanged = () => {
|
|
353
|
+
persistModelChoice();
|
|
354
|
+
broadcast({ type: 'model_changed', model: session.model?.id });
|
|
355
|
+
};
|
|
312
356
|
const buildSnapshot = () => ({
|
|
313
|
-
messages: session
|
|
357
|
+
messages: snapshotMessages(session),
|
|
314
358
|
stats: session.getSessionStats(),
|
|
315
359
|
state: {
|
|
316
360
|
sessionId: session.sessionId,
|
|
@@ -339,6 +383,7 @@ export async function runBroker(nodeId) {
|
|
|
339
383
|
role: client.role,
|
|
340
384
|
controller_id: controllerId,
|
|
341
385
|
pending_dialog: first !== undefined ? first.request : null,
|
|
386
|
+
agentDir: getAgentDir(),
|
|
342
387
|
}, true);
|
|
343
388
|
};
|
|
344
389
|
// T4 re-route on become-controller: a dialog raised while a prior controller was
|
|
@@ -407,6 +452,7 @@ export async function runBroker(nodeId) {
|
|
|
407
452
|
controller: controllerClient,
|
|
408
453
|
forward: (client, request) => sendFrame(client, request),
|
|
409
454
|
pending: pendingDialogs,
|
|
455
|
+
broadcast,
|
|
410
456
|
});
|
|
411
457
|
// -------------------------------------------------------------------------
|
|
412
458
|
// 3+4 (plan): bind the FULL canvas extensions (mode 'print') AND fan the single
|
|
@@ -520,18 +566,29 @@ export async function runBroker(nodeId) {
|
|
|
520
566
|
// -------------------------------------------------------------------------
|
|
521
567
|
/** Reject a non-controller for a controller-only op. Returns true when rejected
|
|
522
568
|
* (the caller should `break`). */
|
|
523
|
-
const notController = (client, what) => {
|
|
569
|
+
const notController = (client, what, id) => {
|
|
524
570
|
if (client.id === controllerId)
|
|
525
571
|
return false;
|
|
526
572
|
sendFrame(client, {
|
|
527
573
|
type: 'error',
|
|
528
574
|
code: 'not_controller',
|
|
529
575
|
message: `only the controlling client may ${what}`,
|
|
576
|
+
// M1: echo a correlated request's id so its pending-by-id promise rejects
|
|
577
|
+
// rather than hanging (e.g. a non-controller `dequeue`).
|
|
578
|
+
...(id !== undefined ? { id } : {}),
|
|
530
579
|
});
|
|
531
580
|
return true;
|
|
532
581
|
};
|
|
533
582
|
const ackTo = (client, op, ok = true, detail) => sendFrame(client, { type: 'ack', for: op, ok, ...(detail !== undefined ? { detail } : {}) });
|
|
534
|
-
const engineErrorTo = (client) => (err) => sendFrame(client, {
|
|
583
|
+
const engineErrorTo = (client, id) => (err) => sendFrame(client, {
|
|
584
|
+
type: 'error',
|
|
585
|
+
code: 'engine_error',
|
|
586
|
+
message: String(err),
|
|
587
|
+
// M1: a correlated read-op / dequeue failure echoes its id so the viewer's
|
|
588
|
+
// pending-by-id promise rejects instead of hanging; uncorrelated callers
|
|
589
|
+
// pass no id (unchanged behavior).
|
|
590
|
+
...(id !== undefined ? { id } : {}),
|
|
591
|
+
});
|
|
535
592
|
/** Resolve a `provider/id` model spec against the LIVE services registry (which
|
|
536
593
|
* carries any extension-registered providers). Pure: returns undefined on a
|
|
537
594
|
* malformed spec or an unknown model. */
|
|
@@ -541,6 +598,63 @@ export async function runBroker(nodeId) {
|
|
|
541
598
|
return undefined;
|
|
542
599
|
return services.modelRegistry.find(spec.slice(0, slash), spec.slice(slash + 1));
|
|
543
600
|
};
|
|
601
|
+
/** Resolve a model from a free-text query — `/model opus`, `/model fable`. Tries
|
|
602
|
+
* the exact `provider/id` spec first; failing that, searches the registry the
|
|
603
|
+
* way the picker's search box does (case-insensitive substring on the model id
|
|
604
|
+
* and the `provider/id` key) and returns the BEST match, preferring authed/
|
|
605
|
+
* available models. Returns undefined only when nothing matches. */
|
|
606
|
+
const resolveModelQuery = (query) => {
|
|
607
|
+
const exact = findModelSpec(query);
|
|
608
|
+
if (exact)
|
|
609
|
+
return exact;
|
|
610
|
+
const q = query.trim().toLowerCase();
|
|
611
|
+
if (q === '')
|
|
612
|
+
return undefined;
|
|
613
|
+
let all;
|
|
614
|
+
try {
|
|
615
|
+
all = services.modelRegistry.getAll();
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
let availableKeys;
|
|
621
|
+
try {
|
|
622
|
+
availableKeys = new Set(services.modelRegistry.getAvailable().map(modelKey));
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
availableKeys = new Set();
|
|
626
|
+
}
|
|
627
|
+
// Rank: exact id (100) > exact provider/id (90) > id prefix (80) > id
|
|
628
|
+
// substring (60) > provider/id substring (40); +5 if the model is available.
|
|
629
|
+
const rank = (m) => {
|
|
630
|
+
const id = m.id.toLowerCase();
|
|
631
|
+
const key = modelKey(m).toLowerCase();
|
|
632
|
+
let base = -1;
|
|
633
|
+
if (id === q)
|
|
634
|
+
base = 100;
|
|
635
|
+
else if (key === q)
|
|
636
|
+
base = 90;
|
|
637
|
+
else if (id.startsWith(q))
|
|
638
|
+
base = 80;
|
|
639
|
+
else if (id.includes(q))
|
|
640
|
+
base = 60;
|
|
641
|
+
else if (key.includes(q))
|
|
642
|
+
base = 40;
|
|
643
|
+
else
|
|
644
|
+
return -1;
|
|
645
|
+
return base + (availableKeys.has(modelKey(m)) ? 5 : 0);
|
|
646
|
+
};
|
|
647
|
+
let best;
|
|
648
|
+
let bestRank = 0;
|
|
649
|
+
for (const m of all) {
|
|
650
|
+
const r = rank(m);
|
|
651
|
+
if (r > bestRank) {
|
|
652
|
+
bestRank = r;
|
|
653
|
+
best = m;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return best;
|
|
657
|
+
};
|
|
544
658
|
/** The merged command list for `get_commands` (C6/M9): the engine's registered
|
|
545
659
|
* extension + skill commands and file-based prompt templates, MERGED with the
|
|
546
660
|
* vendored BUILTIN_SLASH_COMMANDS (RPC omits builtins). Returned to the viewer
|
|
@@ -569,6 +683,128 @@ export async function runBroker(nodeId) {
|
|
|
569
683
|
}
|
|
570
684
|
return out;
|
|
571
685
|
};
|
|
686
|
+
// -------------------------------------------------------------------------
|
|
687
|
+
// Read-op data builders (operator-view picker payloads, §5 Unit A). Each is a
|
|
688
|
+
// PURE getter read against the live engine session — the data a native pi
|
|
689
|
+
// picker's constructor needs, serialized for the viewer. Not controller-gated
|
|
690
|
+
// (read-only, like get_commands), so the web bridge's observer connection can
|
|
691
|
+
// populate pickers too.
|
|
692
|
+
// -------------------------------------------------------------------------
|
|
693
|
+
const toModelRef = (m) => m === undefined ? null : { provider: m.provider, id: m.id };
|
|
694
|
+
const modelKey = (m) => `${m.provider}/${m.id}`;
|
|
695
|
+
const buildListModelsData = (id) => {
|
|
696
|
+
const reg = session.modelRegistry;
|
|
697
|
+
let availableIds = [];
|
|
698
|
+
try {
|
|
699
|
+
availableIds = reg.getAvailable().map(modelKey);
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
/* getAvailable touches auth config; degrade to "none known available" */
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
type: 'data',
|
|
706
|
+
id,
|
|
707
|
+
kind: 'list_models',
|
|
708
|
+
models: reg.getAll(),
|
|
709
|
+
current: toModelRef(session.model),
|
|
710
|
+
availableIds,
|
|
711
|
+
scopedModels: session.scopedModels.map((s) => ({
|
|
712
|
+
model: s.model,
|
|
713
|
+
thinkingLevel: s.thinkingLevel,
|
|
714
|
+
})),
|
|
715
|
+
enabledModelIds: session.settingsManager.getEnabledModels() ?? null,
|
|
716
|
+
};
|
|
717
|
+
};
|
|
718
|
+
const buildScopedModelsData = (id) => ({
|
|
719
|
+
type: 'data',
|
|
720
|
+
id,
|
|
721
|
+
kind: 'list_scoped_models',
|
|
722
|
+
allModels: session.modelRegistry.getAll(),
|
|
723
|
+
enabledModelIds: session.settingsManager.getEnabledModels() ?? null,
|
|
724
|
+
});
|
|
725
|
+
const buildGetTreeData = (id) => {
|
|
726
|
+
const sm = session.sessionManager;
|
|
727
|
+
const forkPoints = session.getUserMessagesForForking().map((u) => ({
|
|
728
|
+
id: u.entryId,
|
|
729
|
+
text: u.text,
|
|
730
|
+
timestamp: sm.getEntry(u.entryId)?.timestamp,
|
|
731
|
+
}));
|
|
732
|
+
return {
|
|
733
|
+
type: 'data',
|
|
734
|
+
id,
|
|
735
|
+
kind: 'get_tree',
|
|
736
|
+
tree: sm.getTree(),
|
|
737
|
+
currentLeafId: sm.getLeafId(),
|
|
738
|
+
forkPoints,
|
|
739
|
+
};
|
|
740
|
+
};
|
|
741
|
+
const buildSettingsData = (id) => {
|
|
742
|
+
const sm = session.settingsManager;
|
|
743
|
+
// Theme names are enumerable broker-side via the resource loader. The theme
|
|
744
|
+
// SUBMENU itself is viewer-local (theme is a viewer-only concern), but the
|
|
745
|
+
// settings menu still shows currentTheme/availableThemes, so include them.
|
|
746
|
+
let availableThemes = [];
|
|
747
|
+
try {
|
|
748
|
+
availableThemes = session.resourceLoader
|
|
749
|
+
.getThemes()
|
|
750
|
+
.themes.map((t) => t.name ?? '')
|
|
751
|
+
.filter((n) => n !== '');
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
/* loader without themes — viewer falls back to its local theme registry */
|
|
755
|
+
}
|
|
756
|
+
const settings = {
|
|
757
|
+
autoCompact: session.autoCompactionEnabled,
|
|
758
|
+
showImages: sm.getShowImages(),
|
|
759
|
+
imageWidthCells: sm.getImageWidthCells(),
|
|
760
|
+
autoResizeImages: sm.getImageAutoResize(),
|
|
761
|
+
blockImages: sm.getBlockImages(),
|
|
762
|
+
enableSkillCommands: sm.getEnableSkillCommands(),
|
|
763
|
+
steeringMode: session.steeringMode,
|
|
764
|
+
followUpMode: session.followUpMode,
|
|
765
|
+
transport: sm.getTransport(),
|
|
766
|
+
httpIdleTimeoutMs: sm.getHttpIdleTimeoutMs(),
|
|
767
|
+
thinkingLevel: session.thinkingLevel,
|
|
768
|
+
availableThinkingLevels: session.getAvailableThinkingLevels(),
|
|
769
|
+
currentTheme: sm.getTheme() ?? '',
|
|
770
|
+
availableThemes,
|
|
771
|
+
hideThinkingBlock: sm.getHideThinkingBlock(),
|
|
772
|
+
collapseChangelog: sm.getCollapseChangelog(),
|
|
773
|
+
enableInstallTelemetry: sm.getEnableInstallTelemetry(),
|
|
774
|
+
doubleEscapeAction: sm.getDoubleEscapeAction(),
|
|
775
|
+
treeFilterMode: sm.getTreeFilterMode(),
|
|
776
|
+
showHardwareCursor: sm.getShowHardwareCursor(),
|
|
777
|
+
editorPaddingX: sm.getEditorPaddingX(),
|
|
778
|
+
autocompleteMaxVisible: sm.getAutocompleteMaxVisible(),
|
|
779
|
+
quietStartup: sm.getQuietStartup(),
|
|
780
|
+
clearOnShrink: sm.getClearOnShrink(),
|
|
781
|
+
showTerminalProgress: sm.getShowTerminalProgress(),
|
|
782
|
+
warnings: sm.getWarnings(),
|
|
783
|
+
defaultProjectTrust: sm.getDefaultProjectTrust(),
|
|
784
|
+
autoRetry: session.autoRetryEnabled,
|
|
785
|
+
model: toModelRef(session.model),
|
|
786
|
+
};
|
|
787
|
+
return { type: 'data', id, kind: 'get_settings', settings };
|
|
788
|
+
};
|
|
789
|
+
const buildSessionsData = async (id, scope) => {
|
|
790
|
+
const sm = session.sessionManager;
|
|
791
|
+
const raw = scope === 'all'
|
|
792
|
+
? await engine.SessionManager.listAll()
|
|
793
|
+
: await engine.SessionManager.list(sm.getCwd(), sm.getSessionDir());
|
|
794
|
+
const sessions = raw.map((s) => ({
|
|
795
|
+
...s,
|
|
796
|
+
created: s.created.toISOString(),
|
|
797
|
+
modified: s.modified.toISOString(),
|
|
798
|
+
}));
|
|
799
|
+
return {
|
|
800
|
+
type: 'data',
|
|
801
|
+
id,
|
|
802
|
+
kind: 'list_sessions',
|
|
803
|
+
scope,
|
|
804
|
+
sessions,
|
|
805
|
+
currentSessionFile: session.sessionFile,
|
|
806
|
+
};
|
|
807
|
+
};
|
|
572
808
|
/** Run a session-replacing op (new_session/switch_session/fork). The runtime
|
|
573
809
|
* rebinds extensions + re-subscribes via setRebindSession before it resolves,
|
|
574
810
|
* so on success we just re-snapshot every viewer onto the new session. */
|
|
@@ -594,9 +830,11 @@ export async function runBroker(nodeId) {
|
|
|
594
830
|
case 'hello': {
|
|
595
831
|
client.id = frame.client_id;
|
|
596
832
|
client.helloed = true;
|
|
597
|
-
// First-attach-wins
|
|
598
|
-
//
|
|
599
|
-
|
|
833
|
+
// First-attach-wins (§5.3), but only against a LIVE controller: admit as
|
|
834
|
+
// controller iff none is currently held by a live client (controllerClient
|
|
835
|
+
// is liveness-aware, so a controllerId stranded on a departed peer reads as
|
|
836
|
+
// free here). Otherwise read-only observer.
|
|
837
|
+
if (frame.role === 'controller' && controllerClient() === null) {
|
|
600
838
|
client.role = 'controller';
|
|
601
839
|
controllerId = client.id;
|
|
602
840
|
}
|
|
@@ -631,19 +869,24 @@ export async function runBroker(nodeId) {
|
|
|
631
869
|
break;
|
|
632
870
|
}
|
|
633
871
|
case 'request_control': {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
872
|
+
// §D preemptive handoff (last-requester-wins): a control request ALWAYS
|
|
873
|
+
// succeeds, reassigning control to the requester and demoting the prior
|
|
874
|
+
// controller to observer. This makes a tmux pane and a web tab true peers —
|
|
875
|
+
// either can take control of a node the other currently drives — which is
|
|
876
|
+
// the broker-is-the-host invariant (the prior cooperative-only model could
|
|
877
|
+
// not preempt an idle/abandoned controller, the common case). The prior
|
|
878
|
+
// controller demotes itself on receiving the control_changed broadcast
|
|
879
|
+
// (attach-cmd.ts already does this; the web client implements the same
|
|
880
|
+
// rule). Idempotent when the requester already holds control.
|
|
881
|
+
if (client.id === controllerId)
|
|
882
|
+
break;
|
|
883
|
+
const prior = controllerClient();
|
|
884
|
+
if (prior !== null)
|
|
885
|
+
prior.role = 'observer';
|
|
886
|
+
controllerId = client.id;
|
|
887
|
+
client.role = 'controller';
|
|
888
|
+
broadcastControlChanged();
|
|
889
|
+
reroutePendingDialogsTo(client); // T4: hand the new controller pending dialogs
|
|
647
890
|
break;
|
|
648
891
|
}
|
|
649
892
|
case 'release_control': {
|
|
@@ -663,7 +906,9 @@ export async function runBroker(nodeId) {
|
|
|
663
906
|
break;
|
|
664
907
|
let model;
|
|
665
908
|
try {
|
|
666
|
-
|
|
909
|
+
// Accept both an exact `provider/id` (from the picker) and a free-text
|
|
910
|
+
// query (`/model opus`) — resolveModelQuery falls back to a search match.
|
|
911
|
+
model = resolveModelQuery(frame.model);
|
|
667
912
|
}
|
|
668
913
|
catch (err) {
|
|
669
914
|
// N2: registry.find should never throw on the real SDK, but a degenerate
|
|
@@ -675,17 +920,53 @@ export async function runBroker(nodeId) {
|
|
|
675
920
|
sendFrame(client, {
|
|
676
921
|
type: 'error',
|
|
677
922
|
code: 'engine_error',
|
|
678
|
-
message: `model '${frame.model}'
|
|
923
|
+
message: `no model matching '${frame.model}' in the registry`,
|
|
679
924
|
});
|
|
680
925
|
break;
|
|
681
926
|
}
|
|
682
|
-
void session
|
|
927
|
+
void session
|
|
928
|
+
.setModel(model)
|
|
929
|
+
.then(() => {
|
|
930
|
+
ackTo(client, 'set_model');
|
|
931
|
+
broadcastModelChanged();
|
|
932
|
+
})
|
|
933
|
+
.catch(engineErrorTo(client));
|
|
683
934
|
break;
|
|
684
935
|
}
|
|
685
936
|
case 'cycle_model': {
|
|
686
937
|
if (notController(client, 'cycle the model'))
|
|
687
938
|
break;
|
|
688
|
-
void session
|
|
939
|
+
void session
|
|
940
|
+
.cycleModel(frame.direction)
|
|
941
|
+
.then(() => {
|
|
942
|
+
ackTo(client, 'cycle_model');
|
|
943
|
+
broadcastModelChanged();
|
|
944
|
+
})
|
|
945
|
+
.catch(engineErrorTo(client));
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
case 'cycle_thinking': {
|
|
949
|
+
if (notController(client, 'cycle the thinking level'))
|
|
950
|
+
break;
|
|
951
|
+
try {
|
|
952
|
+
session.cycleThinkingLevel();
|
|
953
|
+
ackTo(client, 'cycle_thinking');
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
engineErrorTo(client)(err);
|
|
957
|
+
}
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
case 'dequeue': {
|
|
961
|
+
if (notController(client, 'dequeue messages', frame.id))
|
|
962
|
+
break;
|
|
963
|
+
try {
|
|
964
|
+
const { steering, followUp } = session.clearQueue();
|
|
965
|
+
sendFrame(client, { type: 'data', id: frame.id, kind: 'dequeue', steering, followUp });
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
engineErrorTo(client, frame.id)(err);
|
|
969
|
+
}
|
|
689
970
|
break;
|
|
690
971
|
}
|
|
691
972
|
case 'set_thinking_level': {
|
|
@@ -761,12 +1042,68 @@ export async function runBroker(nodeId) {
|
|
|
761
1042
|
}
|
|
762
1043
|
break;
|
|
763
1044
|
}
|
|
1045
|
+
// --- read-op picker data (§5 Unit A) ----------------------------------
|
|
1046
|
+
// NOT controller-gated: read-only, like get_commands. The web bridge's
|
|
1047
|
+
// observer connection populates pickers through these too.
|
|
1048
|
+
case 'list_models': {
|
|
1049
|
+
try {
|
|
1050
|
+
sendFrame(client, buildListModelsData(frame.id));
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
engineErrorTo(client, frame.id)(err);
|
|
1054
|
+
}
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
case 'list_scoped_models': {
|
|
1058
|
+
try {
|
|
1059
|
+
sendFrame(client, buildScopedModelsData(frame.id));
|
|
1060
|
+
}
|
|
1061
|
+
catch (err) {
|
|
1062
|
+
engineErrorTo(client, frame.id)(err);
|
|
1063
|
+
}
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
case 'get_tree': {
|
|
1067
|
+
try {
|
|
1068
|
+
sendFrame(client, buildGetTreeData(frame.id));
|
|
1069
|
+
}
|
|
1070
|
+
catch (err) {
|
|
1071
|
+
engineErrorTo(client, frame.id)(err);
|
|
1072
|
+
}
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
case 'get_settings': {
|
|
1076
|
+
try {
|
|
1077
|
+
sendFrame(client, buildSettingsData(frame.id));
|
|
1078
|
+
}
|
|
1079
|
+
catch (err) {
|
|
1080
|
+
engineErrorTo(client, frame.id)(err);
|
|
1081
|
+
}
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
case 'list_sessions': {
|
|
1085
|
+
// Session listing reads the session dir off disk — async; reply when it
|
|
1086
|
+
// resolves (or relay the error, correlated by id so the viewer doesn't hang).
|
|
1087
|
+
void buildSessionsData(frame.id, frame.scope ?? 'cwd')
|
|
1088
|
+
.then((d) => sendFrame(client, d))
|
|
1089
|
+
.catch(engineErrorTo(client, frame.id));
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
764
1092
|
case 'navigate_tree': {
|
|
765
1093
|
if (notController(client, 'navigate the session tree'))
|
|
766
1094
|
break;
|
|
1095
|
+
// navigateTree rewinds IN-PLACE (same session file, new leaf) and emits no
|
|
1096
|
+
// relayed event, so every viewer must be re-snapshotted onto the rewound
|
|
1097
|
+
// transcript — same reWelcomeAll the session-replacing ops use. The ack's
|
|
1098
|
+
// detail carries the navigated-to user message's text (pi parity: the
|
|
1099
|
+
// interactive tree navigator restores it to the editor for re-editing).
|
|
767
1100
|
void session
|
|
768
1101
|
.navigateTree(frame.targetId, frame.options)
|
|
769
|
-
.then((r) =>
|
|
1102
|
+
.then((r) => {
|
|
1103
|
+
if (!r.cancelled)
|
|
1104
|
+
reWelcomeAll();
|
|
1105
|
+
ackTo(client, 'navigate_tree', !r.cancelled, r.editorText);
|
|
1106
|
+
})
|
|
770
1107
|
.catch(engineErrorTo(client));
|
|
771
1108
|
break;
|
|
772
1109
|
}
|
|
@@ -814,6 +1151,74 @@ export async function runBroker(nodeId) {
|
|
|
814
1151
|
runReplacement(client, 'fork', (rt) => rt.fork(frame.entryId));
|
|
815
1152
|
break;
|
|
816
1153
|
}
|
|
1154
|
+
case 'clone': {
|
|
1155
|
+
if (notController(client, 'clone the session'))
|
|
1156
|
+
break;
|
|
1157
|
+
const sm = session.sessionManager;
|
|
1158
|
+
const leafId = sm.getLeafId();
|
|
1159
|
+
if (leafId === null) {
|
|
1160
|
+
ackTo(client, 'clone', false, 'no current leaf to clone from');
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
let newPath;
|
|
1164
|
+
try {
|
|
1165
|
+
newPath = sm.createBranchedSession(leafId);
|
|
1166
|
+
}
|
|
1167
|
+
catch (err) {
|
|
1168
|
+
engineErrorTo(client)(err);
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
if (newPath === undefined) {
|
|
1172
|
+
ackTo(client, 'clone', false, 'createBranchedSession returned no path');
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
runReplacement(client, 'clone', (rt) => rt.switchSession(newPath));
|
|
1176
|
+
break;
|
|
1177
|
+
}
|
|
1178
|
+
case 'share': {
|
|
1179
|
+
if (notController(client, 'share the session'))
|
|
1180
|
+
break;
|
|
1181
|
+
const tmpPath = join(tmpdir(), `pi-share-${Date.now()}.html`);
|
|
1182
|
+
void session.exportToHtml(tmpPath)
|
|
1183
|
+
.then(() => new Promise((resolve, reject) => {
|
|
1184
|
+
execFile('gh', ['gist', 'create', '--secret', tmpPath], { timeout: 30_000, maxBuffer: 1024 * 1024 }, (err, stdout) => {
|
|
1185
|
+
try {
|
|
1186
|
+
if (existsSync(tmpPath))
|
|
1187
|
+
unlinkSync(tmpPath);
|
|
1188
|
+
}
|
|
1189
|
+
catch { /* cleanup */ }
|
|
1190
|
+
if (err) {
|
|
1191
|
+
reject(err);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
resolve(stdout.trim());
|
|
1195
|
+
});
|
|
1196
|
+
}))
|
|
1197
|
+
.then((url) => {
|
|
1198
|
+
ackTo(client, 'share', true, url);
|
|
1199
|
+
})
|
|
1200
|
+
.catch((err) => {
|
|
1201
|
+
const msg = String(err?.message ?? err);
|
|
1202
|
+
ackTo(client, 'share', false, msg.includes('not found') || msg.includes('gh: command not found') || msg.includes('ENOENT')
|
|
1203
|
+
? '`gh` CLI not found — install GitHub CLI and authenticate with `gh auth login`'
|
|
1204
|
+
: `share failed: ${msg}`);
|
|
1205
|
+
});
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
case 'reload_auth': {
|
|
1209
|
+
if (notController(client, 'reload auth'))
|
|
1210
|
+
break;
|
|
1211
|
+
try {
|
|
1212
|
+
services.authStorage.reload();
|
|
1213
|
+
services.modelRegistry.refresh();
|
|
1214
|
+
ackTo(client, 'reload_auth');
|
|
1215
|
+
broadcastModelChanged();
|
|
1216
|
+
}
|
|
1217
|
+
catch (err) {
|
|
1218
|
+
engineErrorTo(client)(err);
|
|
1219
|
+
}
|
|
1220
|
+
break;
|
|
1221
|
+
}
|
|
817
1222
|
case 'bye':
|
|
818
1223
|
client.socket.end();
|
|
819
1224
|
break;
|
|
@@ -907,17 +1312,36 @@ export async function runBroker(nodeId) {
|
|
|
907
1312
|
}
|
|
908
1313
|
}
|
|
909
1314
|
// ---------------------------------------------------------------------------
|
|
1315
|
+
// snapshotMessages — the catch-up snapshot's ordered message history.
|
|
1316
|
+
//
|
|
1317
|
+
// The broker is the SOLE writer of the node's session `.jsonl`, so the session
|
|
1318
|
+
// manager's persisted tree (`buildSessionContext`) is the canonical, complete,
|
|
1319
|
+
// ordered history — byte-identical to what a dormant reader (crouter-web's static
|
|
1320
|
+
// normalizer) reconstructs from the same file. We serve THAT, NOT the agent's
|
|
1321
|
+
// live `session.messages` (`agent.state.messages`), because pi's recovery paths
|
|
1322
|
+
// mutate the live array away from the persisted history: auto-retry, context-
|
|
1323
|
+
// overflow recovery, and compaction each SLICE the errored/superseded assistant
|
|
1324
|
+
// message out of `state.messages` while DELIBERATELY keeping it on disk ("keep in
|
|
1325
|
+
// session for history", agent-session.js). So `session.messages` can OMIT — or, via
|
|
1326
|
+
// branch/compaction reshaping, reorder — a turn the `.jsonl` still holds. Pre-
|
|
1327
|
+
// close that drift is invisible (the live stream and the live array agree); after
|
|
1328
|
+
// a revive the welcome snapshot built from the reloaded array would diverge from
|
|
1329
|
+
// the on-disk history a dormant viewer just saw. Reconstructing from the session
|
|
1330
|
+
// manager makes the live snapshot == the persisted history (single source of
|
|
1331
|
+
// truth); the relayed live stream then continues from there.
|
|
1332
|
+
//
|
|
1333
|
+
// `AgentSession.sessionManager` is a public readonly field of the real pi SDK; the
|
|
1334
|
+
// fake-engine test fixture mirrors the same `sessionManager.buildSessionContext()`
|
|
1335
|
+
// surface, so this is a single path with no engine-capability fallback.
|
|
1336
|
+
export function snapshotMessages(session) {
|
|
1337
|
+
return session.sessionManager.buildSessionContext().messages;
|
|
1338
|
+
}
|
|
1339
|
+
// ---------------------------------------------------------------------------
|
|
910
1340
|
// buildBrokerSession (plan T4 steps 2–4) — turn the launch recipe into a live
|
|
911
1341
|
// engine session via the pi SDK SERVICES path. Exported so the C3/C4 real-SDK
|
|
912
1342
|
// regression tests can drive the EXACT production wiring (not the mock).
|
|
913
1343
|
// ---------------------------------------------------------------------------
|
|
914
1344
|
export async function buildBrokerSession(engine, cfg) {
|
|
915
|
-
// Fork-on-spawn is the runtime.fork() path (createAgentSessionRuntime), not
|
|
916
|
-
// wired headless in Phase 3. Fail loud rather than silently mis-resume.
|
|
917
|
-
if (cfg.forkFrom !== undefined && cfg.forkFrom !== '') {
|
|
918
|
-
throw new Error(`[broker] --fork is not supported by the headless broker in Phase 3 ` +
|
|
919
|
-
`(forkFrom=${cfg.forkFrom}); the SDK fork path is createAgentSessionRuntime.fork()`);
|
|
920
|
-
}
|
|
921
1345
|
// 2. Build cwd-bound runtime services via the SERVICES path (C3) — NOT plain
|
|
922
1346
|
// createAgentSession. createAgentSessionServices builds + reloads the
|
|
923
1347
|
// resource loader (the `-e` canvas extensions + --append-system-prompt) and
|
|
@@ -978,9 +1402,12 @@ export async function buildBrokerSession(engine, cfg) {
|
|
|
978
1402
|
});
|
|
979
1403
|
return { ...created, services };
|
|
980
1404
|
};
|
|
981
|
-
// 4.
|
|
1405
|
+
// 4. Select the SessionManager: FORK (spawn-time --fork), OPEN (resume), or
|
|
1406
|
+
// CREATE (fresh). The broker is the sole writer of the resulting .jsonl.
|
|
982
1407
|
const resumePath = cfg.resumeSessionPath;
|
|
983
|
-
|
|
1408
|
+
const forking = cfg.forkFrom !== undefined && cfg.forkFrom !== '';
|
|
1409
|
+
if (!forking &&
|
|
1410
|
+
(resumePath === undefined || resumePath === '') &&
|
|
984
1411
|
cfg.resumeSessionId !== undefined &&
|
|
985
1412
|
cfg.resumeSessionId !== '') {
|
|
986
1413
|
// SessionManager.open() takes a FILE path — it only path-normalizes; it does
|
|
@@ -992,10 +1419,29 @@ export async function buildBrokerSession(engine, cfg) {
|
|
|
992
1419
|
throw new Error(`[broker] resume requires a session .jsonl PATH; got only a bare id ` +
|
|
993
1420
|
`'${cfg.resumeSessionId}' (pi_session_file was never captured for this node)`);
|
|
994
1421
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1422
|
+
// A fork is a fresh spawn (resuming=false) so the kickoff firstPrompt fires: a
|
|
1423
|
+
// `node new --fork-from <id>` gives the new node the source's full history AND a
|
|
1424
|
+
// new task to start on. A resume replays the inbox instead and sends no kickoff.
|
|
1425
|
+
const resuming = !forking && resumePath !== undefined && resumePath !== '';
|
|
1426
|
+
let sessionManager;
|
|
1427
|
+
if (forking) {
|
|
1428
|
+
// Spawn-time fork via pi's REAL fork seam, SessionManager.forkFrom — the exact
|
|
1429
|
+
// method pi's own `--fork` CLI flag uses (main.js forkSessionOrExit). It writes
|
|
1430
|
+
// a NEW session file with a NEW id and a `parentSession` header pointing at the
|
|
1431
|
+
// source, then copies the source's history into it. NOT a naïve `.jsonl` copy
|
|
1432
|
+
// (which would collide session ids and drop the fork metadata), and NOT
|
|
1433
|
+
// runtime.fork() (which forks an entry WITHIN an already-loaded session — there
|
|
1434
|
+
// is no current session to fork at boot). The caller (spawn.ts resolveForkSource)
|
|
1435
|
+
// resolves cfg.forkFrom to an absolute source .jsonl before launch, so a bad ref
|
|
1436
|
+
// fails loudly here (forkFrom throws on an empty/invalid/header-less source).
|
|
1437
|
+
sessionManager = engine.SessionManager.forkFrom(cfg.forkFrom, cfg.cwd);
|
|
1438
|
+
}
|
|
1439
|
+
else if (resuming) {
|
|
1440
|
+
sessionManager = engine.SessionManager.open(resumePath);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
sessionManager = engine.SessionManager.create(cfg.cwd);
|
|
1444
|
+
}
|
|
999
1445
|
// When the engine exposes the replacement API (real SDK), wrap the builder in an
|
|
1000
1446
|
// AgentSessionRuntime so new_session/switch_session/fork work; the runtime
|
|
1001
1447
|
// re-invokes `buildForManager` for each new SessionManager. The fake-engine
|
|
@@ -1101,18 +1547,33 @@ export function makeBrokerUiContext(deps) {
|
|
|
1101
1547
|
confirm: (title, message, opts) => dialogPromise(false, { type: 'extension_ui_request', id: randomUUID(), method: 'confirm', title, message, timeout: opts?.timeout }, (r) => ('cancelled' in r && r.cancelled ? false : 'confirmed' in r ? r.confirmed : false), opts),
|
|
1102
1548
|
input: (title, placeholder, opts) => dialogPromise(undefined, { type: 'extension_ui_request', id: randomUUID(), method: 'input', title, placeholder, timeout: opts?.timeout }, (r) => ('cancelled' in r && r.cancelled ? undefined : 'value' in r ? r.value : undefined), opts),
|
|
1103
1549
|
editor: (title, prefill) => dialogPromise(undefined, { type: 'extension_ui_request', id: randomUUID(), method: 'editor', title, prefill }, (r) => ('cancelled' in r && r.cancelled ? undefined : 'value' in r ? r.value : undefined)),
|
|
1550
|
+
// Non-blocking extension-UI relay (§5 Unit A task 3): broadcast these to all
|
|
1551
|
+
// viewers as display frames instead of dropping them. The viewer (Unit E) owns
|
|
1552
|
+
// the slots that render them. The other non-blocking methods stay inert.
|
|
1553
|
+
setStatus: (key, text) => deps.broadcast({ type: 'display_status', key, text }),
|
|
1554
|
+
setWidget: (key, content, options) => {
|
|
1555
|
+
// setWidget's component-factory overload cannot be serialized over the
|
|
1556
|
+
// socket (R1 caveat) — drop it and relay only the string[] form, exactly as
|
|
1557
|
+
// pi's own RPC setWidget carries only `widgetLines`.
|
|
1558
|
+
if (typeof content === 'function')
|
|
1559
|
+
return;
|
|
1560
|
+
deps.broadcast({
|
|
1561
|
+
type: 'display_widget',
|
|
1562
|
+
key,
|
|
1563
|
+
lines: content,
|
|
1564
|
+
placement: options?.placement ?? 'aboveEditor',
|
|
1565
|
+
});
|
|
1566
|
+
},
|
|
1567
|
+
setTitle: (title) => deps.broadcast({ type: 'display_title', title }),
|
|
1104
1568
|
// Inert print-mode surface (never reached by a Model hook in Phase 3).
|
|
1105
1569
|
notify: noop,
|
|
1106
1570
|
onTerminalInput: () => noop,
|
|
1107
|
-
setStatus: noop,
|
|
1108
1571
|
setWorkingMessage: noop,
|
|
1109
1572
|
setWorkingVisible: noop,
|
|
1110
1573
|
setWorkingIndicator: noop,
|
|
1111
1574
|
setHiddenThinkingLabel: noop,
|
|
1112
|
-
setWidget: noop,
|
|
1113
1575
|
setFooter: noop,
|
|
1114
1576
|
setHeader: noop,
|
|
1115
|
-
setTitle: noop,
|
|
1116
1577
|
custom: () => Promise.resolve(undefined),
|
|
1117
1578
|
pasteToEditor: noop,
|
|
1118
1579
|
setEditorText: noop,
|