@north-light/crouter-api 0.3.249 → 0.3.251

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.
@@ -1,6 +1,6 @@
1
1
  // client.test.ts — regression for issue #516: a cold-start `/healthz` timeout
2
2
  // used to throw a bare `daemon_unavailable` message that discarded the actual
3
- // startup failure (the tail of crtrd.err). `coldStartTimeoutMessage` is the
3
+ // startup failure (the tail of crtrd.log). `coldStartTimeoutMessage` is the
4
4
  // pure composition `handleColdSocket` uses on timeout; this proves an injected
5
5
  // diagnostic is appended (not dropped) and that a missing/empty diagnostic
6
6
  // still falls back to the original bare message unchanged. `safeColdStartDiagnostic`
@@ -11,19 +11,19 @@
11
11
  import { test } from 'node:test';
12
12
  import assert from 'node:assert/strict';
13
13
  import { createServer } from 'node:http';
14
- import { mkdtempSync, rmSync } from 'node:fs';
14
+ import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
15
15
  import { tmpdir } from 'node:os';
16
16
  import { join } from 'node:path';
17
17
  import { coldStartTimeoutMessage, CrtrClient, safeColdStartDiagnostic } from '../../client.js';
18
18
  import { ApiError } from '../../errors.js';
19
19
  test('coldStartTimeoutMessage appends a present diagnostic to the base message', () => {
20
- const msg = coldStartTimeoutMessage('crtrd.err (tail):\nError: bind EADDRINUSE');
20
+ const msg = coldStartTimeoutMessage('crtrd.log (tail):\nError: bind EADDRINUSE');
21
21
  assert.match(msg, /crtrd did not start/);
22
- assert.match(msg, /crtrd\.err \(tail\):\nError: bind EADDRINUSE/);
22
+ assert.match(msg, /crtrd\.log \(tail\):\nError: bind EADDRINUSE/);
23
23
  });
24
24
  test('coldStartTimeoutMessage falls back to the bare message when no diagnostic is available', () => {
25
- assert.equal(coldStartTimeoutMessage(undefined), 'crtrd did not start; run `crtr sys daemon start` and check crtrd.err.');
26
- assert.equal(coldStartTimeoutMessage(''), 'crtrd did not start; run `crtr sys daemon start` and check crtrd.err.');
25
+ assert.equal(coldStartTimeoutMessage(undefined), 'crtrd did not start; run `crtr sys daemon start` and check crtrd.log.');
26
+ assert.equal(coldStartTimeoutMessage(''), 'crtrd did not start; run `crtr sys daemon start` and check crtrd.log.');
27
27
  });
