@north-light/crouter 0.3.175 → 0.3.176
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/dto/nodes.d.ts +7 -1
- package/dist/api/dto/profiles.d.ts +3 -0
- 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/viewer.js +531 -561
- package/dist/commands/node/create.js +7 -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/sys/logs.js +4 -2
- package/dist/core/__tests__/branded-host-concurrency.test.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/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/command-plugins/transport/http-invoke.js +10 -2
- package/dist/core/command.js +3 -1
- package/dist/core/config.js +4 -27
- package/dist/core/exclusive-lock.d.ts +14 -0
- package/dist/core/exclusive-lock.js +124 -0
- package/dist/core/human/claim.js +2 -1
- package/dist/core/human/convention.d.ts +0 -10
- package/dist/core/human/convention.js +1 -123
- 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 +12 -1
- package/dist/core/profiles/manifest.js +57 -48
- package/dist/core/review/store.d.ts +4 -1
- package/dist/core/review/store.js +10 -0
- package/dist/core/runtime/branded-host.js +6 -84
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/runtime/warm-pool.d.ts +9 -8
- package/dist/core/runtime/warm-pool.js +58 -62
- 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 +15 -23
- package/dist/core/termrender/termrender.js +16 -96
- package/dist/core/worktree.js +11 -100
- package/dist/daemon/api/handlers/nodes.js +32 -9
- 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 +22 -0
- package/dist/daemon/api/handlers/validate.js +59 -0
- package/dist/daemon/api/map.js +1 -0
- package/dist/daemon/review/finish.js +1 -10
- package/dist/pi-extensions/canvas-doc-substrate.js +1 -1
- package/dist/pi-extensions/canvas-preview-result.d.ts +22 -0
- package/dist/pi-extensions/canvas-preview-result.js +60 -0
- package/dist/types.d.ts +4 -0
- 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
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { existsSync, linkSync, mkdirSync, readFileSync,
|
|
1
|
+
import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
3
|
import { buildSummary } from './summary.js';
|
|
5
4
|
export function deckPath(dir) { return `${dir}/deck.json`; }
|
|
6
5
|
export function reviewPath(dir) { return `${dir}/review.json`; }
|
|
@@ -62,127 +61,6 @@ export function readJson(path) {
|
|
|
62
61
|
return null;
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
|
-
function pause(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
66
|
-
// The holder's identity lives in the NAME of its single marker file. The marker's
|
|
67
|
-
// mtime is the heartbeat and its presence is ownership, so the observed token, the
|
|
68
|
-
// staleness clock, and the reclaim gate are one atomic fact.
|
|
69
|
-
const MARKER_PREFIX = 'owner.';
|
|
70
|
-
function markerPath(path, token) { return `${path}/${MARKER_PREFIX}${token}`; }
|
|
71
|
-
function observedMarkerToken(path) {
|
|
72
|
-
try {
|
|
73
|
-
return readdirSync(path).find((entry) => entry.startsWith(MARKER_PREFIX))?.slice(MARKER_PREFIX.length) ?? null;
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
function ownsLock(lock) { return existsSync(markerPath(lock.path, lock.token)); }
|
|
80
|
-
function releaseDirectoryLock(lock) { if (ownsLock(lock))
|
|
81
|
-
rmSync(lock.path, { recursive: true, force: true }); }
|
|
82
|
-
function lockAge(path, token) {
|
|
83
|
-
const target = token !== null ? markerPath(path, token) : path;
|
|
84
|
-
try {
|
|
85
|
-
return Date.now() - statSync(existsSync(target) ? target : path).mtimeMs;
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return 0;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// Reclamation is bound to the exact instance it observed: it unlinks ONLY that
|
|
92
|
-
// token's marker, then removes the now-empty directory with an empty-guarded
|
|
93
|
-
// rmdir. A successor lock holds a different random token (a different marker
|
|
94
|
-
// name), so a lagging reclaimer that saw the old stale lock can never strip a
|
|
95
|
-
// live successor — its unlink targets a name that no longer exists.
|
|
96
|
-
function reclaimIfStale(path, staleMs) {
|
|
97
|
-
const token = observedMarkerToken(path);
|
|
98
|
-
if (lockAge(path, token) <= staleMs)
|
|
99
|
-
return;
|
|
100
|
-
if (token !== null) {
|
|
101
|
-
try {
|
|
102
|
-
unlinkSync(markerPath(path, token));
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
if (error.code === 'ENOENT')
|
|
106
|
-
return;
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
rmdirSync(path);
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
const code = error.code;
|
|
115
|
-
if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'EEXIST')
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
function tryAcquireDirectoryLock(path, staleMs) {
|
|
120
|
-
const token = randomUUID();
|
|
121
|
-
try {
|
|
122
|
-
mkdirSync(path, { mode: 0o700 });
|
|
123
|
-
writeFileSync(markerPath(path, token), '', { flag: 'wx', mode: 0o600 });
|
|
124
|
-
return { path, token };
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
if (error.code !== 'EEXIST')
|
|
128
|
-
throw error;
|
|
129
|
-
reclaimIfStale(path, staleMs);
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
function acquireDirectoryLock(path, staleMs, timeoutMs) {
|
|
134
|
-
const startedAt = Date.now();
|
|
135
|
-
while (true) {
|
|
136
|
-
const lock = tryAcquireDirectoryLock(path, staleMs);
|
|
137
|
-
if (lock !== null)
|
|
138
|
-
return lock;
|
|
139
|
-
if (Date.now() - startedAt >= timeoutMs)
|
|
140
|
-
throw new Error('exclusive operation lock acquisition timed out');
|
|
141
|
-
pause(5);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async function acquireDirectoryLockAsync(path, staleMs, timeoutMs) {
|
|
145
|
-
const startedAt = Date.now();
|
|
146
|
-
while (true) {
|
|
147
|
-
const lock = tryAcquireDirectoryLock(path, staleMs);
|
|
148
|
-
if (lock !== null)
|
|
149
|
-
return lock;
|
|
150
|
-
if (Date.now() - startedAt >= timeoutMs)
|
|
151
|
-
throw new Error('exclusive operation lock acquisition timed out');
|
|
152
|
-
await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/** Runs a short filesystem transition under a token-checked, crash-reclaimable directory lock. */
|
|
156
|
-
export function withExclusiveDirectoryLock(path, operation, options = {}) {
|
|
157
|
-
const lock = acquireDirectoryLock(path, options.staleMs ?? 30_000, options.timeoutMs ?? 5_000);
|
|
158
|
-
try {
|
|
159
|
-
return operation();
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
releaseDirectoryLock(lock);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/** Async counterpart heartbeats while its operation runs, so a valid long handler is never stolen. */
|
|
166
|
-
export async function withExclusiveDirectoryLockAsync(path, operation, options = {}) {
|
|
167
|
-
const staleMs = options.staleMs ?? 35_000;
|
|
168
|
-
const lock = await acquireDirectoryLockAsync(path, staleMs, options.timeoutMs ?? staleMs + 5_000);
|
|
169
|
-
const heartbeat = setInterval(() => {
|
|
170
|
-
if (ownsLock(lock)) {
|
|
171
|
-
try {
|
|
172
|
-
utimesSync(markerPath(lock.path, lock.token), new Date(), new Date());
|
|
173
|
-
}
|
|
174
|
-
catch { /* a reclaimed lock is no longer ours */ }
|
|
175
|
-
}
|
|
176
|
-
}, 1_000);
|
|
177
|
-
try {
|
|
178
|
-
return await operation();
|
|
179
|
-
}
|
|
180
|
-
finally {
|
|
181
|
-
clearInterval(heartbeat);
|
|
182
|
-
releaseDirectoryLock(lock);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
// Kept for the existing panel until H2 routes all finalization through tickets.ts.
|
|
186
64
|
export function writeResponse(dir, responses, completedAt, deck) {
|
|
187
65
|
const summary = deck === undefined ? '' : buildSummary(deck, responses);
|
|
188
66
|
atomicWriteJson(responsePath(dir), { schema: 'humanloop.response/v2', kind: 'deck', responses, summary, completedAt });
|
package/dist/core/io.d.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { CrtrError } from './errors.js';
|
|
2
2
|
import { type ExitCodeValue } from '../types.js';
|
|
3
3
|
import { ApiError } from '../api/index.js';
|
|
4
|
+
/** The private result mirror for one crtr CLI invocation launched by the
|
|
5
|
+
* canvas bash-preview extension. This deliberately never enters stdout: bash
|
|
6
|
+
* text is model context, while Pi tool-result details are viewer-only. */
|
|
7
|
+
export interface CrtrPreviewRecord {
|
|
8
|
+
path: string;
|
|
9
|
+
result?: Record<string, unknown>;
|
|
10
|
+
jsonl?: Record<string, unknown>[];
|
|
11
|
+
error?: ErrorPayload;
|
|
12
|
+
}
|
|
13
|
+
/** Begin collecting the current leaf's structured response, if its caller
|
|
14
|
+
* supplied the private per-tool-call result path. */
|
|
15
|
+
export declare function beginPreview(path: string): void;
|
|
16
|
+
/** Publish the single-object or collected JSONL result after leaf dispatch. */
|
|
17
|
+
export declare function publishPreviewResult(result: Record<string, unknown> | void): void;
|
|
18
|
+
/** Mirror a structured record whose caller renders it itself instead of using
|
|
19
|
+
* the generic dispatcher (JSONL and event-stream leaves). */
|
|
20
|
+
export declare function recordPreviewLine(obj: Record<string, unknown>): void;
|
|
21
|
+
/** Preserve a verbatim JSONL stream's structured frames without changing the
|
|
22
|
+
* bytes written to stdout. Invalid third-party lines remain raw-only. */
|
|
23
|
+
export declare function recordPreviewJsonLine(line: string): void;
|
|
24
|
+
/** Record a terminal streaming failure that correctly remains off stdout
|
|
25
|
+
* because prior frames have already been delivered. */
|
|
26
|
+
export declare function recordPreviewError(payload: ErrorPayload): void;
|
|
4
27
|
/** Set by the dispatcher when `--json` is present anywhere in argv. */
|
|
5
28
|
export declare function setJsonOutput(v: boolean): void;
|
|
6
29
|
/** True when the caller asked for raw JSON instead of rendered prose. */
|
package/dist/core/io.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// the model, not data it parses); structured errors; stderr is diagnostics only
|
|
4
4
|
// and never carries the result. The raw JSON object is available behind the
|
|
5
5
|
// `--json` global for tooling. See the cli-design reference.
|
|
6
|
+
import { renameSync, writeFileSync } from 'node:fs';
|
|
6
7
|
import { CrtrError } from './errors.js';
|
|
7
8
|
import { ExitCode } from '../types.js';
|
|
8
9
|
import { renderError } from './render.js';
|
|
@@ -11,6 +12,62 @@ import { ApiError } from '../api/index.js';
|
|
|
11
12
|
// output mode — prose (default) vs raw JSON (--json global, for tooling)
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
let jsonOutput = false;
|
|
15
|
+
let preview;
|
|
16
|
+
/** Begin collecting the current leaf's structured response, if its caller
|
|
17
|
+
* supplied the private per-tool-call result path. */
|
|
18
|
+
export function beginPreview(path) {
|
|
19
|
+
const resultPath = process.env['CRTR_PREVIEW_RESULT_PATH'];
|
|
20
|
+
preview = resultPath === undefined || resultPath === '' ? undefined : { path, jsonl: [] };
|
|
21
|
+
}
|
|
22
|
+
function publishPreview(record) {
|
|
23
|
+
const resultPath = process.env['CRTR_PREVIEW_RESULT_PATH'];
|
|
24
|
+
if (resultPath === undefined || resultPath === '')
|
|
25
|
+
return;
|
|
26
|
+
const tempPath = `${resultPath}.${process.pid}.tmp`;
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(tempPath, `${JSON.stringify({ path: preview?.path ?? '', ...record })}\n`, 'utf8');
|
|
29
|
+
renameSync(tempPath, resultPath);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Preview transport is optional UI metadata. Its failure must never alter
|
|
33
|
+
// the CLI's stdout/error contract.
|
|
34
|
+
try {
|
|
35
|
+
writeFileSync(tempPath, '', 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch { /* nothing to clean */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Publish the single-object or collected JSONL result after leaf dispatch. */
|
|
41
|
+
export function publishPreviewResult(result) {
|
|
42
|
+
if (preview === undefined)
|
|
43
|
+
return;
|
|
44
|
+
if (result !== undefined && result !== null)
|
|
45
|
+
publishPreview({ result });
|
|
46
|
+
else if (preview.jsonl.length > 0 || preview.error !== undefined) {
|
|
47
|
+
publishPreview({ ...(preview.jsonl.length > 0 ? { jsonl: preview.jsonl } : {}), ...(preview.error === undefined ? {} : { error: preview.error }) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Mirror a structured record whose caller renders it itself instead of using
|
|
51
|
+
* the generic dispatcher (JSONL and event-stream leaves). */
|
|
52
|
+
export function recordPreviewLine(obj) {
|
|
53
|
+
preview?.jsonl.push(obj);
|
|
54
|
+
}
|
|
55
|
+
/** Preserve a verbatim JSONL stream's structured frames without changing the
|
|
56
|
+
* bytes written to stdout. Invalid third-party lines remain raw-only. */
|
|
57
|
+
export function recordPreviewJsonLine(line) {
|
|
58
|
+
try {
|
|
59
|
+
const value = JSON.parse(line);
|
|
60
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value))
|
|
61
|
+
recordPreviewLine(value);
|
|
62
|
+
}
|
|
63
|
+
catch { /* a malformed remote frame remains the command's raw truth */ }
|
|
64
|
+
}
|
|
65
|
+
/** Record a terminal streaming failure that correctly remains off stdout
|
|
66
|
+
* because prior frames have already been delivered. */
|
|
67
|
+
export function recordPreviewError(payload) {
|
|
68
|
+
if (preview !== undefined)
|
|
69
|
+
preview.error = payload;
|
|
70
|
+
}
|
|
14
71
|
/** Set by the dispatcher when `--json` is present anywhere in argv. */
|
|
15
72
|
export function setJsonOutput(v) {
|
|
16
73
|
jsonOutput = v;
|
|
@@ -88,6 +145,7 @@ export function emit(obj) {
|
|
|
88
145
|
}
|
|
89
146
|
/** One JSONL record. Call per event in a stream; partial reads stay parseable. */
|
|
90
147
|
export function emitLine(obj) {
|
|
148
|
+
recordPreviewLine(obj);
|
|
91
149
|
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
92
150
|
}
|
|
93
151
|
/**
|
|
@@ -234,6 +292,8 @@ export function handle(e) {
|
|
|
234
292
|
}
|
|
235
293
|
if (e instanceof CrtrError) {
|
|
236
294
|
const payload = payloadOf(e);
|
|
295
|
+
if (preview !== undefined)
|
|
296
|
+
publishPreview({ error: payload });
|
|
237
297
|
const out = jsonOutput
|
|
238
298
|
? JSON.stringify(payload, null, 2)
|
|
239
299
|
: renderError(payload);
|
|
@@ -23,8 +23,13 @@ export interface SummaryLine {
|
|
|
23
23
|
text: string;
|
|
24
24
|
tone?: SummaryTone;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
export interface CrtrPreview {
|
|
27
|
+
path: string;
|
|
28
|
+
result?: Record<string, unknown>;
|
|
29
|
+
jsonl?: Record<string, unknown>[];
|
|
30
|
+
error?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
/** Summarize an exact CLI result record. The recorded path is authoritative:
|
|
33
|
+
* shell tokenization is used only for an invocation's static icon/help chrome,
|
|
34
|
+
* never to identify or decode the returned leaf data. */
|
|
35
|
+
export declare function summarizePath(preview: CrtrPreview, tokens?: readonly string[]): SummaryLine[] | null;
|