@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
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
// Cycle guard: the subscription graph is declared acyclic (a node cannot
|
|
13
13
|
// subscribe to its own ancestor), but we track visited ids defensively because
|
|
14
14
|
// the db is mutable and bugs happen.
|
|
15
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { getNode, listNodes, subscriptionsOf, view } from './canvas.js';
|
|
18
18
|
import { fullName } from './labels.js';
|
|
19
19
|
import { jobDir, contextDir } from './paths.js';
|
|
20
|
-
import { countAsks } from './attention.js';
|
|
20
|
+
import { countAsks, asksForNodes } from './attention.js';
|
|
21
|
+
import { isPidAlive } from './pid.js';
|
|
22
|
+
import { listFocuses } from './focuses.js';
|
|
21
23
|
// ---------------------------------------------------------------------------
|
|
22
24
|
// Glyphs
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
@@ -181,54 +183,98 @@ function readGoalText(nodeId) {
|
|
|
181
183
|
return undefined;
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
|
-
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
186
|
+
/** The node's session-derived preview text in ONE file read (today's two reads,
|
|
187
|
+
* folded): EVERY user prompt across the pi session (`prompts` — the whole
|
|
188
|
+
* conversation, not just the spawn prompt) AND the LAST assistant message text
|
|
189
|
+
* (`lastAssistant`). The single jsonl read is split once; a forward pass collects
|
|
190
|
+
* user prompts (prefiltering to user-role lines so the big assistant/toolResult
|
|
191
|
+
* lines are never JSON-parsed) and a backward pass finds the newest assistant
|
|
192
|
+
* reply with text content (tool-only replies skipped). Both are capped per-message
|
|
193
|
+
* + total so a long session can't bloat the snapshot. Never throws; returns an
|
|
194
|
+
* empty object when there is no session file yet (a node that was never revived). */
|
|
190
195
|
const CONVO_CAP = 8192;
|
|
191
196
|
const CONVO_MSG_CAP = 2048;
|
|
192
|
-
function
|
|
197
|
+
function readSessionParts(sessionFile) {
|
|
193
198
|
if (sessionFile === undefined || sessionFile === null || sessionFile === '')
|
|
194
|
-
return
|
|
199
|
+
return {};
|
|
200
|
+
let lines;
|
|
195
201
|
try {
|
|
196
202
|
if (!existsSync(sessionFile))
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
continue;
|
|
216
|
-
const text = extractUserText(rec.message.content);
|
|
217
|
-
if (text === '')
|
|
218
|
-
continue;
|
|
219
|
-
const capped = text.length > CONVO_MSG_CAP ? text.slice(0, CONVO_MSG_CAP) : text;
|
|
220
|
-
parts.push(capped);
|
|
221
|
-
total += capped.length + 1;
|
|
222
|
-
if (total >= CONVO_CAP)
|
|
223
|
-
break;
|
|
203
|
+
return {};
|
|
204
|
+
lines = readFileSync(sessionFile, 'utf8').split('\n');
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
// Forward pass: every user prompt, joined + capped.
|
|
210
|
+
const parts = [];
|
|
211
|
+
let total = 0;
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
// Cheap prefilter: skip every line that isn't a user-role message before the
|
|
214
|
+
// (relatively costly) JSON.parse. Pi writes compact JSON (no spaces), but
|
|
215
|
+
// tolerate the spaced form too.
|
|
216
|
+
if (line === '' || (line.indexOf('"role":"user"') === -1 && line.indexOf('"role": "user"') === -1))
|
|
217
|
+
continue;
|
|
218
|
+
let rec;
|
|
219
|
+
try {
|
|
220
|
+
rec = JSON.parse(line);
|
|
224
221
|
}
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (rec.type !== 'message' || rec.message?.role !== 'user')
|
|
226
|
+
continue;
|
|
227
|
+
const text = extractUserText(rec.message.content);
|
|
228
|
+
if (text === '')
|
|
229
|
+
continue;
|
|
230
|
+
const capped = text.length > CONVO_MSG_CAP ? text.slice(0, CONVO_MSG_CAP) : text;
|
|
231
|
+
parts.push(capped);
|
|
232
|
+
total += capped.length + 1;
|
|
233
|
+
if (total >= CONVO_CAP)
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
let prompts;
|
|
237
|
+
if (parts.length > 0) {
|
|
227
238
|
const joined = parts.join('\n');
|
|
228
|
-
|
|
239
|
+
prompts = joined.length > CONVO_CAP ? joined.slice(0, CONVO_CAP) : joined;
|
|
240
|
+
}
|
|
241
|
+
// Backward pass: the newest assistant message carrying text.
|
|
242
|
+
let lastAssistant;
|
|
243
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
244
|
+
const line = lines[i];
|
|
245
|
+
if (line === '' || (line.indexOf('"role":"assistant"') === -1 && line.indexOf('"role": "assistant"') === -1))
|
|
246
|
+
continue;
|
|
247
|
+
let rec;
|
|
248
|
+
try {
|
|
249
|
+
rec = JSON.parse(line);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (rec.type !== 'message' || rec.message?.role !== 'assistant')
|
|
255
|
+
continue;
|
|
256
|
+
const text = extractUserText(rec.message.content);
|
|
257
|
+
if (text === '')
|
|
258
|
+
continue;
|
|
259
|
+
lastAssistant = text.length > CONVO_MSG_CAP ? text.slice(0, CONVO_MSG_CAP) : text;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
return { prompts, lastAssistant };
|
|
263
|
+
}
|
|
264
|
+
/** Is the node GENUINELY mid-turn right now? The `busy` marker (touched on
|
|
265
|
+
* agent_start, removed on agent_end) AND-ed with broker-pid liveness, so a stale
|
|
266
|
+
* marker from a crashed pi reads false. This is the live "generating?" signal
|
|
267
|
+
* — distinct from `status: 'active'`, which only means the engine never closed
|
|
268
|
+
* (an active node is usually dormant between turns). Read directly off disk
|
|
269
|
+
* (mirrors telemetry) to avoid inverting the canvas→runtime dependency. */
|
|
270
|
+
function isStreaming(nodeId, piPid) {
|
|
271
|
+
if (!isPidAlive(piPid))
|
|
272
|
+
return false;
|
|
273
|
+
try {
|
|
274
|
+
return existsSync(join(jobDir(nodeId), 'busy'));
|
|
229
275
|
}
|
|
230
276
|
catch {
|
|
231
|
-
return
|
|
277
|
+
return false;
|
|
232
278
|
}
|
|
233
279
|
}
|
|
234
280
|
/** Concatenate the `text` blocks of one pi user message's content. Content is
|
|
@@ -269,26 +315,114 @@ export function dashboardRows(rootId) {
|
|
|
269
315
|
}];
|
|
270
316
|
});
|
|
271
317
|
}
|
|
272
|
-
/**
|
|
318
|
+
/** "Most recent activity" sort key (epoch ms) read CHEAPLY: a `statSync` (no file
|
|
319
|
+
* read, no parse) of the node's job/telemetry.json — rewritten on every turn_end at
|
|
320
|
+
* a deterministic canvas-home path, so it tracks last-message recency without a meta
|
|
321
|
+
* read. Falls back to `created`'s epoch when a node never ran a turn (no telemetry).
|
|
322
|
+
* Never throws. */
|
|
323
|
+
function sessionMtime(nodeId, created) {
|
|
324
|
+
try {
|
|
325
|
+
const st = statSync(join(jobDir(nodeId), 'telemetry.json'), { throwIfNoEntry: false });
|
|
326
|
+
if (st !== undefined)
|
|
327
|
+
return st.mtimeMs;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
/* fall through to created */
|
|
331
|
+
}
|
|
332
|
+
const t = Date.parse(created);
|
|
333
|
+
return Number.isNaN(t) ? 0 : t;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* CHEAP boot builder — one row per node across the entire canvas, with only the
|
|
337
|
+
* fields that need no per-node meta/telemetry/session read. Two db queries total:
|
|
338
|
+
* `listNodes()` + `listFocuses()` (the focus set replaces 1473 `getFocusByNode`
|
|
339
|
+
* calls). For each node:
|
|
340
|
+
* - identity/status/kind/mode/cwd/created/lifecycle straight off the db row;
|
|
341
|
+
* - `name` is the db HANDLE only (the informative label needs meta.description —
|
|
342
|
+
* fold it in lazily via {@link enrichRow}/{@link enrichRows});
|
|
343
|
+
* - `viewed` from the one focus-set;
|
|
344
|
+
* - `streaming` only for LIVE nodes (active|idle — the only ones that can be
|
|
345
|
+
* mid-turn); dormant rows are always false (skips an isPidAlive + statSync);
|
|
346
|
+
* - `mtimeMs` via a cheap telemetry statSync (no read) for the attention sort;
|
|
347
|
+
* - `ctx_tokens`/`asks` left 0 and `goal`/`prompts`/`lastAssistant` undefined —
|
|
348
|
+
* all deferred to the lazy enrichment API below.
|
|
349
|
+
*
|
|
350
|
+
* This is the first-frame path: paint from it immediately, then enrich the visible
|
|
351
|
+
* viewport on demand. See {@link enrichRow}, {@link enrichRows}, {@link loadPreview}.
|
|
352
|
+
*/
|
|
273
353
|
export function dashboardRowsAll() {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}];
|
|
354
|
+
const focusedNodeIds = new Set(listFocuses().map((f) => f.node_id));
|
|
355
|
+
return listNodes().map((row) => {
|
|
356
|
+
const live = row.status === 'active' || row.status === 'idle';
|
|
357
|
+
return {
|
|
358
|
+
node_id: row.node_id,
|
|
359
|
+
name: row.name, // handle only; enrichRow upgrades to fullName (meta.description)
|
|
360
|
+
status: row.status,
|
|
361
|
+
kind: row.kind,
|
|
362
|
+
mode: row.mode,
|
|
363
|
+
ctx_tokens: 0, // lazy: enrichRow
|
|
364
|
+
asks: 0, // lazy: enrichRow / enrichRows
|
|
365
|
+
cwd: row.cwd,
|
|
366
|
+
created: row.created,
|
|
367
|
+
lifecycle: row.lifecycle,
|
|
368
|
+
mtimeMs: sessionMtime(row.node_id, row.created),
|
|
369
|
+
streaming: live ? isStreaming(row.node_id, row.pi_pid) : false,
|
|
370
|
+
viewed: focusedNodeIds.has(row.node_id),
|
|
371
|
+
};
|
|
293
372
|
});
|
|
294
373
|
}
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Lazy enrichment API — fold the expensive per-node reads into rows on demand,
|
|
376
|
+
// for the VISIBLE viewport / top tiers only (never all 1473 up front). Each
|
|
377
|
+
// function mutates the row in place (the browse tree holds the row reference, so
|
|
378
|
+
// a later flush re-renders the upgraded data) and is idempotent via a guard flag
|
|
379
|
+
// so progressive paint can call it for the viewport every keystroke for free.
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
/** Fold the cheap-boot row's deferred fields in for ONE row: the full label
|
|
382
|
+
* (meta.description → fullName), ctx tokens (telemetry.json), and ⚑ asks. One
|
|
383
|
+
* meta read + one telemetry read + one ask scan. Idempotent (no-op once enriched).
|
|
384
|
+
* For a batch, prefer {@link enrichRows} — it scans each cwd's ask inbox once. */
|
|
385
|
+
export function enrichRow(row) {
|
|
386
|
+
if (row.enriched === true)
|
|
387
|
+
return row;
|
|
388
|
+
const meta = getNode(row.node_id);
|
|
389
|
+
if (meta !== null)
|
|
390
|
+
row.name = fullName(meta);
|
|
391
|
+
row.ctx_tokens = readNodeTelemetry(row.node_id).tokens_in ?? 0;
|
|
392
|
+
row.asks = countAsks(row.node_id);
|
|
393
|
+
row.enriched = true;
|
|
394
|
+
return row;
|
|
395
|
+
}
|
|
396
|
+
/** Batch {@link enrichRow}: enrich every not-yet-enriched row, scanning each
|
|
397
|
+
* distinct cwd's ask inbox exactly ONCE (via `asksForNodes`) instead of per row.
|
|
398
|
+
* Use this for a whole viewport / the full forest; mutates each row in place. */
|
|
399
|
+
export function enrichRows(rows) {
|
|
400
|
+
const todo = rows.filter((r) => r.enriched !== true);
|
|
401
|
+
if (todo.length === 0)
|
|
402
|
+
return;
|
|
403
|
+
const asks = asksForNodes(todo.map((r) => r.node_id));
|
|
404
|
+
for (const row of todo) {
|
|
405
|
+
const meta = getNode(row.node_id);
|
|
406
|
+
if (meta !== null)
|
|
407
|
+
row.name = fullName(meta);
|
|
408
|
+
row.ctx_tokens = readNodeTelemetry(row.node_id).tokens_in ?? 0;
|
|
409
|
+
row.asks = asks[row.node_id] ?? 0;
|
|
410
|
+
row.enriched = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** Load the SELECTED row's preview text: the spawn `goal` (initial-prompt.md) plus
|
|
414
|
+
* the whole-conversation `prompts` and the `lastAssistant` reply — the latter two
|
|
415
|
+
* folded into ONE session-file read (see {@link readSessionParts}). Mutates the row
|
|
416
|
+
* in place; idempotent. Call only for the cursor row (and lazily/in-background to
|
|
417
|
+
* warm the prompt super-search corpus), never on the boot path. */
|
|
418
|
+
export function loadPreview(row) {
|
|
419
|
+
if (row.previewLoaded === true)
|
|
420
|
+
return row;
|
|
421
|
+
row.goal = readGoalText(row.node_id);
|
|
422
|
+
const meta = getNode(row.node_id);
|
|
423
|
+
const { prompts, lastAssistant } = readSessionParts(meta?.pi_session_file);
|
|
424
|
+
row.prompts = prompts;
|
|
425
|
+
row.lastAssistant = lastAssistant;
|
|
426
|
+
row.previewLoaded = true;
|
|
427
|
+
return row;
|
|
428
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { SpawnChildOpts } from '../runtime/spawn.js';
|
|
2
|
-
/** What a node is doing right now. `active` means the engine process
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
/** What a node is doing right now. `active` means the engine process — a detached
|
|
3
|
+
* headless broker — is live, NOT that it holds a window or that anyone is
|
|
4
|
+
* watching it (attachment is a separate axis: the `focuses` table tracks each
|
|
5
|
+
* node's one viewer pane, and the broker records its helloed-viewer count).
|
|
6
|
+
* Crucially, `active` does NOT mean the node is GENERATING right now: a live
|
|
7
|
+
* engine sits dormant between turns (waiting on a child, a human, or just left
|
|
8
|
+
* open), so `active` ≠ busy — it only means the process was never closed. To tell
|
|
9
|
+
* whether an active node is actually working, check its pi session-file mtime or
|
|
10
|
+
* CPU, not its status. UI shows active+idle; `done` is hidden but revivable;
|
|
11
|
+
* `canceled` is a user-closed node (also hidden, also revivable — not a fault);
|
|
12
|
+
* only `dead` is a fault. */
|
|
8
13
|
export type NodeStatus = 'active' | 'idle' | 'done' | 'dead' | 'canceled';
|
|
9
14
|
/** Does stopping finalize the node? terminal = worker (finalizes on push --final);
|
|
10
15
|
* resident = manager/orchestrator (stays dormant, woken by inbox). */
|
|
@@ -84,16 +89,6 @@ export interface NodeIdentity {
|
|
|
84
89
|
fork_from?: string | null;
|
|
85
90
|
/** New subscriptions this node opens default to passive when true. */
|
|
86
91
|
passive_default?: boolean;
|
|
87
|
-
/** REVIVE-HOME — the tmux session a node is (re)opened into when it must
|
|
88
|
-
* generate but is NOT focused (the durable revive target, distinct from the
|
|
89
|
-
* live LOCATION held by the runtime `tmux_session`). Set once at birth
|
|
90
|
-
* (managed child → the shared backstage `nodeSession()`; inline root → the
|
|
91
|
-
* adopted caller session; independent `--root` → the caller session), and
|
|
92
|
-
* rewritten only by demote-recycle. Durable identity (like `cwd`), never
|
|
93
|
-
* touched by a focus swap — this is what keeps a background revive off the
|
|
94
|
-
* user's session. Legacy metas omit it; readers default to
|
|
95
|
-
* `tmux_session ?? nodeSession()` (see `homeSessionOf`). */
|
|
96
|
-
home_session?: string;
|
|
97
92
|
/** The pi session id for `--session <id>` revival. */
|
|
98
93
|
pi_session_id?: string | null;
|
|
99
94
|
/** Absolute path to pi's session `.jsonl` file, captured at session_start via
|
|
@@ -126,13 +121,11 @@ export interface NodeRuntime {
|
|
|
126
121
|
* Optional on the hydrated view (a fresh construction omits it); the row
|
|
127
122
|
* column is always present, defaulting null. */
|
|
128
123
|
intent?: ExitIntent;
|
|
129
|
-
/** OS pid of the live
|
|
130
|
-
* The daemon's authoritative liveness signal
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* re-records it; left intact by an in-place respawn (reviveInPlace) so a
|
|
135
|
-
* failed respawn surfaces as a dead pid. */
|
|
124
|
+
/** OS pid of the live broker process, recorded on boot (stophook session_start).
|
|
125
|
+
* The daemon's authoritative liveness signal — every node is a detached broker,
|
|
126
|
+
* so `isPidAlive(pi_pid)` is the sole liveness check. Cleared to null by
|
|
127
|
+
* reviveNode until the fresh broker re-records it, so the relaunch gap reads as
|
|
128
|
+
* a dead pid (guarded by the daemon's revive grace, not a double-spawn). */
|
|
136
129
|
pi_pid?: number | null;
|
|
137
130
|
/** Presence: the tmux session (its root's home) and window this node renders
|
|
138
131
|
* in while active. Cleared when the node goes done/dead and its window closes.
|
package/dist/core/config.js
CHANGED
|
@@ -118,10 +118,7 @@ function mergeConfig(partial) {
|
|
|
118
118
|
? Math.floor(rawMaxPanes)
|
|
119
119
|
: defaults.max_panes_per_window;
|
|
120
120
|
const canvasNav = mergeCanvasNav(partial.canvasNav);
|
|
121
|
-
|
|
122
|
-
// is lost on every read-modify-write of config.json.
|
|
123
|
-
const headless = typeof partial.headless === 'boolean' ? partial.headless : defaults.headless;
|
|
124
|
-
return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, canvasNav, headless };
|
|
121
|
+
return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, canvasNav };
|
|
125
122
|
}
|
|
126
123
|
export function updateConfig(scope, mutate) {
|
|
127
124
|
const cfg = readConfig(scope);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join, relative, sep } from 'node:path';
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
2
3
|
import { pathExists, readText, walkFiles } from './fs-utils.js';
|
|
3
4
|
import { parseFrontmatterGeneric } from './frontmatter.js';
|
|
4
5
|
import { listInstalledPlugins, parseSkillQualifier } from './resolver.js';
|
|
@@ -33,19 +34,49 @@ export function listMemoryDocs(scope) {
|
|
|
33
34
|
if (!dir || !pathExists(dir))
|
|
34
35
|
return [];
|
|
35
36
|
const docs = [];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Walk: a dir containing SKILL.md is a single substrate bundle — emit ONE doc
|
|
38
|
+
// named by the dir's relative path, sourced from its SKILL.md, and STOP
|
|
39
|
+
// descending (deeper files are bundle assets, not docs). Every other dir
|
|
40
|
+
// yields its flat *.md files (topical subdirs / INDEX.md) as before.
|
|
41
|
+
const found = [];
|
|
42
|
+
const stack = [dir];
|
|
43
|
+
while (stack.length) {
|
|
44
|
+
const d = stack.pop();
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
39
50
|
continue;
|
|
51
|
+
}
|
|
52
|
+
const hasSkill = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
|
|
53
|
+
if (hasSkill) {
|
|
54
|
+
const name = relative(dir, d).split(sep).join('/');
|
|
55
|
+
if (name)
|
|
56
|
+
found.push({ path: join(d, 'SKILL.md'), name });
|
|
57
|
+
continue; // stop descent — deeper dirs are assets
|
|
58
|
+
}
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
if (e.isDirectory())
|
|
61
|
+
stack.push(join(d, e.name));
|
|
62
|
+
else if (e.isFile() && e.name.endsWith('.md')) {
|
|
63
|
+
const file = join(d, e.name);
|
|
64
|
+
const name = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
|
|
65
|
+
if (name)
|
|
66
|
+
found.push({ path: file, name });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const { path, name } of found) {
|
|
40
71
|
// COLLECTION layer: the strict frontmatter parser throws on invalid YAML.
|
|
41
72
|
// Isolate one malformed doc with a clear scoped notice + skip, so a single
|
|
42
73
|
// bad file can't brick `memory list`/`find` or the substrate boot render.
|
|
43
74
|
try {
|
|
44
|
-
docs.push(loadMemoryDoc(name, scope,
|
|
75
|
+
docs.push(loadMemoryDoc(name, scope, path));
|
|
45
76
|
}
|
|
46
77
|
catch (e) {
|
|
47
78
|
const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
|
|
48
|
-
warn(`invalid frontmatter in ${
|
|
79
|
+
warn(`invalid frontmatter in ${path}: ${msg}`);
|
|
49
80
|
}
|
|
50
81
|
}
|
|
51
82
|
return docs.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -105,6 +136,13 @@ function findMemoryMatches(name, scope) {
|
|
|
105
136
|
matches.push(loadMemoryDoc(name, s, path));
|
|
106
137
|
continue;
|
|
107
138
|
}
|
|
139
|
+
// Bundle marker is authoritative: a `<name>/SKILL.md` resolves the bare
|
|
140
|
+
// dir name to its bundle doc, and wins over INDEX.md if both exist.
|
|
141
|
+
const skillPath = join(dir, ...name.split('/'), 'SKILL.md');
|
|
142
|
+
if (pathExists(skillPath)) {
|
|
143
|
+
matches.push(loadMemoryDoc(name, s, skillPath));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
108
146
|
const indexPath = join(dir, ...name.split('/'), 'INDEX.md');
|
|
109
147
|
if (pathExists(indexPath)) {
|
|
110
148
|
matches.push(loadMemoryDoc(name, s, indexPath));
|
|
@@ -56,6 +56,6 @@ export declare function buildWakeBearings(origin: WakeOrigin): string;
|
|
|
56
56
|
* any copied-in persona) followed by the `<crtr-context>` bearings block. Base
|
|
57
57
|
* framing rides for EVERY node; the across-cycles context-dir note is added
|
|
58
58
|
* ONLY for an orchestrator (by mode) — the one node whose dir a future cycle
|
|
59
|
-
* resumes from. The `<
|
|
59
|
+
* resumes from. The `<knowledge>` block carries the substrate knowledge docs
|
|
60
60
|
* + node-local docs. */
|
|
61
61
|
export declare function buildContextBearings(nodeId: string): string;
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
// one place other nodes on the canvas can read from, so it is for documents
|
|
20
20
|
// worth a shared reference, NOT a task tracker, and NOT a "future memory-wiped
|
|
21
21
|
// you" stash (a terminal worker has no future cycle — that framing only makes
|
|
22
|
-
// sense once a node is a resident orchestrator). The `<
|
|
23
|
-
// (substrate
|
|
22
|
+
// sense once a node is a resident orchestrator). The `<knowledge>` block
|
|
23
|
+
// (substrate knowledge docs + node-local docs) rides into the context message.
|
|
24
24
|
//
|
|
25
25
|
// Orchestrator addendum (gated on orchestrator MODE): the dir ALSO survives
|
|
26
26
|
// refresh cycles, so it is where a future cycle of the orchestrator resumes.
|
|
27
27
|
// This across-cycles note is the ONE thing a terminal worker's bearings drop.
|
|
28
28
|
import { contextDir, getNode, fullName } from '../canvas/index.js';
|
|
29
29
|
import { cadenceDisplay } from '../wake.js';
|
|
30
|
-
import {
|
|
30
|
+
import { renderKnowledgeBlock } from '../substrate/index.js';
|
|
31
31
|
/** Base framing — present for every node. No path baked in: the caller carries
|
|
32
32
|
* the dir in the `<crtr-context dir="…">` attribute. */
|
|
33
33
|
export const BASE_CONTEXT_NOTE = 'This is your context directory — durable scratch space on disk, and the one place the other ' +
|
|
@@ -145,7 +145,7 @@ export function buildWakeBearings(origin) {
|
|
|
145
145
|
* any copied-in persona) followed by the `<crtr-context>` bearings block. Base
|
|
146
146
|
* framing rides for EVERY node; the across-cycles context-dir note is added
|
|
147
147
|
* ONLY for an orchestrator (by mode) — the one node whose dir a future cycle
|
|
148
|
-
* resumes from. The `<
|
|
148
|
+
* resumes from. The `<knowledge>` block carries the substrate knowledge docs
|
|
149
149
|
* + node-local docs. */
|
|
150
150
|
export function buildContextBearings(nodeId) {
|
|
151
151
|
const identity = buildIdentityAssertion(nodeId);
|
|
@@ -155,11 +155,11 @@ export function buildContextBearings(nodeId) {
|
|
|
155
155
|
// Orchestrator-only: the across-cycles framing (a terminal has no future cycle).
|
|
156
156
|
if (node?.mode === 'orchestrator')
|
|
157
157
|
parts.push(orchestratorContextNote(nodeId));
|
|
158
|
-
// The substrate
|
|
158
|
+
// The substrate knowledge block: eligible `knowledge` docs at their
|
|
159
159
|
// system-prompt rung + node-local memory docs; dropped (returns '') when
|
|
160
160
|
// nothing is eligible.
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
parts.push(
|
|
161
|
+
const knowledge = renderKnowledgeBlock(nodeId);
|
|
162
|
+
if (knowledge !== '')
|
|
163
|
+
parts.push(knowledge);
|
|
164
164
|
return `${identity}\n<crtr-context dir="${dir}">\n${parts.join('\n')}\n</crtr-context>`;
|
|
165
165
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Machine-global host dir. Deliberately NOT under `CRTR_HOME` (tests override
|
|
2
|
+
* that to an isolated temp): the branded binary is a per-machine artifact keyed
|
|
3
|
+
* to the installed node, shared across every canvas home. */
|
|
4
|
+
export declare function brandedHostDir(): string;
|
|
5
|
+
export declare function brandedHostBin(): string;
|
|
6
|
+
/** Ensure the branded host binary exists and matches the live node; return its
|
|
7
|
+
* absolute path. Idempotent and self-healing. Throws on a genuine build failure
|
|
8
|
+
* (disk full / permissions) — branding is load-bearing for FDA, so a failure is
|
|
9
|
+
* surfaced, never silently masked by falling back to `node`. */
|
|
10
|
+
export declare function ensureBrandedHost(): string;
|
|
11
|
+
/** The path the engine (broker / daemon) should be launched from. On macOS in a
|
|
12
|
+
* compiled (production) build this is the branded host; otherwise — non-darwin
|
|
13
|
+
* (TCC is macOS-only), source/tsx (dev + the test suite: never copy 70 MB into
|
|
14
|
+
* the real ~/.crouter), or an explicit `CRTR_BRANDED_HOST=0` opt-out — it is the
|
|
15
|
+
* live node. These are environment gates, not error fallbacks. */
|
|
16
|
+
export declare function hostExecPath(): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// branded-host.ts — a crouter-branded copy of the `node` binary, so the
|
|
2
|
+
// long-lived engine processes (brokers + the crtrd daemon) show "crouter" in
|
|
3
|
+
// macOS' TCC / Full Disk Access list instead of the generic "node".
|
|
4
|
+
//
|
|
5
|
+
// WHY a real copied binary (not a symlink, not process.title): macOS keys a
|
|
6
|
+
// Full Disk Access entry on the Mach-O binary's *filename* — not argv[0], not
|
|
7
|
+
// process.title, and symlinks resolve to their realpath. So the ONLY way to show
|
|
8
|
+
// a branded name is to launch the engine from a real Mach-O file whose filename
|
|
9
|
+
// is the brand. We copy the node launcher to `~/.crouter/host/crouter`.
|
|
10
|
+
//
|
|
11
|
+
// WHY self-contained (libnode beside it): a Homebrew node is a small (~70 KB)
|
|
12
|
+
// launcher that dynamically loads `@rpath/libnode.<abi>.dylib` (~70 MB). node's
|
|
13
|
+
// LC_RPATH includes `@loader_path`, so a copy of that dylib placed *beside* the
|
|
14
|
+
// branded binary resolves with no DYLD_* env var (which SIP can strip) and no
|
|
15
|
+
// dependency on the Cellar path surviving — even after a `brew uninstall` of the
|
|
16
|
+
// old keg, the branded copy keeps running. A statically-linked node (nodejs.org)
|
|
17
|
+
// has no separate libnode; we just copy the binary. The remaining absolute
|
|
18
|
+
// `/opt/homebrew/opt/...` deps resolve by their baked-in absolute paths.
|
|
19
|
+
//
|
|
20
|
+
// SELF-HEAL: ensureBrandedHost() rebuilds the copy whenever the live node drifts
|
|
21
|
+
// (a `brew upgrade node` moves realpath(execPath) to a new Cellar version) or the
|
|
22
|
+
// copy is missing. The drift check is a cheap marker compare — no subprocess, no
|
|
23
|
+
// copy — on the happy path.
|
|
24
|
+
import { copyFileSync, mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, readdirSync, renameSync, realpathSync, unlinkSync, } from 'node:fs';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import { dirname, join } from 'node:path';
|
|
27
|
+
import { CRTR_DIR_NAME } from '../../types.js';
|
|
28
|
+
/** The filename the FDA / TCC entry is shown under. */
|
|
29
|
+
const BRANDED_BIN_NAME = 'crouter';
|
|
30
|
+
/** Machine-global host dir. Deliberately NOT under `CRTR_HOME` (tests override
|
|
31
|
+
* that to an isolated temp): the branded binary is a per-machine artifact keyed
|
|
32
|
+
* to the installed node, shared across every canvas home. */
|
|
33
|
+
export function brandedHostDir() {
|
|
34
|
+
return join(homedir(), CRTR_DIR_NAME, 'host');
|
|
35
|
+
}
|
|
36
|
+
export function brandedHostBin() {
|
|
37
|
+
return join(brandedHostDir(), BRANDED_BIN_NAME);
|
|
38
|
+
}
|
|
39
|
+
function markerPath() {
|
|
40
|
+
return join(brandedHostDir(), '.source.json');
|
|
41
|
+
}
|
|
42
|
+
/** Locate the `libnode.<abi>.dylib` a Homebrew node loads via `@rpath`. node's
|
|
43
|
+
* rpaths are `@loader_path` and `@loader_path/../lib`, so check the binary's own
|
|
44
|
+
* dir first, then the sibling `lib/`. Returns the absolute path, or null for a
|
|
45
|
+
* statically-linked node that has no separate libnode. */
|
|
46
|
+
function findLibnode(nodeBin) {
|
|
47
|
+
const binDir = dirname(nodeBin);
|
|
48
|
+
for (const dir of [binDir, join(binDir, '..', 'lib')]) {
|
|
49
|
+
if (!existsSync(dir))
|
|
50
|
+
continue;
|
|
51
|
+
const hit = readdirSync(dir).find((f) => f.startsWith('libnode.') && f.endsWith('.dylib'));
|
|
52
|
+
if (hit !== undefined)
|
|
53
|
+
return join(dir, hit);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/** Copy `src` → `dst` via a unique temp + atomic rename, so concurrent rebuilds
|
|
58
|
+
* (two brokers spawning at once) never observe a half-written file. */
|
|
59
|
+
function atomicCopy(src, dst, mode) {
|
|
60
|
+
const tmp = `${dst}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
61
|
+
copyFileSync(src, tmp);
|
|
62
|
+
if (mode !== undefined)
|
|
63
|
+
chmodSync(tmp, mode);
|
|
64
|
+
renameSync(tmp, dst);
|
|
65
|
+
}
|
|
66
|
+
function readMarker() {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(readFileSync(markerPath(), 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Ensure the branded host binary exists and matches the live node; return its
|
|
75
|
+
* absolute path. Idempotent and self-healing. Throws on a genuine build failure
|
|
76
|
+
* (disk full / permissions) — branding is load-bearing for FDA, so a failure is
|
|
77
|
+
* surfaced, never silently masked by falling back to `node`. */
|
|
78
|
+
export function ensureBrandedHost() {
|
|
79
|
+
const dst = brandedHostBin();
|
|
80
|
+
const src = realpathSync(process.execPath);
|
|
81
|
+
// If THIS process is already running as the branded host (the daemon or a
|
|
82
|
+
// broker spawning a child), don't rebuild from ourselves — we can't see the
|
|
83
|
+
// real node from here, and the live process proves the copy works. Drift is
|
|
84
|
+
// healed by the next real-`node` CLI invocation.
|
|
85
|
+
if (existsSync(dst) && realpathSync(dst) === src)
|
|
86
|
+
return dst;
|
|
87
|
+
const marker = readMarker();
|
|
88
|
+
const libnodeSrc = findLibnode(src);
|
|
89
|
+
const libnodeName = libnodeSrc !== null ? libnodeSrc.split('/').pop() : null;
|
|
90
|
+
const fresh = existsSync(dst) &&
|
|
91
|
+
marker !== null &&
|
|
92
|
+
marker.source === src &&
|
|
93
|
+
marker.libnode === libnodeName &&
|
|
94
|
+
(libnodeName === null || existsSync(join(brandedHostDir(), libnodeName)));
|
|
95
|
+
if (fresh)
|
|
96
|
+
return dst;
|
|
97
|
+
// (Re)build. Clear a stale libnode of a different ABI so the dir stays clean.
|
|
98
|
+
mkdirSync(brandedHostDir(), { recursive: true });
|
|
99
|
+
if (marker?.libnode != null && marker.libnode !== libnodeName) {
|
|
100
|
+
try {
|
|
101
|
+
unlinkSync(join(brandedHostDir(), marker.libnode));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* best-effort */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (libnodeSrc !== null && libnodeName !== null) {
|
|
108
|
+
atomicCopy(libnodeSrc, join(brandedHostDir(), libnodeName));
|
|
109
|
+
}
|
|
110
|
+
atomicCopy(src, dst, 0o755);
|
|
111
|
+
writeFileSync(markerPath(), JSON.stringify({ source: src, libnode: libnodeName }));
|
|
112
|
+
return dst;
|
|
113
|
+
}
|
|
114
|
+
/** The path the engine (broker / daemon) should be launched from. On macOS in a
|
|
115
|
+
* compiled (production) build this is the branded host; otherwise — non-darwin
|
|
116
|
+
* (TCC is macOS-only), source/tsx (dev + the test suite: never copy 70 MB into
|
|
117
|
+
* the real ~/.crouter), or an explicit `CRTR_BRANDED_HOST=0` opt-out — it is the
|
|
118
|
+
* live node. These are environment gates, not error fallbacks. */
|
|
119
|
+
export function hostExecPath() {
|
|
120
|
+
if (process.env['CRTR_BRANDED_HOST'] === '0')
|
|
121
|
+
return process.execPath;
|
|
122
|
+
if (process.platform !== 'darwin')
|
|
123
|
+
return process.execPath;
|
|
124
|
+
if (import.meta.url.endsWith('.ts'))
|
|
125
|
+
return process.execPath;
|
|
126
|
+
return ensureBrandedHost();
|
|
127
|
+
}
|