28
28
  function startDelayedHealthzServer(socketPath, delayMs) {
29
29
  const server = createServer((_req, res) => {
@@ -85,6 +85,27 @@ test('a hang-up mid-mutation reports daemon_restarting, not daemon_unavailable',
85
85
  rmSync(dir, { recursive: true, force: true });
86
86
  }
87
87
  });
88
+ test('a non-socket cold-start path surfaces its diagnostic after autostart times out', async () => {
89
+ const dir = mkdtempSync(join(tmpdir(), 'crtr-client-nonsocket-'));
90
+ const socketPath = join(dir, 'crtrd.sock');
91
+ mkdirSync(socketPath);
92
+ const diagnostic = 'crtrd.log (tail):\napi.server.failed: EADDRINUSE';
93
+ const client = new CrtrClient({
94
+ socketPath,
95
+ autostart: true,
96
+ onColdSocket: async () => { },
97
+ coldStartPollWindowMs: 50,
98
+ coldStartDiagnostic: () => diagnostic,
99
+ });
100
+ try {
101
+ await assert.rejects(() => client.healthz(), (error) => error instanceof ApiError
102
+ && error.code === 'daemon_unavailable'
103
+ && error.message.includes(diagnostic));
104
+ }
105
+ finally {
106
+ rmSync(dir, { recursive: true, force: true });
107
+ }
108
+ });
88
109
  test('cliClient-style cold start fails loud when the injected poll window is shorter than the (valid) startup delay', async () => {
89
110
  const dir = mkdtempSync(join(tmpdir(), 'crtr-client-coldstart-'));
90
111
  const socketPath = join(dir, 'crtrd.sock');
@@ -132,7 +153,7 @@ test('safeColdStartDiagnostic returns undefined for an absent hook', () => {
132
153
  assert.equal(safeColdStartDiagnostic(undefined), undefined);
133
154
  });
134
155
  test('safeColdStartDiagnostic returns the hook result when it succeeds', () => {
135
- assert.equal(safeColdStartDiagnostic(() => 'crtrd.err (tail):\nboom'), 'crtrd.err (tail):\nboom');
156
+ assert.equal(safeColdStartDiagnostic(() => 'crtrd.log (tail):\nboom'), 'crtrd.log (tail):\nboom');
136
157
  });
137
158
  test('safeColdStartDiagnostic treats a THROWING hook as absent, not a propagated error', () => {
138
159
  assert.equal(safeColdStartDiagnostic(() => {
@@ -39,7 +39,7 @@ export interface CrtrClientOptions {
39
39
  onColdSocket?: () => Promise<void>;
40
40
  /** Injected cold-start diagnostic. Called ONLY when the bounded
41
41
  * `/healthz` poll times out after `onColdSocket`, so the caller can attach
42
- * operator-useful context (e.g. a bounded tail of crtrd's stderr log) to the
42
+ * operator-useful context (e.g. a bounded tail of `crtrd.log`) to the
43
43
  * `daemon_unavailable` error instead of it staying a bare message. Must
44
44
  * return synchronously and cheaply — it runs on the failure path, not the
45
45
  * happy path. A thrown/undefined result is treated as "no diagnostic". */
@@ -668,7 +668,7 @@ export class CrtrClient {
668
668
  if (this.socketPath === undefined)
669
669
  return false;
670
670
  const code = err?.code;
671
- return code === 'ECONNREFUSED' || code === 'ENOENT';
671
+ return code === 'ECONNREFUSED' || code === 'ENOENT' || code === 'ENOTSOCK';
672
672
  }
673
673
  /** A connection torn down MID-request (Node's "socket hang up" / a broken
674
674
  * pipe) — as distinct from a refused connect, which means nothing is
@@ -817,7 +817,7 @@ function sleep(ms) {
817
817
  * design) purely so the regression test can assert the composition without
818
818
  * waiting out the real poll window. */
819
819
  export function coldStartTimeoutMessage(diagnostic) {
820
- const base = 'crtrd did not start; run `crtr sys daemon start` and check crtrd.err.';
820
+ const base = 'crtrd did not start; run `crtr sys daemon start` and check crtrd.log.';
821
821
  return diagnostic !== undefined && diagnostic !== '' ? `${base}\n${diagnostic}` : base;
822
822
  }
823
823
  /** Invoke the injected `coldStartDiagnostic` hook, treating a THROW the same
@@ -47,6 +47,9 @@ export interface ReviveAllResultDTO {
47
47
  * from edges; the flag only opts out of cascading descendants. */
48
48
  export interface CloseRequest {
49
49
  cascade?: boolean;
50
+ /** PID of the shell that invoked this close. The daemon excludes this process
51
+ * and its descendants from broker teardown so a node can close itself. */
52
+ caller_pid?: number;
50
53
  /** Root close disposition: `true` finalizes the root to `done` (the browse `x`
51
54
  * "finish" semantics); default/`false` cancels it. The cascade set (computed
52
55
  * server-side) is torn down either way. */
@@ -62,6 +62,10 @@ export interface MessageResultDTO {
62
62
  * frozen and the daemon relaunches it when one frees. The entry is durably
63
63
  * appended either way, and delivery follows that relaunch. */
64
64
  not_revived_reason?: 'capacity_frozen';
65
+ /** Present only when the entry was appended ABOVE the requested tier: a
66
+ * terminal target never takes the later cycle `normal`/`deferred` wait for,
67
+ * so its mail is raised to the steering tier. */
68
+ delivered_tier?: InboxTierDTO;
65
69
  delivered_at?: IsoTime;
66
70
  /** Which channel a `delivery:'interactive'` send actually used: `'engine'`
67
71
  * (live broker frame loop, no inbox entry) or `'inbox'` (durable fallback).
@@ -36,6 +36,8 @@ export interface CreateNodeRequest {
36
36
  /** Caller-supplied short description, preserved against automatic naming. */
37
37
  description?: string;
38
38
  parent?: NodeIdDTO | null;
39
+ /** Node that created this request; absent for an external process. */
40
+ creator?: NodeIdDTO;
39
41
  root?: boolean;
40
42
  /** Lifecycle of the new node. Roots default to `resident`; managed children
41
43
  * default to `terminal`. Use `resident` for a child that remains wakeable
@@ -165,6 +167,8 @@ export interface NodeFaultDTO {
165
167
  /** The full node view — summary ∪ identity extras ∪ edges ∪ paths. Returned by
166
168
  * `GET /v1/nodes/{id}` and by the create/lifecycle actions that yield a node. */
167
169
  export interface NodeDetailDTO extends NodeSummaryDTO {
170
+ /** Node that created this node, or null when an external process did. */
171
+ creator: NodeIdDTO | null;
168
172
  description?: string;
169
173
  /** The namer's prose form of `description` — sentence case, punctuation intact
170
174
  * (`NodeMeta.title`). What a surface showing this node to a person reads;
@@ -1,9 +1,14 @@
1
1
  import type { IsoTime, NodeIdDTO } from './common.js';
2
- /** Report tier — `crtr push {update,urgent,final}`. */
2
+ /** Report kind — `crtr push {update,final}`; legacy `urgent` rows remain readable. */
3
3
  export type ReportTierDTO = 'update' | 'urgent' | 'final';
4
- /** `POST /v1/nodes/{id}/reports` body ({id} = the reporting node). */
4
+ /** Delivery urgency for an update report's subscriber fan-out. */
5
+ export type ReportDeliveryTierDTO = 'deferred' | 'normal' | 'urgent';
6
+ /** `POST /v1/nodes/{id}/reports` body ({id} = the reporting node).
7
+ * `tier` is the stored report kind; `delivery_tier` is the optional inbox
8
+ * delivery urgency for an update's subscriber fan-out. */
5
9
  export interface PushReportRequest {
6
10
  tier: ReportTierDTO;
11
+ delivery_tier?: ReportDeliveryTierDTO;
7
12
  body: string;
8
13
  }
9
14
  /** Result of a push. `transitioned` is present only for `final`, which drives a
@@ -10,10 +10,14 @@ export declare const REVIEW_BOUNDARY_CUSTOM_TYPE = "crtr-review-boundary";
10
10
  export declare const STALL_REPROMPT: string;
11
11
  /** The daemon's parking mandate: the last turn of a conversation the unattended
12
12
  * clock is concluding. Delivered live, wrapped in a `park` runtime card. It
13
- * shares a CONTRACT with `node yield`'s pre-invocation guide (roadmap current,
14
- * short and shrinking; context dir for in-progress material; memory only for
15
- * gated permanent lessons) but not a string: yield is read by an agent choosing
16
- * to refresh, this by an agent being told to conclude. */
13
+ * governs both the node's durable inheritance and reader-facing output:
14
+ * one update for subscribers and history, then a goodbye that does not narrate
15
+ * the parking.
16
+ *
17
+ * The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
18
+ * guide (roadmap current, short and shrinking; context dir for in-progress
19
+ * material; memory only for gated permanent lessons) but not a string: yield is
20
+ * read by an agent choosing to refresh, this by an agent being told to conclude. */
17
21
  export declare const PARK_SUMMARY_PROMPT: string;
18
22
  /** Static recovery prompts shared by the broker producer and display classifier. */
19
23
  export declare const AUTH_FAULT_RECOVERY_BODY = "Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.";
@@ -15,15 +15,21 @@ export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone a
15
15
  "Pipe the result to `crtr push final` through a single-quoted heredoc if the work is done, or use `crtr human send` if you are blocked or need the user.";
16
16
  /** The daemon's parking mandate: the last turn of a conversation the unattended
17
17
  * clock is concluding. Delivered live, wrapped in a `park` runtime card. It
18
- * shares a CONTRACT with `node yield`'s pre-invocation guide (roadmap current,
19
- * short and shrinking; context dir for in-progress material; memory only for
20
- * gated permanent lessons) but not a string: yield is read by an agent choosing
21
- * to refresh, this by an agent being told to conclude. */
22
- export const PARK_SUMMARY_PROMPT = 'This conversation has been idle with nothing waiting on it, so it is being concluded. This is your last turn — do these four things now, then stop.\n\n'
23
- + '1. Bring your roadmap current. It lives at `$CRTR_CONTEXT_DIR/roadmap.md`, and it is the document a fresh cycle reads to know where things stand: what was done, what is still open, and what someone picking this up needs. Write one now if you never had one. Keep it short and shrinking — strategy and present state only, deleting completed items rather than marking them done.\n\n'
24
- + '2. Save anything else worth keeping. In-progress task material — what has happened, identifiers, in-flight state — goes to one or more files in your context directory. A permanent lesson worth remembering indefinitely goes to node memory (`crtr memory write -h`), gated deliberately: transcript state does not belong in memory.\n\n'
25
- + '3. Push one regular report to your subscribers summarizing where things stand: `crtr push update`. NOT `crtr push final` — this concludes the conversation, it does not finish a mandate.\n\n'
26
- + '4. Stop. A later message reopens you on a fresh context window grounded in your goal and that roadmap, so what you leave behind now is what you get back.';
18
+ * governs both the node's durable inheritance and reader-facing output:
19
+ * one update for subscribers and history, then a goodbye that does not narrate
20
+ * the parking.
21
+ *
22
+ * The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
23
+ * guide (roadmap current, short and shrinking; context dir for in-progress
24
+ * material; memory only for gated permanent lessons) but not a string: yield is
25
+ * read by an agent choosing to refresh, this by an agent being told to conclude. */
26
+ export const PARK_SUMMARY_PROMPT = 'This conversation has been idle with nothing left to wake it, so it is being concluded. This is your last turn. Use it to leave a trustworthy inheritance, not to restart or broaden the work. Do these six things now, then stop.\n\n'
27
+ + '1. Establish current truth. Check only state that may have changed outside the transcript and matters to resuming—such as the working tree, a remote run, or an external decision. Do not start new work; perform only a quick check needed to avoid recording an unverified claim. If no mandate or work ever began, say that plainly and keep every artifact minimal.\n\n'
28
+ + '2. Rewrite `$CRTR_CONTEXT_DIR/roadmap.md` for a fresh context window. This is the only handoff document a later fresh cycle receives in full. Preserve the current goal and exit criteria when they still apply, then state the present outcome; what remains or is blocked; decisions or questions still open; exact recovery handles for in-flight state; and the first safe move on return. Keep strategy and present state, not a transcript recap. Delete stale and completed steps instead of marking them done; the roadmap should stay short and shrink. Write a minimal one now if none exists.\n\n'
29
+ + '3. Put supporting material in your context directory only when the roadmap would become bulky without it. Rewrite existing living documents rather than leave superseded versions. Name every supporting file the next cycle must read from the roadmap and say what it is for—the revive shows filenames but does not inject their contents. Task state, identifiers, and recovery detail belong here, not in memory.\n\n'
30
+ + '4. Use memory only for a non-obvious, reusable lesson that should survive this task and is not already recorded. Read `crtr memory write -h`, find before writing, and choose the narrowest scope that will reach the next agent who needs it. Do not put a conversation recap, task status, recovery handles, or facts already captured in code or docs into memory.\n\n'
31
+ + '5. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write it for subscribers and history, not as a second roadmap. Its first line must stand alone as the current outcome, blocker, or decision that matters; then include only unfinished work, a needed decision, and concrete handles a subscriber may need. This concludes the conversation; it does not finish the mandate.\n\n'
32
+ + '6. End with a short sign-off to the reader in second person: say you are putting your notes in order and pausing here until they come back, and mention anything genuinely worth their attention, such as unfinished work or an open question. Keep all visible text this turn—including the update\'s first line—about their work, not crouter\'s machinery: do not mention roadmap or context paths, filing a report, parking, idling, residency, or node ids. Then stop. A later message reopens you on a fresh context window grounded in your goal and roadmap, so the inheritance you leave now is what you get back.';
27
33
  /** Static recovery prompts shared by the broker producer and display classifier. */
28
34
  export const AUTH_FAULT_RECOVERY_BODY = 'Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.';
29
35
  export const CONNECTION_FAULT_RECOVERY_BODY = 'The network connection is back online. Your previous turn stopped on a connection error (the network was down). Continue from where you left off and retry the work that failed.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter-api",
3
- "version": "0.3.249",
3
+ "version": "0.3.251",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, and the CrtrClient. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",