@north-light/api 0.3.446 → 0.3.447
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/__tests__/serial/client.test.d.ts +1 -0
- package/dist/api/__tests__/serial/client.test.js +141 -0
- package/dist/api/dto/memory.d.ts +171 -0
- package/dist/api/dto/memory.js +7 -0
- package/dist/core/canvas/types.d.ts +446 -0
- package/dist/core/canvas/types.js +6 -0
- package/dist/core/fault-classifier.d.ts +34 -0
- package/dist/core/fault-classifier.js +96 -0
- package/dist/core/provider-error-recognition.d.ts +27 -0
- package/dist/core/provider-error-recognition.js +74 -0
- package/dist/core/provider-failure.d.ts +27 -0
- package/dist/core/provider-failure.js +109 -0
- package/dist/core/runtime/broker-protocol.d.ts +721 -0
- package/dist/core/runtime/broker-protocol.js +152 -0
- package/dist/core/runtime/managed-provider-cooling.d.ts +24 -0
- package/dist/core/runtime/managed-provider-cooling.js +62 -0
- package/dist/core/runtime/stamp/protocol.d.ts +90 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/scopes.d.ts +17 -0
- package/dist/core/scopes.js +42 -0
- package/dist/core/substrate/subject-fields.d.ts +48 -0
- package/dist/core/substrate/subject-fields.js +39 -0
- package/dist/shared/tool-groups.d.ts +26 -0
- package/dist/shared/tool-groups.js +64 -0
- package/package.json +1 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/** What a node is doing right now. `active` means the engine process — a detached
|
|
2
|
+
* headless broker — is live, NOT that it holds a window or that anyone is
|
|
3
|
+
* watching it (attachment is a separate axis: the `focuses` table tracks each
|
|
4
|
+
* node's one viewer pane, and the broker records its helloed-viewer count).
|
|
5
|
+
* Crucially, `active` does NOT mean the node is GENERATING right now: a live
|
|
6
|
+
* engine sits dormant between turns (waiting on a child, a human, or just left
|
|
7
|
+
* open), so `active` ≠ busy — it only means the process was never closed. To tell
|
|
8
|
+
* whether an active node is actually working, check its pi session-file mtime or
|
|
9
|
+
* CPU, not its status. UI shows active+idle; `done` is hidden but revivable;
|
|
10
|
+
* `canceled` is a user-closed node (also hidden, also revivable — not a fault);
|
|
11
|
+
* only `dead` is a fault. */
|
|
12
|
+
export type NodeStatus = 'active' | 'idle' | 'done' | 'dead' | 'canceled';
|
|
13
|
+
/** Does stopping finalize the node? terminal = worker (finalizes on push --final);
|
|
14
|
+
* resident = an interactable conversation that parks and remains wakeable
|
|
15
|
+
* without being forced to submit a final. */
|
|
16
|
+
export type Lifecycle = 'terminal' | 'resident';
|
|
17
|
+
/** base = hands-on worker; orchestrator = delegating manager. Bespoke per kind. */
|
|
18
|
+
export type Mode = 'base' | 'orchestrator';
|
|
19
|
+
/** Why a node last stopped — drives the daemon's reap-vs-revive decision. */
|
|
20
|
+
export type ExitIntent = 'done' | 'refresh' | 'idle-release' | 'parked' | null;
|
|
21
|
+
export type TerminalReason = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded' | 'boot_failed' | 'crash_looped' | 'launch_failed' | 'context_overflow' | 'deadline_exceeded' | 'provider_fatal' | 'declined';
|
|
22
|
+
export type NodeOutcomeKind = 'result' | 'failure';
|
|
23
|
+
/** Maximum serialized opaque action payload; enforced at registration in Phase 5. */
|
|
24
|
+
export declare const OUTCOME_PAYLOAD_MAX_BYTES: number;
|
|
25
|
+
export interface NodeOutcomeDetailV1 {
|
|
26
|
+
schema: 'crtr.node-outcome-detail/v1';
|
|
27
|
+
message?: string;
|
|
28
|
+
fault_kind?: import('../fault-classifier.js').FaultKind;
|
|
29
|
+
error_class?: import('../events/types.js').ErrorClassCode;
|
|
30
|
+
retry?: {
|
|
31
|
+
attempt: number;
|
|
32
|
+
max: number;
|
|
33
|
+
};
|
|
34
|
+
respawn_failures?: number;
|
|
35
|
+
deadline?: {
|
|
36
|
+
deadline_at: string;
|
|
37
|
+
elapsed_ms: number;
|
|
38
|
+
};
|
|
39
|
+
/** Present only for reason `declined`: what the node gave `crtr push result --decline`. */
|
|
40
|
+
declined?: DeclinedResult;
|
|
41
|
+
truncated?: true;
|
|
42
|
+
}
|
|
43
|
+
/** A declined structured result. `code` is opaque here — the requester validates it. */
|
|
44
|
+
export interface DeclinedResult {
|
|
45
|
+
reason: string;
|
|
46
|
+
code: string;
|
|
47
|
+
retryable: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** The two structural edges. `subscribes_to` is the load-bearing spine (flow,
|
|
50
|
+
* org chart, views, completion routing). `spawned_by` is audit only. */
|
|
51
|
+
/** The pi launch recipe, persisted so the daemon can faithfully revive a node
|
|
52
|
+
* as its *current* self. Rewritten on every polymorph (base→orchestrator). */
|
|
53
|
+
export interface LaunchSpec {
|
|
54
|
+
/** Model id/pattern passed to pi `--model`. */
|
|
55
|
+
model?: string;
|
|
56
|
+
/** Original logical family/strength preference, retained after normalization. */
|
|
57
|
+
modelIntent?: import('../../types.js').ModelIntent;
|
|
58
|
+
/** True for an exact-consult launch: `model` is the verbatim captured
|
|
59
|
+
* `provider/id:thinking` spec, bypassing the review floor/normalize and
|
|
60
|
+
* every launch-time/turn-time provider fallback — an unavailable exact
|
|
61
|
+
* model must fail rather than substitute. See `buildLaunchSpec`. */
|
|
62
|
+
modelExact?: boolean;
|
|
63
|
+
/** pi `--tools` allow-list. */
|
|
64
|
+
tools?: string[];
|
|
65
|
+
/** pi `-e` extension paths, loaded once; they self-gate on live {kind,mode}. */
|
|
66
|
+
extensions: string[];
|
|
67
|
+
/** Resolved system prompt text (passed via --append-system-prompt / --system-prompt). */
|
|
68
|
+
systemPrompt?: string;
|
|
69
|
+
/** Extra env injected into the pi process. */
|
|
70
|
+
env: Record<string, string>;
|
|
71
|
+
}
|
|
72
|
+
/** The repository fingerprint a refusal was decided against. An examination
|
|
73
|
+
* that reads the same values can skip the whole proof cascade: nothing about
|
|
74
|
+
* the refusal can have changed. */
|
|
75
|
+
export interface ManagedWorktreeObservation {
|
|
76
|
+
/** Tip of `refs/heads/<base_ref>`, or null when it no longer resolves. */
|
|
77
|
+
base: string | null;
|
|
78
|
+
/** Tip of the base branch's upstream (or `origin/<base_ref>` fallback). */
|
|
79
|
+
upstream: string | null;
|
|
80
|
+
/** Tip of `refs/heads/<branch>`, or null when the branch is gone. */
|
|
81
|
+
branch: string | null;
|
|
82
|
+
/** `absent`, or a hash of the checkout's HEAD plus `git status --porcelain`. */
|
|
83
|
+
checkout: string | null;
|
|
84
|
+
}
|
|
85
|
+
/** Reconciliation bookkeeping owned SOLELY by the daemon's managed-worktree
|
|
86
|
+
* sweep. Present only while a record is not yet reconciled; cleared when
|
|
87
|
+
* cleanup completes. */
|
|
88
|
+
export interface ManagedWorktreeSweep {
|
|
89
|
+
/** Structured code for why the last pass did not complete cleanup. */
|
|
90
|
+
reason: string;
|
|
91
|
+
detail?: string;
|
|
92
|
+
observed: ManagedWorktreeObservation;
|
|
93
|
+
/** Consecutive passes that examined this record without completing it. */
|
|
94
|
+
attempts: number;
|
|
95
|
+
last_attempt: string;
|
|
96
|
+
/** The sweep does not re-examine this record before this instant. */
|
|
97
|
+
next_attempt: string;
|
|
98
|
+
/** Set once `attempts` reaches the terminal backoff tier: this record needs a
|
|
99
|
+
* human, and `crtr sys worktrees` lists it with its reason. It is still
|
|
100
|
+
* fingerprint-checked at the capped interval, so work that lands later is
|
|
101
|
+
* still cleaned up. */
|
|
102
|
+
quarantined?: true;
|
|
103
|
+
}
|
|
104
|
+
export interface ManagedWorktree {
|
|
105
|
+
state: 'open' | 'closed' | 'abandoned';
|
|
106
|
+
/** A closed worktree remains registered until its owner has exited, so Git
|
|
107
|
+
* never deletes the caller's live checkout beneath it. The daemon sweep is
|
|
108
|
+
* the one authority that turns `pending` into `complete`. */
|
|
109
|
+
cleanup?: 'pending' | 'complete';
|
|
110
|
+
path: string;
|
|
111
|
+
branch: string;
|
|
112
|
+
repo_root: string;
|
|
113
|
+
base_ref: string;
|
|
114
|
+
base_sha: string;
|
|
115
|
+
created: string;
|
|
116
|
+
closed?: string;
|
|
117
|
+
abandoned?: {
|
|
118
|
+
branch: string;
|
|
119
|
+
tip_sha: string;
|
|
120
|
+
at: string;
|
|
121
|
+
by: string;
|
|
122
|
+
};
|
|
123
|
+
/** Recorded by `close` when the rebase succeeded but fast-forwarding the base
|
|
124
|
+
* was blocked. The sweep lands a dead node's commits ONLY for a record
|
|
125
|
+
* carrying this: the daemon finishes a job someone started, it never invents
|
|
126
|
+
* the intent to land. */
|
|
127
|
+
land_intent?: {
|
|
128
|
+
at: string;
|
|
129
|
+
blocked: string;
|
|
130
|
+
};
|
|
131
|
+
sweep?: ManagedWorktreeSweep;
|
|
132
|
+
/** Recorded ONLY by an explicit branch-moved `close` when the checkout's
|
|
133
|
+
* actual branch has drifted from `branch` (its recorded `crtr/*` ref is
|
|
134
|
+
* gone — ordinary PR work moved it there) but that branch's content was
|
|
135
|
+
* proven, immediately before this was written, already contained in the
|
|
136
|
+
* recorded base or its upstream. Authorizes ONLY the daemon sweep's later
|
|
137
|
+
* checkout removal for this EXACT path + git common dir + observed branch
|
|
138
|
+
* + observed SHA — the sweep re-verifies all four before acting and NEVER
|
|
139
|
+
* deletes `observed_branch`, since it is not a `crtr/*` branch crouter
|
|
140
|
+
* created. A mismatch on re-verification invalidates the authorization; a
|
|
141
|
+
* fresh explicit close must re-establish it. Absent for every ordinary
|
|
142
|
+
* managed worktree, and for `abandon`'s branch-moved path (which disposes
|
|
143
|
+
* immediately instead of deferring, so it needs no persisted grant). */
|
|
144
|
+
disposal?: {
|
|
145
|
+
observed_branch: string;
|
|
146
|
+
observed_sha: string;
|
|
147
|
+
common_dir: string;
|
|
148
|
+
authorized: string;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** Immutable companion provenance. The origin session/leaf/model coordinates
|
|
152
|
+
* remain canonical in the review row rather than being duplicated into node
|
|
153
|
+
* metadata. `kind` names the surface the companion serves: absent or `review`
|
|
154
|
+
* is a human document review; `page_feedback` binds a page ticket's feedback
|
|
155
|
+
* conversation, with review_id = ticket id and target_file = the page source. */
|
|
156
|
+
export interface ReviewBinding {
|
|
157
|
+
kind?: 'review' | 'page_feedback';
|
|
158
|
+
review_id: string;
|
|
159
|
+
origin_node_id: string;
|
|
160
|
+
branch_file: string;
|
|
161
|
+
target_file: string;
|
|
162
|
+
}
|
|
163
|
+
/** A node's durable configuration, stored in SQL node columns. Sessions and
|
|
164
|
+
* managed worktrees have their own SQL stores. */
|
|
165
|
+
export interface NodeIdentity {
|
|
166
|
+
node_id: string;
|
|
167
|
+
name: string;
|
|
168
|
+
/** A 2-4 word kebab-case handle derived from the node's first prompt (named
|
|
169
|
+
* headlessly by pi; see runtime/naming.ts). Shown in the editor label. */
|
|
170
|
+
description?: string;
|
|
171
|
+
/** The same first prompt named as PROSE — a sentence-case phrase with its
|
|
172
|
+
* punctuation intact, produced by the same headless namer in the same call as
|
|
173
|
+
* `description`. Kebab case is right for a tmux window and an editor label
|
|
174
|
+
* and wrong for a conversation list, so a surface reading to a person takes
|
|
175
|
+
* this. Absent on a node named before titles existed. */
|
|
176
|
+
title?: string;
|
|
177
|
+
/** A single Nerd Font glyph depicting the node's work, chosen by the same
|
|
178
|
+
* headless namer that produced `description` (see runtime/naming.ts). Kept as
|
|
179
|
+
* its own field rather than baked into the description so each surface decides
|
|
180
|
+
* whether to render it; absent on a node named before icons existed, or when
|
|
181
|
+
* the model returned nothing usable. */
|
|
182
|
+
icon?: string;
|
|
183
|
+
/** How many times this node has been (re)launched — born at 0, bumped on
|
|
184
|
+
* every revive. Exposed to the pi footer as CRTR_CYCLES so a refresh/crash
|
|
185
|
+
* cycle reads at a glance there. */
|
|
186
|
+
cycles?: number;
|
|
187
|
+
created: string;
|
|
188
|
+
/** The dir this node is pinned to — its cwd (where pi runs, bash executes). */
|
|
189
|
+
cwd: string;
|
|
190
|
+
/** Which HOST launches + supervises this node: 'tmux' (a pane) or 'broker' (the
|
|
191
|
+
* headless in-process engine). NULL ⇒ 'tmux' (every pre-broker node). */
|
|
192
|
+
host_kind?: 'tmux' | 'broker' | null;
|
|
193
|
+
/** The profile this node runs under, a stable profile-directory id
|
|
194
|
+
* (`<slug>-<id>`, see `core/profiles/manifest.ts`). It is chosen once at spawn
|
|
195
|
+
* (inherited from the spawner, or `--profile`) and is the runtime→resolver seam
|
|
196
|
+
* (`CRTR_PROFILE_ID`) the memory/gate substrate reads. NULL only for historical
|
|
197
|
+
* root/no-profile rows; new root conversations use the stable root profile id. */
|
|
198
|
+
profile_id?: string | null;
|
|
199
|
+
/** Role the node was born as: explore | developer | plan | review | general… */
|
|
200
|
+
kind: string;
|
|
201
|
+
mode: Mode;
|
|
202
|
+
lifecycle: Lifecycle;
|
|
203
|
+
/** The last {kind,mode,lifecycle} whose context delivery was acknowledged.
|
|
204
|
+
* Meta-only (not a db column). Born equal to the initial persona; later
|
|
205
|
+
* changes advance only after a baseline or provider-gated transition is
|
|
206
|
+
* durably registered. Historical rows may omit `kind`; reads resolve it to
|
|
207
|
+
* the current kind so they do not fabricate a transition. */
|
|
208
|
+
persona_ack?: {
|
|
209
|
+
kind?: string;
|
|
210
|
+
mode: Mode;
|
|
211
|
+
lifecycle: Lifecycle;
|
|
212
|
+
};
|
|
213
|
+
/** Spine parent — my manager (who subscribes to me); drives canvas nesting +
|
|
214
|
+
* orphaning. null for a root (top-level, no manager). */
|
|
215
|
+
parent?: string | null;
|
|
216
|
+
/** Provenance — who spawned me (the `spawned_by` edge). Decoupled from
|
|
217
|
+
* `parent` so an INDEPENDENT root (parent=null) still records its lineage.
|
|
218
|
+
* Audit only; null for a user-opened root. Defaults to `parent` for a child. */
|
|
219
|
+
spawned_by?: string | null;
|
|
220
|
+
/** Actor that created this node; null when an external process did. Unlike
|
|
221
|
+
* `parent` (manager) and `spawned_by` (provenance edge), this determines
|
|
222
|
+
* whether birth is announced to the manager. */
|
|
223
|
+
creator?: string | null;
|
|
224
|
+
/** Raw `--fork-from` operand retained for identity framing and audit. */
|
|
225
|
+
fork_from?: string | null;
|
|
226
|
+
/** Resolved absolute source session file used for every pre-bind fork retry. */
|
|
227
|
+
fork_source_file?: string | null;
|
|
228
|
+
/** Immutable review-companion binding. Undefined for ordinary nodes. */
|
|
229
|
+
review_binding?: ReviewBinding | null;
|
|
230
|
+
/** New subscriptions this node opens default to passive when true. */
|
|
231
|
+
passive_default?: boolean;
|
|
232
|
+
/** The pi session id for `--session <id>` revival. */
|
|
233
|
+
pi_session_id?: string | null;
|
|
234
|
+
/** Absolute path to pi's session `.jsonl` file, captured at session_start via
|
|
235
|
+
* ctx.sessionManager.getSessionFile(). Preferred over pi_session_id when
|
|
236
|
+
* resuming: pi resolves a BARE `--session <id>` relative to the launch cwd
|
|
237
|
+
* first (and shows an interactive cross-project "Fork? [y/N]" prompt when the
|
|
238
|
+
* revive cwd differs from the session's creation cwd), whereas an absolute
|
|
239
|
+
* PATH is opened directly — immune to any cwd discrepancy. Null for older
|
|
240
|
+
* nodes booted before this field existed → revive falls back to the bare id. */
|
|
241
|
+
pi_session_file?: string | null;
|
|
242
|
+
/** True while a cycling (`--crtr-new-cycle`) launch remains unconfirmed. Set
|
|
243
|
+
* monotonically by reviveNode before launch and cleared only by the stophook's
|
|
244
|
+
* confirmed `session_start`. While true, reviveNode's locked arbitration
|
|
245
|
+
* vetoes every caller's strict-resume preference so a failed cycle cannot
|
|
246
|
+
* reopen the old context. */
|
|
247
|
+
cycle_pending?: boolean | null;
|
|
248
|
+
/** A caller-pinned model TIER (ultra/strong/medium/light) that overrides the
|
|
249
|
+
* persona's declared default. Durable so it survives a polymorph: every
|
|
250
|
+
* spec-rebuild path (promote/demote/lifecycle/reset) re-passes it to
|
|
251
|
+
* buildLaunchSpec, while the persona default is recomputed fresh for the
|
|
252
|
+
* (possibly new) kind. Null/undefined ⇒ use the persona default. */
|
|
253
|
+
model_override?: string | null;
|
|
254
|
+
/** Per-run allow-list; null inherits the full runtime vocabulary. */
|
|
255
|
+
scopes?: string[] | null;
|
|
256
|
+
grantee?: string;
|
|
257
|
+
sub?: string | null;
|
|
258
|
+
grant_id?: string | null;
|
|
259
|
+
client_id?: string | null;
|
|
260
|
+
metadata_json?: string | null;
|
|
261
|
+
run_id?: string | null;
|
|
262
|
+
event_cursor?: number | null;
|
|
263
|
+
last_activity_at?: string | null;
|
|
264
|
+
usage_json?: string | null;
|
|
265
|
+
deleting_at?: string | null;
|
|
266
|
+
/** Full pi launch recipe; rewritten on every polymorph. */
|
|
267
|
+
launch?: LaunchSpec;
|
|
268
|
+
/** Managed git worktree owned by this node (if any). */
|
|
269
|
+
managed_worktree?: ManagedWorktree | null;
|
|
270
|
+
}
|
|
271
|
+
/** A node's live runtime state, stored in the WAL-backed `nodes` row. Each
|
|
272
|
+
* field is mutated by an atomic single-statement `UPDATE` (setStatus / setIntent /
|
|
273
|
+
* setPresence / recordPid+clearPid). The daemon reconciles process and presence
|
|
274
|
+
* state from tmux and broker reality. */
|
|
275
|
+
export interface NodeRuntime {
|
|
276
|
+
/** What the node is doing right now. */
|
|
277
|
+
status: NodeStatus;
|
|
278
|
+
/** Why the node last stopped (done | refresh | idle-release). Drives reap-vs-revive.
|
|
279
|
+
* Optional on the hydrated view (a fresh construction omits it); the row
|
|
280
|
+
* column is always present, defaulting null. */
|
|
281
|
+
intent?: ExitIntent;
|
|
282
|
+
/** Controller whose next inbox delivery permits this terminal node to sleep.
|
|
283
|
+
* Mutable durable runtime state: ancestry never implies a wait. */
|
|
284
|
+
waiting_for?: string | null;
|
|
285
|
+
frozen_at?: string | null;
|
|
286
|
+
respawn_failures?: number;
|
|
287
|
+
respawn_not_before?: string | null;
|
|
288
|
+
launched_at?: string | null;
|
|
289
|
+
last_settled_at?: string | null;
|
|
290
|
+
terminal_reason?: TerminalReason | null;
|
|
291
|
+
/** Immutable basename of the committed canonical final report, or null. */
|
|
292
|
+
final_report?: string | null;
|
|
293
|
+
/** ISO timestamp carried by the committed canonical final report, or null. */
|
|
294
|
+
finalized_at?: string | null;
|
|
295
|
+
/** Durable terminal outcome. `outcome_at` is its write-once latch. */
|
|
296
|
+
outcome_kind?: NodeOutcomeKind | null;
|
|
297
|
+
outcome_reason?: TerminalReason | null;
|
|
298
|
+
outcome_at?: string | null;
|
|
299
|
+
outcome_revision?: number;
|
|
300
|
+
outcome_detail_json?: string | null;
|
|
301
|
+
/** Absolute wall-clock spawn deadline, or null when unbounded. */
|
|
302
|
+
deadline_at?: string | null;
|
|
303
|
+
/** Cumulative input tokens reported by the broker telemetry producer. */
|
|
304
|
+
telemetry_tokens_in?: number | null;
|
|
305
|
+
/** Latest live context-window token gauge reported by the broker. */
|
|
306
|
+
telemetry_context_tokens?: number | null;
|
|
307
|
+
/** Latest tool summary reported by the broker. */
|
|
308
|
+
telemetry_last_activity?: string | null;
|
|
309
|
+
/** Timestamp of the latest broker telemetry update. */
|
|
310
|
+
telemetry_updated_at?: string | null;
|
|
311
|
+
/** OS pid of the live broker process, recorded on boot (stophook session_start).
|
|
312
|
+
* The daemon's authoritative liveness signal — every node is a detached broker,
|
|
313
|
+
* so `isPidAlive(pi_pid)` is the sole liveness check. Cleared to null by
|
|
314
|
+
* reviveNode until the fresh broker re-records it, so the relaunch gap reads as
|
|
315
|
+
* a dead pid (guarded by the daemon's revive grace, not a double-spawn). */
|
|
316
|
+
pi_pid?: number | null;
|
|
317
|
+
/** Stable process-identity fingerprint (`pid.ts`'s `composeIdentity`) captured
|
|
318
|
+
* at the SAME moment `pi_pid` is recorded (`recordPid`) — the LAUNCH-time
|
|
319
|
+
* baseline `headlessBrokerHost.teardown()` compares `pi_pid`'s CURRENT
|
|
320
|
+
* identity against before signaling anything:
|
|
321
|
+
* a mismatch proves the OS has already recycled `pi_pid` for an unrelated
|
|
322
|
+
* process since this node launched it, and teardown must refuse to signal
|
|
323
|
+
* that stranger. `null` for a node booted before this field existed, or if
|
|
324
|
+
* the launch-time identity capture itself failed (fail-open — no baseline
|
|
325
|
+
* means no guard, not a false mismatch). Cleared to null alongside `pi_pid`
|
|
326
|
+
* by `clearPid` and the boot-liveness sweep (a new boot invalidates both). */
|
|
327
|
+
pi_pid_identity?: string | null;
|
|
328
|
+
/** Presence: the tmux session (its root's home) and window this node renders
|
|
329
|
+
* in while active. Cleared when the node goes done/dead and its window closes.
|
|
330
|
+
* The row IS the presence registry (one atomic setPresence per move).
|
|
331
|
+
* v3: a DERIVED CACHE of `pane`'s current location — reconciled from the pane,
|
|
332
|
+
* never trusted when a user move could have desynced them. */
|
|
333
|
+
tmux_session?: string | null;
|
|
334
|
+
window?: string | null;
|
|
335
|
+
/** LOCATION's authoritative handle — the durable tmux `%pane_id` this node's
|
|
336
|
+
* pane is anchored on. tmux preserves it across `move-pane`/`join-pane`/
|
|
337
|
+
* `break-pane` and window renumbering, so `window`/`tmux_session` above are a
|
|
338
|
+
* cache reconciled from it and pane-existence is the liveness probe. A
|
|
339
|
+
* not-focused + not-generating node has `pane = null` (no pane). Authoritative
|
|
340
|
+
* in the row exactly like `window`/`tmux_session` — a RUNTIME field, NOT meta
|
|
341
|
+
* identity — written by the one atomic `setPresence` UPDATE. */
|
|
342
|
+
pane?: string | null;
|
|
343
|
+
/** Active fault projected from SQL recovery state; stale daemon provider faults are suppressed. */
|
|
344
|
+
fault?: import('../fault-classifier.js').Fault | null;
|
|
345
|
+
/** True only while the live execution has an open turn and its PID is alive. */
|
|
346
|
+
streaming?: boolean;
|
|
347
|
+
}
|
|
348
|
+
/** The hydrated node view `getNode()` returns: durable configuration and live
|
|
349
|
+
* runtime from SQL, with session and worktree state from their named stores.
|
|
350
|
+
* It retains the historical `NodeMeta` name for existing callers. */
|
|
351
|
+
export type NodeMeta = NodeIdentity & NodeRuntime;
|
|
352
|
+
/** The queryable projection of a node's SQL configuration and runtime state. */
|
|
353
|
+
export interface NodeRow {
|
|
354
|
+
node_id: string;
|
|
355
|
+
name: string;
|
|
356
|
+
/** The pi-generated first-task description; see NodeIdentity.description.
|
|
357
|
+
* Absent for a node not yet named — the column's NULL is mapped to undefined
|
|
358
|
+
* at `rowFrom` so identity, row, and DTO all spell "unnamed" the same way. */
|
|
359
|
+
description?: string;
|
|
360
|
+
/** Relaunch count; see NodeIdentity.cycles. Absent on a row written before
|
|
361
|
+
* this column. */
|
|
362
|
+
cycles?: number;
|
|
363
|
+
kind: string;
|
|
364
|
+
mode: Mode;
|
|
365
|
+
lifecycle: Lifecycle;
|
|
366
|
+
status: NodeStatus;
|
|
367
|
+
cwd: string;
|
|
368
|
+
/** Which HOST launches + supervises this node; see NodeIdentity.host_kind. */
|
|
369
|
+
host_kind: 'tmux' | 'broker' | null;
|
|
370
|
+
/** The profile this node runs under; see NodeIdentity.profile_id. NULL only
|
|
371
|
+
* for historical root/no-profile rows. */
|
|
372
|
+
profile_id: string | null;
|
|
373
|
+
/** Per-run allow-list; null inherits the full runtime vocabulary. */
|
|
374
|
+
scopes: string[] | null;
|
|
375
|
+
grantee?: string;
|
|
376
|
+
sub?: string | null;
|
|
377
|
+
grant_id?: string | null;
|
|
378
|
+
client_id?: string | null;
|
|
379
|
+
metadata_json?: string | null;
|
|
380
|
+
run_id?: string | null;
|
|
381
|
+
event_cursor?: number | null;
|
|
382
|
+
last_activity_at?: string | null;
|
|
383
|
+
usage_json?: string | null;
|
|
384
|
+
deleting_at?: string | null;
|
|
385
|
+
parent: string | null;
|
|
386
|
+
created: string;
|
|
387
|
+
/** Authoritative runtime columns (see NodeRuntime). */
|
|
388
|
+
intent: ExitIntent;
|
|
389
|
+
waiting_for: string | null;
|
|
390
|
+
frozen_at?: string | null;
|
|
391
|
+
respawn_failures?: number;
|
|
392
|
+
respawn_not_before?: string | null;
|
|
393
|
+
launched_at?: string | null;
|
|
394
|
+
last_settled_at?: string | null;
|
|
395
|
+
terminal_reason?: TerminalReason | null;
|
|
396
|
+
final_report?: string | null;
|
|
397
|
+
finalized_at?: string | null;
|
|
398
|
+
outcome_kind?: NodeOutcomeKind | null;
|
|
399
|
+
outcome_reason?: TerminalReason | null;
|
|
400
|
+
outcome_at?: string | null;
|
|
401
|
+
outcome_revision?: number;
|
|
402
|
+
outcome_detail_json?: string | null;
|
|
403
|
+
deadline_at?: string | null;
|
|
404
|
+
/** Cumulative input tokens reported by the broker telemetry producer. */
|
|
405
|
+
telemetry_tokens_in?: number | null;
|
|
406
|
+
/** Latest live context-window token gauge reported by the broker. */
|
|
407
|
+
telemetry_context_tokens?: number | null;
|
|
408
|
+
/** Latest tool summary reported by the broker. */
|
|
409
|
+
telemetry_last_activity?: string | null;
|
|
410
|
+
/** Timestamp of the latest broker telemetry update. */
|
|
411
|
+
telemetry_updated_at?: string | null;
|
|
412
|
+
pi_pid: number | null;
|
|
413
|
+
/** See `NodeRuntime.pi_pid_identity`. */
|
|
414
|
+
pi_pid_identity: string | null;
|
|
415
|
+
window: string | null;
|
|
416
|
+
tmux_session: string | null;
|
|
417
|
+
/** The durable LOCATION handle (the tmux `%pane_id`); see NodeRuntime.pane. */
|
|
418
|
+
pane: string | null;
|
|
419
|
+
/** Active fault projected from SQL recovery state; stale daemon provider faults are suppressed. */
|
|
420
|
+
fault?: import('../fault-classifier.js').Fault | null;
|
|
421
|
+
/** True only while the live execution has an open turn and its PID is alive. */
|
|
422
|
+
streaming?: boolean;
|
|
423
|
+
}
|
|
424
|
+
/** A FOCUS row as stored in the `focuses` table (canvas.db, migration v6): one
|
|
425
|
+
* durable on-screen viewport the user looks at, bound to one node. Plural —
|
|
426
|
+
* many focuses live at once across windows and sessions (the plural
|
|
427
|
+
* generalization of the old single focus pointer). Anchored on the durable tmux
|
|
428
|
+
* `%pane_id`; `session` is a derived cache reconciled from the pane. `node_id`
|
|
429
|
+
* is UNIQUE — a node occupies at most one focus (Q5). */
|
|
430
|
+
export interface FocusRow {
|
|
431
|
+
/** Stable internal id for the viewport (the table's primary key). */
|
|
432
|
+
focus_id: string;
|
|
433
|
+
/** The durable `%pane_id` realizing the focus, or null before it is placed. */
|
|
434
|
+
pane: string | null;
|
|
435
|
+
/** Derived cache of the user session the pane lives in (reconciled from pane). */
|
|
436
|
+
session: string | null;
|
|
437
|
+
/** The node this focus shows. UNIQUE → a node occupies ≤1 focus. */
|
|
438
|
+
node_id: string;
|
|
439
|
+
}
|
|
440
|
+
/** A subscription as seen from one endpoint. */
|
|
441
|
+
export interface SubscriptionRef {
|
|
442
|
+
/** The node id at the other end of the edge. */
|
|
443
|
+
node_id: string;
|
|
444
|
+
active: boolean;
|
|
445
|
+
created: string;
|
|
446
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The canvas vocabulary — the node + edge model the whole runtime hangs on.
|
|
2
|
+
//
|
|
3
|
+
// One global canvas (`~/.crouter/canvas/canvas.db`) holds the node graph and
|
|
4
|
+
// runtime state. Each node directory holds only its artifacts.
|
|
5
|
+
/** Maximum serialized opaque action payload; enforced at registration in Phase 5. */
|
|
6
|
+
export const OUTCOME_PAYLOAD_MAX_BYTES = 64 * 1024;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ErrorClass, OperationId } from './events/types.js';
|
|
2
|
+
import { type ProviderFailureDetails } from './provider-failure.js';
|
|
3
|
+
export type FaultLink = 'pi→provider' | 'viewer↔broker' | 'relay↔broker' | 'viewer↔crtrd' | 'broker↔crtrd' | 'daemon→node' | 'crtr→pi';
|
|
4
|
+
export type FaultKind = 'rate-limit' | 'overloaded' | 'connection' | 'auth' | 'protocol' | 'context-overflow' | 'other' | 'wedged' | 'model-not-found';
|
|
5
|
+
export type FaultRetryDisposition = 'auto' | 'manual' | 'fatal';
|
|
6
|
+
export type FaultRetryOwner = 'sdk' | 'daemon' | 'client';
|
|
7
|
+
export interface FaultRetry {
|
|
8
|
+
disposition: FaultRetryDisposition;
|
|
9
|
+
by?: FaultRetryOwner;
|
|
10
|
+
attempt?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
nextAt?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface FaultProviderError extends ProviderFailureDetails {
|
|
15
|
+
provider: string;
|
|
16
|
+
model: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Fault {
|
|
19
|
+
link: FaultLink;
|
|
20
|
+
op: string;
|
|
21
|
+
kind: FaultKind;
|
|
22
|
+
retry: FaultRetry;
|
|
23
|
+
message: string;
|
|
24
|
+
since: string;
|
|
25
|
+
operation_id: OperationId;
|
|
26
|
+
anchorEntryId?: string;
|
|
27
|
+
providerError?: FaultProviderError;
|
|
28
|
+
}
|
|
29
|
+
export interface FaultClassification {
|
|
30
|
+
kind: FaultKind;
|
|
31
|
+
disposition: FaultRetryDisposition;
|
|
32
|
+
}
|
|
33
|
+
export declare function errorClassFromFault(fault: Pick<Fault, 'kind' | 'retry'>): ErrorClass;
|
|
34
|
+
export declare function classify(link: FaultLink, raw: unknown): FaultClassification;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { hasManagedProviderCoolingDiagnostic } from './runtime/managed-provider-cooling.js';
|
|
2
|
+
import { recognizeProviderError } from './provider-error-recognition.js';
|
|
3
|
+
import { extractProviderFailureDetails } from './provider-failure.js';
|
|
4
|
+
import { isRecord } from '../shared/predicates.js';
|
|
5
|
+
const errorClassByFaultKind = {
|
|
6
|
+
'rate-limit': 'rate_limit',
|
|
7
|
+
overloaded: 'overloaded',
|
|
8
|
+
connection: 'connection',
|
|
9
|
+
auth: 'auth',
|
|
10
|
+
protocol: 'protocol',
|
|
11
|
+
'context-overflow': 'context_overflow',
|
|
12
|
+
wedged: 'wedged',
|
|
13
|
+
'model-not-found': 'model_not_found',
|
|
14
|
+
other: 'unknown',
|
|
15
|
+
};
|
|
16
|
+
export function errorClassFromFault(fault) {
|
|
17
|
+
return {
|
|
18
|
+
class: errorClassByFaultKind[fault.kind],
|
|
19
|
+
retry_disposition: fault.retry.disposition,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function faultKindFromProviderSignal(signal) {
|
|
23
|
+
switch (signal.kind) {
|
|
24
|
+
case 'rate-limit': return 'rate-limit';
|
|
25
|
+
case 'auth': return 'auth';
|
|
26
|
+
case 'protocol': return 'protocol';
|
|
27
|
+
case 'context-overflow': return 'context-overflow';
|
|
28
|
+
case 'overloaded':
|
|
29
|
+
case 'overloaded-only':
|
|
30
|
+
case 'transient-http': return 'overloaded';
|
|
31
|
+
case 'connection':
|
|
32
|
+
case 'connection-only': return 'connection';
|
|
33
|
+
case 'socket':
|
|
34
|
+
case 'service-unavailable':
|
|
35
|
+
case 'other': return 'other';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function classifyDisposition(link, kind) {
|
|
39
|
+
if (kind === 'auth' || kind === 'protocol' || kind === 'context-overflow')
|
|
40
|
+
return 'fatal';
|
|
41
|
+
if (link === 'pi→provider') {
|
|
42
|
+
if (kind === 'connection' || kind === 'rate-limit' || kind === 'overloaded')
|
|
43
|
+
return 'auto';
|
|
44
|
+
return 'fatal';
|
|
45
|
+
}
|
|
46
|
+
return 'manual';
|
|
47
|
+
}
|
|
48
|
+
function rawText(raw) {
|
|
49
|
+
if (typeof raw === 'string')
|
|
50
|
+
return raw;
|
|
51
|
+
if (!isRecord(raw))
|
|
52
|
+
return '';
|
|
53
|
+
const candidates = [
|
|
54
|
+
raw['message'],
|
|
55
|
+
raw['errorMessage'],
|
|
56
|
+
raw['reason'],
|
|
57
|
+
raw['body'],
|
|
58
|
+
raw['statusText'],
|
|
59
|
+
];
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
if (typeof candidate === 'string' && candidate !== '')
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
const nested = raw['error'];
|
|
65
|
+
if (isRecord(nested)) {
|
|
66
|
+
const nestedMessage = nested['message'];
|
|
67
|
+
if (typeof nestedMessage === 'string' && nestedMessage !== '')
|
|
68
|
+
return nestedMessage;
|
|
69
|
+
const nestedReason = nested['reason'];
|
|
70
|
+
if (typeof nestedReason === 'string' && nestedReason !== '')
|
|
71
|
+
return nestedReason;
|
|
72
|
+
}
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
export function classify(link, raw) {
|
|
76
|
+
// This diagnostic is the stable machine carrier for an all-managed-pools
|
|
77
|
+
// cooldown. Do not make its retryability depend on explanatory error prose.
|
|
78
|
+
if (link === 'pi→provider' && hasManagedProviderCoolingDiagnostic(raw)) {
|
|
79
|
+
return { kind: 'rate-limit', disposition: 'auto' };
|
|
80
|
+
}
|
|
81
|
+
const value = isRecord(raw) ? raw : {};
|
|
82
|
+
const failure = extractProviderFailureDetails(raw);
|
|
83
|
+
const nested = failure?.causes ?? [];
|
|
84
|
+
const signal = recognizeProviderError({
|
|
85
|
+
code: value['code'] ?? failure?.code ?? nested.find((cause) => cause.code !== undefined)?.code,
|
|
86
|
+
errno: value['errno'] ?? failure?.errno ?? nested.find((cause) => cause.errno !== undefined)?.errno,
|
|
87
|
+
status: value['status'] ?? failure?.status,
|
|
88
|
+
wsCloseCode: value['code'],
|
|
89
|
+
wsCloseReason: value['reason'],
|
|
90
|
+
message: [rawText(raw), failure?.body, ...nested.flatMap((cause) => [cause.message, cause.body])]
|
|
91
|
+
.filter((part) => typeof part === 'string' && part !== '')
|
|
92
|
+
.join(' '),
|
|
93
|
+
});
|
|
94
|
+
const kind = faultKindFromProviderSignal(signal);
|
|
95
|
+
return { kind, disposition: classifyDisposition(link, kind) };
|
|
96
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type ProviderErrorSignalKind = 'rate-limit' | 'auth' | 'protocol' | 'context-overflow' | 'overloaded' | 'overloaded-only' | 'transient-http' | 'connection' | 'connection-only' | 'socket' | 'service-unavailable' | 'other';
|
|
2
|
+
export type ProviderErrorMatchedOn = 'errno' | 'status' | 'websocket-close' | 'text' | 'none';
|
|
3
|
+
export interface ProviderErrorTextMatch {
|
|
4
|
+
kind: ProviderErrorSignalKind;
|
|
5
|
+
match: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProviderErrorSignal {
|
|
8
|
+
kind: ProviderErrorSignalKind;
|
|
9
|
+
matchedOn: ProviderErrorMatchedOn;
|
|
10
|
+
status?: number;
|
|
11
|
+
retryAfterMs?: number;
|
|
12
|
+
match?: string;
|
|
13
|
+
textKind?: ProviderErrorSignalKind;
|
|
14
|
+
textMatch?: string;
|
|
15
|
+
textMatches?: readonly ProviderErrorTextMatch[];
|
|
16
|
+
}
|
|
17
|
+
export interface ProviderErrorRecognitionInput {
|
|
18
|
+
status?: unknown;
|
|
19
|
+
retryAfterMs?: number;
|
|
20
|
+
code?: unknown;
|
|
21
|
+
errno?: unknown;
|
|
22
|
+
wsCloseCode?: unknown;
|
|
23
|
+
wsCloseReason?: unknown;
|
|
24
|
+
message?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Recognizes provider failure facts. Consumers apply their own retry policy. */
|
|
27
|
+
export declare function recognizeProviderError(input: ProviderErrorRecognitionInput): ProviderErrorSignal;
|