@crouton-kit/crouter 0.3.82 → 0.3.84

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.
Files changed (94) hide show
  1. package/dist/builtin-pi-packages/pi-mode-switch/README.md +3 -3
  2. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +12 -4
  3. package/dist/clients/attach/__tests__/attach-keybindings.test.js +21 -4
  4. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +104 -0
  5. package/dist/clients/attach/__tests__/crtr-output-render.test.js +17 -44
  6. package/dist/clients/attach/attach-cmd.d.ts +2 -3
  7. package/dist/clients/attach/attach-cmd.js +689 -689
  8. package/dist/clients/attach/attach-def.d.ts +2 -0
  9. package/dist/clients/attach/attach-def.js +93 -0
  10. package/dist/clients/attach/canvas-panels.d.ts +10 -3
  11. package/dist/clients/attach/canvas-panels.js +42 -16
  12. package/dist/clients/attach/chat-view.d.ts +20 -1
  13. package/dist/clients/attach/chat-view.js +85 -2
  14. package/dist/commands/__tests__/human.test.js +7 -22
  15. package/dist/commands/attention.js +31 -30
  16. package/dist/commands/human/prompts.js +57 -38
  17. package/dist/commands/human/queue.d.ts +1 -28
  18. package/dist/commands/human/queue.js +156 -520
  19. package/dist/commands/human/shared.d.ts +28 -68
  20. package/dist/commands/human/shared.js +37 -120
  21. package/dist/commands/human.js +15 -17
  22. package/dist/commands/node.js +5 -2
  23. package/dist/commands/surface.js +1 -1
  24. package/dist/commands/sys/__tests__/setup-core.test.js +1 -1
  25. package/dist/commands/sys/__tests__/setup-front-door.test.js +85 -0
  26. package/dist/commands/sys/setup-core.d.ts +10 -0
  27. package/dist/commands/sys/setup-core.js +39 -1
  28. package/dist/commands/sys/setup.js +10 -1
  29. package/dist/core/__tests__/boot.test.js +141 -6
  30. package/dist/core/__tests__/human-deliver-e2e.test.js +95 -0
  31. package/dist/core/__tests__/human-deliver.test.js +332 -0
  32. package/dist/core/__tests__/human-node-not-supervised.test.js +5 -4
  33. package/dist/core/__tests__/pid-identity-match.test.js +145 -0
  34. package/dist/core/__tests__/tmux-surface.test.js +180 -2
  35. package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
  36. package/dist/core/canvas/__tests__/attention.test.js +126 -0
  37. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +4 -4
  38. package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
  39. package/dist/core/canvas/attention.d.ts +16 -14
  40. package/dist/core/canvas/attention.js +34 -29
  41. package/dist/core/canvas/boot.d.ts +45 -0
  42. package/dist/core/canvas/boot.js +36 -3
  43. package/dist/core/canvas/browse/app.js +3 -3
  44. package/dist/core/canvas/canvas.d.ts +40 -0
  45. package/dist/core/canvas/canvas.js +65 -1
  46. package/dist/core/canvas/pid.d.ts +86 -17
  47. package/dist/core/canvas/pid.js +152 -37
  48. package/dist/core/canvas/remote-canvas-source.d.ts +1 -1
  49. package/dist/core/canvas/remote-canvas-source.js +4 -2
  50. package/dist/core/canvas/render.d.ts +5 -5
  51. package/dist/core/canvas/render.js +14 -14
  52. package/dist/core/canvas/source.d.ts +2 -2
  53. package/dist/core/canvas/source.js +3 -3
  54. package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +1 -0
  55. package/dist/core/keybindings/__tests__/inbox-affordance.test.js +38 -0
  56. package/dist/core/keybindings/__tests__/resolve.test.js +2 -1
  57. package/dist/core/keybindings/catalog.d.ts +2 -2
  58. package/dist/core/keybindings/catalog.js +4 -2
  59. package/dist/core/keybindings/inbox.d.ts +21 -0
  60. package/dist/core/keybindings/inbox.js +35 -0
  61. package/dist/core/keybindings/index.d.ts +1 -0
  62. package/dist/core/keybindings/index.js +1 -0
  63. package/dist/core/preview-registry.js +23 -29
  64. package/dist/core/profiles/select.js +4 -4
  65. package/dist/core/runtime/pi-vendored.js +15 -1
  66. package/dist/core/runtime/placement.d.ts +1 -10
  67. package/dist/core/runtime/placement.js +43 -26
  68. package/dist/core/runtime/session-list-cache.d.ts +22 -2
  69. package/dist/core/runtime/session-list-cache.js +92 -24
  70. package/dist/core/runtime/spawn.js +2 -1
  71. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  72. package/dist/core/runtime/tmux-chrome.js +1 -1
  73. package/dist/core/runtime/tmux.d.ts +1 -0
  74. package/dist/core/runtime/tmux.js +55 -1
  75. package/dist/core/spawn.d.ts +0 -59
  76. package/dist/core/spawn.js +5 -163
  77. package/dist/daemon/crtrd.js +36 -11
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js +1 -1
  80. package/dist/web-client/assets/index-DZdKS-FB.js +78 -0
  81. package/dist/web-client/index.html +1 -1
  82. package/dist/web-client/sw.js +1 -1
  83. package/package.json +2 -2
  84. package/dist/core/__tests__/full/dead-pane-regression.test.js +0 -154
  85. package/dist/core/__tests__/full/human-new-window-regression.test.js +0 -104
  86. package/dist/core/__tests__/full/review-render-pane-regression.test.js +0 -133
  87. package/dist/core/__tests__/human-stranded-deliver.test.js +0 -145
  88. package/dist/core/__tests__/human-surface-target.test.js +0 -98
  89. package/dist/web-client/assets/index--md2ylfi.js +0 -78
  90. /package/dist/{core/__tests__/full/dead-pane-regression.test.d.ts → clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts} +0 -0
  91. /package/dist/{core/__tests__/full/human-new-window-regression.test.d.ts → commands/sys/__tests__/setup-front-door.test.d.ts} +0 -0
  92. /package/dist/core/__tests__/{full/review-render-pane-regression.test.d.ts → human-deliver-e2e.test.d.ts} +0 -0
  93. /package/dist/core/__tests__/{human-stranded-deliver.test.d.ts → human-deliver.test.d.ts} +0 -0
  94. /package/dist/core/__tests__/{human-surface-target.test.d.ts → pid-identity-match.test.d.ts} +0 -0
