@north-light/crouter 0.3.207 → 0.3.209
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/broker-ops.d.ts +2 -12
- package/dist/api/dto/nodes.d.ts +16 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base.md +3 -2
- package/dist/builtin-memory/01-spine/00-has-manager.md +3 -2
- package/dist/builtin-memory/01-spine/01-no-manager.md +3 -2
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -2
- package/dist/builtin-memory/02-lifecycle/01-resident.md +3 -2
- package/dist/builtin-memory/04-base-worker.md +3 -2
- package/dist/builtin-memory/04-orchestration-kernel.md +3 -2
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/design/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/developer/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/explore/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +3 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -2
- package/dist/builtin-memory/advisor/council.md +0 -2
- package/dist/builtin-memory/design.md +3 -2
- package/dist/builtin-memory/development.md +3 -2
- package/dist/builtin-memory/insights/capture.md +1 -3
- package/dist/builtin-memory/insights/init.md +1 -3
- package/dist/builtin-memory/insights/listen.md +3 -2
- package/dist/builtin-memory/internal/INDEX.md +5 -4
- package/dist/builtin-memory/internal/agent-shaping.md +5 -4
- package/dist/builtin-memory/internal/examples/INDEX.md +3 -2
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -2
- package/dist/builtin-memory/internal/marketplaces.md +3 -2
- package/dist/builtin-memory/internal/memory-loading.md +31 -20
- package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -2
- package/dist/builtin-memory/internal/plugins.md +7 -6
- package/dist/builtin-memory/internal/storage-tiers.md +3 -2
- package/dist/builtin-memory/plan/roadmap.md +3 -2
- package/dist/builtin-memory/spec/guide.md +0 -2
- package/dist/builtin-memory/spec/requirements.md +0 -2
- package/dist/builtin-memory/spec/roadmap.md +3 -2
- package/dist/builtin-memory/testing.md +1 -3
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +3 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +1 -4
- package/dist/clients/attach/__tests__/group-activity.test.js +8 -1
- package/dist/clients/attach/render/card-presentation.d.ts +31 -0
- package/dist/clients/attach/render/card-presentation.js +163 -0
- package/dist/clients/attach/render/chat-view.d.ts +0 -2
- package/dist/clients/attach/render/chat-view.js +12 -23
- package/dist/clients/attach/render/context-message.d.ts +5 -9
- package/dist/clients/attach/render/context-message.js +20 -43
- package/dist/clients/attach/render/group-activity.d.ts +28 -4
- package/dist/clients/attach/render/group-activity.js +65 -11
- package/dist/clients/attach/render/group-recap.d.ts +3 -0
- package/dist/clients/attach/render/group-recap.js +17 -0
- package/dist/clients/attach/viewer.js +516 -516
- package/dist/commands/memory/delete.js +3 -18
- package/dist/commands/memory/edit.js +23 -30
- package/dist/commands/memory/history.js +1 -1
- package/dist/commands/memory/lint.d.ts +7 -8
- package/dist/commands/memory/lint.js +178 -132
- package/dist/commands/memory/list.d.ts +0 -1
- package/dist/commands/memory/list.js +2 -12
- package/dist/commands/memory/move.d.ts +1 -0
- package/dist/commands/memory/move.js +195 -0
- package/dist/commands/memory/read.js +135 -141
- package/dist/commands/memory/shared.d.ts +18 -17
- package/dist/commands/memory/shared.js +93 -39
- package/dist/commands/memory/write.js +23 -33
- package/dist/commands/memory.js +5 -4
- package/dist/commands/pkg/browse/catalog.js +2 -4
- package/dist/commands/pkg/browse/doc-view.js +17 -11
- package/dist/commands/pkg/browse/model.d.ts +7 -9
- package/dist/commands/sys/migrate.d.ts +1 -0
- package/dist/commands/sys/migrate.js +106 -0
- package/dist/commands/sys/sync-deps.js +5 -10
- package/dist/commands/sys/sync-project-guidance.js +36 -16
- package/dist/commands/sys/sync-skills.js +8 -4
- package/dist/commands/sys.js +3 -2
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +1 -1
- package/dist/core/__tests__/helpers/broker-clients.d.ts +3 -1
- package/dist/core/__tests__/helpers/broker-clients.js +7 -2
- package/dist/core/__tests__/inline-memory-refs.test.js +8 -5
- package/dist/core/__tests__/memory-resolver-precedence.test.js +5 -4
- package/dist/core/__tests__/nested-store-discovery.test.js +1 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +3 -3
- package/dist/core/__tests__/on-read-dedup-resume.test.js +12 -12
- package/dist/core/__tests__/on-read-nested-store.test.js +23 -20
- package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +9 -0
- package/dist/core/__tests__/serial/broker-snapshot-history.test.js +52 -1
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +1 -1
- package/dist/core/canvas/db.d.ts +3 -1
- package/dist/core/canvas/db.js +12 -2
- package/dist/core/memory/history.d.ts +4 -1
- package/dist/core/memory/history.js +1 -0
- package/dist/core/memory/inline-ref-inventory.d.ts +5 -4
- package/dist/core/memory/inline-ref-inventory.js +23 -19
- package/dist/core/memory-resolver.d.ts +4 -4
- package/dist/core/memory-resolver.js +16 -43
- package/dist/core/runtime/bearings.d.ts +4 -3
- package/dist/core/runtime/bearings.js +4 -4
- package/dist/core/runtime/broker/fault-retry.js +2 -7
- package/dist/core/runtime/broker/frame-dispatch.js +1 -1
- package/dist/core/runtime/broker/tool-groups.js +7 -6
- package/dist/core/runtime/broker-extension-render.d.ts +3 -2
- package/dist/core/runtime/broker-extension-render.js +3 -3
- package/dist/core/runtime/broker.js +4 -4
- package/dist/core/runtime/kickoff.js +4 -10
- package/dist/core/runtime/memory.js +2 -3
- package/dist/core/runtime/node-read.js +7 -1
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/core/substrate/index.d.ts +7 -4
- package/dist/core/substrate/index.js +6 -4
- package/dist/core/substrate/injected-store.d.ts +24 -12
- package/dist/core/substrate/injected-store.js +80 -33
- package/dist/core/substrate/listings.d.ts +21 -0
- package/dist/core/substrate/listings.js +88 -0
- package/dist/core/substrate/on-read-node.d.ts +5 -5
- package/dist/core/substrate/on-read-node.js +4 -5
- package/dist/core/substrate/on-read.d.ts +25 -4
- package/dist/core/substrate/on-read.js +81 -102
- package/dist/core/substrate/render-node.d.ts +5 -2
- package/dist/core/substrate/render-node.js +5 -3
- package/dist/core/substrate/render.d.ts +9 -8
- package/dist/core/substrate/render.js +104 -96
- package/dist/core/substrate/schema.d.ts +34 -18
- package/dist/core/substrate/schema.js +75 -32
- package/dist/core/substrate/surface-match.d.ts +32 -0
- package/dist/core/substrate/surface-match.js +179 -0
- package/dist/daemon/api/handlers/nodes.js +9 -0
- package/dist/daemon/review/comment-notify.js +1 -8
- package/dist/daemon/review/deliver.js +1 -1
- package/dist/daemon/review/finish.js +1 -1
- package/dist/migrations/001-surfaces-frontmatter.d.ts +2 -0
- package/dist/migrations/001-surfaces-frontmatter.js +276 -0
- package/dist/migrations/convergent.d.ts +31 -0
- package/dist/migrations/convergent.js +71 -0
- package/dist/migrations/registry.d.ts +2 -0
- package/dist/migrations/registry.js +19 -0
- package/dist/migrations/types.d.ts +40 -0
- package/dist/migrations/types.js +11 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
- package/dist/pi-extensions/canvas-context-intro.d.ts +2 -1
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +2 -1
- package/dist/pi-extensions/canvas-doc-substrate.js +57 -34
- package/dist/pi-extensions/canvas-review-boundary.js +1 -1
- package/dist/shared/__tests__/generated-context-grammar.test.js +19 -22
- package/dist/shared/generated-context.d.ts +0 -18
- package/dist/shared/generated-context.js +16 -120
- package/dist/shared/tool-groups.d.ts +1 -0
- package/dist/shared/tool-groups.js +7 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/core/substrate/ceiling.d.ts +0 -17
- package/dist/core/substrate/ceiling.js +0 -67
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// `crtr sys sync project-guidance` — one-way migration of a project's own
|
|
2
2
|
// CLAUDE.md/AGENTS.md/.claude/rules into that SAME directory's OWN
|
|
3
|
-
// `.crouter/memory/`. Each project guide becomes the
|
|
4
|
-
// rules
|
|
5
|
-
//
|
|
3
|
+
// `.crouter/memory/`. Each project guide becomes the workspace front door
|
|
4
|
+
// (workspace-open + ./** read entries); rules keep their explicit path routes
|
|
5
|
+
// as read entries, or take the front-door pair when they are pathless.
|
|
6
6
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
7
7
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
8
8
|
import { defineLeaf } from '../../core/command.js';
|
|
@@ -218,9 +218,30 @@ function projectDocLabel(candidate) {
|
|
|
218
218
|
.replace(/^\.$/, '');
|
|
219
219
|
return relDir === '' ? rootLabel : normalizedName(`${rootLabel}/${relDir}`);
|
|
220
220
|
}
|
|
221
|
+
/** The surfaces pair every workspace front door carries: whole-doc delivery
|
|
222
|
+
* when cwd/profile mounts the store, plus on any file read beneath the
|
|
223
|
+
* store's owning dir. */
|
|
224
|
+
function frontDoorSurfaces() {
|
|
225
|
+
return [
|
|
226
|
+
{ on: 'workspace-open', at: 'content' },
|
|
227
|
+
{ on: 'read', match: './**', at: 'content' },
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
/** `./`-anchor a rule glob that could only match repo-relatively: any glob
|
|
231
|
+
* containing `/` that starts with neither `/`, `**`, nor `./`. Mirrors the
|
|
232
|
+
* surfaces migration's anchoring rule — this leaf MINTS new read globs, so it
|
|
233
|
+
* must anchor them the same way or the routes it writes silently never fire. */
|
|
234
|
+
function anchorReadGlob(g) {
|
|
235
|
+
const t = g.trim();
|
|
236
|
+
if (!t.includes('/'))
|
|
237
|
+
return t;
|
|
238
|
+
if (t.startsWith('/') || t.startsWith('**') || t.startsWith('./'))
|
|
239
|
+
return t;
|
|
240
|
+
return `./${t}`;
|
|
241
|
+
}
|
|
221
242
|
/** AGENTS.md/CLAUDE.md → `<sourceDir>/.crouter/memory/INDEX.md`, the
|
|
222
243
|
* workspace front door. Its explicit project identity keeps sibling roots
|
|
223
|
-
* distinct;
|
|
244
|
+
* distinct; the workspace-open + ./** read entry pair puts the wholly
|
|
224
245
|
* important guide in first-message context when cwd/profile mounts the store,
|
|
225
246
|
* and surfaces it on any file read beneath the store's owning dir. */
|
|
226
247
|
function prepareAgentsCandidate(candidate) {
|
|
@@ -231,9 +252,7 @@ function prepareAgentsCandidate(candidate) {
|
|
|
231
252
|
name: docLabel,
|
|
232
253
|
'when-and-why-to-read': `When working in ${docLabel}, this knowledge should be read because it is the project's operating guide.`,
|
|
233
254
|
'short-form': synthesizeShortForm(body),
|
|
234
|
-
|
|
235
|
-
'file-read-visibility': 'content',
|
|
236
|
-
'applies-to': ['.'],
|
|
255
|
+
surfaces: frontDoorSurfaces(),
|
|
237
256
|
};
|
|
238
257
|
return {
|
|
239
258
|
candidate,
|
|
@@ -244,9 +263,10 @@ function prepareAgentsCandidate(candidate) {
|
|
|
244
263
|
};
|
|
245
264
|
}
|
|
246
265
|
/** `.claude/rules/<name>.md` → `<sourceDir>/.crouter/memory/<name>.md`. A
|
|
247
|
-
* `paths:` rule keeps its
|
|
248
|
-
*
|
|
249
|
-
* file beneath the store's owning dir is read.
|
|
266
|
+
* `paths:` rule keeps its file globs as a `content` read entry (`./`-anchored
|
|
267
|
+
* when repo-relative); a pathless rule takes the front-door pair and surfaces
|
|
268
|
+
* when the workspace opens or a file beneath the store's owning dir is read.
|
|
269
|
+
* Like the INDEX docs, each rule
|
|
250
270
|
* carries an explicit namespaced substrate name (`<root>/<subdir>/<leaf>`):
|
|
251
271
|
* without it the resolver falls back to the store-relative leaf, and two
|
|
252
272
|
* nested stores sharing a rule filename (e.g. apps/core and apps/gateway both
|
|
@@ -266,11 +286,11 @@ function prepareRuleCandidate(candidate) {
|
|
|
266
286
|
? routeFromDescription(description, 'knowledge')
|
|
267
287
|
: `When working under ${dirLabel}, this knowledge should be read because it is migrated project rule guidance.`,
|
|
268
288
|
'short-form': description !== '' ? description.replace(/\s+/g, ' ').trim() : synthesizeShortForm(parsed.body),
|
|
269
|
-
'system-prompt-visibility': 'none',
|
|
270
|
-
'file-read-visibility': 'content',
|
|
271
289
|
};
|
|
272
|
-
const globs = normalizeRulePaths(fm['paths']);
|
|
273
|
-
frontmatter['
|
|
290
|
+
const globs = normalizeRulePaths(fm['paths']).map(anchorReadGlob);
|
|
291
|
+
frontmatter['surfaces'] = globs.length > 0
|
|
292
|
+
? [{ on: 'read', match: globs.length === 1 ? globs[0] : globs, at: 'content' }]
|
|
293
|
+
: frontDoorSurfaces();
|
|
274
294
|
return {
|
|
275
295
|
candidate,
|
|
276
296
|
name,
|
|
@@ -321,7 +341,7 @@ function renderSummary(results) {
|
|
|
321
341
|
export const sysSyncProjectGuidanceLeaf = defineLeaf({
|
|
322
342
|
name: 'project-guidance',
|
|
323
343
|
description: "migrate CLAUDE.md/AGENTS.md/.claude/rules into that directory's own crouter memory docs",
|
|
324
|
-
whenToUse: "migrating per-project CLAUDE.md/AGENTS.md/.claude/rules into the crouter memory-doc model: one-way copy into that same directory's OWN .crouter/memory/. The project guide becomes its
|
|
344
|
+
whenToUse: "migrating per-project CLAUDE.md/AGENTS.md/.claude/rules into the crouter memory-doc model: one-way copy into that same directory's OWN .crouter/memory/. The project guide becomes its workspace front door; path-scoped rules keep their globs as read entries and pathless rules open with the workspace. Never exports memory docs back to these source files.",
|
|
325
345
|
help: {
|
|
326
346
|
name: 'sys sync project-guidance',
|
|
327
347
|
summary: "one-way migration: CLAUDE.md/AGENTS.md/.claude/rules → that dir's own crouter memory docs",
|
|
@@ -338,7 +358,7 @@ export const sysSyncProjectGuidanceLeaf = defineLeaf({
|
|
|
338
358
|
],
|
|
339
359
|
outputKind: 'object',
|
|
340
360
|
effects: [
|
|
341
|
-
"Copies each discovered CLAUDE.md/AGENTS.md into <dir>/.crouter/memory/INDEX.md with an explicit project name
|
|
361
|
+
"Copies each discovered CLAUDE.md/AGENTS.md into <dir>/.crouter/memory/INDEX.md with an explicit project name and the workspace front-door surfaces pair ({on: workspace-open, at: content} + {on: read, match: './**', at: content}). Copies each .claude/rules/*.md into <dir>/.crouter/memory/<name>.md with a content read entry over the source paths (‘./’-anchored when repo-relative), or the same front-door pair when no paths are declared. Originals are left untouched.",
|
|
342
362
|
'Skips existing memory docs unless --overwrite is present.',
|
|
343
363
|
'With --dry-run: read-only; writes nothing.',
|
|
344
364
|
],
|
|
@@ -167,6 +167,12 @@ function convertPrepared(prepared, opts) {
|
|
|
167
167
|
delete frontmatter.description;
|
|
168
168
|
delete frontmatter.type;
|
|
169
169
|
delete frontmatter.keywords;
|
|
170
|
+
// Retired visibility-axis fields a stale source may still carry — they must
|
|
171
|
+
// never pass through into a substrate doc.
|
|
172
|
+
delete frontmatter['system-prompt-visibility'];
|
|
173
|
+
delete frontmatter['file-read-visibility'];
|
|
174
|
+
delete frontmatter['applies-to'];
|
|
175
|
+
delete frontmatter['read-when'];
|
|
170
176
|
frontmatter.kind = prepared.kind;
|
|
171
177
|
if (typeof frontmatter['when-and-why-to-read'] !== 'string' || frontmatter['when-and-why-to-read'].trim() === '') {
|
|
172
178
|
frontmatter['when-and-why-to-read'] = routeFromDescription(description, prepared.kind);
|
|
@@ -174,10 +180,8 @@ function convertPrepared(prepared, opts) {
|
|
|
174
180
|
if (typeof frontmatter['short-form'] !== 'string' || frontmatter['short-form'].trim() === '') {
|
|
175
181
|
frontmatter['short-form'] = description.replace(/\s+/g, ' ').trim();
|
|
176
182
|
}
|
|
177
|
-
if (
|
|
178
|
-
frontmatter['
|
|
179
|
-
if (typeof frontmatter['file-read-visibility'] !== 'string')
|
|
180
|
-
frontmatter['file-read-visibility'] = 'none';
|
|
183
|
+
if (!Array.isArray(frontmatter['surfaces']))
|
|
184
|
+
frontmatter['surfaces'] = [{ on: 'boot', at: 'preview' }];
|
|
181
185
|
if (!opts.dryRun)
|
|
182
186
|
writeText(prepared.target, serializeMemoryDoc(frontmatter, prepared.parsed.body));
|
|
183
187
|
return {
|
package/dist/commands/sys.js
CHANGED
|
@@ -11,19 +11,20 @@ import { supportBranch } from './sys/support.js';
|
|
|
11
11
|
import { sysSyspromptLeaf } from './sys/sysprompt.js';
|
|
12
12
|
import { promptReviewLeaf } from './sys/prompt-review.js';
|
|
13
13
|
import { sysSyncBranch } from './sys/sync.js';
|
|
14
|
+
import { sysMigrateLeaf } from './sys/migrate.js';
|
|
14
15
|
import { sysUpdateLeaf, sysVersionLeaf } from './sys/update.js';
|
|
15
16
|
export function registerSys() {
|
|
16
17
|
return defineBranch({
|
|
17
18
|
name: 'sys',
|
|
18
19
|
rootEntry: {
|
|
19
20
|
concept: 'crtr configuration, diagnostics, and self-management',
|
|
20
|
-
desc: 'settings, config, setup, doctor, sysprompt, prompt-review, sync, update, version, feedback, logs, support, daemon',
|
|
21
|
+
desc: 'settings, config, setup, doctor, sysprompt, prompt-review, sync, migrate, update, version, feedback, logs, support, daemon',
|
|
21
22
|
useWhen: 'managing the crtr installation, inspecting diagnostics, or operating the crtrd supervisor process',
|
|
22
23
|
},
|
|
23
24
|
help: {
|
|
24
25
|
name: 'sys',
|
|
25
26
|
summary: 'crtr system configuration, settings, diagnostics, and self-management',
|
|
26
27
|
},
|
|
27
|
-
children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysSyncBranch, daemonBranch],
|
|
28
|
+
children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysMigrateLeaf, sysSyncBranch, daemonBranch],
|
|
28
29
|
});
|
|
29
30
|
}
|
|
@@ -106,7 +106,7 @@ test('coalesce renders one inbox card whose entries inline short report bodies a
|
|
|
106
106
|
const card = parseCard({ role: 'user', content: digest });
|
|
107
107
|
assert.notEqual(card, null);
|
|
108
108
|
assert.equal(card.kind, 'inbox');
|
|
109
|
-
assert.deepEqual({ ...card.facts }, {
|
|
109
|
+
assert.deepEqual({ ...card.facts }, {});
|
|
110
110
|
assert.equal(card.senders.length, 1);
|
|
111
111
|
assert.equal(card.senders[0].id, 'child');
|
|
112
112
|
// The label is NOT reprinted above the body it duplicates.
|
|
@@ -25,7 +25,9 @@ export interface RawClient {
|
|
|
25
25
|
}
|
|
26
26
|
export interface AttachKit {
|
|
27
27
|
/** Connect a BrokerClient to a node's running broker, hello, await welcome. */
|
|
28
|
-
attach(id: string, role: ClientRole, clientId: string
|
|
28
|
+
attach(id: string, role: ClientRole, clientId: string, opts?: {
|
|
29
|
+
snapshotTail?: number;
|
|
30
|
+
}): Promise<Attached>;
|
|
29
31
|
/** Attach (as `role`) and retry until the welcome satisfies `pred`. */
|
|
30
32
|
attachUntil(id: string, role: ClientRole, clientId: string, pred: (a: Attached) => boolean, label: string): Promise<Attached>;
|
|
31
33
|
/** A raw node:net peer. read:false leaves the socket PAUSED (G8 stalled viewer). */
|
|
@@ -37,7 +37,7 @@ export function createAttachKit(getH) {
|
|
|
37
37
|
// awaitBoot returns once the boot proof is written — which is BEFORE the broker's
|
|
38
38
|
// server.listen() binds view.sock — so a fresh attach can momentarily race the
|
|
39
39
|
// bind; retry the connect on BrokerUnavailable until it is listening.
|
|
40
|
-
async function attach(id, role, clientId) {
|
|
40
|
+
async function attach(id, role, clientId, opts = {}) {
|
|
41
41
|
const h = getH();
|
|
42
42
|
await h.waitFor(() => existsSync(h.brokerSock(id)), { label: `view.sock for ${id}`, timeoutMs: 20_000 });
|
|
43
43
|
const frames = [];
|
|
@@ -65,7 +65,12 @@ export function createAttachKit(getH) {
|
|
|
65
65
|
// round-trip — so a welcome timeout cannot leak a connected socket past the test.
|
|
66
66
|
liveClients.push({ close: () => client.close() });
|
|
67
67
|
const waitFrame = (pred, label, timeoutMs = 15_000) => h.waitFor(() => frames.find(pred) ?? null, { label, timeoutMs });
|
|
68
|
-
client.send({
|
|
68
|
+
client.send({
|
|
69
|
+
type: 'hello',
|
|
70
|
+
role,
|
|
71
|
+
client_id: clientId,
|
|
72
|
+
...(opts.snapshotTail === undefined ? {} : { snapshot_tail: opts.snapshotTail }),
|
|
73
|
+
});
|
|
69
74
|
let welcome;
|
|
70
75
|
try {
|
|
71
76
|
welcome = (await waitFrame((f) => f.type === 'welcome', `welcome for ${clientId}`));
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Load-bearing corpus regression for `buildRefInventory` — the authoritative
|
|
4
4
|
// inventory the broker and terminal attach viewer both resolve inline memory
|
|
5
5
|
// references against. Covers scope precedence dedup, referenceability
|
|
6
|
-
// (both kinds, gated and slash docs),
|
|
6
|
+
// (both kinds, gated and slash docs), directory browse names, malformed-doc omission,
|
|
7
7
|
// metadata-only rows, and stable ordering, all over real on-disk fixtures.
|
|
8
8
|
import { test, describe, before, beforeEach, after } from 'node:test';
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
@@ -17,7 +17,7 @@ import { buildRefInventory } from '../memory/inline-ref-inventory.js';
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Wave C4 — buildRefInventory: the authoritative corpus builder. Fixture-
|
|
19
19
|
// backed (node/project/user scope docs on disk), since precedence dedup,
|
|
20
|
-
// referenceability-ignores-gate/slash, and
|
|
20
|
+
// referenceability-ignores-gate/slash, and directory browse names are all corpus-shape
|
|
21
21
|
// facts that a pure in-memory test can't exercise. Fixture names are prefixed
|
|
22
22
|
// to avoid any collision with the real shipped builtin/user corpus that
|
|
23
23
|
// listAllMemoryDocs() also picks up in this environment.
|
|
@@ -128,12 +128,15 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
|
|
|
128
128
|
assert.equal(refs.find((r) => r.name === 'crtr-inline-ref-fixture-knowledge')?.kind, 'knowledge');
|
|
129
129
|
assert.equal(refs.find((r) => r.name === 'crtr-inline-ref-fixture-preference')?.kind, 'preference');
|
|
130
130
|
});
|
|
131
|
-
test('an INDEX doc
|
|
131
|
+
test('an INDEX doc keeps its canonical name; the bare dir name is a browse link, not a ref row', () => {
|
|
132
132
|
writeDoc(join(projectMemoryDir(), 'crtr-inline-ref-fixture-taste'), 'INDEX.md', "kind: knowledge\nshort-form: 'taste index'");
|
|
133
133
|
const { refs, names } = buildRefInventory();
|
|
134
|
+
assert.ok(names.has('crtr-inline-ref-fixture-taste/INDEX'));
|
|
135
|
+
// The directory name resolves as a listing link — in `names` for linkify,
|
|
136
|
+
// never a RefMeta row (no doc answers for a directory).
|
|
134
137
|
assert.ok(names.has('crtr-inline-ref-fixture-taste'));
|
|
135
|
-
assert.ok(!
|
|
136
|
-
const winner = refs.find((r) => r.name === 'crtr-inline-ref-fixture-taste');
|
|
138
|
+
assert.ok(!refs.some((r) => r.name === 'crtr-inline-ref-fixture-taste'));
|
|
139
|
+
const winner = refs.find((r) => r.name === 'crtr-inline-ref-fixture-taste/INDEX');
|
|
137
140
|
assert.equal(winner?.shortForm, 'taste index');
|
|
138
141
|
});
|
|
139
142
|
test('a root INDEX doc is omitted, never exposed as an empty ref name', () => {
|
|
@@ -128,13 +128,14 @@ test('a plain path-named doc with no explicit name resolves to the nearest copy'
|
|
|
128
128
|
process.chdir(prevCwd);
|
|
129
129
|
}
|
|
130
130
|
});
|
|
131
|
-
test('a bare-directory query
|
|
131
|
+
test('a bare-directory query does not resolve to a doc — directories answer with listings, not INDEX.md', () => {
|
|
132
132
|
process.chdir(child);
|
|
133
133
|
resetScopeCache();
|
|
134
134
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
assert.throws(() => resolveMemoryDoc('taste'), (err) => {
|
|
136
|
+
assert.equal(err.code, 'not_found');
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
138
139
|
}
|
|
139
140
|
finally {
|
|
140
141
|
process.chdir(prevCwd);
|
|
@@ -76,9 +76,7 @@ test('resolveMemoryDocForTarget resolves a nested doc only with includeDescendan
|
|
|
76
76
|
mkdirSync(nestedMemory, { recursive: true });
|
|
77
77
|
writeFileSync(join(nestedMemory, 'guide.md'), '---\nkind: knowledge\n' +
|
|
78
78
|
'name: nested-guide\n' +
|
|
79
|
-
'when-and-why-to-read: When working in sub, this knowledge should be read because it carries the subsystem constraints.\n' +
|
|
80
|
-
'system-prompt-visibility: none\n' +
|
|
81
|
-
'file-read-visibility: none\n---\n' +
|
|
79
|
+
'when-and-why-to-read: When working in sub, this knowledge should be read because it carries the subsystem constraints.\n---\n' +
|
|
82
80
|
'NESTED GUIDE BODY\n');
|
|
83
81
|
const target = { cwd: proj, profileId: null, nodeId: null };
|
|
84
82
|
assert.throws(() => resolveMemoryDocForTarget('nested-guide', target, {}), (e) => e instanceof CrtrError && e.code === 'not_found', 'without the opt-in the flat ancestor stack does not see the nested store');
|
|
@@ -50,15 +50,15 @@ test('a store nested in the crouter home does not leak into a canvas-artifact re
|
|
|
50
50
|
mkdirSync(leakedStore, { recursive: true });
|
|
51
51
|
writeFileSync(join(leakedStore, 'private-thing.md'), '---\nkind: knowledge\n' +
|
|
52
52
|
"when-and-why-to-read: When X, read this because Y.\n" +
|
|
53
|
-
'
|
|
54
|
-
'
|
|
53
|
+
'surfaces:\n' +
|
|
54
|
+
' - {on: read, match: "**/*.md", at: content}\n---\n' +
|
|
55
55
|
'PRIVATE ASSISTANT MEMORY MUST NOT LEAK\n');
|
|
56
56
|
// A canvas runtime artifact under the crouter home — the shape of every node
|
|
57
57
|
// report/context file the harness auto-reads.
|
|
58
58
|
const reportFile = join(fakeHome, '.crouter', 'canvas', 'nodes', 'somenode', 'reports', 'r.md');
|
|
59
59
|
mkdirSync(join(fakeHome, '.crouter', 'canvas', 'nodes', 'somenode', 'reports'), { recursive: true });
|
|
60
60
|
writeFileSync(reportFile, '# a node report\n');
|
|
61
|
-
const rendered = renderOnReadDocs(node, reportFile, new
|
|
61
|
+
const rendered = renderOnReadDocs(node, reportFile, new Map());
|
|
62
62
|
assert.ok(!rendered.includes('PRIVATE ASSISTANT MEMORY MUST NOT LEAK'), `crouter-home-nested store must not surface on a canvas read; got: ${rendered}`);
|
|
63
63
|
assert.ok(!rendered.includes('private-thing'), `crouter-home-nested store name must not surface on a canvas read; got: ${rendered}`);
|
|
64
64
|
});
|
|
@@ -45,7 +45,7 @@ after(() => {
|
|
|
45
45
|
else
|
|
46
46
|
process.env['HOME'] = prevHomeEnv;
|
|
47
47
|
});
|
|
48
|
-
test('project and user memory docs without
|
|
48
|
+
test('project and user memory docs without read surfaces do not fire on file reads', () => {
|
|
49
49
|
const fakeHome = mkdtempSync(join(tmpdir(), 'crtr-onread-user-home-'));
|
|
50
50
|
process.env['HOME'] = fakeHome;
|
|
51
51
|
resetScopeCache();
|
|
@@ -53,18 +53,16 @@ test('project and user memory docs without applies-to do not fire on file reads'
|
|
|
53
53
|
const userMemDir = join(fakeHome, '.crouter', 'memory');
|
|
54
54
|
mkdirSync(userMemDir, { recursive: true });
|
|
55
55
|
writeFileSync(join(userMemDir, 'noisy-global.md'), '---\nkind: preference\n' +
|
|
56
|
-
'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire without an explicit route\n' +
|
|
57
|
-
'file-read-visibility: content\n---\n' +
|
|
56
|
+
'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire without an explicit route\n---\n' +
|
|
58
57
|
'NOISY USER BODY\n');
|
|
59
58
|
const projectMemDir = join(fakeHome, 'work', '.crouter', 'memory');
|
|
60
59
|
mkdirSync(projectMemDir, { recursive: true });
|
|
61
60
|
writeFileSync(join(projectMemDir, 'local.md'), '---\nkind: knowledge\n' +
|
|
62
|
-
'when-and-why-to-read: When reading project files, this reference should be read because this fixture proves explicit file routing\n' +
|
|
63
|
-
'file-read-visibility: content\n---\n' +
|
|
61
|
+
'when-and-why-to-read: When reading project files, this reference should be read because this fixture proves explicit file routing\n---\n' +
|
|
64
62
|
'LOCAL PROJECT BODY\n');
|
|
65
63
|
const readFile = join(fakeHome, 'work', 'ideas.md');
|
|
66
64
|
writeFileSync(readFile, 'turn off xml closer\n');
|
|
67
|
-
const rendered = renderOnReadDocs(node, readFile, new
|
|
65
|
+
const rendered = renderOnReadDocs(node, readFile, new Map());
|
|
68
66
|
assert.ok(!rendered.includes('NOISY USER BODY'), 'user-global doc has no explicit file route');
|
|
69
67
|
assert.ok(!rendered.includes('LOCAL PROJECT BODY'), 'project doc also has no implicit file route');
|
|
70
68
|
assert.equal(rendered, '', 'no file context renders without an explicit trigger');
|
|
@@ -72,13 +70,15 @@ test('project and user memory docs without applies-to do not fire on file reads'
|
|
|
72
70
|
});
|
|
73
71
|
test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfaces after a fresh launch', () => {
|
|
74
72
|
const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
|
|
75
|
-
// A file-routed substrate doc in a `.crouter/memory/` ancestor.
|
|
73
|
+
// A file-routed substrate doc in a `.crouter/memory/` ancestor. The
|
|
74
|
+
// `./`-anchored glob pins the repo-relative anchor: `src/file.ts` matches
|
|
75
|
+
// only relative to the doc's owning repo dir, never absolutely.
|
|
76
76
|
const memDir = join(work, '.crouter', 'memory');
|
|
77
77
|
mkdirSync(memDir, { recursive: true });
|
|
78
78
|
writeFileSync(join(memDir, 'onread-fixture.md'), '---\nkind: knowledge\n' +
|
|
79
79
|
'when-and-why-to-read: When reading work files, this reference should be read because it is the on-read regression fixture\n' +
|
|
80
|
-
'
|
|
81
|
-
'
|
|
80
|
+
'surfaces:\n' +
|
|
81
|
+
' - {on: read, match: "./src/**", at: content}\n---\n' +
|
|
82
82
|
`${FIXTURE_BODY}\n`);
|
|
83
83
|
const readFile = join(work, 'src', 'file.ts');
|
|
84
84
|
mkdirSync(join(work, 'src'), { recursive: true });
|
|
@@ -106,13 +106,13 @@ test('an explicitly routed doc injects on-read under its frontmatter name, not t
|
|
|
106
106
|
writeFileSync(join(memDir, 'physical-guide.md'), '---\nkind: knowledge\n' +
|
|
107
107
|
'name: acme-project\n' +
|
|
108
108
|
'when-and-why-to-read: When reading acme source, this knowledge should be read because it carries the project-specific implementation constraints.\n' +
|
|
109
|
-
'
|
|
110
|
-
'
|
|
109
|
+
'surfaces:\n' +
|
|
110
|
+
' - {on: read, match: "./src/**", at: content}\n---\n' +
|
|
111
111
|
'ACME PROJECT OPERATING GUIDE\n');
|
|
112
112
|
const readFile = join(work, 'src', 'file.ts');
|
|
113
113
|
mkdirSync(join(work, 'src'), { recursive: true });
|
|
114
114
|
writeFileSync(readFile, 'export const x = 1;\n');
|
|
115
|
-
const rendered = renderOnReadDocs(node, readFile, new
|
|
115
|
+
const rendered = renderOnReadDocs(node, readFile, new Map());
|
|
116
116
|
assert.ok(rendered.includes('ACME PROJECT OPERATING GUIDE'), 'the matching explicit route fires');
|
|
117
117
|
assert.ok(rendered.includes('<memory kind="knowledge" name="acme-project"'), `expected explicit frontmatter name "acme-project" in the injected envelope, got: ${rendered}`);
|
|
118
118
|
assert.ok(!rendered.includes('name="physical-guide"'), 'must not fall back to the path-derived name when frontmatter sets one');
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// A `.crouter/memory` store nested BELOW a mounted project root is delivered
|
|
2
|
-
// by the read path, not by boot: its
|
|
3
|
-
// beneath the store's owning dir, deduplicated against the
|
|
4
|
-
// render through the shared transcript dedup set. The
|
|
5
|
-
// to project-store docs — a hand-authored
|
|
6
|
-
// match a read.
|
|
2
|
+
// by the read path, not by boot: its `./**`-routed docs surface on the first
|
|
3
|
+
// read beneath the store's owning dir, deduplicated against the
|
|
4
|
+
// workspace-open render through the shared transcript dedup set. The `./`
|
|
5
|
+
// anchor stays fenced to project-store docs — a hand-authored `./` glob in a
|
|
6
|
+
// user-scope doc must never match a read.
|
|
7
7
|
//
|
|
8
8
|
// Run: node --import tsx/esm --test src/core/__tests__/on-read-nested-store.test.ts
|
|
9
9
|
import { test, before, beforeEach, after } from 'node:test';
|
|
@@ -45,22 +45,25 @@ after(() => {
|
|
|
45
45
|
process.env['HOME'] = prevHomeEnv;
|
|
46
46
|
});
|
|
47
47
|
/** A work tree with a root front door and a nested store front door, both
|
|
48
|
-
*
|
|
48
|
+
* carrying the explicit front-door pair (workspace-open + `./**` read).
|
|
49
|
+
* Returns the work dir. */
|
|
49
50
|
function makeWorkTree() {
|
|
50
51
|
const work = join(fakeHome, 'work');
|
|
51
52
|
const rootStore = join(work, '.crouter', 'memory');
|
|
52
53
|
mkdirSync(rootStore, { recursive: true });
|
|
53
54
|
writeFileSync(join(rootStore, 'INDEX.md'), '---\nkind: knowledge\n' +
|
|
54
55
|
'when-and-why-to-read: When working in this workspace, this knowledge should be read because it is the operating guide.\n' +
|
|
55
|
-
'
|
|
56
|
-
'
|
|
56
|
+
'surfaces:\n' +
|
|
57
|
+
' - {on: workspace-open, at: content}\n' +
|
|
58
|
+
' - {on: read, match: "./**", at: content}\n---\n' +
|
|
57
59
|
`${ROOT_BODY}\n`);
|
|
58
60
|
const nestedStore = join(work, 'pkg', '.crouter', 'memory');
|
|
59
61
|
mkdirSync(nestedStore, { recursive: true });
|
|
60
62
|
writeFileSync(join(nestedStore, 'INDEX.md'), '---\nkind: knowledge\n' +
|
|
61
63
|
'when-and-why-to-read: When working in pkg, this knowledge should be read because it carries the package constraints.\n' +
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
+
'surfaces:\n' +
|
|
65
|
+
' - {on: workspace-open, at: content}\n' +
|
|
66
|
+
' - {on: read, match: "./**", at: content}\n---\n' +
|
|
64
67
|
`${NESTED_BODY}\n`);
|
|
65
68
|
return work;
|
|
66
69
|
}
|
|
@@ -70,18 +73,18 @@ test('a nested store front door surfaces on a read beneath its owning dir, not o
|
|
|
70
73
|
const inside = join(work, 'pkg', 'lib', 'file.ts');
|
|
71
74
|
mkdirSync(join(work, 'pkg', 'lib'), { recursive: true });
|
|
72
75
|
writeFileSync(inside, 'export const x = 1;\n');
|
|
73
|
-
const rendered = renderOnReadDocs(node, inside, new
|
|
76
|
+
const rendered = renderOnReadDocs(node, inside, new Map());
|
|
74
77
|
assert.ok(rendered.includes(NESTED_BODY), `read beneath pkg surfaces the nested front door; got: ${rendered}`);
|
|
75
78
|
const outside = join(work, 'docs', 'notes.ts');
|
|
76
79
|
mkdirSync(join(work, 'docs'), { recursive: true });
|
|
77
80
|
writeFileSync(outside, 'export const y = 2;\n');
|
|
78
|
-
const renderedOutside = renderOnReadDocs(node, outside, new
|
|
81
|
+
const renderedOutside = renderOnReadDocs(node, outside, new Map());
|
|
79
82
|
assert.ok(!renderedOutside.includes(NESTED_BODY), `read outside pkg must not surface the nested front door; got: ${renderedOutside}`);
|
|
80
83
|
});
|
|
81
84
|
test('workspace-open and on-read share one dedup set: root front door once, nested store on first read beneath it', () => {
|
|
82
85
|
const work = makeWorkTree();
|
|
83
86
|
const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
|
|
84
|
-
const seen = new
|
|
87
|
+
const seen = new Map();
|
|
85
88
|
const opened = renderWorkspaceOpenDocs(node, seen);
|
|
86
89
|
assert.ok(opened.includes(ROOT_BODY), 'workspace open delivers the mounted root front door');
|
|
87
90
|
assert.ok(!opened.includes(NESTED_BODY), 'boot stays flat: the nested store is not in the mounted stack');
|
|
@@ -92,18 +95,18 @@ test('workspace-open and on-read share one dedup set: root front door once, nest
|
|
|
92
95
|
assert.ok(onRead.includes(NESTED_BODY), 'the nested front door delivers on the first read beneath it');
|
|
93
96
|
assert.ok(!onRead.includes(ROOT_BODY), 'the root front door already delivered at workspace open — never twice');
|
|
94
97
|
});
|
|
95
|
-
test('a hand-authored
|
|
98
|
+
test('a hand-authored `./`-anchored read glob in a user-scope doc never matches a read', () => {
|
|
96
99
|
const userStore = join(fakeHome, '.crouter', 'memory');
|
|
97
100
|
mkdirSync(userStore, { recursive: true });
|
|
98
101
|
writeFileSync(join(userStore, 'control.md'), '---\nkind: knowledge\n' +
|
|
99
102
|
'when-and-why-to-read: When reading markdown, this knowledge should be read because it is the corpus-loaded control fixture.\n' +
|
|
100
|
-
'
|
|
101
|
-
'
|
|
103
|
+
'surfaces:\n' +
|
|
104
|
+
' - {on: read, match: "**/*.md", at: content}\n---\n' +
|
|
102
105
|
'USER CONTROL BODY\n');
|
|
103
106
|
writeFileSync(join(userStore, 'dot-routed.md'), '---\nkind: knowledge\n' +
|
|
104
107
|
'when-and-why-to-read: When anything, this knowledge should be read because it must never fire.\n' +
|
|
105
|
-
'
|
|
106
|
-
'
|
|
108
|
+
'surfaces:\n' +
|
|
109
|
+
' - {on: read, match: "./**", at: content}\n---\n' +
|
|
107
110
|
'USER DOT BODY MUST NOT FIRE\n');
|
|
108
111
|
const work = join(fakeHome, 'work');
|
|
109
112
|
mkdirSync(work, { recursive: true });
|
|
@@ -111,7 +114,7 @@ test('a hand-authored `.` route in a user-scope doc never matches a read', () =>
|
|
|
111
114
|
const readFile = join(fakeHome, 'notes', 'thing.md');
|
|
112
115
|
mkdirSync(join(fakeHome, 'notes'), { recursive: true });
|
|
113
116
|
writeFileSync(readFile, '# a note\n');
|
|
114
|
-
const rendered = renderOnReadDocs(node, readFile, new
|
|
117
|
+
const rendered = renderOnReadDocs(node, readFile, new Map());
|
|
115
118
|
assert.ok(rendered.includes('USER CONTROL BODY'), 'the glob-routed user doc proves the user store is in the corpus');
|
|
116
|
-
assert.ok(!rendered.includes('USER DOT BODY MUST NOT FIRE'), `the
|
|
119
|
+
assert.ok(!rendered.includes('USER DOT BODY MUST NOT FIRE'), `the \`./\` anchor is project-store only — a user doc's owning root is ~; got: ${rendered}`);
|
|
117
120
|
});
|
|
@@ -253,3 +253,12 @@ test('G10 — retained extension display state rides IN welcome.snapshot.display
|
|
|
253
253
|
assert.equal(live.method, 'setStatus');
|
|
254
254
|
assert.equal(live.statusText, 'painted while attached');
|
|
255
255
|
});
|
|
256
|
+
test('snapshot_tail is ignored when the session has no stable message ids', async () => {
|
|
257
|
+
const token = tok('snapshot-tail-no-ids');
|
|
258
|
+
h.fakeCmd(id, { cmd: 'stream', text: token });
|
|
259
|
+
const full = await attachUntil(id, 'observer', 'snapshot-tail-full', (a) => JSON.stringify(a.welcome.snapshot.messages).includes(token), 'full snapshot after a tail-less attach');
|
|
260
|
+
assert.equal(full.welcome.snapshot.messageIds, undefined, 'the fake session has no stable ids');
|
|
261
|
+
const tailed = await attach(id, 'observer', 'snapshot-tail-request', { snapshotTail: 1 });
|
|
262
|
+
assert.equal(tailed.welcome.snapshot.messageIds, undefined, 'the tail request cannot invent stable ids');
|
|
263
|
+
assert.deepEqual(tailed.welcome.snapshot.messages, full.welcome.snapshot.messages, 'a welcome without stable ids keeps the full history instead of stranding an unpageable tail');
|
|
264
|
+
});
|
|
@@ -34,7 +34,7 @@ import { CANVAS_EXTENSIONS } from '../../runtime/launch.js';
|
|
|
34
34
|
import { buildBrokerSession, snapshotMessages } from '../../runtime/broker.js';
|
|
35
35
|
import { createNode } from '../../canvas/canvas.js';
|
|
36
36
|
import { closeDb } from '../../canvas/db.js';
|
|
37
|
-
import { transcriptMarkdown } from '../../runtime/node-read.js';
|
|
37
|
+
import { readNodeMessagesPage, transcriptMarkdown } from '../../runtime/node-read.js';
|
|
38
38
|
// M-9 (broker.ts fault-and-die model gate): a boot with no cfg.model now calls
|
|
39
39
|
// the REAL SDK's modelRegistry.getAvailable() and faults if it's empty, which it
|
|
40
40
|
// always is on a host/CI runner with no authenticated provider. This test only
|
|
@@ -128,6 +128,57 @@ test('broker welcome snapshot serves the persisted history, not pi\u2019s sliced
|
|
|
128
128
|
rmSync(cwd, { recursive: true, force: true });
|
|
129
129
|
}
|
|
130
130
|
});
|
|
131
|
+
test('readNodeMessagesPage keeps a tool call with all following tool results across cursor pages', async () => {
|
|
132
|
+
const home = mkdtempSync(join(tmpdir(), 'crtr-page-tools-home-'));
|
|
133
|
+
const sessionCwd = mkdtempSync(join(tmpdir(), 'crtr-page-tools-session-'));
|
|
134
|
+
const prevHome = process.env['CRTR_HOME'];
|
|
135
|
+
process.env['CRTR_HOME'] = home;
|
|
136
|
+
closeDb();
|
|
137
|
+
try {
|
|
138
|
+
const seed = SessionManager.create(sessionCwd);
|
|
139
|
+
seed.appendMessage(userMsg('before the tool run'));
|
|
140
|
+
seed.appendMessage({
|
|
141
|
+
...assistantMsg('calling tools'),
|
|
142
|
+
content: [
|
|
143
|
+
{ type: 'toolCall', id: 'tool-1', name: 'read', arguments: { path: 'one.txt' } },
|
|
144
|
+
{ type: 'toolCall', id: 'tool-2', name: 'read', arguments: { path: 'two.txt' } },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
seed.appendMessage({ role: 'toolResult', toolCallId: 'tool-1', toolName: 'read', content: [{ type: 'text', text: 'one' }], isError: false, timestamp: Date.now() });
|
|
148
|
+
seed.appendMessage({ role: 'toolResult', toolCallId: 'tool-2', toolName: 'read', content: [{ type: 'text', text: 'two' }], isError: false, timestamp: Date.now() });
|
|
149
|
+
seed.appendMessage(userMsg('after the tool run'));
|
|
150
|
+
seed.appendMessage(assistantMsg('finished'));
|
|
151
|
+
const sessionFile = seed.getSessionFile();
|
|
152
|
+
const nodeId = 'tool-page-boundary';
|
|
153
|
+
createNode({
|
|
154
|
+
node_id: nodeId,
|
|
155
|
+
name: nodeId,
|
|
156
|
+
created: new Date().toISOString(),
|
|
157
|
+
cwd: sessionCwd,
|
|
158
|
+
kind: 'general',
|
|
159
|
+
mode: 'base',
|
|
160
|
+
lifecycle: 'terminal',
|
|
161
|
+
status: 'active',
|
|
162
|
+
pi_session_file: sessionFile,
|
|
163
|
+
});
|
|
164
|
+
const newest = await readNodeMessagesPage(nodeId, { limit: 3 });
|
|
165
|
+
assert.equal(newest.messages[0].role, 'assistant', 'the page walks back from the tool-result cut to its call');
|
|
166
|
+
assert.deepEqual(newest.messages.slice(1, 3).map((message) => message.role), ['toolResult', 'toolResult'], 'all tool results share the page with their call');
|
|
167
|
+
assert.ok(newest.nextCursor, 'the widened page still has an older cursor');
|
|
168
|
+
const older = await readNodeMessagesPage(nodeId, { cursor: newest.nextCursor, limit: 3 });
|
|
169
|
+
const persisted = SessionManager.open(sessionFile).buildSessionContext().messages;
|
|
170
|
+
assert.deepEqual([...older.messages, ...newest.messages], persisted, 'cursor pages remain gap-free and non-overlapping after widening');
|
|
171
|
+
assert.deepEqual([...(older.messageIds ?? []), ...(newest.messageIds ?? [])], SessionManager.open(sessionFile).getEntries().map(({ id }) => id), 'stable ids remain aligned across the widened page boundary');
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
closeDb();
|
|
175
|
+
process.env['CRTR_HOME'] = prevHome;
|
|
176
|
+
if (prevHome === undefined)
|
|
177
|
+
delete process.env['CRTR_HOME'];
|
|
178
|
+
rmSync(home, { recursive: true, force: true });
|
|
179
|
+
rmSync(sessionCwd, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
131
182
|
// C3 (inline memory references, Wave 1A): transcriptMarkdown is the human-
|
|
132
183
|
// facing render of a dormant node's persisted session. It must generically
|
|
133
184
|
// skip any custom message whose `display === false` (the same hidden-guidance
|
|
@@ -229,7 +229,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
229
229
|
const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
|
|
230
230
|
assert.equal(stop.action, 'reprompt', 'orchestrator B with no live sub, no final → reprompt');
|
|
231
231
|
assert.equal(stop.reason, 'stalled', 'classified stalled');
|
|
232
|
-
assert.equal(stop.action === 'reprompt' ? stop.message : '', formatCard('stop-guard', {
|
|
232
|
+
assert.equal(stop.action === 'reprompt' ? stop.message : '', formatCard('stop-guard', {}, STALL_REPROMPT), 'the stall reprompt nudge, delivered as a whole-message stop-guard envelope');
|
|
233
233
|
}
|
|
234
234
|
// ===================================================================
|
|
235
235
|
// S10 — B YIELDS. The real plain `node yield` verb sets intent=refresh
|
package/dist/core/canvas/db.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
/** The ordered migration list. Index `i` is migration version `i + 1`; the db's
|
|
3
|
-
* `user_version` tracks how many have been applied. Append only.
|
|
3
|
+
* `user_version` tracks how many have been applied. Append only. Journaled
|
|
4
|
+
* entries from `src/migrations/registry.ts` follow the local steps, so a new
|
|
5
|
+
* registry migration lands as the next version. */
|
|
4
6
|
export declare const MIGRATIONS: ReadonlyArray<(db: DatabaseSync) => void>;
|
|
5
7
|
/** Bring `db` up to the latest schema version. Reads `user_version`, runs each
|
|
6
8
|
* pending migration in order, and bumps `user_version` after each so the work
|
package/dist/core/canvas/db.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { DatabaseSync } from 'node:sqlite';
|
|
8
8
|
import { existsSync, readFileSync, readdirSync, appendFileSync } from 'node:fs';
|
|
9
9
|
import { basename, join, resolve } from 'node:path';
|
|
10
|
-
import { canvasDbPath, ensureHome, isSafeNodeId, nodesRoot, nodeMetaPath } from './paths.js';
|
|
10
|
+
import { canvasDbPath, crtrHome, ensureHome, isSafeNodeId, nodesRoot, nodeMetaPath } from './paths.js';
|
|
11
|
+
import { STATE_MIGRATIONS } from '../../migrations/registry.js';
|
|
11
12
|
import { customExtensionPaths } from './extensions.js';
|
|
12
13
|
import { updateJsonFileDurably } from './meta-file.js';
|
|
13
14
|
// --- Schema as a forward-only migration list ------------------------------
|
|
@@ -1152,8 +1153,16 @@ ALTER TABLE crons ADD COLUMN held INTEGER NOT NULL DEFAULT 0;
|
|
|
1152
1153
|
ALTER TABLE crons ADD COLUMN last_poke_at TEXT;
|
|
1153
1154
|
`);
|
|
1154
1155
|
}
|
|
1156
|
+
/** The journaled lane of the state-migration registry, adapted to this chain's
|
|
1157
|
+
* step shape. Registry order is preserved, so these versions stay stable as
|
|
1158
|
+
* long as the registry is append-only (same contract as the local list). */
|
|
1159
|
+
const JOURNALED_STATE_MIGRATIONS = STATE_MIGRATIONS
|
|
1160
|
+
.filter((m) => m.lane === 'journaled')
|
|
1161
|
+
.map((m) => (db) => m.apply(db, crtrHome()));
|
|
1155
1162
|
/** The ordered migration list. Index `i` is migration version `i + 1`; the db's
|
|
1156
|
-
* `user_version` tracks how many have been applied. Append only.
|
|
1163
|
+
* `user_version` tracks how many have been applied. Append only. Journaled
|
|
1164
|
+
* entries from `src/migrations/registry.ts` follow the local steps, so a new
|
|
1165
|
+
* registry migration lands as the next version. */
|
|
1157
1166
|
export const MIGRATIONS = [
|
|
1158
1167
|
/* v1 */ baselineSchema,
|
|
1159
1168
|
/* v2 */ addRuntimeColumns,
|
|
@@ -1189,6 +1198,7 @@ export const MIGRATIONS = [
|
|
|
1189
1198
|
/* v30 */ dropConsultOutbox,
|
|
1190
1199
|
/* v31 */ addReviewSubmitRequested,
|
|
1191
1200
|
/* v32 */ addCronHeldColumns,
|
|
1201
|
+
...JOURNALED_STATE_MIGRATIONS,
|
|
1192
1202
|
];
|
|
1193
1203
|
/** Migration indexes that manage their OWN transaction and therefore must not
|
|
1194
1204
|
* be wrapped by `migrate()` — `node:sqlite` rejects a nested BEGIN. Index 3
|