@north-light/crouter 0.3.180 → 0.3.182
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 +10 -1
- package/dist/api/client.js +13 -0
- package/dist/api/dto/broker.d.ts +32 -0
- package/dist/api/dto/crons.d.ts +17 -0
- package/dist/api/dto/memory.d.ts +17 -0
- package/dist/api/dto/memory.js +6 -0
- package/dist/api/dto/messages.d.ts +5 -0
- package/dist/api/dto/reviews.d.ts +8 -4
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +2 -0
- package/dist/api/routes.js +4 -0
- package/dist/build-root.d.ts +7 -0
- package/dist/build-root.js +21 -0
- package/dist/builtin-memory/insights/init.md +48 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/__tests__/insights-active-init.test.ts +98 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +7 -50
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +16 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +34 -0
- package/dist/cli.js +1 -2
- package/dist/clients/attach/__tests__/context-message.test.js +5 -2
- package/dist/clients/attach/assets/README.md +7 -0
- package/dist/clients/attach/assets/whip-06.mp3 +0 -0
- package/dist/clients/attach/assets/whip-crack.mp3 +0 -0
- package/dist/clients/attach/assets/whip-snap.mp3 +0 -0
- package/dist/clients/attach/chrome/canvas-panels.d.ts +7 -1
- package/dist/clients/attach/chrome/canvas-panels.js +20 -3
- package/dist/clients/attach/chrome/review-wait.d.ts +6 -0
- package/dist/clients/attach/chrome/review-wait.js +22 -0
- package/dist/clients/attach/chrome/roster.js +23 -2
- package/dist/clients/attach/chrome/widgets.js +1 -1
- package/dist/clients/attach/input/controller.js +4 -3
- package/dist/clients/attach/overlays/mcp.js +3 -1
- package/dist/clients/attach/render/chat-view.js +1 -1
- package/dist/clients/attach/session/whip.d.ts +1 -0
- package/dist/clients/attach/session/whip.js +26 -0
- package/dist/clients/attach/slash/dispatch.js +2 -0
- package/dist/clients/attach/viewer.js +578 -573
- package/dist/clients/inbox/review/document-surface.d.ts +1 -1
- package/dist/clients/inbox/review/document-surface.js +4 -4
- package/dist/clients/inbox/review/launch.js +16 -4
- package/dist/clients/inbox/review/review-client.d.ts +9 -4
- package/dist/clients/inbox/review/review-client.js +3 -0
- package/dist/commands/cron.js +30 -8
- package/dist/commands/human/prompts.d.ts +7 -2
- package/dist/commands/human/prompts.js +15 -10
- package/dist/commands/human.js +1 -2
- package/dist/commands/memory/find.js +11 -8
- package/dist/commands/memory/read.js +111 -11
- package/dist/commands/memory/write.js +1 -1
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.d.ts +13 -0
- package/dist/commands/pkg/market-manage.js +39 -33
- package/dist/commands/pkg/plugin-inspect.js +4 -3
- package/dist/commands/pkg/plugin-manage.js +12 -11
- package/dist/commands/surface/node/focus.js +1 -2
- package/dist/commands/sys/doctor.js +4 -4
- package/dist/commands/sys/setup-core.d.ts +14 -7
- package/dist/commands/sys/setup-core.js +66 -11
- package/dist/commands/sys/setup-wizard.js +2 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/core/__tests__/cron-held-settlement.test.d.ts +1 -0
- package/dist/core/__tests__/cron-held-settlement.test.js +222 -0
- package/dist/core/__tests__/helpers/harness.js +1 -2
- package/dist/core/__tests__/phase4-review-store.test.js +1 -0
- package/dist/core/__tests__/serial/command-plugins.test.js +88 -1
- package/dist/core/__tests__/session-model.test.js +5 -3
- package/dist/core/bootstrap.d.ts +0 -4
- package/dist/core/bootstrap.js +1 -55
- package/dist/core/canvas/crons.d.ts +54 -2
- package/dist/core/canvas/crons.js +48 -4
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/command-manifests/manifest.d.ts +11 -0
- package/dist/core/command-manifests/manifest.js +45 -4
- package/dist/core/command-manifests/schema.d.ts +1 -1
- package/dist/core/command-plugins/bundle.d.ts +1 -0
- package/dist/core/command-plugins/bundle.js +3 -3
- package/dist/core/command-plugins/discovery.d.ts +5 -2
- package/dist/core/command-plugins/discovery.js +5 -5
- package/dist/core/command-plugins/help-addenda.d.ts +12 -0
- package/dist/core/command-plugins/help-addenda.js +30 -0
- package/dist/core/command.js +25 -2
- package/dist/core/config.js +0 -1
- package/dist/core/human/convention.d.ts +0 -1
- package/dist/core/human/convention.js +0 -6
- package/dist/core/keybindings/inbox.d.ts +6 -8
- package/dist/core/keybindings/inbox.js +6 -15
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/index.js +1 -1
- package/dist/core/memory/doc-link-grammar.js +4 -1
- package/dist/core/memory-resolver.d.ts +28 -4
- package/dist/core/memory-resolver.js +51 -39
- package/dist/core/review/stage.js +1 -0
- package/dist/core/review/store.d.ts +5 -0
- package/dist/core/review/store.js +10 -0
- package/dist/core/review/types.d.ts +4 -0
- package/dist/core/runtime/broker/event-projection.d.ts +8 -1
- package/dist/core/runtime/broker/event-projection.js +25 -1
- package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
- package/dist/core/runtime/broker/frame-dispatch.js +50 -8
- package/dist/core/runtime/broker/inbox.d.ts +4 -0
- package/dist/core/runtime/broker/inbox.js +17 -8
- package/dist/core/runtime/broker/message-ledger.d.ts +53 -0
- package/dist/core/runtime/broker/message-ledger.js +143 -0
- package/dist/core/runtime/broker/rebind.js +14 -0
- package/dist/core/runtime/broker-protocol.d.ts +46 -1
- package/dist/core/runtime/broker.js +11 -2
- package/dist/core/runtime/interactive-deliver.d.ts +5 -2
- package/dist/core/runtime/interactive-deliver.js +6 -3
- package/dist/core/runtime/shell-expansion.d.ts +32 -0
- package/dist/core/runtime/shell-expansion.js +102 -0
- package/dist/core/session-model/session-state.d.ts +9 -4
- package/dist/core/session-model/session-state.js +5 -1
- package/dist/daemon/api/handlers/broker-ops.js +8 -0
- package/dist/daemon/api/handlers/crons.js +14 -1
- package/dist/daemon/api/handlers/inbox.js +5 -0
- package/dist/daemon/api/handlers/memory.d.ts +2 -0
- package/dist/daemon/api/handlers/memory.js +48 -0
- package/dist/daemon/api/handlers/messages.js +7 -1
- package/dist/daemon/api/handlers/reviews.js +7 -5
- package/dist/daemon/api/map.js +3 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +71 -3
- package/dist/daemon/crtrd.js +3 -0
- package/dist/daemon/reconcilers/pending-review-submit.d.ts +7 -0
- package/dist/daemon/reconcilers/pending-review-submit.js +35 -0
- package/dist/daemon/review/companion.d.ts +8 -0
- package/dist/daemon/review/companion.js +35 -0
- package/dist/daemon/review/deliver.js +2 -1
- package/dist/daemon/review/finish.d.ts +29 -2
- package/dist/daemon/review/finish.js +75 -2
- package/dist/pi-extensions/canvas-inbox-watcher.js +34 -1
- package/dist/shared/generated-context.d.ts +3 -4
- package/dist/shared/generated-context.js +24 -6
- package/dist/types.d.ts +0 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -6,7 +6,7 @@ import { parseFrontmatterGeneric } from './frontmatter.js';
|
|
|
6
6
|
import { listInstalledPlugins, listInstalledPluginsInRoot, parseSkillQualifier } from './resolver.js';
|
|
7
7
|
import { ambiguous, notFound, usage } from './errors.js';
|
|
8
8
|
import { warn } from './output.js';
|
|
9
|
-
import { pluginMemoryDir,
|
|
9
|
+
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';
|
|
@@ -14,33 +14,35 @@ import { memoryDir as nodeMemoryDir } from './runtime/memory.js';
|
|
|
14
14
|
export function memoryDocId(doc) {
|
|
15
15
|
return `${doc.scope}/${doc.name}`;
|
|
16
16
|
}
|
|
17
|
-
/** The
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
function
|
|
21
|
-
return
|
|
17
|
+
/** The target the ambient process describes: cwd + the two env seams every
|
|
18
|
+
* profile/node-aware memory path reads through (`CRTR_PROFILE_ID`,
|
|
19
|
+
* `CRTR_NODE_ID`). */
|
|
20
|
+
function ambientTarget() {
|
|
21
|
+
return {
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
profileId: process.env['CRTR_PROFILE_ID'] || null,
|
|
24
|
+
nodeId: process.env['CRTR_NODE_ID'] || null,
|
|
25
|
+
};
|
|
22
26
|
}
|
|
23
|
-
/** The
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
function selectedNodeId() {
|
|
27
|
-
return process.env['CRTR_NODE_ID'] || '';
|
|
27
|
+
/** The selected profile id from the process env, or '' when none. */
|
|
28
|
+
function selectedProfileId() {
|
|
29
|
+
return ambientTarget().profileId ?? '';
|
|
28
30
|
}
|
|
29
31
|
/** The memory scopes in resolution precedence: node > project stack > profile >
|
|
30
|
-
* user > builtin. Node is included only
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
function scopesInPrecedence(scope) {
|
|
32
|
+
* user > builtin. Node is included only when the target names one and ranks
|
|
33
|
+
* NEAREST — a node doc overrides any wider scope. Project is included only when
|
|
34
|
+
* a project scope exists for the target's cwd (or its profile's project stack);
|
|
35
|
+
* profile is included only when the target has one. A single `scope` narrows to
|
|
36
|
+
* just that scope. */
|
|
37
|
+
function scopesInPrecedence(target, scope) {
|
|
36
38
|
if (scope)
|
|
37
39
|
return [scope];
|
|
38
40
|
const out = [];
|
|
39
|
-
if (
|
|
41
|
+
if (target.nodeId)
|
|
40
42
|
out.push('node');
|
|
41
|
-
if (
|
|
43
|
+
if (projectScopeRoots(target.cwd, target.profileId).length > 0)
|
|
42
44
|
out.push('project');
|
|
43
|
-
if (
|
|
45
|
+
if (target.profileId)
|
|
44
46
|
out.push('profile');
|
|
45
47
|
out.push('user');
|
|
46
48
|
out.push('builtin');
|
|
@@ -53,11 +55,11 @@ function scopesInPrecedence(scope) {
|
|
|
53
55
|
* join — and drops out silently (never throws the resolver) when the
|
|
54
56
|
* selected profile id no longer resolves to a manifest. User and builtin
|
|
55
57
|
* remain singleton scopes after that. */
|
|
56
|
-
function memorySourcesInPrecedence(scope) {
|
|
58
|
+
function memorySourcesInPrecedence(target, scope) {
|
|
57
59
|
const out = [];
|
|
58
|
-
for (const s of scopesInPrecedence(scope)) {
|
|
60
|
+
for (const s of scopesInPrecedence(target, scope)) {
|
|
59
61
|
if (s === 'project') {
|
|
60
|
-
for (const root of projectScopeRoots()) {
|
|
62
|
+
for (const root of projectScopeRoots(target.cwd, target.profileId)) {
|
|
61
63
|
out.push({
|
|
62
64
|
scope: 'project',
|
|
63
65
|
memoryDir: join(root, 'memory'),
|
|
@@ -66,7 +68,7 @@ function memorySourcesInPrecedence(scope) {
|
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
else if (s === 'profile') {
|
|
69
|
-
const profileId =
|
|
71
|
+
const profileId = target.profileId ?? '';
|
|
70
72
|
if (profileId !== '') {
|
|
71
73
|
try {
|
|
72
74
|
const { profileId: resolvedId } = loadProfileManifest(profileId);
|
|
@@ -79,8 +81,8 @@ function memorySourcesInPrecedence(scope) {
|
|
|
79
81
|
}
|
|
80
82
|
else if (s === 'node') {
|
|
81
83
|
// The this-node store — a singleton dir in the node's context, no plugins.
|
|
82
|
-
// Silently absent when
|
|
83
|
-
const nodeId =
|
|
84
|
+
// Silently absent when the target names no node.
|
|
85
|
+
const nodeId = target.nodeId ?? '';
|
|
84
86
|
if (nodeId !== '')
|
|
85
87
|
out.push({ scope: 'node', memoryDir: nodeMemoryDir(nodeId), plugins: [] });
|
|
86
88
|
}
|
|
@@ -157,11 +159,12 @@ function listMemoryDocsInDir(scope, dir, quiet = false) {
|
|
|
157
159
|
* stack); profile is the selected profile's own singleton store, resolved
|
|
158
160
|
* through `loadProfileManifest`; user and builtin are singleton stores. */
|
|
159
161
|
export function listMemoryDocs(scope, quiet = false) {
|
|
162
|
+
const target = ambientTarget();
|
|
160
163
|
if (scope === 'project') {
|
|
161
|
-
return listProjectMemoryDocs(
|
|
164
|
+
return listProjectMemoryDocs(target.cwd, target.profileId, quiet);
|
|
162
165
|
}
|
|
163
166
|
if (scope === 'profile') {
|
|
164
|
-
const profileId =
|
|
167
|
+
const profileId = target.profileId ?? '';
|
|
165
168
|
if (profileId === '')
|
|
166
169
|
return [];
|
|
167
170
|
try {
|
|
@@ -173,7 +176,7 @@ export function listMemoryDocs(scope, quiet = false) {
|
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
if (scope === 'node') {
|
|
176
|
-
const nodeId =
|
|
179
|
+
const nodeId = target.nodeId ?? '';
|
|
177
180
|
if (nodeId === '')
|
|
178
181
|
return [];
|
|
179
182
|
return listMemoryDocsInDir('node', nodeMemoryDir(nodeId), quiet);
|
|
@@ -215,10 +218,10 @@ function sourceMemoryDocs(source, quiet = false) {
|
|
|
215
218
|
...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet)),
|
|
216
219
|
];
|
|
217
220
|
}
|
|
218
|
-
/** All project-scoped docs visible from an explicit node workspace/profile
|
|
219
|
-
*
|
|
220
|
-
* (
|
|
221
|
-
*
|
|
221
|
+
/** All project-scoped docs visible from an explicit node workspace/profile — the
|
|
222
|
+
* project-only slice of a `MemoryTarget` view, used where only workspace docs
|
|
223
|
+
* are wanted (a workspace-open render). For a full-precedence target-addressed
|
|
224
|
+
* lookup, use `resolveMemoryDocForTarget`. */
|
|
222
225
|
export function listProjectMemoryDocs(startDir = process.cwd(), profileId = selectedProfileId() || null, quiet = false) {
|
|
223
226
|
return projectScopeRoots(startDir, profileId).flatMap((root) => sourceMemoryDocs({
|
|
224
227
|
scope: 'project',
|
|
@@ -232,7 +235,7 @@ export function listProjectMemoryDocs(startDir = process.cwd(), profileId = sele
|
|
|
232
235
|
* native docs are emitted before enabled-plugin docs, so native wins on the
|
|
233
236
|
* caller's first-wins dedup. */
|
|
234
237
|
export function listAllMemoryDocs(scope, quiet = false) {
|
|
235
|
-
return memorySourcesInPrecedence(scope).flatMap((source) => sourceMemoryDocs(source, quiet));
|
|
238
|
+
return memorySourcesInPrecedence(ambientTarget(), scope).flatMap((source) => sourceMemoryDocs(source, quiet));
|
|
236
239
|
}
|
|
237
240
|
/** Find the direct child of `dir` — a `.md` file (matched on name minus
|
|
238
241
|
* extension) or a directory — whose NORMALIZED display name equals
|
|
@@ -356,21 +359,22 @@ function formatLeafAmbiguous(leaf, matches) {
|
|
|
356
359
|
const ids = matches.map(memoryDocId).join(', ');
|
|
357
360
|
return `ambiguous memory document: ${leaf} matches multiple documents: ${ids}`;
|
|
358
361
|
}
|
|
359
|
-
function loadMemorySources(scope) {
|
|
362
|
+
function loadMemorySources(target, scope) {
|
|
360
363
|
// Quiet: a targeted read must not spew other docs' frontmatter warnings
|
|
361
364
|
// before its own result (esp. a not_found) — corpus health is `lint`'s job.
|
|
362
|
-
return memorySourcesInPrecedence(scope).map((source) => ({
|
|
365
|
+
return memorySourcesInPrecedence(target, scope).map((source) => ({
|
|
363
366
|
source,
|
|
364
367
|
docs: sourceMemoryDocs(source, true),
|
|
365
368
|
}));
|
|
366
369
|
}
|
|
367
370
|
export function createMemoryDocSnapshot() {
|
|
368
|
-
const
|
|
371
|
+
const target = ambientTarget();
|
|
372
|
+
const defaultSources = loadMemorySources(target);
|
|
369
373
|
const sourcesByScope = new Map([[undefined, defaultSources]]);
|
|
370
374
|
const sourceFor = (scope) => {
|
|
371
375
|
let sources = sourcesByScope.get(scope);
|
|
372
376
|
if (sources === undefined) {
|
|
373
|
-
sources = loadMemorySources(scope);
|
|
377
|
+
sources = loadMemorySources(target, scope);
|
|
374
378
|
sourcesByScope.set(scope, sources);
|
|
375
379
|
}
|
|
376
380
|
return sources;
|
|
@@ -448,10 +452,18 @@ export function resolveMemoryDocs(names) {
|
|
|
448
452
|
return createMemoryDocSnapshot().resolve(names);
|
|
449
453
|
}
|
|
450
454
|
export function resolveMemoryDoc(rawName, opts = {}) {
|
|
455
|
+
return resolveMemoryDocForTarget(rawName, ambientTarget(), opts);
|
|
456
|
+
}
|
|
457
|
+
/** Resolve a memory document as ANOTHER node would see it — the same precedence
|
|
458
|
+
* chain (node-local > project stack > profile > user > builtin), read from the
|
|
459
|
+
* target's cwd/profile/node rather than the host process's. This is what crtrd
|
|
460
|
+
* resolves a `[[name]]` link through: the daemon's own cwd and env name no
|
|
461
|
+
* node, and the same name can be a different document for two nodes. */
|
|
462
|
+
export function resolveMemoryDocForTarget(rawName, target, opts = {}) {
|
|
451
463
|
const parsed = parseSkillQualifier(rawName);
|
|
452
464
|
if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
|
|
453
465
|
throw usage(`scope conflict: identifier "${rawName}" uses scope "${parsed.scope}" but --scope is "${opts.scope}"`);
|
|
454
466
|
}
|
|
455
467
|
const effectiveScope = opts.scope ?? parsed.scope;
|
|
456
|
-
return resolveMemoryDocFromSources(rawName, opts, loadMemorySources(effectiveScope));
|
|
468
|
+
return resolveMemoryDocFromSources(rawName, opts, loadMemorySources(target, effectiveScope));
|
|
457
469
|
}
|
|
@@ -29,9 +29,14 @@ export declare function listReviews(filter: {
|
|
|
29
29
|
file?: string;
|
|
30
30
|
}): ReviewRecord[];
|
|
31
31
|
export declare function listBindingReviews(): ReviewRecord[];
|
|
32
|
+
/** Open reviews the human has already submitted, still waiting on their companion. */
|
|
33
|
+
export declare function listSubmitRequestedReviews(): ReviewRecord[];
|
|
32
34
|
export declare function listUnprojectedTerminalReviews(): ReviewRecord[];
|
|
33
35
|
export declare function bindBridgeNode(reviewId: string, bridgeNodeId: string): boolean;
|
|
34
36
|
export declare function openReviewRow(reviewId: string, openedAt: string): boolean;
|
|
37
|
+
/** Record the human's submit intent on a review that stays open meanwhile.
|
|
38
|
+
* False when the review already left `open` or already carries an intent. */
|
|
39
|
+
export declare function requestReviewSubmit(reviewId: string, at: string): boolean;
|
|
35
40
|
export declare function approveReviewLocked(db: DatabaseSync, args: ApproveReviewArgs): boolean;
|
|
36
41
|
export declare function cancelReviewRow(reviewId: string, args: {
|
|
37
42
|
at: string;
|
|
@@ -24,6 +24,7 @@ function recordFromRow(raw) {
|
|
|
24
24
|
created: raw['created'],
|
|
25
25
|
captured_at: raw['captured_at'],
|
|
26
26
|
opened_at: raw['opened_at'] ?? null,
|
|
27
|
+
submit_requested_at: raw['submit_requested_at'] ?? null,
|
|
27
28
|
output_path: raw['output_path'] ?? null,
|
|
28
29
|
subtitle: raw['subtitle'] ?? null,
|
|
29
30
|
approved_at: raw['approved_at'] ?? null,
|
|
@@ -118,6 +119,10 @@ export function listReviews(filter) {
|
|
|
118
119
|
export function listBindingReviews() {
|
|
119
120
|
return readRecords(openDb(), "state = 'binding'");
|
|
120
121
|
}
|
|
122
|
+
/** Open reviews the human has already submitted, still waiting on their companion. */
|
|
123
|
+
export function listSubmitRequestedReviews() {
|
|
124
|
+
return readRecords(openDb(), "state = 'open' AND submit_requested_at IS NOT NULL");
|
|
125
|
+
}
|
|
121
126
|
export function listUnprojectedTerminalReviews() {
|
|
122
127
|
return readRecords(openDb(), "state IN ('approved', 'canceled') AND projected_at IS NULL");
|
|
123
128
|
}
|
|
@@ -127,6 +132,11 @@ export function bindBridgeNode(reviewId, bridgeNodeId) {
|
|
|
127
132
|
export function openReviewRow(reviewId, openedAt) {
|
|
128
133
|
return withCanvasWrite((db) => db.prepare("UPDATE reviews SET state = 'open', opened_at = ? WHERE review_id = ? AND state = 'binding'").run(openedAt, reviewId).changes > 0);
|
|
129
134
|
}
|
|
135
|
+
/** Record the human's submit intent on a review that stays open meanwhile.
|
|
136
|
+
* False when the review already left `open` or already carries an intent. */
|
|
137
|
+
export function requestReviewSubmit(reviewId, at) {
|
|
138
|
+
return withCanvasWrite((db) => db.prepare("UPDATE reviews SET submit_requested_at = ? WHERE review_id = ? AND state = 'open' AND submit_requested_at IS NULL").run(at, reviewId).changes > 0);
|
|
139
|
+
}
|
|
130
140
|
export function approveReviewLocked(db, args) {
|
|
131
141
|
return db.prepare(`
|
|
132
142
|
UPDATE reviews SET
|
|
@@ -47,6 +47,10 @@ export interface ReviewRow {
|
|
|
47
47
|
created: string;
|
|
48
48
|
captured_at: string;
|
|
49
49
|
opened_at: string | null;
|
|
50
|
+
/** When the human pressed submit on a review whose companion was still
|
|
51
|
+
* working. The review stays `open` — comments still land and still remap —
|
|
52
|
+
* until the companion goes quiet and the daemon completes the approval. */
|
|
53
|
+
submit_requested_at: string | null;
|
|
50
54
|
output_path: string | null;
|
|
51
55
|
subtitle: string | null;
|
|
52
56
|
approved_at: string | null;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { BrokerToClient } from '../broker-protocol.js';
|
|
2
2
|
import type { BrokerSession } from './read-ops.js';
|
|
3
3
|
import type { BrokerClientRegistry } from './client-registry.js';
|
|
4
|
+
import type { MessageIdLedger } from './message-ledger.js';
|
|
4
5
|
import type { SubscriptionGeneration } from './rebind.js';
|
|
5
6
|
import type { FaultRetry } from './fault-retry.js';
|
|
6
7
|
type EventProjectionDeps = {
|
|
7
8
|
registry: BrokerClientRegistry;
|
|
8
9
|
faultRetry: FaultRetry;
|
|
10
|
+
ledger: MessageIdLedger;
|
|
9
11
|
installedGeneration: () => SubscriptionGeneration | undefined;
|
|
10
12
|
currentSession: () => BrokerSession;
|
|
11
13
|
notifyTurnAccepted: () => void;
|
|
@@ -31,7 +33,12 @@ export declare class EventProjection {
|
|
|
31
33
|
reset(): void;
|
|
32
34
|
dispose(): void;
|
|
33
35
|
knownStreamWait(deadlineMs: number): () => void;
|
|
34
|
-
|
|
36
|
+
/** Message-identity enrichment (see MessageIdLedger): fold pi's queue state
|
|
37
|
+
* into the ledger and relay id arrays parallel to the texts; attach the
|
|
38
|
+
* retired/dispatched id to the user message that carries it. Both are
|
|
39
|
+
* additive fields on the verbatim pi event — consumers read or ignore them. */
|
|
40
|
+
private withMessageIdentity;
|
|
41
|
+
relayEvent(rawEvent: BrokerToClient, generation: SubscriptionGeneration): void;
|
|
35
42
|
private clearPendingUpdate;
|
|
36
43
|
private flushPendingUpdate;
|
|
37
44
|
private invalidatePendingBroadcasts;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentText } from '@earendil-works/pi-ai';
|
|
1
2
|
import { readConfig } from '../../config.js';
|
|
2
3
|
import { emitEvent } from '../../events/emit.js';
|
|
3
4
|
import { releaseKnownStreamWait, resolveStreamWatchdogMs, StreamWatchdog } from '../stream-watchdog.js';
|
|
@@ -64,7 +65,30 @@ export class EventProjection {
|
|
|
64
65
|
releaseKnownStreamWait(this.streamWatchdog, generation.id, waitingSession.isStreaming, generation.executingTools.size > 0);
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
/** Message-identity enrichment (see MessageIdLedger): fold pi's queue state
|
|
69
|
+
* into the ledger and relay id arrays parallel to the texts; attach the
|
|
70
|
+
* retired/dispatched id to the user message that carries it. Both are
|
|
71
|
+
* additive fields on the verbatim pi event — consumers read or ignore them. */
|
|
72
|
+
withMessageIdentity(event) {
|
|
73
|
+
if (event.type === 'queue_update') {
|
|
74
|
+
const ids = this.deps.ledger.reconcileQueueUpdate(event.steering, event.followUp);
|
|
75
|
+
const enriched = { ...event, steeringIds: ids.steeringIds, followUpIds: ids.followUpIds };
|
|
76
|
+
return enriched;
|
|
77
|
+
}
|
|
78
|
+
if (event.type === 'message_start' && event.message.role === 'user') {
|
|
79
|
+
// contentText(content, '') is pi's OWN retirement text extraction
|
|
80
|
+
// (agent-session.js _handleAgentEvent) — the same function, so the
|
|
81
|
+
// ledger's text matching cannot diverge from pi's queue splice.
|
|
82
|
+
const crtrMessageId = this.deps.ledger.takeUserMessageId(contentText(event.message.content, ''));
|
|
83
|
+
if (crtrMessageId === undefined)
|
|
84
|
+
return event;
|
|
85
|
+
const enriched = { ...event, crtrMessageId };
|
|
86
|
+
return enriched;
|
|
87
|
+
}
|
|
88
|
+
return event;
|
|
89
|
+
}
|
|
90
|
+
relayEvent(rawEvent, generation) {
|
|
91
|
+
const event = this.withMessageIdentity(rawEvent);
|
|
68
92
|
this.deps.faultRetry.onEvent(event);
|
|
69
93
|
const type = event.type;
|
|
70
94
|
if (type === 'message_start' && event.message.role === 'user') {
|
|
@@ -6,6 +6,7 @@ import type { BrokerClient, BrokerClientRegistry } from './client-registry.js';
|
|
|
6
6
|
import { type BrokerSession } from './read-ops.js';
|
|
7
7
|
import type { ToolGroupTracker } from './tool-groups.js';
|
|
8
8
|
import type { EventProjection } from './event-projection.js';
|
|
9
|
+
import type { MessageIdLedger } from './message-ledger.js';
|
|
9
10
|
import type { RebindDriver } from './rebind.js';
|
|
10
11
|
export declare const REVIEW_BOUNDARY_NAVIGATION_CODE = "review_boundary_navigation";
|
|
11
12
|
export declare class ReviewBoundaryNavigationError extends Error {
|
|
@@ -180,6 +181,7 @@ export type FrameDispatchDeps = {
|
|
|
180
181
|
registry: BrokerClientRegistry;
|
|
181
182
|
toolGroups: ToolGroupTracker;
|
|
182
183
|
projection: EventProjection;
|
|
184
|
+
ledger: MessageIdLedger;
|
|
183
185
|
rebind: RebindDriver;
|
|
184
186
|
reviewCompanion: boolean;
|
|
185
187
|
pendingDialogs: Map<string, PendingDialog>;
|
|
@@ -279,7 +279,7 @@ export function stripHiddenEntriesFromJsonlExport(filePath) {
|
|
|
279
279
|
}
|
|
280
280
|
/** Owns frame-local input state while dispatching through explicit broker collaborators. */
|
|
281
281
|
export function createFrameDispatchContext(deps) {
|
|
282
|
-
const { nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
|
|
282
|
+
const { nodeId, cfg, registry, toolGroups, projection, ledger, rebind, reviewCompanion, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
|
|
283
283
|
const currentSession = () => rebind.session();
|
|
284
284
|
const currentServices = () => rebind.services();
|
|
285
285
|
let kickoffPending;
|
|
@@ -301,9 +301,14 @@ export function createFrameDispatchContext(deps) {
|
|
|
301
301
|
const prompt = kickoffPending;
|
|
302
302
|
kickoffPending = undefined;
|
|
303
303
|
kickoffInFlight = true;
|
|
304
|
+
// The kickoff prompt is a broker-originated dispatch — mint its identity
|
|
305
|
+
// here so its relayed user message_start carries an id like any other.
|
|
306
|
+
const kickoffMessageId = randomUUID();
|
|
307
|
+
ledger.noteDispatch(kickoffMessageId, prompt);
|
|
304
308
|
trackPromptDispatch(operationIdContext.fresh(() => currentSession()
|
|
305
309
|
.prompt(prompt)
|
|
306
310
|
.catch((err) => {
|
|
311
|
+
ledger.dropIntent(kickoffMessageId);
|
|
307
312
|
emitEvent({
|
|
308
313
|
level: 'error',
|
|
309
314
|
event: 'broker.prompt.first_failed',
|
|
@@ -392,6 +397,7 @@ export function createFrameDispatchContext(deps) {
|
|
|
392
397
|
// is producing now, then start this as a new turn. A normal mid-stream
|
|
393
398
|
// prompt deliberately steers into pi's queue; the whip must not.
|
|
394
399
|
if (frame.type === 'prompt' && frame.interrupt === true) {
|
|
400
|
+
const whipMessageId = frame.message_id;
|
|
395
401
|
return (async () => {
|
|
396
402
|
if (currentSession().isBashRunning) {
|
|
397
403
|
currentSession().abortBash();
|
|
@@ -414,6 +420,7 @@ export function createFrameDispatchContext(deps) {
|
|
|
414
420
|
type: 'prompt',
|
|
415
421
|
text: frame.text,
|
|
416
422
|
...(frame.images !== undefined ? { images: frame.images } : {}),
|
|
423
|
+
...(whipMessageId !== undefined ? { message_id: whipMessageId } : {}),
|
|
417
424
|
});
|
|
418
425
|
})().catch((err) => {
|
|
419
426
|
relayError(err);
|
|
@@ -444,6 +451,19 @@ export function createFrameDispatchContext(deps) {
|
|
|
444
451
|
const dispatch = () => {
|
|
445
452
|
const route = resolveEngineRoute(frame, currentSession().isStreaming);
|
|
446
453
|
const guidance = resolveFrameGuidance(frame.text);
|
|
454
|
+
// Message identity (see MessageIdLedger): note the dispatch intent
|
|
455
|
+
// before the engine call so pi's synchronous queue_update/message_start
|
|
456
|
+
// can adopt it. A leading engine command never produces a matching
|
|
457
|
+
// user message — no intent. On rejection the intent is dropped.
|
|
458
|
+
let noteFailure = relayError;
|
|
459
|
+
if (!isLeadingEngineCommand(frame.text, currentSession())) {
|
|
460
|
+
const messageId = frame.message_id ?? randomUUID();
|
|
461
|
+
ledger.noteDispatch(messageId, frame.text);
|
|
462
|
+
noteFailure = (err) => {
|
|
463
|
+
ledger.dropIntent(messageId);
|
|
464
|
+
relayError(err);
|
|
465
|
+
};
|
|
466
|
+
}
|
|
447
467
|
if (guidance === null) {
|
|
448
468
|
if (route.call === 'prompt') {
|
|
449
469
|
// A routed prompt STARTS a turn — including an idle follow_up
|
|
@@ -452,12 +472,12 @@ export function createFrameDispatchContext(deps) {
|
|
|
452
472
|
// steer.
|
|
453
473
|
return settle(trackPromptDispatch(operationIdContext.fresh(() => currentSession()
|
|
454
474
|
.prompt(frame.text, route.options)
|
|
455
|
-
.catch(
|
|
475
|
+
.catch(noteFailure))));
|
|
456
476
|
}
|
|
457
477
|
// C1: pi's followUp() takes images as a POSITIONAL 2nd arg (`followUp(text, images?)`).
|
|
458
478
|
return settle(currentSession()
|
|
459
479
|
.followUp(frame.text, frame.images)
|
|
460
|
-
.catch(
|
|
480
|
+
.catch(noteFailure));
|
|
461
481
|
}
|
|
462
482
|
const deliverAs = chooseGuidanceDeliveryMode(frame.type, route.call, currentSession().isStreaming);
|
|
463
483
|
if (route.call === 'prompt') {
|
|
@@ -469,18 +489,18 @@ export function createFrameDispatchContext(deps) {
|
|
|
469
489
|
// ATOMICALLY clears its own pending entry if prompt() settles
|
|
470
490
|
// without ever reaching that drain (an early throw/return leaves
|
|
471
491
|
// nothing stranded to leak into a later, unrelated turn).
|
|
472
|
-
return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => currentSession().prompt(frame.text, route.options))).catch(
|
|
492
|
+
return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => currentSession().prompt(frame.text, route.options))).catch(noteFailure)));
|
|
473
493
|
}
|
|
474
494
|
return settle(trackPromptDispatch(operationIdContext.fresh(() => currentSession()
|
|
475
495
|
.prompt(frame.text, route.options)
|
|
476
|
-
.catch(
|
|
496
|
+
.catch(noteFailure)
|
|
477
497
|
.then(() => sendGuidance(guidance, deliverAs)))));
|
|
478
498
|
}
|
|
479
499
|
// deliverAs is always 'followUp' here per chooseGuidanceDeliveryMode's
|
|
480
500
|
// contract (a followUp routeCall never yields 'nextTurn').
|
|
481
501
|
return settle(currentSession()
|
|
482
502
|
.followUp(frame.text, frame.images)
|
|
483
|
-
.catch(
|
|
503
|
+
.catch(noteFailure)
|
|
484
504
|
.then(() => sendGuidance(guidance, deliverAs)));
|
|
485
505
|
};
|
|
486
506
|
// /login acknowledges before its asynchronous local refresh completes.
|
|
@@ -510,6 +530,8 @@ export function createFrameDispatchContext(deps) {
|
|
|
510
530
|
// and executes it immediately even while streaming. It also expands a
|
|
511
531
|
// matching skill/template before applying the same steer behavior.
|
|
512
532
|
if (resolveSteerCall(frame.text, currentSession()) === 'prompt') {
|
|
533
|
+
// The steer→prompt leading-command path never yields a matching user
|
|
534
|
+
// message — no ledger intent (see MessageIdLedger).
|
|
513
535
|
void currentSession()
|
|
514
536
|
.prompt(frame.text, {
|
|
515
537
|
images: frame.images,
|
|
@@ -518,17 +540,25 @@ export function createFrameDispatchContext(deps) {
|
|
|
518
540
|
.catch(relayError);
|
|
519
541
|
break;
|
|
520
542
|
}
|
|
543
|
+
// Message identity: note the intent before steer() so pi's synchronous
|
|
544
|
+
// enqueue queue_update can adopt it; drop it if the call rejects.
|
|
545
|
+
const steerMessageId = frame.message_id ?? randomUUID();
|
|
546
|
+
ledger.noteDispatch(steerMessageId, frame.text);
|
|
547
|
+
const steerFailure = (err) => {
|
|
548
|
+
ledger.dropIntent(steerMessageId);
|
|
549
|
+
relayError(err);
|
|
550
|
+
};
|
|
521
551
|
const guidance = resolveFrameGuidance(frame.text);
|
|
522
552
|
// C1: pi's steer() takes images as a POSITIONAL 2nd arg (`steer(text, images?)`).
|
|
523
553
|
if (guidance === null) {
|
|
524
554
|
void currentSession()
|
|
525
555
|
.steer(frame.text, frame.images)
|
|
526
|
-
.catch(
|
|
556
|
+
.catch(steerFailure);
|
|
527
557
|
break;
|
|
528
558
|
}
|
|
529
559
|
void currentSession()
|
|
530
560
|
.steer(frame.text, frame.images)
|
|
531
|
-
.catch(
|
|
561
|
+
.catch(steerFailure)
|
|
532
562
|
.then(() => sendGuidance(guidance, 'steer'));
|
|
533
563
|
break;
|
|
534
564
|
}
|
|
@@ -806,6 +836,9 @@ export function createFrameDispatchContext(deps) {
|
|
|
806
836
|
type: 'prompt',
|
|
807
837
|
text: frame.text,
|
|
808
838
|
...(frame.images !== undefined ? { images: frame.images } : {}),
|
|
839
|
+
// Carry the sender-minted message identity onto the synthesized prompt
|
|
840
|
+
// frame so a gateway/Core deliver retires its optimistic entry by id.
|
|
841
|
+
...(frame.message_id !== undefined ? { message_id: frame.message_id } : {}),
|
|
809
842
|
});
|
|
810
843
|
if (via === 'steer' || outcome === undefined) {
|
|
811
844
|
turnAcceptedWaiters.delete(ackAccepted);
|
|
@@ -1203,6 +1236,15 @@ export function createFrameDispatchContext(deps) {
|
|
|
1203
1236
|
void currentSession()
|
|
1204
1237
|
.reload()
|
|
1205
1238
|
.then(() => {
|
|
1239
|
+
// pi's reload() re-syncs queue modes from the shared settings.json
|
|
1240
|
+
// (its ONE post-bind syncQueueModesFromSettings call site) —
|
|
1241
|
+
// re-apply the broker's 'all' modes so a reload cannot silently
|
|
1242
|
+
// restore one-at-a-time draining (see rebind.enqueueRebind).
|
|
1243
|
+
const agent = currentSession().agent;
|
|
1244
|
+
if (agent) {
|
|
1245
|
+
agent.steeringMode = 'all';
|
|
1246
|
+
agent.followUpMode = 'all';
|
|
1247
|
+
}
|
|
1206
1248
|
memoizedRefInventory = null;
|
|
1207
1249
|
clearSessionCache();
|
|
1208
1250
|
ackTo(client, 'reload');
|
|
@@ -18,5 +18,9 @@ export declare function readBrokerCursor(nodeId: string): BrokerInboxEntryId | u
|
|
|
18
18
|
export declare function readBrokerInboxSince(nodeId: string, cursor?: string): BrokerInboxEntry[];
|
|
19
19
|
export declare function readBrokerInboxRange(nodeId: string, after: string | undefined, through: string): BrokerInboxEntry[];
|
|
20
20
|
export declare function readBrokerCanceledEntryIds(nodeId: string): Set<BrokerInboxEntryId>;
|
|
21
|
+
/** The node id a canonical report ref points at, or undefined when the ref is
|
|
22
|
+
* absent, absolute, or not a report path. Callers use it to tell whether the
|
|
23
|
+
* report-node projection they hold can still resolve the ref. */
|
|
24
|
+
export declare function reportRefNodeId(ref: string | undefined): string | undefined;
|
|
21
25
|
/** Render the existing inbox digest wording with daemon-projected report existence. */
|
|
22
26
|
export declare function coalesceBrokerInbox(entries: readonly BrokerInboxEntry[], reportNodes: ReadonlyMap<string, BrokerReportNodeDTO>): string;
|
|
@@ -101,16 +101,25 @@ function clipBody(body) {
|
|
|
101
101
|
}
|
|
102
102
|
return { text: text.trimEnd(), clipped };
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
/** The node id a canonical report ref points at, or undefined when the ref is
|
|
105
|
+
* absent, absolute, or not a report path. Callers use it to tell whether the
|
|
106
|
+
* report-node projection they hold can still resolve the ref. */
|
|
107
|
+
export function reportRefNodeId(ref) {
|
|
108
|
+
if (ref === undefined || ref.startsWith('/'))
|
|
109
|
+
return undefined;
|
|
110
|
+
const separator = ref.indexOf(':');
|
|
108
111
|
if (separator <= 0)
|
|
112
|
+
return undefined;
|
|
113
|
+
const relpath = ref.slice(separator + 1);
|
|
114
|
+
if (!relpath.startsWith('reports/') || relpath.includes('..') || relpath.startsWith('/'))
|
|
115
|
+
return undefined;
|
|
116
|
+
return ref.slice(0, separator);
|
|
117
|
+
}
|
|
118
|
+
function inlineReport(entry, reportNodes) {
|
|
119
|
+
const nodeId = reportRefNodeId(entry.ref);
|
|
120
|
+
if (nodeId === undefined || !reportNodes.has(nodeId))
|
|
109
121
|
return null;
|
|
110
|
-
const
|
|
111
|
-
const relpath = entry.ref.slice(separator + 1);
|
|
112
|
-
if (!reportNodes.has(nodeId) || !relpath.startsWith('reports/') || relpath.includes('..') || relpath.startsWith('/'))
|
|
113
|
-
return null;
|
|
122
|
+
const relpath = entry.ref.slice(entry.ref.indexOf(':') + 1);
|
|
114
123
|
const path = `${nodeDir(nodeId)}/${relpath}`;
|
|
115
124
|
try {
|
|
116
125
|
const body = readFileSync(path, 'utf8');
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** One id-bearing message the ledger tracks. */
|
|
2
|
+
export interface LedgerEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class MessageIdLedger {
|
|
7
|
+
private steering;
|
|
8
|
+
private followUp;
|
|
9
|
+
private pendingIntents;
|
|
10
|
+
private stagedRetirementId;
|
|
11
|
+
private recent;
|
|
12
|
+
/** Record an accepted client frame's identity before its engine call runs.
|
|
13
|
+
* `id` is the sender's `message_id` when it minted one, else broker-minted. */
|
|
14
|
+
noteDispatch(id: string, text: string): void;
|
|
15
|
+
/** Remove a noted intent whose engine call rejected — the message will never
|
|
16
|
+
* appear in a queue or as a user message. No-op if already adopted. */
|
|
17
|
+
dropIntent(id: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Fold one pi `queue_update` into the ledger and return the id arrays
|
|
20
|
+
* parallel to its text arrays (total: every position gets an id).
|
|
21
|
+
*
|
|
22
|
+
* Per-queue positional diff against the mirrored entries: a text matching an
|
|
23
|
+
* old entry at-or-after the cursor keeps its id (everything skipped is
|
|
24
|
+
* removed); an unmatched text ADOPTS the first pending intent with equal
|
|
25
|
+
* text, else mints a fresh id. Exactly ONE removal across both queues is
|
|
26
|
+
* pi's per-message retirement (its queue_update precedes the user
|
|
27
|
+
* `message_start` it belongs to) and is staged for `takeUserMessageId`;
|
|
28
|
+
* 0 or >1 removals (an enqueue, clearQueue/dequeue, abort) stage nothing.
|
|
29
|
+
*/
|
|
30
|
+
reconcileQueueUpdate(steering: readonly string[], followUp: readonly string[]): {
|
|
31
|
+
steeringIds: string[];
|
|
32
|
+
followUpIds: string[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The id for a relayed user `message_start`, or undefined when this message
|
|
36
|
+
* carries none (an engine-command expansion, a text pi transformed, or a
|
|
37
|
+
* pre-ledger replay). A staged retirement wins — pi emitted its queue_update
|
|
38
|
+
* synchronously right before this message; otherwise the first pending
|
|
39
|
+
* intent with equal text (the idle-prompt case, which never touches a
|
|
40
|
+
* queue). Id-bearing results are remembered for `recentUserMessages`.
|
|
41
|
+
*/
|
|
42
|
+
takeUserMessageId(messageText: string): string | undefined;
|
|
43
|
+
/** The last {@link MAX_RECENT} id-bearing user-message dispatches, oldest
|
|
44
|
+
* first — the `welcome.recentUserMessages` payload. */
|
|
45
|
+
recentUserMessages(): Array<{
|
|
46
|
+
id: string;
|
|
47
|
+
text: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** Session rebind/replacement: the mirrored queues, staged retirement, and
|
|
50
|
+
* recent list all described the OLD session — drop everything. */
|
|
51
|
+
reset(): void;
|
|
52
|
+
private remember;
|
|
53
|
+
}
|