@north-light/crouter 0.3.182 → 0.3.184
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/canvas/nav-model.d.ts +1 -8
- package/dist/core/canvas/nav-model.js +30 -59
- package/dist/core/canvas/nav-render.d.ts +2 -2
- package/dist/core/canvas/nav-render.js +8 -6
- 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-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/daemon/__tests__/subtree-filter-freshness.test.d.ts +1 -0
- package/dist/daemon/__tests__/subtree-filter-freshness.test.js +63 -0
- 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/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
|
+
});
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { type FlatRow, type FoldState, type SubtreeActivity } from './nav-render.js';
|
|
2
|
-
import type { NodeMeta
|
|
2
|
+
import type { NodeMeta } from './index.js';
|
|
3
3
|
export { PI_MAX_WIDGET_LINES, VIEWPORT_FALLBACK_ROWS, ESC, RESET, BOLD, DIM, REVERSE, BG_ATTACHED, GREEN, RED, YELLOW, CYAN, GRAY, nodeGlyph, visibleWidth, truncate, fillWidth, fillBar, readTelemetry, fmtTokens, tokensCell, activityCell, cycleBadge, navLabel, shortId, askBadge, liveBelowBadge, isAttached, hasBrokerViewers, fetchAsksMap, graphWidgetBudget, GRAPH_HINT, type Telemetry, type FlatRow, type FoldState, type SubtreeActivity, } from './nav-render.js';
|
|
4
|
-
export declare function beginFrame(): void;
|
|
5
|
-
/** getNode memoized for the current frame (meta.json read + sqlite row). */
|
|
6
|
-
export declare function cNode(id: string): NodeMeta | null;
|
|
7
|
-
/** subscriptionsOf (a node's reports) memoized for the current frame. */
|
|
8
|
-
export declare function cSubscriptions(id: string): SubscriptionRef[];
|
|
9
|
-
/** subscribersOf (a node's managers) memoized for the current frame. */
|
|
10
|
-
export declare function cSubscribers(id: string): SubscriptionRef[];
|
|
11
4
|
/** First manager (by created) — the UP step for the ancestry spine. */
|
|
12
5
|
export declare function managerOf(id: string): string | undefined;
|
|
13
6
|
/** A kind:'human' node is a control-plane ASK (a deck on the human's
|
|
@@ -15,74 +15,45 @@ import { activeFaultForDisplay } from './render.js';
|
|
|
15
15
|
import { readFault } from '../runtime/fault.js';
|
|
16
16
|
import { faultSummary } from './status-glyph.js';
|
|
17
17
|
import { statusRank } from './node-order.js';
|
|
18
|
-
import {
|
|
18
|
+
import { nodeGlyph, navLabel, shortId, tokensCell, activityCell, cycleBadge, askBadge, liveBelowBadge, truncate, fillBar, fillWidth, isAttached, BOLD, DIM, RESET, REVERSE, BG_ATTACHED, } from './nav-render.js';
|
|
19
19
|
// Re-export the pure render/telemetry half so existing nav-model importers
|
|
20
20
|
// (chrome/canvas-panels, overlays/graph, resume) keep resolving these here.
|
|
21
21
|
export { PI_MAX_WIDGET_LINES, VIEWPORT_FALLBACK_ROWS, ESC, RESET, BOLD, DIM, REVERSE, BG_ATTACHED, GREEN, RED, YELLOW, CYAN, GRAY, nodeGlyph, visibleWidth, truncate, fillWidth, fillBar, readTelemetry, fmtTokens, tokensCell, activityCell, cycleBadge, navLabel, shortId, askBadge, liveBelowBadge, isAttached, hasBrokerViewers, fetchAsksMap, graphWidgetBudget, GRAPH_HINT, } from './nav-render.js';
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
|
-
//
|
|
23
|
+
// Read primitives — UNCACHED, deliberately
|
|
24
24
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// That redundancy is the GRAPH view's open/scroll lag.
|
|
25
|
+
// These once memoized into module-global Maps that a `beginFrame()` cleared at
|
|
26
|
+
// the top of each TUI render. That cache is gone. Nothing in this module is
|
|
27
|
+
// frame-scoped any more: the GRAPH overlay reads the canvas through crtrd's
|
|
28
|
+
// remote CanvasSource (clients/attach/overlays/graph.ts) and never imports this
|
|
29
|
+
// file, while the one live consumer left — crtrd's `GET /v1/nodes?under=` via
|
|
30
|
+
// `subtreeIds` — is a daemon that runs for HOURS between restarts.
|
|
32
31
|
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
32
|
+
// A process-lifetime memo there is not "at worst one frame stale", it is
|
|
33
|
+
// permanent: the first `?under=X` request that lands before X has spawned any
|
|
34
|
+
// children pinned `[]` under X and every later request returned it. Correctness
|
|
35
|
+
// on the daemon outranks a render amortization no caller asks for, so each read
|
|
36
|
+
// goes straight to the canvas db. The only frame cache still in play is
|
|
37
|
+
// nav-render's telemetry memo, whose TUI callers clear it themselves via
|
|
38
|
+
// `beginFrameTelem()`.
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const frameMgrs = new Map();
|
|
43
|
-
export function beginFrame() {
|
|
44
|
-
frameNodes.clear();
|
|
45
|
-
frameSubs.clear();
|
|
46
|
-
frameMgrs.clear();
|
|
47
|
-
beginFrameTelem();
|
|
48
|
-
}
|
|
49
|
-
/** getNode memoized for the current frame (meta.json read + sqlite row). */
|
|
50
|
-
export function cNode(id) {
|
|
51
|
-
if (frameNodes.has(id))
|
|
52
|
-
return frameNodes.get(id) ?? null;
|
|
53
|
-
const v = getNode(id);
|
|
54
|
-
frameNodes.set(id, v);
|
|
55
|
-
return v;
|
|
56
|
-
}
|
|
57
|
-
/** subscriptionsOf (a node's reports) memoized for the current frame. */
|
|
58
|
-
export function cSubscriptions(id) {
|
|
59
|
-
const hit = frameSubs.get(id);
|
|
60
|
-
if (hit !== undefined)
|
|
61
|
-
return hit;
|
|
62
|
-
let v;
|
|
40
|
+
/** subscriptionsOf (a node's reports); an unreadable row reads as no edges. */
|
|
41
|
+
function subsOf(id) {
|
|
63
42
|
try {
|
|
64
|
-
|
|
43
|
+
return subscriptionsOf(id);
|
|
65
44
|
}
|
|
66
45
|
catch {
|
|
67
|
-
|
|
46
|
+
return [];
|
|
68
47
|
}
|
|
69
|
-
frameSubs.set(id, v);
|
|
70
|
-
return v;
|
|
71
48
|
}
|
|
72
|
-
/** subscribersOf (a node's managers)
|
|
73
|
-
|
|
74
|
-
const hit = frameMgrs.get(id);
|
|
75
|
-
if (hit !== undefined)
|
|
76
|
-
return hit;
|
|
77
|
-
let v;
|
|
49
|
+
/** subscribersOf (a node's managers); an unreadable row reads as no edges. */
|
|
50
|
+
function mgrsOf(id) {
|
|
78
51
|
try {
|
|
79
|
-
|
|
52
|
+
return subscribersOf(id);
|
|
80
53
|
}
|
|
81
54
|
catch {
|
|
82
|
-
|
|
55
|
+
return [];
|
|
83
56
|
}
|
|
84
|
-
frameMgrs.set(id, v);
|
|
85
|
-
return v;
|
|
86
57
|
}
|
|
87
58
|
// ---------------------------------------------------------------------------
|
|
88
59
|
// Attachment — is a human currently WATCHING a node? A separate axis from
|
|
@@ -100,7 +71,7 @@ export function cSubscribers(id) {
|
|
|
100
71
|
// ---------------------------------------------------------------------------
|
|
101
72
|
/** First manager (by created) — the UP step for the ancestry spine. */
|
|
102
73
|
export function managerOf(id) {
|
|
103
|
-
return
|
|
74
|
+
return mgrsOf(id)[0]?.node_id;
|
|
104
75
|
}
|
|
105
76
|
/** A kind:'human' node is a control-plane ASK (a deck on the human's
|
|
106
77
|
* screen), NOT a pi conversation — it has no session, so focusing/reviving it
|
|
@@ -110,17 +81,17 @@ export function managerOf(id) {
|
|
|
110
81
|
* own. Drop it from every navigable list (the tree, BASE reports, child counts,
|
|
111
82
|
* subtree expansion) so it can never be selected. */
|
|
112
83
|
export function isHumanAsk(id) {
|
|
113
|
-
return
|
|
84
|
+
return getNode(id)?.kind === 'human';
|
|
114
85
|
}
|
|
115
86
|
/** A node's direct children that are navigable conversations — human-ask nodes
|
|
116
87
|
* dropped. The one place the nav chrome enumerates children. */
|
|
117
88
|
export function convoChildIds(id) {
|
|
118
|
-
return
|
|
89
|
+
return subsOf(id).map((s) => s.node_id).filter((cid) => !isHumanAsk(cid));
|
|
119
90
|
}
|
|
120
91
|
/** Live reports (active|idle) of a node — the DOWN set in BASE. */
|
|
121
92
|
export function liveReports(id) {
|
|
122
93
|
return convoChildIds(id).filter((cid) => {
|
|
123
|
-
const st =
|
|
94
|
+
const st = getNode(cid)?.status;
|
|
124
95
|
return st === 'active' || st === 'idle';
|
|
125
96
|
});
|
|
126
97
|
}
|
|
@@ -175,7 +146,7 @@ export function childBadge(node) {
|
|
|
175
146
|
* the tree and when stepping into a subtree (`l`). Array.sort is stable, so
|
|
176
147
|
* equal-status siblings keep their creation order. */
|
|
177
148
|
export function sortedChildIds(id) {
|
|
178
|
-
return convoChildIds(id).sort((a, b) => statusRank(
|
|
149
|
+
return convoChildIds(id).sort((a, b) => statusRank(getNode(a)?.status) - statusRank(getNode(b)?.status));
|
|
179
150
|
}
|
|
180
151
|
// ---------------------------------------------------------------------------
|
|
181
152
|
// GRAPH model — flatten the local graph fold-aware. Rebuilt every render (cheap
|
|
@@ -189,7 +160,7 @@ export function computeSubtreeActivity(root, self) {
|
|
|
189
160
|
// Returns the count of active nodes in subtree(id) INCLUDING id, and whether
|
|
190
161
|
// that subtree is worth revealing (holds an active node or self).
|
|
191
162
|
const visit = (id) => {
|
|
192
|
-
const selfActive =
|
|
163
|
+
const selfActive = getNode(id)?.status === 'active';
|
|
193
164
|
if (seen.has(id))
|
|
194
165
|
return { active: selfActive ? 1 : 0, reveal: selfActive || id === self };
|
|
195
166
|
seen.add(id);
|
|
@@ -254,7 +225,7 @@ export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
|
|
|
254
225
|
const line = `${r.branch} ${DIM}↺ ${shortId(r.id)}${RESET}`;
|
|
255
226
|
return wrap(line, false);
|
|
256
227
|
}
|
|
257
|
-
const node =
|
|
228
|
+
const node = getNode(r.id);
|
|
258
229
|
const dot = nodeGlyph(node);
|
|
259
230
|
const rawName = navLabel(node, r.id);
|
|
260
231
|
const name = r.isSelf ? `${BOLD}${rawName}${RESET}` : rawName;
|
|
@@ -50,8 +50,8 @@ export declare function fillWidth(): number;
|
|
|
50
50
|
* bar; the visible width is padded out to `width`; the line closes with a real
|
|
51
51
|
* RESET so the style never bleeds into the editor below. */
|
|
52
52
|
export declare function fillBar(content: string, width: number, open: string): string;
|
|
53
|
-
/** Clear the per-frame telemetry cache.
|
|
54
|
-
*
|
|
53
|
+
/** Clear the per-frame telemetry cache. Every render pass calls this first, so
|
|
54
|
+
* one keystroke re-reads each node's telemetry exactly once. */
|
|
55
55
|
export declare function beginFrameTelem(): void;
|
|
56
56
|
export interface Telemetry {
|
|
57
57
|
/** Live context-window size (pi's getContextUsage) — the figure pi's own footer
|
|
@@ -130,14 +130,16 @@ export function fillBar(content, width, open) {
|
|
|
130
130
|
return `${open}${reasserted}${' '.repeat(pad)}${RESET}`;
|
|
131
131
|
}
|
|
132
132
|
// ---------------------------------------------------------------------------
|
|
133
|
-
// Per-frame telemetry cache.
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// worst be one frame old.
|
|
133
|
+
// Per-frame telemetry cache. Its callers are TUI render passes ONLY (the GRAPH
|
|
134
|
+
// overlay and the canvas panels), each of which calls beginFrameTelem() at the
|
|
135
|
+
// top of the pass, so token/activity cells read fresh once per frame instead of
|
|
136
|
+
// once per row and a stale entry can at worst be one frame old. Never memoize a
|
|
137
|
+
// canvas read for a caller that is not a render pass: crtrd lives for hours, and
|
|
138
|
+
// a memo it never clears is permanent, not per-frame.
|
|
137
139
|
// ---------------------------------------------------------------------------
|
|
138
140
|
const frameTelem = new Map();
|
|
139
|
-
/** Clear the per-frame telemetry cache.
|
|
140
|
-
*
|
|
141
|
+
/** Clear the per-frame telemetry cache. Every render pass calls this first, so
|
|
142
|
+
* one keystroke re-reads each node's telemetry exactly once. */
|
|
141
143
|
export function beginFrameTelem() {
|
|
142
144
|
frameTelem.clear();
|
|
143
145
|
}
|
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[];
|