@north-light/crouter 0.3.175 → 0.3.177

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 (185) hide show
  1. package/dist/api/client.d.ts +4 -4
  2. package/dist/api/client.js +2 -2
  3. package/dist/api/dto/lifecycle.d.ts +1 -1
  4. package/dist/api/dto/nodes.d.ts +7 -1
  5. package/dist/api/dto/profiles.d.ts +3 -0
  6. package/dist/api/dto/reports.d.ts +1 -1
  7. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  8. package/dist/clients/attach/config.js +5 -5
  9. package/dist/clients/attach/input/clipboard-image.js +2 -2
  10. package/dist/clients/attach/input/ref-autocomplete.js +1 -1
  11. package/dist/clients/attach/input/titled-editor.d.ts +12 -13
  12. package/dist/clients/attach/input/titled-editor.js +14 -15
  13. package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
  14. package/dist/clients/attach/overlays/dialogs.js +2 -2
  15. package/dist/clients/attach/overlays/mcp.js +5 -12
  16. package/dist/clients/attach/render/chat-view.d.ts +1 -1
  17. package/dist/clients/attach/render/chat-view.js +2 -2
  18. package/dist/clients/attach/render/crtr-output.d.ts +3 -3
  19. package/dist/clients/attach/render/crtr-output.js +25 -13
  20. package/dist/clients/attach/render/group-recap.js +7 -0
  21. package/dist/clients/attach/session/context.js +3 -3
  22. package/dist/clients/attach/session/editor-inventory.js +6 -6
  23. package/dist/clients/attach/session/input-wiring.js +1 -1
  24. package/dist/clients/attach/session/teardown.js +7 -9
  25. package/dist/clients/attach/slash/dispatch.d.ts +1 -2
  26. package/dist/clients/attach/slash/dispatch.js +4 -8
  27. package/dist/clients/attach/viewer.js +536 -566
  28. package/dist/clients/inbox/controller.js +3 -2
  29. package/dist/clients/inbox/deck-adapter.js +3 -2
  30. package/dist/clients/surfaces/host.d.ts +1 -1
  31. package/dist/commands/api-client.d.ts +12 -13
  32. package/dist/commands/api-client.js +15 -16
  33. package/dist/commands/chord.js +1 -2
  34. package/dist/commands/human/queue.js +6 -5
  35. package/dist/commands/memory/find.js +3 -3
  36. package/dist/commands/node/create.js +7 -3
  37. package/dist/commands/node/lifecycle.js +1 -1
  38. package/dist/commands/node-lifecycle-revive.d.ts +1 -1
  39. package/dist/commands/node-lifecycle-revive.js +3 -3
  40. package/dist/commands/profile/list.js +3 -1
  41. package/dist/commands/profile/new.js +4 -1
  42. package/dist/commands/profile/project.js +23 -9
  43. package/dist/commands/profile/show.js +3 -1
  44. package/dist/commands/profile.js +2 -2
  45. package/dist/commands/push.js +1 -1
  46. package/dist/commands/surface/node/placement.d.ts +1 -1
  47. package/dist/commands/surface/node/placement.js +1 -1
  48. package/dist/commands/surface-edit.js +1 -4
  49. package/dist/commands/sys/logs.js +4 -2
  50. package/dist/commands/sys/panels/panel.d.ts +3 -3
  51. package/dist/commands/sys/panels/panel.js +3 -3
  52. package/dist/commands/sys/setup-core.js +1 -7
  53. package/dist/commands/sys/sync-deps.js +1 -3
  54. package/dist/commands/sys/sync-project-guidance.js +2 -11
  55. package/dist/commands/sys/sysprompt.js +1 -3
  56. package/dist/core/__tests__/branded-host-concurrency.test.js +1 -1
  57. package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
  58. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  59. package/dist/core/__tests__/helpers/harness.js +1 -1
  60. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +1 -1
  61. package/dist/core/__tests__/on-read-dedup-resume.test.js +1 -1
  62. package/dist/core/canvas/boot.js +1 -1
  63. package/dist/core/canvas/browse/app.js +2 -2
  64. package/dist/core/canvas/canvas.d.ts +3 -3
  65. package/dist/core/canvas/canvas.js +3 -3
  66. package/dist/core/canvas/daemon-owner.js +1 -2
  67. package/dist/core/canvas/db.js +7 -8
  68. package/dist/core/canvas/extensions.d.ts +1 -1
  69. package/dist/core/canvas/extensions.js +7 -1
  70. package/dist/core/canvas/index.js +2 -2
  71. package/dist/core/canvas/pid.d.ts +12 -14
  72. package/dist/core/canvas/pid.js +16 -20
  73. package/dist/core/canvas/remote-transport.js +2 -2
  74. package/dist/core/canvas/status-glyph.d.ts +1 -1
  75. package/dist/core/canvas/status-glyph.js +1 -1
  76. package/dist/core/canvas/types.d.ts +1 -1
  77. package/dist/core/command-manifests/manifest.js +4 -6
  78. package/dist/core/command-manifests/schema.js +10 -12
  79. package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
  80. package/dist/core/command-plugins/transport/http-invoke.js +14 -8
  81. package/dist/core/command.js +3 -1
  82. package/dist/core/config.d.ts +1 -1
  83. package/dist/core/config.js +6 -29
  84. package/dist/core/events/read.js +1 -3
  85. package/dist/core/exclusive-lock.d.ts +14 -0
  86. package/dist/core/exclusive-lock.js +124 -0
  87. package/dist/core/fault-classifier.js +1 -3
  88. package/dist/core/fs-utils.d.ts +7 -0
  89. package/dist/core/fs-utils.js +29 -2
  90. package/dist/core/host-exports/export.d.ts +4 -5
  91. package/dist/core/host-exports/export.js +4 -5
  92. package/dist/core/human/claim.js +4 -2
  93. package/dist/core/human/convention.d.ts +0 -11
  94. package/dist/core/human/convention.js +1 -131
  95. package/dist/core/human/scan.js +5 -4
  96. package/dist/core/io.d.ts +23 -0
  97. package/dist/core/io.js +60 -0
  98. package/dist/core/preview-registry.d.ts +10 -5
  99. package/dist/core/preview-registry.js +136 -2537
  100. package/dist/core/profiles/manifest.d.ts +13 -2
  101. package/dist/core/profiles/manifest.js +58 -49
  102. package/dist/core/review/store.d.ts +4 -1
  103. package/dist/core/review/store.js +10 -0
  104. package/dist/core/review/types.d.ts +1 -1
  105. package/dist/core/review/types.js +1 -1
  106. package/dist/core/runtime/branded-host.js +6 -84
  107. package/dist/core/runtime/broker/frame-dispatch.js +4 -4
  108. package/dist/core/runtime/broker-cli.js +1 -1
  109. package/dist/core/runtime/broker-extension-render.js +1 -1
  110. package/dist/core/runtime/broker-protocol.d.ts +9 -10
  111. package/dist/core/runtime/broker-protocol.js +9 -10
  112. package/dist/core/runtime/broker-sdk.d.ts +1 -1
  113. package/dist/core/runtime/broker.js +6 -6
  114. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  115. package/dist/core/runtime/canvas-extensions.js +2 -0
  116. package/dist/core/runtime/host.js +14 -14
  117. package/dist/core/runtime/launch.d.ts +1 -1
  118. package/dist/core/runtime/launch.js +1 -1
  119. package/dist/core/runtime/lifecycle.js +16 -21
  120. package/dist/core/runtime/node-read.js +2 -3
  121. package/dist/core/runtime/package-health.js +1 -8
  122. package/dist/core/runtime/persona.js +2 -2
  123. package/dist/core/runtime/pi-vendored.d.ts +4 -2
  124. package/dist/core/runtime/pi-vendored.js +5 -16
  125. package/dist/core/runtime/promote.js +1 -1
  126. package/dist/core/runtime/reopen.js +2 -2
  127. package/dist/core/runtime/revive-all.d.ts +3 -3
  128. package/dist/core/runtime/revive-all.js +6 -6
  129. package/dist/core/runtime/revive.js +1 -1
  130. package/dist/core/runtime/spawn.d.ts +1 -2
  131. package/dist/core/runtime/spawn.js +2 -3
  132. package/dist/core/runtime/structured-output.js +4 -6
  133. package/dist/core/runtime/tmux-bindings.js +1 -1
  134. package/dist/core/runtime/tmux-driver.d.ts +2 -2
  135. package/dist/core/runtime/tmux-driver.js +3 -5
  136. package/dist/core/runtime/tool-group-summary.js +5 -4
  137. package/dist/core/runtime/warm-pool.d.ts +9 -8
  138. package/dist/core/runtime/warm-pool.js +58 -62
  139. package/dist/core/scope.js +2 -9
  140. package/dist/core/self-update.js +3 -3
  141. package/dist/core/spawn.d.ts +0 -1
  142. package/dist/core/spawn.js +1 -4
  143. package/dist/core/substrate/index.d.ts +2 -1
  144. package/dist/core/substrate/index.js +2 -1
  145. package/dist/core/substrate/on-read-node.d.ts +7 -0
  146. package/dist/core/substrate/on-read-node.js +37 -0
  147. package/dist/core/substrate/on-read.d.ts +7 -5
  148. package/dist/core/substrate/on-read.js +16 -32
  149. package/dist/core/termrender/termrender.js +16 -96
  150. package/dist/core/wake.js +2 -2
  151. package/dist/core/worktree.d.ts +3 -3
  152. package/dist/core/worktree.js +16 -107
  153. package/dist/daemon/api/bridge.js +1 -1
  154. package/dist/daemon/api/handlers/broker-ops.js +1 -2
  155. package/dist/daemon/api/handlers/messages.js +2 -2
  156. package/dist/daemon/api/handlers/nodes.js +33 -10
  157. package/dist/daemon/api/handlers/reports.js +1 -1
  158. package/dist/daemon/api/handlers/review-comments.js +5 -44
  159. package/dist/daemon/api/handlers/reviews.js +5 -53
  160. package/dist/daemon/api/handlers/validate.d.ts +23 -0
  161. package/dist/daemon/api/handlers/validate.js +58 -0
  162. package/dist/daemon/api/map.js +1 -0
  163. package/dist/daemon/cron-run.js +7 -7
  164. package/dist/daemon/human/finish.js +3 -2
  165. package/dist/daemon/manage.d.ts +6 -6
  166. package/dist/daemon/manage.js +15 -16
  167. package/dist/daemon/review/finish.js +1 -10
  168. package/dist/pi-extensions/canvas-context-intro.js +1 -14
  169. package/dist/pi-extensions/canvas-doc-substrate.js +2 -2
  170. package/dist/pi-extensions/canvas-preview-result.d.ts +22 -0
  171. package/dist/pi-extensions/canvas-preview-result.js +60 -0
  172. package/dist/pi-extensions/canvas-review-boundary.js +1 -14
  173. package/dist/pi-extensions/truncate.d.ts +5 -0
  174. package/dist/pi-extensions/truncate.js +14 -0
  175. package/dist/shared/predicates.d.ts +2 -0
  176. package/dist/shared/predicates.js +4 -0
  177. package/dist/shared/shell-quote.d.ts +3 -0
  178. package/dist/shared/shell-quote.js +5 -0
  179. package/dist/shared/tool-groups.d.ts +4 -2
  180. package/dist/shared/tool-groups.js +3 -3
  181. package/dist/types.d.ts +8 -5
  182. package/package.json +1 -1
  183. package/runtime.lock.json +2 -2
  184. package/dist/core/substrate/on-read-pure.d.ts +0 -7
  185. package/dist/core/substrate/on-read-pure.js +0 -272
