@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.
- package/dist/api/client.d.ts +4 -4
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +1 -1
- package/dist/api/dto/nodes.d.ts +7 -1
- package/dist/api/dto/profiles.d.ts +3 -0
- package/dist/api/dto/reports.d.ts +1 -1
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/config.js +5 -5
- package/dist/clients/attach/input/clipboard-image.js +2 -2
- package/dist/clients/attach/input/ref-autocomplete.js +1 -1
- package/dist/clients/attach/input/titled-editor.d.ts +12 -13
- package/dist/clients/attach/input/titled-editor.js +14 -15
- package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
- package/dist/clients/attach/overlays/dialogs.js +2 -2
- package/dist/clients/attach/overlays/mcp.js +5 -12
- package/dist/clients/attach/render/chat-view.d.ts +1 -1
- package/dist/clients/attach/render/chat-view.js +2 -2
- package/dist/clients/attach/render/crtr-output.d.ts +3 -3
- package/dist/clients/attach/render/crtr-output.js +25 -13
- package/dist/clients/attach/render/group-recap.js +7 -0
- package/dist/clients/attach/session/context.js +3 -3
- package/dist/clients/attach/session/editor-inventory.js +6 -6
- package/dist/clients/attach/session/input-wiring.js +1 -1
- package/dist/clients/attach/session/teardown.js +7 -9
- package/dist/clients/attach/slash/dispatch.d.ts +1 -2
- package/dist/clients/attach/slash/dispatch.js +4 -8
- package/dist/clients/attach/viewer.js +536 -566
- package/dist/clients/inbox/controller.js +3 -2
- package/dist/clients/inbox/deck-adapter.js +3 -2
- package/dist/clients/surfaces/host.d.ts +1 -1
- package/dist/commands/api-client.d.ts +12 -13
- package/dist/commands/api-client.js +15 -16
- package/dist/commands/chord.js +1 -2
- package/dist/commands/human/queue.js +6 -5
- package/dist/commands/memory/find.js +3 -3
- package/dist/commands/node/create.js +7 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node-lifecycle-revive.d.ts +1 -1
- package/dist/commands/node-lifecycle-revive.js +3 -3
- package/dist/commands/profile/list.js +3 -1
- package/dist/commands/profile/new.js +4 -1
- package/dist/commands/profile/project.js +23 -9
- package/dist/commands/profile/show.js +3 -1
- package/dist/commands/profile.js +2 -2
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/placement.d.ts +1 -1
- package/dist/commands/surface/node/placement.js +1 -1
- package/dist/commands/surface-edit.js +1 -4
- package/dist/commands/sys/logs.js +4 -2
- package/dist/commands/sys/panels/panel.d.ts +3 -3
- package/dist/commands/sys/panels/panel.js +3 -3
- package/dist/commands/sys/setup-core.js +1 -7
- package/dist/commands/sys/sync-deps.js +1 -3
- package/dist/commands/sys/sync-project-guidance.js +2 -11
- package/dist/commands/sys/sysprompt.js +1 -3
- package/dist/core/__tests__/branded-host-concurrency.test.js +1 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -1
- package/dist/core/__tests__/helpers/harness.js +1 -1
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +1 -1
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +2 -2
- package/dist/core/canvas/canvas.d.ts +3 -3
- package/dist/core/canvas/canvas.js +3 -3
- package/dist/core/canvas/daemon-owner.js +1 -2
- package/dist/core/canvas/db.js +7 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/index.js +2 -2
- package/dist/core/canvas/pid.d.ts +12 -14
- package/dist/core/canvas/pid.js +16 -20
- package/dist/core/canvas/remote-transport.js +2 -2
- package/dist/core/canvas/status-glyph.d.ts +1 -1
- package/dist/core/canvas/status-glyph.js +1 -1
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command-manifests/manifest.js +4 -6
- package/dist/core/command-manifests/schema.js +10 -12
- package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
- package/dist/core/command-plugins/transport/http-invoke.js +14 -8
- package/dist/core/command.js +3 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +6 -29
- package/dist/core/events/read.js +1 -3
- package/dist/core/exclusive-lock.d.ts +14 -0
- package/dist/core/exclusive-lock.js +124 -0
- package/dist/core/fault-classifier.js +1 -3
- package/dist/core/fs-utils.d.ts +7 -0
- package/dist/core/fs-utils.js +29 -2
- package/dist/core/host-exports/export.d.ts +4 -5
- package/dist/core/host-exports/export.js +4 -5
- package/dist/core/human/claim.js +4 -2
- package/dist/core/human/convention.d.ts +0 -11
- package/dist/core/human/convention.js +1 -131
- package/dist/core/human/scan.js +5 -4
- package/dist/core/io.d.ts +23 -0
- package/dist/core/io.js +60 -0
- package/dist/core/preview-registry.d.ts +10 -5
- package/dist/core/preview-registry.js +136 -2537
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +58 -49
- package/dist/core/review/store.d.ts +4 -1
- package/dist/core/review/store.js +10 -0
- package/dist/core/review/types.d.ts +1 -1
- package/dist/core/review/types.js +1 -1
- package/dist/core/runtime/branded-host.js +6 -84
- package/dist/core/runtime/broker/frame-dispatch.js +4 -4
- package/dist/core/runtime/broker-cli.js +1 -1
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -10
- package/dist/core/runtime/broker-protocol.js +9 -10
- package/dist/core/runtime/broker-sdk.d.ts +1 -1
- package/dist/core/runtime/broker.js +6 -6
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/host.js +14 -14
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/lifecycle.js +16 -21
- package/dist/core/runtime/node-read.js +2 -3
- package/dist/core/runtime/package-health.js +1 -8
- package/dist/core/runtime/persona.js +2 -2
- package/dist/core/runtime/pi-vendored.d.ts +4 -2
- package/dist/core/runtime/pi-vendored.js +5 -16
- package/dist/core/runtime/promote.js +1 -1
- package/dist/core/runtime/reopen.js +2 -2
- package/dist/core/runtime/revive-all.d.ts +3 -3
- package/dist/core/runtime/revive-all.js +6 -6
- package/dist/core/runtime/revive.js +1 -1
- package/dist/core/runtime/spawn.d.ts +1 -2
- package/dist/core/runtime/spawn.js +2 -3
- package/dist/core/runtime/structured-output.js +4 -6
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/runtime/tmux-driver.d.ts +2 -2
- package/dist/core/runtime/tmux-driver.js +3 -5
- package/dist/core/runtime/tool-group-summary.js +5 -4
- package/dist/core/runtime/warm-pool.d.ts +9 -8
- package/dist/core/runtime/warm-pool.js +58 -62
- package/dist/core/scope.js +2 -9
- package/dist/core/self-update.js +3 -3
- package/dist/core/spawn.d.ts +0 -1
- package/dist/core/spawn.js +1 -4
- package/dist/core/substrate/index.d.ts +2 -1
- package/dist/core/substrate/index.js +2 -1
- package/dist/core/substrate/on-read-node.d.ts +7 -0
- package/dist/core/substrate/on-read-node.js +37 -0
- package/dist/core/substrate/on-read.d.ts +7 -5
- package/dist/core/substrate/on-read.js +16 -32
- package/dist/core/termrender/termrender.js +16 -96
- package/dist/core/wake.js +2 -2
- package/dist/core/worktree.d.ts +3 -3
- package/dist/core/worktree.js +16 -107
- package/dist/daemon/api/bridge.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +1 -2
- package/dist/daemon/api/handlers/messages.js +2 -2
- package/dist/daemon/api/handlers/nodes.js +33 -10
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/review-comments.js +5 -44
- package/dist/daemon/api/handlers/reviews.js +5 -53
- package/dist/daemon/api/handlers/validate.d.ts +23 -0
- package/dist/daemon/api/handlers/validate.js +58 -0
- package/dist/daemon/api/map.js +1 -0
- package/dist/daemon/cron-run.js +7 -7
- package/dist/daemon/human/finish.js +3 -2
- package/dist/daemon/manage.d.ts +6 -6
- package/dist/daemon/manage.js +15 -16
- package/dist/daemon/review/finish.js +1 -10
- package/dist/pi-extensions/canvas-context-intro.js +1 -14
- package/dist/pi-extensions/canvas-doc-substrate.js +2 -2
- package/dist/pi-extensions/canvas-preview-result.d.ts +22 -0
- package/dist/pi-extensions/canvas-preview-result.js +60 -0
- package/dist/pi-extensions/canvas-review-boundary.js +1 -14
- package/dist/pi-extensions/truncate.d.ts +5 -0
- package/dist/pi-extensions/truncate.js +14 -0
- package/dist/shared/predicates.d.ts +2 -0
- package/dist/shared/predicates.js +4 -0
- package/dist/shared/shell-quote.d.ts +3 -0
- package/dist/shared/shell-quote.js +5 -0
- package/dist/shared/tool-groups.d.ts +4 -2
- package/dist/shared/tool-groups.js +3 -3
- package/dist/types.d.ts +8 -5
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/core/substrate/on-read-pure.d.ts +0 -7
- 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
|
|
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 {
|
|
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.
|
|
98
|
-
*
|
|
99
|
-
|
|
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 =
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
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
|
-
|
|
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
|
|
321
|
-
*
|
|
322
|
-
const
|
|
323
|
-
/** Mint a spare for the most recently used
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
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
|
|
340
|
-
if (seen.has(
|
|
334
|
+
const profileId = row.profile_id ?? null;
|
|
335
|
+
if (seen.has(profileId ?? ''))
|
|
341
336
|
continue;
|
|
342
|
-
seen.add(
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
}
|
package/dist/core/scope.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import { existsSync, statSync
|
|
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;
|
package/dist/core/self-update.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Self-update and content-check primitives
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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';
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/spawn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Tmux reachability
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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 {
|
|
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, '&')
|
|
@@ -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
|
-
|
|
242
|
-
|
|
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
|
-
/**
|
|
259
|
-
*
|
|
260
|
-
export function
|
|
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
|
-
|
|
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,
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
7
|
-
// - nextSlotAfter — per-tick advance / coalescing (daemon
|
|
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
|
package/dist/core/worktree.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
102
|
-
* that never had anything to close in the first place (
|
|
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
|