@north-light/crouter 0.3.181 → 0.3.183
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/build-root.js +10 -3
- package/dist/builtin-memory/internal/memory-loading.md +3 -1
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/__tests__/titled-editor-paste.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/titled-editor-paste.test.js +114 -0
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +4 -1
- package/dist/clients/attach/input/titled-editor.d.ts +18 -0
- package/dist/clients/attach/input/titled-editor.js +62 -0
- package/dist/clients/attach/render/chat-view.d.ts +3 -0
- package/dist/clients/attach/render/chat-view.js +15 -6
- package/dist/clients/attach/render/context-message.js +2 -2
- package/dist/clients/attach/render/group-activity.d.ts +18 -1
- package/dist/clients/attach/render/group-activity.js +54 -6
- package/dist/clients/attach/render/group-recap.js +13 -2
- package/dist/clients/attach/render/{markdown-headings.d.ts → markdown-source.d.ts} +4 -4
- package/dist/clients/attach/render/markdown-source.js +165 -0
- package/dist/clients/attach/session/bindings.js +5 -2
- package/dist/clients/attach/viewer.js +396 -394
- package/dist/clients/inbox/review/keys.js +2 -9
- package/dist/clients/inbox/tui/keys.js +2 -9
- package/dist/commands/cron.js +1 -1
- package/dist/commands/memory/delete.js +1 -1
- package/dist/commands/memory/find.js +1 -1
- package/dist/commands/memory/lint.js +28 -3
- package/dist/commands/memory/list.js +1 -1
- package/dist/commands/memory/origin.js +1 -1
- package/dist/commands/memory/read.js +4 -4
- package/dist/commands/memory/write.js +1 -1
- package/dist/commands/sys/settings-shell.d.ts +0 -3
- package/dist/commands/sys/settings-shell.js +1 -38
- package/dist/commands/sys/settings.js +2 -3
- package/dist/commands/sys/setup-core.d.ts +2 -17
- package/dist/commands/sys/setup-core.js +9 -33
- package/dist/commands/sys/setup-wizard.d.ts +1 -3
- package/dist/commands/sys/setup-wizard.js +9 -39
- package/dist/commands/sys/setup.js +6 -6
- package/dist/commands/sys/sync-project-guidance.js +62 -12
- package/dist/core/__tests__/fault-classifier.test.js +20 -0
- package/dist/core/__tests__/nested-store-discovery.test.d.ts +1 -0
- package/dist/core/__tests__/nested-store-discovery.test.js +89 -0
- package/dist/core/__tests__/on-read-nested-store.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +117 -0
- package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.d.ts +1 -0
- package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.js +38 -0
- package/dist/core/__tests__/serial/tmux-surface.test.js +8 -5
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +34 -1
- package/dist/core/command.js +5 -3
- package/dist/core/fault-classifier.d.ts +1 -1
- package/dist/core/fault-classifier.js +7 -0
- package/dist/core/fs-utils.js +6 -1
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/index.js +1 -1
- package/dist/core/keybindings/match.d.ts +11 -0
- package/dist/core/keybindings/match.js +22 -0
- package/dist/core/memory/doc-link-grammar.js +3 -7
- package/dist/core/memory/inline-ref-guidance.js +1 -1
- package/dist/core/memory-resolver.d.ts +7 -1
- package/dist/core/memory-resolver.js +20 -7
- package/dist/core/nested-stores.d.ts +12 -0
- package/dist/core/nested-stores.js +146 -0
- package/dist/core/preview-registry.js +4 -2
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +2 -2
- package/dist/core/runtime/broker/fault-retry.js +23 -4
- package/dist/core/runtime/broker/inbox.d.ts +4 -0
- package/dist/core/runtime/broker/inbox.js +17 -8
- package/dist/core/runtime/broker-extension-render.d.ts +1 -1
- package/dist/core/runtime/broker-extension-render.js +2 -2
- package/dist/core/runtime/broker-protocol.d.ts +1 -1
- package/dist/core/runtime/deliver-live.js +11 -1
- package/dist/core/runtime/fault.js +1 -1
- package/dist/core/runtime/tmux-bindings.js +42 -20
- package/dist/core/runtime/tool-group-summary.js +2 -6
- package/dist/core/substrate/injected-store.d.ts +6 -0
- package/dist/core/substrate/injected-store.js +19 -0
- package/dist/core/substrate/on-read-node.d.ts +4 -2
- package/dist/core/substrate/on-read-node.js +5 -3
- package/dist/core/substrate/on-read.d.ts +4 -2
- package/dist/core/substrate/on-read.js +27 -5
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -1
- package/dist/pi-extensions/canvas-context-intro.js +14 -3
- package/dist/pi-extensions/canvas-doc-substrate.js +13 -2
- package/dist/pi-extensions/canvas-inbox-watcher.js +34 -1
- package/dist/shared/generated-context.d.ts +4 -0
- package/dist/shared/generated-context.js +20 -1
- package/dist/shared/tool-groups.d.ts +3 -3
- package/dist/shared/tool-groups.js +1 -4
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/postinstall.mjs +1 -1
- package/dist/clients/attach/render/markdown-headings.js +0 -93
- package/dist/commands/search/answer.d.ts +0 -1
- package/dist/commands/search/answer.js +0 -50
- package/dist/commands/search/contents.d.ts +0 -1
- package/dist/commands/search/contents.js +0 -96
- package/dist/commands/search/exa.d.ts +0 -53
- package/dist/commands/search/exa.js +0 -157
- package/dist/commands/search/puremd.d.ts +0 -11
- package/dist/commands/search/puremd.js +0 -53
- package/dist/commands/search/web.d.ts +0 -1
- package/dist/commands/search/web.js +0 -65
- package/dist/commands/search.d.ts +0 -2
- package/dist/commands/search.js +0 -24
- package/dist/commands/sys/panels/keys-panel.d.ts +0 -16
- package/dist/commands/sys/panels/keys-panel.js +0 -60
|
@@ -4,7 +4,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { FaultRetry } from '../runtime/broker/fault-retry.js';
|
|
7
|
-
import { readFault } from '../runtime/fault.js';
|
|
7
|
+
import { readFault, recordFault } from '../runtime/fault.js';
|
|
8
8
|
let home;
|
|
9
9
|
before(() => {
|
|
10
10
|
home = mkdtempSync(join(tmpdir(), 'crtr-watchdog-abort-retry-'));
|
|
@@ -48,3 +48,36 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
|
|
|
48
48
|
settleAbort('human-node', false);
|
|
49
49
|
assert.equal(readFault('human-node'), null, 'an otherwise-identical human abort must not arm recovery');
|
|
50
50
|
});
|
|
51
|
+
test('a failed retry settlement carries the episode forward instead of restarting it', () => {
|
|
52
|
+
// The episode marker a daemon retry records before re-prompting: original
|
|
53
|
+
// anchor, start time, and the attempt just consumed.
|
|
54
|
+
recordFault('retry-episode-node', {
|
|
55
|
+
link: 'pi→provider', op: 'generation turn', kind: 'connection',
|
|
56
|
+
retry: { disposition: 'auto', by: 'daemon', attempt: 2, max: 8 },
|
|
57
|
+
message: 'connection reset', since: '2026-01-01T00:00:00.000Z', anchorEntryId: 'original-anchor',
|
|
58
|
+
});
|
|
59
|
+
const session = {
|
|
60
|
+
sessionManager: { getLeafId: () => 'leaf-after-failed-retry' },
|
|
61
|
+
model: undefined,
|
|
62
|
+
};
|
|
63
|
+
const generation = {
|
|
64
|
+
id: 1,
|
|
65
|
+
session,
|
|
66
|
+
fallback: { blocked: false },
|
|
67
|
+
stagedProviderAgentEnd: {
|
|
68
|
+
messages: [{ role: 'assistant', stopReason: 'error', errorMessage: 'fetch failed' }],
|
|
69
|
+
},
|
|
70
|
+
stagedWatchdogAbort: false,
|
|
71
|
+
stagedOverflowFailure: null,
|
|
72
|
+
executingTools: new Set(),
|
|
73
|
+
};
|
|
74
|
+
new FaultRetry({
|
|
75
|
+
nodeId: 'retry-episode-node',
|
|
76
|
+
currentSession: () => session,
|
|
77
|
+
installedGeneration: () => generation,
|
|
78
|
+
}).publishSettledProviderFault(generation);
|
|
79
|
+
const fault = readFault('retry-episode-node');
|
|
80
|
+
assert.equal(fault?.anchorEntryId, 'original-anchor', 'each retry must rewind to the original fork point, not stack prompts');
|
|
81
|
+
assert.equal(fault?.since, '2026-01-01T00:00:00.000Z', 'the episode start survives a failed retry');
|
|
82
|
+
assert.equal(fault?.retry.attempt, 2, 'the attempt count accumulates toward exhaustion');
|
|
83
|
+
});
|
package/dist/core/command.js
CHANGED
|
@@ -580,9 +580,11 @@ export async function parseArgv(params, tokens, options) {
|
|
|
580
580
|
export async function runCli(root, argv) {
|
|
581
581
|
// argv is process.argv — strip node binary + script path. `--json` is a
|
|
582
582
|
// global: pull it out anywhere it appears so the rest of argv parses against
|
|
583
|
-
// the leaf schema unchanged, and switch stdout from rendered prose to
|
|
584
|
-
//
|
|
585
|
-
// agent
|
|
583
|
+
// the leaf schema unchanged, and switch stdout from rendered prose to the
|
|
584
|
+
// leaf's declared `outputs` object as raw JSON. Prose stays the contract for
|
|
585
|
+
// an agent READING output; --json is the documented channel (root -h globals)
|
|
586
|
+
// for scripts — a cron's bash, external tooling — that must branch on canvas
|
|
587
|
+
// state and would otherwise scrape markdown.
|
|
586
588
|
// `--no-autostart` is likewise a global: it suppresses cold-socket daemon
|
|
587
589
|
// autostart (read from `process.argv` in `cliClient`, spec §7.1) and applies
|
|
588
590
|
// to every API-backed verb, so strip it here rather than declaring it on each
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ErrorClass, OperationId } from './events/types.js';
|
|
2
|
-
export type FaultLink = 'pi→provider' | 'viewer↔broker' | 'relay↔broker' | 'viewer↔crtrd' | '
|
|
2
|
+
export type FaultLink = 'pi→provider' | 'viewer↔broker' | 'relay↔broker' | 'viewer↔crtrd' | 'daemon→node' | 'crtr→pi';
|
|
3
3
|
export type FaultKind = 'rate-limit' | 'overloaded' | 'connection' | 'auth' | 'protocol' | 'context-overflow' | 'other' | 'wedged' | 'model-not-found';
|
|
4
4
|
export type FaultRetryDisposition = 'auto' | 'manual' | 'fatal';
|
|
5
5
|
export type FaultRetryOwner = 'sdk' | 'daemon' | 'client';
|
|
@@ -27,6 +27,13 @@ function classifyProviderText(message) {
|
|
|
27
27
|
// must not become a retryable transport interruption.
|
|
28
28
|
if (/rate.?limit|\b429\b|too many requests|quota/i.test(m))
|
|
29
29
|
return 'rate-limit';
|
|
30
|
+
// A body the other side refuses for SIZE is the request itself being
|
|
31
|
+
// unacceptable, never a transient server condition: the retry re-posts the
|
|
32
|
+
// same conversation and fails identically, forever. This must precede the
|
|
33
|
+
// overloaded rule, which otherwise claims a size rejection wrapped in 5xx
|
|
34
|
+
// prose ("500 ... request entity too large") and arms an unwinnable retry.
|
|
35
|
+
if (/\b413\b|payload too large|entity too large|request too large|entity\.too\.large|body exceeds/i.test(m))
|
|
36
|
+
return 'protocol';
|
|
30
37
|
if (/invalid_grant|refresh token|unauthori[sz]ed|invalid.?api.?key|authentication failed|\b401\b|\b403\b/i.test(m))
|
|
31
38
|
return 'auth';
|
|
32
39
|
// pi compacts and retries a raw provider overflow. Its failed-recovery result
|
package/dist/core/fs-utils.js
CHANGED
|
@@ -124,7 +124,12 @@ export function linkOrCopy(target, linkPath, opts = {}) {
|
|
|
124
124
|
removePath(linkPath);
|
|
125
125
|
const isWindows = platform() === 'win32';
|
|
126
126
|
if (!opts.noSymlink && !isWindows) {
|
|
127
|
-
|
|
127
|
+
// The kernel resolves a relative link against the link's *physical*
|
|
128
|
+
// directory, so both ends must be realpaths: computing `..` hops from a
|
|
129
|
+
// lexical path whose ancestor is itself a symlink (macOS /tmp →
|
|
130
|
+
// /private/tmp) walks up the wrong chain and silently creates a dangling
|
|
131
|
+
// link, since symlinkSync never checks the target.
|
|
132
|
+
const rel = relative(realpathOrSelf(dirname(linkPath)), realpathOrSelf(target));
|
|
128
133
|
try {
|
|
129
134
|
symlinkSync(rel, linkPath, isDir(target) ? 'dir' : 'file');
|
|
130
135
|
return 'symlink';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { BINDING_CATALOG, BINDING_IDS, SAFETY_REQUIREMENTS, isAttachPaneBinding, type BindingId, } from './catalog.js';
|
|
2
2
|
export { GestureSyntaxError, KeybindingValidationError, normalizeGesture, normalizeSparseOverrides, normalizeStroke, resolveKeybindings, resolveUserKeybindingSettings, resolveUserKeybindings, splitGestureExpression, type UserKeybindingSettings, } from './resolve.js';
|
|
3
|
-
export { canonicalTerminalStroke, formatBinding, formatGesture, formatStroke, matchesPiTuiInput, matchesTerminalInput, type PiTuiMatcher, type RawTerminalInput, } from './match.js';
|
|
3
|
+
export { canonicalTerminalStroke, formatBinding, formatGesture, formatStroke, matchesPiTuiInput, matchesTerminalInput, normalizeShiftedMeta, type PiTuiMatcher, type RawTerminalInput, } from './match.js';
|
|
4
4
|
export { inboxOpenHint, inboxShortcut, } from './inbox.js';
|
|
5
5
|
export { backgroundBashHint, backgroundBashShortcut, } from './background-bash.js';
|
|
6
6
|
export { ATTACH_CONTROL_BINDINGS, decodeAttachControlInput, encodeAttachControlInput, extractAttachControlInput, type AttachBindingId, type AttachMenuBindingId, type ExtractedAttachControlInput, } from './attach-control.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { BINDING_CATALOG, BINDING_IDS, SAFETY_REQUIREMENTS, isAttachPaneBinding, } from './catalog.js';
|
|
2
2
|
export { GestureSyntaxError, KeybindingValidationError, normalizeGesture, normalizeSparseOverrides, normalizeStroke, resolveKeybindings, resolveUserKeybindingSettings, resolveUserKeybindings, splitGestureExpression, } from './resolve.js';
|
|
3
|
-
export { canonicalTerminalStroke, formatBinding, formatGesture, formatStroke, matchesPiTuiInput, matchesTerminalInput, } from './match.js';
|
|
3
|
+
export { canonicalTerminalStroke, formatBinding, formatGesture, formatStroke, matchesPiTuiInput, matchesTerminalInput, normalizeShiftedMeta, } from './match.js';
|
|
4
4
|
export { inboxOpenHint, inboxShortcut, } from './inbox.js';
|
|
5
5
|
export { backgroundBashHint, backgroundBashShortcut, } from './background-bash.js';
|
|
6
6
|
export { ATTACH_CONTROL_BINDINGS, decodeAttachControlInput, encodeAttachControlInput, extractAttachControlInput, } from './attach-control.js';
|
|
@@ -26,4 +26,15 @@ export interface RawTerminalInput {
|
|
|
26
26
|
export declare function canonicalTerminalStroke(raw: RawTerminalInput): string | null;
|
|
27
27
|
export declare function matchesTerminalInput(bindingSet: BindingResolution<BindingId>, id: BindingId, raw: RawTerminalInput): boolean;
|
|
28
28
|
export type PiTuiMatcher<Input> = (input: Input, gesture: string) => boolean;
|
|
29
|
+
/** Rewrite the two Alt+Shift+<letter> encodings pi-tui does not recognize into
|
|
30
|
+
* the canonical shift+alt CSI-u sequence it does.
|
|
31
|
+
*
|
|
32
|
+
* A terminal that sends Option as a meta prefix emits Alt+Shift+M as the legacy
|
|
33
|
+
* two-byte `ESC M`, and tmux re-encodes that as CSI-u carrying the UPPERCASE
|
|
34
|
+
* codepoint with only the alt bit set (`ESC [ 77 ; 3 u`). pi-tui matches
|
|
35
|
+
* `alt+shift+m` against `ESC [ 109 ; 4 u` / `ESC [ 77 ; 4 u` and rejects both
|
|
36
|
+
* unshifted forms, so every Alt+Shift chord would be dead on those terminals.
|
|
37
|
+
* An uppercase letter cannot be typed without Shift, so restoring the bit adds
|
|
38
|
+
* nothing the keyboard did not already carry. Anything else passes through. */
|
|
39
|
+
export declare function normalizeShiftedMeta(data: string): string;
|
|
29
40
|
export declare function matchesPiTuiInput<Input>(bindingSet: BindingResolution<BindingId>, id: BindingId, input: Input, matchesKey: PiTuiMatcher<Input>): boolean;
|
|
@@ -100,6 +100,28 @@ export function matchesTerminalInput(bindingSet, id, raw) {
|
|
|
100
100
|
const stroke = canonicalTerminalStroke(raw);
|
|
101
101
|
return stroke !== null && bindingSet.gestures(id).includes(stroke);
|
|
102
102
|
}
|
|
103
|
+
/** Rewrite the two Alt+Shift+<letter> encodings pi-tui does not recognize into
|
|
104
|
+
* the canonical shift+alt CSI-u sequence it does.
|
|
105
|
+
*
|
|
106
|
+
* A terminal that sends Option as a meta prefix emits Alt+Shift+M as the legacy
|
|
107
|
+
* two-byte `ESC M`, and tmux re-encodes that as CSI-u carrying the UPPERCASE
|
|
108
|
+
* codepoint with only the alt bit set (`ESC [ 77 ; 3 u`). pi-tui matches
|
|
109
|
+
* `alt+shift+m` against `ESC [ 109 ; 4 u` / `ESC [ 77 ; 4 u` and rejects both
|
|
110
|
+
* unshifted forms, so every Alt+Shift chord would be dead on those terminals.
|
|
111
|
+
* An uppercase letter cannot be typed without Shift, so restoring the bit adds
|
|
112
|
+
* nothing the keyboard did not already carry. Anything else passes through. */
|
|
113
|
+
export function normalizeShiftedMeta(data) {
|
|
114
|
+
if (data.length === 2 && data[0] === '\x1b' && /[A-Z]/.test(data[1])) {
|
|
115
|
+
return `\x1b[${data.charCodeAt(1)};4u`;
|
|
116
|
+
}
|
|
117
|
+
const altOnlyCsiU = /^\x1b\[(\d+);3u$/.exec(data);
|
|
118
|
+
if (altOnlyCsiU !== null) {
|
|
119
|
+
const codepoint = Number(altOnlyCsiU[1]);
|
|
120
|
+
if (codepoint >= 0x41 && codepoint <= 0x5a)
|
|
121
|
+
return `\x1b[${codepoint};4u`;
|
|
122
|
+
}
|
|
123
|
+
return data;
|
|
124
|
+
}
|
|
103
125
|
export function matchesPiTuiInput(bindingSet, id, input, matchesKey) {
|
|
104
126
|
return bindingSet.gestures(id).some((gesture) => !gesture.includes(' > ') && matchesKey(input, gesture));
|
|
105
127
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
// doc-link-grammar.ts —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// rather than re-implementing the bracket scan or the name shape. It ships to
|
|
5
|
-
// external consumers as `@north-light/crouter-api/doc-link-grammar`, so a
|
|
6
|
-
// browser UI rendering a node's transcript scans with the same grammar crtrd
|
|
7
|
-
// validates `/v1/memory/resolve` names with.
|
|
1
|
+
// doc-link-grammar.ts — crouter's parser for `[[canonical/name]]` memory links.
|
|
2
|
+
// Memory commands and the daemon's resolve endpoint share this module for the
|
|
3
|
+
// bracket scan and canonical-name shape.
|
|
8
4
|
//
|
|
9
5
|
// A doc link is a durable cross-reference written INSIDE a memory document's
|
|
10
6
|
// body, pointing at another memory document by its exact canonical name (the
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// returned string in a `{ role: 'custom', customType: 'crtr-memory-refs',
|
|
10
10
|
// display: false, ... }` message and delivers it alongside the human turn.
|
|
11
11
|
import { findRefTokens } from './inline-ref-grammar.js';
|
|
12
|
-
const GUIDANCE_HEADER = 'The
|
|
12
|
+
const GUIDANCE_HEADER = 'The user explicitly called out specific memories. Read the relevant mentioned memory files:';
|
|
13
13
|
/**
|
|
14
14
|
* Resolve every slash token in `text` against `names` (a
|
|
15
15
|
* `buildRefInventory().names` set), collapse duplicates, and return one
|
|
@@ -57,6 +57,12 @@ export interface MemoryResolutionOpts {
|
|
|
57
57
|
* matching-kind doc into a false not_found — resolution simply continues
|
|
58
58
|
* to the next source. See `effectiveDocKind` for the kind coercion. */
|
|
59
59
|
kind?: string;
|
|
60
|
+
/** Also consult `.crouter/memory` stores discovered BELOW the mounted
|
|
61
|
+
* project roots (bounded discovery — see nested-stores.ts). Opt-in for the
|
|
62
|
+
* addressing surfaces (`memory read/list/find/lint/delete/origin`); OFF for
|
|
63
|
+
* boot render, on-read resolvedDocs, the slash-command snapshot, and
|
|
64
|
+
* persona resolution, which stay on the flat ancestor+profile stack. */
|
|
65
|
+
includeDescendants?: boolean;
|
|
60
66
|
}
|
|
61
67
|
/** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
|
|
62
68
|
export declare function memoryDocId(doc: MemoryDoc): string;
|
|
@@ -98,7 +104,7 @@ export declare function listProjectMemoryDocs(startDir?: string, profileId?: str
|
|
|
98
104
|
* profile's memory (if any), then user, then builtin. Within each source,
|
|
99
105
|
* native docs are emitted before enabled-plugin docs, so native wins on the
|
|
100
106
|
* caller's first-wins dedup. */
|
|
101
|
-
export declare function listAllMemoryDocs(scope?: MemoryScope, quiet?: boolean): MemoryDoc[];
|
|
107
|
+
export declare function listAllMemoryDocs(scope?: MemoryScope, quiet?: boolean, includeDescendants?: boolean): MemoryDoc[];
|
|
102
108
|
export interface MemoryDocSnapshot {
|
|
103
109
|
/** Every document in default scope precedence order, loaded once. */
|
|
104
110
|
docs: readonly MemoryDoc[];
|
|
@@ -10,6 +10,7 @@ import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from './scope.js';
|
|
|
10
10
|
import { effectiveDocKind, normalizeDocName, normalizeNameSegment, resolveDocName } from './substrate/schema.js';
|
|
11
11
|
import { loadProfileManifest, profileMemoryDir } from './profiles/manifest.js';
|
|
12
12
|
import { memoryDir as nodeMemoryDir } from './runtime/memory.js';
|
|
13
|
+
import { descendantStoreRoots } from './nested-stores.js';
|
|
13
14
|
/** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
|
|
14
15
|
export function memoryDocId(doc) {
|
|
15
16
|
return `${doc.scope}/${doc.name}`;
|
|
@@ -55,17 +56,29 @@ function scopesInPrecedence(target, scope) {
|
|
|
55
56
|
* join — and drops out silently (never throws the resolver) when the
|
|
56
57
|
* selected profile id no longer resolves to a manifest. User and builtin
|
|
57
58
|
* remain singleton scopes after that. */
|
|
58
|
-
function memorySourcesInPrecedence(target, scope) {
|
|
59
|
+
function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
|
|
59
60
|
const out = [];
|
|
60
61
|
for (const s of scopesInPrecedence(target, scope)) {
|
|
61
62
|
if (s === 'project') {
|
|
62
|
-
|
|
63
|
+
const roots = projectScopeRoots(target.cwd, target.profileId);
|
|
64
|
+
for (const root of roots) {
|
|
63
65
|
out.push({
|
|
64
66
|
scope: 'project',
|
|
65
67
|
memoryDir: join(root, 'memory'),
|
|
66
68
|
plugins: listInstalledPluginsInRoot('project', root).filter((p) => p.enabled),
|
|
67
69
|
});
|
|
68
70
|
}
|
|
71
|
+
// Descendant stores rank after every ancestor source (and before
|
|
72
|
+
// profile): an ancestor doc always beats a nested doc on a name tie.
|
|
73
|
+
if (includeDescendants) {
|
|
74
|
+
for (const root of descendantStoreRoots(roots)) {
|
|
75
|
+
out.push({
|
|
76
|
+
scope: 'project',
|
|
77
|
+
memoryDir: join(root, 'memory'),
|
|
78
|
+
plugins: listInstalledPluginsInRoot('project', root).filter((p) => p.enabled),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
69
82
|
}
|
|
70
83
|
else if (s === 'profile') {
|
|
71
84
|
const profileId = target.profileId ?? '';
|
|
@@ -234,8 +247,8 @@ export function listProjectMemoryDocs(startDir = process.cwd(), profileId = sele
|
|
|
234
247
|
* profile's memory (if any), then user, then builtin. Within each source,
|
|
235
248
|
* native docs are emitted before enabled-plugin docs, so native wins on the
|
|
236
249
|
* caller's first-wins dedup. */
|
|
237
|
-
export function listAllMemoryDocs(scope, quiet = false) {
|
|
238
|
-
return memorySourcesInPrecedence(ambientTarget(), scope).flatMap((source) => sourceMemoryDocs(source, quiet));
|
|
250
|
+
export function listAllMemoryDocs(scope, quiet = false, includeDescendants = false) {
|
|
251
|
+
return memorySourcesInPrecedence(ambientTarget(), scope, includeDescendants).flatMap((source) => sourceMemoryDocs(source, quiet));
|
|
239
252
|
}
|
|
240
253
|
/** Find the direct child of `dir` — a `.md` file (matched on name minus
|
|
241
254
|
* extension) or a directory — whose NORMALIZED display name equals
|
|
@@ -359,10 +372,10 @@ function formatLeafAmbiguous(leaf, matches) {
|
|
|
359
372
|
const ids = matches.map(memoryDocId).join(', ');
|
|
360
373
|
return `ambiguous memory document: ${leaf} matches multiple documents: ${ids}`;
|
|
361
374
|
}
|
|
362
|
-
function loadMemorySources(target, scope) {
|
|
375
|
+
function loadMemorySources(target, scope, includeDescendants = false) {
|
|
363
376
|
// Quiet: a targeted read must not spew other docs' frontmatter warnings
|
|
364
377
|
// before its own result (esp. a not_found) — corpus health is `lint`'s job.
|
|
365
|
-
return memorySourcesInPrecedence(target, scope).map((source) => ({
|
|
378
|
+
return memorySourcesInPrecedence(target, scope, includeDescendants).map((source) => ({
|
|
366
379
|
source,
|
|
367
380
|
docs: sourceMemoryDocs(source, true),
|
|
368
381
|
}));
|
|
@@ -465,5 +478,5 @@ export function resolveMemoryDocForTarget(rawName, target, opts = {}) {
|
|
|
465
478
|
throw usage(`scope conflict: identifier "${rawName}" uses scope "${parsed.scope}" but --scope is "${opts.scope}"`);
|
|
466
479
|
}
|
|
467
480
|
const effectiveScope = opts.scope ?? parsed.scope;
|
|
468
|
-
return resolveMemoryDocFromSources(rawName, opts, loadMemorySources(target, effectiveScope));
|
|
481
|
+
return resolveMemoryDocFromSources(rawName, opts, loadMemorySources(target, effectiveScope, opts.includeDescendants ?? false));
|
|
469
482
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every `.crouter` store root strictly BELOW the given ancestor roots' owning
|
|
3
|
+
* dirs, in owner order then discovery order. Takes the `projectScopeRoots()`
|
|
4
|
+
* entries themselves (`.crouter` dir paths) and derives owner dirs internally.
|
|
5
|
+
*
|
|
6
|
+
* Any discovered root whose realpath matches an INPUT root is excluded here,
|
|
7
|
+
* inside the function: a profile can manage both `/a` and `/a/b`, making
|
|
8
|
+
* `/a/b/.crouter` simultaneously an ancestor root and a descendant of `/a` —
|
|
9
|
+
* excluding centrally gives the resolver and lint the identical call with no
|
|
10
|
+
* duplicated dedupe logic.
|
|
11
|
+
*/
|
|
12
|
+
export declare function descendantStoreRoots(ancestorRoots: string[]): string[];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// nested-stores.ts — bounded discovery of `.crouter/memory` stores BELOW the
|
|
2
|
+
// mounted ancestor roots, so `crtr memory list/read/find/lint/delete/origin`
|
|
3
|
+
// can address a nested store's docs by name. Deliberately opt-in and never on
|
|
4
|
+
// the boot/render path: the boot catalog, workspace-open render, on-read
|
|
5
|
+
// resolvedDocs, slash-command snapshot, and persona resolution all stay on the
|
|
6
|
+
// flat ancestor+profile stack (on-read delivery finds nested stores through
|
|
7
|
+
// its own read-file walk instead).
|
|
8
|
+
//
|
|
9
|
+
// Two discovery modes per owner dir, both under one shared 500 ms budget:
|
|
10
|
+
// • Git root — `git ls-files` over tracked+untracked (respecting .gitignore),
|
|
11
|
+
// so junk dirs and ignored trees cost nothing and embedded repos are
|
|
12
|
+
// auto-excluded (ls-files does not descend into submodules).
|
|
13
|
+
// • Non-git root — a DFS with junk-dir skips, a depth cap, and an
|
|
14
|
+
// embedded-repo stop.
|
|
15
|
+
import { readdirSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, sep } from 'node:path';
|
|
17
|
+
import { CRTR_DIR_NAME } from '../types.js';
|
|
18
|
+
import { pathExists, realpathOrSelf } from './fs-utils.js';
|
|
19
|
+
import { gitSync } from './git.js';
|
|
20
|
+
import { warn } from './output.js';
|
|
21
|
+
/** Mirrors on-read.ts's junk set, plus `.venv`. */
|
|
22
|
+
const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc', '.venv']);
|
|
23
|
+
const MAX_DEPTH = 6;
|
|
24
|
+
const TIME_BUDGET_MS = 500;
|
|
25
|
+
/** Cache keyed by the sorted joined root list. A `memory read` of an INDEX
|
|
26
|
+
* resolves every `[[link]]` through the resolver, each of which rebuilds its
|
|
27
|
+
* sources — without this cache that is one git spawn + walk per link. CLI
|
|
28
|
+
* processes are short-lived, so staleness is a non-issue; the daemon never
|
|
29
|
+
* opts in. */
|
|
30
|
+
const cache = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Every `.crouter` store root strictly BELOW the given ancestor roots' owning
|
|
33
|
+
* dirs, in owner order then discovery order. Takes the `projectScopeRoots()`
|
|
34
|
+
* entries themselves (`.crouter` dir paths) and derives owner dirs internally.
|
|
35
|
+
*
|
|
36
|
+
* Any discovered root whose realpath matches an INPUT root is excluded here,
|
|
37
|
+
* inside the function: a profile can manage both `/a` and `/a/b`, making
|
|
38
|
+
* `/a/b/.crouter` simultaneously an ancestor root and a descendant of `/a` —
|
|
39
|
+
* excluding centrally gives the resolver and lint the identical call with no
|
|
40
|
+
* duplicated dedupe logic.
|
|
41
|
+
*/
|
|
42
|
+
export function descendantStoreRoots(ancestorRoots) {
|
|
43
|
+
const key = [...ancestorRoots].sort().join('\n');
|
|
44
|
+
const cached = cache.get(key);
|
|
45
|
+
if (cached !== undefined)
|
|
46
|
+
return cached;
|
|
47
|
+
const ancestorReal = new Set(ancestorRoots.map((root) => realpathOrSelf(root)));
|
|
48
|
+
const owners = [];
|
|
49
|
+
const ownerSeen = new Set();
|
|
50
|
+
for (const root of ancestorRoots) {
|
|
51
|
+
const owner = dirname(root);
|
|
52
|
+
const real = realpathOrSelf(owner);
|
|
53
|
+
if (ownerSeen.has(real))
|
|
54
|
+
continue;
|
|
55
|
+
ownerSeen.add(real);
|
|
56
|
+
owners.push(owner);
|
|
57
|
+
}
|
|
58
|
+
const deadline = Date.now() + TIME_BUDGET_MS;
|
|
59
|
+
let warned = false;
|
|
60
|
+
const tripBudget = (owner) => {
|
|
61
|
+
if (warned)
|
|
62
|
+
return;
|
|
63
|
+
warned = true;
|
|
64
|
+
warn(`memory: nested-store discovery under ${owner} exceeded its ${TIME_BUDGET_MS}ms budget — nested stores may be missing from this listing; register deep stores explicitly (add their dirs to the selected profile's projects)`);
|
|
65
|
+
};
|
|
66
|
+
const found = [];
|
|
67
|
+
const foundSeen = new Set();
|
|
68
|
+
const add = (storeRoot) => {
|
|
69
|
+
const real = realpathOrSelf(storeRoot);
|
|
70
|
+
if (foundSeen.has(real) || ancestorReal.has(real))
|
|
71
|
+
return;
|
|
72
|
+
foundSeen.add(real);
|
|
73
|
+
found.push(storeRoot);
|
|
74
|
+
};
|
|
75
|
+
for (const owner of owners) {
|
|
76
|
+
const remaining = deadline - Date.now();
|
|
77
|
+
if (remaining <= 0) {
|
|
78
|
+
tripBudget(owner);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (pathExists(join(owner, '.git'))) {
|
|
82
|
+
collectGitStores(owner, add, remaining, () => tripBudget(owner));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
collectWalkStores(owner, add, deadline, () => tripBudget(owner));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
cache.set(key, found);
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
// Git owner: enumerate via `git ls-files -co --exclude-standard` so
|
|
92
|
+
// .gitignore is respected for free. Git's default pathspec fnmatch lets `*`
|
|
93
|
+
// cross slashes, and the leading `*/` excludes the owner's own root store
|
|
94
|
+
// (nothing precedes its `.crouter`).
|
|
95
|
+
function collectGitStores(owner, add, timeoutMs, trip) {
|
|
96
|
+
const marker = `/${CRTR_DIR_NAME}/memory/`;
|
|
97
|
+
const res = gitSync(['-C', owner, 'ls-files', '-z', '-co', '--exclude-standard', '--', `*/${CRTR_DIR_NAME}/memory/*`], undefined, undefined, { timeoutMs });
|
|
98
|
+
if (res.timedOut === true) {
|
|
99
|
+
trip();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (res.status !== 0)
|
|
103
|
+
return;
|
|
104
|
+
for (const rel of res.stdout.split('\0')) {
|
|
105
|
+
if (rel === '')
|
|
106
|
+
continue;
|
|
107
|
+
const idx = rel.indexOf(marker);
|
|
108
|
+
if (idx <= 0)
|
|
109
|
+
continue;
|
|
110
|
+
add(join(owner, rel.slice(0, idx).split('/').join(sep), CRTR_DIR_NAME));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Non-git owner: DFS with junk skips, a depth cap, and a stop at any embedded
|
|
114
|
+
* `.git` dir (an embedded repo is its own world — register it explicitly). */
|
|
115
|
+
function collectWalkStores(owner, add, deadline, trip) {
|
|
116
|
+
const stack = [{ dir: owner, depth: 0 }];
|
|
117
|
+
while (stack.length > 0) {
|
|
118
|
+
if (Date.now() > deadline) {
|
|
119
|
+
trip();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const { dir, depth } = stack.pop();
|
|
123
|
+
if (dir !== owner) {
|
|
124
|
+
if (pathExists(join(dir, '.git')))
|
|
125
|
+
continue;
|
|
126
|
+
if (pathExists(join(dir, CRTR_DIR_NAME, 'memory')))
|
|
127
|
+
add(join(dir, CRTR_DIR_NAME));
|
|
128
|
+
}
|
|
129
|
+
if (depth >= MAX_DEPTH)
|
|
130
|
+
continue;
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (!entry.isDirectory())
|
|
140
|
+
continue;
|
|
141
|
+
if (JUNK_DIRS.has(entry.name) || entry.name === CRTR_DIR_NAME)
|
|
142
|
+
continue;
|
|
143
|
+
stack.push({ dir: join(dir, entry.name), depth: depth + 1 });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -62,9 +62,11 @@ export const TOOL_ICON = {
|
|
|
62
62
|
write: String.fromCodePoint(0xf0c7),
|
|
63
63
|
edit: String.fromCodePoint(0xf044),
|
|
64
64
|
};
|
|
65
|
-
/** One glyph per root
|
|
65
|
+
/** One glyph per root family (`crtr <family> ...`) — the fallback for any
|
|
66
66
|
* leaf not listed in LEAF_ICON_CP below. Codepoints are Nerd Font
|
|
67
|
-
* private-use-area icons chosen per family concept (see command-icons.md).
|
|
67
|
+
* private-use-area icons chosen per family concept (see command-icons.md).
|
|
68
|
+
* Families a plugin mounts (capture, device, search) are listed here too: this
|
|
69
|
+
* registry is keyed by command path, not by who owns the command. */
|
|
68
70
|
const FAMILY_ICON_CP = {
|
|
69
71
|
canvas: 0xf0e8,
|
|
70
72
|
capture: 0xf03d,
|
|
@@ -81,4 +81,4 @@ export declare function buildForkBearings(nodeId: string): string;
|
|
|
81
81
|
* AGENTS.md/CLAUDE.md injection stays suppressed (`noContextFiles` in
|
|
82
82
|
* broker.ts); `crtr sys sync project-guidance` imports that guidance into
|
|
83
83
|
* the directory's `.crouter/memory/INDEX.md` front door. */
|
|
84
|
-
export declare function buildContextBearings(nodeId: string): string;
|
|
84
|
+
export declare function buildContextBearings(nodeId: string, seen?: Set<string>): string;
|
|
@@ -404,7 +404,7 @@ export function buildForkBearings(nodeId) {
|
|
|
404
404
|
* AGENTS.md/CLAUDE.md injection stays suppressed (`noContextFiles` in
|
|
405
405
|
* broker.ts); `crtr sys sync project-guidance` imports that guidance into
|
|
406
406
|
* the directory's `.crouter/memory/INDEX.md` front door. */
|
|
407
|
-
export function buildContextBearings(nodeId) {
|
|
407
|
+
export function buildContextBearings(nodeId, seen) {
|
|
408
408
|
const node = getNode(nodeId);
|
|
409
409
|
const bearings = ['<crtr-bearings>', buildIdentityAssertion(nodeId)];
|
|
410
410
|
const map = buildGraphMap(nodeId);
|
|
@@ -425,7 +425,7 @@ export function buildContextBearings(nodeId) {
|
|
|
425
425
|
// The consultable catalog rides BESIDE the bearings, not inside them.
|
|
426
426
|
const knowledge = renderKnowledgeBlock(nodeId);
|
|
427
427
|
// A workspace mount is the initial read of its root (`applies-to: "."`).
|
|
428
|
-
const workspaceContext = renderWorkspaceOpenDocs(nodeId);
|
|
428
|
+
const workspaceContext = renderWorkspaceOpenDocs(nodeId, seen);
|
|
429
429
|
// Hidden applet/situation-origin ambient context (see situational-context.ts)
|
|
430
430
|
// — a SIBLING block, never nested under <crtr-bearings>, and never routed
|
|
431
431
|
// through the visible chat/digest path. '' when nothing was ever set.
|
|
@@ -53,6 +53,15 @@ export class FaultRetry {
|
|
|
53
53
|
generation.stagedWatchdogAbort = false;
|
|
54
54
|
generation.stagedOverflowFailure = null;
|
|
55
55
|
const settledSession = generation.session;
|
|
56
|
+
// A failed daemon retry settles here with the episode's fault marker still
|
|
57
|
+
// present (a successful provider round-trip would have cleared it). Carry
|
|
58
|
+
// the episode forward — original anchor, start time, and attempt count — so
|
|
59
|
+
// each retry rewinds to the same fork point instead of stacking recovery
|
|
60
|
+
// prompts, and so backoff/exhaustion actually accumulate.
|
|
61
|
+
const prior = readFault(this.deps.nodeId);
|
|
62
|
+
const episode = this.isActiveAutoFault(prior) && prior.link === 'pi→provider'
|
|
63
|
+
? { since: prior.since, anchorEntryId: prior.anchorEntryId, attempt: prior.retry.attempt }
|
|
64
|
+
: null;
|
|
56
65
|
const messages = Array.isArray(agentEnd?.messages) ? agentEnd.messages : [];
|
|
57
66
|
const last = [...messages].reverse().find((message) => typeof message === 'object' && message !== null && message.role === 'assistant');
|
|
58
67
|
if (overflowFailure !== null) {
|
|
@@ -65,9 +74,11 @@ export class FaultRetry {
|
|
|
65
74
|
}
|
|
66
75
|
if (watchdogAbort && last?.stopReason === 'aborted') {
|
|
67
76
|
recordFault(this.deps.nodeId, {
|
|
68
|
-
link: 'pi→provider', op: 'generation turn', kind: 'connection',
|
|
77
|
+
link: 'pi→provider', op: 'generation turn', kind: 'connection',
|
|
78
|
+
retry: { disposition: 'auto', by: 'daemon', ...(episode?.attempt !== undefined ? { attempt: episode.attempt } : {}) },
|
|
69
79
|
message: typeof last.errorMessage === 'string' ? last.errorMessage : 'engine error (no errorMessage recorded)',
|
|
70
|
-
|
|
80
|
+
...(episode !== null ? { since: episode.since } : {}),
|
|
81
|
+
anchorEntryId: episode?.anchorEntryId ?? settledSession.sessionManager.getLeafId?.() ?? undefined,
|
|
71
82
|
});
|
|
72
83
|
return;
|
|
73
84
|
}
|
|
@@ -84,9 +95,17 @@ export class FaultRetry {
|
|
|
84
95
|
retry: uncompactedOverflow
|
|
85
96
|
? { disposition: 'fatal' }
|
|
86
97
|
: classified.disposition === 'auto'
|
|
87
|
-
? {
|
|
98
|
+
? {
|
|
99
|
+
disposition: 'auto', by: 'daemon',
|
|
100
|
+
...(coolingDeadline !== undefined ? { nextAt: new Date(coolingDeadline).toISOString() } : {}),
|
|
101
|
+
...(episode?.attempt !== undefined ? { attempt: episode.attempt } : {}),
|
|
102
|
+
}
|
|
88
103
|
: { disposition: classified.disposition },
|
|
89
|
-
message: raw,
|
|
104
|
+
message: raw,
|
|
105
|
+
...(!uncompactedOverflow && classified.disposition === 'auto' && episode !== null ? { since: episode.since } : {}),
|
|
106
|
+
anchorEntryId: !uncompactedOverflow && classified.disposition === 'auto' && episode?.anchorEntryId !== undefined
|
|
107
|
+
? episode.anchorEntryId
|
|
108
|
+
: settledSession.sessionManager.getLeafId?.() ?? undefined,
|
|
90
109
|
});
|
|
91
110
|
}
|
|
92
111
|
schedule() {
|
|
@@ -18,5 +18,9 @@ export declare function readBrokerCursor(nodeId: string): BrokerInboxEntryId | u
|
|
|
18
18
|
export declare function readBrokerInboxSince(nodeId: string, cursor?: string): BrokerInboxEntry[];
|
|
19
19
|
export declare function readBrokerInboxRange(nodeId: string, after: string | undefined, through: string): BrokerInboxEntry[];
|
|
20
20
|
export declare function readBrokerCanceledEntryIds(nodeId: string): Set<BrokerInboxEntryId>;
|
|
21
|
+
/** The node id a canonical report ref points at, or undefined when the ref is
|
|
22
|
+
* absent, absolute, or not a report path. Callers use it to tell whether the
|
|
23
|
+
* report-node projection they hold can still resolve the ref. */
|
|
24
|
+
export declare function reportRefNodeId(ref: string | undefined): string | undefined;
|
|
21
25
|
/** Render the existing inbox digest wording with daemon-projected report existence. */
|
|
22
26
|
export declare function coalesceBrokerInbox(entries: readonly BrokerInboxEntry[], reportNodes: ReadonlyMap<string, BrokerReportNodeDTO>): string;
|
|
@@ -101,16 +101,25 @@ function clipBody(body) {
|
|
|
101
101
|
}
|
|
102
102
|
return { text: text.trimEnd(), clipped };
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
/** The node id a canonical report ref points at, or undefined when the ref is
|
|
105
|
+
* absent, absolute, or not a report path. Callers use it to tell whether the
|
|
106
|
+
* report-node projection they hold can still resolve the ref. */
|
|
107
|
+
export function reportRefNodeId(ref) {
|
|
108
|
+
if (ref === undefined || ref.startsWith('/'))
|
|
109
|
+
return undefined;
|
|
110
|
+
const separator = ref.indexOf(':');
|
|
108
111
|
if (separator <= 0)
|
|
112
|
+
return undefined;
|
|
113
|
+
const relpath = ref.slice(separator + 1);
|
|
114
|
+
if (!relpath.startsWith('reports/') || relpath.includes('..') || relpath.startsWith('/'))
|
|
115
|
+
return undefined;
|
|
116
|
+
return ref.slice(0, separator);
|
|
117
|
+
}
|
|
118
|
+
function inlineReport(entry, reportNodes) {
|
|
119
|
+
const nodeId = reportRefNodeId(entry.ref);
|
|
120
|
+
if (nodeId === undefined || !reportNodes.has(nodeId))
|
|
109
121
|
return null;
|
|
110
|
-
const
|
|
111
|
-
const relpath = entry.ref.slice(separator + 1);
|
|
112
|
-
if (!reportNodes.has(nodeId) || !relpath.startsWith('reports/') || relpath.includes('..') || relpath.startsWith('/'))
|
|
113
|
-
return null;
|
|
122
|
+
const relpath = entry.ref.slice(entry.ref.indexOf(':') + 1);
|
|
114
123
|
const path = `${nodeDir(nodeId)}/${relpath}`;
|
|
115
124
|
try {
|
|
116
125
|
const body = readFileSync(path, 'utf8');
|
|
@@ -3,7 +3,7 @@ export declare function editorLabelForBrokerNode(node: BrokerExtensionNodeDTO):
|
|
|
3
3
|
/** Same environment-only project context rendered at normal boot, without a
|
|
4
4
|
* canvas import. */
|
|
5
5
|
export declare function buildProjectContextBlockForBroker(cwd: string): string;
|
|
6
|
-
export declare function buildContextBearingsFromState(state: BrokerExtensionStateDTO, projectContext: string, situational?: string): string;
|
|
6
|
+
export declare function buildContextBearingsFromState(state: BrokerExtensionStateDTO, projectContext: string, situational?: string, seen?: Set<string>): string;
|
|
7
7
|
export declare function buildForkBearingsFromState(state: BrokerExtensionStateDTO, situational?: string): string;
|
|
8
8
|
export declare function renderPreferencesFromState(state: BrokerExtensionStateDTO): string;
|
|
9
9
|
/** A canonical report ref has already been validated by crtrd in state. The
|
|
@@ -120,7 +120,7 @@ function reviewNote(node) {
|
|
|
120
120
|
return '';
|
|
121
121
|
return `You exist for one human review of \`${review.target_file}\`. The inherited conversation belongs to the origin node, not you — carry only the context needed for this review. The person's visible transcript begins at the boundary marker above; everything earlier is context they cannot see.`;
|
|
122
122
|
}
|
|
123
|
-
export function buildContextBearingsFromState(state, projectContext, situational = '') {
|
|
123
|
+
export function buildContextBearingsFromState(state, projectContext, situational = '', seen) {
|
|
124
124
|
const { node } = state;
|
|
125
125
|
const bearings = ['<crtr-bearings>', identity(state)];
|
|
126
126
|
const map = graphMap(state);
|
|
@@ -137,7 +137,7 @@ export function buildContextBearingsFromState(state, projectContext, situational
|
|
|
137
137
|
bearings.push('', `Because you persist across refresh cycles, your context directory (${contextDir(node.node_id)}) is also where a future cycle of you resumes the work — keep the working notes and decisions a refreshed you would need there, alongside the docs you share with the nodes you spawn.`);
|
|
138
138
|
bearings.push('</crtr-bearings>');
|
|
139
139
|
const knowledge = renderKnowledgeForSubject(state.subject, node.node_id);
|
|
140
|
-
const workspace = renderWorkspaceOpenDocsForSubject(state.subject, node.cwd, node.profile_id);
|
|
140
|
+
const workspace = renderWorkspaceOpenDocsForSubject(state.subject, node.cwd, node.profile_id, seen);
|
|
141
141
|
return [bearings.join('\n'), knowledge, workspace, situational, projectContext].filter((part) => part !== '').join('\n');
|
|
142
142
|
}
|
|
143
143
|
export function buildForkBearingsFromState(state, situational = '') {
|
|
@@ -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
|
|
50
|
+
* Structured objects with bullets 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
|