@@ -44,8 +44,13 @@
44
44
  //
45
45
  // Pool depth is 1 per key. It is populated lazily (the first `prefer_warm`
46
46
  // create for a key falls back to a cold spawn and mints the spare behind it)
47
- // and at daemon start for the most recent live roots, so the FIRST node after
48
- // sitting down is warm too.
47
+ // and at daemon start for the most recently used profiles, so the FIRST node
48
+ // after sitting down is warm too.
49
+ //
50
+ // The pool does not fan out per directory: a node runs in its profile's home
51
+ // (`resolveNodeCwd` in the create handler), so the cwd a spare freezes is a
52
+ // function of its profile, and "which spare serves this create" is in practice
53
+ // a question about the profile alone.
49
54
  //
50
55
  // Nothing ever RESUMES a spare: the daemon's recovery sweep reads `listNodes`,
51
56
  // which hides the pool, so a spare whose broker dies (every daemon handover
@@ -57,7 +62,8 @@ import { createHash } from 'node:crypto';
57
62
  import { claimWarmSpare, deleteNode, getNode, listNodes, listWarmSpares, registerWarmSpare, unregisterWarmSpare, updateNode, warmSpareCount, } from '../canvas/index.js';
58
63
  import { recordedPidLiveness } from '../canvas/pid.js';
