@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
|
@@ -4,6 +4,15 @@ export declare function profilesRoot(): string;
|
|
|
4
4
|
export declare function profileRoot(profileId: string): string;
|
|
5
5
|
export declare function profileManifestPath(profileId: string): string;
|
|
6
6
|
export declare function profileMemoryDir(profileId: string): string;
|
|
7
|
+
/** The directory a node running under `profileId` is pinned to — the profile's
|
|
8
|
+
* home, but only while it still exists on disk (a node pinned to a deleted dir
|
|
9
|
+
* cannot boot). Null for the root profile, a profile with no projects, a home
|
|
10
|
+
* that has since been deleted, and an unresolvable id.
|
|
11
|
+
*
|
|
12
|
+
* This is the whole of the profile→cwd direction: cwd SELECTS a profile at the
|
|
13
|
+
* front door, and the selected profile then SUPPLIES the node's cwd. Never
|
|
14
|
+
* throws — it sits on the node-create path. */
|
|
15
|
+
export declare function profileHome(profileId: string | null): string | null;
|
|
7
16
|
export interface ProfileEntry {
|
|
8
17
|
profileId: string;
|
|
9
18
|
manifest: ProfileManifest;
|
|
@@ -18,7 +27,7 @@ export declare function listProfiles(): ProfileEntry[];
|
|
|
18
27
|
* actually-enumerated dirs, never a blind join), then a unique manifest
|
|
19
28
|
* `name` match. Ambiguous names fail listing every matching id; no match
|
|
20
29
|
* fails naming `profile list`/`profile new` as recovery. This is the ONLY
|
|
21
|
-
* function every command leaf and
|
|
30
|
+
* function every command leaf and runtime consumer should
|
|
22
31
|
* call to turn a `<profile>` operand or `CRTR_PROFILE_ID` into a concrete,
|
|
23
32
|
* safe profile id. */
|
|
24
33
|
export declare function loadProfileManifest(profileIdOrName: string): ProfileEntry;
|
|
@@ -30,6 +39,8 @@ export declare function ensureRootProfile(): ProfileEntry;
|
|
|
30
39
|
export declare function createProfile(name: string, projects?: string[]): ProfileEntry;
|
|
31
40
|
export declare function updateProfileLastUsed(profileId: string): ProfileEntry;
|
|
32
41
|
export declare function renameProfile(profileId: string, name: string): ProfileEntry;
|
|
33
|
-
export declare function addProfileProject(profileId: string, dir: string
|
|
42
|
+
export declare function addProfileProject(profileId: string, dir: string, opts?: {
|
|
43
|
+
home?: boolean;
|
|
44
|
+
}): ProfileEntry;
|
|
34
45
|
export declare function removeProfileProject(profileId: string, dir: string): ProfileEntry;
|
|
35
46
|
export declare function deleteProfile(profileId: string): void;
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
// here, so no caller ever passes a raw CLI operand or `CRTR_PROFILE_ID`
|
|
7
7
|
// straight into `join(profilesRoot(), value)`.
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
|
-
import { existsSync,
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
10
10
|
import { dirname, join, relative, resolve as resolvePath, sep } from 'node:path';
|
|
11
11
|
import { SCHEMA_VERSION, PROFILE_DIR } from '../../types.js';
|
|
12
12
|
import { userScopeRoot, resetScopeCache } from '../scope.js';
|
|
13
13
|
import { ensureDir, nowIso } from '../fs-utils.js';
|
|
14
14
|
import { usage, notFound, ambiguous, general } from '../errors.js';
|
|
15
|
+
import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Path safety — the only code in the tree that builds a path under the
|
|
17
18
|
// profiles root. Every other module reaches profile paths through this file.
|
|
@@ -73,11 +74,47 @@ export function profileMemoryDir(profileId) {
|
|
|
73
74
|
// a profile id. Both walk immediate children of the canonical root; neither
|
|
74
75
|
// ever joins an unvalidated operand into a path.
|
|
75
76
|
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
+
/** A profile's `home` DEFAULTS to its first project dir. Normalizing here (the
|
|
78
|
+
* single read path) rather than at write time means a manifest written before
|
|
79
|
+
* homes existed, and one whose explicit home was later removed from purview,
|
|
80
|
+
* both still answer with a usable home — no migration pass.
|
|
81
|
+
*
|
|
82
|
+
* The root profile is the exception and never takes the default: it is the
|
|
83
|
+
* identity a directory nothing covers falls back to, so its nodes belong where
|
|
84
|
+
* the user started them, not in whatever dir was last added to its purview. */
|
|
85
|
+
function normalizeHome(profileId, manifest) {
|
|
86
|
+
const stored = typeof manifest.home === 'string' && manifest.home !== '' ? manifest.home : null;
|
|
87
|
+
if (profileId === ROOT_PROFILE_ID)
|
|
88
|
+
return { ...manifest, home: stored };
|
|
89
|
+
return { ...manifest, home: stored ?? manifest.projects[0] ?? null };
|
|
90
|
+
}
|
|
91
|
+
function readManifestFile(path, profileId) {
|
|
77
92
|
if (!existsSync(path))
|
|
78
93
|
return null;
|
|
79
94
|
try {
|
|
80
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
95
|
+
return normalizeHome(profileId, JSON.parse(readFileSync(path, 'utf8')));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The directory a node running under `profileId` is pinned to — the profile's
|
|
102
|
+
* home, but only while it still exists on disk (a node pinned to a deleted dir
|
|
103
|
+
* cannot boot). Null for the root profile, a profile with no projects, a home
|
|
104
|
+
* that has since been deleted, and an unresolvable id.
|
|
105
|
+
*
|
|
106
|
+
* This is the whole of the profile→cwd direction: cwd SELECTS a profile at the
|
|
107
|
+
* front door, and the selected profile then SUPPLIES the node's cwd. Never
|
|
108
|
+
* throws — it sits on the node-create path. */
|
|
109
|
+
export function profileHome(profileId) {
|
|
110
|
+
if (profileId === null || profileId === '')
|
|
111
|
+
return null;
|
|
112
|
+
try {
|
|
113
|
+
const { manifest } = loadProfileManifest(profileId);
|
|
114
|
+
const home = manifest.home;
|
|
115
|
+
if (home === null)
|
|
116
|
+
return null;
|
|
117
|
+
return existsSync(home) && statSync(home).isDirectory() ? home : null;
|
|
81
118
|
}
|
|
82
119
|
catch {
|
|
83
120
|
return null;
|
|
@@ -99,7 +136,7 @@ export function listProfiles() {
|
|
|
99
136
|
const profileId = dirent.name;
|
|
100
137
|
if (!ID_SHAPE.test(profileId))
|
|
101
138
|
continue;
|
|
102
|
-
const manifest = readManifestFile(join(root, profileId, 'profile.json'));
|
|
139
|
+
const manifest = readManifestFile(join(root, profileId, 'profile.json'), profileId);
|
|
103
140
|
if (manifest === null)
|
|
104
141
|
continue;
|
|
105
142
|
out.push({ profileId, manifest });
|
|
@@ -110,7 +147,7 @@ export function listProfiles() {
|
|
|
110
147
|
* actually-enumerated dirs, never a blind join), then a unique manifest
|
|
111
148
|
* `name` match. Ambiguous names fail listing every matching id; no match
|
|
112
149
|
* fails naming `profile list`/`profile new` as recovery. This is the ONLY
|
|
113
|
-
* function every command leaf and
|
|
150
|
+
* function every command leaf and runtime consumer should
|
|
114
151
|
* call to turn a `<profile>` operand or `CRTR_PROFILE_ID` into a concrete,
|
|
115
152
|
* safe profile id. */
|
|
116
153
|
export function loadProfileManifest(profileIdOrName) {
|
|
@@ -142,55 +179,20 @@ export function loadProfileManifest(profileIdOrName) {
|
|
|
142
179
|
: 'No profiles exist on this host yet — create one with `crtr profile new --name <name>`.',
|
|
143
180
|
});
|
|
144
181
|
}
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
// Locking — a plain mkdir-based mutex under `profilesRoot()/.locks/`. mkdir is
|
|
147
|
-
// atomic (EEXIST when already held), so this needs no extra dependency. The
|
|
148
|
-
// blocking wait uses Atomics.wait on a throwaway SharedArrayBuffer to stay
|
|
149
|
-
// synchronous, matching every other sync fs call in this module.
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
const LOCK_TIMEOUT_MS = 5000;
|
|
152
|
-
const LOCK_POLL_MS = 25;
|
|
153
182
|
function locksDir() {
|
|
154
183
|
return join(profilesRoot(), '.locks');
|
|
155
184
|
}
|
|
156
|
-
function sleepSync(ms) {
|
|
157
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
158
|
-
}
|
|
159
185
|
/** Hold the per-profile manifest lock for the duration of `fn`. ALL manifest
|
|
160
186
|
* mutations (create/rename/add-project/remove-project/delete/last-used)
|
|
161
187
|
* below run inside this. */
|
|
162
188
|
export function withProfileManifestLock(profileId, fn) {
|
|
163
189
|
assertSafeProfileId(profileId);
|
|
164
190
|
ensureDir(locksDir());
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
if (err.code !== 'EEXIST')
|
|
174
|
-
throw err;
|
|
175
|
-
if (Date.now() > deadline) {
|
|
176
|
-
throw general(`timed out waiting for the profile lock: ${profileId}`, {
|
|
177
|
-
next: 'Retry — another `crtr profile` command may be running concurrently on this profile.',
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
sleepSync(LOCK_POLL_MS);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
return fn();
|
|
185
|
-
}
|
|
186
|
-
finally {
|
|
187
|
-
try {
|
|
188
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
/* best-effort release */
|
|
192
|
-
}
|
|
193
|
-
}
|
|
191
|
+
return withExclusiveDirectoryLock(join(locksDir(), `${profileId}.lock`), fn, {
|
|
192
|
+
timeoutError: () => general(`timed out waiting for the profile lock: ${profileId}`, {
|
|
193
|
+
next: 'Retry — another `crtr profile` command may be running concurrently on this profile.',
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
194
196
|
}
|
|
195
197
|
function writeManifestAtomic(path, manifest) {
|
|
196
198
|
ensureDir(dirname(path));
|
|
@@ -254,7 +256,7 @@ export function ensureRootProfile() {
|
|
|
254
256
|
return withProfileManifestLock(ROOT_PROFILE_ID, () => {
|
|
255
257
|
const manifestPath = profileManifestPath(ROOT_PROFILE_ID);
|
|
256
258
|
const manifestExists = existsSync(manifestPath);
|
|
257
|
-
const manifest = manifestExists ? readManifestFile(manifestPath) : null;
|
|
259
|
+
const manifest = manifestExists ? readManifestFile(manifestPath, ROOT_PROFILE_ID) : null;
|
|
258
260
|
if (manifestExists && manifest === null) {
|
|
259
261
|
throw general(`root profile manifest is unreadable: ${manifestPath}`);
|
|
260
262
|
}
|
|
@@ -267,6 +269,7 @@ export function ensureRootProfile() {
|
|
|
267
269
|
schema_version: SCHEMA_VERSION,
|
|
268
270
|
name: ROOT_PROFILE_NAME,
|
|
269
271
|
projects: [],
|
|
272
|
+
home: null,
|
|
270
273
|
created_at: nowIso(),
|
|
271
274
|
last_used_at: nowIso(),
|
|
272
275
|
};
|
|
@@ -286,6 +289,7 @@ export function createProfile(name, projects = []) {
|
|
|
286
289
|
schema_version: SCHEMA_VERSION,
|
|
287
290
|
name: trimmed,
|
|
288
291
|
projects: resolvedProjects,
|
|
292
|
+
home: resolvedProjects[0] ?? null,
|
|
289
293
|
created_at: nowIso(),
|
|
290
294
|
last_used_at: null,
|
|
291
295
|
};
|
|
@@ -307,7 +311,7 @@ function mutateManifest(profileId, mutate) {
|
|
|
307
311
|
requireExistingProfile(profileId);
|
|
308
312
|
return withProfileManifestLock(profileId, () => {
|
|
309
313
|
const path = profileManifestPath(profileId);
|
|
310
|
-
const current = readManifestFile(path);
|
|
314
|
+
const current = readManifestFile(path, profileId);
|
|
311
315
|
if (current === null) {
|
|
312
316
|
throw notFound(`profile not found: ${profileId}`, { received: profileId });
|
|
313
317
|
}
|
|
@@ -327,11 +331,12 @@ export function renameProfile(profileId, name) {
|
|
|
327
331
|
resetScopeCache();
|
|
328
332
|
return result;
|
|
329
333
|
}
|
|
330
|
-
export function addProfileProject(profileId, dir) {
|
|
334
|
+
export function addProfileProject(profileId, dir, opts = {}) {
|
|
331
335
|
const resolved = resolveExistingProjectDir(dir);
|
|
332
336
|
const result = mutateManifest(profileId, (m) => ({
|
|
333
337
|
...m,
|
|
334
338
|
projects: dedupeOrdered([...m.projects, resolved]),
|
|
339
|
+
...(opts.home === true ? { home: resolved } : {}),
|
|
335
340
|
}));
|
|
336
341
|
resetScopeCache();
|
|
337
342
|
return result;
|
|
@@ -351,13 +356,17 @@ export function removeProfileProject(profileId, dir) {
|
|
|
351
356
|
}
|
|
352
357
|
const result = mutateManifest(profileId, (m) => {
|
|
353
358
|
const filtered = m.projects.filter((p) => p !== real && p !== abs);
|
|
359
|
+
// Dropping the home dir from purview falls the home back to the first
|
|
360
|
+
// remaining project, exactly as `normalizeHome` would for a manifest that
|
|
361
|
+
// never named one — never leaves the profile pointing outside its purview.
|
|
362
|
+
const home = m.home === real || m.home === abs ? filtered[0] ?? null : m.home;
|
|
354
363
|
if (filtered.length === m.projects.length) {
|
|
355
364
|
throw notFound(`project not found on profile: ${dir}`, {
|
|
356
365
|
received: dir,
|
|
357
366
|
next: `Current projects: ${m.projects.length > 0 ? m.projects.join(', ') : '(none)'}.`,
|
|
358
367
|
});
|
|
359
368
|
}
|
|
360
|
-
return { ...m, projects: filtered };
|
|
369
|
+
return { ...m, projects: filtered, home };
|
|
361
370
|
});
|
|
362
371
|
resetScopeCache();
|
|
363
372
|
return result;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import type
|
|
2
|
+
import { type NewReviewRow, type ReviewRecord, type ReviewState } from './types.js';
|
|
3
3
|
/** Fields written when an open review is conditionally approved. */
|
|
4
4
|
export interface ApproveReviewArgs {
|
|
5
5
|
review_id: string;
|
|
@@ -17,6 +17,9 @@ export declare function commitBindingReview(row: NewReviewRow): {
|
|
|
17
17
|
won: boolean;
|
|
18
18
|
};
|
|
19
19
|
export declare function getReview(reviewId: string): ReviewRecord | null;
|
|
20
|
+
/** The review `reviewId` names, for a caller that cannot proceed without one.
|
|
21
|
+
* A `binding` row is still mid-creation and reads as absent. */
|
|
22
|
+
export declare function requireVisibleReview(reviewId: string): ReviewRecord;
|
|
20
23
|
export declare function getReviewByIdempotencyKey(key: string): ReviewRecord | null;
|
|
21
24
|
export declare function getReviewByCompanion(nodeId: string): ReviewRecord | null;
|
|
22
25
|
export declare function getReviewByBridge(nodeId: string): ReviewRecord | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { newNodeId } from '../runtime/nodes.js';
|
|
3
3
|
import { openDb, withCanvasWrite } from '../canvas/db.js';
|
|
4
|
+
import { ReviewOperationError, } from './types.js';
|
|
4
5
|
function recordFromRow(raw) {
|
|
5
6
|
return {
|
|
6
7
|
review_id: raw['review_id'],
|
|
@@ -79,6 +80,15 @@ export function commitBindingReview(row) {
|
|
|
79
80
|
export function getReview(reviewId) {
|
|
80
81
|
return readRecord(openDb(), 'review_id = ?', reviewId);
|
|
81
82
|
}
|
|
83
|
+
/** The review `reviewId` names, for a caller that cannot proceed without one.
|
|
84
|
+
* A `binding` row is still mid-creation and reads as absent. */
|
|
85
|
+
export function requireVisibleReview(reviewId) {
|
|
86
|
+
const review = getReview(reviewId);
|
|
87
|
+
if (review === null || review.state === 'binding') {
|
|
88
|
+
throw new ReviewOperationError('unknown_review', 'review was not found', { received: reviewId });
|
|
89
|
+
}
|
|
90
|
+
return review;
|
|
91
|
+
}
|
|
82
92
|
export function getReviewByIdempotencyKey(key) {
|
|
83
93
|
return readRecord(openDb(), 'idempotency_key = ?', key);
|
|
84
94
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal review/comment/error vocabulary and constants.
|
|
3
|
-
*
|
|
3
|
+
* Backs the daemon-owned review lifecycle and comment authority.
|
|
4
4
|
* This module declares no HTTP status, DTOs, or I/O.
|
|
5
5
|
*/
|
|
6
6
|
import type { FeedbackComment } from '../human/types.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal review/comment/error vocabulary and constants.
|
|
3
|
-
*
|
|
3
|
+
* Backs the daemon-owned review lifecycle and comment authority.
|
|
4
4
|
* This module declares no HTTP status, DTOs, or I/O.
|
|
5
5
|
*/
|
|
6
6
|
/** Limits */
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
// (a `brew upgrade node` moves realpath(execPath) to a new Cellar version) or the
|
|
22
22
|
// copy is missing. The drift check is a cheap marker compare — no subprocess, no
|
|
23
23
|
// copy — on the happy path.
|
|
24
|
-
import { copyFileSync, mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, readdirSync, renameSync, realpathSync, unlinkSync, rmSync,
|
|
24
|
+
import { copyFileSync, mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, readdirSync, renameSync, realpathSync, unlinkSync, rmSync, linkSync, } from 'node:fs';
|
|
25
25
|
import { homedir } from 'node:os';
|
|
26
26
|
import { dirname, join } from 'node:path';
|
|
27
27
|
import { spawnSync } from 'node:child_process';
|
|
28
28
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
29
29
|
import { emitEvent } from '../events/emit.js';
|
|
30
|
+
import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
|
|
30
31
|
/** The filename the FDA / TCC entry is shown under. */
|
|
31
32
|
const BRANDED_BIN_NAME = 'crouter';
|
|
32
33
|
/** Machine-global host dir. Deliberately NOT under `CRTR_HOME` (tests override
|
|
@@ -75,95 +76,16 @@ function readMarker(path = markerPath()) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
const HOST_LOCK_TIMEOUT_MS = 30_000;
|
|
78
|
-
const HOST_LOCK_POLL_MS = 25;
|
|
79
|
-
const UNOWNED_LOCK_GRACE_MS = 1_000;
|
|
80
|
-
function sleepSync(ms) {
|
|
81
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
82
|
-
}
|
|
83
|
-
function processIsAlive(pid) {
|
|
84
|
-
try {
|
|
85
|
-
process.kill(pid, 0);
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
return error.code === 'EPERM';
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function reclaimDeadHostLock(lockPath) {
|
|
93
|
-
let reclaimable = false;
|
|
94
|
-
try {
|
|
95
|
-
const owner = JSON.parse(readFileSync(join(lockPath, 'owner.json'), 'utf8'));
|
|
96
|
-
reclaimable = typeof owner.pid === 'number' && !processIsAlive(owner.pid);
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
try {
|
|
100
|
-
reclaimable = Date.now() - statSync(lockPath).mtimeMs > UNOWNED_LOCK_GRACE_MS;
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (!reclaimable)
|
|
107
|
-
return false;
|
|
108
|
-
const stalePath = `${lockPath}.stale.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
109
|
-
try {
|
|
110
|
-
renameSync(lockPath, stalePath);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
if (error.code === 'ENOENT')
|
|
114
|
-
return true;
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
rmSync(stalePath, { recursive: true, force: true });
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
79
|
/** Serialize reconciliation of the one machine-global host tuple. This lock is
|
|
121
80
|
* deliberately separate from the immutable runtime-generation install lock. */
|
|
122
81
|
function withBrandedHostLock(run) {
|
|
123
82
|
const root = join(homedir(), CRTR_DIR_NAME);
|
|
124
83
|
const lockPath = join(root, 'host.lock');
|
|
125
|
-
const token = `${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
126
|
-
const candidatePath = `${lockPath}.candidate.${token}`;
|
|
127
84
|
mkdirSync(root, { recursive: true });
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
// The owner record and lock become visible together, so another process
|
|
134
|
-
// can never mistake an in-progress live acquisition for an abandoned lock.
|
|
135
|
-
renameSync(candidatePath, lockPath);
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
const code = error.code;
|
|
140
|
-
if (code !== 'EEXIST' && code !== 'ENOTEMPTY') {
|
|
141
|
-
rmSync(candidatePath, { recursive: true, force: true });
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
if (reclaimDeadHostLock(lockPath))
|
|
145
|
-
continue;
|
|
146
|
-
if (Date.now() > deadline) {
|
|
147
|
-
rmSync(candidatePath, { recursive: true, force: true });
|
|
148
|
-
throw new Error(`timed out waiting for branded host lock: ${lockPath}`);
|
|
149
|
-
}
|
|
150
|
-
sleepSync(HOST_LOCK_POLL_MS);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
return run();
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
try {
|
|
158
|
-
const owner = JSON.parse(readFileSync(join(lockPath, 'owner.json'), 'utf8'));
|
|
159
|
-
if (owner.token === token)
|
|
160
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
/* A reclaimed/replaced lock is not ours to remove. */
|
|
164
|
-
}
|
|
165
|
-
rmSync(candidatePath, { recursive: true, force: true });
|
|
166
|
-
}
|
|
85
|
+
return withExclusiveDirectoryLock(lockPath, run, {
|
|
86
|
+
timeoutMs: HOST_LOCK_TIMEOUT_MS,
|
|
87
|
+
timeoutError: () => new Error(`timed out waiting for branded host lock: ${lockPath}`),
|
|
88
|
+
});
|
|
167
89
|
}
|
|
168
90
|
/** macOS keys FDA grants on the Mach-O cdhash. A real developer identity
|
|
169
91
|
* keeps that hash stable across branded-host rebuilds; ad-hoc signing is the
|
|
@@ -620,8 +620,8 @@ export function createFrameDispatchContext(deps) {
|
|
|
620
620
|
}
|
|
621
621
|
};
|
|
622
622
|
// -------------------------------------------------------------------------
|
|
623
|
-
// Command-op helpers
|
|
624
|
-
//
|
|
623
|
+
// Command-op helpers. The write guard is hoisted here and reused by all
|
|
624
|
+
// mutating ops; the ack/error replies
|
|
625
625
|
// and a few resolvers keep the per-op cases one-liners.
|
|
626
626
|
// -------------------------------------------------------------------------
|
|
627
627
|
/** Reject a read-only client for a mutating op. The decision reads this
|
|
@@ -841,7 +841,7 @@ export function createFrameDispatchContext(deps) {
|
|
|
841
841
|
};
|
|
842
842
|
const handleEngineControlFrame = (client, frame) => {
|
|
843
843
|
switch (frame.type) {
|
|
844
|
-
// --- extended engine-command ops
|
|
844
|
+
// --- extended engine-command ops -------------------------------------
|
|
845
845
|
case 'set_model': {
|
|
846
846
|
if (notWritable(client, 'set the model'))
|
|
847
847
|
break;
|
|
@@ -1070,7 +1070,7 @@ export function createFrameDispatchContext(deps) {
|
|
|
1070
1070
|
// engine, so it is not a controller-gated op. Terminal observer
|
|
1071
1071
|
// connections use it to populate the command palette.
|
|
1072
1072
|
// The merged command list rides in ack.detail as JSON (the foundation's
|
|
1073
|
-
// AckFrame.detail field) — the viewer
|
|
1073
|
+
// AckFrame.detail field) — the viewer JSON.parses it when for ===
|
|
1074
1074
|
// 'get_commands'. Keeps every command op a uniform ack reply.
|
|
1075
1075
|
try {
|
|
1076
1076
|
ackTo(client, 'get_commands', true, JSON.stringify(buildCommandList(currentSession())));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// broker-cli.ts — the headless broker's dedicated detached entry (plan T4 /
|
|
2
2
|
// decision §1.13). Spawned directly via `spawn(execPath, [thisFile, nodeId], …)`
|
|
3
|
-
// by HeadlessBrokerHost.launch
|
|
3
|
+
// by HeadlessBrokerHost.launch — NEVER routed through src/cli.ts, which
|
|
4
4
|
// would run the whole bootstrap chain (auto-update / scope-init / slash-template
|
|
5
5
|
// rewrite) on every broker boot. Mirrors src/daemon/crtrd-cli.ts: parse the one
|
|
6
6
|
// positional arg and hand off; keep this file a thin shim.
|
|
@@ -7,7 +7,7 @@ import { hostname } from 'node:os';
|
|
|
7
7
|
import { interpolateNodePaths } from '../canvas/paths.js';
|
|
8
8
|
import { fullName } from '../canvas/labels.js';
|
|
9
9
|
import { renderKnowledgeForSubject, renderPreferencesForSubject } from '../substrate/render.js';
|
|
10
|
-
import { renderWorkspaceOpenDocsForSubject } from '../substrate/on-read
|
|
10
|
+
import { renderWorkspaceOpenDocsForSubject } from '../substrate/on-read.js';
|
|
11
11
|
import { loadProfileManifest } from '../profiles/manifest.js';
|
|
12
12
|
export function editorLabelForBrokerNode(node) {
|
|
13
13
|
const icon = (node.icon ?? '').trim();
|
|
@@ -47,7 +47,7 @@ export interface BrokerSnapshot {
|
|
|
47
47
|
title: string | undefined;
|
|
48
48
|
};
|
|
49
49
|
/** Broker-owned summaries keyed by the first tool-call id in each group.
|
|
50
|
-
* Structured objects with bullets, nodesSpawned, and
|
|
50
|
+
* Structured objects with bullets, nodesSpawned, and filesDeleted.
|
|
51
51
|
* Optional so snapshots from an older live runtime remain readable. */
|
|
52
52
|
toolGroupSummaries?: Record<string, ToolGroupSummary>;
|
|
53
53
|
/** The broker-selected normal activity label for the current turn. Optional so
|
|
@@ -68,7 +68,7 @@ export interface HelloFrame {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
/** Drive the engine — writable (`controller`) clients only. Map 1:1 to session.prompt/steer/followUp/abort.
|
|
71
|
-
* `images` carries pasted/attached images to the engine
|
|
71
|
+
* `images` carries pasted/attached images to the engine: pi accepts
|
|
72
72
|
* `prompt(text,{images})` / `steer(text,images)` / `followUp(text,images)` at
|
|
73
73
|
* 0.78.1. The wire TYPE lives here; T3/T6 wire the runtime side. The BROKER read
|
|
74
74
|
* cap (`BROKER_READ_CAPS`) is sized to hold a resizeImage-bounded PNG's base64. */
|
|
@@ -612,15 +612,14 @@ export declare class FrameOverflowError extends Error {
|
|
|
612
612
|
}
|
|
613
613
|
/** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the
|
|
614
614
|
* full message history and can be many MiB, so these are generous. Covers
|
|
615
|
-
* realistic long sessions; snapshot CHUNKING is deferred
|
|
616
|
-
*
|
|
617
|
-
*
|
|
615
|
+
* realistic long sessions; snapshot CHUNKING is deferred — the long-lived
|
|
616
|
+
* broker makes a big welcome a realistic, not pathological, trigger, but the
|
|
617
|
+
* fixed cap is accepted. */
|
|
618
618
|
export declare const CLIENT_READ_CAPS: FrameDecoderCaps;
|
|
619
|
-
/** Caps the BROKER uses reading CLIENT frames.
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
* `follow_up` frame (M1). Still bounded, so a malicious/buggy client→broker frame
|
|
619
|
+
/** Caps the BROKER uses reading CLIENT frames. Sized so an image-paste frame
|
|
620
|
+
* fits: a resizeImage-bounded PNG's base64 is a few MiB and would clip a
|
|
621
|
+
* tighter 4 MiB line cap, so these hold an image-bearing `prompt`/`steer`/
|
|
622
|
+
* `follow_up` frame. Still bounded, so a malicious/buggy client→broker frame
|
|
624
623
|
* is cap-and-dropped, never grow-to-OOM (C5). */
|
|
625
624
|
export declare const BROKER_READ_CAPS: FrameDecoderCaps;
|
|
626
625
|
/** Incremental newline-delimited JSON reader: feed raw socket chunks, get back
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// broker-protocol.ts — the client↔broker wire protocol for the headless node
|
|
2
2
|
// Surface (design §5.2). Pure types + a newline-delimited JSON codec; no I/O, no
|
|
3
|
-
// SDK construction. Consumed by the broker (src/core/runtime/broker.ts)
|
|
4
|
-
//
|
|
3
|
+
// SDK construction. Consumed by the broker (src/core/runtime/broker.ts), the
|
|
4
|
+
// `crtr surface attach` terminal client, and crtrd's attach bridge.
|
|
5
5
|
//
|
|
6
6
|
// The transport is one unix socket per node (`nodeDir(id)/view.sock`) speaking
|
|
7
7
|
// newline-delimited JSON frames. Live engine events are relayed VERBATIM — the
|
|
@@ -43,18 +43,17 @@ export class FrameOverflowError extends Error {
|
|
|
43
43
|
}
|
|
44
44
|
/** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the
|
|
45
45
|
* full message history and can be many MiB, so these are generous. Covers
|
|
46
|
-
* realistic long sessions; snapshot CHUNKING is deferred
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* realistic long sessions; snapshot CHUNKING is deferred — the long-lived
|
|
47
|
+
* broker makes a big welcome a realistic, not pathological, trigger, but the
|
|
48
|
+
* fixed cap is accepted. */
|
|
49
49
|
export const CLIENT_READ_CAPS = {
|
|
50
50
|
maxLineBytes: 256 * 1024 * 1024,
|
|
51
51
|
maxTotalBytes: 256 * 1024 * 1024,
|
|
52
52
|
};
|
|
53
|
-
/** Caps the BROKER uses reading CLIENT frames.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* `follow_up` frame (M1). Still bounded, so a malicious/buggy client→broker frame
|
|
53
|
+
/** Caps the BROKER uses reading CLIENT frames. Sized so an image-paste frame
|
|
54
|
+
* fits: a resizeImage-bounded PNG's base64 is a few MiB and would clip a
|
|
55
|
+
* tighter 4 MiB line cap, so these hold an image-bearing `prompt`/`steer`/
|
|
56
|
+
* `follow_up` frame. Still bounded, so a malicious/buggy client→broker frame
|
|
58
57
|
* is cap-and-dropped, never grow-to-OOM (C5). */
|
|
59
58
|
export const BROKER_READ_CAPS = {
|
|
60
59
|
maxLineBytes: 24 * 1024 * 1024,
|
|
@@ -15,7 +15,7 @@ export interface BrokerEngine {
|
|
|
15
15
|
createAgentSessionServices: typeof createAgentSessionServices;
|
|
16
16
|
createAgentSessionFromServices: typeof createAgentSessionFromServices;
|
|
17
17
|
/**
|
|
18
|
-
* The session-replacement runtime factory (
|
|
18
|
+
* The session-replacement runtime factory (new_session/switch_session/fork).
|
|
19
19
|
* OPTIONAL: the real SDK (0.78.1) exposes it, so production gets full session
|
|
20
20
|
* replacement; the `fake-engine` test fixture does NOT provide it, so the
|
|
21
21
|
* broker degrades those three ops to an `error{engine_error}` reply rather than
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// broker.ts — the headless node broker (design §4, §5
|
|
1
|
+
// broker.ts — the headless node broker (design §4, §5).
|
|
2
2
|
//
|
|
3
3
|
// One broker process per node — the SOLE host. It hosts ONE pi engine IN-PROCESS
|
|
4
4
|
// via the SDK (createAgentSession), is the SOLE writer of the node's session
|
|
@@ -81,9 +81,9 @@ function formatExactModelSpec(model, thinkingLevel) {
|
|
|
81
81
|
return base === null ? null : `${base}:${thinkingLevel === undefined || thinkingLevel === '' ? 'off' : thinkingLevel}`;
|
|
82
82
|
}
|
|
83
83
|
// ---------------------------------------------------------------------------
|
|
84
|
-
// Tunables (
|
|
84
|
+
// Tunables (backpressure / dialog anti-deadlock)
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
86
|
-
/** Broker-side default dialog timeout (
|
|
86
|
+
/** Broker-side default dialog timeout (anti-deadlock). When an extension
|
|
87
87
|
* dialog is forwarded to every writable client, the broker ALWAYS arms a timeout
|
|
88
88
|
* (this default, or a shorter per-dialog `opts.timeout` if the extension passed
|
|
89
89
|
* one) so unanswered dialogs — including ones whose writable viewers all detach
|
|
@@ -167,7 +167,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
167
167
|
};
|
|
168
168
|
// Defensive: any `crtr` child the engine/extensions spawn must inherit the
|
|
169
169
|
// front-door recursion guard (see
|
|
170
|
-
// src/core/runtime/.crouter/memory/INDEX.md). The host
|
|
170
|
+
// src/core/runtime/.crouter/memory/INDEX.md). The host also sets this;
|
|
171
171
|
// setting it here keeps the broker self-sufficient.
|
|
172
172
|
process.env[FRONT_DOOR_ENV] = '1';
|
|
173
173
|
// A successful in-turn crtr command stamps this process id into its stop
|
|
@@ -203,7 +203,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
203
203
|
// 2–4. Build the engine session via the SERVICES path (C3) — see
|
|
204
204
|
// buildBrokerSession below. Register it so the FATAL exit path (M3) can
|
|
205
205
|
// dispose it and reap detached bash children.
|
|
206
|
-
// `session` + `services` are MUTABLE holders (
|
|
206
|
+
// `session` + `services` are MUTABLE holders (session-rebind): when the
|
|
207
207
|
// controller drives new_session/switch_session/fork, the AgentSessionRuntime
|
|
208
208
|
// tears down the old session and builds the next one, then runs our rebind
|
|
209
209
|
// callback which reassigns these. Every closure below (buildSnapshot,
|
|
@@ -411,7 +411,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
411
411
|
registry.forEachHelloed(sendWelcome);
|
|
412
412
|
};
|
|
413
413
|
// -------------------------------------------------------------------------
|
|
414
|
-
// The single exit helper
|
|
414
|
+
// The single exit helper: dispose the engine, close + unlink the
|
|
415
415
|
// socket, exit 0. Idempotent — every trigger converges here.
|
|
416
416
|
// -------------------------------------------------------------------------
|
|
417
417
|
const completeExit = (status) => {
|
|
@@ -9,6 +9,7 @@ export declare const CANVAS_TOOL_GUIDE_PATH: string;
|
|
|
9
9
|
export declare const CANVAS_DOC_SUBSTRATE_PATH: string;
|
|
10
10
|
export declare const CANVAS_STRUCTURED_OUTPUT_PATH: string;
|
|
11
11
|
export declare const CANVAS_BASH_VALVE_PATH: string;
|
|
12
|
+
export declare const CANVAS_PREVIEW_RESULT_PATH: string;
|
|
12
13
|
/** The canvas extensions every node loads, in order. The inbox watcher commits
|
|
13
14
|
* its cursor at agent_settled before the stophook checks it for idle release.
|
|
14
15
|
* The review boundary must run before the stophook commits companion session
|
|
@@ -21,6 +21,7 @@ export const CANVAS_TOOL_GUIDE_PATH = resolveExtension('canvas-tool-guide');
|
|
|
21
21
|
export const CANVAS_DOC_SUBSTRATE_PATH = resolveExtension('canvas-doc-substrate');
|
|
22
22
|
export const CANVAS_STRUCTURED_OUTPUT_PATH = resolveExtension('canvas-structured-output');
|
|
23
23
|
export const CANVAS_BASH_VALVE_PATH = resolveExtension('canvas-bash-valve');
|
|
24
|
+
export const CANVAS_PREVIEW_RESULT_PATH = resolveExtension('canvas-preview-result');
|
|
24
25
|
/** The canvas extensions every node loads, in order. The inbox watcher commits
|
|
25
26
|
* its cursor at agent_settled before the stophook checks it for idle release.
|
|
26
27
|
* The review boundary must run before the stophook commits companion session
|
|
@@ -37,6 +38,7 @@ export const CANVAS_EXTENSIONS = [
|
|
|
37
38
|
CANVAS_DOC_SUBSTRATE_PATH,
|
|
38
39
|
CANVAS_STRUCTURED_OUTPUT_PATH,
|
|
39
40
|
CANVAS_BASH_VALVE_PATH,
|
|
41
|
+
CANVAS_PREVIEW_RESULT_PATH,
|
|
40
42
|
];
|
|
41
43
|
/** Preserve the invariant: mandatory extensions are always present exactly once.
|
|
42
44
|
*
|