@@ -14,62 +14,3 @@ export declare function isInTmux(): boolean;
14
14
  * degrade to the inbox-drain follow-up. */
15
15
  export declare function tmuxServerReachable(): boolean;
16
16
  export declare function shellQuote(s: string): string;
17
- /** Count panes in a tmux window (0 outside tmux / on error). With `targetPane`,
18
- * counts the window THAT pane lives in (the placement decision must reflect the
19
- * window the new pane will actually open into, not the caller's backstage one);
20
- * without it, the caller's current window. */
21
- export declare function countPanesInWindow(targetPane?: string): number;
22
- /** Back-compat alias: panes in the caller's current window. */
23
- export declare function countPanesInCurrentWindow(): number;
24
- /** Does this tmux pane id still exist? `display-message` EXITS 0 with EMPTY
25
- * output on an unresolvable pane, so test for non-empty stdout, not just `.ok`.
26
- * False outside tmux / on error. */
27
- export declare function paneAlive(pane: string): boolean;
28
- /** Resolve a tmux pane id to its `session:window_index` — the target form
29
- * `new-window -t` accepts. tmux REJECTS a pane id for new-window ("can't
30
- * specify pane here"); only split-window -t takes a pane. null outside tmux /
31
- * on a bad pane id / on error / empty. */
32
- export declare function paneWindowTarget(pane: string): string | null;
33
- /** The active pane of the user's attached tmux client — where they are looking
34
- * right now. `list-clients` first attached client, then its current pane. Used
35
- * to surface a human prompt in the user's view when nothing in the asking
36
- * node's graph is focused. null outside tmux / no client / on error. */
37
- export declare function attachedClientPane(): string | null;
38
- /**
39
- * Schedule a kill-pane on the *current* tmux pane after `delaySeconds`, detached
40
- * so the caller can return normally before the pane dies. No-op outside tmux,
41
- * when TMUX_PANE is unset, or when delaySeconds <= 0.
42
- */
43
- export declare function scheduleKillCurrentPane(delaySeconds: number): boolean;
44
- export interface DetachOptions {
45
- /** Inner command to run in the new pane. */
46
- command: string;
47
- cwd: string;
48
- /** Optional id injected as the CRTR_JOB_ID env var in the pane. */
49
- jobId?: string;
50
- /** Where to open the new pane. */
51
- placement: 'split-h' | 'split-v' | 'new-window';
52
- /** Seconds before killing the originating pane so the caller can finish. */
53
- killAfterSeconds: number;
54
- /** Pin the new pane to this tmux pane: split-window splits it; new-window is
55
- * inserted immediately after its window (-a -t <pane>). Without this, tmux
56
- * uses the attached client's currently-focused pane — which drifts if the
57
- * user switches windows between kickoff and spawn. */
58
- targetPane?: string;
59
- /** Pass tmux `-d` to new-window so CREATING the window never switches the
60
- * attached client to it (split-window already leaves the client's view put).
61
- * The prompt lands in the target session/window without jumping the user out
62
- * of what they are looking at. No effect on split-h/split-v. */
63
- detached?: boolean;
64
- }
65
- export interface DetachResult {
66
- status: 'spawned' | 'spawn-failed' | 'not-in-tmux';
67
- paneId?: string;
68
- message: string;
69
- }
70
- /**
71
- * Fire-and-forget: launch `opts.command` in a new pane (or window), then
72
- * schedule the originating pane to be killed after `killAfterSeconds`. Returns
73
- * as soon as the new pane is up; does NOT wait for the command to finish.
74
- */
75
- export declare function spawnAndDetach(opts: DetachOptions): DetachResult;
@@ -1,11 +1,9 @@
1
- // Tmux pane detach helpers.
1
+ // Tmux reachability + quoting helpers for the `human` command tree.
2
2
  //
