@crouton-kit/crouter 0.3.31 → 0.3.33
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/README.md +8 -0
- package/dist/build-root.js +1 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +2 -2
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +2 -2
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +5 -5
- package/dist/builtin-memory/crouter-development/personas.md +3 -4
- package/dist/builtin-memory/crouter-development/plugins.md +18 -18
- package/dist/builtin-memory/design.md +4 -1
- package/dist/builtin-memory/development.md +4 -1
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +6 -6
- package/dist/builtin-memory/internal/storage-tiers.md +5 -5
- package/dist/builtin-memory/planning.md +4 -1
- package/dist/builtin-memory/product.md +80 -0
- package/dist/builtin-memory/spec.md +4 -1
- package/dist/builtin-personas/advisor/PERSONA.md +10 -0
- package/dist/builtin-personas/design/PERSONA.md +2 -2
- package/dist/builtin-personas/design/orchestrator.md +3 -3
- package/dist/builtin-personas/developer/PERSONA.md +1 -1
- package/dist/builtin-personas/developer/orchestrator.md +2 -2
- package/dist/builtin-personas/explore/PERSONA.md +3 -3
- package/dist/builtin-personas/explore/orchestrator.md +4 -2
- package/dist/builtin-personas/general/PERSONA.md +1 -1
- package/dist/builtin-personas/general/orchestrator.md +1 -1
- package/dist/builtin-personas/orchestration-kernel.md +7 -14
- package/dist/builtin-personas/plan/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/orchestrator.md +2 -2
- package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -1
- package/dist/builtin-personas/product/PERSONA.md +18 -0
- package/dist/builtin-personas/product/orchestrator.md +14 -0
- package/dist/builtin-personas/product/teardown/PERSONA.md +13 -0
- package/dist/builtin-personas/review/PERSONA.md +1 -1
- package/dist/builtin-personas/review/orchestrator.md +1 -1
- package/dist/builtin-personas/runtime-base.md +5 -0
- package/dist/builtin-personas/spec/PERSONA.md +1 -1
- package/dist/builtin-personas/spec/orchestrator.md +2 -2
- package/dist/builtin-personas/spec/requirements/PERSONA.md +1 -1
- package/dist/builtin-views/canvas/core.mjs +82 -1
- package/dist/builtin-views/canvas/tui.mjs +9 -5
- package/dist/builtin-views/canvas/web.jsx +3 -2
- package/dist/builtin-views/chat/core.mjs +725 -0
- package/dist/builtin-views/chat/text.mjs +101 -0
- package/dist/builtin-views/chat/tui.mjs +368 -0
- package/dist/builtin-views/chat/web.jsx +367 -0
- package/dist/builtin-views/prompt-review/core.mjs +863 -0
- package/dist/builtin-views/prompt-review/text.mjs +15 -0
- package/dist/builtin-views/prompt-review/tui.mjs +196 -0
- package/dist/builtin-views/prompt-review/web.jsx +484 -0
- package/dist/builtin-views/settings/core.mjs +397 -0
- package/dist/builtin-views/settings/text.mjs +40 -0
- package/dist/builtin-views/settings/tui.mjs +95 -0
- package/dist/builtin-views/settings/web.jsx +167 -0
- package/dist/cli.js +16 -3
- package/dist/clients/attach/__tests__/autocomplete-and-bash-mode.test.js +96 -0
- package/dist/clients/attach/__tests__/bash-bang-routing.test.js +58 -0
- package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.js +56 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +22 -1
- package/dist/clients/attach/attach-cmd.d.ts +18 -0
- package/dist/clients/attach/attach-cmd.js +1723 -795
- package/dist/clients/attach/canvas-panels.js +2 -2
- package/dist/clients/attach/chat-view.d.ts +10 -0
- package/dist/clients/attach/chat-view.js +23 -0
- package/dist/clients/attach/config-load.d.ts +8 -4
- package/dist/clients/attach/config-load.js +2 -0
- package/dist/clients/attach/extension-dialogs.d.ts +2 -3
- package/dist/clients/attach/extension-dialogs.js +7 -9
- package/dist/clients/attach/graph-overlay.js +3 -2
- package/dist/clients/attach/input-controller.d.ts +4 -3
- package/dist/clients/attach/input-controller.js +24 -3
- package/dist/clients/attach/slash-commands.d.ts +7 -7
- package/dist/clients/attach/slash-commands.js +38 -14
- package/dist/clients/attach/titled-editor.js +18 -14
- package/dist/clients/attach/view-socket.d.ts +2 -1
- package/dist/clients/attach/view-socket.js +27 -8
- package/dist/clients/web/__tests__/source-cache.test.d.ts +1 -0
- package/dist/clients/web/__tests__/source-cache.test.js +32 -0
- package/dist/clients/web/dev-server.js +1 -0
- package/dist/clients/web/events.js +9 -11
- package/dist/clients/web/server.d.ts +4 -3
- package/dist/clients/web/server.js +138 -31
- package/dist/clients/web/source-cache.d.ts +10 -0
- package/dist/clients/web/source-cache.js +57 -0
- package/dist/clients/web/web-cmd.js +28 -9
- package/dist/commands/__tests__/human.test.js +30 -28
- package/dist/commands/canvas-history/search.js +1 -1
- package/dist/commands/canvas-history.js +5 -8
- package/dist/commands/canvas-issue.d.ts +2 -0
- package/dist/commands/canvas-issue.js +148 -0
- package/dist/commands/canvas-prune.js +19 -0
- package/dist/commands/canvas-rebuild-index.d.ts +2 -0
- package/dist/commands/canvas-rebuild-index.js +57 -0
- package/dist/commands/canvas-snapshot.d.ts +2 -0
- package/dist/commands/canvas-snapshot.js +48 -0
- package/dist/commands/canvas-tmux-spread.d.ts +2 -0
- package/dist/commands/canvas-tmux-spread.js +188 -0
- package/dist/commands/canvas.d.ts +1 -0
- package/dist/commands/canvas.js +16 -1
- package/dist/commands/chord.js +143 -48
- package/dist/commands/daemon.js +3 -3
- package/dist/commands/human/prompts.d.ts +0 -1
- package/dist/commands/human/prompts.js +39 -54
- package/dist/commands/human/queue.d.ts +12 -1
- package/dist/commands/human/queue.js +468 -73
- package/dist/commands/human/shared.d.ts +3 -4
- package/dist/commands/human/shared.js +3 -3
- package/dist/commands/human.js +11 -10
- package/dist/commands/memory/find.js +3 -2
- package/dist/commands/memory/list.js +4 -3
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +1 -4
- package/dist/commands/memory/shared.js +6 -9
- package/dist/commands/memory/write.js +3 -3
- package/dist/commands/memory.js +4 -7
- package/dist/commands/node-context.d.ts +2 -0
- package/dist/commands/node-context.js +172 -0
- package/dist/commands/node-snapshot.d.ts +2 -0
- package/dist/commands/node-snapshot.js +123 -0
- package/dist/commands/node.js +228 -52
- package/dist/commands/pkg/plugin-inspect.js +6 -6
- package/dist/commands/pkg/plugin-manage.js +1 -1
- package/dist/commands/pkg/plugin.js +2 -2
- package/dist/commands/pkg.js +2 -2
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +79 -11
- package/dist/commands/revive.js +74 -1
- package/dist/commands/search/answer.d.ts +1 -0
- package/dist/commands/search/answer.js +50 -0
- package/dist/commands/search/contents.d.ts +1 -0
- package/dist/commands/search/contents.js +96 -0
- package/dist/commands/search/exa.d.ts +53 -0
- package/dist/commands/search/exa.js +155 -0
- package/dist/commands/search/puremd.d.ts +11 -0
- package/dist/commands/search/puremd.js +53 -0
- package/dist/commands/search/web.d.ts +1 -0
- package/dist/commands/search/web.js +65 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +24 -0
- package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/sync-import.test.js +72 -0
- package/dist/commands/sys/config.js +59 -10
- package/dist/commands/sys/doctor.js +56 -4
- package/dist/commands/sys/prompt-review.d.ts +1 -0
- package/dist/commands/sys/prompt-review.js +178 -0
- package/dist/commands/sys/promptstudio.d.ts +2 -0
- package/dist/commands/sys/promptstudio.js +65 -0
- package/dist/commands/sys/settings.d.ts +2 -0
- package/dist/commands/sys/settings.js +16 -0
- package/dist/commands/sys/sync.js +311 -159
- package/dist/commands/sys/sysprompt.d.ts +1 -0
- package/dist/commands/sys/sysprompt.js +86 -0
- package/dist/commands/sys.js +9 -5
- package/dist/commands/view-new.js +2 -2
- package/dist/core/__tests__/artifact-paths.test.d.ts +1 -0
- package/dist/core/__tests__/artifact-paths.test.js +44 -0
- package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
- package/dist/core/__tests__/broker-preflight.test.js +85 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +15 -0
- package/dist/core/__tests__/canvas.test.js +37 -1
- package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
- package/dist/core/__tests__/chat-view-reconnect.test.js +70 -0
- package/dist/core/__tests__/child-death-wake.test.js +11 -2
- package/dist/core/__tests__/close.test.js +39 -1
- package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
- package/dist/core/__tests__/connection-reconnect-resume.test.js +37 -0
- package/dist/core/__tests__/context-intro.test.js +48 -14
- package/dist/core/__tests__/daemon-boot.test.js +182 -9
- package/dist/core/__tests__/editor-label.test.d.ts +1 -0
- package/dist/core/__tests__/editor-label.test.js +26 -0
- package/dist/core/__tests__/fault-marker.test.d.ts +1 -0
- package/dist/core/__tests__/fault-marker.test.js +112 -0
- package/dist/core/__tests__/fault-retry-rewind.test.d.ts +1 -0
- package/dist/core/__tests__/fault-retry-rewind.test.js +123 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +30 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +156 -17
- package/dist/core/__tests__/fixtures/fake-pi-host.js +10 -1
- package/dist/core/__tests__/full/human-new-window-regression.test.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -1
- package/dist/core/__tests__/helpers/harness.js +9 -3
- package/dist/core/__tests__/human-stranded-deliver.test.js +38 -1
- package/dist/core/__tests__/human-surface-target.test.js +1 -1
- package/dist/core/__tests__/kickoff.test.js +18 -19
- package/dist/core/__tests__/memory-resolver.test.js +12 -1
- package/dist/core/__tests__/migration.test.js +49 -14
- package/dist/core/__tests__/model-ladders.test.d.ts +1 -0
- package/dist/core/__tests__/model-ladders.test.js +160 -0
- package/dist/core/__tests__/node-env.test.d.ts +1 -0
- package/dist/core/__tests__/node-env.test.js +26 -0
- package/dist/core/__tests__/push-final-guard.test.js +7 -1
- package/dist/core/__tests__/relaunch-root.test.js +9 -9
- package/dist/core/__tests__/revive.test.js +36 -6
- package/dist/core/__tests__/spawn-root.test.js +24 -1
- package/dist/core/__tests__/stop-guard.test.js +10 -0
- package/dist/core/__tests__/stranded-relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/stranded-relaunch.test.js +72 -0
- package/dist/core/__tests__/tmux-surface.test.js +5 -1
- package/dist/core/__tests__/unknown-path.test.js +19 -19
- package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
- package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
- package/dist/core/artifact.d.ts +2 -33
- package/dist/core/artifact.js +27 -87
- package/dist/core/bootstrap.js +2 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +35 -1
- package/dist/core/canvas/browse/app.js +87 -17
- package/dist/core/canvas/browse/model.d.ts +10 -1
- package/dist/core/canvas/browse/model.js +37 -1
- package/dist/core/canvas/browse/render.js +35 -33
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +54 -5
- package/dist/core/canvas/db.js +35 -8
- package/dist/core/canvas/labels.d.ts +6 -8
- package/dist/core/canvas/labels.js +8 -11
- package/dist/core/canvas/nav-model.d.ts +5 -4
- package/dist/core/canvas/nav-model.js +31 -17
- package/dist/core/canvas/paths.d.ts +18 -1
- package/dist/core/canvas/paths.js +37 -2
- package/dist/core/canvas/render.d.ts +25 -0
- package/dist/core/canvas/render.js +96 -13
- package/dist/core/canvas/status-glyph.d.ts +35 -0
- package/dist/core/canvas/status-glyph.js +104 -0
- package/dist/core/canvas/types.d.ts +2 -2
- package/dist/core/config.js +114 -16
- package/dist/core/fault-classifier.d.ts +41 -0
- package/dist/core/fault-classifier.js +110 -0
- package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +1 -0
- package/dist/core/feed/__tests__/empty-node-guard.test.js +45 -0
- package/dist/core/feed/feed.js +8 -0
- package/dist/core/feed/inbox.d.ts +15 -0
- package/dist/core/feed/inbox.js +40 -14
- package/dist/core/frontmatter.d.ts +1 -11
- package/dist/core/frontmatter.js +1 -55
- package/dist/core/hearth/config.d.ts +2 -0
- package/dist/core/hearth/config.js +75 -0
- package/dist/core/hearth/guest-env.d.ts +2 -0
- package/dist/core/hearth/guest-env.js +11 -0
- package/dist/core/hearth/index.d.ts +5 -0
- package/dist/core/hearth/index.js +5 -0
- package/dist/core/hearth/provider.d.ts +21 -0
- package/dist/core/hearth/provider.js +10 -0
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +10 -0
- package/dist/core/hearth/providers/blaxel-bootstrap.js +39 -0
- package/dist/core/hearth/providers/blaxel-home.d.ts +11 -0
- package/dist/core/hearth/providers/blaxel-home.js +126 -0
- package/dist/core/hearth/providers/blaxel.d.ts +26 -0
- package/dist/core/hearth/providers/blaxel.js +208 -0
- package/dist/core/hearth/providers/types.d.ts +72 -0
- package/dist/core/hearth/providers/types.js +1 -0
- package/dist/core/hearth/registry.d.ts +15 -0
- package/dist/core/hearth/registry.js +179 -0
- package/dist/core/hearth/types.d.ts +121 -0
- package/dist/core/hearth/types.js +1 -0
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +79 -0
- package/dist/core/{skill-sync → host-exports}/builtins.d.ts +21 -13
- package/dist/core/host-exports/builtins.js +68 -0
- package/dist/core/{skill-sync → host-exports}/export.d.ts +9 -6
- package/dist/core/{skill-sync → host-exports}/export.js +37 -14
- package/dist/core/io.js +1 -1
- package/dist/core/log.d.ts +9 -0
- package/dist/core/log.js +113 -0
- package/dist/core/memory-resolver.d.ts +8 -8
- package/dist/core/memory-resolver.js +15 -28
- package/dist/core/personas/index.d.ts +4 -4
- package/dist/core/personas/index.js +2 -2
- package/dist/core/personas/loader.d.ts +51 -0
- package/dist/core/personas/loader.js +54 -50
- package/dist/core/personas/resolve.d.ts +43 -27
- package/dist/core/personas/resolve.js +336 -94
- package/dist/core/runtime/auth-reload.d.ts +7 -0
- package/dist/core/runtime/auth-reload.js +69 -0
- package/dist/core/runtime/bearings.d.ts +35 -15
- package/dist/core/runtime/bearings.js +305 -44
- package/dist/core/runtime/branded-host.d.ts +4 -2
- package/dist/core/runtime/branded-host.js +66 -4
- package/dist/core/runtime/broker-protocol.d.ts +38 -3
- package/dist/core/runtime/broker.js +343 -18
- package/dist/core/runtime/close.js +37 -29
- package/dist/core/runtime/connectivity.d.ts +12 -0
- package/dist/core/runtime/connectivity.js +73 -0
- package/dist/core/runtime/fault-recovery-nudge.d.ts +2 -0
- package/dist/core/runtime/fault-recovery-nudge.js +2 -0
- package/dist/core/runtime/fault-recovery.d.ts +23 -0
- package/dist/core/runtime/fault-recovery.js +74 -0
- package/dist/core/runtime/fault.d.ts +25 -0
- package/dist/core/runtime/fault.js +176 -0
- package/dist/core/runtime/front-door.d.ts +1 -1
- package/dist/core/runtime/front-door.js +2 -2
- package/dist/core/runtime/host.d.ts +10 -0
- package/dist/core/runtime/host.js +71 -9
- package/dist/core/runtime/kickoff.js +31 -45
- package/dist/core/runtime/launch.d.ts +32 -18
- package/dist/core/runtime/launch.js +148 -43
- package/dist/core/runtime/model-swap.d.ts +18 -0
- package/dist/core/runtime/model-swap.js +95 -0
- package/dist/core/runtime/naming.d.ts +7 -0
- package/dist/core/runtime/naming.js +61 -9
- package/dist/core/runtime/nodes.js +6 -1
- package/dist/core/runtime/persona.js +16 -17
- package/dist/core/runtime/placement.d.ts +36 -31
- package/dist/core/runtime/placement.js +149 -64
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +23 -3
- package/dist/core/runtime/recycle.js +6 -4
- package/dist/core/runtime/reset.d.ts +5 -2
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/revive.d.ts +4 -0
- package/dist/core/runtime/revive.js +27 -9
- package/dist/core/runtime/roadmap.d.ts +1 -1
- package/dist/core/runtime/roadmap.js +1 -1
- package/dist/core/runtime/spawn.d.ts +2 -2
- package/dist/core/runtime/spawn.js +30 -6
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +29 -6
- package/dist/core/runtime/tmux.js +134 -80
- package/dist/core/runtime/view-socket-client.d.ts +46 -0
- package/dist/core/runtime/view-socket-client.js +203 -0
- package/dist/core/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/on-read.js +16 -4
- package/dist/core/substrate/render.d.ts +10 -13
- package/dist/core/substrate/render.js +56 -51
- package/dist/core/tui/host.js +83 -18
- package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
- package/dist/core/view/__tests__/transport-cache.test.js +62 -0
- package/dist/core/view/contract.d.ts +20 -0
- package/dist/core/view/stream-local.d.ts +3 -0
- package/dist/core/view/stream-local.js +184 -0
- package/dist/core/view/transport-cache.d.ts +8 -0
- package/dist/core/view/transport-cache.js +38 -0
- package/dist/core/view/transport-local.js +4 -0
- package/dist/core/view/transport.d.ts +7 -1
- package/dist/daemon/crtrd.d.ts +3 -19
- package/dist/daemon/crtrd.js +139 -178
- package/dist/daemon/manage.d.ts +18 -1
- package/dist/daemon/manage.js +54 -9
- package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +289 -0
- package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +34 -0
- package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +203 -0
- package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/hardening.test.js +59 -0
- package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +372 -0
- package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +258 -0
- package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +437 -0
- package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +455 -0
- package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +15 -0
- package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +141 -0
- package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +143 -0
- package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +58 -0
- package/dist/hearth/wake-proxy/anthropic-oauth.js +189 -0
- package/dist/hearth/wake-proxy/auth.d.ts +22 -0
- package/dist/hearth/wake-proxy/auth.js +128 -0
- package/dist/hearth/wake-proxy/config.d.ts +2 -0
- package/dist/hearth/wake-proxy/config.js +151 -0
- package/dist/hearth/wake-proxy/guest-source.d.ts +14 -0
- package/dist/hearth/wake-proxy/guest-source.js +130 -0
- package/dist/hearth/wake-proxy/hearth-status.d.ts +44 -0
- package/dist/hearth/wake-proxy/hearth-status.js +267 -0
- package/dist/hearth/wake-proxy/home.d.ts +67 -0
- package/dist/hearth/wake-proxy/home.js +297 -0
- package/dist/hearth/wake-proxy/main.d.ts +1 -0
- package/dist/hearth/wake-proxy/main.js +134 -0
- package/dist/hearth/wake-proxy/model-auth.d.ts +13 -0
- package/dist/hearth/wake-proxy/model-auth.js +345 -0
- package/dist/hearth/wake-proxy/proxy.d.ts +35 -0
- package/dist/hearth/wake-proxy/proxy.js +716 -0
- package/dist/hearth/wake-proxy/public-source-gate.d.ts +9 -0
- package/dist/hearth/wake-proxy/public-source-gate.js +409 -0
- package/dist/hearth/wake-proxy/redact.d.ts +1 -0
- package/dist/hearth/wake-proxy/redact.js +17 -0
- package/dist/hearth/wake-proxy/server.d.ts +11 -0
- package/dist/hearth/wake-proxy/server.js +142 -0
- package/dist/hearth/wake-proxy/state.d.ts +18 -0
- package/dist/hearth/wake-proxy/state.js +342 -0
- package/dist/hearth/wake-proxy/types.d.ts +76 -0
- package/dist/hearth/wake-proxy/types.js +1 -0
- package/dist/index.js +3 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +6 -5
- package/dist/pi-extensions/canvas-context-intro.js +7 -7
- package/dist/pi-extensions/canvas-doc-substrate.js +19 -22
- package/dist/pi-extensions/canvas-nav.js +15 -8
- package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
- package/dist/pi-extensions/canvas-stophook.js +78 -11
- package/dist/types.d.ts +39 -20
- package/dist/types.js +38 -13
- package/dist/web/runtime.js +141 -42
- package/dist/web/transport-http.js +7 -5
- package/dist/web/transport-stream.d.ts +3 -0
- package/dist/web/transport-stream.js +204 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
- package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
- package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
- package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
- package/dist/web-client/assets/index-BZUxTkv5.css +2 -0
- package/dist/web-client/assets/index-D36PNBj4.js +80 -0
- package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
- package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
- package/dist/web-client/index.html +3 -2
- package/package.json +32 -10
- package/dist/core/__tests__/error-stall-recycle.test.js +0 -141
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +0 -57
- package/dist/core/skill-sync/builtins.js +0 -112
- package/dist/core/skill-sync/claude-plugins.d.ts +0 -23
- package/dist/core/skill-sync/claude-plugins.js +0 -71
- package/dist/core/skill-sync/engine.d.ts +0 -42
- package/dist/core/skill-sync/engine.js +0 -633
- package/dist/core/skill-sync/manifest.d.ts +0 -64
- package/dist/core/skill-sync/manifest.js +0 -181
- package/dist/core/skill-sync/profile.d.ts +0 -76
- package/dist/core/skill-sync/profile.js +0 -173
- package/dist/core/skill-sync/snapshot.d.ts +0 -57
- package/dist/core/skill-sync/snapshot.js +0 -120
- package/dist/pi-extensions/canvas-commands.d.ts +0 -37
- package/dist/pi-extensions/canvas-commands.js +0 -113
- package/dist/pi-extensions/canvas-resume.d.ts +0 -21
- package/dist/pi-extensions/canvas-resume.js +0 -83
- package/dist/pi-extensions/canvas-view.d.ts +0 -21
- package/dist/pi-extensions/canvas-view.js +0 -76
- package/dist/web-client/assets/index-BUvQb4hR.css +0 -2
- package/dist/web-client/assets/index-ClLQXYAE.js +0 -10
- /package/dist/{core/__tests__/error-stall-recycle.test.d.ts → clients/attach/__tests__/autocomplete-and-bash-mode.test.d.ts} +0 -0
- /package/dist/{core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts → clients/attach/__tests__/bash-bang-routing.test.d.ts} +0 -0
|
@@ -82,6 +82,13 @@ export interface PruneResult {
|
|
|
82
82
|
pruned: PrunedNode[];
|
|
83
83
|
dryRun: boolean;
|
|
84
84
|
}
|
|
85
|
+
/** Hard-delete ONE node: drop its row (the edges→nodes FK, ON DELETE CASCADE,
|
|
86
|
+
* GCs its edges) and remove its on-disk `nodes/<id>/` dir. The single-node
|
|
87
|
+
* analogue of {@link pruneNodes}, for IMMEDIATE reaping — e.g. an empty node the
|
|
88
|
+
* user closed or detached from. Pure persisted-state removal: the caller MUST
|
|
89
|
+
* have already torn down the broker engine + viewer (this does not signal any
|
|
90
|
+
* process). Best-effort dir removal after the row delete, mirroring pruneNodes. */
|
|
91
|
+
export declare function deleteNode(nodeId: string): void;
|
|
85
92
|
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
86
93
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
87
94
|
* node rows + dirs. The edges→nodes FK (`ON DELETE CASCADE`, migration v4) GCs
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// Source-of-truth split: a node's meta.json is canonical for its own fields;
|
|
5
5
|
// the db row is a queryable index re-derivable from it. The subscribes_to edges
|
|
6
6
|
// are db-authoritative (mutable, many-writers — what WAL is for).
|
|
7
|
-
import { existsSync, readFileSync,
|
|
8
|
-
import { join } from 'node:path';
|
|
7
|
+
import { existsSync, readFileSync, renameSync, readdirSync, rmSync, openSync, writeSync, fsyncSync, closeSync, } from 'node:fs';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
9
|
import { openDb } from './db.js';
|
|
10
10
|
import { isPidAlive } from './pid.js';
|
|
11
11
|
import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, } from './paths.js';
|
|
@@ -40,14 +40,53 @@ function readMeta(nodeId) {
|
|
|
40
40
|
// Legacy metas may still carry runtime fields on disk; toIdentity-on-read is
|
|
41
41
|
// unnecessary (callers go through getNode, which overlays the row), but the
|
|
42
42
|
// raw parse is typed as identity — extra props are ignored.
|
|
43
|
-
|
|
43
|
+
//
|
|
44
|
+
// Tolerate an EMPTY or unparseable meta.json by treating it as MISSING (null)
|
|
45
|
+
// rather than throwing. A bare write-temp-then-rename without fsync can leave a
|
|
46
|
+
// 0-byte meta.json on an NFS-backed home after the holding process/sandbox dies
|
|
47
|
+
// (see writeMeta + the Hearth M0 cold-boot bug); a single corrupt node must not
|
|
48
|
+
// crash rebuildIndex/revive for the whole canvas. Recovery (re-seed from a
|
|
49
|
+
// durable snapshot) is the caller's job.
|
|
50
|
+
const raw = readFileSync(p, 'utf8');
|
|
51
|
+
if (raw.trim() === '')
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
44
59
|
}
|
|
45
|
-
/** Serialize ONLY the identity subset → meta.json never holds runtime fields.
|
|
60
|
+
/** Serialize ONLY the identity subset → meta.json never holds runtime fields.
|
|
61
|
+
*
|
|
62
|
+
* Durable write: fsync the temp file before the rename, then fsync the parent
|
|
63
|
+
* directory so the rename itself is on disk. A bare writeFileSync+rename can
|
|
64
|
+
* leave a 0-byte meta.json on an NFS-backed home (Hearth's durable Volume mounts
|
|
65
|
+
* `local_lock=none,noac`): when meta.json is held open, the rename silly-renames
|
|
66
|
+
* the open inode and the new content is never flushed, so a sandbox
|
|
67
|
+
* recreate-from-template loses it. meta.json is written rarely (birth, polymorph,
|
|
68
|
+
* session-id capture, naming) so the per-write fsync cost is negligible.
|
|
69
|
+
* Bug: Hearth M0 cold-boot — 0-byte meta.json → rebuild-index/revive JSON.parse(''). */
|
|
46
70
|
function writeMeta(meta) {
|
|
47
71
|
const p = nodeMetaPath(meta.node_id);
|
|
48
72
|
const tmp = `${p}.tmp`;
|
|
49
|
-
|
|
73
|
+
const data = JSON.stringify(toIdentity(meta), null, 2);
|
|
74
|
+
const fd = openSync(tmp, 'w');
|
|
75
|
+
try {
|
|
76
|
+
writeSync(fd, data);
|
|
77
|
+
fsyncSync(fd);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
closeSync(fd);
|
|
81
|
+
}
|
|
50
82
|
renameSync(tmp, p);
|
|
83
|
+
const dirFd = openSync(dirname(p), 'r');
|
|
84
|
+
try {
|
|
85
|
+
fsyncSync(dirFd);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
closeSync(dirFd);
|
|
89
|
+
}
|
|
51
90
|
}
|
|
52
91
|
// ---------------------------------------------------------------------------
|
|
53
92
|
// row index — identity columns are a derived projection of meta; runtime
|
|
@@ -356,6 +395,16 @@ export function rebuildIndex() {
|
|
|
356
395
|
recordSpawn(meta.node_id, prov);
|
|
357
396
|
}
|
|
358
397
|
}
|
|
398
|
+
/** Hard-delete ONE node: drop its row (the edges→nodes FK, ON DELETE CASCADE,
|
|
399
|
+
* GCs its edges) and remove its on-disk `nodes/<id>/` dir. The single-node
|
|
400
|
+
* analogue of {@link pruneNodes}, for IMMEDIATE reaping — e.g. an empty node the
|
|
401
|
+
* user closed or detached from. Pure persisted-state removal: the caller MUST
|
|
402
|
+
* have already torn down the broker engine + viewer (this does not signal any
|
|
403
|
+
* process). Best-effort dir removal after the row delete, mirroring pruneNodes. */
|
|
404
|
+
export function deleteNode(nodeId) {
|
|
405
|
+
openDb().prepare('DELETE FROM nodes WHERE node_id = ?').run(nodeId);
|
|
406
|
+
rmSync(nodeDir(nodeId), { recursive: true, force: true });
|
|
407
|
+
}
|
|
359
408
|
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
360
409
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
361
410
|
* node rows + dirs. The edges→nodes FK (`ON DELETE CASCADE`, migration v4) GCs
|
package/dist/core/canvas/db.js
CHANGED
|
@@ -44,15 +44,27 @@ CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
|
|
|
44
44
|
CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
|
|
45
45
|
`);
|
|
46
46
|
}
|
|
47
|
+
/** The node row's current columns, used to make additive migrations safe to
|
|
48
|
+
* re-run against a partially applied schema. */
|
|
49
|
+
function nodeColumns(db) {
|
|
50
|
+
return new Set(db.prepare('PRAGMA table_info(nodes)').all().map((r) => r.name));
|
|
51
|
+
}
|
|
52
|
+
function addNodeColumnIfMissing(db, columns, name, ddl) {
|
|
53
|
+
if (columns.has(name))
|
|
54
|
+
return;
|
|
55
|
+
db.exec(ddl);
|
|
56
|
+
columns.add(name);
|
|
57
|
+
}
|
|
47
58
|
/** v2 — additive runtime columns the keystone (Phase 2) will make
|
|
48
59
|
* authoritative: `intent, pi_pid, window, tmux_session`. `status` already
|
|
49
60
|
* lives in the baseline row, so it is NOT re-added here. All four default to
|
|
50
61
|
* NULL, so nothing observes a behavior change until a later phase reads them. */
|
|
51
62
|
function addRuntimeColumns(db) {
|
|
52
|
-
|
|
53
|
-
db
|
|
54
|
-
db
|
|
55
|
-
db
|
|
63
|
+
const columns = nodeColumns(db);
|
|
64
|
+
addNodeColumnIfMissing(db, columns, 'intent', `ALTER TABLE nodes ADD COLUMN intent TEXT;`);
|
|
65
|
+
addNodeColumnIfMissing(db, columns, 'pi_pid', `ALTER TABLE nodes ADD COLUMN pi_pid INTEGER;`);
|
|
66
|
+
addNodeColumnIfMissing(db, columns, 'window', `ALTER TABLE nodes ADD COLUMN window TEXT;`);
|
|
67
|
+
addNodeColumnIfMissing(db, columns, 'tmux_session', `ALTER TABLE nodes ADD COLUMN tmux_session TEXT;`);
|
|
56
68
|
}
|
|
57
69
|
/** v3 — DATA backfill (keystone, Phase 2). The runtime fields
|
|
58
70
|
* (`intent, pi_pid, window, tmux_session`) become authoritative in the row;
|
|
@@ -190,7 +202,8 @@ CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
|
|
|
190
202
|
* Defaults NULL — nothing reads it until the placement layer lands, so this
|
|
191
203
|
* observes no behavior change. Additive, forward-only. */
|
|
192
204
|
function addPaneColumn(db) {
|
|
193
|
-
|
|
205
|
+
const columns = nodeColumns(db);
|
|
206
|
+
addNodeColumnIfMissing(db, columns, 'pane', `ALTER TABLE nodes ADD COLUMN pane TEXT;`);
|
|
194
207
|
}
|
|
195
208
|
/** v6 — the `focuses` table: durable, PLURAL on-screen viewports, one row per
|
|
196
209
|
* viewport (Q7 widens canvas.db from "topology" to "topology + focuses"). Each
|
|
@@ -250,7 +263,8 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_wakeups_deadline ON wakeups(node_id) WHERE
|
|
|
250
263
|
* Defaults NULL ⇒ `'tmux'` (every existing node is tmux-hosted); no data
|
|
251
264
|
* backfill. Additive, forward-only. */
|
|
252
265
|
function addHostKindColumn(db) {
|
|
253
|
-
|
|
266
|
+
const columns = nodeColumns(db);
|
|
267
|
+
addNodeColumnIfMissing(db, columns, 'host_kind', `ALTER TABLE nodes ADD COLUMN host_kind TEXT;`);
|
|
254
268
|
}
|
|
255
269
|
/** The ordered migration list. Index `i` is migration version `i + 1`; the db's
|
|
256
270
|
* `user_version` tracks how many have been applied. Append only. */
|
|
@@ -284,8 +298,21 @@ const handles = new Map();
|
|
|
284
298
|
export function openDb() {
|
|
285
299
|
const path = canvasDbPath();
|
|
286
300
|
const existing = handles.get(path);
|
|
287
|
-
if (existing)
|
|
288
|
-
|
|
301
|
+
if (existing) {
|
|
302
|
+
try {
|
|
303
|
+
existing.prepare('PRAGMA database_list').all();
|
|
304
|
+
return existing;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
try {
|
|
308
|
+
existing.close();
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
/* ignore */
|
|
312
|
+
}
|
|
313
|
+
handles.delete(path);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
289
316
|
ensureHome();
|
|
290
317
|
const db = new DatabaseSync(path);
|
|
291
318
|
db.exec('PRAGMA journal_mode = WAL;');
|
|
@@ -15,13 +15,11 @@ export interface NamedNode {
|
|
|
15
15
|
* message). Never empty. */
|
|
16
16
|
export declare function fullName(node: NamedNode): string;
|
|
17
17
|
/** The pi session display name — the editor label in the top-left. Format is
|
|
18
|
-
* `<kind> (<mode>)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* cycle working the auth refactor. The name segment is omitted while it
|
|
23
|
-
* collapses to the bare kind (a bare root before its first message is named).
|
|
18
|
+
* `<fullName> <kind> (<mode>)`, where `<fullName>` is the node's handle plus
|
|
19
|
+
* the pi-generated description of its first task (see fullName). The name
|
|
20
|
+
* segment is omitted while it collapses to the bare kind (a bare root before
|
|
21
|
+
* its first message is named), so the fallback is just `<kind> (<mode>)`.
|
|
24
22
|
* Recomputed from meta on every revive (and pushed live via pi.setSessionName
|
|
25
|
-
* when a bare root is named mid-session), so a base→orchestrator polymorph
|
|
26
|
-
*
|
|
23
|
+
* when a bare root is named mid-session), so a base→orchestrator polymorph or
|
|
24
|
+
* a first-message naming update the label immediately. */
|
|
27
25
|
export declare function editorLabel(meta: NodeMeta): string;
|
|
@@ -19,18 +19,15 @@ export function fullName(node) {
|
|
|
19
19
|
return combined !== '' ? combined : node.name;
|
|
20
20
|
}
|
|
21
21
|
/** The pi session display name — the editor label in the top-left. Format is
|
|
22
|
-
* `<kind> (<mode>)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* cycle working the auth refactor. The name segment is omitted while it
|
|
27
|
-
* collapses to the bare kind (a bare root before its first message is named).
|
|
22
|
+
* `<fullName> <kind> (<mode>)`, where `<fullName>` is the node's handle plus
|
|
23
|
+
* the pi-generated description of its first task (see fullName). The name
|
|
24
|
+
* segment is omitted while it collapses to the bare kind (a bare root before
|
|
25
|
+
* its first message is named), so the fallback is just `<kind> (<mode>)`.
|
|
28
26
|
* Recomputed from meta on every revive (and pushed live via pi.setSessionName
|
|
29
|
-
* when a bare root is named mid-session), so a base→orchestrator polymorph
|
|
30
|
-
*
|
|
27
|
+
* when a bare root is named mid-session), so a base→orchestrator polymorph or
|
|
28
|
+
* a first-message naming update the label immediately. */
|
|
31
29
|
export function editorLabel(meta) {
|
|
32
|
-
const base = `${meta.kind} (${meta.mode})`;
|
|
33
30
|
const full = fullName(meta);
|
|
34
|
-
const
|
|
35
|
-
return full !== '' && full !== meta.kind ? `${
|
|
31
|
+
const base = `${meta.kind} (${meta.mode})`;
|
|
32
|
+
return full !== '' && full !== meta.kind ? `${full} ${base}` : base;
|
|
36
33
|
}
|
|
@@ -18,8 +18,9 @@ export declare const RED = "\u001B[31m";
|
|
|
18
18
|
export declare const YELLOW = "\u001B[33m";
|
|
19
19
|
export declare const CYAN = "\u001B[36m";
|
|
20
20
|
export declare const GRAY = "\u001B[90m";
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/** Overlay-aware glyph for a node: hanging ⚠ outranks streaming ⟳, which
|
|
22
|
+
* outranks the base status glyph. */
|
|
23
|
+
export declare function nodeGlyph(node: NodeMeta | null): string;
|
|
23
24
|
/** Visible width, ignoring ANSI escapes. */
|
|
24
25
|
export declare function visibleWidth(s: string): number;
|
|
25
26
|
/** Truncate to `max` VISIBLE columns: escape sequences are copied through
|
|
@@ -151,8 +152,8 @@ export declare function computeDefaultExpanded(root: string, self: string): Set<
|
|
|
151
152
|
export declare function buildGraphModel(self: string, folds: FoldState, expand?: ReadonlySet<string>): FlatRow[];
|
|
152
153
|
/** Render one GRAPH row. CURSOR (selected) → reverse-video bar; an ATTACHED
|
|
153
154
|
* (human-watched) node → a coloured background bar; SELF → bold name. The
|
|
154
|
-
* cursor outranks the attached tint when both land on the same row.
|
|
155
|
-
*
|
|
155
|
+
* cursor outranks the attached tint when both land on the same row. The
|
|
156
|
+
* overlay-aware glyph alone carries the live signal (status / ⟳ / ⚠). */
|
|
156
157
|
export declare function renderGraphRow(r: FlatRow, isCursor: boolean, focused: ReadonlySet<string>, activeBelow: ReadonlyMap<string, number>, asks: Record<string, number>): string;
|
|
157
158
|
/** Total lines the GRAPH widget may emit. pi hard-caps extension widgets at
|
|
158
159
|
* MAX_WIDGET_LINES — anything past that pi truncates itself, eating our own
|
|
@@ -15,6 +15,11 @@ import { execFileSync } from 'node:child_process';
|
|
|
15
15
|
import { existsSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { getNode, subscribersOf, subscriptionsOf, jobDir, listFocuses } from './index.js';
|
|
18
|
+
import { isPidAlive } from './pid.js';
|
|
19
|
+
import { isBusy } from '../runtime/busy.js';
|
|
20
|
+
import { readFault } from '../runtime/fault.js';
|
|
21
|
+
import { activeFaultForDisplay } from './render.js';
|
|
22
|
+
import { resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
18
23
|
// ---------------------------------------------------------------------------
|
|
19
24
|
// Tuning constants
|
|
20
25
|
// ---------------------------------------------------------------------------
|
|
@@ -27,9 +32,9 @@ export const VIEWPORT_FALLBACK_ROWS = 30;
|
|
|
27
32
|
// ANSI styling. pi renders embedded escapes in widget lines and measures width
|
|
28
33
|
// ANSI-aware, so raw escapes are safe and need no pi-tui dependency. The cursor
|
|
29
34
|
// (selected row) uses a theme-agnostic ATTRIBUTE (reverse), so it reads under
|
|
30
|
-
// NO_COLOR; the attached-row tint is a background COLOUR, while the
|
|
31
|
-
// (
|
|
32
|
-
// stripped.
|
|
35
|
+
// NO_COLOR; the attached-row tint is a background COLOUR, while the overlay-
|
|
36
|
+
// aware glyph (●/○/✓/✗/⟳/⚠) carries the live signal independently, even where
|
|
37
|
+
// colour is stripped.
|
|
33
38
|
// ---------------------------------------------------------------------------
|
|
34
39
|
export const ESC = '\x1b[';
|
|
35
40
|
export const RESET = `${ESC}0m`;
|
|
@@ -45,18 +50,25 @@ export const RED = `${ESC}31m`;
|
|
|
45
50
|
export const YELLOW = `${ESC}33m`;
|
|
46
51
|
export const CYAN = `${ESC}36m`;
|
|
47
52
|
export const GRAY = `${ESC}90m`;
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
function hangingFor(node) {
|
|
54
|
+
return activeFaultForDisplay(node, readFault(node.node_id));
|
|
55
|
+
}
|
|
56
|
+
function streamingFor(node) {
|
|
57
|
+
if (node.status !== 'active' && node.status !== 'idle')
|
|
58
|
+
return false;
|
|
59
|
+
if (!isPidAlive(node.pi_pid))
|
|
60
|
+
return false;
|
|
61
|
+
// `isBusy` (the canonical mid-turn marker) AND-ed with the live pid above —
|
|
62
|
+
// a stale marker from a crashed pi fails the pid check, never the existsSync.
|
|
63
|
+
return isBusy(node.node_id);
|
|
64
|
+
}
|
|
65
|
+
/** Overlay-aware glyph for a node: hanging ⚠ outranks streaming ⟳, which
|
|
66
|
+
* outranks the base status glyph. */
|
|
67
|
+
export function nodeGlyph(node) {
|
|
50
68
|
if (node === null)
|
|
51
69
|
return '?';
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
case 'idle': return `${GRAY}○${RESET}`;
|
|
55
|
-
case 'done': return `${CYAN}✓${RESET}`;
|
|
56
|
-
case 'dead': return `${RED}✗${RESET}`;
|
|
57
|
-
case 'canceled': return `${YELLOW}⊘${RESET}`;
|
|
58
|
-
default: return '?';
|
|
59
|
-
}
|
|
70
|
+
const v = resolveNodeVisual(node.status, { streaming: streamingFor(node), hanging: hangingFor(node) });
|
|
71
|
+
return `${ESC}${v.color}m${v.glyph}${RESET}`;
|
|
60
72
|
}
|
|
61
73
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
62
74
|
/** Visible width, ignoring ANSI escapes. */
|
|
@@ -451,8 +463,8 @@ export function buildGraphModel(self, folds, expand) {
|
|
|
451
463
|
}
|
|
452
464
|
/** Render one GRAPH row. CURSOR (selected) → reverse-video bar; an ATTACHED
|
|
453
465
|
* (human-watched) node → a coloured background bar; SELF → bold name. The
|
|
454
|
-
* cursor outranks the attached tint when both land on the same row.
|
|
455
|
-
*
|
|
466
|
+
* cursor outranks the attached tint when both land on the same row. The
|
|
467
|
+
* overlay-aware glyph alone carries the live signal (status / ⟳ / ⚠). */
|
|
456
468
|
export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
|
|
457
469
|
const wrap = (line, attached) => isCursor ? fillBar(line, fillWidth(), REVERSE)
|
|
458
470
|
: attached ? fillBar(line, fillWidth(), BG_ATTACHED)
|
|
@@ -462,7 +474,7 @@ export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
|
|
|
462
474
|
return wrap(line, false);
|
|
463
475
|
}
|
|
464
476
|
const node = cNode(r.id);
|
|
465
|
-
const dot =
|
|
477
|
+
const dot = nodeGlyph(node);
|
|
466
478
|
const rawName = navLabel(node, r.id);
|
|
467
479
|
const name = r.isSelf ? `${BOLD}${rawName}${RESET}` : rawName;
|
|
468
480
|
const kind = `${DIM}${node?.kind ?? ''}${RESET}`;
|
|
@@ -473,7 +485,9 @@ export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
|
|
|
473
485
|
const expandable = r.hasKids && r.collapsed;
|
|
474
486
|
const caret = !isCursor && expandable ? `${DIM}▸${RESET} ` : ' ';
|
|
475
487
|
const fold = expandable ? ` ${DIM}[+${childCount(r.id)}]${RESET}` : '';
|
|
476
|
-
const
|
|
488
|
+
const fault = node !== null ? activeFaultForDisplay(node, readFault(r.id)) : null;
|
|
489
|
+
const faultSuffix = fault !== null ? ` · ${faultSummary(fault)}` : '';
|
|
490
|
+
const line = `${r.branch}${caret}${dot} ${name} ${kind} ${tokens}${cycleBadge(node)}${childBadge(node)}${liveBelowBadge(node, activeBelow)}${fold}${askBadge(r.id, asks)}${activityCell(r.id, node)}${faultSuffix}`;
|
|
477
491
|
return wrap(line, isAttached(r.id, node, focused));
|
|
478
492
|
}
|
|
479
493
|
/** Total lines the GRAPH widget may emit. pi hard-caps extension widgets at
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
/** Root of the global canvas home (`~/.crouter/canvas` unless `CRTR_HOME` is set).
|
|
2
2
|
* Nested under the `.crouter` scope root so the whole runtime lives in one
|
|
3
3
|
* visible top-level dir; `canvas/` keeps node-graph runtime state separate from
|
|
4
|
-
* durable user content (
|
|
4
|
+
* durable user content (memory/plugins/marketplaces/config) at the scope root. */
|
|
5
5
|
export declare function crtrHome(): string;
|
|
6
|
+
/** Absolute path to the SQLite canvas db file. Defaults to `canvas.db` under
|
|
7
|
+
* the canvas home, but `CRTR_CANVAS_DB` overrides ONLY this file's location
|
|
8
|
+
* (its `-wal`/`-shm` siblings follow it) while leaving `nodesRoot()` and all
|
|
9
|
+
* other node data under `crtrHome()`. This split exists for the Hearth guest:
|
|
10
|
+
* node data (meta.json/sessions, plain-file writes) stays on the durable NFS
|
|
11
|
+
* Volume via `CRTR_HOME`, but canvas.db's SQLite fcntl/WAL locks would hang on
|
|
12
|
+
* NFS (`local_lock=none`) — so it must live on local disk. Parent dir is
|
|
13
|
+
* created on use so an unset-default and an override both just work. */
|
|
6
14
|
export declare function canvasDbPath(): string;
|
|
15
|
+
/** Absolute path to a node's broker `view.sock`. Defaults to `view.sock` under
|
|
16
|
+
* the node dir, but `CRTR_SOCK_DIR` overrides the PARENT dir (the socket becomes
|
|
17
|
+
* `<CRTR_SOCK_DIR>/<nodeId>.sock`) while node data stays under `crtrHome()`.
|
|
18
|
+
* Sibling of `CRTR_CANVAS_DB`, and exists for the same Hearth-guest reason: an
|
|
19
|
+
* AF_UNIX socket cannot be bound on the durable NFS Volume (`bind()` → EACCES,
|
|
20
|
+
* even where plain file writes into the same dir succeed), so the broker socket
|
|
21
|
+
* must live on local disk. Unset (the default) is unchanged for normal
|
|
22
|
+
* local-disk hosts. Parent dir is created on use so both paths just work. */
|
|
23
|
+
export declare function viewSocketPath(nodeId: string): string;
|
|
7
24
|
export declare function nodesRoot(): string;
|
|
8
25
|
export declare function nodeDir(nodeId: string): string;
|
|
9
26
|
export declare function contextDir(nodeId: string): string;
|
|
@@ -13,22 +13,51 @@
|
|
|
13
13
|
//
|
|
14
14
|
// `CRTR_HOME` overrides the root (used by tests and isolated runs).
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
|
-
import { join } from 'node:path';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
17
|
import { mkdirSync } from 'node:fs';
|
|
18
18
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
19
19
|
/** Root of the global canvas home (`~/.crouter/canvas` unless `CRTR_HOME` is set).
|
|
20
20
|
* Nested under the `.crouter` scope root so the whole runtime lives in one
|
|
21
21
|
* visible top-level dir; `canvas/` keeps node-graph runtime state separate from
|
|
22
|
-
* durable user content (
|
|
22
|
+
* durable user content (memory/plugins/marketplaces/config) at the scope root. */
|
|
23
23
|
export function crtrHome() {
|
|
24
24
|
const override = process.env['CRTR_HOME'];
|
|
25
25
|
return override !== undefined && override !== ''
|
|
26
26
|
? override
|
|
27
27
|
: join(homedir(), CRTR_DIR_NAME, 'canvas');
|
|
28
28
|
}
|
|
29
|
+
/** Absolute path to the SQLite canvas db file. Defaults to `canvas.db` under
|
|
30
|
+
* the canvas home, but `CRTR_CANVAS_DB` overrides ONLY this file's location
|
|
31
|
+
* (its `-wal`/`-shm` siblings follow it) while leaving `nodesRoot()` and all
|
|
32
|
+
* other node data under `crtrHome()`. This split exists for the Hearth guest:
|
|
33
|
+
* node data (meta.json/sessions, plain-file writes) stays on the durable NFS
|
|
34
|
+
* Volume via `CRTR_HOME`, but canvas.db's SQLite fcntl/WAL locks would hang on
|
|
35
|
+
* NFS (`local_lock=none`) — so it must live on local disk. Parent dir is
|
|
36
|
+
* created on use so an unset-default and an override both just work. */
|
|
29
37
|
export function canvasDbPath() {
|
|
38
|
+
const override = process.env['CRTR_CANVAS_DB'];
|
|
39
|
+
if (override !== undefined && override !== '') {
|
|
40
|
+
mkdirSync(dirname(override), { recursive: true });
|
|
41
|
+
return override;
|
|
42
|
+
}
|
|
30
43
|
return join(crtrHome(), 'canvas.db');
|
|
31
44
|
}
|
|
45
|
+
/** Absolute path to a node's broker `view.sock`. Defaults to `view.sock` under
|
|
46
|
+
* the node dir, but `CRTR_SOCK_DIR` overrides the PARENT dir (the socket becomes
|
|
47
|
+
* `<CRTR_SOCK_DIR>/<nodeId>.sock`) while node data stays under `crtrHome()`.
|
|
48
|
+
* Sibling of `CRTR_CANVAS_DB`, and exists for the same Hearth-guest reason: an
|
|
49
|
+
* AF_UNIX socket cannot be bound on the durable NFS Volume (`bind()` → EACCES,
|
|
50
|
+
* even where plain file writes into the same dir succeed), so the broker socket
|
|
51
|
+
* must live on local disk. Unset (the default) is unchanged for normal
|
|
52
|
+
* local-disk hosts. Parent dir is created on use so both paths just work. */
|
|
53
|
+
export function viewSocketPath(nodeId) {
|
|
54
|
+
const override = process.env['CRTR_SOCK_DIR'];
|
|
55
|
+
if (override !== undefined && override !== '') {
|
|
56
|
+
mkdirSync(override, { recursive: true });
|
|
57
|
+
return join(override, `${nodeId}.sock`);
|
|
58
|
+
}
|
|
59
|
+
return join(nodeDir(nodeId), 'view.sock');
|
|
60
|
+
}
|
|
32
61
|
export function nodesRoot() {
|
|
33
62
|
return join(crtrHome(), 'nodes');
|
|
34
63
|
}
|
|
@@ -73,6 +102,12 @@ export function injectedDocsPath(nodeId) {
|
|
|
73
102
|
}
|
|
74
103
|
/** Create the full directory skeleton for a node. Idempotent. */
|
|
75
104
|
export function ensureNodeDirs(nodeId) {
|
|
105
|
+
// Refuse an empty nodeId: it would mint stray `nodes/context`, `nodes/job`,
|
|
106
|
+
// `nodes/reports` siblings (non-node entries the test harness's node-dir
|
|
107
|
+
// scan would miscount). A node skeleton must belong to a real node id.
|
|
108
|
+
if (!nodeId) {
|
|
109
|
+
throw new Error('ensureNodeDirs: empty nodeId — refusing to create stray node-skeleton dirs under nodes/');
|
|
110
|
+
}
|
|
76
111
|
for (const d of [contextDir(nodeId), jobDir(nodeId), reportsDir(nodeId)]) {
|
|
77
112
|
mkdirSync(d, { recursive: true });
|
|
78
113
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { type Fault } from '../runtime/fault.js';
|
|
1
2
|
import type { NodeStatus, Lifecycle } from './types.js';
|
|
3
|
+
export declare function activeFaultForDisplay<T extends {
|
|
4
|
+
status: NodeStatus;
|
|
5
|
+
pi_pid?: number | null;
|
|
6
|
+
}>(node: T, fault: Fault | null): Fault | null;
|
|
2
7
|
/**
|
|
3
8
|
* Render the subscription sub-DAG rooted at `rootId` as an ASCII tree.
|
|
4
9
|
* The root is the first line (no connector prefix); children are indented.
|
|
@@ -59,6 +64,13 @@ export interface DashboardRow {
|
|
|
59
64
|
* live "is it generating?" cue. Computed on the cheap boot path for LIVE nodes
|
|
60
65
|
* only (dormant rows are always false). */
|
|
61
66
|
streaming?: boolean;
|
|
67
|
+
/** Set when the node is parked on an active fault. The otherwise-invisible
|
|
68
|
+
* "stuck, awaiting recovery" window. Mutually exclusive with `streaming`
|
|
69
|
+
* (hanging means the turn ended, so `busy` is gone); when set,
|
|
70
|
+
* dashboardRowsAll forces `streaming:false`. Computed on the cheap boot path
|
|
71
|
+
* for every node, then suppressed only for stale daemon-owned provider faults
|
|
72
|
+
* whose broker pid is gone. */
|
|
73
|
+
hanging?: Fault | null;
|
|
62
74
|
/** True when a viewer (focus row) is attached to the node — i.e. someone has it
|
|
63
75
|
* open on screen. Set on the cheap boot path from the one listFocuses() query. */
|
|
64
76
|
viewed?: boolean;
|
|
@@ -70,6 +82,19 @@ export interface DashboardRow {
|
|
|
70
82
|
* session (prompts + lastAssistant). Idempotency marker for the selected row. */
|
|
71
83
|
previewLoaded?: boolean;
|
|
72
84
|
}
|
|
85
|
+
/** Did this node's engine ever PRODUCE something — an assistant message with real
|
|
86
|
+
* text, a tool call, or non-empty reasoning? The signal that a node did work and
|
|
87
|
+
* is not an empty shell. A never-revived node (no session file) is trivially
|
|
88
|
+
* false; an assistant turn that is only an empty/aborted `thinking` stub does NOT
|
|
89
|
+
* count (that's a node started and killed before it did anything). CONSERVATIVE
|
|
90
|
+
* by design: ANY substance keeps the node, so the reap never deletes real work
|
|
91
|
+
* — a tool-call-only turn (no final text) still counts. Drives reap-on-close/
|
|
92
|
+
* detach. One file read with an early exit on the first substantive turn. */
|
|
93
|
+
export declare function nodeHasAssistantMessage(sessionFile: string | null | undefined): boolean;
|
|
94
|
+
/** {@link isStreaming} keyed by node id alone — hydrates the broker pid off the
|
|
95
|
+
* db row. The exported "is this node actively generating right now?" guard so a
|
|
96
|
+
* reap never nukes a node mid-first-turn before its output lands on disk. */
|
|
97
|
+
export declare function isNodeStreaming(nodeId: string): boolean;
|
|
73
98
|
/** One row per node visible in the sub-DAG of `rootId` (including root). */
|
|
74
99
|
export declare function dashboardRows(rootId: string): DashboardRow[];
|
|
75
100
|
/**
|
|
@@ -20,16 +20,22 @@ import { jobDir, contextDir } from './paths.js';
|
|
|
20
20
|
import { countAsks, asksForNodes } from './attention.js';
|
|
21
21
|
import { isPidAlive } from './pid.js';
|
|
22
22
|
import { listFocuses } from './focuses.js';
|
|
23
|
+
import { resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
24
|
+
import { readFault } from '../runtime/fault.js';
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
24
|
-
//
|
|
26
|
+
// Active-fault overlay: stale daemon-owned provider faults are hidden once the
|
|
27
|
+
// broker pid is gone; manual/client faults stay visible until cleared.
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
29
|
+
export function activeFaultForDisplay(node, fault) {
|
|
30
|
+
if (fault === null)
|
|
31
|
+
return null;
|
|
32
|
+
if (fault.link === 'pi→provider' && fault.retry.by === 'daemon' && fault.retry.disposition === 'auto' && !isPidAlive(node.pi_pid))
|
|
33
|
+
return null;
|
|
34
|
+
return fault;
|
|
35
|
+
}
|
|
36
|
+
function hangingFor(node) {
|
|
37
|
+
return activeFaultForDisplay(node, readFault(node.node_id));
|
|
38
|
+
}
|
|
33
39
|
function readNodeTelemetry(nodeId) {
|
|
34
40
|
const path = join(jobDir(nodeId), 'telemetry.json');
|
|
35
41
|
if (!existsSync(path))
|
|
@@ -57,12 +63,14 @@ function nodeLine(nodeId, indent, connector) {
|
|
|
57
63
|
// Node id is in the db but meta.json is gone — paranoid guard.
|
|
58
64
|
return `${indent}${connector}? <missing meta: ${nodeId}>`;
|
|
59
65
|
}
|
|
60
|
-
const
|
|
66
|
+
const hanging = hangingFor(node);
|
|
67
|
+
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
61
68
|
const tel = readNodeTelemetry(nodeId);
|
|
62
69
|
const ctx = fmtCtx(tel.tokens_in);
|
|
63
70
|
const asks = countAsks(nodeId);
|
|
64
71
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
65
|
-
|
|
72
|
+
const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
|
|
73
|
+
return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`;
|
|
66
74
|
}
|
|
67
75
|
/**
|
|
68
76
|
* Recursively walk the subscription sub-DAG rooted at `nodeId`, appending
|
|
@@ -112,9 +120,11 @@ export function renderTree(rootId) {
|
|
|
112
120
|
const ctx = fmtCtx(tel.tokens_in);
|
|
113
121
|
const asks = countAsks(rootId);
|
|
114
122
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
115
|
-
const
|
|
123
|
+
const hanging = hangingFor(node);
|
|
124
|
+
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
125
|
+
const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
|
|
116
126
|
const out = [];
|
|
117
|
-
out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`);
|
|
127
|
+
out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`);
|
|
118
128
|
// visited starts with root already rendered (walkTree doesn't re-emit root).
|
|
119
129
|
const visited = new Set([rootId]);
|
|
120
130
|
const children = subscriptionsOf(rootId);
|
|
@@ -261,6 +271,77 @@ function readSessionParts(sessionFile) {
|
|
|
261
271
|
}
|
|
262
272
|
return { prompts, lastAssistant };
|
|
263
273
|
}
|
|
274
|
+
/** Did this node's engine ever PRODUCE something — an assistant message with real
|
|
275
|
+
* text, a tool call, or non-empty reasoning? The signal that a node did work and
|
|
276
|
+
* is not an empty shell. A never-revived node (no session file) is trivially
|
|
277
|
+
* false; an assistant turn that is only an empty/aborted `thinking` stub does NOT
|
|
278
|
+
* count (that's a node started and killed before it did anything). CONSERVATIVE
|
|
279
|
+
* by design: ANY substance keeps the node, so the reap never deletes real work
|
|
280
|
+
* — a tool-call-only turn (no final text) still counts. Drives reap-on-close/
|
|
281
|
+
* detach. One file read with an early exit on the first substantive turn. */
|
|
282
|
+
export function nodeHasAssistantMessage(sessionFile) {
|
|
283
|
+
if (sessionFile === undefined || sessionFile === null || sessionFile === '')
|
|
284
|
+
return false;
|
|
285
|
+
let lines;
|
|
286
|
+
try {
|
|
287
|
+
if (!existsSync(sessionFile))
|
|
288
|
+
return false;
|
|
289
|
+
lines = readFileSync(sessionFile, 'utf8').split('\n');
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
for (const line of lines) {
|
|
295
|
+
if (line === '' || (line.indexOf('"role":"assistant"') === -1 && line.indexOf('"role": "assistant"') === -1))
|
|
296
|
+
continue;
|
|
297
|
+
let rec;
|
|
298
|
+
try {
|
|
299
|
+
rec = JSON.parse(line);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (rec.type !== 'message' || rec.message?.role !== 'assistant')
|
|
305
|
+
continue;
|
|
306
|
+
if (assistantContentIsSubstantive(rec.message.content))
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
/** True when an assistant message's content has at least one block that PRODUCED
|
|
312
|
+
* something: non-empty text, non-empty thinking, a tool call (`toolCall`/
|
|
313
|
+
* `tool_use`/…), or any other non-thinking/text block type. The lone non-case is
|
|
314
|
+
* an empty thinking stub (`[{type:'thinking',thinking:''}]`) or empty content —
|
|
315
|
+
* what an immediately-aborted first turn leaves behind. */
|
|
316
|
+
function assistantContentIsSubstantive(content) {
|
|
317
|
+
if (typeof content === 'string')
|
|
318
|
+
return content.trim() !== '';
|
|
319
|
+
if (!Array.isArray(content))
|
|
320
|
+
return false;
|
|
321
|
+
for (const block of content) {
|
|
322
|
+
if (block === null || typeof block !== 'object')
|
|
323
|
+
continue;
|
|
324
|
+
const b = block;
|
|
325
|
+
const t = (b.type ?? '').toLowerCase();
|
|
326
|
+
if (t === 'text') {
|
|
327
|
+
if (typeof b.text === 'string' && b.text.trim() !== '')
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
else if (t === 'thinking') {
|
|
331
|
+
if (typeof b.thinking === 'string' && b.thinking.trim() !== '')
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
else
|
|
335
|
+
return true; // toolCall / tool_use / toolResult / any other block = real output
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
/** {@link isStreaming} keyed by node id alone — hydrates the broker pid off the
|
|
340
|
+
* db row. The exported "is this node actively generating right now?" guard so a
|
|
341
|
+
* reap never nukes a node mid-first-turn before its output lands on disk. */
|
|
342
|
+
export function isNodeStreaming(nodeId) {
|
|
343
|
+
return isStreaming(nodeId, getNode(nodeId)?.pi_pid ?? null);
|
|
344
|
+
}
|
|
264
345
|
/** Is the node GENUINELY mid-turn right now? The `busy` marker (touched on
|
|
265
346
|
* agent_start, removed on agent_end) AND-ed with broker-pid liveness, so a stale
|
|
266
347
|
* marker from a crashed pi reads false. This is the live "generating?" signal
|
|
@@ -354,6 +435,7 @@ export function dashboardRowsAll() {
|
|
|
354
435
|
const focusedNodeIds = new Set(listFocuses().map((f) => f.node_id));
|
|
355
436
|
return listNodes().map((row) => {
|
|
356
437
|
const live = row.status === 'active' || row.status === 'idle';
|
|
438
|
+
const hanging = activeFaultForDisplay(row, readFault(row.node_id));
|
|
357
439
|
return {
|
|
358
440
|
node_id: row.node_id,
|
|
359
441
|
name: row.name, // handle only; enrichRow upgrades to fullName (meta.description)
|
|
@@ -366,7 +448,8 @@ export function dashboardRowsAll() {
|
|
|
366
448
|
created: row.created,
|
|
367
449
|
lifecycle: row.lifecycle,
|
|
368
450
|
mtimeMs: sessionMtime(row.node_id, row.created),
|
|
369
|
-
streaming: live ? isStreaming(row.node_id, row.pi_pid) : false,
|
|
451
|
+
streaming: hanging === null && live ? isStreaming(row.node_id, row.pi_pid) : false,
|
|
452
|
+
hanging,
|
|
370
453
|
viewed: focusedNodeIds.has(row.node_id),
|
|
371
454
|
};
|
|
372
455
|
});
|