@north-light/crouter 0.3.261 → 0.3.263
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/health.d.ts +2 -0
- package/dist/builtin-memory/internal/memory-loading.md +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +5 -3
- package/dist/clients/attach/input/controller.d.ts +4 -0
- package/dist/clients/attach/input/controller.js +8 -0
- package/dist/clients/attach/session/bindings.d.ts +3 -0
- package/dist/clients/attach/session/bindings.js +7 -0
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +3 -3
- package/dist/clients/attach/viewer.js +616 -616
- package/dist/commands/memory/read.js +15 -38
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
- package/dist/commands/sys/context/admin/model.d.ts +7 -2
- package/dist/commands/sys/context/admin/model.js +7 -1
- package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
- package/dist/commands/sys/context/admin/read-view.js +9 -4
- package/dist/commands/sys/context/resolve.d.ts +32 -3
- package/dist/commands/sys/context/resolve.js +74 -25
- package/dist/commands/sys/panels/models-panel.d.ts +28 -5
- package/dist/commands/sys/panels/models-panel.js +558 -75
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes-config.test.js +142 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +2 -2
- package/dist/core/canvas/render-source.d.ts +1 -1
- package/dist/core/canvas/render-source.js +3 -2
- package/dist/core/config.d.ts +19 -2
- package/dist/core/config.js +162 -47
- package/dist/core/keybindings/catalog.js +9 -7
- package/dist/core/model-routes.js +10 -3
- package/dist/core/substrate/listings.d.ts +1 -1
- package/dist/core/substrate/listings.js +6 -2
- package/dist/core/substrate/on-read.d.ts +61 -5
- package/dist/core/substrate/on-read.js +129 -22
- package/dist/core/substrate/render.d.ts +3 -2
- package/dist/core/substrate/render.js +133 -38
- package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
- package/dist/daemon/api/handlers/canvas.js +18 -2
- package/dist/daemon/api/handlers/health.js +17 -12
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -41,11 +41,13 @@
|
|
|
41
41
|
// the plan→text adapter: it owns the `<memory>` envelope, the
|
|
42
42
|
// `<auto-loaded-context>` wrapper, and the exposure ledger.
|
|
43
43
|
import { sep } from 'node:path';
|
|
44
|
-
import {
|
|
44
|
+
import { realpathOrSelf } from '../fs-utils.js';
|
|
45
|
+
import { ambientMemoryTarget, listAllMemoryDocs, loadMemoryTargetView, } from '../memory-resolver.js';
|
|
45
46
|
import { owningRootOf } from './surface-match.js';
|
|
46
|
-
import { demoteDocumentTranscriptExposure, documentExposedAtOrAbove, emptyContextExposureState, exposureTarget, registerDocumentExposure, } from './injected-store.js';
|
|
47
|
+
import { demoteDocumentTranscriptExposure, documentExposedAtOrAbove, emptyContextExposureState, exposedAtOrAbove, exposureTarget, registerDocumentExposure, registerExposure, } from './injected-store.js';
|
|
47
48
|
import { planDelivery } from './plan.js';
|
|
48
|
-
import { parseSubstrateDoc, previewLine } from './schema.js';
|
|
49
|
+
import { parseSubstrateDoc, previewLine, rungRank } from './schema.js';
|
|
50
|
+
import { dirDedupKey, docListingDirs, docsByName, isDirName, renderDirListing } from './listings.js';
|
|
49
51
|
import { cachedEventCorpusInclusive } from './session-cache.js';
|
|
50
52
|
function attr(s) {
|
|
51
53
|
return s
|
|
@@ -88,24 +90,105 @@ function renderDocEnvelope(doc, rung) {
|
|
|
88
90
|
}
|
|
89
91
|
return `<memory ${attrs} />`;
|
|
90
92
|
}
|
|
91
|
-
/** One winner per doc, in plan (precedence) order. */
|
|
92
93
|
function planWinners(plan) {
|
|
93
|
-
return plan.docs.filter((r) => r.winner).map((r) => ({ doc: r.doc, rung: r.finalRung }));
|
|
94
|
+
return plan.docs.filter((r) => r.winner).map((r) => ({ doc: r.doc, rung: r.finalRung, source: r.source }));
|
|
95
|
+
}
|
|
96
|
+
/** The listing corpus for one event: every document the plan CONSIDERED, in
|
|
97
|
+
* the plan's own precedence order. Winners alone are not enough — a bootless
|
|
98
|
+
* sibling never wins anything, and a store discovered by the read walk is
|
|
99
|
+
* absent from the ambient corpus entirely, so both would drop out of the
|
|
100
|
+
* directory the delivered doc sits in. */
|
|
101
|
+
function planListingCorpus(plan) {
|
|
102
|
+
return docsByName(plan.docs.map(({ source }) => source));
|
|
103
|
+
}
|
|
104
|
+
function listingBlock(dir, lines) {
|
|
105
|
+
return `<memory-listing dir="${attr(dir)}">\n${lines.join('\n')}\n</memory-listing>`;
|
|
106
|
+
}
|
|
107
|
+
export function deliveryAccounting() {
|
|
108
|
+
return { deliveries: new Map() };
|
|
109
|
+
}
|
|
110
|
+
export function accountDelivery(accounting, doc, rung, cause, source) {
|
|
111
|
+
if (accounting === undefined || rung === 'none')
|
|
112
|
+
return;
|
|
113
|
+
const key = realpathOrSelf(doc.path);
|
|
114
|
+
const held = accounting.deliveries.get(key);
|
|
115
|
+
if (held !== undefined && rungRank(held.rung) >= rungRank(rung))
|
|
116
|
+
return;
|
|
117
|
+
accounting.deliveries.set(key, {
|
|
118
|
+
rung,
|
|
119
|
+
...(cause === undefined ? {} : { cause }),
|
|
120
|
+
...(source === undefined ? {} : { source }),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** The attachment semantics of a content delivery. Content is read-equivalent:
|
|
124
|
+
* it exposes its own/enclosing directory listings and fires memory-read routes.
|
|
125
|
+
* The exposure ledger makes this finite even when companion routes cycle. */
|
|
126
|
+
export function contentReadAttachments(doc, opts) {
|
|
127
|
+
const { target, byName } = opts;
|
|
128
|
+
const dirs = docListingDirs(byName, doc.name);
|
|
129
|
+
const ownDir = dirs[0] === doc.name;
|
|
130
|
+
const blocks = [];
|
|
131
|
+
let listing;
|
|
132
|
+
for (const [index, dir] of dirs.entries()) {
|
|
133
|
+
// A directory absent from THIS corpus discloses nothing here, so claiming
|
|
134
|
+
// its `dir://` identity would tell the ledger a listing rendered that never
|
|
135
|
+
// did — and silence the channel that would have rendered it.
|
|
136
|
+
if (!isDirName(byName, dir))
|
|
137
|
+
continue;
|
|
138
|
+
const own = ownDir && index === 0;
|
|
139
|
+
if (!own && exposedAtOrAbove(target.state, dirDedupKey(dir), 'preview'))
|
|
140
|
+
continue;
|
|
141
|
+
if (opts.listings === 'tree') {
|
|
142
|
+
// The caller renders these members and registers the directory only for
|
|
143
|
+
// the rows it actually emits.
|
|
144
|
+
opts.dirs?.push(dir);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const explicitOwn = own && opts.explicit === true;
|
|
148
|
+
const lines = renderDirListing(byName, dir, target, !explicitOwn, (listed) => accountDelivery(opts.accounting, listed, 'preview', 'directory listing attached to content delivery', listed));
|
|
149
|
+
// An empty listing is not a listing: registering here would claim a render
|
|
150
|
+
// that produced no line and silence the next channel to reach this dir.
|
|
151
|
+
if (lines.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
registerExposure(target, dirDedupKey(dir), 'preview');
|
|
154
|
+
if (explicitOwn)
|
|
155
|
+
listing = lines;
|
|
156
|
+
else
|
|
157
|
+
blocks.push(listingBlock(dir, lines));
|
|
158
|
+
}
|
|
159
|
+
blocks.push(...memoryReadDocBlocks(realpathOrSelf(doc.path), doc.name, {
|
|
160
|
+
...opts,
|
|
161
|
+
deliveryCause: 'memory-read companion of content delivery',
|
|
162
|
+
}));
|
|
163
|
+
return { blocks, ...(listing === undefined ? {} : { listing }) };
|
|
94
164
|
}
|
|
95
165
|
/** Render the plan's winners that are not already loaded at their matched rung. */
|
|
96
|
-
function renderWinnerBlocks(winners,
|
|
166
|
+
function renderWinnerBlocks(winners, opts) {
|
|
167
|
+
const { target } = opts;
|
|
97
168
|
const rendered = [];
|
|
98
|
-
for (const { doc, rung } of winners) {
|
|
169
|
+
for (const { doc, rung, source } of winners) {
|
|
99
170
|
if (documentExposedAtOrAbove(target.state, doc.path, doc.body, rung))
|
|
100
171
|
continue;
|
|
101
172
|
const block = renderDocEnvelope(doc, rung);
|
|
102
173
|
if (block === null)
|
|
103
174
|
continue;
|
|
104
175
|
registerDocumentExposure(target, doc.path, doc.body, rung);
|
|
176
|
+
accountDelivery(opts.accounting, doc, rung, opts.deliveryCause, source);
|
|
177
|
+
if (rung === 'content') {
|
|
178
|
+
rendered.push(...contentReadAttachments(doc, {
|
|
179
|
+
...opts,
|
|
180
|
+
deliveryCause: 'memory-read companion of content delivery',
|
|
181
|
+
}).blocks);
|
|
182
|
+
}
|
|
105
183
|
rendered.push(block);
|
|
106
184
|
}
|
|
107
185
|
return rendered;
|
|
108
186
|
}
|
|
187
|
+
/** The attachment options for one event render: listings inline as blocks,
|
|
188
|
+
* answered from the event corpus ∪ this plan's own winners. */
|
|
189
|
+
function eventAttachmentOptions(subject, target, plan, accounting) {
|
|
190
|
+
return { subject, target, byName: planListingCorpus(plan), listings: 'blocks', accounting };
|
|
191
|
+
}
|
|
109
192
|
function transientTranscriptTarget() {
|
|
110
193
|
return exposureTarget(emptyContextExposureState(), 'transcript');
|
|
111
194
|
}
|
|
@@ -114,14 +197,19 @@ function wrapAutoLoaded(blocks) {
|
|
|
114
197
|
}
|
|
115
198
|
/** The corpus-carrying plan for one event. Every renderer here passes the
|
|
116
199
|
* session-cached corpus so a hook never re-walks the filesystem. */
|
|
117
|
-
function eventPlan(subject, event, payload) {
|
|
118
|
-
|
|
200
|
+
function eventPlan(subject, event, payload, memoryTarget) {
|
|
201
|
+
// An explicit memory target names a workspace other than this process's, so
|
|
202
|
+
// its corpus is resolved for that target rather than taken from the ambient
|
|
203
|
+
// session cache.
|
|
204
|
+
return memoryTarget === undefined
|
|
205
|
+
? planDelivery(subject, ambientMemoryTarget(), event, payload, resolvedDocs())
|
|
206
|
+
: planDelivery(subject, memoryTarget, event, payload);
|
|
119
207
|
}
|
|
120
208
|
/** Surface docs whose `read` entries match a successfully read file, using
|
|
121
209
|
* the caller-supplied daemon snapshot subject. */
|
|
122
|
-
export function renderOnReadDocsForSubject(subject, readFilePath, target = transientTranscriptTarget()) {
|
|
210
|
+
export function renderOnReadDocsForSubject(subject, readFilePath, target = transientTranscriptTarget(), accounting) {
|
|
123
211
|
const plan = eventPlan(subject, 'read', { absoluteFile: readFilePath });
|
|
124
|
-
return wrapAutoLoaded(renderWinnerBlocks(planWinners(plan), target));
|
|
212
|
+
return wrapAutoLoaded(renderWinnerBlocks(planWinners(plan), eventAttachmentOptions(plan.subject, target, plan, accounting)));
|
|
125
213
|
}
|
|
126
214
|
/** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
|
|
127
215
|
* lookup. The shared exposure target prevents a later read from repeating a
|
|
@@ -131,11 +219,18 @@ export function renderOnReadDocsForSubject(subject, readFilePath, target = trans
|
|
|
131
219
|
* relationship cap BEFORE filtering, ordering, rendering, and exposure
|
|
132
220
|
* registration. A later explicit `crtr memory read` can still raise a document
|
|
133
221
|
* that workspace-open disclosed at a lower rung. */
|
|
134
|
-
export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, target = transientTranscriptTarget()) {
|
|
222
|
+
export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, target = transientTranscriptTarget(), accounting) {
|
|
135
223
|
const workspaceTarget = { cwd, profileId, nodeId: null };
|
|
136
224
|
let winners;
|
|
225
|
+
let byName;
|
|
137
226
|
try {
|
|
138
227
|
winners = planWinners(planDelivery(subject, workspaceTarget, 'workspace-open'));
|
|
228
|
+
// Listings and companion routes answer from the workspace this event
|
|
229
|
+
// opened — the ambient cwd/profile of the process rendering it is a
|
|
230
|
+
// different corpus and would disclose another workspace's neighbourhood.
|
|
231
|
+
// The workspace-open plan considers project stores only, so the listing
|
|
232
|
+
// corpus is the target's full view rather than the plan's.
|
|
233
|
+
byName = docsByName(loadMemoryTargetView(workspaceTarget, { quiet: true }).docs);
|
|
139
234
|
}
|
|
140
235
|
catch {
|
|
141
236
|
return '';
|
|
@@ -149,7 +244,14 @@ export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, targe
|
|
|
149
244
|
const depth = aRoot.split(sep).filter(Boolean).length - bRoot.split(sep).filter(Boolean).length;
|
|
150
245
|
return depth || aRoot.localeCompare(bRoot) || a.doc.path.localeCompare(b.doc.path);
|
|
151
246
|
});
|
|
152
|
-
return wrapAutoLoaded(renderWinnerBlocks(winners,
|
|
247
|
+
return wrapAutoLoaded(renderWinnerBlocks(winners, {
|
|
248
|
+
subject,
|
|
249
|
+
target,
|
|
250
|
+
byName,
|
|
251
|
+
memoryTarget: workspaceTarget,
|
|
252
|
+
listings: 'blocks',
|
|
253
|
+
accounting,
|
|
254
|
+
}));
|
|
153
255
|
}
|
|
154
256
|
/** Inner `<memory>` blocks for the memory-read event: docs whose
|
|
155
257
|
* `memory-read` entries match the doc a `crtr memory read` just resolved
|
|
@@ -157,26 +259,31 @@ export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, targe
|
|
|
157
259
|
* `excludeRealpath` keeps the resolved doc from delivering itself). Returns
|
|
158
260
|
* unwrapped blocks — the read leaf composes one `<auto-loaded-context>`
|
|
159
261
|
* envelope from these plus the directory listings. */
|
|
160
|
-
export function memoryReadDocBlocks(
|
|
161
|
-
const plan = eventPlan(subject, 'memory-read', {
|
|
162
|
-
|
|
163
|
-
|
|
262
|
+
export function memoryReadDocBlocks(excludeRealpath, name, opts) {
|
|
263
|
+
const plan = eventPlan(opts.subject, 'memory-read', { resolvedCanonicalName: name, resolvedDocPath: excludeRealpath }, opts.memoryTarget);
|
|
264
|
+
// A caller with no corpus of its own (the `sys context resolve` simulation)
|
|
265
|
+
// gets this plan's; every nested companion arrives with its parent's corpus
|
|
266
|
+
// already resolved.
|
|
267
|
+
return renderWinnerBlocks(planWinners(plan), {
|
|
268
|
+
...opts,
|
|
269
|
+
byName: opts.byName ?? planListingCorpus(plan),
|
|
164
270
|
});
|
|
165
|
-
return renderWinnerBlocks(planWinners(plan), target);
|
|
166
271
|
}
|
|
167
272
|
/** Surface docs whose `command` entries match an executed bash command.
|
|
168
273
|
* The corpus is the resolved cwd/profile set; a command has no file from which
|
|
169
274
|
* to discover enclosing project stores. */
|
|
170
|
-
export function renderOnCommandDocsForSubject(subject, command, target = transientTranscriptTarget()) {
|
|
171
|
-
|
|
275
|
+
export function renderOnCommandDocsForSubject(subject, command, target = transientTranscriptTarget(), accounting) {
|
|
276
|
+
const plan = eventPlan(subject, 'command', { command });
|
|
277
|
+
return wrapAutoLoaded(renderWinnerBlocks(planWinners(plan), eventAttachmentOptions(subject, target, plan, accounting)));
|
|
172
278
|
}
|
|
173
279
|
/** Surface docs whose `pre-command` entries match a bash command that has NOT
|
|
174
280
|
* run yet. The exact twin of the post-execution renderer above, and its empty
|
|
175
281
|
* string is load-bearing: `''` means every matching doc is already exposed at
|
|
176
282
|
* its matching rung or above, which is the release — the caller lets the
|
|
177
283
|
* command through. */
|
|
178
|
-
export function renderPreCommandDocsForSubject(subject, command, target = transientTranscriptTarget()) {
|
|
179
|
-
|
|
284
|
+
export function renderPreCommandDocsForSubject(subject, command, target = transientTranscriptTarget(), accounting) {
|
|
285
|
+
const plan = eventPlan(subject, 'pre-command', { command });
|
|
286
|
+
return wrapAutoLoaded(renderWinnerBlocks(planWinners(plan), eventAttachmentOptions(subject, target, plan, accounting)));
|
|
180
287
|
}
|
|
181
288
|
function carriesPreCommandEntry(doc) {
|
|
182
289
|
return doc.surfaces.some((entry) => entry.on === 'pre-command');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ExposureTarget } from './injected-store.js';
|
|
2
2
|
import { type SubstrateDoc } from './schema.js';
|
|
3
3
|
import type { NodeConfigSubject } from './subject-fields.js';
|
|
4
|
+
import { type DeliveryAccounting } from './on-read.js';
|
|
4
5
|
/** The computed "sub-personas you may spawn" menu for a subject's kind — the
|
|
5
6
|
* live restoration of the old `personas/resolve.ts` static menu, now derived
|
|
6
7
|
* at render time from the merged kind registry (`subKindsAvailableTo`) so it
|
|
@@ -9,7 +10,7 @@ import type { NodeConfigSubject } from './subject-fields.js';
|
|
|
9
10
|
* header). Emitted for BOTH base and orchestrator modes per the CTO's ruling —
|
|
10
11
|
* mode is irrelevant to which sub-kinds are spawnable. */
|
|
11
12
|
export declare function buildSubPersonaMenu(kind: string): string;
|
|
12
|
-
export declare function renderPreferencesForSubject(subject: NodeConfigSubject | null, target?: ExposureTarget): {
|
|
13
|
+
export declare function renderPreferencesForSubject(subject: NodeConfigSubject | null, target?: ExposureTarget, accounting?: DeliveryAccounting): {
|
|
13
14
|
block: string;
|
|
14
15
|
contentDocs: SubstrateDoc[];
|
|
15
16
|
tree: string;
|
|
@@ -26,7 +27,7 @@ export declare function renderPreferencesForSubject(subject: NodeConfigSubject |
|
|
|
26
27
|
* renders: floored docs never reach the `content` rung). Procedural guidance and factual
|
|
27
28
|
* references both live here as `knowledge`. Returns '' when nothing is
|
|
28
29
|
* eligible. */
|
|
29
|
-
export declare function renderKnowledgeForSubject(subject: NodeConfigSubject, nodeId: string, target?: ExposureTarget): string;
|
|
30
|
+
export declare function renderKnowledgeForSubject(subject: NodeConfigSubject, nodeId: string, target?: ExposureTarget, accounting?: DeliveryAccounting): string;
|
|
30
31
|
/** What a node's configuration change newly exposes, rendered as up to three
|
|
31
32
|
* blocks in one string: the shipped protocol prose, a `<memory kind="preference">`
|
|
32
33
|
* block, and a `<memory kind="knowledge">` block — each dropped when empty, ''
|
|
@@ -69,9 +69,11 @@ import { projectScopeRoots } from '../scope.js';
|
|
|
69
69
|
// pull subject.js (canvas-db) transitively, re-tainting every CLI consumer of
|
|
70
70
|
// the pure render fns. The canvas-db wrappers (renderPreferencesSection /
|
|
71
71
|
// renderKnowledgeBlock) live in render-node.js instead.
|
|
72
|
-
import { documentExposedAtOrAbove, registerDocumentExposure, } from './injected-store.js';
|
|
72
|
+
import { documentExposedAtOrAbove, emptyContextExposureState, exposureTarget, registerDocumentExposure, registerExposure, } from './injected-store.js';
|
|
73
73
|
import { bootPartitionOf, planDelivery } from './plan.js';
|
|
74
|
-
import { parseSubstrateDoc, previewLine, rungRank, } from './schema.js';
|
|
74
|
+
import { parseSubstrateDoc, previewLine, rungAtLeast, rungRank, } from './schema.js';
|
|
75
|
+
import { accountDelivery, contentReadAttachments, } from './on-read.js';
|
|
76
|
+
import { dirDedupKey, docsByName } from './listings.js';
|
|
75
77
|
import { cachedEventCorpusInclusive } from './session-cache.js';
|
|
76
78
|
/** The node's own store (`nodes/<id>/context/memory/`), loaded through the
|
|
77
79
|
* resolver's descriptor loader like every other store. Returned across ALL
|
|
@@ -134,16 +136,6 @@ function bootWinners(plan, partition) {
|
|
|
134
136
|
function rendersAtBoot(doc) {
|
|
135
137
|
return isVisible(doc.bootRung) && (doc.bootRung !== 'content' || doc.body.trim() !== '');
|
|
136
138
|
}
|
|
137
|
-
/** Register every visibly rendered winner at its displayed rung. */
|
|
138
|
-
function recordRendered(target, docs) {
|
|
139
|
-
if (target === undefined)
|
|
140
|
-
return;
|
|
141
|
-
for (const d of docs) {
|
|
142
|
-
if (!rendersAtBoot(d))
|
|
143
|
-
continue;
|
|
144
|
-
registerDocumentExposure(target, d.path, d.body, d.bootRung);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
139
|
// Step 2 — display ordering + grouped render.
|
|
148
140
|
/** Content-prose ordering rank: general → specific. builtin=0, user=1,
|
|
149
141
|
* profile=2, project ranks start at 3 and climb by root nearness — the
|
|
@@ -204,12 +196,74 @@ function compareTreePosition(a, b) {
|
|
|
204
196
|
function isAgentEditable(doc) {
|
|
205
197
|
return doc.scope !== 'builtin' && doc.plugin === undefined;
|
|
206
198
|
}
|
|
199
|
+
/** The members of every directory this render's content deliveries disclosed,
|
|
200
|
+
* raised to `preview` so the catalog tree carries the routing lines a
|
|
201
|
+
* `<memory-listing>` would have carried on an explicit read. A member is
|
|
202
|
+
* raised only when the raise is honest: it renders as a row (content bodies
|
|
203
|
+
* render as prose instead), it does not already disclose its line, it is not
|
|
204
|
+
* `unlisted`, its project's profile cap permits preview, and this context does
|
|
205
|
+
* not already hold it at preview or above.
|
|
206
|
+
*
|
|
207
|
+
* The corpus is `listingDocs` — the partition's own winners. A directory
|
|
208
|
+
* holding both knowledge and preference docs therefore discloses only the
|
|
209
|
+
* current partition's members here, and the other partition skips the
|
|
210
|
+
* directory's `dir://` key: under-disclosure, never a false ledger claim.
|
|
211
|
+
*
|
|
212
|
+
* A directory registers its `dir://` identity here and ONLY here — when it
|
|
213
|
+
* actually contributed a row. A directory whose every member was rejected
|
|
214
|
+
* rendered no listing, so the next channel to reach it must still be free to
|
|
215
|
+
* render one. */
|
|
216
|
+
function exposedDirEntries(listingDocs, dirs, target) {
|
|
217
|
+
if (dirs.length === 0)
|
|
218
|
+
return [];
|
|
219
|
+
const byPath = new Map(listingDocs.map((doc) => [doc.path, doc]));
|
|
220
|
+
const tree = buildMemoryTree(listingDocs.map((doc) => doc.source));
|
|
221
|
+
const raised = new Map();
|
|
222
|
+
for (const dir of dirs) {
|
|
223
|
+
const node = tree.node(dir);
|
|
224
|
+
if (node === null)
|
|
225
|
+
continue;
|
|
226
|
+
let rendered = false;
|
|
227
|
+
const members = [
|
|
228
|
+
...node.childDocuments.map((member) => member.document),
|
|
229
|
+
...node.childDirectories.flatMap((child) => (child.document === null ? [] : [child.document])),
|
|
230
|
+
];
|
|
231
|
+
for (const member of members) {
|
|
232
|
+
const doc = byPath.get(member.path);
|
|
233
|
+
if (doc === undefined || raised.has(doc.path))
|
|
234
|
+
continue;
|
|
235
|
+
if (doc.bootRung === 'content' || rungAtLeast(doc.bootRung, 'preview'))
|
|
236
|
+
continue;
|
|
237
|
+
if (doc.unlisted)
|
|
238
|
+
continue;
|
|
239
|
+
if (!rungAtLeast(doc.projectMemory, 'preview'))
|
|
240
|
+
continue;
|
|
241
|
+
if (documentExposedAtOrAbove(target.state, doc.path, doc.body, 'preview'))
|
|
242
|
+
continue;
|
|
243
|
+
raised.set(doc.path, { ...doc, bootRung: 'preview' });
|
|
244
|
+
rendered = true;
|
|
245
|
+
}
|
|
246
|
+
if (rendered)
|
|
247
|
+
registerExposure(target, dirDedupKey(dir), 'preview');
|
|
248
|
+
}
|
|
249
|
+
return [...raised.values()];
|
|
250
|
+
}
|
|
207
251
|
/** STEP 2 — split deduped `winners` by rung and render each group in its
|
|
208
252
|
* display order: `content` docs first, as clean prose (general→specific,
|
|
209
253
|
* then tree position, then filename — no tree chrome); every
|
|
210
|
-
* other winner (preview/name/hidden-bootless) folds into ONE catalog tree
|
|
211
|
-
*
|
|
212
|
-
|
|
254
|
+
* other winner (preview/name/hidden-bootless) folds into ONE catalog tree,
|
|
255
|
+
* which additionally carries the members that the content deliveries'
|
|
256
|
+
* directory listings disclosed.
|
|
257
|
+
*
|
|
258
|
+
* This function owns exposure registration for the boot renders: every doc
|
|
259
|
+
* registers at the rung it ACTUALLY rendered at, here, rather than being
|
|
260
|
+
* registered wholesale by the caller beforehand — which would have let a body
|
|
261
|
+
* already delivered as a companion in another partition render again as
|
|
262
|
+
* prose. A caller that passes no `target` renders against a throwaway ledger
|
|
263
|
+
* (the `sys context` diagnostics), so its output matches live boot without
|
|
264
|
+
* recording anything. */
|
|
265
|
+
function renderGrouped(winners, rootLabel, subject, target, listingDocs = winners, accounting) {
|
|
266
|
+
const ledger = target ?? exposureTarget(emptyContextExposureState(), 'transcript');
|
|
213
267
|
const content = [];
|
|
214
268
|
const rest = [];
|
|
215
269
|
for (const d of winners) {
|
|
@@ -221,11 +275,52 @@ function renderGrouped(winners, rootLabel) {
|
|
|
221
275
|
content.sort((a, b) => scopeGeneralityRank(a) - scopeGeneralityRank(b) ||
|
|
222
276
|
compareTreePosition(a, b) ||
|
|
223
277
|
a.name.localeCompare(b.name));
|
|
224
|
-
|
|
278
|
+
// A body this context already holds at content does not render twice, even
|
|
279
|
+
// when the other delivery was a companion in the other boot partition.
|
|
280
|
+
const fresh = content.filter((d) => !documentExposedAtOrAbove(ledger.state, d.path, d.body, 'content'));
|
|
281
|
+
const contentProse = fresh
|
|
225
282
|
.map((d) => d.body.trim())
|
|
226
283
|
.filter((b) => b !== '')
|
|
227
284
|
.join('\n\n');
|
|
228
|
-
|
|
285
|
+
// Register the prose BEFORE its attachments run, so the listings and
|
|
286
|
+
// companion routes dedupe against the bodies this render just delivered.
|
|
287
|
+
for (const d of fresh) {
|
|
288
|
+
if (!rendersAtBoot(d))
|
|
289
|
+
continue;
|
|
290
|
+
registerDocumentExposure(ledger, d.path, d.body, d.bootRung);
|
|
291
|
+
accountDelivery(accounting, d, d.bootRung, undefined, d.source);
|
|
292
|
+
}
|
|
293
|
+
// ONE options object for the whole pass: `dirs` accumulates across every
|
|
294
|
+
// content doc AND every companion it routes, and the shared corpus keeps the
|
|
295
|
+
// listing tree built once.
|
|
296
|
+
const attachments = {
|
|
297
|
+
subject: subject ?? null,
|
|
298
|
+
target: ledger,
|
|
299
|
+
byName: docsByName(listingDocs.map((d) => d.source)),
|
|
300
|
+
listings: 'tree',
|
|
301
|
+
dirs: [],
|
|
302
|
+
accounting,
|
|
303
|
+
};
|
|
304
|
+
const attachmentBlocks = fresh.flatMap((doc) => contentReadAttachments(doc, attachments).blocks);
|
|
305
|
+
const raised = exposedDirEntries(listingDocs, attachments.dirs ?? [], ledger);
|
|
306
|
+
const raisedByPath = new Map(raised.map((d) => [d.path, d]));
|
|
307
|
+
const restPaths = new Set(rest.map((d) => d.path));
|
|
308
|
+
const treeDocs = [
|
|
309
|
+
...rest.map((d) => raisedByPath.get(d.path) ?? d),
|
|
310
|
+
...raised.filter((d) => !restPaths.has(d.path)),
|
|
311
|
+
];
|
|
312
|
+
for (const d of treeDocs) {
|
|
313
|
+
if (!rendersAtBoot(d))
|
|
314
|
+
continue;
|
|
315
|
+
registerDocumentExposure(ledger, d.path, d.body, d.bootRung);
|
|
316
|
+
accountDelivery(accounting, d, d.bootRung, raisedByPath.has(d.path) ? 'directory listing attached to content delivery' : undefined, d.source);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
contentProse,
|
|
320
|
+
tree: buildTree(treeDocs, rootLabel),
|
|
321
|
+
contentDocs: fresh,
|
|
322
|
+
attachmentBlocks,
|
|
323
|
+
};
|
|
229
324
|
}
|
|
230
325
|
function leafSegment(name) {
|
|
231
326
|
const i = name.lastIndexOf('/');
|
|
@@ -456,7 +551,7 @@ export function buildSubPersonaMenu(kind) {
|
|
|
456
551
|
* across nodes is preserved. ALWAYS returns a non-empty splice — the guidance
|
|
457
552
|
* frame is unconditional, so it is never empty even when no preference is
|
|
458
553
|
* eligible. */
|
|
459
|
-
function buildPreferencesBlock(subject, target) {
|
|
554
|
+
function buildPreferencesBlock(subject, target, accounting) {
|
|
460
555
|
let contentDocs = [];
|
|
461
556
|
let tree = '';
|
|
462
557
|
let protocol = '';
|
|
@@ -465,8 +560,7 @@ function buildPreferencesBlock(subject, target) {
|
|
|
465
560
|
// No nodeId: a node-local doc rides the knowledge partition whatever its
|
|
466
561
|
// kind, so it never reaches the preference block.
|
|
467
562
|
const winners = bootWinners(bootPlan(subject, null), 'preference');
|
|
468
|
-
|
|
469
|
-
const grouped = renderGrouped(winners, 'preferences');
|
|
563
|
+
const grouped = renderGrouped(winners, 'preferences', subject, target, winners, accounting);
|
|
470
564
|
contentDocs = grouped.contentDocs;
|
|
471
565
|
tree = grouped.tree;
|
|
472
566
|
const shipped = contentDocs
|
|
@@ -481,7 +575,7 @@ function buildPreferencesBlock(subject, target) {
|
|
|
481
575
|
})
|
|
482
576
|
.filter((b) => b !== '');
|
|
483
577
|
const subPersonaMenu = buildSubPersonaMenu(subject.kind);
|
|
484
|
-
protocol = [...shipped, ...(subPersonaMenu === '' ? [] : [subPersonaMenu])].join('\n\n');
|
|
578
|
+
protocol = [...shipped, ...grouped.attachmentBlocks, ...(subPersonaMenu === '' ? [] : [subPersonaMenu])].join('\n\n');
|
|
485
579
|
if (editable.length > 0 || tree !== '') {
|
|
486
580
|
memBody += `\n\n${PREFERENCES_INTRO}`;
|
|
487
581
|
if (editable.length > 0)
|
|
@@ -494,8 +588,8 @@ function buildPreferencesBlock(subject, target) {
|
|
|
494
588
|
const memoryBlock = `<memory kind="preference">\n${memBody}\n</memory>`;
|
|
495
589
|
return { block: protocol === '' ? memoryBlock : `${protocol}\n\n${memoryBlock}`, contentDocs, tree };
|
|
496
590
|
}
|
|
497
|
-
export function renderPreferencesForSubject(subject, target) {
|
|
498
|
-
return buildPreferencesBlock(subject, target);
|
|
591
|
+
export function renderPreferencesForSubject(subject, target, accounting) {
|
|
592
|
+
return buildPreferencesBlock(subject, target, accounting);
|
|
499
593
|
}
|
|
500
594
|
// 2. Knowledge — `<memory kind="knowledge">` (the first-message bearings).
|
|
501
595
|
/** The `<memory kind="knowledge">` block embedded in the session_start bearings
|
|
@@ -510,15 +604,15 @@ export function renderPreferencesForSubject(subject, target) {
|
|
|
510
604
|
* renders: floored docs never reach the `content` rung). Procedural guidance and factual
|
|
511
605
|
* references both live here as `knowledge`. Returns '' when nothing is
|
|
512
606
|
* eligible. */
|
|
513
|
-
export function renderKnowledgeForSubject(subject, nodeId, target) {
|
|
607
|
+
export function renderKnowledgeForSubject(subject, nodeId, target, accounting) {
|
|
514
608
|
const winners = bootWinners(bootPlan(subject, nodeId), 'knowledge');
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
if (
|
|
609
|
+
const { contentProse, tree, attachmentBlocks } = renderGrouped(winners, 'knowledge', subject, target, winners, accounting);
|
|
610
|
+
const deliveredContent = [contentProse, ...attachmentBlocks].filter((block) => block !== '').join('\n\n');
|
|
611
|
+
if (deliveredContent === '' && tree === '')
|
|
518
612
|
return '';
|
|
519
613
|
let out = KNOWLEDGE_INTRO;
|
|
520
|
-
if (
|
|
521
|
-
out += `\n\n${
|
|
614
|
+
if (deliveredContent !== '')
|
|
615
|
+
out += `\n\n${deliveredContent}`;
|
|
522
616
|
if (tree !== '')
|
|
523
617
|
out += `\n\n${tree}`;
|
|
524
618
|
out += `\n\n${KNOWLEDGE_OUTRO}`;
|
|
@@ -549,9 +643,7 @@ function undelivered(target, docs) {
|
|
|
549
643
|
export function renderConfigChangeDelta(subject, nodeId, target) {
|
|
550
644
|
const plan = bootPlan(subject, nodeId);
|
|
551
645
|
const prefs = undelivered(target, bootWinners(plan, 'preference'));
|
|
552
|
-
const
|
|
553
|
-
recordRendered(target, [...prefs, ...knowledgeDelta]);
|
|
554
|
-
const { contentDocs, tree } = renderGrouped(prefs, 'preferences');
|
|
646
|
+
const { contentDocs, tree, attachmentBlocks: preferenceAttachments } = renderGrouped(prefs, 'preferences', subject, target, bootWinners(plan, 'preference'));
|
|
555
647
|
// Same split as the boot preference render: shipped bodies are protocol prose
|
|
556
648
|
// (the rules of the node's world), agent-editable bodies are memory the node
|
|
557
649
|
// can revise, and every non-content preference winner rides the one catalog.
|
|
@@ -567,8 +659,8 @@ export function renderConfigChangeDelta(subject, nodeId, target) {
|
|
|
567
659
|
})
|
|
568
660
|
.filter((b) => b !== '');
|
|
569
661
|
const blocks = [];
|
|
570
|
-
if (shipped.length > 0)
|
|
571
|
-
blocks.push(`${DELTA_PROTOCOL_INTRO}\n\n${shipped.join('\n\n')}`);
|
|
662
|
+
if (shipped.length > 0 || preferenceAttachments.length > 0)
|
|
663
|
+
blocks.push(`${DELTA_PROTOCOL_INTRO}\n\n${[...shipped, ...preferenceAttachments].join('\n\n')}`);
|
|
572
664
|
if (editable.length > 0 || tree !== '') {
|
|
573
665
|
let body = DELTA_PREFERENCES_INTRO;
|
|
574
666
|
if (editable.length > 0)
|
|
@@ -578,11 +670,14 @@ export function renderConfigChangeDelta(subject, nodeId, target) {
|
|
|
578
670
|
body += `\n\n${PREFERENCES_OUTRO}`;
|
|
579
671
|
blocks.push(`<memory kind="preference">\n${body}\n</memory>`);
|
|
580
672
|
}
|
|
581
|
-
|
|
582
|
-
|
|
673
|
+
// Computed AFTER the preference render: that pass may have delivered a
|
|
674
|
+
// knowledge body as a companion, and this delta must not repeat it.
|
|
675
|
+
const knowledge = renderGrouped(undelivered(target, bootWinners(plan, 'knowledge')), 'knowledge', subject, target, bootWinners(plan, 'knowledge'));
|
|
676
|
+
const knowledgeContent = [knowledge.contentProse, ...knowledge.attachmentBlocks].filter((block) => block !== '').join('\n\n');
|
|
677
|
+
if (knowledgeContent !== '' || knowledge.tree !== '') {
|
|
583
678
|
let body = DELTA_KNOWLEDGE_INTRO;
|
|
584
|
-
if (
|
|
585
|
-
body += `\n\n${
|
|
679
|
+
if (knowledgeContent !== '')
|
|
680
|
+
body += `\n\n${knowledgeContent}`;
|
|
586
681
|
if (knowledge.tree !== '')
|
|
587
682
|
body += `\n\n${knowledge.tree}`;
|
|
588
683
|
body += `\n\n${KNOWLEDGE_OUTRO}`;
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// auth.json FILE WRITE only — the plan's sanctioned substitution.
|
|
26
26
|
import { test, before, after } from 'node:test';
|
|
27
27
|
import assert from 'node:assert/strict';
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
28
29
|
import { statSync, existsSync, readFileSync, mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
29
30
|
import { tmpdir } from 'node:os';
|
|
30
31
|
import { join, dirname } from 'node:path';
|
|
@@ -33,8 +34,10 @@ import { createHeadlessHarness } from '../../../../core/__tests__/helpers/harnes
|
|
|
33
34
|
import { createAttachKit } from '../../../../core/__tests__/helpers/broker-clients.js';
|
|
34
35
|
import { ApiError, CrtrClient } from '../../../../api/index.js';
|
|
35
36
|
import { createApiServer } from '../../server.js';
|
|
37
|
+
import { createNode } from '../../../../core/canvas/canvas.js';
|
|
36
38
|
import { apiSocketPath, inboxPath } from '../../../../core/canvas/paths.js';
|
|
37
39
|
import { isPidAlive } from '../../../../core/canvas/pid.js';
|
|
40
|
+
import { clearBusy, markBusy } from '../../../../core/runtime/busy.js';
|
|
38
41
|
let h;
|
|
39
42
|
let server;
|
|
40
43
|
let client;
|
|
@@ -79,6 +82,37 @@ test('A1: /healthz is ok and the unix socket is bound mode 0600', async () => {
|
|
|
79
82
|
const mode = statSync(sockPath).mode & 0o777;
|
|
80
83
|
assert.equal(mode, 0o600, `socket mode is 0600 (fs-perms-as-auth), got 0o${mode.toString(8)}`);
|
|
81
84
|
});
|
|
85
|
+
test('status counts only live, busy non-human engine rows', async () => {
|
|
86
|
+
const agent = h.fabricateBrokerNode({ pi_pid: process.pid });
|
|
87
|
+
const deadPid = spawnSync(process.execPath, ['-e', '']).pid;
|
|
88
|
+
assert.ok(deadPid !== undefined, 'dead-pid fixture process started');
|
|
89
|
+
const deadAgent = h.fabricateBrokerNode({ pi_pid: deadPid });
|
|
90
|
+
const bridge = `human-status-${process.pid}`;
|
|
91
|
+
createNode({
|
|
92
|
+
node_id: bridge,
|
|
93
|
+
name: bridge,
|
|
94
|
+
created: new Date().toISOString(),
|
|
95
|
+
cwd: process.cwd(),
|
|
96
|
+
kind: 'human',
|
|
97
|
+
mode: 'base',
|
|
98
|
+
lifecycle: 'terminal',
|
|
99
|
+
status: 'active',
|
|
100
|
+
host_kind: 'broker',
|
|
101
|
+
pi_pid: process.pid,
|
|
102
|
+
});
|
|
103
|
+
const baseline = (await client.status()).busy_node_count;
|
|
104
|
+
markBusy(agent);
|
|
105
|
+
markBusy(deadAgent);
|
|
106
|
+
markBusy(bridge);
|
|
107
|
+
try {
|
|
108
|
+
assert.equal((await client.status()).busy_node_count, baseline + 1);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
clearBusy(agent);
|
|
112
|
+
clearBusy(deadAgent);
|
|
113
|
+
clearBusy(bridge);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
82
116
|
test('attention counts reads only the requested node slice', async () => {
|
|
83
117
|
const root = h.spawnRoot('attention root');
|
|
84
118
|
const result = await client.attentionCounts([root, 'unknown-node']);
|
|
@@ -266,9 +266,25 @@ function handleHistoryRead(ctx) {
|
|
|
266
266
|
}
|
|
267
267
|
// snapshot \u2014 the enriched browser canvas roster + subscribes_to adjacency.
|
|
268
268
|
// Lifted from `commands/canvas-snapshot.ts` run() (dashboardRowsAll + enrichRows).
|
|
269
|
-
|
|
269
|
+
// `?live=true` selects active/idle rows before the row builder's disk reads.
|
|
270
|
+
function snapshotLiveOnly(ctx) {
|
|
271
|
+
for (const [name] of ctx.query) {
|
|
272
|
+
if (name !== 'live')
|
|
273
|
+
throw usage(`unknown snapshot query parameter: ${name}`);
|
|
274
|
+
}
|
|
275
|
+
const values = ctx.query.getAll('live');
|
|
276
|
+
if (values.length > 1)
|
|
277
|
+
throw usage('live must appear at most once');
|
|
278
|
+
const value = values[0];
|
|
279
|
+
if (value === undefined || value === 'false' || value === '0')
|
|
280
|
+
return false;
|
|
281
|
+
if (value === 'true' || value === '1')
|
|
282
|
+
return true;
|
|
283
|
+
throw usage('live must be true or false');
|
|
284
|
+
}
|
|
285
|
+
async function handleSnapshot(ctx) {
|
|
270
286
|
const focusedNodeIds = new Set(listFocuses().map((f) => f.node_id));
|
|
271
|
-
const rows = await dashboardRowsAllFromSource(localCanvasSource, focusedNodeIds);
|
|
287
|
+
const rows = await dashboardRowsAllFromSource(localCanvasSource, focusedNodeIds, snapshotLiveOnly(ctx));
|
|
272
288
|
const asks = ticketCountsForNodes(rows.map((r) => r.node_id));
|
|
273
289
|
await enrichRowsFromSource(localCanvasSource, rows, asks);
|
|
274
290
|
const nodes = rows.map((row) => {
|