3
- // A small set of tmux primitives used by the `human` command tree to put the
4
- // humanloop TUI in a detached pane: spawnAndDetach (open a pane running a given
5
- // command), countPanesInCurrentWindow (placement decision), plus shellQuote and
6
- // tmuxServerReachable. The canvas runtime has its own one-window-per-node
7
- // machinery in core/runtime/tmux.ts; this module is only the pane-split path the
8
- // human TUI needs.
3
+ // The human surface gates on whether the canvas tmux server is reachable
4
+ // (tmuxServerReachable) so it can decide whether to surface a prompt or degrade
5
+ // to the inbox-drain follow-up. The canvas runtime has its own
6
+ // one-window-per-node machinery in core/runtime/tmux.ts.
9
7
  import { spawnSync } from 'node:child_process';
10
8
  /** Does THIS process sit inside a tmux client (its own $TMUX is set)? NOT the
11
9
  * gate for the human surface — see tmuxServerReachable. A headless-broker child
@@ -30,159 +28,3 @@ export function tmuxServerReachable() {
30
28
  export function shellQuote(s) {
31
29
  return `'${s.replace(/'/g, "'\\''")}'`;
32
30
  }
33
- /** Count panes in a tmux window (0 outside tmux / on error). With `targetPane`,
34
- * counts the window THAT pane lives in (the placement decision must reflect the
35
- * window the new pane will actually open into, not the caller's backstage one);
36
- * without it, the caller's current window. */
37
- export function countPanesInWindow(targetPane) {
38
- const args = targetPane !== undefined && targetPane !== ''
39
- ? ['list-panes', '-t', targetPane, '-F', '#{pane_id}']
40
- : ['list-panes', '-F', '#{pane_id}'];
41
- const result = spawnSync('tmux', args, { encoding: 'utf8' });
42
- if (result.status !== 0)
43
- return 0;
44
- return result.stdout.split('\n').filter((line) => line.trim() !== '').length;
45
- }
46
- /** Back-compat alias: panes in the caller's current window. */
47
- export function countPanesInCurrentWindow() {
48
- return countPanesInWindow();
49
- }
50
- /** Does this tmux pane id still exist? `display-message` EXITS 0 with EMPTY
51
- * output on an unresolvable pane, so test for non-empty stdout, not just `.ok`.
52
- * False outside tmux / on error. */
53
- export function paneAlive(pane) {
54
- if (!/^%\d+$/.test(pane))
55
- return false;
56
- const r = spawnSync('tmux', ['display-message', '-p', '-t', pane, '#{pane_id}'], {
57
- encoding: 'utf8',
58
- });
59
- return r.status === 0 && r.stdout.trim() !== '';
60
- }
61
- /** Resolve a tmux pane id to its `session:window_index` — the target form
62
- * `new-window -t` accepts. tmux REJECTS a pane id for new-window ("can't
63
- * specify pane here"); only split-window -t takes a pane. null outside tmux /
64
- * on a bad pane id / on error / empty. */
65
- export function paneWindowTarget(pane) {
66
- if (!/^%\d+$/.test(pane))
67
- return null;
68
- const r = spawnSync('tmux', ['display-message', '-p', '-t', pane, '#{session_name}:#{window_index}'], { encoding: 'utf8' });
69
- if (r.status !== 0)
70
- return null;
71
- const t = r.stdout.trim();
72
- return t !== '' ? t : null;
73
- }
74
- /** The active pane of the user's attached tmux client — where they are looking
75
- * right now. `list-clients` first attached client, then its current pane. Used
76
- * to surface a human prompt in the user's view when nothing in the asking
77
- * node's graph is focused. null outside tmux / no client / on error. */
78
- export function attachedClientPane() {
79
- const clients = spawnSync('tmux', ['list-clients', '-F', '#{client_name}'], {
80
- encoding: 'utf8',
81
- });
82
- if (clients.status !== 0)
83
- return null;
84
- const name = clients.stdout
85
- .split('\n')
86
- .map((l) => l.trim())
87
- .find((l) => l !== '');
88
- if (name === undefined)
89
- return null;
90
- const pane = spawnSync('tmux', ['display-message', '-p', '-c', name, '#{pane_id}'], {
91
- encoding: 'utf8',
92
- });
93
- if (pane.status !== 0)
94
- return null;
95
- const id = pane.stdout.trim();
96
- return id !== '' ? id : null;
97
- }
98
- /**
99
- * Schedule a kill-pane on the *current* tmux pane after `delaySeconds`, detached
100
- * so the caller can return normally before the pane dies. No-op outside tmux,
101
- * when TMUX_PANE is unset, or when delaySeconds <= 0.
102
- */
103
- export function scheduleKillCurrentPane(delaySeconds) {
104
- const currentPane = process.env.TMUX_PANE;
105
- if (currentPane === undefined || currentPane === '' || delaySeconds <= 0) {
106
- return false;
107
- }
108
- const killCmd = `sleep ${delaySeconds}; tmux kill-pane -t ${currentPane}`;
109
- spawnSync('sh', ['-c', `nohup sh -c ${shellQuote(killCmd)} </dev/null >/dev/null 2>&1 &`], {
110
- stdio: 'ignore',
111
- });
112
- return true;
113
- }
114
- /**
115
- * Fire-and-forget: launch `opts.command` in a new pane (or window), then
116
- * schedule the originating pane to be killed after `killAfterSeconds`. Returns
117
- * as soon as the new pane is up; does NOT wait for the command to finish.
118
- */
119
- export function spawnAndDetach(opts) {
120
- // Gate on the CANVAS tmux server's reachability, NOT the caller's $TMUX. A
121
- // headless-broker child strips its own $TMUX (broker.ts) but the canvas server
122
- // is on the default socket, so `split-window -t <pane>` against a resolved
123
- // target pane (resolveHumanTarget) works fine from it. Only a genuine
124
- // no-server case degrades to the inbox-drain follow-up.
125
- if (!tmuxServerReachable()) {
126
- return {
127
- status: 'not-in-tmux',
128
- message: 'handoff requires a reachable tmux server',
129
- };
130
- }
131
- const splitArgs = [];
132
- if (opts.placement === 'new-window') {
133
- splitArgs.push('new-window');
134
- if (opts.detached === true)
135
- splitArgs.push('-d'); // don't switch the client to it
136
- if (opts.targetPane !== undefined && opts.targetPane !== '') {
137
- // new-window -t REJECTS a pane id (tmux exits 1: "can't specify pane
138
- // here") — only split-window -t accepts a pane. Resolve the target pane to
139
- // its session:window first; -a then inserts the new window right after it.
140
- // If the pane can't be resolved, fall back to no -t (tmux uses current).
141
- const winTarget = paneWindowTarget(opts.targetPane);
142
- if (winTarget !== null)
143
- splitArgs.push('-a', '-t', winTarget);
144
- }
145
- }
146
- else {
147
- splitArgs.push('split-window');
148
- splitArgs.push(opts.placement === 'split-h' ? '-h' : '-v');
149
- if (opts.targetPane !== undefined && opts.targetPane !== '') {
150
- splitArgs.push('-t', opts.targetPane);
151
- }
152
- }
153
- splitArgs.push('-P', '-F', '#{pane_id}');
154
- splitArgs.push('-c', opts.cwd);
155
- if (opts.jobId !== undefined) {
156
- splitArgs.push('-e', `CRTR_JOB_ID=${opts.jobId}`);
157
- }
158
- splitArgs.push(opts.command);
159
- const split = spawnSync('tmux', splitArgs, { encoding: 'utf8' });
160
- if (split.status !== 0) {
161
- const stderrText = split.stderr.trim();
162
- const msg = stderrText === '' ? 'tmux split-window/new-window failed' : stderrText;
163
- return { status: 'spawn-failed', message: msg };
164
- }
165
- const paneId = split.stdout.trim();
166
- // Force `remain-on-exit off` at PANE scope on the new pane. remain-on-exit is
167
- // a pane option (tmux 3.x) inherited from the window-scoped value, and the
168
- // canvas runtime arms `remain-on-exit on` on a node's vehicle/focus WINDOW
169
- // (F3 freeze, see runtime/tmux.ts setRemainOnExit). A split-window pane opened
170
- // into that window inherits the `on`, so the humanloop TUI pane would linger
171
- // as a dead pane ("pane is dead (status 0, …)") when `crtr human _run` exits 0
172
- // instead of closing. Overriding at pane scope destroys this pane on clean
173
- // exit WITHOUT touching the window's value (focus freeze still works) or the
174
- // user's global config. Best-effort: harmless no-op on tmux where the option
175
- // is window-only.
176
- if (paneId !== '') {
177
- spawnSync('tmux', ['set-option', '-p', '-t', paneId, 'remain-on-exit', 'off'], {
178
- stdio: 'ignore',
179
- });
180
- }
181
- // Schedule self-kill of the originating pane.
182
- scheduleKillCurrentPane(opts.killAfterSeconds);
183
- return {
184
- status: 'spawned',
185
- paneId,
186
- message: `handed off to pane ${paneId}`,
187
- };
188
- }
@@ -56,7 +56,7 @@ import { dirname, join } from 'node:path';
56
56
  import { homedir } from 'node:os';
