@north-light/api 0.3.447 → 0.3.449

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.
Files changed (35) hide show
  1. package/dist/api/contract.d.ts +1 -1
  2. package/dist/api/contract.js +1 -1
  3. package/dist/api/dto/nodes.d.ts +5 -0
  4. package/dist/api/dto/objects.d.ts +33 -2
  5. package/dist/api/error-codes.d.ts +15 -0
  6. package/dist/api/error-codes.js +6 -0
  7. package/dist/core/scoped-state/settings.js +26 -8
  8. package/dist/shared/generated-context.d.ts +8 -0
  9. package/dist/shared/generated-context.js +6 -1
  10. package/dist/shared/inbox-entry-body.js +1 -1
  11. package/package.json +1 -1
  12. package/dist/api/__tests__/serial/client.test.d.ts +0 -1
  13. package/dist/api/__tests__/serial/client.test.js +0 -141
  14. package/dist/api/dto/memory.d.ts +0 -171
  15. package/dist/api/dto/memory.js +0 -7
  16. package/dist/core/canvas/types.d.ts +0 -446
  17. package/dist/core/canvas/types.js +0 -6
  18. package/dist/core/fault-classifier.d.ts +0 -34
  19. package/dist/core/fault-classifier.js +0 -96
  20. package/dist/core/provider-error-recognition.d.ts +0 -27
  21. package/dist/core/provider-error-recognition.js +0 -74
  22. package/dist/core/provider-failure.d.ts +0 -27
  23. package/dist/core/provider-failure.js +0 -109
  24. package/dist/core/runtime/broker-protocol.d.ts +0 -721
  25. package/dist/core/runtime/broker-protocol.js +0 -152
  26. package/dist/core/runtime/managed-provider-cooling.d.ts +0 -24
  27. package/dist/core/runtime/managed-provider-cooling.js +0 -62
  28. package/dist/core/runtime/stamp/protocol.d.ts +0 -90
  29. package/dist/core/runtime/stamp/protocol.js +0 -113
  30. package/dist/core/scopes.d.ts +0 -17
  31. package/dist/core/scopes.js +0 -42
  32. package/dist/core/substrate/subject-fields.d.ts +0 -48
  33. package/dist/core/substrate/subject-fields.js +0 -39
  34. package/dist/shared/tool-groups.d.ts +0 -26
  35. package/dist/shared/tool-groups.js +0 -64