59
64
  import { nowIso } from '../fs-utils.js';
60
- import { resolveProfileId, spawnChildPrepared } from './spawn.js';
65
+ import { spawnChildPrepared } from './spawn.js';
66
+ import { profileHome } from '../profiles/manifest.js';
61
67
  import { buildLaunchSpec } from './launch.js';
62
68
  import { setModelLive } from './model-swap.js';
63
69
  import { setSituationalContextLive } from './situational-live.js';
@@ -94,31 +100,23 @@ function warmRecipeKey(cwd, profileId, launch) {
94
100
  ]);
95
101
  return createHash('sha256').update(canonical).digest('hex').slice(0, 32);
96
102
  }
97
- /** Resolve a request's frozen half. `resolveProfileId(explicit, null, cwd)` is
98
- * the SAME call the cold create path makes for an API-created root (no spawner
99
- * — a warm-eligible create is parentless by construction), so keying the
100
- * resolved id costs nothing extra and keys what actually matters. */
101
- async function resolveRecipe(request) {
102
- const profileId = await resolveProfileId(request.profile, null, request.cwd);
103
+ /** Resolve a request's frozen half. Both cwd and profile arrive already
104
+ * resolved from the create handler, so this is pure launch-spec construction. */
105
+ function resolveRecipe(request) {
103
106
  const { launch } = buildLaunchSpec(request.kind, request.mode, {
104
107
  lifecycle: 'resident',
105
108
  hasManager: false,
106
109
  ...(request.model !== null ? { model: request.model } : {}),
107
110
  cwd: request.cwd,
108
- profileId,
111
+ profileId: request.profileId,
109
112
  });
110
- return {
111
- key: warmRecipeKey(request.cwd, profileId, launch),
112
- launch,
113
- profileId,
114
- request,
115
- };
113
+ return { key: warmRecipeKey(request.cwd, request.profileId, launch), launch, request };
116
114
  }
117
115
  /** Claim a spare for `request`, reshaped onto its persona — or null when the
118
116
  * pool cannot serve it. The caller falls back to a cold spawn; a claim never
119
117
  * fails the create. */
120
118
  export async function claimWarmNode(request) {
121
- const recipe = await resolveRecipe(request);
119
+ const recipe = resolveRecipe(request);
122
120
  const created = nowIso();
123
121
  // The row's stamp moves inside the claim transaction; meta.json (the identity
124
122
  // source of truth `rebuildIndex` re-derives the row from) follows with the
@@ -261,18 +259,15 @@ const minting = new Set();
261
259
  export function refillWarmPool(request) {
262
260
  if (!warmPoolEnabled())
263
261
  return;
264
- void (async () => {
265
- try {
266
- // Mint at the kind's DEFAULT model and with NO situational context: both
267
- // are claim-time properties, so baking either in would only narrow who
268
- // the spare can serve.
269
- const recipe = await resolveRecipe({ ...request, model: null, situationalContext: null });
270
- mint(recipe);
271
- }
272
- catch (err) {
273
- console.error(`[warm-pool] recipe resolution failed: ${err.message}`);
274
- }
275
- })();
262
+ try {
263
+ // Mint at the kind's DEFAULT model and with NO situational context: both
264
+ // are claim-time properties, so baking either in would only narrow who the
265
+ // spare can serve.
266
+ mint(resolveRecipe({ ...request, model: null, situationalContext: null }));
267
+ }
268
+ catch (err) {
269
+ console.error(`[warm-pool] recipe resolution failed: ${err.message}`);
270
+ }
276
271
  }
277
272
  /** Mint one spare for an already-resolved recipe, if the pool is short. */
278
273
  function mint(recipe) {
@@ -298,7 +293,7 @@ function mint(recipe) {
298
293
  // Mint at the kind's DEFAULT model: `model_override` then stays unset on
299
294
  // the spare, so a claim only ever SETS it (updateNode cannot clear a
300
295
  // field) and a no-model claim gets a clean row.
301
- ...(request.profile !== undefined ? { profile: request.profile } : {}),
296
+ profile: request.profileId,
302
297
  // No situational context either: the claimant's is delivered live at
303
298
  // claim time, so a spare that baked one in would serve nobody else.
304
299
  // Mark it unclaimed the instant its row exists and BEFORE the broker
@@ -317,14 +312,14 @@ function mint(recipe) {
317
312
  // ---------------------------------------------------------------------------
318
313
  // Daemon-start pre-warm
319
314
  // ---------------------------------------------------------------------------
320
- /** How many distinct (cwd, profile) recipes get a spare at daemon start. Each
321
- * spare is a resident broker at ~150-300 MB, so this is deliberately tiny. */
322
- const PREWARM_RECIPES = 2;
323
- /** Mint a spare for the most recently used project recipes so the FIRST create
324
- * after crtrd boots is warm too, not only the second. The recipe is sourced
325
- * from real recent roots — (cwd, profile) is the whole of it, since everything
326
- * else a claim reshapes live. Fire-and-forget: returns as soon as the mints
327
- * are scheduled, never blocking the daemon's work lane. */
315
+ /** How many profiles get a spare at daemon start. Each spare is a resident
316
+ * broker at ~150-300 MB, so this is deliberately tiny. */
317
+ const PREWARM_PROFILES = 2;
318
+ /** Mint a spare for the most recently used profiles so the FIRST create after
319
+ * crtrd boots is warm too, not only the second. The profile is the whole
320
+ * recipe: it supplies the cwd, and everything else a claim reshapes live.
321
+ * Fire-and-forget — returns as soon as the mints are scheduled, never blocking
322
+ * the daemon's work lane. */
328
323
  export function prewarmRecentRecipes() {
329
324
  if (!warmPoolEnabled())
330
325
  return;
@@ -336,12 +331,15 @@ export function prewarmRecentRecipes() {
336
331
  for (const row of listNodes({ status: ['active', 'idle'] }).reverse()) {
337
332
  if (row.parent !== null || row.kind === 'human')
338
333
  continue;
339
- const dedupe = `${row.cwd}\u0000${row.profile_id ?? ''}`;
340
- if (seen.has(dedupe))
334
+ const profileId = row.profile_id ?? null;
335
+ if (seen.has(profileId ?? ''))
341
336
  continue;
342
- seen.add(dedupe);
343
- candidates.push({ cwd: row.cwd, profileId: row.profile_id ?? null });
344
- if (candidates.length >= PREWARM_RECIPES)
337
+ seen.add(profileId ?? '');
338
+ // The profile's home is where its next node will land. A profile without
339
+ // one (root, or a home since deleted) still gets a spare, keyed on the
340
+ // directory its last root actually ran in.
341
+ candidates.push({ cwd: profileHome(profileId) ?? row.cwd, profileId });
342
+ if (candidates.length >= PREWARM_PROFILES)
345
343
  break;
346
344
  }
347
345
  }
@@ -350,24 +348,22 @@ export function prewarmRecentRecipes() {
350
348
  return;
351
349
  }
352
350
  for (const candidate of candidates) {
353
- void (async () => {
354
- try {
355
- // `general`/`base` is only the shape the spare BOOTS as — the key it is
356
- // filed under excludes everything a claim can reshape, so any kind can
357
- // claim it.
358
- mint(await resolveRecipe({
359
- kind: 'general',
360
- mode: 'base',
361
- cwd: candidate.cwd,
362
- profile: candidate.profileId,
363
- model: null,
364
- situationalContext: null,
365
- name: null,
366
- }));
367
- }
368
- catch (err) {
369
- console.error(`[warm-pool] prewarm failed for ${candidate.cwd}: ${err.message}`);
370
- }
371
- })();
351
+ try {
352
+ // `general`/`base` is only the shape the spare BOOTS as — the key it is
353
+ // filed under excludes everything a claim can reshape, so any kind can
354
+ // claim it.
355
+ mint(resolveRecipe({
356
+ kind: 'general',
357
+ mode: 'base',
358
+ cwd: candidate.cwd,
359
+ profileId: candidate.profileId,
360
+ model: null,
361
+ situationalContext: null,
362
+ name: null,
363
+ }));
364
+ }
365
+ catch (err) {
366
+ console.error(`[warm-pool] prewarm failed for ${candidate.cwd}: ${err.message}`);
367
+ }
372
368
  }
373
369
  }
@@ -1,10 +1,11 @@
1
1
  import { homedir } from 'node:os';
2
- import { existsSync, statSync, realpathSync } from 'node:fs';
2
+ import { existsSync, statSync } from 'node:fs';
3
3
  import { join, resolve, dirname } from 'node:path';
4
4
  import { CRTR_DIR_NAME } from '../types.js';
5
5
  import { assetRoot } from './asset-root.js';
6
6
  import { usage } from './errors.js';
7
7
  import { loadProfileManifest } from './profiles/manifest.js';
8
+ import { realpathOrSelf } from './fs-utils.js';
8
9
  // Cache of resolved project-scope-root STACKS, keyed by (resolved cwd, profile
9
10
  // id). A profile widens the pointer set beyond the bare cwd (see
10
11
  // findProjectScopeRoots below), so the cache key must carry both — the same
@@ -31,14 +32,6 @@ export function builtinPiPackageDir(name) {
31
32
  export function userScopeRoot() {
32
33
  return join(homedir(), CRTR_DIR_NAME);
33
34
  }
34
- function realpathOrSelf(p) {
35
- try {
36
- return realpathSync(p);
37
- }
38
- catch {
39
- return p;
40
- }
41
- }
42
35
  function isProjectScopeDir(candidate, userRoot) {
43
36
  if (!existsSync(candidate))
44
37
  return false;
@@ -1,6 +1,6 @@
1
- // Self-update and content-check primitives extracted from commands/update.ts.
2
- // Moved here to break the core→commands import inversion: auto-update.ts
3
- // (core) previously imported from commands/update.ts (commands layer).
1
+ // Self-update and content-check primitives. They live in core/ so auto-update.ts
2
+ // (core) reaches them without importing commands/update.ts, which would invert
3
+ // the core→commands layering.
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { join, dirname } from 'node:path';
@@ -13,4 +13,3 @@ export declare function isInTmux(): boolean;
13
13
  * no server is running — the true "no tmux to surface into" case that should
14
14
  * degrade to the inbox-drain follow-up. */
15
15
  export declare function tmuxServerReachable(): boolean;
16
- export declare function shellQuote(s: string): string;
@@ -1,4 +1,4 @@
1
- // Tmux reachability + quoting helpers for the `human` command tree.
1
+ // Tmux reachability for the `human` command tree.
2
2
  //
3
3
  // The human surface gates on whether the canvas tmux server is reachable
4
4
  // (tmuxServerReachable) so it can decide whether to surface a prompt or degrade
@@ -25,6 +25,3 @@ export function tmuxServerReachable() {
25
25
  const r = spawnSync('tmux', ['list-clients', '-F', '#{client_name}'], { encoding: 'utf8' });
26
26
  return r.status === 0;
27
27
  }
28
- export function shellQuote(s) {
29
- return `'${s.replace(/'/g, "'\\''")}'`;
30
- }
@@ -7,4 +7,5 @@ export { gatePasses } from './gate.js';
7
7
  export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveSystemPromptRung, } from './ceiling.js';
8
8
  export { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
9
9
  export { renderPreferencesSection, renderKnowledgeBlock } from './render-node.js';
10
- export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read.js';
10
+ export { renderOnReadDocsForSubject, renderWorkspaceOpenDocsForSubject } from './on-read.js';
11
+ export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read-node.js';
@@ -22,4 +22,5 @@ export { gatePasses } from './gate.js';
22
22
  export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveSystemPromptRung, } from './ceiling.js';
23
23
  export { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
24
24
  export { renderPreferencesSection, renderKnowledgeBlock } from './render-node.js';
25
- export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read.js';
25
+ export { renderOnReadDocsForSubject, renderWorkspaceOpenDocsForSubject } from './on-read.js';
26
+ export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read-node.js';
@@ -0,0 +1,7 @@
1
+ /** Surface docs matched by a successful read tool call. An unknown node id or
2
+ * an unreadable canvas renders nothing. */
3
+ export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: Set<string>): string;
4
+ /** Surface workspace-wide docs during first-message assembly, using the node's
5
+ * recorded cwd and selected profile. `.` is a reserved applies-to target
6
+ * meaning “when this project store is mounted by cwd/profile”. */
7
+ export declare function renderWorkspaceOpenDocs(nodeId: string): string;
@@ -0,0 +1,37 @@
1
+ // on-read-node.ts — the CANVAS-DB wrappers over the pure on-read functions.
2
+ // Both take a node id, assemble its subject from the canvas-db, then delegate
3
+ // to on-read.ts. Split out for the same reason as render-node.ts: a consumer
4
+ // that already holds a subject imports on-read.js without subject.js's
5
+ // canvas-db reach.
6
+ import { getNode } from '../canvas/index.js';
7
+ import { assembleNodeSubject } from './subject.js';
8
+ import { renderOnReadDocsForSubject, renderWorkspaceOpenDocsForSubject } from './on-read.js';
9
+ /** Surface docs matched by a successful read tool call. An unknown node id or
10
+ * an unreadable canvas renders nothing. */
11
+ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
12
+ let subject;
13
+ try {
14
+ subject = assembleNodeSubject(nodeId);
15
+ }
16
+ catch {
17
+ return '';
18
+ }
19
+ if (subject === null)
20
+ return '';
21
+ return renderOnReadDocsForSubject(subject, readFilePath, seen);
22
+ }
23
+ /** Surface workspace-wide docs during first-message assembly, using the node's
24
+ * recorded cwd and selected profile. `.` is a reserved applies-to target
25
+ * meaning “when this project store is mounted by cwd/profile”. */
26
+ export function renderWorkspaceOpenDocs(nodeId) {
27
+ try {
28
+ const subject = assembleNodeSubject(nodeId);
29
+ const node = getNode(nodeId);
30
+ if (subject === null || node === null)
31
+ return '';
32
+ return renderWorkspaceOpenDocsForSubject(subject, node.cwd, node.profile_id ?? null);
33
+ }
34
+ catch {
35
+ return '';
36
+ }
37
+ }
@@ -1,5 +1,7 @@
1
- /** Surface docs matched by a successful read tool call. */
2
- export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: Set<string>): string;
3
- /** Surface workspace-wide docs during first-message assembly. `.` is a reserved
4
- * applies-to target meaning “when this project store is mounted by cwd/profile”. */
5
- export declare function renderWorkspaceOpenDocs(nodeId: string): string;
1
+ import type { NodeConfigSubject } from './subject-fields.js';
2
+ /** Surface docs matched by a successful read tool call using the caller-supplied
3
+ * daemon snapshot subject. */
4
+ export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, readFilePath: string, seen?: Set<string>): string;
5
+ /** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
6
+ * lookup. */
7
+ export declare function renderWorkspaceOpenDocsForSubject(subject: NodeConfigSubject, cwd: string, profileId: string | null): string;
@@ -1,4 +1,8 @@
1
- // on-read.ts — file-context delivery for the document substrate.
1
+ // on-read.ts — the pure file-context delivery functions for the document
2
+ // substrate. Both take a caller-supplied subject; the canvas-db wrappers that
3
+ // resolve one from a node id live in on-read-node.ts, so a consumer that
4
+ // already holds a subject imports this module WITHOUT pulling subject.js's
5
+ // canvas-db reach (the same split as render.ts / render-node.ts).
2
6
  //
3
7
  // A memory with file-read visibility participates only through an explicit
4
8
  // `applies-to` path glob (authoring lint requires one). Two events use that one
@@ -20,7 +24,6 @@
20
24
  // Every candidate renders at its own fileReadVisibility rung. Explicit routing
21
25
  // is the whole on-read model, so directory INDEX ceilings remain a boot-catalog
22
26
  // concern and do not cap a deliberately matched document.
23
- import { realpathSync } from 'node:fs';
24
27
  import { homedir } from 'node:os';
25
28
  import { basename, dirname, matchesGlob, parse, relative, sep } from 'node:path';
26
29
  import { CRTR_DIR_NAME } from '../../types.js';
@@ -28,19 +31,13 @@ import { pathExists, readText, walkFiles } from '../fs-utils.js';
28
31
  import { parseFrontmatterGeneric } from '../frontmatter.js';
29
32
  import { evalCondition } from '../predicate.js';
30
33
  import { listAllMemoryDocs, listProjectMemoryDocs } from '../memory-resolver.js';
31
- import { getNode } from '../canvas/index.js';
32
34
  import { userScopeRoot } from '../scope.js';
33
- import { assembleNodeSubject, displayName, gatePasses, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, } from './index.js';
35
+ import { displayName } from './ceiling.js';
36
+ import { gatePasses } from './gate.js';
37
+ import { normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName } from './schema.js';
34
38
  import { cachedSubstrateDocs } from './session-cache.js';
39
+ import { realpathOrSelf } from '../fs-utils.js';
35
40
  const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
36
- function realpathOrSelf(p) {
37
- try {
38
- return realpathSync(p);
39
- }
40
- catch {
41
- return p;
42
- }
43
- }
44
41
  function attr(s) {
45
42
  return s
46
43
  .replace(/&/g, '&amp;')
@@ -237,17 +234,9 @@ function renderCandidates(subject, docs, seen) {
237
234
  ? ''
238
235
  : `<auto-loaded-context>\n${rendered.join('\n')}\n</auto-loaded-context>`;
239
236
  }
240
- /** Surface docs matched by a successful read tool call. */
241
- export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
242
- let subject;
243
- try {
244
- subject = assembleNodeSubject(nodeId);
245
- }
246
- catch {
247
- return '';
248
- }
249
- if (subject === null)
250
- return '';
237
+ /** Surface docs matched by a successful read tool call using the caller-supplied
238
+ * daemon snapshot subject. */
239
+ export function renderOnReadDocsForSubject(subject, readFilePath, seen = new Set()) {
251
240
  const absReadFile = realpathOrSelf(readFilePath);
252
241
  const readFrontmatter = readFileFrontmatter(absReadFile);
253
242
  const docs = dedupeByPhysicalPath([...enclosingProjectDocs(absReadFile), ...resolvedDocs()])
@@ -255,17 +244,12 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
255
244
  .filter((doc) => matchesReadEvent(doc, absReadFile, readFrontmatter));
256
245
  return renderCandidates(subject, docs, seen);
257
246
  }
258
- /** Surface workspace-wide docs during first-message assembly. `.` is a reserved
259
- * applies-to target meaning “when this project store is mounted by cwd/profile”. */
260
- export function renderWorkspaceOpenDocs(nodeId) {
261
- let subject;
247
+ /** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
248
+ * lookup. */
249
+ export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId) {
262
250
  let docs;
263
251
  try {
264
- subject = assembleNodeSubject(nodeId);
265
- const node = getNode(nodeId);
266
- if (subject === null || node === null)
267
- return '';
268
- docs = listProjectMemoryDocs(node.cwd, node.profile_id ?? null)
252
+ docs = listProjectMemoryDocs(cwd, profileId)
269
253
  .map(parseSubstrateDoc)
270
254
  .filter((doc) => doc !== null);
271
255
  }
@@ -1,8 +1,9 @@
1
1
  import { execFileSync, spawnSync } from 'node:child_process';
2
- import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, openSync, closeSync, unlinkSync, renameSync, accessSync, realpathSync, constants, } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, unlinkSync, renameSync, accessSync, realpathSync, constants, } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import stringWidth from 'string-width';
6
+ import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
6
7
  import { TERMRENDER_VERSION } from './version.js';
7
8
  // ── The sole org-wide termrender binding ─────────────────────────────────────
8
9
  //
@@ -31,15 +32,11 @@ const VENV_STAMP = join(VENV_DIR, '.crtr-termrender-stamp.json');
31
32
  // in a read-only runtime image, and serializes venv mutation + stamp publication
32
33
  // across processes: a stamp can never certify a concurrently-changing venv.
33
34
  const VENV_LOCK = join(RENDERER_CACHE_DIR, '.crtr-termrender.lock');
34
- // A lock older than this is from a crashed process and may be stolen. Set
35
- // comfortably above the worst-case held path (uv probe 5s + venv 60s + install
36
- // 120s + re-verify ~10s ≈ 195s) so a slow-but-alive holder is never judged
37
- // stale while it still holds.
38
- const LOCK_STALE_MS = 300_000;
39
35
  // Absolute cap on how long a waiter spins before giving up to plaintext for
40
36
  // this session (the next launch retries) — a safety valve so a wedged holder
41
- // can never hang a process, WITHOUT ever stealing a lock we can't prove stale.
42
- const LOCK_GIVE_UP_MS = LOCK_STALE_MS + 60_000;
37
+ // can never hang a process. Set comfortably above the worst-case held path
38
+ // (uv probe 5s + venv 60s + install 120s + re-verify ~10s ≈ 195s).
39
+ const LOCK_GIVE_UP_MS = 360_000;
43
40
  let rendererState = 'unchecked';
44
41
  function isFp(x) {
45
42
  const e = x;
@@ -167,46 +164,12 @@ function stampValid() {
167
164
  }
168
165
  return true;
169
166
  }
170
- function sleepSync(ms) {
171
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
172
- }
173
- function lockIsStale() {
174
- try {
175
- const at = JSON.parse(readFileSync(VENV_LOCK, 'utf8')).at;
176
- if (typeof at === 'number')
177
- return Date.now() - at > LOCK_STALE_MS;
178
- }
179
- catch {
180
- // Unreadable/malformed lock — fall through to mtime.
181
- }
182
- try {
183
- return Date.now() - statSync(VENV_LOCK).mtimeMs > LOCK_STALE_MS;
184
- }
185
- catch {
186
- return true; // vanished — the retry loop re-acquires
187
- }
188
- }
189
- // Run `provision` while holding the exclusive provisioning lock. If another
190
- // live process holds it, wait for that process to publish (re-checking the
191
- // stamp) rather than mutating the venv concurrently. Stale locks are stolen.
192
- // Steal a lock judged stale by renaming it to a unique name first: rename is
193
- // atomic, so if two waiters race only the one whose rename succeeds owns (and
194
- // deletes) it — the loser gets ENOENT and re-loops. This can never delete a
195
- // lock a peer has freshly re-acquired (that peer holds a DIFFERENT inode at the
196
- // same path; our rename of the old name either already happened or fails).
197
- function stealStaleLock() {
198
- const tmp = `${VENV_LOCK}.steal.${process.pid}.${Date.now()}`;
199
- try {
200
- renameSync(VENV_LOCK, tmp);
201
- }
202
- catch {
203
- return; // lost the steal race or already gone — caller re-loops
204
- }
205
- try {
206
- unlinkSync(tmp);
207
- }
208
- catch { /* best effort */ }
209
- }
167
+ // Run `provision` holding the exclusive provisioning lock, so no two processes
168
+ // mutate the venv (or certify it) at once. A crashed holder is reclaimed at
169
+ // once; a live one is waited on, and if it outlasts the give-up window this
170
+ // process falls back to plaintext for the session rather than break mutual
171
+ // exclusion. `provision` re-checks the stamp first, so a waiter that finally
172
+ // acquires adopts the peer's published result instead of reinstalling.
210
173
  function withProvisionLock(provision) {
211
174
  try {
212
175
  mkdirSync(RENDERER_CACHE_DIR, { recursive: true });
@@ -216,54 +179,11 @@ function withProvisionLock(provision) {
216
179
  process.stderr.write(`[crtr] termrender unavailable — renderer cache cannot be created (${err instanceof Error ? err.message : String(err)}); using plaintext fallback\n`);
217
180
  return;
218
181
  }
219
- const giveUpAt = Date.now() + LOCK_GIVE_UP_MS;
220
- for (;;) {
221
- let fd;
222
- try {
223
- fd = openSync(VENV_LOCK, 'wx'); // O_CREAT | O_EXCL — atomic acquire
224
- }
225
- catch (err) {
226
- if (err.code !== 'EEXIST') {
227
- rendererState = 'unavailable';
228
- process.stderr.write(`[crtr] termrender unavailable — renderer cache cannot be locked (${err instanceof Error ? err.message : String(err)}); using plaintext fallback\n`);
229
- return;
230
- }
231
- if (lockIsStale()) {
232
- stealStaleLock();
233
- continue;
234
- }
235
- // A live process is provisioning — give it a chance, then adopt its result.
236
- // Never steal a lock we can't prove stale; if we wait too long, give up to
237
- // plaintext this session rather than break mutual exclusion.
238
- if (Date.now() > giveUpAt) {
239
- rendererState = 'unavailable';
240
- return;
241
- }
242
- sleepSync(200);
243
- if (stampValid()) {
244
- rendererState = 'ready';
245
- return;
246
- }
247
- continue;
248
- }
249
- try {
250
- writeFileSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
251
- }
252
- catch { /* lock held regardless of whether the marker body wrote */ }
253
- try {
254
- provision();
255
- }
256
- finally {
257
- try {
258
- closeSync(fd);
259
- }
260
- catch { /* already closed */ }
261
- try {
262
- unlinkSync(VENV_LOCK);
263
- }
264
- catch { /* stolen as stale by another process */ }
265
- }
266
- return;
182
+ try {
183
+ withExclusiveDirectoryLock(VENV_LOCK, provision, { timeoutMs: LOCK_GIVE_UP_MS });
184
+ }
185
+ catch {
186
+ rendererState = 'unavailable';
267
187
  }
268
188
  }
269
189
  function binaryOk() {
package/dist/core/wake.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // This is the ONE place that owns cron time logic. Both the CLI surface and the
4
4
  // daemon's firing pass import from here, so there is no duplicated cron usage
5
5
  // and no surface<->daemon inverted dependency:
6
- // - parseWhen / parseCadence — arm-time grammar (surface, T7)
7
- // - nextSlotAfter — per-tick advance / coalescing (daemon, T4)
6
+ // - parseWhen / parseCadence — arm-time grammar (surface)
7
+ // - nextSlotAfter — per-tick advance / coalescing (daemon)
8
8
  //
9
9
  // Design refs: surface-design §4 (grammar), §5 (timezone/DST ruling),
10
10
  // design §5.3 + D7 (engine evaluates UTC; the surface bakes the IANA zone
@@ -95,11 +95,11 @@ export interface AutoDroppedWorktree {
95
95
  * branch and the live checkout stay fully recoverable and the caller's
96
96
  * `open_managed_worktree` guard fires. */
97
97
  export declare function autoDropCleanManagedWorktreeLocked(nodeId: string, wt: ManagedWorktree, proof: CleanEmptyProof): AutoDroppedWorktree | null;
98
- /** `push final`'s escape hatch (#327, #333): a managed worktree that has
98
+ /** `push final`'s escape hatch: a managed worktree that has
99
99
  * nothing to land is auto-dropped instead of blocking on an explicit
100
100
  * `node worktree close` — one the caller may have been told NOT to run
101
- * because a manager already landed the same change another way (#327), or
102
- * that never had anything to close in the first place (#333, a read-only
101
+ * because a manager already landed the same change another way, or
102
+ * that never had anything to close in the first place (a read-only
103
103
  * review node). Returns null and changes nothing when the worktree is not
104
104
  * PROVABLY empty-and-clean by `proveCleanEmptyManagedWorktree`, OR when a
105
105
  * concurrent mutation invalidates that proof before the destructive delete