@north-light/crouter 0.3.226 → 0.3.228
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/api/client.d.ts +5 -5
- package/dist/api/client.js +3 -3
- package/dist/api/dto/broker-ops.d.ts +8 -0
- package/dist/api/dto/memory.d.ts +8 -7
- package/dist/api/dto/memory.js +2 -2
- package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
- package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
- package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
- package/dist/builtin-memory/design/guide.md +38 -16
- package/dist/builtin-memory/design/roadmap.md +4 -4
- package/dist/builtin-memory/insights/init.md +5 -5
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +6 -6
- package/dist/builtin-memory/internal/plugins.md +1 -1
- package/dist/builtin-memory/plan/guide.md +53 -0
- package/dist/builtin-memory/plan/roadmap.md +10 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
- package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
- package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
- package/dist/clients/attach/chrome/inbox-strip.js +11 -7
- package/dist/clients/attach/session/pane-tag.d.ts +28 -2
- package/dist/clients/attach/session/pane-tag.js +48 -22
- package/dist/clients/attach/session/profile-files.d.ts +6 -0
- package/dist/clients/attach/session/profile-files.js +44 -11
- package/dist/clients/attach/slash/dispatch.js +4 -7
- package/dist/clients/attach/viewer.js +588 -596
- package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
- package/dist/commands/api-client.d.ts +3 -7
- package/dist/commands/api-client.js +7 -12
- package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
- package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
- package/dist/commands/memory/delete.js +51 -24
- package/dist/commands/memory/edit.js +50 -10
- package/dist/commands/memory/find.js +105 -56
- package/dist/commands/memory/history.js +51 -31
- package/dist/commands/memory/lint.d.ts +0 -9
- package/dist/commands/memory/lint.js +78 -322
- package/dist/commands/memory/list.d.ts +12 -4
- package/dist/commands/memory/list.js +64 -25
- package/dist/commands/memory/move.js +103 -73
- package/dist/commands/memory/origin.js +35 -5
- package/dist/commands/memory/read.d.ts +4 -0
- package/dist/commands/memory/read.js +132 -80
- package/dist/commands/memory/shared.d.ts +115 -25
- package/dist/commands/memory/shared.js +331 -74
- package/dist/commands/memory/write.js +72 -21
- package/dist/commands/memory.js +2 -2
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/commands/pkg/market-manage.js +25 -24
- package/dist/commands/pkg/plugin-manage.d.ts +22 -6
- package/dist/commands/pkg/plugin-manage.js +118 -25
- package/dist/commands/pkg/shared.d.ts +8 -0
- package/dist/commands/pkg/shared.js +23 -20
- package/dist/commands/surface-reload.d.ts +5 -0
- package/dist/commands/surface-reload.js +9 -1
- package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
- package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
- package/dist/commands/sys/migrate.js +126 -140
- package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
- package/dist/commands/sys/panels/profiles-panel.js +599 -0
- package/dist/commands/sys/settings-shell.d.ts +4 -2
- package/dist/commands/sys/settings-shell.js +35 -4
- package/dist/commands/sys/settings.js +21 -5
- package/dist/commands/sys/sync-deps.d.ts +2 -0
- package/dist/commands/sys/sync-deps.js +26 -15
- package/dist/commands/sys/sync-project-guidance.js +228 -145
- package/dist/commands/sys/sync-skills.js +28 -17
- package/dist/commands/sys/update.js +11 -3
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
- package/dist/core/__tests__/config-change-delta.test.js +106 -0
- package/dist/core/__tests__/context-intro.test.js +13 -9
- package/dist/core/__tests__/daemon-boot.test.js +22 -16
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
- package/dist/core/__tests__/human-deliver.test.js +19 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
- package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
- package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
- package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
- package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
- package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
- package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
- package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
- package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
- package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
- package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
- package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
- package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
- package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
- package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
- package/dist/core/__tests__/kickoff.test.js +16 -5
- package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
- package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
- package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
- package/dist/core/__tests__/repository-association.test.d.ts +1 -0
- package/dist/core/__tests__/repository-association.test.js +153 -0
- package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
- package/dist/core/__tests__/repository-root-identity.test.js +219 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
- package/dist/core/canvas/canvas.d.ts +3 -0
- package/dist/core/canvas/canvas.js +30 -10
- package/dist/core/canvas/db.js +28 -1
- package/dist/core/canvas/paths.d.ts +4 -6
- package/dist/core/canvas/paths.js +8 -6
- package/dist/core/canvas/render-source.js +2 -2
- package/dist/core/canvas/types.d.ts +9 -13
- package/dist/core/exclusive-lock.d.ts +2 -0
- package/dist/core/exclusive-lock.js +21 -0
- package/dist/core/git.d.ts +0 -1
- package/dist/core/git.js +0 -3
- package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
- package/dist/core/human/feedback-companion.js +3 -0
- package/dist/core/human/scan.js +8 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/memory/doc-link-grammar.js +2 -2
- package/dist/core/memory/history.d.ts +24 -0
- package/dist/core/memory/history.js +66 -1
- package/dist/core/memory/identity.d.ts +65 -0
- package/dist/core/memory/identity.js +185 -0
- package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
- package/dist/core/memory/inline-ref-guidance.js +1 -1
- package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
- package/dist/core/memory/inline-ref-inventory.js +30 -62
- package/dist/core/memory/lint.d.ts +129 -0
- package/dist/core/memory/lint.js +517 -0
- package/dist/core/memory/project-namespace.d.ts +31 -0
- package/dist/core/memory/project-namespace.js +85 -0
- package/dist/core/memory/repository-association.d.ts +31 -0
- package/dist/core/memory/repository-association.js +129 -0
- package/dist/core/memory/tree.d.ts +39 -0
- package/dist/core/memory/tree.js +93 -0
- package/dist/core/memory-resolver.d.ts +193 -90
- package/dist/core/memory-resolver.js +461 -375
- package/dist/core/nested-stores.js +6 -13
- package/dist/core/profiles/select.d.ts +4 -1
- package/dist/core/profiles/select.js +108 -48
- package/dist/core/review/__tests__/capture-origin.test.js +2 -2
- package/dist/core/review/__tests__/stage-identity.test.js +2 -2
- package/dist/core/review/companion.js +11 -2
- package/dist/core/runtime/bearings.d.ts +2 -2
- package/dist/core/runtime/bearings.js +3 -3
- package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
- package/dist/core/runtime/broker/rebind.js +5 -0
- package/dist/core/runtime/broker-extension-render.d.ts +7 -3
- package/dist/core/runtime/broker-extension-render.js +10 -5
- package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
- package/dist/core/runtime/broker-persona-guidance.js +122 -27
- package/dist/core/runtime/deliver-live.d.ts +16 -4
- package/dist/core/runtime/deliver-live.js +29 -15
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +8 -16
- package/dist/core/runtime/lifecycle.js +2 -3
- package/dist/core/runtime/nodes.d.ts +3 -4
- package/dist/core/runtime/nodes.js +3 -4
- package/dist/core/runtime/persona.d.ts +8 -12
- package/dist/core/runtime/persona.js +24 -96
- package/dist/core/runtime/promote.d.ts +2 -2
- package/dist/core/runtime/promote.js +8 -21
- package/dist/core/runtime/revive.js +20 -17
- package/dist/core/runtime/spawn.js +18 -7
- package/dist/core/runtime/tmux-bindings.js +2 -3
- package/dist/core/runtime/warm-pool.js +3 -4
- package/dist/core/scope.js +2 -0
- package/dist/core/self-update.d.ts +0 -2
- package/dist/core/self-update.js +2 -35
- package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
- package/dist/core/substrate/index.d.ts +2 -2
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +43 -27
- package/dist/core/substrate/injected-store.js +208 -104
- package/dist/core/substrate/listings.d.ts +19 -12
- package/dist/core/substrate/listings.js +75 -52
- package/dist/core/substrate/on-read-node.d.ts +4 -7
- package/dist/core/substrate/on-read-node.js +6 -8
- package/dist/core/substrate/on-read.d.ts +22 -25
- package/dist/core/substrate/on-read.js +103 -147
- package/dist/core/substrate/render-node.d.ts +4 -7
- package/dist/core/substrate/render-node.js +5 -7
- package/dist/core/substrate/render.d.ts +21 -3
- package/dist/core/substrate/render.js +291 -223
- package/dist/core/substrate/schema.d.ts +1 -13
- package/dist/core/substrate/schema.js +5 -40
- package/dist/core/substrate/session-cache.d.ts +14 -4
- package/dist/core/substrate/session-cache.js +40 -22
- package/dist/core/substrate/surface-match.d.ts +9 -7
- package/dist/core/substrate/surface-match.js +26 -25
- package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
- package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
- package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
- package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +37 -9
- package/dist/daemon/api/bridge.d.ts +35 -4
- package/dist/daemon/api/bridge.js +50 -8
- package/dist/daemon/api/handlers/broker-ops.js +21 -16
- package/dist/daemon/api/handlers/memory.js +2 -0
- package/dist/daemon/crtrd.js +2 -0
- package/dist/daemon/human/finish.js +8 -1
- package/dist/daemon/manage.d.ts +0 -1
- package/dist/daemon/manage.js +7 -16
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/migrations/001-surfaces-frontmatter.js +21 -109
- package/dist/migrations/002-profile-project-memory.js +1 -0
- package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
- package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
- package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
- package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
- package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
- package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
- package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
- package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
- package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
- package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
- package/dist/migrations/__tests__/activation.test.d.ts +1 -0
- package/dist/migrations/__tests__/activation.test.js +149 -0
- package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
- package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
- package/dist/migrations/activation.d.ts +16 -0
- package/dist/migrations/activation.js +78 -0
- package/dist/migrations/convergent.d.ts +14 -3
- package/dist/migrations/convergent.js +21 -10
- package/dist/migrations/corpus.d.ts +78 -0
- package/dist/migrations/corpus.js +497 -0
- package/dist/migrations/frontmatter-splice.d.ts +15 -0
- package/dist/migrations/frontmatter-splice.js +176 -0
- package/dist/migrations/registry.d.ts +6 -1
- package/dist/migrations/registry.js +7 -2
- package/dist/migrations/runner.d.ts +41 -0
- package/dist/migrations/runner.js +81 -0
- package/dist/migrations/types.d.ts +148 -9
- package/dist/migrations/types.js +9 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
- package/dist/pi-extensions/canvas-context-intro.js +50 -46
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
- package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
- package/dist/pi-extensions/canvas-stophook.js +6 -13
- package/dist/shared/generated-context.d.ts +0 -3
- package/dist/shared/generated-context.js +0 -57
- package/dist/shared/tool-groups.js +2 -3
- package/package.json +5 -4
- package/runtime.lock.json +2 -2
- /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
- /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
- /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
- /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
- /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
- /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
- /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
- /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
- /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
|
@@ -6,25 +6,53 @@ class FakeSocket extends EventEmitter {
|
|
|
6
6
|
pings = 0;
|
|
7
7
|
terminated = 0;
|
|
8
8
|
ping() { this.pings += 1; }
|
|
9
|
-
terminate() {
|
|
9
|
+
terminate() {
|
|
10
|
+
this.terminated += 1;
|
|
11
|
+
this.emit('terminated');
|
|
12
|
+
}
|
|
10
13
|
}
|
|
11
14
|
function sleep(ms) {
|
|
12
15
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
16
|
}
|
|
14
|
-
|
|
17
|
+
function waitForTermination(ws, timeoutMs = 1_000) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timeout = setTimeout(() => reject(new Error('heartbeat did not terminate the peer')), timeoutMs);
|
|
20
|
+
timeout.unref();
|
|
21
|
+
ws.once('terminated', () => {
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
resolve();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
test('a quiet attach puts nothing on the wire', async () => {
|
|
28
|
+
// Regression: an unconditional 25s ping made a hosted guest's connection never
|
|
29
|
+
// idle, so the sandbox never suspended and whole fleets billed for 15h awake.
|
|
30
|
+
const ws = new FakeSocket();
|
|
31
|
+
// The first timer tick is deliberately after the quiet window, so this test
|
|
32
|
+
// observes the contract without depending on scheduler timing.
|
|
33
|
+
const heartbeat = startRemoteAttachHeartbeat(ws, 20, 15);
|
|
34
|
+
await sleep(40);
|
|
35
|
+
assert.equal(ws.pings, 0, 'a quiet attach sends no heartbeat after its quiet window');
|
|
36
|
+
assert.equal(ws.terminated, 0, 'a quiet attach is left open, never terminated by the heartbeat');
|
|
37
|
+
heartbeat.stop();
|
|
38
|
+
});
|
|
39
|
+
test('relayed traffic arms the heartbeat and a pong keeps a relaying observer alive', async () => {
|
|
15
40
|
const ws = new FakeSocket();
|
|
16
|
-
const
|
|
41
|
+
const heartbeat = startRemoteAttachHeartbeat(ws, 20, 10_000);
|
|
42
|
+
heartbeat.noteActivity();
|
|
17
43
|
await sleep(25);
|
|
18
|
-
assert.equal(ws.pings, 1, '
|
|
44
|
+
assert.equal(ws.pings, 1, 'a relaying attach is pinged across a gap between frames');
|
|
19
45
|
ws.emit('pong');
|
|
20
46
|
await sleep(10);
|
|
21
47
|
assert.equal(ws.terminated, 0, 'a pong renews the heartbeat without terminating the observer');
|
|
22
|
-
stop();
|
|
48
|
+
heartbeat.stop();
|
|
23
49
|
});
|
|
24
|
-
test('
|
|
50
|
+
test('an unresponsive peer is dropped only while the relay is active', async () => {
|
|
25
51
|
const ws = new FakeSocket();
|
|
26
|
-
const
|
|
27
|
-
|
|
52
|
+
const heartbeat = startRemoteAttachHeartbeat(ws, 5, 10_000);
|
|
53
|
+
const terminated = waitForTermination(ws);
|
|
54
|
+
heartbeat.noteActivity();
|
|
55
|
+
await terminated;
|
|
28
56
|
assert.equal(ws.terminated, 1, 'two missed heartbeat intervals terminate the detached viewer');
|
|
29
|
-
stop();
|
|
57
|
+
heartbeat.stop();
|
|
30
58
|
});
|
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
2
|
import type { Duplex } from 'node:stream';
|
|
3
3
|
import { type WebSocket } from 'ws';
|
|
4
|
-
/** Keep
|
|
5
|
-
* idle limits. A viewer only observes a broker, so a missed heartbeat
|
|
6
|
-
* this socket but must never affect the broker or its in-flight work. */
|
|
4
|
+
/** Keep a RELAYING connection alive across a gap between frames, through common
|
|
5
|
+
* 60s proxy idle limits. A viewer only observes a broker, so a missed heartbeat
|
|
6
|
+
* may close this socket but must never affect the broker or its in-flight work. */
|
|
7
7
|
export declare const REMOTE_ATTACH_HEARTBEAT_MS = 25000;
|
|
8
|
-
|
|
8
|
+
/** How long after the last relayed frame the heartbeat keeps pinging. Past this,
|
|
9
|
+
* the attach is carrying nothing — no viewer output, no streaming node — and the
|
|
10
|
+
* heartbeat goes silent so the network path underneath can reap the connection.
|
|
11
|
+
*
|
|
12
|
+
* This matters far beyond a socket: on a hosted guest (Blaxel) a connection that
|
|
13
|
+
* is pinged every 25s is never idle, so the host never suspends the sandbox. An
|
|
14
|
+
* unconditional heartbeat held whole fleets awake for 15h at a time. A drop on a
|
|
15
|
+
* quiet attach is a normal close the remote peer re-attaches from, and real work
|
|
16
|
+
* is anchored by its own keep-alive, not by this ping.
|
|
17
|
+
*
|
|
18
|
+
* The clock starts when the attach OPENS, not at its first relayed frame: a
|
|
19
|
+
* never-relaying attach still gets pings until this window elapses. The welcome
|
|
20
|
+
* frame lands in milliseconds, so this only pads the tail of the very first
|
|
21
|
+
* window.
|
|
22
|
+
*
|
|
23
|
+
* ROLLOUT ORDER — this half is only half the answer, and shipping it FIRST is
|
|
24
|
+
* strictly worse than shipping nothing. The host that owns the attach must also
|
|
25
|
+
* let go of a quiet one; Northlight Core does that at its own 60s quiesce
|
|
26
|
+
* (`CROUTER_FOLLOW_QUIESCE_MS`). Against a host that holds the attach forever,
|
|
27
|
+
* going quiet here only moves the drop to the network path, the host reports a
|
|
28
|
+
* detach, its client reconnects, and the box wakes again — the same pin, now
|
|
29
|
+
* with a full re-snapshot every couple of minutes. Ship the host's quiesce
|
|
30
|
+
* first, then roll the guest image. */
|
|
31
|
+
export declare const REMOTE_ATTACH_QUIET_MS = 60000;
|
|
32
|
+
export interface RemoteAttachHeartbeat {
|
|
33
|
+
/** Stop the heartbeat permanently (teardown). Idempotent. */
|
|
34
|
+
stop: () => void;
|
|
35
|
+
/** Record a relayed frame in either direction — this is what keeps the
|
|
36
|
+
* heartbeat armed, and what re-arms it after a quiet stretch. */
|
|
37
|
+
noteActivity: () => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function startRemoteAttachHeartbeat(ws: WebSocket, intervalMs?: number, quietMs?: number): RemoteAttachHeartbeat;
|
|
9
40
|
/** Handle the HTTP upgrade for `GET /v1/nodes/{id}/attach`. A-9 matches the
|
|
10
41
|
* route, extracts `{id}`, and calls this with the raw upgrade triplet; this
|
|
11
42
|
* owns the WS handshake (its own noServer `wss` above) AND the spec §5.3
|
|
@@ -33,13 +33,38 @@ import { probeViewSocket, waitForBrokerViewSocket } from '../../core/runtime/vie
|
|
|
33
33
|
* client cap) so a remote peer cannot buffer 256MiB before the broker's 24MiB
|
|
34
34
|
* decoder rejects the message. */
|
|
35
35
|
const wss = new WebSocketServer({ noServer: true, maxPayload: BROKER_READ_CAPS.maxLineBytes });
|
|
36
|
-
/** Keep
|
|
37
|
-
* idle limits. A viewer only observes a broker, so a missed heartbeat
|
|
38
|
-
* this socket but must never affect the broker or its in-flight work. */
|
|
36
|
+
/** Keep a RELAYING connection alive across a gap between frames, through common
|
|
37
|
+
* 60s proxy idle limits. A viewer only observes a broker, so a missed heartbeat
|
|
38
|
+
* may close this socket but must never affect the broker or its in-flight work. */
|
|
39
39
|
export const REMOTE_ATTACH_HEARTBEAT_MS = 25_000;
|
|
40
|
-
|
|
40
|
+
/** How long after the last relayed frame the heartbeat keeps pinging. Past this,
|
|
41
|
+
* the attach is carrying nothing — no viewer output, no streaming node — and the
|
|
42
|
+
* heartbeat goes silent so the network path underneath can reap the connection.
|
|
43
|
+
*
|
|
44
|
+
* This matters far beyond a socket: on a hosted guest (Blaxel) a connection that
|
|
45
|
+
* is pinged every 25s is never idle, so the host never suspends the sandbox. An
|
|
46
|
+
* unconditional heartbeat held whole fleets awake for 15h at a time. A drop on a
|
|
47
|
+
* quiet attach is a normal close the remote peer re-attaches from, and real work
|
|
48
|
+
* is anchored by its own keep-alive, not by this ping.
|
|
49
|
+
*
|
|
50
|
+
* The clock starts when the attach OPENS, not at its first relayed frame: a
|
|
51
|
+
* never-relaying attach still gets pings until this window elapses. The welcome
|
|
52
|
+
* frame lands in milliseconds, so this only pads the tail of the very first
|
|
53
|
+
* window.
|
|
54
|
+
*
|
|
55
|
+
* ROLLOUT ORDER — this half is only half the answer, and shipping it FIRST is
|
|
56
|
+
* strictly worse than shipping nothing. The host that owns the attach must also
|
|
57
|
+
* let go of a quiet one; Northlight Core does that at its own 60s quiesce
|
|
58
|
+
* (`CROUTER_FOLLOW_QUIESCE_MS`). Against a host that holds the attach forever,
|
|
59
|
+
* going quiet here only moves the drop to the network path, the host reports a
|
|
60
|
+
* detach, its client reconnects, and the box wakes again — the same pin, now
|
|
61
|
+
* with a full re-snapshot every couple of minutes. Ship the host's quiesce
|
|
62
|
+
* first, then roll the guest image. */
|
|
63
|
+
export const REMOTE_ATTACH_QUIET_MS = 60_000;
|
|
64
|
+
export function startRemoteAttachHeartbeat(ws, intervalMs = REMOTE_ATTACH_HEARTBEAT_MS, quietMs = REMOTE_ATTACH_QUIET_MS) {
|
|
41
65
|
let alive = true;
|
|
42
66
|
let stopped = false;
|
|
67
|
+
let lastActivityAt = Date.now();
|
|
43
68
|
const acknowledge = () => { alive = true; };
|
|
44
69
|
const stop = () => {
|
|
45
70
|
if (stopped)
|
|
@@ -50,6 +75,13 @@ export function startRemoteAttachHeartbeat(ws, intervalMs = REMOTE_ATTACH_HEARTB
|
|
|
50
75
|
};
|
|
51
76
|
ws.on('pong', acknowledge);
|
|
52
77
|
const timer = setInterval(() => {
|
|
78
|
+
if (Date.now() - lastActivityAt > quietMs) {
|
|
79
|
+
// Nothing is flowing: send no ping and run no liveness check, so an idle
|
|
80
|
+
// attach puts zero packets on the wire. A dead peer here is closed by the
|
|
81
|
+
// network path or by teardown, not by us.
|
|
82
|
+
alive = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
53
85
|
if (!alive) {
|
|
54
86
|
stop();
|
|
55
87
|
ws.terminate();
|
|
@@ -65,7 +97,15 @@ export function startRemoteAttachHeartbeat(ws, intervalMs = REMOTE_ATTACH_HEARTB
|
|
|
65
97
|
}
|
|
66
98
|
}, intervalMs);
|
|
67
99
|
timer.unref();
|
|
68
|
-
return
|
|
100
|
+
return {
|
|
101
|
+
stop,
|
|
102
|
+
// Frames are not liveness evidence: only a pong proves the peer is there.
|
|
103
|
+
// Marking a relayed frame alive would disable dead-peer detection for
|
|
104
|
+
// exactly as long as a turn streams — the one stretch it is for.
|
|
105
|
+
noteActivity: () => {
|
|
106
|
+
lastActivityAt = Date.now();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
69
109
|
}
|
|
70
110
|
/** A WS close reason is capped at 123 UTF-8 bytes by the protocol. */
|
|
71
111
|
function clampReason(reason) {
|
|
@@ -140,7 +180,7 @@ function bridgeConnection(ws, nodeId) {
|
|
|
140
180
|
const decoder = new FrameDecoder(CLIENT_READ_CAPS);
|
|
141
181
|
let closed = false;
|
|
142
182
|
let viewerFaultRecorded = false;
|
|
143
|
-
let
|
|
183
|
+
let heartbeat;
|
|
144
184
|
socket.on('connect', () => {
|
|
145
185
|
clearFault(nodeId, { link: 'relay↔broker' });
|
|
146
186
|
});
|
|
@@ -148,7 +188,7 @@ function bridgeConnection(ws, nodeId) {
|
|
|
148
188
|
if (closed)
|
|
149
189
|
return;
|
|
150
190
|
closed = true;
|
|
151
|
-
|
|
191
|
+
heartbeat?.stop();
|
|
152
192
|
if (!socket.destroyed) {
|
|
153
193
|
try {
|
|
154
194
|
socket.destroy();
|
|
@@ -164,9 +204,10 @@ function bridgeConnection(ws, nodeId) {
|
|
|
164
204
|
/* ignore */
|
|
165
205
|
}
|
|
166
206
|
};
|
|
167
|
-
|
|
207
|
+
heartbeat = startRemoteAttachHeartbeat(ws);
|
|
168
208
|
// socket → WS: bounded decode, one WS text message per complete frame.
|
|
169
209
|
socket.on('data', (chunk) => {
|
|
210
|
+
heartbeat?.noteActivity();
|
|
170
211
|
let frames;
|
|
171
212
|
try {
|
|
172
213
|
frames = decoder.push(chunk);
|
|
@@ -245,6 +286,7 @@ function bridgeConnection(ws, nodeId) {
|
|
|
245
286
|
ws.on('message', (data) => {
|
|
246
287
|
if (closed || socket.destroyed)
|
|
247
288
|
return;
|
|
289
|
+
heartbeat?.noteActivity();
|
|
248
290
|
const buf = Array.isArray(data)
|
|
249
291
|
? Buffer.concat(data)
|
|
250
292
|
: Buffer.isBuffer(data)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// Broker-originated canvas operations. The broker sends Pi lifecycle facts;
|
|
2
2
|
// crtrd validates and commits their durable effects while the handler stays
|
|
3
3
|
// strictly canvas-write-only — no broker launch, kill, or view.sock round trip.
|
|
4
|
-
import { getNode, isWarmSpare, listNodes, recordPid, setPresence, subscriptionsOf, updateNode } from '../../../core/canvas/canvas.js';
|
|
5
|
-
import { isSafeNodeId } from '../../../core/canvas/paths.js';
|
|
4
|
+
import { canonicalSessionFile, findNodeBySessionFile, getNode, isWarmSpare, listNodes, recordPid, setPresence, subscriptionsOf, updateNode } from '../../../core/canvas/canvas.js';
|
|
6
5
|
import { editorLabel } from '../../../core/canvas/labels.js';
|
|
7
6
|
import { assembleNodeSubject } from '../../../core/substrate/subject.js';
|
|
8
7
|
import { withCanvasWrite } from '../../../core/canvas/db.js';
|
|
@@ -128,9 +127,9 @@ function extensionState(nodeId) {
|
|
|
128
127
|
.filter((child) => child !== null && child.kind !== 'human')
|
|
129
128
|
.sort((a, b) => (a.status === b.status ? 0 : a.status === 'active' ? -1 : b.status === 'active' ? 1 : 0))
|
|
130
129
|
.map(extensionNode);
|
|
131
|
-
const source =
|
|
132
|
-
?
|
|
133
|
-
:
|
|
130
|
+
const source = typeof node.fork_source_file === 'string'
|
|
131
|
+
? findNodeBySessionFile(node.fork_source_file)
|
|
132
|
+
: null;
|
|
134
133
|
const subject = assembleNodeSubject(nodeId);
|
|
135
134
|
if (subject === null)
|
|
136
135
|
throw notFound(`unknown node: ${nodeId}`, { received: nodeId });
|
|
@@ -146,7 +145,7 @@ function extensionState(nodeId) {
|
|
|
146
145
|
};
|
|
147
146
|
}
|
|
148
147
|
function samePersona(a, b) {
|
|
149
|
-
return a?.mode === b.mode && a?.lifecycle === b.lifecycle;
|
|
148
|
+
return a?.kind === b.kind && a?.mode === b.mode && a?.lifecycle === b.lifecycle;
|
|
150
149
|
}
|
|
151
150
|
function parseGeneratedNameBody(body) {
|
|
152
151
|
const value = objectBody(body, 'broker generated-name');
|
|
@@ -169,6 +168,7 @@ function parsePersonaAckBody(body) {
|
|
|
169
168
|
const parse = (key) => {
|
|
170
169
|
const persona = value[key];
|
|
171
170
|
if (typeof persona !== 'object' || persona === null || Array.isArray(persona)
|
|
171
|
+
|| typeof persona['kind'] !== 'string'
|
|
172
172
|
|| (persona['mode'] !== 'base' && persona['mode'] !== 'orchestrator')
|
|
173
173
|
|| (persona['lifecycle'] !== 'terminal' && persona['lifecycle'] !== 'resident')) {
|
|
174
174
|
throw usage(`${key} must be a persona state`);
|
|
@@ -180,6 +180,7 @@ function parsePersonaAckBody(body) {
|
|
|
180
180
|
function handleSessionBound(ctx) {
|
|
181
181
|
const nodeId = ctx.params['id'];
|
|
182
182
|
const request = parseSessionBoundBody(ctx.body);
|
|
183
|
+
const sessionFile = request.sessionFile === null ? null : canonicalSessionFile(request.sessionFile);
|
|
183
184
|
const result = withCanvasWrite(() => {
|
|
184
185
|
const before = requireNode(nodeId);
|
|
185
186
|
// The broker reports the markers present in Pi's current branch; crtrd owns
|
|
@@ -196,10 +197,10 @@ function handleSessionBound(ctx) {
|
|
|
196
197
|
action = 'relaunch_root';
|
|
197
198
|
}
|
|
198
199
|
else {
|
|
199
|
-
handleNewSession(nodeId, request.piSessionId,
|
|
200
|
+
handleNewSession(nodeId, request.piSessionId, sessionFile);
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
|
-
else if (before.fork_from != null && before.pi_session_id == null &&
|
|
203
|
+
else if (before.fork_from != null && before.pi_session_id == null && sessionFile === null) {
|
|
203
204
|
// Pi has not exposed complete writable coordinates for this ordinary fork.
|
|
204
205
|
// Leave it unbound so recovery cannot fork provenance twice.
|
|
205
206
|
action = 'shutdown';
|
|
@@ -210,7 +211,7 @@ function handleSessionBound(ctx) {
|
|
|
210
211
|
clearYieldMessage(nodeId);
|
|
211
212
|
updateNode(nodeId, {
|
|
212
213
|
pi_session_id: request.piSessionId,
|
|
213
|
-
pi_session_file:
|
|
214
|
+
pi_session_file: sessionFile,
|
|
214
215
|
cycle_pending: false,
|
|
215
216
|
});
|
|
216
217
|
recordPid(nodeId, request.pid);
|
|
@@ -378,20 +379,24 @@ function handleGeneratedName(ctx) {
|
|
|
378
379
|
});
|
|
379
380
|
return { status: 200, body: result };
|
|
380
381
|
}
|
|
381
|
-
/** Ack after
|
|
382
|
-
* stale broker from acknowledging a later
|
|
382
|
+
/** Ack after the matching baseline or transition context is durable. The
|
|
383
|
+
* expected pair prevents a stale broker from acknowledging a later change. */
|
|
383
384
|
function handlePersonaAck(ctx) {
|
|
384
385
|
const nodeId = ctx.params['id'];
|
|
385
386
|
const request = parsePersonaAckBody(ctx.body);
|
|
386
|
-
withCanvasWrite(() => {
|
|
387
|
+
const result = withCanvasWrite(() => {
|
|
387
388
|
const node = requireNode(nodeId);
|
|
388
|
-
const current = { mode: node.mode, lifecycle: node.lifecycle };
|
|
389
|
-
|
|
389
|
+
const current = { kind: node.kind, mode: node.mode, lifecycle: node.lifecycle };
|
|
390
|
+
// A pre-kind ack reads its kind as the node's current one, matching how the
|
|
391
|
+
// broker resolved the `from` it is acknowledging.
|
|
392
|
+
const ack = node.persona_ack;
|
|
393
|
+
const acknowledged = ack === undefined ? current : { kind: ack.kind ?? node.kind, mode: ack.mode, lifecycle: ack.lifecycle };
|
|
390
394
|
if (!samePersona(current, request.to) || !samePersona(acknowledged, request.from))
|
|
391
|
-
return;
|
|
395
|
+
return { applied: false };
|
|
392
396
|
updateNode(nodeId, { persona_ack: request.to });
|
|
397
|
+
return { applied: true };
|
|
393
398
|
});
|
|
394
|
-
return { status:
|
|
399
|
+
return { status: 200, body: result };
|
|
395
400
|
}
|
|
396
401
|
export const brokerOperationRoutes = [
|
|
397
402
|
{ method: 'POST', pattern: '/v1/nodes/:id/broker/session-bound', handler: handleSessionBound },
|
|
@@ -39,6 +39,8 @@ function handleResolve(ctx) {
|
|
|
39
39
|
name: doc.name,
|
|
40
40
|
scope: doc.scope,
|
|
41
41
|
path: doc.path,
|
|
42
|
+
store_root: doc.store.storeRoot,
|
|
43
|
+
physical_relative_path: doc.physicalRelativePath,
|
|
42
44
|
...(doc.plugin === undefined ? {} : { plugin: doc.plugin }),
|
|
43
45
|
};
|
|
44
46
|
return { status: 200, body };
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -75,6 +75,7 @@ import { migrateLegacyInboxIdentities } from '../core/feed/inbox-migrate.js';
|
|
|
75
75
|
import { emitEvent } from '../core/events/emit.js';
|
|
76
76
|
import { operationIdContext } from '../core/events/operation-id.js';
|
|
77
77
|
import { bindDaemonEventSource } from '../core/events/source.js';
|
|
78
|
+
import { ensureOnDiskMigrations } from '../migrations/activation.js';
|
|
78
79
|
// A broker DECLARES its owning canvas in argv at spawn (host.ts launch:
|
|
79
80
|
// `broker-cli.js --canvas-home <home> --epoch <id> <nodeId>`), so ownership is
|
|
80
81
|
// an exact match on this daemon's own canvas home — no path inference. An
|
|
@@ -350,6 +351,7 @@ export async function superviseTick(now = Date.now(), lifecycle = directTickLife
|
|
|
350
351
|
* quiesces admitted work, removes its readiness mirror, and releases its exact
|
|
351
352
|
* claim. */
|
|
352
353
|
export async function runDaemon(opts = {}) {
|
|
354
|
+
await ensureOnDiskMigrations();
|
|
353
355
|
const ownership = claimDaemonOwnership(randomBytes(16).toString('hex'));
|
|
354
356
|
if (!ownership.claimed) {
|
|
355
357
|
// A losing claim isn't always a singleton race: `own_identity_indeterminate`
|
|
@@ -282,7 +282,14 @@ export async function deliverTerminalResult(ticketId) {
|
|
|
282
282
|
}
|
|
283
283
|
if (failedTo.length > 0)
|
|
284
284
|
throw new Error(`cancellation notice failed for ${failedTo.join(', ')}`);
|
|
285
|
-
|
|
285
|
+
// Parking the bridge done is the cancellation's END STATE, not a step it
|
|
286
|
+
// owes: a bridge already done — parked by an earlier cancel replay, or by a
|
|
287
|
+
// close that finished it while the page was still pending — is already
|
|
288
|
+
// there, and `finish` is illegal from done. Re-running it would turn a
|
|
289
|
+
// recorded, delivered cancellation into delivery_failed and make every
|
|
290
|
+
// retry repeat that error.
|
|
291
|
+
if (node.status !== 'done')
|
|
292
|
+
transition(bridgeNodeId, 'finish');
|
|
286
293
|
return true;
|
|
287
294
|
}
|
|
288
295
|
if (result.kind === 'page') {
|
package/dist/daemon/manage.d.ts
CHANGED
|
@@ -47,7 +47,6 @@ export interface SpawnDaemonResult {
|
|
|
47
47
|
/** PID of the already-running daemon, if it was already up. */
|
|
48
48
|
existing_pid?: number;
|
|
49
49
|
}
|
|
50
|
-
export declare const DAEMON_VERIFY_WINDOW_MS = 20000;
|
|
51
50
|
export interface DaemonWaitDeps {
|
|
52
51
|
readPidfile?: () => number | null;
|
|
53
52
|
isPidAlive?: (pid: number) => boolean;
|
package/dist/daemon/manage.js
CHANGED
|
@@ -14,6 +14,7 @@ import { hostExecPath } from '../core/runtime/branded-host.js';
|
|
|
14
14
|
// Pulled from the lean db-free pidfile module (NOT crtrd.js, whose module graph
|
|
15
15
|
// reaches openDb) so the CLI daemon front door stays canvas.db-free (plan B-0).
|
|
16
16
|
import { isDaemonRunning, readPidfile, isPidAlive } from './pidfile.js';
|
|
17
|
+
import { DAEMON_STARTUP_WINDOW_MS } from './startup-policy.js';
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Daemon env sanitization
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
@@ -204,18 +205,8 @@ export async function execDaemon() {
|
|
|
204
205
|
const launchArgs = await resolveDaemonLaunch();
|
|
205
206
|
return execve(executable, [executable, ...launchArgs, '--canvas-home', crtrHome()], sanitizedDaemonEnv());
|
|
206
207
|
}
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
// A cold boot right after `install-runtime` (empty module cache, fresh
|
|
211
|
-
// generation, possible pending db migration) can take several seconds to reach
|
|
212
|
-
// writePidfile(), so the window is wide enough to tolerate that slow-but-valid
|
|
213
|
-
// cold start rather than report a false "did not become ready"; a process exit
|
|
214
|
-
// (signal/non-zero code) still fails immediately regardless of this window.
|
|
215
|
-
// Exported so `commands/api-client.ts` can align the CLI-side `CrtrClient`'s
|
|
216
|
-
// cold-start `/healthz` poll window with this authoritative valid-startup bound
|
|
217
|
-
// instead of the two windows drifting independently.
|
|
218
|
-
export const DAEMON_VERIFY_WINDOW_MS = 20_000;
|
|
208
|
+
// Shutdown stays short even though startup may include corpus migration.
|
|
209
|
+
const DAEMON_SHUTDOWN_WINDOW_MS = 20_000;
|
|
219
210
|
const DAEMON_VERIFY_POLL_MS = 10;
|
|
220
211
|
function sleepMs(ms) {
|
|
221
212
|
return new Promise((resolve) => {
|
|
@@ -235,7 +226,7 @@ function sleepMs(ms) {
|
|
|
235
226
|
*
|
|
236
227
|
* Returns null when the spawned pid owns the pidfile; returns a different live
|
|
237
228
|
* daemon pid when startup lost the race to an already-running daemon. */
|
|
238
|
-
export async function verifyDaemonStartup(pid, timeoutMs =
|
|
229
|
+
export async function verifyDaemonStartup(pid, timeoutMs = DAEMON_STARTUP_WINDOW_MS, deps = {}) {
|
|
239
230
|
const read = deps.readPidfile ?? readPidfile;
|
|
240
231
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
241
232
|
const exited = deps.childExited;
|
|
@@ -265,7 +256,7 @@ export async function verifyDaemonStartup(pid, timeoutMs = DAEMON_VERIFY_WINDOW_
|
|
|
265
256
|
/** Wait until a signaled daemon has exited and relinquished its pidfile. This
|
|
266
257
|
* makes a following start safe to claim the singleton rather than racing its
|
|
267
258
|
* previous owner. */
|
|
268
|
-
export async function waitForDaemonExit(pid, timeoutMs =
|
|
259
|
+
export async function waitForDaemonExit(pid, timeoutMs = DAEMON_SHUTDOWN_WINDOW_MS, deps = {}) {
|
|
269
260
|
const read = deps.readPidfile ?? readPidfile;
|
|
270
261
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
271
262
|
const sleep = deps.sleepMs ?? sleepMs;
|
|
@@ -298,7 +289,7 @@ const DAEMON_KILL_WINDOW_MS = 2_000;
|
|
|
298
289
|
* waits on process death alone after a kill, never on pidfile release.
|
|
299
290
|
*
|
|
300
291
|
* Returns how the daemon went down, for a truthful report. */
|
|
301
|
-
export async function stopDaemonProcess(pid, timeoutMs =
|
|
292
|
+
export async function stopDaemonProcess(pid, timeoutMs = DAEMON_SHUTDOWN_WINDOW_MS, deps = {}) {
|
|
302
293
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
303
294
|
const sleep = deps.sleepMs ?? sleepMs;
|
|
304
295
|
const now = deps.now ?? Date.now;
|
|
@@ -377,7 +368,7 @@ export async function spawnDaemon() {
|
|
|
377
368
|
exitState = { code, signal };
|
|
378
369
|
});
|
|
379
370
|
child.unref();
|
|
380
|
-
const existingPid = await verifyDaemonStartup(pid,
|
|
371
|
+
const existingPid = await verifyDaemonStartup(pid, DAEMON_STARTUP_WINDOW_MS, {
|
|
381
372
|
childExited: () => exitState,
|
|
382
373
|
});
|
|
383
374
|
if (existingPid !== null) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DAEMON_STARTUP_WINDOW_MS: number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DAEMON_STARTUP_WINDOW_MS = 5 * 60 * 1_000;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// 001 — the surfaces cut's frontmatter codemod. Folds the four old routing
|
|
2
2
|
// fields (`system-prompt-visibility`, `file-read-visibility`, `applies-to`,
|
|
3
|
-
// `read-when`) into explicit `surfaces` entries
|
|
4
|
-
// each doc's boot rung (per store)
|
|
5
|
-
// their `[[dir/INDEX]]` docs (a bare-dir ref used to deliver the INDEX body;
|
|
6
|
-
// after the cut it returns the directory listing).
|
|
3
|
+
// `read-when`) into explicit `surfaces` entries and bakes INDEX boot ceilings
|
|
4
|
+
// into each doc's boot rung (per store).
|
|
7
5
|
//
|
|
8
6
|
// Only docs with a valid substrate `kind` are touched. The old-schema parsing
|
|
9
7
|
// and ceiling walk are FROZEN COPIES of the pre-cut substrate code: the live
|
|
@@ -13,14 +11,13 @@
|
|
|
13
11
|
// Serialization is a textual splice: the dead keys' exact line spans are cut
|
|
14
12
|
// from the raw frontmatter and a rendered `surfaces:` block is appended, so
|
|
15
13
|
// every other byte survives verbatim and a migrated doc's diff shows only the
|
|
16
|
-
// dead fields dying
|
|
17
|
-
//
|
|
18
|
-
// folded scalars). Each splice is validated by re-parsing: the result must
|
|
14
|
+
// dead fields dying and `surfaces` appearing — never unrelated reformatting
|
|
15
|
+
// (re-serializing through the Document API would unfold folded scalars). Each splice is validated by re-parsing: the result must
|
|
19
16
|
// equal the original record minus the dead fields plus `surfaces`, or the
|
|
20
17
|
// migration throws naming the doc.
|
|
21
|
-
import {
|
|
18
|
+
import { parse as parseYaml } from 'yaml';
|
|
22
19
|
import { parseFrontmatterGeneric } from '../core/frontmatter.js';
|
|
23
|
-
import {
|
|
20
|
+
import { appendRenderedKey, dropTopLevelKeys, proveFrontmatterRoundTrip, rebuildDocSource, } from './frontmatter-splice.js';
|
|
24
21
|
const OLD_FIELDS = ['system-prompt-visibility', 'file-read-visibility', 'applies-to', 'read-when'];
|
|
25
22
|
// ---------------------------------------------------------------------------
|
|
26
23
|
// Frozen old-schema parsing (pre-cut substrate/schema.ts semantics).
|
|
@@ -133,122 +130,37 @@ function planEntries(d, bootRung) {
|
|
|
133
130
|
return entries;
|
|
134
131
|
}
|
|
135
132
|
// ---------------------------------------------------------------------------
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// to one migration target; profile-local names never leak across stores.
|
|
133
|
+
// Serialization: cut the dead keys' exact line spans out of the raw
|
|
134
|
+
// frontmatter text, append the rendered `surfaces:` block, re-parse to prove
|
|
135
|
+
// the splice is meaning-preserving.
|
|
140
136
|
// ---------------------------------------------------------------------------
|
|
141
|
-
function rewriteBareDirRefs(body, context, decisions) {
|
|
142
|
-
const links = findDocLinks(body);
|
|
143
|
-
if (links.length === 0)
|
|
144
|
-
return body;
|
|
145
|
-
let out = '';
|
|
146
|
-
let pos = 0;
|
|
147
|
-
for (const link of links) {
|
|
148
|
-
let repl = decisions.get(link.name);
|
|
149
|
-
if (repl === undefined) {
|
|
150
|
-
repl = context?.legacyDirectoryIndex(link.name) ?? null;
|
|
151
|
-
decisions.set(link.name, repl);
|
|
152
|
-
}
|
|
153
|
-
if (repl === null)
|
|
154
|
-
continue;
|
|
155
|
-
out += body.slice(pos, link.start) + `[[${repl}]]`;
|
|
156
|
-
pos = link.end;
|
|
157
|
-
}
|
|
158
|
-
return out + body.slice(pos);
|
|
159
|
-
}
|
|
160
|
-
function deepEqual(a, b) {
|
|
161
|
-
if (a === b)
|
|
162
|
-
return true;
|
|
163
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
164
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
165
|
-
return false;
|
|
166
|
-
return a.every((v, i) => deepEqual(v, b[i]));
|
|
167
|
-
}
|
|
168
|
-
if (a !== null && b !== null && typeof a === 'object' && typeof b === 'object') {
|
|
169
|
-
const ak = Object.keys(a);
|
|
170
|
-
const bk = Object.keys(b);
|
|
171
|
-
if (ak.length !== bk.length)
|
|
172
|
-
return false;
|
|
173
|
-
return ak.every((k) => deepEqual(a[k], b[k]));
|
|
174
|
-
}
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
137
|
function migratedYaml(rawYaml, entries, docPath) {
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// the value's node-end bounds its last continuation line).
|
|
184
|
-
const spans = [];
|
|
185
|
-
for (const item of ydoc.contents.items) {
|
|
186
|
-
if (!OLD_FIELDS.includes(String(item.key)))
|
|
187
|
-
continue;
|
|
188
|
-
const keyRange = item.key.range;
|
|
189
|
-
const valueRange = item.value?.range;
|
|
190
|
-
if (keyRange === undefined)
|
|
191
|
-
throw new Error(`surfaces migration: rangeless key in ${docPath}`);
|
|
192
|
-
const start = rawYaml.lastIndexOf('\n', keyRange[0] - 1) + 1;
|
|
193
|
-
let end = valueRange?.[2] ?? keyRange[2];
|
|
194
|
-
// Consume through end-of-line unless a block value already owns its newline.
|
|
195
|
-
if (!(end > 0 && rawYaml[end - 1] === '\n')) {
|
|
196
|
-
while (end < rawYaml.length && (rawYaml[end] === ' ' || rawYaml[end] === '\t' || rawYaml[end] === '\r'))
|
|
197
|
-
end++;
|
|
198
|
-
if (rawYaml[end] === '\n')
|
|
199
|
-
end++;
|
|
200
|
-
}
|
|
201
|
-
spans.push([start, end]);
|
|
202
|
-
}
|
|
203
|
-
spans.sort((a, b) => b[0] - a[0]);
|
|
204
|
-
let out = rawYaml;
|
|
205
|
-
for (const [s, e] of spans)
|
|
206
|
-
out = out.slice(0, s) + out.slice(e);
|
|
207
|
-
if (entries.length > 0) {
|
|
208
|
-
// lineWidth 0: render each entry's scalars on one line, never folded.
|
|
209
|
-
const block = stringify({ surfaces: entries }, { lineWidth: 0 });
|
|
210
|
-
out = out === '' ? block : (out.endsWith('\n') ? out : out + '\n') + block;
|
|
211
|
-
}
|
|
212
|
-
out = out.replace(/\r?\n$/, '');
|
|
138
|
+
let out = dropTopLevelKeys(rawYaml, OLD_FIELDS, docPath);
|
|
139
|
+
if (entries.length > 0)
|
|
140
|
+
out = appendRenderedKey(out, 'surfaces', entries, docPath);
|
|
141
|
+
else
|
|
142
|
+
out = out.replace(/\r?\n$/, '');
|
|
213
143
|
const expected = { ...parseYaml(rawYaml) };
|
|
214
144
|
for (const k of OLD_FIELDS)
|
|
215
145
|
delete expected[k];
|
|
216
146
|
if (entries.length > 0)
|
|
217
147
|
expected['surfaces'] = entries;
|
|
218
|
-
|
|
219
|
-
if (!deepEqual(actual ?? {}, expected)) {
|
|
220
|
-
throw new Error(`surfaces migration: spliced frontmatter does not round-trip in ${docPath}`);
|
|
221
|
-
}
|
|
148
|
+
proveFrontmatterRoundTrip(out, expected, docPath);
|
|
222
149
|
return out;
|
|
223
150
|
}
|
|
224
|
-
function rebuildSource(d, newYaml, newBody) {
|
|
225
|
-
const { doc } = d;
|
|
226
|
-
const bodyStart = doc.source.length - doc.body.length;
|
|
227
|
-
const fmBlock = doc.source.slice(0, bodyStart);
|
|
228
|
-
if (newYaml === null)
|
|
229
|
-
return fmBlock + newBody;
|
|
230
|
-
const { raw } = parseFrontmatterGeneric(doc.source);
|
|
231
|
-
const at = fmBlock.indexOf(raw);
|
|
232
|
-
if (raw === '' || at < 0)
|
|
233
|
-
throw new Error(`surfaces migration: cannot locate frontmatter block in ${doc.path}`);
|
|
234
|
-
return fmBlock.slice(0, at) + newYaml + fmBlock.slice(at + raw.length) + newBody;
|
|
235
|
-
}
|
|
236
151
|
export const surfacesFrontmatterMigration = {
|
|
152
|
+
id: '001-surfaces-frontmatter',
|
|
237
153
|
lane: 'convergent',
|
|
238
|
-
description: 'surfaces frontmatter: fold visibility/applies-to/read-when routing into surfaces entries
|
|
239
|
-
apply(store
|
|
154
|
+
description: 'surfaces frontmatter: fold visibility/applies-to/read-when routing into surfaces entries',
|
|
155
|
+
apply(store) {
|
|
240
156
|
const docs = store.docs.map(classify).filter((d) => d !== null);
|
|
241
157
|
const ceiling = buildCeiling(docs);
|
|
242
|
-
const decisions = new Map();
|
|
243
158
|
const changes = [];
|
|
244
159
|
for (const d of docs) {
|
|
245
|
-
|
|
246
|
-
if (!d.hasOldFields && newBody === d.doc.body)
|
|
160
|
+
if (!d.hasOldFields)
|
|
247
161
|
continue;
|
|
248
|
-
const newYaml = d.
|
|
249
|
-
|
|
250
|
-
: null;
|
|
251
|
-
changes.push({ path: d.doc.path, after: rebuildSource(d, newYaml, newBody) });
|
|
162
|
+
const newYaml = migratedYaml(parseFrontmatterGeneric(d.doc.source).raw, planEntries(d, effectiveBootRung(d, ceiling)), d.doc.path);
|
|
163
|
+
changes.push({ path: d.doc.path, after: rebuildDocSource(d.doc.source, newYaml, d.doc.path) });
|
|
252
164
|
}
|
|
253
165
|
return changes;
|
|
254
166
|
},
|
|
@@ -32,6 +32,7 @@ function isMemory(value) {
|
|
|
32
32
|
return typeof value === 'string' && PROFILE_PROJECT_MEMORY_VALUES.includes(value);
|
|
33
33
|
}
|
|
34
34
|
export const profileProjectMemoryMigration = {
|
|
35
|
+
id: '002-profile-project-memory',
|
|
35
36
|
lane: 'profile-manifest',
|
|
36
37
|
description: 'profile projects carry an explicit memory value',
|
|
37
38
|
apply(input) {
|