@@ -1,152 +0,0 @@
1
- // broker-protocol.ts — the client↔broker wire protocol for the headless node
2
- // Surface (design §5.2). Pure types + a newline-delimited JSON codec; no I/O, no
3
- // SDK construction. Consumed by the broker (src/core/runtime/broker.ts), the
4
- // `crtr surface attach` terminal client, and crtrd's attach bridge.
5
- //
6
- // The transport is one unix socket per node (`nodeDir(id)/view.sock`) speaking
7
- // newline-delimited JSON frames. Live engine events are relayed VERBATIM — the
8
- // broker is a transparent multiplexer, so a broker→client frame is either one of
9
- // the broker's own control frames (welcome/error) or a raw pi
10
- // `AgentSessionEvent` / `extension_ui_request`. The `type` namespaces never
11
- // collide, so the union below stays a clean discriminated union.
12
- //
13
- // The extension-dialog frames ARE pi's RPC types (decision §1.15) — imported
14
- // from the package root and re-exported here, so `broker-protocol` remains the
15
- // one import site for the wire union while the shapes stay pi's, versioned by
16
- // pi. pi names the correlation field `id` (design §5.2 wrote `request_id`); we
17
- // follow pi.
18
- import { StringDecoder } from 'node:string_decoder';
19
- // Client → broker frames
20
- /** Largest transcript window a client may request in its welcome snapshot. */
21
- export const SNAPSHOT_TAIL_MAX = 500;
22
- /** Invalid requests preserve the complete welcome; valid requests are capped. */
23
- export function normalizeSnapshotTail(value) {
24
- if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1)
25
- return undefined;
26
- return Math.min(value, SNAPSHOT_TAIL_MAX);
27
- }
28
- // Newline-delimited JSON codec (the framing pi solves with `jsonl`, reproduced
29
- // locally so the protocol module owns no transport dependency)
30
- /** Encode one frame as a single newline-terminated JSON line. */
31
- export function encodeFrame(frame) {
32
- return JSON.stringify(frame) + '\n';
33
- }
34
- /** Thrown by {@link FrameDecoder.push} when a peer's buffered bytes exceed a cap
35
- * (C5). The caller catches it, best-effort sends `error{code:'frame_overflow'}`,
36
- * and DESTROYS that socket — cap-and-drop the peer, never grow-to-OOM. `kind`
37
- * says which bound tripped: a single oversized frame (`line`) or total backlog
38
- * (`total`). */
39
- export class FrameOverflowError extends Error {
40
- kind;
41
- bytes;
42
- cap;
43
- constructor(kind, bytes, cap) {
44
- super(`frame decoder overflow (${kind}): ${bytes} bytes exceeds cap ${cap}`);
45
- this.kind = kind;
46
- this.bytes = bytes;
47
- this.cap = cap;
48
- this.name = 'FrameOverflowError';
49
- }
50
- }
51
- /** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the
52
- * full message history and can be many MiB, so these are generous. Covers
53
- * realistic long sessions; snapshot CHUNKING is deferred — the long-lived
54
- * broker makes a big welcome a realistic, not pathological, trigger, but the
55
- * fixed cap is accepted. */
56
- export const CLIENT_READ_CAPS = {
57
- maxLineBytes: 256 * 1024 * 1024,
58
- maxTotalBytes: 256 * 1024 * 1024,
59
- };
60
- /** Caps the BROKER uses reading CLIENT frames. Sized so an image-paste frame
61
- * fits: a resizeImage-bounded PNG's base64 is a few MiB and would clip a
62
- * tighter 4 MiB line cap, so these hold an image-bearing `prompt`/`steer`/
63
- * `follow_up` frame. Still bounded, so a malicious/buggy client→broker frame
64
- * is cap-and-dropped, never grow-to-OOM (C5). */
65
- export const BROKER_READ_CAPS = {
66
- maxLineBytes: 24 * 1024 * 1024,
67
- maxTotalBytes: 48 * 1024 * 1024,
68
- };
69
- /** Incremental newline-delimited JSON reader: feed raw socket chunks, get back
70
- * the complete frames decoded so far. Buffers a partial trailing line across
71
- * pushes; silently drops a malformed (unparseable) line (a viewer must never
72
- * crash the broker on bad JSON). The caller narrows the `unknown` against the
73
- * frame unions.
74
- *
75
- * BOUNDED (C5): an oversized buffer is the THROW case. If a single push peaks the
76
- * internal buffer above `maxTotalBytes`, or the surviving (unterminated) partial
77
- * line exceeds `maxLineBytes`, {@link push} throws {@link FrameOverflowError}
78
- * instead of returning frames — the caller drops that peer rather than letting a
79
- * malicious/buggy stream grow the buffer to OOM. Bytes are measured with
80
- * `Buffer.byteLength`, never string length; a running counter makes each push's
81
- * size CHECK O(1) (no re-scan of the whole buffer to measure it).
82
- *
83
- * AMORTIZED O(total bytes) (perf regression fix, 2026-06-09): the partial line
84
- * carried across pushes is held as an ARRAY of chunk strings (`parts`), joined
85
- * only when its terminating `\n` arrives. The `\n` scan looks at each incoming
86
- * chunk ONCE — never re-scanning the accumulated carry — so a multi-MiB frame
87
- * arriving in ~64 KiB socket chunks costs O(frame) total instead of O(frame ×
88
- * chunks). The old `buf += chunk; buf.indexOf('\n')` shape flattened + re-walked
89
- * the whole carry per chunk: a 16 MiB welcome snapshot cost ~250 ms of
90
- * event-loop stall in `crtr surface attach` (typing lag) and the same again in the
91
- * broker; this shape decodes it in ~13 ms.
92
- *
93
- * A `StringDecoder` decodes incoming Buffer chunks: an incomplete trailing
94
- * multibyte sequence is held back across pushes instead of being mangled to
95
- * U+FFFD, so a char split at a `node:net` chunk boundary is never corrupted, and
96
- * `partBytes` counts decoded-string bytes (a raw `chunk.length` would desync
97
- * and weaken the C5 cap on multibyte/invalid input). */
98
- export class FrameDecoder {
99
- caps;
100
- /** The unterminated partial line carried across pushes, as unjoined chunks
101
- * (never contains a '\n'). Joined lazily when its newline arrives. */
102
- parts = [];
103
- /** Byte length (Buffer.byteLength) of `parts` joined — kept in exact lockstep. */
104
- partBytes = 0;
105
- utf8 = new StringDecoder('utf8');
106
- constructor(caps) {
107
- this.caps = caps;
108
- }
109
- push(chunk) {
110
- // A string input is already complete text; a Buffer goes through the
111
- // StringDecoder so a boundary-split multibyte char is buffered, not corrupted.
112
- const str = typeof chunk === 'string' ? chunk : this.utf8.write(chunk);
113
- const strBytes = Buffer.byteLength(str);
114
- // Peak-buffer bound: caps the most we ever hold in one push (carry + this
115
- // entire chunk, before draining complete frames) — identical accounting to
116
- // the old whole-buffer check.
117
- if (this.partBytes + strBytes > this.caps.maxTotalBytes) {
118
- throw new FrameOverflowError('total', this.partBytes + strBytes, this.caps.maxTotalBytes);
119
- }
120
- const out = [];
121
- let from = 0; // scan offset into `str` — each chunk is scanned exactly once
122
- let nl;
123
- while ((nl = str.indexOf('\n', from)) >= 0) {
124
- const tail = str.slice(from, nl);
125
- // First line of the push completes the carried partial; later lines live
126
- // entirely inside `str` (parts is empty after the first join).
127
- const line = (this.parts.length > 0 ? this.parts.join('') + tail : tail).trim();
128
- this.parts.length = 0;
129
- this.partBytes = 0;
130
- from = nl + 1;
131
- if (line === '')
132
- continue;
133
- try {
134
- out.push(JSON.parse(line));
135
- }
136
- catch {
137
- /* drop a malformed frame — never throw out of the reader for bad JSON */
138
- }
139
- }
140
- if (from < str.length) {
141
- const rest = from === 0 ? str : str.slice(from);
142
- this.parts.push(rest);
143
- this.partBytes += from === 0 ? strBytes : Buffer.byteLength(rest);
144
- }
145
- // Single-frame bound: a partial line that has grown past one frame's cap is a
146
- // peer streaming bytes with no newline — drop it rather than buffer forever.
147
- if (this.partBytes > this.caps.maxLineBytes) {
148
- throw new FrameOverflowError('line', this.partBytes, this.caps.maxLineBytes);
149
- }
150
- return out;
151
- }
152
- }
@@ -1,24 +0,0 @@
1
- import type { AssistantMessageDiagnostic } from '@earendil-works/pi-ai';
2
- /** Diagnostic `type` tag carrying the cooling deadline; also the wire contract the
3
- * stophook matches on. */
4
- export declare const MANAGED_PROVIDER_COOLING_DIAGNOSTIC = "crtr:managed-provider-cooling";
5
- /** Stable error code so a caller can recognize the typed error without message parsing. */
6
- export declare const MANAGED_PROVIDER_COOLING_CODE = "managed_provider_cooling";
7
- /** Every managed provider pool tried this turn is cooling; `rateLimitedUntil` is
8
- * the earliest future deadline (epoch ms) across those pools. */
9
- export declare class ManagedProviderCoolingError extends Error {
10
- readonly code = "managed_provider_cooling";
11
- readonly rateLimitedUntil: number;
12
- constructor(message: string, rateLimitedUntil: number);
13
- /** The diagnostic to attach to the terminal assistant error so the deadline
14
- * reaches the stophook without regex-parsing the message. Built inline (not via
15
- * pi-ai's `createAssistantMessageDiagnostic`) to keep this module's runtime
16
- * import graph pi-free — the shape matches `AssistantMessageDiagnostic`. */
17
- toDiagnostic(): AssistantMessageDiagnostic;
18
- }
19
- /** Strictly read the managed-cooling deadline (epoch ms) off an assistant
20
- * message's diagnostics. Returns the finite, strictly-positive `rateLimitedUntil`
21
- * from the first cooling diagnostic, or undefined when the message carries none
22
- * (or a malformed one). */
23
- export declare function hasManagedProviderCoolingDiagnostic(message: unknown): boolean;
24
- export declare function extractCoolingDeadline(message: unknown): number | undefined;
@@ -1,62 +0,0 @@
1
- // managed-provider-cooling.ts — the typed carrier for "every managed provider
2
- // pool tried this turn is temporarily cooling down".
3
- //
4
- // runManagedProvider (provider-rotation.ts) raises this once every attempted
5
- // managed-pool credential is rate-limited with at least one finite future
6
- // deadline. The error converts to an AssistantMessageDiagnostic that survives
7
- // verbatim into the terminal error message and therefore into agent_end.messages
8
- // (pi-ai's AssistantMessageEventStream.result() returns the exact error message
9
- // object). The canvas-stophook reads the earliest deadline back off that message
10
- // and stamps it onto the pi→provider fault's retry.nextAt, so the broker's
11
- // existing auto-retry re-drives the pre-content turn exactly when the soonest
12
- // pool frees up — turning a transient pool cooldown into an auto (not fatal)
13
- // fault instead of a permanent one.
14
- /** Diagnostic `type` tag carrying the cooling deadline; also the wire contract the
15
- * stophook matches on. */
16
- export const MANAGED_PROVIDER_COOLING_DIAGNOSTIC = "crtr:managed-provider-cooling";
17
- /** Stable error code so a caller can recognize the typed error without message parsing. */
18
- export const MANAGED_PROVIDER_COOLING_CODE = "managed_provider_cooling";
19
- /** Every managed provider pool tried this turn is cooling; `rateLimitedUntil` is
20
- * the earliest future deadline (epoch ms) across those pools. */
21
- export class ManagedProviderCoolingError extends Error {
22
- code = MANAGED_PROVIDER_COOLING_CODE;
23
- rateLimitedUntil;
24
- constructor(message, rateLimitedUntil) {
25
- super(message);
26
- this.name = "ManagedProviderCoolingError";
27
- this.rateLimitedUntil = rateLimitedUntil;
28
- }
29
- /** The diagnostic to attach to the terminal assistant error so the deadline
30
- * reaches the stophook without regex-parsing the message. Built inline (not via
31
- * pi-ai's `createAssistantMessageDiagnostic`) to keep this module's runtime
32
- * import graph pi-free — the shape matches `AssistantMessageDiagnostic`. */
33
- toDiagnostic() {
34
- return {
35
- type: MANAGED_PROVIDER_COOLING_DIAGNOSTIC,
36
- timestamp: Date.now(),
37
- details: { rateLimitedUntil: this.rateLimitedUntil },
38
- };
39
- }
40
- }
41
- /** Strictly read the managed-cooling deadline (epoch ms) off an assistant
42
- * message's diagnostics. Returns the finite, strictly-positive `rateLimitedUntil`
43
- * from the first cooling diagnostic, or undefined when the message carries none
44
- * (or a malformed one). */
45
- export function hasManagedProviderCoolingDiagnostic(message) {
46
- const diagnostics = message?.diagnostics;
47
- return Array.isArray(diagnostics) && diagnostics.some((diagnostic) => diagnostic?.type === MANAGED_PROVIDER_COOLING_DIAGNOSTIC);
48
- }
49
- export function extractCoolingDeadline(message) {
50
- const diagnostics = message?.diagnostics;
51
- if (!Array.isArray(diagnostics))
52
- return undefined;
53
- for (const diagnostic of diagnostics) {
54
- if (diagnostic?.type !== MANAGED_PROVIDER_COOLING_DIAGNOSTIC)
55
- continue;
56
- const raw = diagnostic?.details?.rateLimitedUntil;
57
- const value = typeof raw === "number" ? raw : Number(raw);
58
- if (Number.isFinite(value) && value > 0)
59
- return value;
60
- }
61
- return undefined;
62
- }
@@ -1,90 +0,0 @@
1
- export declare const CRTR_STAMP_CUSTOM_TYPE = "crtr-stamp";
2
- /** Envelope version. Readers switch on `(kind, v)`. */
3
- export declare const STAMP_SCHEMA_VERSION = 1;
4
- export interface StampEnvelope<K extends string = string, P = object> {
5
- v: number;
6
- kind: K;
7
- payload: P;
8
- /** Plugin/contributor fields, namespaced. The core writer never interprets them. */
9
- ext?: Record<string, Record<string, unknown>>;
10
- }
11
- /** One session file belongs to one node: which node, and which crtr wrote it.
12
- * Makes the file self-describing off-box, where canvas.db is unavailable. */
13
- export interface SessionStampPayload {
14
- node: string;
15
- crtr: string;
16
- }
17
- /** Transport a person's words arrived on. crouter core knows the attach viewer;
18
- * other front doors announce their own channel. */
19
- export interface HumanOrigin {
20
- type: 'human';
21
- channel?: {
22
- type: string;
23
- };
24
- }
25
- /** The runtime kinds mirror the `<runtime kind="…">` card vocabulary. */
26
- export type RuntimeOriginKind = 'inbox' | 'wake' | 'revive' | 'restart-continuation' | 'stop-guard' | 'park' | 'recovery' | 'context-nudge' | 'kickoff';
27
- /** One durable inbox entry's attribution, as delivered. */
28
- export interface InboxOriginEntry {
29
- from?: string;
30
- fromName?: string;
31
- kind: string;
32
- tier: string;
33
- ref?: string;
34
- }
35
- export interface RuntimeOrigin {
36
- type: 'runtime';
37
- kind: RuntimeOriginKind;
38
- /** `wake` carries the fired cron's `WakeOrigin` under this key rather than
39
- * spread flat: WakeOrigin has its own `kind` (`node_birth` /
40
- * `schedule_revive`), which flat would overwrite the origin kind. */
41
- wake?: {
42
- kind: string;
43
- ownerId: string | null;
44
- ownerName?: string;
45
- armedAt: string;
46
- recur?: string | null;
47
- };
48
- /** `inbox` carries one element per delivered entry. */
49
- entries?: InboxOriginEntry[];
50
- /** Other kind-specific structured fields — the structure the seam holds,
51
- * where the card carries only prose. */
52
- [field: string]: unknown;
53
- }
54
- export type MessageOrigin = HumanOrigin | RuntimeOrigin;
55
- export type MessageVisibility = 'visible' | 'internal';
56
- export interface MessageStampPayload {
57
- /** The target message's own `timestamp` (unix ms) — the join key. */
58
- msgTs: number;
59
- /** Absent means the message arrived with no announced origin (unknown),
60
- * never a guess. */
61
- origin?: MessageOrigin;
62
- /** Absent means visible. Internal messages remain in the raw session and
63
- * carry their classification through transcript projections. */
64
- visibility?: MessageVisibility;
65
- }
66
- export type SessionStamp = StampEnvelope<'session', SessionStampPayload>;
67
- export type MessageStamp = StampEnvelope<'message', MessageStampPayload>;
68
- /** A kickoff prompt is composed by the daemon but sent by the broker, in a
69
- * different process, so its origin rides the broker's spawn env instead of the
70
- * in-process announcement channel. */
71
- export declare const KICKOFF_ORIGIN_ENV = "CRTR_KICKOFF_ORIGIN";
72
- export declare function encodeKickoffOrigin(origin: MessageOrigin): string;
73
- /** The kickoff origin the spawner recorded, or null when this launch carried
74
- * none (an idle broker, or a launch predating the variable). */
75
- export declare function decodeKickoffOrigin(raw: string | undefined): MessageOrigin | null;
76
- /** The stamp envelope carried by one session entry, or null when the entry is
77
- * not a well-formed crtr-stamp. */
78
- export declare function readStampEntry(entry: unknown): StampEnvelope | null;
79
- /** Every well-formed stamp in `entries`, in file order. */
80
- export declare function readStamps(entries: readonly unknown[]): StampEnvelope[];
81
- /** The session stamp in force at the end of `entries` — a fork carries the
82
- * source node's stamp until the new node appends its own, so "latest" is what
83
- * a reader wants. */
84
- export declare function latestSessionStamp(entries: readonly unknown[]): SessionStamp | null;
85
- /** The stamp for the message persisted at `msgTs`. Late or revised metadata is
86
- * a second stamp with the same key, so the last one wins. */
87
- export declare function messageStampFor(entries: readonly unknown[], msgTs: number): MessageStamp | null;
88
- /** Who authored the message persisted at `msgTs`, or null when nothing
89
- * announced an origin for it. */
90
- export declare function originFor(entries: readonly unknown[], msgTs: number): MessageOrigin | null;
@@ -1,113 +0,0 @@
1
- // crtr-stamp — the durable per-message metadata crouter hangs on a pi session
2
- // jsonl. One customType carries every record; the record shape is discriminated
3
- // inside `data` by `kind`.
4
- //
5
- // A stamp is a pi CustomEntry: a standalone jsonl line that pi's
6
- // buildSessionContext ignores, so nothing here ever reaches the LLM. Stamps are
7
- // correlated to messages by the target message's own `timestamp` (`msgTs`), not
8
- // by tree position — an extension's message_end handler runs BEFORE pi persists
9
- // the message, so a stamp is physically the message's parent, never its child.
10
- //
11
- // Readers MUST ignore unknown envelope keys: that is what makes adding an
12
- // optional field later non-breaking. `v` bumps only for a rename, a removal, or
13
- // a change of meaning.
14
- //
15
- // The writer is `src/pi-extensions/canvas-stamp.ts` and nothing else.
16
- export const CRTR_STAMP_CUSTOM_TYPE = 'crtr-stamp';
17
- /** Envelope version. Readers switch on `(kind, v)`. */
18
- export const STAMP_SCHEMA_VERSION = 1;
19
- /** A kickoff prompt is composed by the daemon but sent by the broker, in a
20
- * different process, so its origin rides the broker's spawn env instead of the
21
- * in-process announcement channel. */
22
- export const KICKOFF_ORIGIN_ENV = 'CRTR_KICKOFF_ORIGIN';
23
- export function encodeKickoffOrigin(origin) {
24
- return JSON.stringify(origin);
25
- }
26
- /** The kickoff origin the spawner recorded, or null when this launch carried
27
- * none (an idle broker, or a launch predating the variable). */
28
- export function decodeKickoffOrigin(raw) {
29
- if (raw === undefined || raw.trim() === '')
30
- return null;
31
- let parsed;
32
- try {
33
- parsed = JSON.parse(raw);
34
- }
35
- catch {
36
- return null;
37
- }
38
- if (!isRecord(parsed))
39
- return null;
40
- if (parsed['type'] !== 'human' && parsed['type'] !== 'runtime')
41
- return null;
42
- return parsed;
43
- }
44
- function isRecord(value) {
45
- return typeof value === 'object' && value !== null && !Array.isArray(value);
46
- }
47
- /** The stamp envelope carried by one session entry, or null when the entry is
48
- * not a well-formed crtr-stamp. */
49
- export function readStampEntry(entry) {
50
- if (!isRecord(entry))
51
- return null;
52
- const candidate = entry;
53
- if (candidate.type !== 'custom' || candidate.customType !== CRTR_STAMP_CUSTOM_TYPE)
54
- return null;
55
- const data = candidate.data;
56
- if (!isRecord(data))
57
- return null;
58
- if (typeof data['v'] !== 'number' || typeof data['kind'] !== 'string')
59
- return null;
60
- if (!isRecord(data['payload']))
61
- return null;
62
- const ext = data['ext'];
63
- if (ext !== undefined && !isRecord(ext))
64
- return null;
65
- return data;
66
- }
67
- /** Every well-formed stamp in `entries`, in file order. */
68
- export function readStamps(entries) {
69
- const stamps = [];
70
- for (const entry of entries) {
71
- const stamp = readStampEntry(entry);
72
- if (stamp !== null)
73
- stamps.push(stamp);
74
- }
75
- return stamps;
76
- }
77
- function isSessionStamp(stamp) {
78
- if (stamp.kind !== 'session' || stamp.v !== STAMP_SCHEMA_VERSION)
79
- return false;
80
- const payload = stamp.payload;
81
- return typeof payload.node === 'string' && typeof payload.crtr === 'string';
82
- }
83
- function isMessageStamp(stamp) {
84
- if (stamp.kind !== 'message' || stamp.v !== STAMP_SCHEMA_VERSION)
85
- return false;
86
- return typeof stamp.payload.msgTs === 'number';
87
- }
88
- /** The session stamp in force at the end of `entries` — a fork carries the
89
- * source node's stamp until the new node appends its own, so "latest" is what
90
- * a reader wants. */
91
- export function latestSessionStamp(entries) {
92
- let latest = null;
93
- for (const stamp of readStamps(entries)) {
94
- if (isSessionStamp(stamp))
95
- latest = stamp;
96
- }
97
- return latest;
98
- }
99
- /** The stamp for the message persisted at `msgTs`. Late or revised metadata is
100
- * a second stamp with the same key, so the last one wins. */
101
- export function messageStampFor(entries, msgTs) {
102
- let match = null;
103
- for (const stamp of readStamps(entries)) {
104
- if (isMessageStamp(stamp) && stamp.payload.msgTs === msgTs)
105
- match = stamp;
106
- }
107
- return match;
108
- }
109
- /** Who authored the message persisted at `msgTs`, or null when nothing
110
- * announced an origin for it. */
111
- export function originFor(entries, msgTs) {
112
- return messageStampFor(entries, msgTs)?.payload.origin ?? null;
113
- }
@@ -1,17 +0,0 @@
1
- import { scopeAllowed } from '../shared/env.js';
2
- export { scopeAllowed };
3
- /** The scopes an unrestricted local terminal run presents to the document substrate. */
4
- export declare const SCOPE_VOCABULARY: readonly ["crtr:llm", "crtr:act", "crtr:schedule", "crtr:memory:rw:user", "crtr:memory:manage:user", "crtr:files:rw:user:."];
5
- /** Reject unknown scope strings before they can silently change an allow-list. */
6
- export declare function validateScopes(value: unknown): string[];
7
- export declare function assertScope(meta: {
8
- node_id: string;
9
- scopes?: readonly string[] | null;
10
- }, scope: string): void;
11
- /** `holder` names who lacks the scope in prose — `node <id>` for a node's own
12
- * allow-list, `the bearer token` for a scoped API token's ceiling. */
13
- export declare class ScopeDeniedError extends Error {
14
- readonly holder: string;
15
- readonly scope: string;
16
- constructor(holder: string, scope: string);
17
- }
@@ -1,42 +0,0 @@
1
- import { parseScope } from '@north-light/crouter-identity';
2
- import { scopeAllowed } from '../shared/env.js';
3
- import { InputError } from './io.js';
4
- export { scopeAllowed };
5
- /** The scopes an unrestricted local terminal run presents to the document substrate. */
6
- export const SCOPE_VOCABULARY = [
7
- 'crtr:llm', 'crtr:act', 'crtr:schedule', 'crtr:memory:rw:user',
8
- 'crtr:memory:manage:user', 'crtr:files:rw:user:.',
9
- ];
10
- /** Reject unknown scope strings before they can silently change an allow-list. */
11
- export function validateScopes(value) {
12
- if (!Array.isArray(value))
13
- throw new InputError({ error: 'unknown_scope', message: 'scopes must be a list', received: value, field: 'scopes', next: 'Pass identity scope strings.' });
14
- for (const scope of value) {
15
- try {
16
- if (typeof scope !== 'string')
17
- throw new TypeError('scope is not a string');
18
- parseScope(scope);
19
- }
20
- catch {
21
- throw new InputError({ error: 'unknown_scope', message: `unknown scope: ${String(scope)}`, received: scope, field: 'scopes', next: 'Pass identity scope strings.' });
22
- }
23
- }
24
- return [...value];
25
- }
26
- export function assertScope(meta, scope) {
27
- if (scopeAllowed(meta.scopes, scope))
28
- return;
29
- throw new ScopeDeniedError(`node ${meta.node_id}`, scope);
30
- }
31
- /** `holder` names who lacks the scope in prose — `node <id>` for a node's own
32
- * allow-list, `the bearer token` for a scoped API token's ceiling. */
33
- export class ScopeDeniedError extends Error {
34
- holder;
35
- scope;
36
- constructor(holder, scope) {
37
- super(`${holder} does not hold the ${scope} scope`);
38
- this.holder = holder;
39
- this.scope = scope;
40
- this.name = 'ScopeDeniedError';
41
- }
42
- }
@@ -1,48 +0,0 @@
1
- import type { Mode, Lifecycle } from '../canvas/types.js';
2
- /** The gate input: a node's configuration as a structured object. Field names
3
- * and shape are the predicate vocabulary's dotted paths (`orchestration.depth`,
4
- * `hasManager`, …). There is intentionally NO `tags` field — NodeMeta has none. */
5
- export interface NodeConfigSubject {
6
- /** The node's role, free-form (developer | explore | design | …). NodeMeta.kind. */
7
- kind: string;
8
- /** base (hands-on worker) | orchestrator (delegating manager). NodeMeta.mode. */
9
- mode: Mode;
10
- /** terminal (worker) | resident (manager). NodeMeta.lifecycle. */
11
- lifecycle: Lifecycle;
12
- /** Spine position: does this node have a manager? (= parent !== null). */
13
- hasManager: boolean;
14
- /** The node's working directory on disk. NodeMeta.cwd. */
15
- cwd: string;
16
- /** DERIVED: the scope scope.ts resolves for `cwd` — `project` when a
17
- * nearest-ancestor `.crouter/` exists at/above cwd, else `user`. */
18
- scope: 'user' | 'project';
19
- /** DERIVED orchestration metrics. */
20
- orchestration: {
21
- /** Spine distance (hops) from this node up to its root orchestrator. The
22
- * root itself is 0; a direct child of the root is 1; etc. */
23
- depth: number;
24
- };
25
- /** DERIVED: `meta.profile_id` resolved to its manifest's mutable `name` (NOT
26
- * the stable directory id — gate authors write `gate: {profile: <name>}`).
27
- * null for historical null-profile rows, and null (never throwing) when the
28
- * profile has been deleted or its manifest is unreadable — this is a HOT,
29
- * every-doc-render path and must never brick boot/on-read gating. */
30
- profile: string | null;
31
- /** Effective per-run scopes. */
32
- scopes: readonly string[];
33
- /** DERIVED: handles of the repos enclosing `cwd`, nearest first. A rule's
34
- * `if: {repo: crouter}` holds when any element is `crouter`. */
35
- repo: readonly string[];
36
- }
37
- export declare function effectiveScopes(scopes: readonly string[] | null | undefined): string[];
38
- /** The scope a cwd resolves into: `project` when scope.ts finds a
39
- * nearest-ancestor `.crouter/` at/above it, else `user`. Reuses the existing
40
- * resolver (`projectScopeRoot`). */
41
- export declare function scopeForCwd(cwd: string): 'user' | 'project';
42
- /** Resolve `profile_id` to the manifest's mutable `name` for the gate subject.
43
- * NEVER throws: a missing/deleted profile resolves to `null` silently (the
44
- * ordinary lifecycle of a node whose profile was deleted out from under it);
45
- * any OTHER failure (e.g. an unreadable/corrupt manifest slipping past
46
- * `loadProfileManifest`'s own tolerance) is logged and still resolves to
47
- * `null` rather than throwing — this runs on every gated-doc render. */
48
- export declare function profileNameFor(profileId: string | null | undefined): string | null;
@@ -1,39 +0,0 @@
1
- // subject-fields.ts — the PURE half of subject assembly: the NodeConfigSubject
2
- // shape plus the two derived-field resolvers (`scopeForCwd`, `profileNameFor`)
3
- // that read scope/profile config but NEVER the canvas-db. Split out of
4
- // subject.ts so CLI consumers (`sys context prompt-review`) can build a subject from an
5
- // API-fetched node without pulling subject.ts's canvas-db reach (getNode/getRow).
6
- // The canvas-db assembler (`assembleNodeSubject`, `spineDepth`) stays in
7
- // subject.ts for broker/daemon-side callers.
8
- import { projectScopeRoot } from '../scope.js';
9
- import { loadProfileManifest } from '../profiles/manifest.js';
10
- import { CrtrError } from '../errors.js';
11
- import { SCOPE_VOCABULARY } from '../scopes.js';
12
- export function effectiveScopes(scopes) {
13
- return scopes === null || scopes === undefined ? [...SCOPE_VOCABULARY] : [...scopes];
14
- }
15
- /** The scope a cwd resolves into: `project` when scope.ts finds a
16
- * nearest-ancestor `.crouter/` at/above it, else `user`. Reuses the existing
17
- * resolver (`projectScopeRoot`). */
18
- export function scopeForCwd(cwd) {
19
- return projectScopeRoot(cwd) !== null ? 'project' : 'user';
20
- }
21
- /** Resolve `profile_id` to the manifest's mutable `name` for the gate subject.
22
- * NEVER throws: a missing/deleted profile resolves to `null` silently (the
23
- * ordinary lifecycle of a node whose profile was deleted out from under it);
24
- * any OTHER failure (e.g. an unreadable/corrupt manifest slipping past
25
- * `loadProfileManifest`'s own tolerance) is logged and still resolves to
26
- * `null` rather than throwing — this runs on every gated-doc render. */
27
- export function profileNameFor(profileId) {
28
- if (profileId === undefined || profileId === null || profileId === '')
29
- return null;
30
- try {
31
- return loadProfileManifest(profileId).manifest.name;
32
- }
33
- catch (err) {
34
- if (err instanceof CrtrError && err.code === 'not_found')
35
- return null;
36
- console.warn(`[subject] profile "${profileId}" failed to resolve for node subject assembly: ${err.message}`);
37
- return null;
38
- }
39
- }
@@ -1,26 +0,0 @@
1
- /** Broker-generated activity recap for one completed tool-call group. Changed
2
- * files and spawned/finished nodes are counted deterministically by the viewer's
3
- * GroupActivityRecorder; deletions have no tool of their own, so only the model
4
- * can evidence them. */
5
- export interface ToolGroupSummary {
6
- bullets: string[];
7
- filesDeleted: number;
8
- }
9
- export interface ToolGroupMessageLike {
10
- role?: string;
11
- content?: unknown;
12
- customType?: string;
13
- }
14
- export declare function isGroupBoundaryKind(kind: string): boolean;
15
- /** Validate the one durable summary shape and cap its display-sized fields.
16
- * Invalid entries are absent rather than adapted, so their calls stay visible. */
17
- export declare function normalizeToolGroupSummary(value: unknown): ToolGroupSummary | null;
18
- /** Validate sidecar/snapshot entries independently so one malformed or legacy
19
- * value never collapses any otherwise valid group. */
20
- export declare function normalizeToolGroupSummaryRecord(value: unknown): Record<string, ToolGroupSummary>;
21
- /** Text a renderer presents as an assistant bubble. Thinking and tool-call
22
- * blocks deliberately do not count: only visible prose establishes a boundary. */
23
- export declare function assistantVisibleText(message: ToolGroupMessageLike): string;
24
- /** A person-authored user message closes the preceding group. Runtime-generated
25
- * user context retains role=user for delivery semantics but is not human prose. */
26
- export declare function isTrueUserMessage(message: ToolGroupMessageLike): boolean;