@crouton-kit/crouter 0.3.39 → 0.3.41
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 +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
- package/dist/clients/attach/attach-cmd.js +706 -733
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/web/server.js +5 -28
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
- package/dist/commands/canvas-rebuild-index.js +5 -5
- package/dist/commands/node-snapshot.js +9 -1
- package/dist/commands/node.js +37 -17
- package/dist/commands/push.js +8 -0
- package/dist/commands/revive.d.ts +10 -0
- package/dist/commands/revive.js +28 -10
- package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/setup-core.js +3 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +94 -0
- package/dist/core/__tests__/boot.test.js +4 -4
- package/dist/core/__tests__/canvas.test.js +19 -7
- package/dist/core/__tests__/child-followup.test.js +15 -5
- package/dist/core/__tests__/daemon-boot.test.js +6 -1
- package/dist/core/__tests__/daemon-wedge.test.js +18 -1
- package/dist/core/__tests__/fault-classifier.test.js +30 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
- package/dist/core/__tests__/full/spike-harness.test.js +8 -5
- package/dist/core/__tests__/grace-clock.test.js +18 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
- package/dist/core/__tests__/prune-to-limit.test.js +14 -0
- package/dist/core/__tests__/review-model-floor.test.js +32 -0
- package/dist/core/__tests__/revive.test.js +18 -4
- package/dist/core/__tests__/session-cycles.test.js +77 -0
- package/dist/core/__tests__/worktree.test.js +85 -0
- package/dist/core/canvas/boot.js +12 -7
- package/dist/core/canvas/canvas.d.ts +27 -8
- package/dist/core/canvas/canvas.js +54 -26
- package/dist/core/canvas/db.js +14 -0
- package/dist/core/canvas/history.js +1 -0
- package/dist/core/canvas/paths.d.ts +10 -9
- package/dist/core/canvas/paths.js +10 -9
- package/dist/core/canvas/pid.d.ts +155 -1
- package/dist/core/canvas/pid.js +306 -1
- package/dist/core/canvas/status-glyph.d.ts +7 -0
- package/dist/core/canvas/status-glyph.js +10 -1
- package/dist/core/canvas/types.d.ts +34 -0
- package/dist/core/fault-classifier.js +5 -1
- package/dist/core/runtime/bearings.js +4 -0
- package/dist/core/runtime/branded-host.d.ts +7 -0
- package/dist/core/runtime/branded-host.js +44 -17
- package/dist/core/runtime/broker-sdk.js +28 -68
- package/dist/core/runtime/broker.js +35 -4
- package/dist/core/runtime/host.d.ts +3 -3
- package/dist/core/runtime/host.js +148 -33
- package/dist/core/runtime/launch.d.ts +15 -15
- package/dist/core/runtime/launch.js +58 -4
- package/dist/core/runtime/naming.js +3 -2
- package/dist/core/runtime/nodes.d.ts +3 -1
- package/dist/core/runtime/nodes.js +1 -0
- package/dist/core/runtime/pi-cli.d.ts +2 -0
- package/dist/core/runtime/pi-cli.js +51 -0
- package/dist/core/runtime/placement.d.ts +1 -1
- package/dist/core/runtime/placement.js +5 -2
- package/dist/core/runtime/recap.js +2 -1
- package/dist/core/runtime/revive.d.ts +21 -15
- package/dist/core/runtime/revive.js +87 -52
- package/dist/core/runtime/session-cycles.d.ts +30 -0
- package/dist/core/runtime/session-cycles.js +77 -0
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +152 -100
- package/dist/core/runtime/tmux.d.ts +7 -10
- package/dist/core/runtime/tmux.js +9 -11
- package/dist/core/worktree.d.ts +35 -0
- package/dist/core/worktree.js +158 -0
- package/dist/daemon/crtrd.d.ts +17 -8
- package/dist/daemon/crtrd.js +191 -40
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
- package/dist/pi-extensions/canvas-context-intro.js +34 -23
- package/dist/pi-extensions/canvas-stophook.js +11 -5
- package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
- package/dist/web-client/assets/index-DrkcvANq.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/docs/compat/hearth-crtr-v1.md +191 -0
- package/docs/public-api.md +75 -0
- package/package.json +3 -4
- package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
- package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
- package/dist/core/hearth/config.d.ts +0 -3
- package/dist/core/hearth/config.js +0 -108
- package/dist/core/hearth/guest-env.d.ts +0 -9
- package/dist/core/hearth/guest-env.js +0 -27
- package/dist/core/hearth/index.d.ts +0 -4
- package/dist/core/hearth/index.js +0 -4
- package/dist/core/hearth/model-auth-guest.d.ts +0 -8
- package/dist/core/hearth/model-auth-guest.js +0 -430
- package/dist/core/hearth/provider.d.ts +0 -36
- package/dist/core/hearth/provider.js +0 -10
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -254
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
- package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
- package/dist/core/hearth/providers/blaxel-home.d.ts +0 -54
- package/dist/core/hearth/providers/blaxel-home.js +0 -386
- package/dist/core/hearth/providers/blaxel.d.ts +0 -36
- package/dist/core/hearth/providers/blaxel.js +0 -364
- package/dist/core/hearth/providers/types.d.ts +0 -93
- package/dist/core/hearth/types.d.ts +0 -155
- package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
- package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
- package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
- package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
- package/dist/hearth/control-plane/config.d.ts +0 -21
- package/dist/hearth/control-plane/config.js +0 -77
- package/dist/hearth/control-plane/db.d.ts +0 -30
- package/dist/hearth/control-plane/db.js +0 -561
- package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
- package/dist/hearth/control-plane/hearth-target.js +0 -68
- package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
- package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
- package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
- package/dist/hearth/control-plane/ingress/route-store.js +0 -61
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
- package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
- package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
- package/dist/hearth/control-plane/main.d.ts +0 -1
- package/dist/hearth/control-plane/main.js +0 -88
- package/dist/hearth/control-plane/node-message.d.ts +0 -31
- package/dist/hearth/control-plane/node-message.js +0 -98
- package/dist/hearth/control-plane/register.d.ts +0 -15
- package/dist/hearth/control-plane/register.js +0 -34
- package/dist/hearth/control-plane/registry.d.ts +0 -22
- package/dist/hearth/control-plane/registry.js +0 -168
- package/dist/hearth/control-plane/relay.d.ts +0 -44
- package/dist/hearth/control-plane/relay.js +0 -711
- package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
- package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
- package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
- package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
- package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
- package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
- package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
- package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
- package/dist/hearth/control-plane/secrets.d.ts +0 -31
- package/dist/hearth/control-plane/secrets.js +0 -134
- package/dist/hearth/control-plane/server.d.ts +0 -27
- package/dist/hearth/control-plane/server.js +0 -482
- package/dist/hearth/control-plane/serving.d.ts +0 -15
- package/dist/hearth/control-plane/serving.js +0 -106
- package/dist/hearth/control-plane/session.d.ts +0 -68
- package/dist/hearth/control-plane/session.js +0 -273
- package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
- package/dist/hearth/control-plane/triggers/acl.js +0 -52
- package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
- package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
- package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
- package/dist/hearth/control-plane/triggers/deliver.js +0 -76
- package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
- package/dist/hearth/control-plane/triggers/envelope.js +0 -38
- package/dist/hearth/control-plane/types.d.ts +0 -86
- package/dist/hearth/control-plane/types.js +0 -1
- package/dist/hearth/control-plane/wake.d.ts +0 -86
- package/dist/hearth/control-plane/wake.js +0 -550
- package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
- /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
- /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/session-cycles.test.ts
|
|
2
|
+
//
|
|
3
|
+
// `cycleAwareMessages` reuses pi's own `buildSessionContext` to replay a PRIOR
|
|
4
|
+
// cycle's branch, so a cycle that was compacted before it yielded renders its
|
|
5
|
+
// compaction summary (and only the kept + post-compaction messages) instead of
|
|
6
|
+
// every raw pre-compaction message. This is a light smoke test — the
|
|
7
|
+
// compaction resolution itself is SDK behavior being delegated to, not
|
|
8
|
+
// reimplemented here; this only proves the wiring is real.
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { buildSessionContext } from '@earendil-works/pi-coding-agent';
|
|
12
|
+
import { cycleAwareMessages, CRTR_CYCLE_CUSTOM_TYPE } from '../runtime/session-cycles.js';
|
|
13
|
+
function userMsg(id, parentId, text) {
|
|
14
|
+
return {
|
|
15
|
+
type: 'message',
|
|
16
|
+
id,
|
|
17
|
+
parentId,
|
|
18
|
+
timestamp: new Date(0).toISOString(),
|
|
19
|
+
message: { role: 'user', content: text, timestamp: 0 },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
test('cycleAwareMessages: a compacted prior cycle replays via buildSessionContext (one summary, no raw pre-compaction messages)', () => {
|
|
23
|
+
// Cycle 0: m1 -> m2 -> [compaction c1, keeping from m2] -> m3 (cycle 0's leaf
|
|
24
|
+
// at yield time). Cycle marker cy1 roots a FRESH branch (parentId: null, per
|
|
25
|
+
// resetLeaf()+appendCustomEntry semantics), fromLeaf points back at m3. Cycle
|
|
26
|
+
// 1: m4 is the live leaf.
|
|
27
|
+
const m1 = userMsg('m1', null, 'pre-compaction message — must NOT appear in the replay');
|
|
28
|
+
const m2 = userMsg('m2', 'm1', 'kept message — compaction keeps from here');
|
|
29
|
+
const c1 = {
|
|
30
|
+
type: 'compaction',
|
|
31
|
+
id: 'c1',
|
|
32
|
+
parentId: 'm2',
|
|
33
|
+
timestamp: new Date(0).toISOString(),
|
|
34
|
+
summary: 'summary of the abandoned context',
|
|
35
|
+
firstKeptEntryId: 'm2',
|
|
36
|
+
tokensBefore: 12_000,
|
|
37
|
+
};
|
|
38
|
+
const m3 = userMsg('m3', 'c1', 'post-compaction message, cycle 0 leaf at yield');
|
|
39
|
+
const cy1 = {
|
|
40
|
+
type: 'custom',
|
|
41
|
+
id: 'cy1',
|
|
42
|
+
parentId: null,
|
|
43
|
+
timestamp: new Date(1).toISOString(),
|
|
44
|
+
customType: CRTR_CYCLE_CUSTOM_TYPE,
|
|
45
|
+
data: { cycle: 2, fromLeaf: 'm3' },
|
|
46
|
+
};
|
|
47
|
+
const m4 = userMsg('m4', 'cy1', 'cycle 1 live message');
|
|
48
|
+
const entries = [m1, m2, c1, m3, cy1, m4];
|
|
49
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
50
|
+
function branchFrom(fromId) {
|
|
51
|
+
const path = [];
|
|
52
|
+
let current = fromId ? byId.get(fromId) : undefined;
|
|
53
|
+
while (current) {
|
|
54
|
+
path.push(current);
|
|
55
|
+
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
56
|
+
}
|
|
57
|
+
return path.reverse();
|
|
58
|
+
}
|
|
59
|
+
// Minimal fake mirroring the real SDK's SessionManager just enough to drive
|
|
60
|
+
// cycleAwareMessages: buildSessionContext() (current leaf only, m4) plus the
|
|
61
|
+
// tree accessors it reads (getEntries/getBranch).
|
|
62
|
+
const sm = {
|
|
63
|
+
buildSessionContext: () => buildSessionContext(entries, 'm4', byId),
|
|
64
|
+
getEntries: () => entries,
|
|
65
|
+
getBranch: (fromId) => branchFrom(fromId ?? 'm4'),
|
|
66
|
+
};
|
|
67
|
+
const out = cycleAwareMessages(sm);
|
|
68
|
+
const summaries = out.filter((m) => m.role === 'compactionSummary');
|
|
69
|
+
assert.equal(summaries.length, 1, 'exactly one compactionSummary message from the replayed prior cycle');
|
|
70
|
+
const texts = out.map((m) => (typeof m.content === 'string' ? m.content : ''));
|
|
71
|
+
assert.ok(!texts.some((t) => t.includes('pre-compaction message')), 'the pre-compaction raw message (m1) must NOT appear');
|
|
72
|
+
assert.ok(texts.some((t) => t.includes('kept message')), 'the kept message (m2) DOES appear');
|
|
73
|
+
assert.ok(texts.some((t) => t.includes('cycle 0 leaf at yield')), 'the post-compaction message (m3) DOES appear');
|
|
74
|
+
assert.ok(texts.some((t) => t.includes('cycle 1 live message')), 'the live cycle (m4) DOES appear');
|
|
75
|
+
const dividers = out.filter((m) => m.customType === 'crtr-cycle-divider');
|
|
76
|
+
assert.equal(dividers.length, 1, 'one divider between the replayed prior cycle and the live one');
|
|
77
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync, existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { gitSync } from '../git.js';
|
|
7
|
+
import { createManagedWorktree, closeManagedWorktree, managedWorktreePath } from '../worktree.js';
|
|
8
|
+
import { createNode, getNode } from '../canvas/canvas.js';
|
|
9
|
+
import { closeDb } from '../canvas/db.js';
|
|
10
|
+
let home;
|
|
11
|
+
let remoteDir;
|
|
12
|
+
let sourceDir;
|
|
13
|
+
function run(args, cwd) {
|
|
14
|
+
const res = gitSync(args, cwd);
|
|
15
|
+
if (res.status !== 0) {
|
|
16
|
+
throw new Error(`git ${args.join(' ')} failed in ${cwd}: ${res.stderr || res.stdout}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function node(id, over = {}) {
|
|
20
|
+
return {
|
|
21
|
+
node_id: id,
|
|
22
|
+
name: id,
|
|
23
|
+
created: new Date().toISOString(),
|
|
24
|
+
cwd: '/tmp/work',
|
|
25
|
+
kind: 'general',
|
|
26
|
+
mode: 'base',
|
|
27
|
+
lifecycle: 'terminal',
|
|
28
|
+
status: 'active',
|
|
29
|
+
...over,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
before(() => {
|
|
33
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-worktree-home-'));
|
|
34
|
+
process.env['CRTR_HOME'] = home;
|
|
35
|
+
// Bare "remote" the source repo pushes to, so `origin/main` resolves.
|
|
36
|
+
remoteDir = mkdtempSync(join(tmpdir(), 'crtr-worktree-remote-'));
|
|
37
|
+
run(['init', '--bare'], remoteDir);
|
|
38
|
+
sourceDir = mkdtempSync(join(tmpdir(), 'crtr-worktree-source-'));
|
|
39
|
+
run(['init', '-b', 'main'], sourceDir);
|
|
40
|
+
run(['config', 'user.email', 'test@example.com'], sourceDir);
|
|
41
|
+
run(['config', 'user.name', 'Test'], sourceDir);
|
|
42
|
+
writeFileSync(join(sourceDir, 'README.md'), 'hello\n');
|
|
43
|
+
run(['add', '.'], sourceDir);
|
|
44
|
+
run(['commit', '-m', 'init'], sourceDir);
|
|
45
|
+
run(['remote', 'add', 'origin', remoteDir], sourceDir);
|
|
46
|
+
run(['push', '-u', 'origin', 'main'], sourceDir);
|
|
47
|
+
});
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
closeDb();
|
|
50
|
+
rmSync(home, { recursive: true, force: true });
|
|
51
|
+
process.env['CRTR_HOME'] = home;
|
|
52
|
+
});
|
|
53
|
+
after(() => {
|
|
54
|
+
closeDb();
|
|
55
|
+
rmSync(home, { recursive: true, force: true });
|
|
56
|
+
rmSync(remoteDir, { recursive: true, force: true });
|
|
57
|
+
rmSync(sourceDir, { recursive: true, force: true });
|
|
58
|
+
delete process.env['CRTR_HOME'];
|
|
59
|
+
});
|
|
60
|
+
// Regression for #120: `git worktree remove` deletes the checkout directory
|
|
61
|
+
// that IS the calling node's own pi-process cwd. A process can never chdir
|
|
62
|
+
// itself out of a directory that vanishes out from under it, so once that
|
|
63
|
+
// dir is gone every subsequent bash in that node fails permanently and the
|
|
64
|
+
// mandated post-close `crtr push final` can never run. closeManagedWorktree
|
|
65
|
+
// must leave the path resolvable (as an empty tombstone dir) even though the
|
|
66
|
+
// git worktree registration itself is gone.
|
|
67
|
+
test('closeManagedWorktree recreates the worktree path as a tombstone dir so the node cwd still resolves', () => {
|
|
68
|
+
const nodeId = 'wt-close-1';
|
|
69
|
+
const wt = createManagedWorktree(sourceDir, nodeId);
|
|
70
|
+
assert.equal(wt.path, managedWorktreePath(nodeId));
|
|
71
|
+
assert.ok(existsSync(wt.path), 'worktree checkout should exist after create');
|
|
72
|
+
createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
|
|
73
|
+
const result = closeManagedWorktree(nodeId);
|
|
74
|
+
assert.equal(result.worktree_removed, true);
|
|
75
|
+
// The path must still resolve as a directory — this is the actual fix: a
|
|
76
|
+
// node whose cwd was wt.path can still chdir there / run bash / push final.
|
|
77
|
+
assert.ok(existsSync(wt.path), 'worktree path must still exist after close (tombstone)');
|
|
78
|
+
assert.ok(statSync(wt.path).isDirectory(), 'recreated path must be a directory');
|
|
79
|
+
assert.deepEqual(readdirSync(wt.path), [], 'tombstone dir should be empty — git already removed the real checkout');
|
|
80
|
+
// git itself no longer tracks it as a worktree.
|
|
81
|
+
const list = gitSync(['worktree', 'list', '--porcelain'], sourceDir);
|
|
82
|
+
assert.ok(!list.stdout.includes(wt.path), 'git should no longer register the removed worktree');
|
|
83
|
+
const meta = getNode(nodeId);
|
|
84
|
+
assert.equal(meta?.managed_worktree?.state, 'closed');
|
|
85
|
+
});
|
package/dist/core/canvas/boot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Detects a boot change (a host reboot, or — the case this exists for — a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// VM/sandbox destroy+recreate) so callers can stop trusting any `pi_pid`
|
|
3
|
+
// recorded before it.
|
|
4
4
|
//
|
|
5
5
|
// canvas.db is durable — it survives a VM/sandbox destroy+recreate or a host
|
|
6
6
|
// reboot on whatever volume it lives on (see paths.ts / the storage-tiers
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
//
|
|
15
15
|
// The signal (primary): the Linux kernel's per-boot identity at
|
|
16
16
|
// `/proc/sys/kernel/random/boot_id` — a fresh random UUID minted on every
|
|
17
|
-
// kernel boot. A
|
|
17
|
+
// kernel boot. A microVM recreate (a remote/guest deployment's shape) boots
|
|
18
18
|
// a brand-new kernel, so `boot_id` changes; a plain daemon restart within the
|
|
19
19
|
// same boot leaves it identical. This is an EXACT identity, so on Linux we need
|
|
20
20
|
// no wall-clock window at all. NOTE this detects a *kernel boot*, not an
|
|
21
21
|
// arbitrary container recreate: an ordinary container that shares the host
|
|
22
22
|
// kernel keeps the host's `boot_id` across recreation, so this would NOT fire
|
|
23
|
-
// there — that shape is out of scope
|
|
23
|
+
// there — that shape is out of scope.
|
|
24
24
|
//
|
|
25
25
|
// The signal (fallback, non-Linux dev only): `os.uptime()` resets to ~0 on a
|
|
26
26
|
// reboot and is otherwise monotonic, so `now - uptime` estimates the current
|
|
@@ -44,8 +44,8 @@ import { openDb } from './db.js';
|
|
|
44
44
|
* dev). A false positive would clear a genuinely live pid and let reviveNode
|
|
45
45
|
* double-spawn onto the same session; a false negative just falls back to the
|
|
46
46
|
* manual `--fresh` status quo — so it is kept generous, biased against the
|
|
47
|
-
* dangerous direction. The production target (Linux
|
|
48
|
-
* this branch; there `boot_id` is exact. */
|
|
47
|
+
* dangerous direction. The production target (Linux microVM guests) never
|
|
48
|
+
* reaches this branch; there `boot_id` is exact. */
|
|
49
49
|
const BOOT_EPOCH_TOLERANCE_MS = 60_000;
|
|
50
50
|
function readKernelBootId() {
|
|
51
51
|
try {
|
|
@@ -135,7 +135,12 @@ export function reconcileBootLiveness(current = currentBootIdentity()) {
|
|
|
135
135
|
const rows = db
|
|
136
136
|
.prepare('SELECT node_id FROM nodes WHERE pi_pid IS NOT NULL')
|
|
137
137
|
.all();
|
|
138
|
-
|
|
138
|
+
// Clear the launch-time identity baseline (crouter#98 review finding 1)
|
|
139
|
+
// alongside `pi_pid` — a boot resets the OS process table, so any recorded
|
|
140
|
+
// identity is as meaningless as the pid it was captured for; leaving it
|
|
141
|
+
// behind would let a coincidentally-reused pid on the NEW boot compare
|
|
142
|
+
// against a stale baseline from the OLD one.
|
|
143
|
+
const upd = db.prepare('UPDATE nodes SET pi_pid = NULL, pi_pid_identity = NULL WHERE node_id = ?');
|
|
139
144
|
const cleared = [];
|
|
140
145
|
for (const row of rows) {
|
|
141
146
|
upd.run(row.node_id);
|
|
@@ -32,9 +32,23 @@ export declare function setPresence(nodeId: string, presence: {
|
|
|
32
32
|
window?: string | null;
|
|
33
33
|
pane?: string | null;
|
|
34
34
|
}): void;
|
|
35
|
-
/** Record the live pi pid (daemon liveness signal)
|
|
35
|
+
/** Record the live pi pid (daemon liveness signal) PLUS its launch-time
|
|
36
|
+
* identity fingerprint, in one atomic write (crouter#98 review finding 1):
|
|
37
|
+
* `capturePidIdentities([pid])` is a signal-0-adjacent `ps -p <pid>` probe
|
|
38
|
+
* taken RIGHT NOW, while `pid` is the process we just spawned/bound and
|
|
39
|
+
* therefore definitely still exists — this is the LAUNCH-time baseline
|
|
40
|
+
* `headlessBrokerHost.teardown()` later compares against before signaling
|
|
41
|
+
* anything. A failed probe (rare — `ps` itself misbehaving) or a probe that
|
|
42
|
+
* finds no row for `pid` (raced away in the instant between spawn and this
|
|
43
|
+
* call) both write `NULL`: no baseline recorded, so the teardown guard fails
|
|
44
|
+
* open exactly like a node booted before this column existed — never a false
|
|
45
|
+
* identity that could itself cause a false mismatch later. */
|
|
36
46
|
export declare function recordPid(nodeId: string, pid: number): void;
|
|
37
|
-
/** Clear the pi pid (window-backed relaunch, before the fresh pi re-records it)
|
|
47
|
+
/** Clear the pi pid (window-backed relaunch, before the fresh pi re-records it)
|
|
48
|
+
* AND its recorded launch-time identity — the two are written together by
|
|
49
|
+
* `recordPid` and must be cleared together, or a subsequent relaunch that
|
|
50
|
+
* reuses the pid namespace could compare its own fresh pid against a STALE
|
|
51
|
+
* identity left over from the pid this row held before. */
|
|
38
52
|
export declare function clearPid(nodeId: string): void;
|
|
39
53
|
/** All rows, optionally filtered by status. */
|
|
40
54
|
export declare function listNodes(filter?: {
|
|
@@ -121,14 +135,19 @@ export declare function pruneNodes(opts: {
|
|
|
121
135
|
* `max(maxNodes, liveCount)`. Bounds the otherwise-unbounded growth the TTL
|
|
122
136
|
* sweep can't (a canvas that never crosses the TTL still grows without limit).
|
|
123
137
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
138
|
+
* Protected (never a prune candidate, regardless of recency rank): any
|
|
139
|
+
* non-terminal node — status active|idle — REGARDLESS of `pi_pid` liveness.
|
|
140
|
+
* A crashed-but-non-terminal row (dead pid, status still active/idle) must
|
|
141
|
+
* survive for daemon crash-recovery to resume, not be hard-deleted by a
|
|
142
|
+
* count-based sweep — only a genuinely terminal/dead-lifecycle row is
|
|
143
|
+
* eligible for count-based pruning. The caller ($CRTR_NODE_ID) is also always
|
|
144
|
+
* protected.
|
|
127
145
|
*
|
|
128
146
|
* Algorithm: order ALL nodes by recency DESC (telemetry.json mtime, falling back
|
|
129
|
-
* to `created`); `target = max(maxNodes,
|
|
130
|
-
* beyond `target` (0-indexed rank >= target) that is NOT
|
|
131
|
-
* Retained = all
|
|
147
|
+
* to `created`); `target = max(maxNodes, protectedCount)`; delete every node
|
|
148
|
+
* ranked beyond `target` (0-indexed rank >= target) that is NOT protected and
|
|
149
|
+
* NOT the caller. Retained = all protected + the newest terminal nodes up to
|
|
150
|
+
* `target`.
|
|
132
151
|
*
|
|
133
152
|
* Mirrors {@link pruneNodes} exactly for the delete (shared `deletePrunedNodes`:
|
|
134
153
|
* transactioned row sweep + FK edge cascade + best-effort dir removal). `dryRun`
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync, readFileSync, renameSync, readdirSync, rmSync, openSync, writeSync, fsyncSync, closeSync, statSync, } from 'node:fs';
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
9
|
import { openDb } from './db.js';
|
|
10
|
-
import { isPidAlive } from './pid.js';
|
|
10
|
+
import { isPidAlive, capturePidIdentities } from './pid.js';
|
|
11
11
|
import { cancelTriggersFor } from './triggers.js';
|
|
12
12
|
import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, } from './paths.js';
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -23,7 +23,7 @@ import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, }
|
|
|
23
23
|
const IDENTITY_KEYS = [
|
|
24
24
|
'node_id', 'name', 'description', 'cycles', 'created', 'cwd', 'host_kind', 'profile_id', 'kind', 'mode',
|
|
25
25
|
'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'passive_default',
|
|
26
|
-
'pi_session_id', 'pi_session_file', 'model_override', 'launch',
|
|
26
|
+
'pi_session_id', 'pi_session_file', 'cycle_pending', 'model_override', 'launch', 'managed_worktree',
|
|
27
27
|
];
|
|
28
28
|
/** Project any node object down to its durable-identity subset. */
|
|
29
29
|
function toIdentity(m) {
|
|
@@ -45,9 +45,9 @@ function readMeta(nodeId) {
|
|
|
45
45
|
// Tolerate an EMPTY or unparseable meta.json by treating it as MISSING (null)
|
|
46
46
|
// rather than throwing. A bare write-temp-then-rename without fsync can leave a
|
|
47
47
|
// 0-byte meta.json on an NFS-backed home after the holding process/sandbox dies
|
|
48
|
-
// (see writeMeta + the
|
|
49
|
-
// crash rebuildIndex/revive for the whole canvas. Recovery
|
|
50
|
-
// durable snapshot) is the caller's job.
|
|
48
|
+
// (see writeMeta + the guest cold-boot bug it guards against); a single corrupt
|
|
49
|
+
// node must not crash rebuildIndex/revive for the whole canvas. Recovery
|
|
50
|
+
// (re-seed from a durable snapshot) is the caller's job.
|
|
51
51
|
const raw = readFileSync(p, 'utf8');
|
|
52
52
|
if (raw.trim() === '')
|
|
53
53
|
return null;
|
|
@@ -62,12 +62,13 @@ function readMeta(nodeId) {
|
|
|
62
62
|
*
|
|
63
63
|
* Durable write: fsync the temp file before the rename, then fsync the parent
|
|
64
64
|
* directory so the rename itself is on disk. A bare writeFileSync+rename can
|
|
65
|
-
* leave a 0-byte meta.json on an NFS-backed home (
|
|
66
|
-
* `local_lock=none,noac`): when meta.json is held open, the rename
|
|
67
|
-
* the open inode and the new content is never flushed, so a sandbox
|
|
65
|
+
* leave a 0-byte meta.json on an NFS-backed guest home (a durable network Volume
|
|
66
|
+
* mounted `local_lock=none,noac`): when meta.json is held open, the rename
|
|
67
|
+
* silly-renames the open inode and the new content is never flushed, so a sandbox
|
|
68
68
|
* recreate-from-template loses it. meta.json is written rarely (birth, polymorph,
|
|
69
69
|
* session-id capture, naming) so the per-write fsync cost is negligible.
|
|
70
|
-
* Bug:
|
|
70
|
+
* Bug guarded against: a guest cold-boot 0-byte meta.json → rebuild-index/revive
|
|
71
|
+
* JSON.parse(''). */
|
|
71
72
|
function writeMeta(meta) {
|
|
72
73
|
const p = nodeMetaPath(meta.node_id);
|
|
73
74
|
const tmp = `${p}.tmp`;
|
|
@@ -143,6 +144,7 @@ function rowFrom(r) {
|
|
|
143
144
|
created: r['created'],
|
|
144
145
|
intent: r['intent'] ?? null,
|
|
145
146
|
pi_pid: r['pi_pid'] ?? null,
|
|
147
|
+
pi_pid_identity: r['pi_pid_identity'] ?? null,
|
|
146
148
|
window: r['window'] ?? null,
|
|
147
149
|
tmux_session: r['tmux_session'] ?? null,
|
|
148
150
|
pane: r['pane'] ?? null,
|
|
@@ -152,7 +154,7 @@ function rowFrom(r) {
|
|
|
152
154
|
* no row exists yet (the hydration then falls back to whatever the meta held). */
|
|
153
155
|
function runtimeFromRow(nodeId) {
|
|
154
156
|
const r = openDb()
|
|
155
|
-
.prepare('SELECT status, intent, pi_pid, "window", tmux_session, pane FROM nodes WHERE node_id = ?')
|
|
157
|
+
.prepare('SELECT status, intent, pi_pid, pi_pid_identity, "window", tmux_session, pane FROM nodes WHERE node_id = ?')
|
|
156
158
|
.get(nodeId);
|
|
157
159
|
if (r === undefined)
|
|
158
160
|
return null;
|
|
@@ -160,6 +162,7 @@ function runtimeFromRow(nodeId) {
|
|
|
160
162
|
status: r['status'] ?? 'active',
|
|
161
163
|
intent: r['intent'] ?? null,
|
|
162
164
|
pi_pid: r['pi_pid'] ?? null,
|
|
165
|
+
pi_pid_identity: r['pi_pid_identity'] ?? null,
|
|
163
166
|
window: r['window'] ?? null,
|
|
164
167
|
tmux_session: r['tmux_session'] ?? null,
|
|
165
168
|
pane: r['pane'] ?? null,
|
|
@@ -244,13 +247,30 @@ export function setPresence(nodeId, presence) {
|
|
|
244
247
|
.prepare('UPDATE nodes SET tmux_session = ?, "window" = ?, pane = ? WHERE node_id = ?')
|
|
245
248
|
.run(presence.tmux_session ?? null, presence.window ?? null, presence.pane ?? null, nodeId);
|
|
246
249
|
}
|
|
247
|
-
/** Record the live pi pid (daemon liveness signal)
|
|
250
|
+
/** Record the live pi pid (daemon liveness signal) PLUS its launch-time
|
|
251
|
+
* identity fingerprint, in one atomic write (crouter#98 review finding 1):
|
|
252
|
+
* `capturePidIdentities([pid])` is a signal-0-adjacent `ps -p <pid>` probe
|
|
253
|
+
* taken RIGHT NOW, while `pid` is the process we just spawned/bound and
|
|
254
|
+
* therefore definitely still exists — this is the LAUNCH-time baseline
|
|
255
|
+
* `headlessBrokerHost.teardown()` later compares against before signaling
|
|
256
|
+
* anything. A failed probe (rare — `ps` itself misbehaving) or a probe that
|
|
257
|
+
* finds no row for `pid` (raced away in the instant between spawn and this
|
|
258
|
+
* call) both write `NULL`: no baseline recorded, so the teardown guard fails
|
|
259
|
+
* open exactly like a node booted before this column existed — never a false
|
|
260
|
+
* identity that could itself cause a false mismatch later. */
|
|
248
261
|
export function recordPid(nodeId, pid) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
262
|
+
const identity = capturePidIdentities([pid])?.get(pid) ?? null;
|
|
263
|
+
openDb()
|
|
264
|
+
.prepare('UPDATE nodes SET pi_pid = ?, pi_pid_identity = ? WHERE node_id = ?')
|
|
265
|
+
.run(pid, identity, nodeId);
|
|
266
|
+
}
|
|
267
|
+
/** Clear the pi pid (window-backed relaunch, before the fresh pi re-records it)
|
|
268
|
+
* AND its recorded launch-time identity — the two are written together by
|
|
269
|
+
* `recordPid` and must be cleared together, or a subsequent relaunch that
|
|
270
|
+
* reuses the pid namespace could compare its own fresh pid against a STALE
|
|
271
|
+
* identity left over from the pid this row held before. */
|
|
252
272
|
export function clearPid(nodeId) {
|
|
253
|
-
openDb().prepare('UPDATE nodes SET pi_pid = NULL WHERE node_id = ?').run(nodeId);
|
|
273
|
+
openDb().prepare('UPDATE nodes SET pi_pid = NULL, pi_pid_identity = NULL WHERE node_id = ?').run(nodeId);
|
|
254
274
|
}
|
|
255
275
|
/** All rows, optionally filtered by status. */
|
|
256
276
|
export function listNodes(filter) {
|
|
@@ -522,14 +542,19 @@ function recencyMs(nodeId, created) {
|
|
|
522
542
|
* `max(maxNodes, liveCount)`. Bounds the otherwise-unbounded growth the TTL
|
|
523
543
|
* sweep can't (a canvas that never crosses the TTL still grows without limit).
|
|
524
544
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
545
|
+
* Protected (never a prune candidate, regardless of recency rank): any
|
|
546
|
+
* non-terminal node — status active|idle — REGARDLESS of `pi_pid` liveness.
|
|
547
|
+
* A crashed-but-non-terminal row (dead pid, status still active/idle) must
|
|
548
|
+
* survive for daemon crash-recovery to resume, not be hard-deleted by a
|
|
549
|
+
* count-based sweep — only a genuinely terminal/dead-lifecycle row is
|
|
550
|
+
* eligible for count-based pruning. The caller ($CRTR_NODE_ID) is also always
|
|
551
|
+
* protected.
|
|
528
552
|
*
|
|
529
553
|
* Algorithm: order ALL nodes by recency DESC (telemetry.json mtime, falling back
|
|
530
|
-
* to `created`); `target = max(maxNodes,
|
|
531
|
-
* beyond `target` (0-indexed rank >= target) that is NOT
|
|
532
|
-
* Retained = all
|
|
554
|
+
* to `created`); `target = max(maxNodes, protectedCount)`; delete every node
|
|
555
|
+
* ranked beyond `target` (0-indexed rank >= target) that is NOT protected and
|
|
556
|
+
* NOT the caller. Retained = all protected + the newest terminal nodes up to
|
|
557
|
+
* `target`.
|
|
533
558
|
*
|
|
534
559
|
* Mirrors {@link pruneNodes} exactly for the delete (shared `deletePrunedNodes`:
|
|
535
560
|
* transactioned row sweep + FK edge cascade + best-effort dir removal). `dryRun`
|
|
@@ -538,20 +563,23 @@ export function pruneToLimit(opts) {
|
|
|
538
563
|
const dryRun = opts.dryRun ?? false;
|
|
539
564
|
const selfId = process.env['CRTR_NODE_ID'] ?? '';
|
|
540
565
|
const rows = listNodes();
|
|
541
|
-
// Rank ALL nodes by recency DESC, tagging
|
|
566
|
+
// Rank ALL nodes by recency DESC, tagging protection as we go. Protection is
|
|
567
|
+
// status-only (active|idle) — NOT gated on pi_pid liveness — so a node whose
|
|
568
|
+
// broker crashed but whose row is still non-terminal stays protected for
|
|
569
|
+
// crash-recovery instead of being hard-deleted here.
|
|
542
570
|
const ranked = rows
|
|
543
571
|
.map((r) => ({
|
|
544
572
|
node_id: r.node_id,
|
|
545
573
|
status: r.status,
|
|
546
574
|
created: r.created,
|
|
547
575
|
recency: recencyMs(r.node_id, r.created),
|
|
548
|
-
|
|
576
|
+
protected: r.status === 'active' || r.status === 'idle',
|
|
549
577
|
}))
|
|
550
578
|
.sort((a, b) => b.recency - a.recency);
|
|
551
|
-
const
|
|
552
|
-
const target = Math.max(opts.maxNodes,
|
|
579
|
+
const protectedCount = ranked.filter((r) => r.protected).length;
|
|
580
|
+
const target = Math.max(opts.maxNodes, protectedCount);
|
|
553
581
|
const candidates = ranked
|
|
554
|
-
.filter((r, rank) => rank >= target && !r.
|
|
582
|
+
.filter((r, rank) => rank >= target && !r.protected && r.node_id !== selfId)
|
|
555
583
|
.map((r) => ({ node_id: r.node_id, status: r.status, created: r.created }));
|
|
556
584
|
if (dryRun || candidates.length === 0)
|
|
557
585
|
return { pruned: candidates, dryRun };
|
package/dist/core/canvas/db.js
CHANGED
|
@@ -359,6 +359,19 @@ function addProfileIdColumn(db) {
|
|
|
359
359
|
const columns = nodeColumns(db);
|
|
360
360
|
addNodeColumnIfMissing(db, columns, 'profile_id', `ALTER TABLE nodes ADD COLUMN profile_id TEXT;`);
|
|
361
361
|
}
|
|
362
|
+
/** v12 — additive runtime column `pi_pid_identity`: the LAUNCH-time process-
|
|
363
|
+
* identity fingerprint (`pid.ts`'s `composeIdentity`) recorded alongside
|
|
364
|
+
* `pi_pid` (v2) by `recordPid`, so `headlessBrokerHost.teardown()` can refuse
|
|
365
|
+
* to signal a `pi_pid` the OS has since recycled for an unrelated process
|
|
366
|
+
* (crouter#98 final review, Fix 1 — see `NodeRuntime.pi_pid_identity`).
|
|
367
|
+
* Defaults NULL ⇒ no baseline ⇒ the guard fails open, exactly like a node
|
|
368
|
+
* booted before this column existed. No data backfill: an existing `pi_pid`
|
|
369
|
+
* with no recorded identity simply has no launch-time baseline to compare
|
|
370
|
+
* against. Additive, forward-only. */
|
|
371
|
+
function addPidIdentityColumn(db) {
|
|
372
|
+
const columns = nodeColumns(db);
|
|
373
|
+
addNodeColumnIfMissing(db, columns, 'pi_pid_identity', `ALTER TABLE nodes ADD COLUMN pi_pid_identity TEXT;`);
|
|
374
|
+
}
|
|
362
375
|
/** The ordered migration list. Index `i` is migration version `i + 1`; the db's
|
|
363
376
|
* `user_version` tracks how many have been applied. Append only. */
|
|
364
377
|
export const MIGRATIONS = [
|
|
@@ -373,6 +386,7 @@ export const MIGRATIONS = [
|
|
|
373
386
|
/* v9 */ migrateWakeupsToTriggers,
|
|
374
387
|
/* v10 */ addCanvasMetaTable,
|
|
375
388
|
/* v11 */ addProfileIdColumn,
|
|
389
|
+
/* v12 */ addPidIdentityColumn,
|
|
376
390
|
];
|
|
377
391
|
/** Bring `db` up to the latest schema version. Reads `user_version`, runs each
|
|
378
392
|
* pending migration in order, and bumps `user_version` after each so the work
|
|
@@ -409,6 +409,7 @@ export function nodeArtifacts(nodeId, types) {
|
|
|
409
409
|
created: node.created,
|
|
410
410
|
intent: node.intent ?? null,
|
|
411
411
|
pi_pid: node.pi_pid ?? null,
|
|
412
|
+
pi_pid_identity: node.pi_pid_identity ?? null,
|
|
412
413
|
window: node.window ?? null,
|
|
413
414
|
tmux_session: node.tmux_session ?? null,
|
|
414
415
|
pane: node.pane ?? null,
|
|
@@ -6,19 +6,20 @@ export declare function crtrHome(): string;
|
|
|
6
6
|
/** Absolute path to the SQLite canvas db file. Defaults to `canvas.db` under
|
|
7
7
|
* the canvas home, but `CRTR_CANVAS_DB` overrides ONLY this file's location
|
|
8
8
|
* (its `-wal`/`-shm` siblings follow it) while leaving `nodesRoot()` and all
|
|
9
|
-
* other node data under `crtrHome()`. This split exists for
|
|
10
|
-
* node data (meta.json/sessions, plain-file writes) stays on
|
|
11
|
-
* Volume via `CRTR_HOME`, but canvas.db's SQLite fcntl/WAL
|
|
12
|
-
* NFS (`local_lock=none`) — so it must live on local disk.
|
|
13
|
-
* created on use so an unset-default and an override both just
|
|
9
|
+
* other node data under `crtrHome()`. This split exists for a remote/guest
|
|
10
|
+
* deployment: node data (meta.json/sessions, plain-file writes) stays on a
|
|
11
|
+
* durable NFS-backed Volume via `CRTR_HOME`, but canvas.db's SQLite fcntl/WAL
|
|
12
|
+
* locks would hang on NFS (`local_lock=none`) — so it must live on local disk.
|
|
13
|
+
* Parent dir is created on use so an unset-default and an override both just
|
|
14
|
+
* work. */
|
|
14
15
|
export declare function canvasDbPath(): string;
|
|
15
16
|
/** Absolute path to a node's broker `view.sock`. Defaults to `view.sock` under
|
|
16
17
|
* the node dir, but `CRTR_SOCK_DIR` overrides the PARENT dir (the socket becomes
|
|
17
18
|
* `<CRTR_SOCK_DIR>/<nodeId>.sock`) while node data stays under `crtrHome()`.
|
|
18
|
-
* Sibling of `CRTR_CANVAS_DB`, and exists for the same
|
|
19
|
-
* AF_UNIX socket cannot be bound on the durable NFS Volume
|
|
20
|
-
* even where plain file writes into the same dir succeed),
|
|
21
|
-
* must live on local disk. Unset (the default) is unchanged for normal
|
|
19
|
+
* Sibling of `CRTR_CANVAS_DB`, and exists for the same remote/guest-deployment
|
|
20
|
+
* reason: an AF_UNIX socket cannot be bound on the durable NFS-backed Volume
|
|
21
|
+
* (`bind()` → EACCES, even where plain file writes into the same dir succeed),
|
|
22
|
+
* so the broker socket must live on local disk. Unset (the default) is unchanged for normal
|
|
22
23
|
* local-disk hosts. Parent dir is created on use so both paths just work. */
|
|
23
24
|
export declare function viewSocketPath(nodeId: string): string;
|
|
24
25
|
export declare function nodesRoot(): string;
|
|
@@ -30,11 +30,12 @@ export function crtrHome() {
|
|
|
30
30
|
/** Absolute path to the SQLite canvas db file. Defaults to `canvas.db` under
|
|
31
31
|
* the canvas home, but `CRTR_CANVAS_DB` overrides ONLY this file's location
|
|
32
32
|
* (its `-wal`/`-shm` siblings follow it) while leaving `nodesRoot()` and all
|
|
33
|
-
* other node data under `crtrHome()`. This split exists for
|
|
34
|
-
* node data (meta.json/sessions, plain-file writes) stays on
|
|
35
|
-
* Volume via `CRTR_HOME`, but canvas.db's SQLite fcntl/WAL
|
|
36
|
-
* NFS (`local_lock=none`) — so it must live on local disk.
|
|
37
|
-
* created on use so an unset-default and an override both just
|
|
33
|
+
* other node data under `crtrHome()`. This split exists for a remote/guest
|
|
34
|
+
* deployment: node data (meta.json/sessions, plain-file writes) stays on a
|
|
35
|
+
* durable NFS-backed Volume via `CRTR_HOME`, but canvas.db's SQLite fcntl/WAL
|
|
36
|
+
* locks would hang on NFS (`local_lock=none`) — so it must live on local disk.
|
|
37
|
+
* Parent dir is created on use so an unset-default and an override both just
|
|
38
|
+
* work. */
|
|
38
39
|
export function canvasDbPath() {
|
|
39
40
|
const override = process.env['CRTR_CANVAS_DB'];
|
|
40
41
|
if (override !== undefined && override !== '') {
|
|
@@ -46,10 +47,10 @@ export function canvasDbPath() {
|
|
|
46
47
|
/** Absolute path to a node's broker `view.sock`. Defaults to `view.sock` under
|
|
47
48
|
* the node dir, but `CRTR_SOCK_DIR` overrides the PARENT dir (the socket becomes
|
|
48
49
|
* `<CRTR_SOCK_DIR>/<nodeId>.sock`) while node data stays under `crtrHome()`.
|
|
49
|
-
* Sibling of `CRTR_CANVAS_DB`, and exists for the same
|
|
50
|
-
* AF_UNIX socket cannot be bound on the durable NFS Volume
|
|
51
|
-
* even where plain file writes into the same dir succeed),
|
|
52
|
-
* must live on local disk. Unset (the default) is unchanged for normal
|
|
50
|
+
* Sibling of `CRTR_CANVAS_DB`, and exists for the same remote/guest-deployment
|
|
51
|
+
* reason: an AF_UNIX socket cannot be bound on the durable NFS-backed Volume
|
|
52
|
+
* (`bind()` → EACCES, even where plain file writes into the same dir succeed),
|
|
53
|
+
* so the broker socket must live on local disk. Unset (the default) is unchanged for normal
|
|
53
54
|
* local-disk hosts. Parent dir is created on use so both paths just work. */
|
|
54
55
|
export function viewSocketPath(nodeId) {
|
|
55
56
|
const override = process.env['CRTR_SOCK_DIR'];
|