@crouton-kit/crouter 0.3.28 → 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 +5 -4
- package/dist/builtin-personas/spec/PERSONA.md +8 -4
- package/dist/builtin-personas/spec/orchestrator.md +5 -3
- package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
- package/dist/builtin-views/canvas/core.mjs +586 -0
- package/dist/builtin-views/canvas/text.mjs +58 -0
- package/dist/builtin-views/canvas/tui.mjs +165 -0
- package/dist/builtin-views/canvas/web.jsx +120 -0
- package/dist/builtin-views/git-pr/core.mjs +673 -0
- package/dist/builtin-views/git-pr/text.mjs +84 -0
- package/dist/builtin-views/git-pr/tui.mjs +302 -0
- package/dist/builtin-views/git-pr/web.jsx +216 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
- package/dist/builtin-views/inbox/core.mjs +1273 -0
- package/dist/builtin-views/inbox/text.mjs +73 -0
- package/dist/builtin-views/inbox/tui.mjs +312 -0
- package/dist/builtin-views/inbox/web.jsx +188 -0
- package/dist/builtin-views/linkedin/core.mjs +906 -0
- package/dist/builtin-views/linkedin/text.mjs +69 -0
- package/dist/builtin-views/linkedin/tui.mjs +427 -0
- package/dist/builtin-views/linkedin/web.jsx +206 -0
- package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
- package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
- package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
- package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
- package/dist/cli.js +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__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +282 -46
- package/dist/clients/attach/auth-pickers.d.ts +9 -0
- package/dist/clients/attach/auth-pickers.js +194 -0
- package/dist/clients/attach/canvas-panels.d.ts +4 -2
- package/dist/clients/attach/canvas-panels.js +45 -17
- package/dist/clients/attach/chat-view.d.ts +27 -2
- package/dist/clients/attach/chat-view.js +98 -9
- package/dist/clients/attach/clipboard-image.d.ts +47 -8
- package/dist/clients/attach/clipboard-image.js +150 -26
- package/dist/clients/attach/clipboard-text.d.ts +3 -0
- package/dist/clients/attach/clipboard-text.js +39 -0
- package/dist/clients/attach/config-load.d.ts +54 -7
- package/dist/clients/attach/config-load.js +99 -3
- package/dist/clients/attach/context-message.d.ts +12 -0
- package/dist/clients/attach/context-message.js +72 -0
- package/dist/clients/attach/git-info.d.ts +20 -0
- package/dist/clients/attach/git-info.js +44 -0
- package/dist/clients/attach/graph-overlay.d.ts +13 -4
- package/dist/clients/attach/graph-overlay.js +59 -19
- package/dist/clients/attach/input-controller.d.ts +75 -13
- package/dist/clients/attach/input-controller.js +305 -67
- package/dist/clients/attach/pickers.d.ts +58 -0
- package/dist/clients/attach/pickers.js +171 -0
- package/dist/clients/attach/slash-commands.d.ts +22 -0
- package/dist/clients/attach/slash-commands.js +91 -25
- package/dist/clients/attach/titled-editor.d.ts +33 -0
- package/dist/clients/attach/titled-editor.js +84 -0
- package/dist/clients/attach/view-socket.d.ts +18 -1
- package/dist/clients/attach/view-socket.js +70 -1
- package/dist/clients/web/dev-server.d.ts +7 -0
- package/dist/clients/web/dev-server.js +59 -0
- package/dist/clients/web/events.d.ts +14 -0
- package/dist/clients/web/events.js +151 -0
- package/dist/clients/web/server.d.ts +18 -0
- package/dist/clients/web/server.js +450 -0
- package/dist/clients/web/web-cmd.d.ts +2 -0
- package/dist/clients/web/web-cmd.js +120 -0
- package/dist/commands/canvas.js +1 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/dashboard.js +5 -1
- package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
- package/dist/commands/memory/lint.js +14 -2
- package/dist/commands/memory/read.js +2 -2
- package/dist/commands/memory/shared.d.ts +7 -1
- package/dist/commands/memory/shared.js +18 -3
- package/dist/commands/memory/write.js +8 -4
- package/dist/commands/memory.js +4 -4
- package/dist/commands/node.d.ts +1 -1
- package/dist/commands/node.js +64 -65
- package/dist/commands/pkg.js +1 -2
- package/dist/commands/revive.js +11 -3
- package/dist/commands/sys/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +3 -2
- package/dist/commands/view-cycle.js +2 -2
- package/dist/commands/view-list.js +8 -8
- package/dist/commands/view-new.js +21 -17
- package/dist/commands/view-pick.js +1 -1
- package/dist/commands/view-run.js +35 -14
- package/dist/commands/view.js +8 -6
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +161 -0
- package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
- package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
- package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
- package/dist/core/__tests__/child-death-wake.test.js +56 -111
- package/dist/core/__tests__/close.test.js +14 -20
- package/dist/core/__tests__/context-intro.test.js +19 -19
- package/dist/core/__tests__/daemon-boot.test.js +30 -23
- package/dist/core/__tests__/draw-style.test.js +1 -1
- package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +25 -13
- package/dist/core/__tests__/fork.test.js +22 -5
- package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
- package/dist/core/__tests__/full/cascade-close.test.js +22 -8
- package/dist/core/__tests__/full/detach-focus.test.js +66 -169
- package/dist/core/__tests__/helpers/harness.d.ts +6 -9
- package/dist/core/__tests__/helpers/harness.js +19 -36
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
- package/dist/core/__tests__/lifecycle.test.js +1 -19
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
- package/dist/core/__tests__/memory-resolver.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
- package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
- package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch-root.test.js +114 -0
- package/dist/core/__tests__/reset.test.js +19 -79
- package/dist/core/__tests__/steer-note.test.js +9 -7
- package/dist/core/__tests__/tmux-surface.test.js +13 -7
- package/dist/core/bootstrap.d.ts +0 -3
- package/dist/core/bootstrap.js +1 -143
- package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
- package/dist/core/canvas/browse/app.js +131 -8
- package/dist/core/canvas/browse/model.d.ts +23 -5
- package/dist/core/canvas/browse/model.js +68 -13
- package/dist/core/canvas/browse/render.d.ts +7 -1
- package/dist/core/canvas/browse/render.js +178 -47
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/focuses.d.ts +4 -4
- package/dist/core/canvas/focuses.js +7 -7
- package/dist/core/canvas/nav-model.js +8 -7
- package/dist/core/canvas/render.d.ts +68 -9
- package/dist/core/canvas/render.js +195 -61
- package/dist/core/canvas/types.d.ts +16 -23
- package/dist/core/config.js +1 -4
- package/dist/core/memory-resolver.js +43 -5
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +8 -8
- package/dist/core/runtime/branded-host.d.ts +16 -0
- package/dist/core/runtime/branded-host.js +127 -0
- package/dist/core/runtime/broker-protocol.d.ts +212 -6
- package/dist/core/runtime/broker.d.ts +4 -1
- package/dist/core/runtime/broker.js +494 -58
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +6 -14
- package/dist/core/runtime/host.d.ts +14 -34
- package/dist/core/runtime/host.js +14 -50
- package/dist/core/runtime/launch.d.ts +12 -9
- package/dist/core/runtime/launch.js +27 -15
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +15 -19
- package/dist/core/runtime/nodes.d.ts +0 -55
- package/dist/core/runtime/nodes.js +6 -74
- package/dist/core/runtime/placement.d.ts +91 -335
- package/dist/core/runtime/placement.js +262 -840
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +24 -6
- package/dist/core/runtime/recap.d.ts +8 -0
- package/dist/core/runtime/recap.js +107 -0
- package/dist/core/runtime/recycle.js +25 -61
- package/dist/core/runtime/reset.d.ts +43 -37
- package/dist/core/runtime/reset.js +131 -218
- package/dist/core/runtime/revive.d.ts +9 -29
- package/dist/core/runtime/revive.js +40 -139
- package/dist/core/runtime/spawn.d.ts +15 -16
- package/dist/core/runtime/spawn.js +165 -194
- package/dist/core/runtime/surface-bg.d.ts +11 -0
- package/dist/core/runtime/surface-bg.js +68 -0
- package/dist/core/runtime/tmux.d.ts +13 -50
- package/dist/core/runtime/tmux.js +33 -87
- package/dist/core/scope.d.ts +1 -2
- package/dist/core/scope.js +1 -5
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
- package/dist/core/skill-sync/builtins.d.ts +42 -0
- package/dist/core/skill-sync/builtins.js +112 -0
- package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
- package/dist/core/skill-sync/claude-plugins.js +71 -0
- package/dist/core/skill-sync/engine.d.ts +42 -0
- package/dist/core/skill-sync/engine.js +633 -0
- package/dist/core/skill-sync/export.d.ts +23 -0
- package/dist/core/skill-sync/export.js +86 -0
- package/dist/core/skill-sync/manifest.d.ts +64 -0
- package/dist/core/skill-sync/manifest.js +181 -0
- package/dist/core/skill-sync/profile.d.ts +76 -0
- package/dist/core/skill-sync/profile.js +173 -0
- package/dist/core/skill-sync/snapshot.d.ts +57 -0
- package/dist/core/skill-sync/snapshot.js +120 -0
- package/dist/core/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.js +3 -3
- package/dist/core/substrate/on-read.js +68 -6
- package/dist/core/substrate/render.d.ts +8 -11
- package/dist/core/substrate/render.js +29 -43
- package/dist/core/substrate/schema.d.ts +10 -3
- package/dist/core/substrate/schema.js +6 -3
- package/dist/core/tui/host.d.ts +12 -4
- package/dist/core/tui/host.js +280 -149
- package/dist/core/view/bridge.d.ts +10 -0
- package/dist/core/view/bridge.js +31 -0
- package/dist/core/view/chrome.d.ts +9 -0
- package/dist/core/view/chrome.js +22 -0
- package/dist/core/view/contract.d.ts +171 -0
- package/dist/core/view/contract.js +23 -0
- package/dist/core/view/loader.d.ts +31 -0
- package/dist/core/view/loader.js +188 -0
- package/dist/core/view/transport-local.d.ts +7 -0
- package/dist/core/view/transport-local.js +70 -0
- package/dist/core/view/transport.d.ts +4 -0
- package/dist/core/view/transport.js +15 -0
- package/dist/daemon/crtrd.d.ts +38 -27
- package/dist/daemon/crtrd.js +303 -376
- package/dist/daemon/manage.js +6 -1
- package/dist/index.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
- package/dist/pi-extensions/canvas-context-intro.js +4 -4
- package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
- package/dist/pi-extensions/canvas-nav.js +5 -0
- package/dist/pi-extensions/canvas-recap.d.ts +37 -0
- package/dist/pi-extensions/canvas-recap.js +236 -0
- package/dist/pi-extensions/canvas-resume.js +2 -1
- package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
- package/dist/pi-extensions/canvas-stophook.js +87 -101
- package/dist/pi-extensions/canvas-view.js +2 -1
- package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
- package/dist/pi-extensions/widget-order-bus.js +34 -0
- package/dist/prompts/view.d.ts +2 -2
- package/dist/prompts/view.js +145 -73
- package/dist/types.d.ts +1 -6
- package/dist/types.js +1 -3
- package/dist/web/ViewChrome.d.ts +7 -0
- package/dist/web/ViewChrome.js +28 -0
- package/dist/web/ViewPane.d.ts +39 -0
- package/dist/web/ViewPane.js +48 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +16 -0
- package/dist/web/runtime.d.ts +39 -0
- package/dist/web/runtime.js +133 -0
- package/dist/web/states.d.ts +24 -0
- package/dist/web/states.js +24 -0
- package/dist/web/transport-http.d.ts +5 -0
- package/dist/web/transport-http.js +28 -0
- package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
- package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
- package/dist/web-client/index.html +13 -0
- package/package.json +20 -6
- package/dist/builtin-views/canvas/client.mjs +0 -303
- package/dist/builtin-views/canvas/view.mjs +0 -576
- package/dist/builtin-views/git-pr/client.mjs +0 -440
- package/dist/builtin-views/git-pr/view.mjs +0 -675
- package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
- package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
- package/dist/builtin-views/inbox/view.mjs +0 -889
- package/dist/builtin-views/linkedin/client.mjs +0 -610
- package/dist/builtin-views/linkedin/view.mjs +0 -1171
- package/dist/commands/pkg/bridge.d.ts +0 -1
- package/dist/commands/pkg/bridge.js +0 -137
- package/dist/commands/skill/author.d.ts +0 -3
- package/dist/commands/skill/author.js +0 -140
- package/dist/commands/skill/shared.d.ts +0 -3
- package/dist/commands/skill/shared.js +0 -19
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.js +0 -21
- package/dist/commands/tmux-spread.d.ts +0 -2
- package/dist/commands/tmux-spread.js +0 -144
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
- package/dist/core/__tests__/full/placement-focus.test.js +0 -309
- package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
- package/dist/core/__tests__/full/placement-revive.test.js +0 -238
- package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
- package/dist/core/__tests__/home-session.test.js +0 -186
- package/dist/core/__tests__/relaunch.test.js +0 -335
- package/dist/core/bridge-map.d.ts +0 -19
- package/dist/core/bridge-map.js +0 -73
- package/dist/core/tui/contract.d.ts +0 -83
- package/dist/core/tui/contract.js +0 -8
- package/dist/core/tui/loader.d.ts +0 -16
- package/dist/core/tui/loader.js +0 -94
- package/dist/prompts/skill.d.ts +0 -2
- package/dist/prompts/skill.js +0 -650
- /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
- /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
|
@@ -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
|
+
});
|
|
@@ -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();
|
|
@@ -56,7 +56,7 @@ test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfa
|
|
|
56
56
|
// A positional substrate doc in a `.crouter/memory/` ancestor of the read file.
|
|
57
57
|
const memDir = join(work, '.crouter', 'memory');
|
|
58
58
|
mkdirSync(memDir, { recursive: true });
|
|
59
|
-
writeFileSync(join(memDir, 'onread-fixture.md'), '---\nkind:
|
|
59
|
+
writeFileSync(join(memDir, 'onread-fixture.md'), '---\nkind: knowledge\n' +
|
|
60
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
61
|
'file-read-visibility: content\n---\n' +
|
|
62
62
|
`${FIXTURE_BODY}\n`);
|
|
@@ -74,7 +74,7 @@ test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfa
|
|
|
74
74
|
const seenAfterRevive = loadInjectedDocs(node);
|
|
75
75
|
assert.deepEqual(seenAfterRevive, seen, 'revive(resume) rehydrates the persisted dedup set');
|
|
76
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
|
|
77
|
+
// --- fresh launch (revive resume=false, e.g. a refresh-yield) clears the set ---
|
|
78
78
|
clearInjectedDocs(node);
|
|
79
79
|
const seenFresh = loadInjectedDocs(node);
|
|
80
80
|
assert.ok(renderOnReadDocs(node, readFile, seenFresh).includes(FIXTURE_BODY), 'a fresh transcript surfaces the doc again');
|
|
@@ -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 {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/relaunch-root.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Bug regression (commit 07e704e, "broker becomes universal host"): `/new` on a
|
|
4
|
+
// ROOT used to do an in-place SAME-id reset (resetRoot reaped + wiped + re-pointed
|
|
5
|
+
// the same node id) instead of starting a genuinely new node. Model B fixes it:
|
|
6
|
+
// relaunchRoot parks the old root `done` (kept as history, NOT canceled) and mints
|
|
7
|
+
// a FRESH node id + broker, re-pointing the viewer pane. These tests lock in that
|
|
8
|
+
// behavior via the injectable deps seam, so they run tmux-free and broker-free.
|
|
9
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { createNode, getNode, subscribe } from '../canvas/canvas.js';
|
|
15
|
+
import { closeDb } from '../canvas/db.js';
|
|
16
|
+
import { relaunchRoot } from '../runtime/reset.js';
|
|
17
|
+
let home;
|
|
18
|
+
function node(id, over = {}) {
|
|
19
|
+
return {
|
|
20
|
+
node_id: id,
|
|
21
|
+
name: id,
|
|
22
|
+
created: new Date().toISOString(),
|
|
23
|
+
cwd: '/tmp/work',
|
|
24
|
+
kind: 'general',
|
|
25
|
+
mode: 'base',
|
|
26
|
+
lifecycle: 'terminal',
|
|
27
|
+
status: 'active',
|
|
28
|
+
...over,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** A tmux-free / broker-free deps seam. `pid` controls the new broker launch:
|
|
32
|
+
* a number = success, null = a boot failure. Records the node ids launchBroker
|
|
33
|
+
* was called with, so the boot-failure test can find the half-born new node. */
|
|
34
|
+
function makeDeps(pid) {
|
|
35
|
+
const launched = [];
|
|
36
|
+
const deps = {
|
|
37
|
+
launchBroker: (nodeId) => {
|
|
38
|
+
launched.push(nodeId);
|
|
39
|
+
return { pid };
|
|
40
|
+
},
|
|
41
|
+
waitForViewSocket: () => true,
|
|
42
|
+
respawnViewer: () => true,
|
|
43
|
+
teardownBroker: () => { },
|
|
44
|
+
};
|
|
45
|
+
return { deps, launched };
|
|
46
|
+
}
|
|
47
|
+
before(() => {
|
|
48
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-relaunch-'));
|
|
49
|
+
process.env['CRTR_HOME'] = home;
|
|
50
|
+
});
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
closeDb();
|
|
53
|
+
rmSync(home, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
after(() => {
|
|
56
|
+
closeDb();
|
|
57
|
+
rmSync(home, { recursive: true, force: true });
|
|
58
|
+
delete process.env['CRTR_HOME'];
|
|
59
|
+
});
|
|
60
|
+
test('relaunchRoot parks the old root done and mints a fresh active root, reaping descendants', () => {
|
|
61
|
+
// root → child → grandchild (a parent that subscribes to its workers).
|
|
62
|
+
createNode(node('root', { parent: null, lifecycle: 'resident', cwd: '/tmp/proj' }));
|
|
63
|
+
createNode(node('child', { parent: 'root' }));
|
|
64
|
+
createNode(node('grand', { parent: 'child' }));
|
|
65
|
+
subscribe('root', 'child', true);
|
|
66
|
+
subscribe('child', 'grand', true);
|
|
67
|
+
const { deps, launched } = makeDeps(4242);
|
|
68
|
+
const res = relaunchRoot('root', deps);
|
|
69
|
+
// A fresh, DIFFERENT node id was minted and booted.
|
|
70
|
+
assert.ok(res, 'relaunchRoot returns the new node id');
|
|
71
|
+
const newId = res.newNodeId;
|
|
72
|
+
assert.notEqual(newId, 'root', 'a genuinely NEW node id (not a same-id reset — the bug)');
|
|
73
|
+
assert.deepEqual(launched, [newId], 'the new node id is the one we booted a broker for');
|
|
74
|
+
// (1) Old root parked DONE — kept as history, NOT canceled (explicit CTO call).
|
|
75
|
+
assert.equal(getNode('root')?.status, 'done', 'old root parked done (kept as history)');
|
|
76
|
+
// (2) The new node is an active, top-level resident inheriting the old root's
|
|
77
|
+
// kind + cwd.
|
|
78
|
+
const fresh = getNode(newId);
|
|
79
|
+
assert.equal(fresh?.status, 'active', 'new node is active');
|
|
80
|
+
assert.equal(fresh?.parent, null, 'new node is a root (no spine parent)');
|
|
81
|
+
assert.equal(fresh?.kind, 'general', 'new node inherits the old root kind');
|
|
82
|
+
assert.equal(fresh?.cwd, '/tmp/proj', 'new node inherits the old root cwd');
|
|
83
|
+
assert.equal(fresh?.lifecycle, 'resident', 'a relaunched root is resident');
|
|
84
|
+
// (3) Descendants reaped → canceled (A5: an externally-reaped node did not
|
|
85
|
+
// finish its OWN work; done is reserved for finalize).
|
|
86
|
+
assert.equal(getNode('child')?.status, 'canceled', 'descendant child canceled');
|
|
87
|
+
assert.equal(getNode('grand')?.status, 'canceled', 'descendant grandchild canceled');
|
|
88
|
+
});
|
|
89
|
+
test('relaunchRoot on a boot failure leaves the old root + descendants fully intact', () => {
|
|
90
|
+
// The mint-and-boot-FIRST ordering: a launch failure (pid=null) must occur
|
|
91
|
+
// BEFORE the old root is parked or its descendants reaped.
|
|
92
|
+
createNode(node('root', { parent: null, lifecycle: 'resident' }));
|
|
93
|
+
createNode(node('child', { parent: 'root' }));
|
|
94
|
+
subscribe('root', 'child', true);
|
|
95
|
+
const { deps, launched } = makeDeps(null); // broker never starts
|
|
96
|
+
const res = relaunchRoot('root', deps);
|
|
97
|
+
assert.equal(res, null, 'a boot failure returns null');
|
|
98
|
+
// Old root + descendant untouched: still live, ready to keep running.
|
|
99
|
+
assert.equal(getNode('root')?.status, 'active', 'old root left active + untouched');
|
|
100
|
+
assert.equal(getNode('child')?.status, 'active', 'descendant left active + untouched');
|
|
101
|
+
// The half-born new node was crashed (so the daemon never watches a zombie).
|
|
102
|
+
assert.equal(launched.length, 1, 'one broker launch was attempted');
|
|
103
|
+
assert.equal(getNode(launched[0])?.status, 'dead', 'the half-born new node is crashed (dead)');
|
|
104
|
+
});
|
|
105
|
+
test('relaunchRoot is a no-op for a non-root child and an already-parked root', () => {
|
|
106
|
+
createNode(node('root', { parent: null, lifecycle: 'resident' }));
|
|
107
|
+
createNode(node('child', { parent: 'root' }));
|
|
108
|
+
createNode(node('done-root', { parent: null, status: 'done' }));
|
|
109
|
+
const { deps, launched } = makeDeps(1);
|
|
110
|
+
assert.equal(relaunchRoot('child', deps), null, 'a child is not a relaunchable root');
|
|
111
|
+
assert.equal(relaunchRoot('done-root', deps), null, 'a done root is not relaunched again (double /new)');
|
|
112
|
+
assert.equal(relaunchRoot('ghost', deps), null, 'an unknown node is a no-op');
|
|
113
|
+
assert.deepEqual(launched, [], 'no broker was booted for any of the no-op cases');
|
|
114
|
+
});
|