@north-light/crouter 0.3.190 → 0.3.191
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/broker-ops.d.ts +6 -1
- package/dist/api/dto/broker.d.ts +25 -3
- package/dist/clients/attach/session/frames.js +6 -0
- package/dist/clients/attach/viewer.js +281 -281
- package/dist/core/runtime/broker/node-named.d.ts +14 -0
- package/dist/core/runtime/broker/node-named.js +27 -0
- package/dist/core/runtime/broker-protocol.d.ts +11 -2
- package/dist/core/runtime/broker.js +6 -0
- package/dist/core/runtime/naming.d.ts +3 -3
- package/dist/core/runtime/naming.js +3 -3
- package/dist/daemon/api/handlers/broker-ops.js +12 -2
- package/dist/pi-extensions/broker-local.js +15 -2
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/core/runtime/naming-persist.d.ts +0 -16
- package/dist/core/runtime/naming-persist.js +0 -68
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** What crtrd stored, as it stored it — the payload of a `node_named` frame. */
|
|
2
|
+
export interface NodeNamedNotice {
|
|
3
|
+
description: string;
|
|
4
|
+
icon: string;
|
|
5
|
+
editorLabel: string;
|
|
6
|
+
}
|
|
7
|
+
/** Register the broker's fan-out. One broker hosts one pi process, so the last
|
|
8
|
+
* registration wins; a second broker in one process is not a thing that
|
|
9
|
+
* exists. */
|
|
10
|
+
export declare function onNodeNamed(fn: (notice: NodeNamedNotice) => void): void;
|
|
11
|
+
/** Announce a generated name crtrd's guarded commit APPLIED. A no-op when no
|
|
12
|
+
* broker is listening — the same extension code also runs under test harnesses
|
|
13
|
+
* that host no client registry. */
|
|
14
|
+
export declare function publishNodeNamed(notice: NodeNamedNotice): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// node-named.ts — the in-process hop from the naming extension to the broker's
|
|
2
|
+
// client fan-out.
|
|
3
|
+
//
|
|
4
|
+
// A node's generated name is committed by the naming extension
|
|
5
|
+
// (`pi-extensions/broker-local.ts`), which runs INSIDE the broker's own pi
|
|
6
|
+
// process but holds no reference to the broker's client registry: pi loads
|
|
7
|
+
// extensions by path, so there is no constructor seam to pass one through. This
|
|
8
|
+
// module is that seam — one module-level slot, both ends resolving the same
|
|
9
|
+
// file in the same process.
|
|
10
|
+
//
|
|
11
|
+
// The frame it carries (`node_named`) exists because the name lands seconds
|
|
12
|
+
// AFTER the turn that triggered it: a consumer settling its display state on
|
|
13
|
+
// turn-end reads an unnamed node and has nothing left to re-read on. The
|
|
14
|
+
// committing side pushes instead.
|
|
15
|
+
let listener;
|
|
16
|
+
/** Register the broker's fan-out. One broker hosts one pi process, so the last
|
|
17
|
+
* registration wins; a second broker in one process is not a thing that
|
|
18
|
+
* exists. */
|
|
19
|
+
export function onNodeNamed(fn) {
|
|
20
|
+
listener = fn;
|
|
21
|
+
}
|
|
22
|
+
/** Announce a generated name crtrd's guarded commit APPLIED. A no-op when no
|
|
23
|
+
* broker is listening — the same extension code also runs under test harnesses
|
|
24
|
+
* that host no client registry. */
|
|
25
|
+
export function publishNodeNamed(notice) {
|
|
26
|
+
listener?.(notice);
|
|
27
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentSession, AgentSessionEvent, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcSessionState, SessionEntry, SessionInfo, SettingsConfig, SessionStats } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { Api, ImageContent, Model } from '@earendil-works/pi-ai';
|
|
3
|
-
import type { BrokerErrorFrame } from '../../api/dto/broker.js';
|
|
3
|
+
import type { BrokerErrorFrame, BrokerNodeNamedFrame } from '../../api/dto/broker.js';
|
|
4
4
|
import type { ToolGroupSummary } from '../../shared/tool-groups.js';
|
|
5
5
|
/** pi's own RPC types, re-exported unchanged. `broker-protocol` stays the one
|
|
6
6
|
* import site for the wire union; the shapes themselves are pi's, versioned by
|
|
@@ -386,6 +386,15 @@ export interface ModelChangedFrame {
|
|
|
386
386
|
* promise instead of hanging it — absent on uncorrelated errors (engine drive
|
|
387
387
|
* errors, command-op failures, frame_overflow). There is NO `retryable` field. */
|
|
388
388
|
export type ErrorFrame = BrokerErrorFrame;
|
|
389
|
+
/** The `node_named` control frame. SOURCED from the shared
|
|
390
|
+
* `@north-light/crouter-api` contract for the same reason `error` is: Northlight
|
|
391
|
+
* Core reads this frame off its own attach, so the wire shape here and the shape
|
|
392
|
+
* it consumes are one compiler-checked type.
|
|
393
|
+
*
|
|
394
|
+
* Broadcast when the naming extension's GUARDED commit applied — a name a human
|
|
395
|
+
* already set never produces one. The in-process hop from the extension to this
|
|
396
|
+
* fan-out is `broker/node-named.ts`. */
|
|
397
|
+
export type NodeNamedFrame = BrokerNodeNamedFrame;
|
|
389
398
|
/** Result of a command op (§1.3): `for` echoes the op name, `ok` the
|
|
390
399
|
* outcome, `detail` an optional human-readable note. */
|
|
391
400
|
export interface AckFrame {
|
|
@@ -589,7 +598,7 @@ export interface ToolGroupSummaryFrame {
|
|
|
589
598
|
/** Everything the broker can send. Live `AgentSessionEvent`s are relayed
|
|
590
599
|
* verbatim (the broker adds nothing); the broker's own control frames carry
|
|
591
600
|
* non-colliding `type` discriminants. */
|
|
592
|
-
export type BrokerToClient = WelcomeFrame | WorkingActivityFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
|
|
601
|
+
export type BrokerToClient = WelcomeFrame | WorkingActivityFrame | ModelChangedFrame | ErrorFrame | NodeNamedFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
|
|
593
602
|
/** Encode one frame as a single newline-terminated JSON line. */
|
|
594
603
|
export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string;
|
|
595
604
|
/** Byte bounds for a {@link FrameDecoder} (C5). */
|
|
@@ -42,6 +42,7 @@ import { visibleMessages } from './session-visibility.js';
|
|
|
42
42
|
import { isUnknownModel } from './model-selection.js';
|
|
43
43
|
import { BrokerClientRegistry } from './broker/client-registry.js';
|
|
44
44
|
import { ToolGroupTracker } from './broker/tool-groups.js';
|
|
45
|
+
import { onNodeNamed } from './broker/node-named.js';
|
|
45
46
|
import { FaultRetry } from './broker/fault-retry.js';
|
|
46
47
|
import { EventProjection } from './broker/event-projection.js';
|
|
47
48
|
import { createFrameDispatchContext, handleFrame } from './broker/frame-dispatch.js';
|
|
@@ -275,6 +276,11 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
275
276
|
const registry = new BrokerClientRegistry(nodeId);
|
|
276
277
|
const pendingDialogs = new Map();
|
|
277
278
|
const toolGroups = new ToolGroupTracker(nodeId, (frame) => registry.broadcast(frame));
|
|
279
|
+
// The naming extension commits the node's generated name a few seconds into
|
|
280
|
+
// its first turn and hands it here (broker/node-named.ts). Every attached
|
|
281
|
+
// client learns it on the pipe it already holds instead of re-reading the node
|
|
282
|
+
// on some later, unrelated trigger.
|
|
283
|
+
onNodeNamed((notice) => registry.broadcast({ type: 'node_named', ...notice }));
|
|
278
284
|
let disposed = false;
|
|
279
285
|
let disposePending = false;
|
|
280
286
|
let server;
|
|
@@ -34,9 +34,9 @@ export declare function sanitizeIcon(raw: string): string;
|
|
|
34
34
|
* to a sanitized SessionName, or `{name:'',icon:''}` on timeout, a missing tool
|
|
35
35
|
* call, or a malformed emission so the caller can fall back to a local slug.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* This module stays canvas-free so CLI leaves can reach it. The canvas-coupled
|
|
38
|
+
* commit path lives in pi-extensions/broker-local.ts, which imports this pure
|
|
39
|
+
* helper and writes through crtrd's guarded generated-name operation. */
|
|
40
40
|
export declare function headlessName(body: string, sourceCap?: number): Promise<SessionName>;
|
|
41
41
|
/** Generate a name from a bounded conversation source rather than a single
|
|
42
42
|
* kickoff message. The caller owns selecting representative conversation text;
|
|
@@ -185,9 +185,9 @@ export function sanitizeIcon(raw) {
|
|
|
185
185
|
* to a sanitized SessionName, or `{name:'',icon:''}` on timeout, a missing tool
|
|
186
186
|
* call, or a malformed emission so the caller can fall back to a local slug.
|
|
187
187
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
188
|
+
* This module stays canvas-free so CLI leaves can reach it. The canvas-coupled
|
|
189
|
+
* commit path lives in pi-extensions/broker-local.ts, which imports this pure
|
|
190
|
+
* helper and writes through crtrd's guarded generated-name operation. */
|
|
191
191
|
export async function headlessName(body, sourceCap = PROMPT_CAP) {
|
|
192
192
|
const empty = { name: '', icon: '' };
|
|
193
193
|
const route = nameModelRequest();
|
|
@@ -350,7 +350,12 @@ function handleGeneratedName(ctx) {
|
|
|
350
350
|
description: request.description,
|
|
351
351
|
...(request.icon.trim() === '' ? {} : { icon: request.icon }),
|
|
352
352
|
});
|
|
353
|
-
return {
|
|
353
|
+
return {
|
|
354
|
+
applied: true,
|
|
355
|
+
editorLabel: editorLabel(updated),
|
|
356
|
+
description: updated.description ?? '',
|
|
357
|
+
icon: updated.icon ?? '',
|
|
358
|
+
};
|
|
354
359
|
}
|
|
355
360
|
const expected = request.expected;
|
|
356
361
|
if (node.name !== expected.name || (node.description ?? '') !== expected.description
|
|
@@ -360,7 +365,12 @@ function handleGeneratedName(ctx) {
|
|
|
360
365
|
description: request.description,
|
|
361
366
|
icon: request.icon.trim() === '' ? undefined : request.icon,
|
|
362
367
|
});
|
|
363
|
-
return {
|
|
368
|
+
return {
|
|
369
|
+
applied: true,
|
|
370
|
+
editorLabel: editorLabel(updated),
|
|
371
|
+
description: updated.description ?? '',
|
|
372
|
+
icon: updated.icon ?? '',
|
|
373
|
+
};
|
|
364
374
|
});
|
|
365
375
|
return { status: 200, body: result };
|
|
366
376
|
}
|
|
@@ -9,6 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
10
10
|
import { REVIVE_KICKOFF_SENTINEL } from '../shared/generated-context.js';
|
|
11
11
|
import { brokerExtensionState, commitBrokerGeneratedName, } from '../core/runtime/broker/daemon-ops.js';
|
|
12
|
+
import { publishNodeNamed } from '../core/runtime/broker/node-named.js';
|
|
12
13
|
import { headlessName, headlessNameFromConversation, sanitizeSessionName, slugFromPrompt, } from '../core/runtime/naming.js';
|
|
13
14
|
export { REVIVE_KICKOFF_SENTINEL };
|
|
14
15
|
function contextDir(nodeId) {
|
|
@@ -58,8 +59,20 @@ export function generateAndCommitInitialName(nodeId, prompt, onNamed) {
|
|
|
58
59
|
}));
|
|
59
60
|
})
|
|
60
61
|
.then((result) => {
|
|
61
|
-
if (result?.applied
|
|
62
|
-
|
|
62
|
+
if (!result?.applied || result.editorLabel === undefined)
|
|
63
|
+
return;
|
|
64
|
+
// Push the committed name down the attach stream BEFORE updating the live
|
|
65
|
+
// session label: both are the same delivery — the label reaches this
|
|
66
|
+
// process's own editor chip, the frame reaches every attached client
|
|
67
|
+
// (Northlight Core titles the conversation from it). The name lands
|
|
68
|
+
// seconds after the turn that triggered it, so a consumer with no frame
|
|
69
|
+
// has nothing left to re-read on.
|
|
70
|
+
publishNodeNamed({
|
|
71
|
+
description: result.description ?? '',
|
|
72
|
+
icon: result.icon ?? '',
|
|
73
|
+
editorLabel: result.editorLabel,
|
|
74
|
+
});
|
|
75
|
+
onNamed?.(result.editorLabel);
|
|
63
76
|
})
|
|
64
77
|
.catch(() => { });
|
|
65
78
|
}
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.191",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.191",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { NodeMeta } from '../canvas/index.js';
|
|
2
|
-
/** Asynchronously generate a name for `prompt` and persist it to the node's
|
|
3
|
-
* meta as `description` + `icon` — only if the node has none yet (so a later
|
|
4
|
-
* message never clobbers it). Non-blocking: safe to call from inside a live pi
|
|
5
|
-
* event loop. Best-effort; swallows all errors. The slug fallback carries no
|
|
6
|
-
* icon: a locally derived name has no judgment behind it to pick a glyph from.
|
|
7
|
-
*
|
|
8
|
-
* `onNamed` (optional) fires with the freshly-persisted meta the moment the
|
|
9
|
-
* name lands — the canvas-goal-capture naming hook passes a callback that calls
|
|
10
|
-
* pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
|
|
11
|
-
* same session, instead of waiting for the next revive/cycle. */
|
|
12
|
-
export declare function generateAndPersistName(nodeId: string, prompt: string, onNamed?: (meta: NodeMeta) => void): void;
|
|
13
|
-
/** Re-name an automatically titled node from its current conversation. The caller
|
|
14
|
-
* supplies the guard that distinguishes the original generated editor title from
|
|
15
|
-
* a title a person changed while this headless request was in flight. */
|
|
16
|
-
export declare function regenerateAndPersistName(nodeId: string, conversation: string, isStillAutomatic: () => boolean, onNamed?: (meta: NodeMeta) => void): void;
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// Session naming — canvas-coupled persistence seam.
|
|
2
|
-
//
|
|
3
|
-
// The pure naming machinery (headless pi call + slug fallbacks) lives in
|
|
4
|
-
// naming.ts and stays canvas-free so CLI leaves can reach it without dragging
|
|
5
|
-
// in canvas state. This one function is the only naming path that writes to a
|
|
6
|
-
// node's meta, so it lives here — broker-side (canvas-goal-capture), never on a
|
|
7
|
-
// CLI leaf's import graph.
|
|
8
|
-
import { headlessName, headlessNameFromConversation, sanitizeSessionName, slugFromPrompt } from './naming.js';
|
|
9
|
-
import { getNode, updateNode } from '../canvas/index.js';
|
|
10
|
-
/** Asynchronously generate a name for `prompt` and persist it to the node's
|
|
11
|
-
* meta as `description` + `icon` — only if the node has none yet (so a later
|
|
12
|
-
* message never clobbers it). Non-blocking: safe to call from inside a live pi
|
|
13
|
-
* event loop. Best-effort; swallows all errors. The slug fallback carries no
|
|
14
|
-
* icon: a locally derived name has no judgment behind it to pick a glyph from.
|
|
15
|
-
*
|
|
16
|
-
* `onNamed` (optional) fires with the freshly-persisted meta the moment the
|
|
17
|
-
* name lands — the canvas-goal-capture naming hook passes a callback that calls
|
|
18
|
-
* pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
|
|
19
|
-
* same session, instead of waiting for the next revive/cycle. */
|
|
20
|
-
export function generateAndPersistName(nodeId, prompt, onNamed) {
|
|
21
|
-
const body = (prompt ?? '').trim();
|
|
22
|
-
if (body === '')
|
|
23
|
-
return;
|
|
24
|
-
const persist = (generated) => {
|
|
25
|
-
try {
|
|
26
|
-
const meta = getNode(nodeId);
|
|
27
|
-
if (meta === null)
|
|
28
|
-
return;
|
|
29
|
-
if ((meta.description ?? '').trim() !== '')
|
|
30
|
-
return; // already named
|
|
31
|
-
const clean = sanitizeSessionName(generated.name);
|
|
32
|
-
const description = clean !== '' ? clean : slugFromPrompt(body);
|
|
33
|
-
const icon = generated.icon.trim();
|
|
34
|
-
const updated = updateNode(nodeId, { description, ...(icon !== '' ? { icon } : {}) });
|
|
35
|
-
onNamed?.(updated);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// best-effort
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
void headlessName(body).then(persist);
|
|
42
|
-
}
|
|
43
|
-
/** Re-name an automatically titled node from its current conversation. The caller
|
|
44
|
-
* supplies the guard that distinguishes the original generated editor title from
|
|
45
|
-
* a title a person changed while this headless request was in flight. */
|
|
46
|
-
export function regenerateAndPersistName(nodeId, conversation, isStillAutomatic, onNamed) {
|
|
47
|
-
const body = (conversation ?? '').trim();
|
|
48
|
-
if (body === '')
|
|
49
|
-
return;
|
|
50
|
-
const persist = (generated) => {
|
|
51
|
-
try {
|
|
52
|
-
if (!isStillAutomatic())
|
|
53
|
-
return;
|
|
54
|
-
const meta = getNode(nodeId);
|
|
55
|
-
if (meta === null)
|
|
56
|
-
return;
|
|
57
|
-
const clean = sanitizeSessionName(generated.name);
|
|
58
|
-
const description = clean !== '' ? clean : slugFromPrompt(body);
|
|
59
|
-
const icon = generated.icon.trim();
|
|
60
|
-
const updated = updateNode(nodeId, { description, icon: icon !== '' ? icon : undefined });
|
|
61
|
-
onNamed?.(updated);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// best-effort
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
void headlessNameFromConversation(body).then(persist);
|
|
68
|
-
}
|