@crouton-kit/crouter 0.3.28 → 0.3.30
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.d.ts +15 -7
- package/dist/build-root.js +48 -34
- 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 +5 -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 +22 -17
- 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__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +282 -46
- 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 +4 -2
- package/dist/clients/attach/canvas-panels.js +45 -17
- 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 +13 -4
- package/dist/clients/attach/graph-overlay.js +59 -19
- package/dist/clients/attach/input-controller.d.ts +75 -13
- package/dist/clients/attach/input-controller.js +305 -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 +22 -0
- package/dist/clients/attach/slash-commands.js +91 -25
- 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 +18 -1
- package/dist/clients/attach/view-socket.js +70 -1
- 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 +4 -25
- package/dist/commands/dashboard.js +5 -1
- package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
- package/dist/commands/memory/lint.js +14 -2
- 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 +8 -4
- 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/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +3 -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__/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 +17 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +22 -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__/lifecycle.test.js +1 -19
- package/dist/core/__tests__/listing-completeness.test.js +2 -2
- 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.js +2 -2
- 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.js +8 -7
- 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.d.ts +4 -1
- package/dist/core/runtime/broker.js +494 -58
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +6 -14
- 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 -335
- package/dist/core/runtime/placement.js +291 -858
- 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 +40 -139
- package/dist/core/runtime/spawn.d.ts +15 -16
- package/dist/core/runtime/spawn.js +165 -194
- 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 -52
- package/dist/core/runtime/tmux.js +35 -99
- 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/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.js +3 -3
- 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 +10 -3
- package/dist/core/substrate/schema.js +6 -3
- 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 +16 -16
- package/dist/pi-extensions/canvas-nav.js +5 -0
- 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 +14 -12
- package/dist/pi-extensions/canvas-stophook.js +87 -101
- 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 -297
- 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__/slash-quit-copy.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.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,6 +319,40 @@ 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
357
|
messages: snapshotMessages(session),
|
|
314
358
|
stats: session.getSessionStats(),
|
|
@@ -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;
|
|
@@ -937,12 +1342,6 @@ export function snapshotMessages(session) {
|
|
|
937
1342
|
// regression tests can drive the EXACT production wiring (not the mock).
|
|
938
1343
|
// ---------------------------------------------------------------------------
|
|
939
1344
|
export async function buildBrokerSession(engine, cfg) {
|
|
940
|
-
// Fork-on-spawn is the runtime.fork() path (createAgentSessionRuntime), not
|
|
941
|
-
// wired headless in Phase 3. Fail loud rather than silently mis-resume.
|
|
942
|
-
if (cfg.forkFrom !== undefined && cfg.forkFrom !== '') {
|
|
943
|
-
throw new Error(`[broker] --fork is not supported by the headless broker in Phase 3 ` +
|
|
944
|
-
`(forkFrom=${cfg.forkFrom}); the SDK fork path is createAgentSessionRuntime.fork()`);
|
|
945
|
-
}
|
|
946
1345
|
// 2. Build cwd-bound runtime services via the SERVICES path (C3) — NOT plain
|
|
947
1346
|
// createAgentSession. createAgentSessionServices builds + reloads the
|
|
948
1347
|
// resource loader (the `-e` canvas extensions + --append-system-prompt) and
|
|
@@ -1003,9 +1402,12 @@ export async function buildBrokerSession(engine, cfg) {
|
|
|
1003
1402
|
});
|
|
1004
1403
|
return { ...created, services };
|
|
1005
1404
|
};
|
|
1006
|
-
// 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.
|
|
1007
1407
|
const resumePath = cfg.resumeSessionPath;
|
|
1008
|
-
|
|
1408
|
+
const forking = cfg.forkFrom !== undefined && cfg.forkFrom !== '';
|
|
1409
|
+
if (!forking &&
|
|
1410
|
+
(resumePath === undefined || resumePath === '') &&
|
|
1009
1411
|
cfg.resumeSessionId !== undefined &&
|
|
1010
1412
|
cfg.resumeSessionId !== '') {
|
|
1011
1413
|
// SessionManager.open() takes a FILE path — it only path-normalizes; it does
|
|
@@ -1017,10 +1419,29 @@ export async function buildBrokerSession(engine, cfg) {
|
|
|
1017
1419
|
throw new Error(`[broker] resume requires a session .jsonl PATH; got only a bare id ` +
|
|
1018
1420
|
`'${cfg.resumeSessionId}' (pi_session_file was never captured for this node)`);
|
|
1019
1421
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
+
}
|
|
1024
1445
|
// When the engine exposes the replacement API (real SDK), wrap the builder in an
|
|
1025
1446
|
// AgentSessionRuntime so new_session/switch_session/fork work; the runtime
|
|
1026
1447
|
// re-invokes `buildForManager` for each new SessionManager. The fake-engine
|
|
@@ -1126,18 +1547,33 @@ export function makeBrokerUiContext(deps) {
|
|
|
1126
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),
|
|
1127
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),
|
|
1128
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 }),
|
|
1129
1568
|
// Inert print-mode surface (never reached by a Model hook in Phase 3).
|
|
1130
1569
|
notify: noop,
|
|
1131
1570
|
onTerminalInput: () => noop,
|
|
1132
|
-
setStatus: noop,
|
|
1133
1571
|
setWorkingMessage: noop,
|
|
1134
1572
|
setWorkingVisible: noop,
|
|
1135
1573
|
setWorkingIndicator: noop,
|
|
1136
1574
|
setHiddenThinkingLabel: noop,
|
|
1137
|
-
setWidget: noop,
|
|
1138
1575
|
setFooter: noop,
|
|
1139
1576
|
setHeader: noop,
|
|
1140
|
-
setTitle: noop,
|
|
1141
1577
|
custom: () => Promise.resolve(undefined),
|
|
1142
1578
|
pasteToEditor: noop,
|
|
1143
1579
|
setEditorText: noop,
|