57
57
  import { spawnSync } from 'node:child_process';
58
58
  import { crtrHome, nodeDir, jobDir } from '../core/canvas/paths.js';
59
- import { listNodes, getRow, getNode, dueClockTriggers, consumeTrigger, advanceTrigger, listFocuses, closeFocusRow, pruneToLimit, subscribersOf, } from '../core/canvas/index.js';
59
+ import { listNodes, getRow, getNode, migrateLegacyPidIdentities, dueClockTriggers, consumeTrigger, advanceTrigger, listFocuses, closeFocusRow, pruneToLimit, subscribersOf, } from '../core/canvas/index.js';
60
60
  import { fullName } from '../core/canvas/labels.js';
61
61
  import { transition } from '../core/runtime/lifecycle.js';
62
62
  import { isBusy, busySince } from '../core/runtime/busy.js';
@@ -65,7 +65,7 @@ import { fanDoctrineWake } from '../core/runtime/close.js';
65
65
  import { logger } from '../core/log.js';
66
66
  export { FAULT_QUIET_MS };
67
67
  import { isPidAlive, isRecordedPidAlive, recordedPidLiveness } from '../core/canvas/pid.js';
68
- import { reconcileBootLiveness } from '../core/canvas/boot.js';
68
+ import { reconcileAndMigrateBoot } from '../core/canvas/boot.js';
69
69
  import { listLivePanes, tearDownNode } from '../core/runtime/placement.js';
