@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
|
@@ -28,7 +28,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
28
28
|
import { createRequire } from 'node:module';
|
|
29
29
|
import { createNode, getNode, subscribersOf, subscriptionsOf, } from '../../canvas/canvas.js';
|
|
30
30
|
import { closeDb } from '../../canvas/db.js';
|
|
31
|
-
import {
|
|
31
|
+
import { isFocused } from '../../runtime/placement.js';
|
|
32
32
|
import { superviseTick } from '../../../daemon/crtrd.js';
|
|
33
33
|
import { readInboxSince } from '../../feed/inbox.js';
|
|
34
34
|
// --- locations --------------------------------------------------------------
|
|
@@ -85,8 +85,7 @@ function cleanBaseEnv() {
|
|
|
85
85
|
// ~/.crouter / ~/.claude / ~/.pi, NOT under CRTR_HOME — HOME is contained too).
|
|
86
86
|
e['CRTR_NO_BOOTSTRAP'] = '1';
|
|
87
87
|
e['CRTR_NO_AUTO_UPDATE'] = '1';
|
|
88
|
-
e['
|
|
89
|
-
e['CRTR_NO_MODE_CMDS'] = '1';
|
|
88
|
+
e['CRTR_NO_EXPORTS'] = '1';
|
|
90
89
|
e['CRTR_NO_AUTO_INIT'] = '1';
|
|
91
90
|
return e;
|
|
92
91
|
}
|
|
@@ -161,7 +160,7 @@ export async function createHarness(opts = {}) {
|
|
|
161
160
|
// home-session.test.ts.) home/ is rmSync'd in dispose.
|
|
162
161
|
writeFileSync(join(home, 'crtrd.pid'), String(process.pid), 'utf8');
|
|
163
162
|
// Pre-create the isolated session on the DEFAULT server so teardown always
|
|
164
|
-
// has a target
|
|
163
|
+
// has a kill-sessions target.
|
|
165
164
|
// ISOLATION ASSUMPTION (see header + MINOR-6): isolation is by SESSION NAME on
|
|
166
165
|
// the DEFAULT tmux server only. The runtime CLI shells `tmux` with no `-L`, so
|
|
167
166
|
// a custom-socket server (`tmux -L foo`) would be invisible to it; this harness
|
|
@@ -169,16 +168,14 @@ export async function createHarness(opts = {}) {
|
|
|
169
168
|
// server.
|
|
170
169
|
// HEADLESS: skip ALL eager tmux setup. No node in a headless run opens a
|
|
171
170
|
// window, so there is no pane to pre-seed an env into and no session to pre-
|
|
172
|
-
// create.
|
|
173
|
-
// name for home_session — dispose kill-sessions it regardless.)
|
|
171
|
+
// create.
|
|
174
172
|
if (!headless) {
|
|
175
173
|
spawnSync('tmux', ['new-session', '-d', '-s', session, '-c', CROUTER, 'sleep 100000'], {
|
|
176
174
|
stdio: 'ignore',
|
|
177
175
|
});
|
|
178
|
-
// Put CRTR_PI_BINARY in the SESSION environment so EVERY pane spawned
|
|
179
|
-
// session inherits it —
|
|
180
|
-
//
|
|
181
|
-
// yield path) the in-process piCommand there substitutes the fake-pi too.
|
|
176
|
+
// Put CRTR_PI_BINARY in the SESSION environment so EVERY pane/process spawned
|
|
177
|
+
// in this session inherits it — so any pi the runtime launches (boot, revive)
|
|
178
|
+
// resolves to the fake-pi vehicle, not a real pi.
|
|
182
179
|
spawnSync('tmux', ['set-environment', '-t', session, 'CRTR_PI_BINARY', FAKE_PI_BINARY], {
|
|
183
180
|
stdio: 'ignore',
|
|
184
181
|
});
|
|
@@ -310,6 +307,7 @@ export async function createHarness(opts = {}) {
|
|
|
310
307
|
lifecycle: o.lifecycle ?? 'terminal',
|
|
311
308
|
parent: o.parent ?? null,
|
|
312
309
|
pi_session_id: o.pi_session_id ?? null,
|
|
310
|
+
pi_session_file: o.pi_session_file ?? null,
|
|
313
311
|
status: o.status ?? 'active',
|
|
314
312
|
intent: o.intent ?? null,
|
|
315
313
|
pi_pid: o.pi_pid ?? null,
|
|
@@ -339,7 +337,13 @@ export async function createHarness(opts = {}) {
|
|
|
339
337
|
},
|
|
340
338
|
async spawnHeadlessChild(parentId, task, o = {}) {
|
|
341
339
|
const before = new Set(nodeDirs());
|
|
342
|
-
|
|
340
|
+
// POST-CUT: `--headless` is gone — EVERY node is a broker. spawnChild may
|
|
341
|
+
// still open a background `crtr attach` viewer WINDOW (the §A.3 spawn UX),
|
|
342
|
+
// but that viewer connects to the broker over view.sock as a SEPARATE
|
|
343
|
+
// process and never touches the engine — so the broker-socket gates here
|
|
344
|
+
// (zero-viewer dialogs, controller arbitration, one-writer) are unaffected
|
|
345
|
+
// by it. The node itself is supervised by pid alone.
|
|
346
|
+
const args = ['node', 'new', task, '--parent', parentId, '--cwd', CROUTER];
|
|
343
347
|
if (o.kind)
|
|
344
348
|
args.push('--kind', o.kind);
|
|
345
349
|
if (o.mode)
|
|
@@ -358,7 +362,7 @@ export async function createHarness(opts = {}) {
|
|
|
358
362
|
},
|
|
359
363
|
async spawnHeadlessChildNoBoot(parentId, task, o = {}) {
|
|
360
364
|
const before = new Set(nodeDirs());
|
|
361
|
-
const args = ['node', 'new', task, '--parent', parentId, '--cwd', CROUTER
|
|
365
|
+
const args = ['node', 'new', task, '--parent', parentId, '--cwd', CROUTER];
|
|
362
366
|
if (o.kind)
|
|
363
367
|
args.push('--kind', o.kind);
|
|
364
368
|
if (o.mode)
|
|
@@ -396,27 +400,6 @@ export async function createHarness(opts = {}) {
|
|
|
396
400
|
await awaitAgentEnd(nodeId, base, `finish agent_end for ${nodeId}`);
|
|
397
401
|
await harness.waitForPaneGone(nodeId);
|
|
398
402
|
},
|
|
399
|
-
async yieldNode(nodeId, note) {
|
|
400
|
-
const res = cli(nodeId, ['node', 'yield', note]);
|
|
401
|
-
if (res.code !== 0) {
|
|
402
|
-
throw new Error(`yieldNode(${nodeId}): node yield failed (code ${res.code})\n${res.stderr}`);
|
|
403
|
-
}
|
|
404
|
-
// node yield set intent=refresh (active, kept). Fire agent_end so the real
|
|
405
|
-
// (b') branch runs reviveInPlace (respawn-pane -k) IN the fake-pi's pane —
|
|
406
|
-
// a FRESH fake-pi boots (resume); its session_start clears intent=refresh.
|
|
407
|
-
const baseBoots = bootCount(nodeId);
|
|
408
|
-
sendCmd(nodeId, { cmd: 'stop', id: `yield-${Date.now()}` });
|
|
409
|
-
await waitFor(() => bootCount(nodeId) > baseBoots, {
|
|
410
|
-
timeoutMs: 30_000,
|
|
411
|
-
label: `fresh boot after yield for ${nodeId}`,
|
|
412
|
-
});
|
|
413
|
-
await waitFor(() => {
|
|
414
|
-
closeDb();
|
|
415
|
-
const n = getNode(nodeId);
|
|
416
|
-
return n?.intent == null && n?.status === 'active';
|
|
417
|
-
}, { timeoutMs: 20_000, label: `intent=refresh cleared after yield for ${nodeId}` });
|
|
418
|
-
await harness.awaitBoot(nodeId, { minCount: baseBoots + 1 });
|
|
419
|
-
},
|
|
420
403
|
async tick(now) {
|
|
421
404
|
closeDb();
|
|
422
405
|
await superviseTick(now);
|
|
@@ -461,8 +444,8 @@ export async function createHarness(opts = {}) {
|
|
|
461
444
|
async waitForPaneGone(nodeId, timeoutMs = 20_000) {
|
|
462
445
|
await waitFor(() => {
|
|
463
446
|
closeDb();
|
|
464
|
-
return !
|
|
465
|
-
}, { timeoutMs, label: `
|
|
447
|
+
return !isFocused(nodeId);
|
|
448
|
+
}, { timeoutMs, label: `viewer gone for ${nodeId}` });
|
|
466
449
|
},
|
|
467
450
|
waitFor,
|
|
468
451
|
node(nodeId) {
|
|
@@ -475,7 +458,7 @@ export async function createHarness(opts = {}) {
|
|
|
475
458
|
},
|
|
476
459
|
paneAlive(nodeId) {
|
|
477
460
|
closeDb();
|
|
478
|
-
return
|
|
461
|
+
return isFocused(nodeId);
|
|
479
462
|
},
|
|
480
463
|
inbox(nodeId) {
|
|
481
464
|
closeDb();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/human-node-not-supervised.test.ts
|
|
2
|
+
//
|
|
3
|
+
// BUG REGRESSION (issue #2 — "Headless node can't reach human-ask: human-kind
|
|
4
|
+
// TUI dies at boot"): a headless orchestrator's every `crtr human ask` node died
|
|
5
|
+
// at boot with "pi vehicle exited before the session came up".
|
|
6
|
+
//
|
|
7
|
+
// ROOT CAUSE: a kind:'human' row is NOT a broker-hosted agent node — it is a
|
|
8
|
+
// bookkeeping row for the `crtr human` bridge. Its decision/review TUI runs in a
|
|
9
|
+
// detached `crtr human _run` pane (NOT a pi engine), and the row's lifecycle is
|
|
10
|
+
// driven entirely by that worker's `pushFinal` (or `human cancel`). So it never
|
|
11
|
+
// records a `pi_pid` or `pi_session_id`. After the headless hard-cut made the
|
|
12
|
+
// broker the universal node host, superviseTick supervised EVERY active|idle row
|
|
13
|
+
// as a broker engine: it read the human row (pid==null, session==null) as a
|
|
14
|
+
// never-booted broker, waited out the boot grace, then crash'd it and fired
|
|
15
|
+
// surfaceBootFailure up the spine — killing the human prompt before the person
|
|
16
|
+
// could ever answer.
|
|
17
|
+
//
|
|
18
|
+
// THE FIX: superviseTick drops kind:'human' rows from all three passes. This
|
|
19
|
+
// drives the REAL daemon decision pass against a human row fabricated directly in
|
|
20
|
+
// an isolated home — two ticks spanning well past the boot grace, which is
|
|
21
|
+
// exactly what crashed it before the fix.
|
|
22
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { createNode, getNode, subscribe } from '../canvas/canvas.js';
|
|
28
|
+
import { closeDb } from '../canvas/db.js';
|
|
29
|
+
import { readInboxSince } from '../feed/inbox.js';
|
|
30
|
+
import { superviseTick } from '../../daemon/crtrd.js';
|
|
31
|
+
let home;
|
|
32
|
+
function node(id, over = {}) {
|
|
33
|
+
return {
|
|
34
|
+
node_id: id,
|
|
35
|
+
name: id,
|
|
36
|
+
created: new Date().toISOString(),
|
|
37
|
+
cwd: '/tmp/work',
|
|
38
|
+
kind: 'general',
|
|
39
|
+
mode: 'base',
|
|
40
|
+
lifecycle: 'terminal',
|
|
41
|
+
status: 'active',
|
|
42
|
+
...over,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
before(() => {
|
|
46
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-human-not-supervised-'));
|
|
47
|
+
process.env['CRTR_HOME'] = home;
|
|
48
|
+
});
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
closeDb();
|
|
51
|
+
rmSync(home, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
after(() => {
|
|
54
|
+
closeDb();
|
|
55
|
+
rmSync(home, { recursive: true, force: true });
|
|
56
|
+
delete process.env['CRTR_HOME'];
|
|
57
|
+
});
|
|
58
|
+
test('a kind:"human" bridge row is NEVER boot-failed by the daemon (it has no broker engine)', async () => {
|
|
59
|
+
// The exact shape spawnNode({kind:'human'}) mints: active, host_kind broker,
|
|
60
|
+
// but NO pi engine ever booted — so pi_pid and pi_session_id stay null.
|
|
61
|
+
createNode(node('HUMAN', {
|
|
62
|
+
kind: 'human',
|
|
63
|
+
lifecycle: 'terminal',
|
|
64
|
+
host_kind: 'broker',
|
|
65
|
+
pi_pid: null,
|
|
66
|
+
pi_session_id: null,
|
|
67
|
+
intent: null,
|
|
68
|
+
}));
|
|
69
|
+
// The asking node is auto-subscribed to its human bridge node at spawn, so a
|
|
70
|
+
// surfaceBootFailure (pushUrgent) would land in ASKER's inbox. Give it a booted
|
|
71
|
+
// session so the daemon leaves IT alone — it is only here to catch a push.
|
|
72
|
+
createNode(node('ASKER', { status: 'active', pi_session_id: 'booted' }));
|
|
73
|
+
subscribe('ASKER', 'HUMAN', true);
|
|
74
|
+
// Two ticks spanning WELL past the boot grace (REVIVE_GRACE_MS): tick 1 would
|
|
75
|
+
// arm the boot-grace clock, tick 2 (far in the future) would fire the crash +
|
|
76
|
+
// surfaceBootFailure. This is the precise sequence that killed every human-ask
|
|
77
|
+
// node before the fix.
|
|
78
|
+
const t0 = Date.now();
|
|
79
|
+
await superviseTick(t0);
|
|
80
|
+
await superviseTick(t0 + 10_000_000);
|
|
81
|
+
assert.equal(getNode('HUMAN').status, 'active', 'human bridge row stays alive — never crash-reaped as a dead broker');
|
|
82
|
+
assert.equal(readInboxSince('ASKER').length, 0, 'no surfaceBootFailure urgent push — the asker is never told its human prompt "never started"');
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/human-stranded-deliver.test.ts
|
|
2
|
+
//
|
|
3
|
+
// BUG REGRESSION (human-ask broker round-trip): a headless-broker asker strips
|
|
4
|
+
// $TMUX, so the detached `crtr human _run` worker — whose `pushFinal` is the SOLE
|
|
5
|
+
// deliver-back + reap step — never spawned. Worse, the `crtr human inbox` drain
|
|
6
|
+
// path writes response.json directly (humanloop's inbox()) and ALSO never calls
|
|
7
|
+
// pushFinal, so an answered deck strands on disk: the asker never learns the
|
|
8
|
+
// answer and the bridge node leaks active forever.
|
|
9
|
+
//
|
|
10
|
+
// This locks the SECONDARY heal: finalizeResolvedInteraction must, for a still-
|
|
11
|
+
// LIVE bridge node whose interaction is resolved on disk, deliver the answer to
|
|
12
|
+
// the asker (parent auto-subscribed) and reap the bridge node — exactly what the
|
|
13
|
+
// _run worker's pushFinal would have done. Idempotent (no double-deliver once
|
|
14
|
+
// done); a canceled-on-disk response reaps without delivering a result.
|
|
15
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { createNode, subscribe, getNode } from '../canvas/canvas.js';
|
|
21
|
+
import { closeDb } from '../canvas/db.js';
|
|
22
|
+
import { readInboxSince } from '../feed/inbox.js';
|
|
23
|
+
import { interactionDir, interactionsRoot } from '../artifact.js';
|
|
24
|
+
import { atomicWriteJson, deckPath } from '@crouton-kit/humanloop';
|
|
25
|
+
import { finalizeResolvedInteraction } from '../../commands/human/queue.js';
|
|
26
|
+
let home;
|
|
27
|
+
// A unique cwd so interactionDir (keyed off the REAL homedir + mangled cwd, NOT
|
|
28
|
+
// CRTR_HOME) lands in an isolated, cleanable subtree.
|
|
29
|
+
let workCwd;
|
|
30
|
+
function node(id, parent) {
|
|
31
|
+
return {
|
|
32
|
+
node_id: id,
|
|
33
|
+
name: id,
|
|
34
|
+
created: new Date().toISOString(),
|
|
35
|
+
cwd: workCwd,
|
|
36
|
+
kind: 'developer',
|
|
37
|
+
mode: 'base',
|
|
38
|
+
lifecycle: 'terminal',
|
|
39
|
+
status: 'active',
|
|
40
|
+
parent,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Seed an interaction dir on disk the way the kickoff + a resolution would. */
|
|
44
|
+
function seed(jobId, run, response) {
|
|
45
|
+
const idir = interactionDir(jobId, workCwd);
|
|
46
|
+
mkdirSync(idir, { recursive: true });
|
|
47
|
+
atomicWriteJson(deckPath(idir), { interactions: [] });
|
|
48
|
+
atomicWriteJson(join(idir, 'run.json'), run);
|
|
49
|
+
atomicWriteJson(join(idir, 'response.json'), response);
|
|
50
|
+
}
|
|
51
|
+
before(() => {
|
|
52
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-stranded-'));
|
|
53
|
+
process.env['CRTR_HOME'] = home;
|
|
54
|
+
workCwd = mkdtempSync(join(tmpdir(), 'crtr-stranded-cwd-'));
|
|
55
|
+
});
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
closeDb();
|
|
58
|
+
rmSync(home, { recursive: true, force: true });
|
|
59
|
+
rmSync(interactionsRoot(workCwd), { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
after(() => {
|
|
62
|
+
closeDb();
|
|
63
|
+
rmSync(home, { recursive: true, force: true });
|
|
64
|
+
rmSync(interactionsRoot(workCwd), { recursive: true, force: true });
|
|
65
|
+
rmSync(workCwd, { recursive: true, force: true });
|
|
66
|
+
delete process.env['CRTR_HOME'];
|
|
67
|
+
});
|
|
68
|
+
test('answered-but-undelivered ask delivers back to the asker and reaps the bridge', async () => {
|
|
69
|
+
// Asker A, bridge B under A (parent auto-subscribes to its child bridge).
|
|
70
|
+
createNode(node('A', null));
|
|
71
|
+
createNode(node('B', 'A'));
|
|
72
|
+
subscribe('A', 'B');
|
|
73
|
+
seed('B', { mode: 'ask', job_id: 'B' }, { responses: [{ id: 'q', selectedOptionId: 'resync', freetext: 'go ahead' }], completedAt: '2026-06-10T22:27:07.000Z' });
|
|
74
|
+
const acted = await finalizeResolvedInteraction('B');
|
|
75
|
+
assert.equal(acted, true, 'should have delivered + reaped');
|
|
76
|
+
// Bridge reaped.
|
|
77
|
+
assert.equal(getNode('B')?.status, 'done', 'bridge node must be reaped to done');
|
|
78
|
+
// Answer fanned into the asker's inbox, carrying the choice.
|
|
79
|
+
const inbox = readInboxSince('A');
|
|
80
|
+
assert.equal(inbox.length >= 1, true, 'asker inbox must receive the answer pointer');
|
|
81
|
+
const blob = JSON.stringify(inbox);
|
|
82
|
+
assert.match(blob, /resync/, 'delivered answer should carry the human selection');
|
|
83
|
+
// Idempotent: a second sweep is a no-op (node already done — no double-deliver).
|
|
84
|
+
const again = await finalizeResolvedInteraction('B');
|
|
85
|
+
assert.equal(again, false, 'must not re-deliver an already-finalized interaction');
|
|
86
|
+
});
|
|
87
|
+
test('a canceled-on-disk response reaps the bridge without delivering a result', async () => {
|
|
88
|
+
createNode(node('A', null));
|
|
89
|
+
createNode(node('B', 'A'));
|
|
90
|
+
subscribe('A', 'B');
|
|
91
|
+
seed('B', { mode: 'ask', job_id: 'B' }, { canceled: true, canceledAt: '2026-06-10T22:42:04.000Z', reason: 'stale' });
|
|
92
|
+
const acted = await finalizeResolvedInteraction('B');
|
|
93
|
+
assert.equal(acted, true);
|
|
94
|
+
assert.equal(getNode('B')?.status, 'done', 'a canceled stranded bridge must still be reaped');
|
|
95
|
+
// The asker gets a quiet "no answer is coming" note, not a result.
|
|
96
|
+
const blob = JSON.stringify(readInboxSince('A'));
|
|
97
|
+
assert.match(blob, /no answer is coming/);
|
|
98
|
+
});
|
|
99
|
+
test('an unresolved (no response.json) live bridge is left untouched', async () => {
|
|
100
|
+
createNode(node('A', null));
|
|
101
|
+
createNode(node('B', 'A'));
|
|
102
|
+
subscribe('A', 'B');
|
|
103
|
+
const idir = interactionDir('B', workCwd);
|
|
104
|
+
mkdirSync(idir, { recursive: true });
|
|
105
|
+
atomicWriteJson(join(idir, 'run.json'), { mode: 'ask', job_id: 'B' });
|
|
106
|
+
assert.equal(await finalizeResolvedInteraction('B'), false);
|
|
107
|
+
assert.equal(getNode('B')?.status, 'active', 'still-pending bridge must stay live');
|
|
108
|
+
});
|
|
@@ -144,28 +144,10 @@ test('revive: any status → active + intent cleared', () => {
|
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
// ---------------------------------------------------------------------------
|
|
147
|
-
// boot → active, intent UNCHANGED (reviveInPlace keeps the refresh safety net),
|
|
148
|
-
// legal only from active|idle
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
test('boot: active|idle → active, intent preserved (refresh net survives)', () => {
|
|
151
|
-
for (const from of LIVE) {
|
|
152
|
-
mk(`n_${from}`, from, 'refresh');
|
|
153
|
-
const m = transition(`n_${from}`, 'boot');
|
|
154
|
-
assert.equal(m.status, 'active', `boot from ${from}`);
|
|
155
|
-
assert.equal(m.intent, 'refresh', `boot from ${from} keeps intent (proof-of-boot net)`);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
test('boot: illegal from done|dead|canceled → throws', () => {
|
|
159
|
-
for (const from of TERMINAL) {
|
|
160
|
-
mk(`n_${from}`, from);
|
|
161
|
-
assert.throws(() => transition(`n_${from}`, 'boot'), /illegal lifecycle transition/);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
147
|
// unknown node → throws for every event
|
|
166
148
|
// ---------------------------------------------------------------------------
|
|
167
149
|
test('transition on an unknown node throws', () => {
|
|
168
|
-
for (const ev of ['finalize', 'cancel', 'crash', 'yield', 'release', 'revive'
|
|
150
|
+
for (const ev of ['finalize', 'cancel', 'crash', 'yield', 'release', 'revive']) {
|
|
169
151
|
assert.throws(() => transition('ghost', ev), /unknown node/);
|
|
170
152
|
}
|
|
171
153
|
});
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// mints a DIFFERENT fresh general/base/resident root.
|
|
15
15
|
// • A4: a base→orchestrator yield auto-promotes (mode→orchestrator) and sets
|
|
16
16
|
// intent=refresh but NEVER commits persona_ack — the drift is left pending
|
|
17
|
-
// and, because a yield's agent_end goes straight to
|
|
18
|
-
// preceding turn_end, the only steer-delivery site is bypassed.
|
|
17
|
+
// and, because a yield's agent_end goes straight to the refresh-revive with
|
|
18
|
+
// NO preceding turn_end, the only steer-delivery site is bypassed.
|
|
19
19
|
// (2) WHY MODEL-LEVEL, NOT PANE/WINDOW — demote writes lifecycle on the row
|
|
20
20
|
// (setLifecycle→updateNode); recycle's FINISH+MINT half is pushFinal +
|
|
21
21
|
// spawnNode (pure model) — only its pane-respawn is tmux (a deliberately-
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// / pending-drift asserts go RED.
|
|
30
30
|
//
|
|
31
31
|
// STOPHOOK CAVEAT — split (option b). The original drove a LIVE fake-pi so the
|
|
32
|
-
// REAL stophook enacted recycle's
|
|
32
|
+
// REAL stophook enacted recycle's viewer teardown and A4's refresh-revive ack-drain
|
|
33
33
|
// at agent_end. Fabrication cannot run those hooks. So: recycle is driven through
|
|
34
34
|
// the real recycleNode (its FINISH+MINT model half runs fully; the pane respawn
|
|
35
35
|
// is the only tmux artifact, intentionally skipped → recycled:false). For A4 the
|
|
@@ -117,7 +117,7 @@ test('node demote flips lifecycle→terminal IN PLACE; node recycle is FINISH+RE
|
|
|
117
117
|
// ===========================================================================
|
|
118
118
|
// (b) A4 BOUNDARY — promote-then-yield emits a steer that is discarded. A
|
|
119
119
|
// `node yield` on a base node auto-promotes (mode→orchestrator, ack NOT
|
|
120
|
-
// committed) and its agent_end goes STRAIGHT to
|
|
120
|
+
// committed) and its agent_end goes STRAIGHT to the refresh-revive with NO
|
|
121
121
|
// preceding turn_end — so the only steer-delivery site (turn_end) is
|
|
122
122
|
// BYPASSED. The deterministic, host-independent facts the yield verb leaves
|
|
123
123
|
// behind pin the boundary:
|
|
@@ -125,7 +125,7 @@ test('node demote flips lifecycle→terminal IN PLACE; node recycle is FINISH+RE
|
|
|
125
125
|
// (2) intent=refresh set.
|
|
126
126
|
// (3) persona_ack STILL base — the verb never commits it (only an injector
|
|
127
127
|
// or the refresh drain does); the drift is left PENDING.
|
|
128
|
-
// The ack's silent advance at the refresh drain (
|
|
128
|
+
// The ack's silent advance at the refresh drain (refresh-revive→drainBearings
|
|
129
129
|
// →commitPersonaAck) is locked by persona.test.ts; because no turn_end ever
|
|
130
130
|
// fires on the yield path, that ack moves WITHOUT the orchestration steer
|
|
131
131
|
// ever being delivered — the A4 LOSS.
|
|
@@ -35,7 +35,7 @@ let projectDir;
|
|
|
35
35
|
let prevCwd;
|
|
36
36
|
const PLUGIN = 'fixplug';
|
|
37
37
|
function doc(name) {
|
|
38
|
-
return `---\nkind:
|
|
38
|
+
return `---\nkind: knowledge\nwhen-and-why-to-read: fixture doc ${name}\n---\n\n# ${name}\n`;
|
|
39
39
|
}
|
|
40
40
|
before(() => {
|
|
41
41
|
prevCwd = process.cwd();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Bug-regression: on-read doc injection "fires a second time per session".
|
|
2
|
+
//
|
|
3
|
+
// The on-read substrate hook dedups so a doc surfaces at most once per
|
|
4
|
+
// conversation. That dedup set USED to live only in the pi process heap (cleared
|
|
5
|
+
// on session_start). But a node's logical session — the .jsonl transcript —
|
|
6
|
+
// spans MULTIPLE pi processes: a dormancy → revive(resume) cycle exits the old
|
|
7
|
+
// process and launches a fresh `pi --session` that REUSES the same transcript.
|
|
8
|
+
// The fresh process started with an empty set, so any doc already injected
|
|
9
|
+
// before dormancy got injected AGAIN on the next read.
|
|
10
|
+
//
|
|
11
|
+
// The fix persists the set to nodes/<id>/injected-docs.json: a resume rehydrates
|
|
12
|
+
// it (dedup holds), and only the FRESH-transcript launch paths clear it. These
|
|
13
|
+
// tests lock in both halves: the store round-trip, and the dedup surviving a
|
|
14
|
+
// simulated revive(resume) while re-surfacing after a fresh launch.
|
|
15
|
+
//
|
|
16
|
+
// Run: node --import tsx/esm --test src/core/__tests__/on-read-dedup-resume.test.ts
|
|
17
|
+
import { test, before, beforeEach, after } from 'node:test';
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { closeDb } from '../canvas/db.js';
|
|
23
|
+
import { injectedDocsPath } from '../canvas/paths.js';
|
|
24
|
+
import { spawnNode } from '../runtime/nodes.js';
|
|
25
|
+
import { renderOnReadDocs } from '../substrate/on-read.js';
|
|
26
|
+
import { loadInjectedDocs, saveInjectedDocs, clearInjectedDocs, } from '../substrate/injected-store.js';
|
|
27
|
+
const FIXTURE_BODY = 'Fixture body that must surface exactly once.';
|
|
28
|
+
let home;
|
|
29
|
+
let work;
|
|
30
|
+
before(() => {
|
|
31
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-onread-home-'));
|
|
32
|
+
process.env['CRTR_HOME'] = home;
|
|
33
|
+
});
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
closeDb();
|
|
36
|
+
rmSync(home, { recursive: true, force: true });
|
|
37
|
+
work = mkdtempSync(join(tmpdir(), 'crtr-onread-work-'));
|
|
38
|
+
});
|
|
39
|
+
after(() => {
|
|
40
|
+
closeDb();
|
|
41
|
+
rmSync(home, { recursive: true, force: true });
|
|
42
|
+
rmSync(work, { recursive: true, force: true });
|
|
43
|
+
delete process.env['CRTR_HOME'];
|
|
44
|
+
});
|
|
45
|
+
test('injected-store round-trips and clears', () => {
|
|
46
|
+
const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
|
|
47
|
+
assert.deepEqual([...loadInjectedDocs(node)], [], 'cold load is empty');
|
|
48
|
+
saveInjectedDocs(node, new Set(['/a', '/b']));
|
|
49
|
+
assert.deepEqual([...loadInjectedDocs(node)].sort(), ['/a', '/b'], 'save then load round-trips');
|
|
50
|
+
clearInjectedDocs(node);
|
|
51
|
+
assert.ok(!existsSync(injectedDocsPath(node)), 'clear removes the file');
|
|
52
|
+
assert.deepEqual([...loadInjectedDocs(node)], [], 'load after clear is empty');
|
|
53
|
+
});
|
|
54
|
+
test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfaces after a fresh launch', () => {
|
|
55
|
+
const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
|
|
56
|
+
// A positional substrate doc in a `.crouter/memory/` ancestor of the read file.
|
|
57
|
+
const memDir = join(work, '.crouter', 'memory');
|
|
58
|
+
mkdirSync(memDir, { recursive: true });
|
|
59
|
+
writeFileSync(join(memDir, 'onread-fixture.md'), '---\nkind: knowledge\n' +
|
|
60
|
+
'when-and-why-to-read: When reading work files, this reference should be read because it is the on-read regression fixture\n' +
|
|
61
|
+
'file-read-visibility: content\n---\n' +
|
|
62
|
+
`${FIXTURE_BODY}\n`);
|
|
63
|
+
const readFile = join(work, 'src', 'file.ts');
|
|
64
|
+
mkdirSync(join(work, 'src'), { recursive: true });
|
|
65
|
+
writeFileSync(readFile, 'export const x = 1;\n');
|
|
66
|
+
// --- pi process #1: cold load; first read surfaces the doc ---
|
|
67
|
+
const seen = loadInjectedDocs(node);
|
|
68
|
+
const first = renderOnReadDocs(node, readFile, seen);
|
|
69
|
+
assert.match(first, new RegExp(FIXTURE_BODY.replace(/[.]/g, '\\.')), 'first read surfaces the doc body');
|
|
70
|
+
saveInjectedDocs(node, seen);
|
|
71
|
+
// Same process, repeat read of the same file → deduped in-memory.
|
|
72
|
+
assert.ok(!renderOnReadDocs(node, readFile, seen).includes(FIXTURE_BODY), 'same-process repeat read is deduped');
|
|
73
|
+
// --- dormancy → revive(resume): NEW process rehydrates the set from disk ---
|
|
74
|
+
const seenAfterRevive = loadInjectedDocs(node);
|
|
75
|
+
assert.deepEqual(seenAfterRevive, seen, 'revive(resume) rehydrates the persisted dedup set');
|
|
76
|
+
assert.ok(!renderOnReadDocs(node, readFile, seenAfterRevive).includes(FIXTURE_BODY), 'resumed process does NOT re-inject the doc (the bug)');
|
|
77
|
+
// --- fresh launch (revive resume=false, e.g. a refresh-yield) clears the set ---
|
|
78
|
+
clearInjectedDocs(node);
|
|
79
|
+
const seenFresh = loadInjectedDocs(node);
|
|
80
|
+
assert.ok(renderOnReadDocs(node, readFile, seenFresh).includes(FIXTURE_BODY), 'a fresh transcript surfaces the doc again');
|
|
81
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/refresh-stall-recycle.test.ts
|
|
2
|
+
//
|
|
3
|
+
// BUG-REGRESSION (broker-universal-host cut, design §H refresh-authority; U7
|
|
4
|
+
// report mq8gq8we-4cadd8e6; stuck-refresh, parent mq7s5o93). A `node yield` /
|
|
5
|
+
// refresh records intent='refresh' and relies on the in-process stophook to call
|
|
6
|
+
// shutdown at stop. But the engine can be left ALIVE forever — it hangs at a huge
|
|
7
|
+
// context, OR a STALE in-process stophook (pi extensions never reload, so a
|
|
8
|
+
// days-old resident carries an old hook) ignores intent='refresh' and never calls
|
|
9
|
+
// shutdown. Engine alive + turn over + intent pending forever. The daemon only
|
|
10
|
+
// revives DEAD engines, so without §H the node is permanently stuck. The DAEMON
|
|
11
|
+
// is the authority: when intent='refresh' persists on a LIVE engine whose turn is
|
|
12
|
+
// over, past YIELD_STALL_GRACE_MS, it force-kills the engine (SIGTERM) so the
|
|
13
|
+
// dead-pid refresh path then revives it fresh.
|
|
14
|
+
//
|
|
15
|
+
// Two legs: the pure truth table (D4a — yieldStallVerdict) and the daemon
|
|
16
|
+
// integration over a real live pid the daemon must SIGTERM (D4b — superviseTick
|
|
17
|
+
// with an injected clock). Regression check: drop §H and D4b's "engine killed"
|
|
18
|
+
// assert goes RED (the live refresh pid lives forever, never recycled).
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { spawn } from 'node:child_process';
|
|
22
|
+
import { createHeadlessHarness } from './helpers/harness.js';
|
|
23
|
+
import { yieldStallVerdict, YIELD_STALL_GRACE_MS, isPidAlive } from '../../daemon/crtrd.js';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// D4a — the pure refresh-stall truth table.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
test('D4a yieldStallVerdict: kill ONLY a live, not-busy, refresh engine past the grace', () => {
|
|
28
|
+
const G = YIELD_STALL_GRACE_MS;
|
|
29
|
+
// The one 'kill' case: engine alive, intent=refresh, turn over (not busy), past grace.
|
|
30
|
+
assert.equal(yieldStallVerdict(true, 'refresh', false, G), 'kill', 'alive+refresh+!busy+>=grace → kill');
|
|
31
|
+
assert.equal(yieldStallVerdict(true, 'refresh', false, G + 60_000), 'kill', 'well past grace → kill');
|
|
32
|
+
// Within the grace → pending (the healthy stophook window).
|
|
33
|
+
assert.equal(yieldStallVerdict(true, 'refresh', false, null), 'pending', 'first observation → pending');
|
|
34
|
+
assert.equal(yieldStallVerdict(true, 'refresh', false, G - 1), 'pending', 'within grace → pending');
|
|
35
|
+
// A working engine is NEVER killed, even past the grace.
|
|
36
|
+
assert.equal(yieldStallVerdict(true, 'refresh', true, G + 60_000), 'leave', 'busy (mid-turn) → leave');
|
|
37
|
+
// Not a refresh → not our concern.
|
|
38
|
+
assert.equal(yieldStallVerdict(true, 'idle-release', false, G + 60_000), 'leave', 'intent≠refresh → leave');
|
|
39
|
+
assert.equal(yieldStallVerdict(true, null, false, G + 60_000), 'leave', 'no pending intent → leave');
|
|
40
|
+
// A dead/unknown pid is the ordinary revive path's job, not this one.
|
|
41
|
+
assert.equal(yieldStallVerdict(false, 'refresh', false, G + 60_000), 'leave', 'dead pid → leave (dead-pid path owns it)');
|
|
42
|
+
assert.equal(yieldStallVerdict(null, 'refresh', false, G + 60_000), 'leave', 'unknown pid → leave');
|
|
43
|
+
});
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// D4b — daemon integration: a stuck refresh on a LIVE engine is force-recycled.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
const T0 = 7_000_000;
|
|
48
|
+
const T_KILL = T0 + YIELD_STALL_GRACE_MS + 1; // one ms past the grace from t0
|
|
49
|
+
test('D4b daemon force-recycles a stuck refresh: a live, not-busy refresh engine past the grace is SIGTERM\'d', { timeout: 30_000 }, async () => {
|
|
50
|
+
const h = await createHeadlessHarness({ sessionPrefix: 'crtr-d4b' });
|
|
51
|
+
// A real live subprocess standing in for the hung/stale-hook engine. `sleep`
|
|
52
|
+
// dies on SIGTERM, so the daemon's force-kill is directly observable.
|
|
53
|
+
const child = spawn('sleep', ['300'], { stdio: 'ignore' });
|
|
54
|
+
const enginePid = child.pid;
|
|
55
|
+
try {
|
|
56
|
+
assert.equal(isPidAlive(enginePid), true, 'precondition: the refresh engine is alive');
|
|
57
|
+
// intent='refresh' + LIVE pid + turn over (no busy marker by default) →
|
|
58
|
+
// the §H stall state the daemon must break.
|
|
59
|
+
const id = h.fabricateBrokerNode({
|
|
60
|
+
kind: 'developer',
|
|
61
|
+
status: 'active',
|
|
62
|
+
intent: 'refresh',
|
|
63
|
+
pi_pid: enginePid,
|
|
64
|
+
pi_session_id: 'sess-d4b',
|
|
65
|
+
});
|
|
66
|
+
assert.equal(h.node(id).intent, 'refresh', 'precondition: intent=refresh pending');
|
|
67
|
+
// --- TICK @ t0: arms the stall clock (yieldStallSince). No kill yet. ---
|
|
68
|
+
await h.tick(T0);
|
|
69
|
+
assert.equal(isPidAlive(enginePid), true, 'within grace: the engine is NOT killed (clock just armed)');
|
|
70
|
+
// --- TICK past the grace: the daemon concludes the refresh stalled and
|
|
71
|
+
// SIGTERMs the engine itself (§H force-recycle). ---
|
|
72
|
+
await h.tick(T_KILL);
|
|
73
|
+
await h.waitFor(() => (isPidAlive(enginePid) ? null : true), {
|
|
74
|
+
label: 'daemon SIGTERM killed the stuck refresh engine',
|
|
75
|
+
timeoutMs: 10_000,
|
|
76
|
+
});
|
|
77
|
+
assert.equal(isPidAlive(enginePid), false, 'past grace: the daemon force-killed the stuck refresh engine (§H)');
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
try {
|
|
81
|
+
child.kill('SIGKILL');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* already gone */
|
|
85
|
+
}
|
|
86
|
+
await h.dispose();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|