70
70
  import { reviveNode } from '../core/runtime/revive.js';
71
71
  import { isBrokerLive } from '../core/runtime/model-swap.js';
@@ -1205,8 +1205,8 @@ function reapDeadResidue(now) {
1205
1205
  * row when its pane dies outside the sanctioned teardown paths (a user
1206
1206
  * kill-pane, a tmux server restart, a crashed swap), so dead-pane rows
1207
1207
  * accumulate forever — the audit found 15 of 21 rows pointing at gone panes,
1208
- * and a stale row both lies to graphSurfaceTarget and blocks its node from
1209
- * being adopted into a fresh viewport (UNIQUE node_id). Sweep them here: one
1208
+ * and a stale row both misreports the node's on-screen viewer and blocks its
1209
+ * node from being adopted into a fresh viewport (UNIQUE node_id). Sweep them here: one
1210
1210
  * batched `list-panes -a` probe (never a per-row display-message), deleting
1211
1211
  * rows whose recorded pane no longer exists. A pane-less row (a bridge/unplaced
1212
1212
  * viewport) has nothing to verify and is left alone. On a FAILED probe (tmux
@@ -1294,10 +1294,11 @@ export async function superviseTick(now = Date.now()) {
1294
1294
  return;
1295
1295
  }
1296
1296
  // kind:'human' rows are NOT broker-hosted agent nodes — they are bookkeeping
1297
- // rows for the `crtr human` bridge. The decision/review TUI runs in a detached
1298
- // `crtr human _run` pane (NOT a pi engine), and the row's lifecycle is driven
1299
- // entirely by that worker's `pushFinal` (or `human cancel`). They never record
1300
- // a `pi_pid` or `pi_session_id`, so the universal broker-liveness path would
1297
+ // rows for the `crtr human` bridge. Their lifecycle is driven entirely by
1298
+ // humanloop's registered `_deliver` handler which pushFinals the answer when
1299
+ // the human responds from the humanloop inbox — or by `human cancel`. They
1300
+ // never record a `pi_pid` or `pi_session_id`, so the universal broker-liveness
1301
+ // path would
1301
1302
  // (after the boot grace) read every one as a never-booted broker and crash it
1302
1303
  // with surfaceBootFailure — exactly the regression where a headless
1303
1304
  // orchestrator's `human ask` nodes all died at boot. The daemon must never
@@ -1613,9 +1614,33 @@ export function runDaemon(opts = {}) {
1613
1614
  // "leave it, nothing to revive" and never calls reviveNode at all — a
1614
1615
  // pid-reuse collision on the first post-boot tick would silently no-op
1615
1616
  // forever with no revive attempt to even hit reviveNode's own reconcile call.
1616
- const { changed, cleared } = reconcileBootLiveness();
1617
- if (changed) {
1618
- process.stderr.write(`[crtrd] boot change detected cleared ${cleared.length} stale pi_pid(s) recorded before this boot\n`);
1617
+ //
1618
+ // Ordering + gate live in reconcileAndMigrateBoot (canvas/boot.ts) so the
1619
+ // migration-gate regression tests exercise the real composition rather than a
1620
+ // test-side copy of these lines.
1621
+ const { reconcile, migrated } = reconcileAndMigrateBoot(migrateLegacyPidIdentities);
1622
+ if (reconcile.changed) {
1623
+ process.stderr.write(`[crtrd] boot change detected — cleared ${reconcile.cleared.length} stale pi_pid(s) recorded before this boot\n`);
1624
+ }
1625
+ // Then, once, complete the boot-scoped-identity format migration for any
1626
+ // broker that survived an in-place crouter upgrade and still carries a legacy
1627
+ // `lstart#ticks` baseline: re-record it as `<bootId>#<ticks>` if its pid still
1628
+ // names the same live process. `identitiesMatch` only TOLERATES legacy rows;
1629
+ // this is what actually closes the cross-boot ticks-collision exposure for
1630
+ // them, so the legacy compare lane then only engages for genuinely no-boot_id
1631
+ // hosts.
1632
+ //
1633
+ // GATED on PROVEN same-boot provenance: the sweep blesses a legacy row's pid
1634
+ // as a current-boot process (`<currentBootId>#<ticks>`) on a ticks match, so
1635
+ // it is correct ONLY when the recorded pid provably belongs to THIS boot. We
1636
+ // run it solely when reconciliation matched an exact kernel `boot_id` on both
1637
+ // sides (`sameBootProven`) — never on a first-ever/absent/malformed marker or
1638
+ // the wall-clock epoch fallback, where an equal-ticks collision with an
1639
+ // unrelated cross-boot process could otherwise baptize a stranger. (On a
1640
+ // detected boot change the prior-boot pids were just cleared, so there is
1641
+ // nothing left to migrate either.)
1642
+ if (migrated.length > 0) {
1643
+ process.stderr.write(`[crtrd] migrated ${migrated.length} legacy pi_pid identity baseline(s) to boot-scoped format\n`);
1619
1644
  }
1620
1645
  writePidfile();
1621
1646
  log.info(`started (pid ${process.pid}, interval ${interval}ms)`);
package/dist/index.d.ts CHANGED
@@ -10,8 +10,8 @@ export { appendSituationalContext } from './core/runtime/situational-context.js'
10
10
  export { getNode, listNodes } from './core/canvas/canvas.js';
11
11
  export { nodeDir } from './core/canvas/paths.js';
12
12
  export type { NodeMeta, NodeRow, NodeStatus, Lifecycle, Mode, } from './core/canvas/types.js';
13
- export { asksForNodes, asksAcrossCanvas } from './core/canvas/attention.js';
14
- export type { AskEntry } from './core/canvas/attention.js';
13
+ export { ticketCountsForNodes, ticketsAcrossCanvas } from './core/canvas/attention.js';
14
+ export type { TicketEntry } from './core/canvas/attention.js';
15
15
  export { readTelemetry, readContextTokens } from './core/canvas/telemetry.js';
16
16
  export type { Telemetry } from './core/canvas/telemetry.js';
17
17
  export { ViewSocketClient, BrokerUnavailableError } from './clients/attach/view-socket.js';
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ export { appendSituationalContext } from './core/runtime/situational-context.js'
23
23
  // ── Canvas reads ─────────────────────────────────────────────────────────
24
24
  export { getNode, listNodes } from './core/canvas/canvas.js';
25
25
  export { nodeDir } from './core/canvas/paths.js';
26
- export { asksForNodes, asksAcrossCanvas } from './core/canvas/attention.js';
26
+ export { ticketCountsForNodes, ticketsAcrossCanvas } from './core/canvas/attention.js';
27
27
  export { readTelemetry, readContextTokens } from './core/canvas/telemetry.js';
28
28
  // ── Broker client ────────────────────────────────────────────────────────
29
29
  export { ViewSocketClient, BrokerUnavailableError } from './clients/attach/view-socket.js';