@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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// client.test.ts — regression for issue #516: a cold-start `/healthz` timeout
|
|
2
|
+
// used to throw a bare `daemon_unavailable` message that discarded the actual
|
|
3
|
+
// startup failure (the tail of crtrd.err). `coldStartTimeoutMessage` is the
|
|
4
|
+
// pure composition `handleColdSocket` uses on timeout; this proves an injected
|
|
5
|
+
// diagnostic is appended (not dropped) and that a missing/empty diagnostic
|
|
6
|
+
// still falls back to the original bare message unchanged. `safeColdStartDiagnostic`
|
|
7
|
+
// is the guard `handleColdSocket` applies before that composition; this proves a
|
|
8
|
+
// THROWING hook is treated as absent (per the documented `CrtrClientOptions`
|
|
9
|
+
// contract) instead of propagating and replacing the typed `daemon_unavailable`
|
|
10
|
+
// error.
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { createServer } from 'node:http';
|
|
14
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { coldStartTimeoutMessage, CrtrClient, safeColdStartDiagnostic } from '../../client.js';
|
|
18
|
+
import { ApiError } from '../../errors.js';
|
|
19
|
+
test('coldStartTimeoutMessage appends a present diagnostic to the base message', () => {
|
|
20
|
+
const msg = coldStartTimeoutMessage('crtrd.err (tail):\nError: bind EADDRINUSE');
|
|
21
|
+
assert.match(msg, /crtrd did not start/);
|
|
22
|
+
assert.match(msg, /crtrd\.err \(tail\):\nError: bind EADDRINUSE/);
|
|
23
|
+
});
|
|
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.');
|
|
27
|
+
});
|
|
28
|
+
function startDelayedHealthzServer(socketPath, delayMs) {
|
|
29
|
+
const server = createServer((_req, res) => {
|
|
30
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
31
|
+
res.end(JSON.stringify({ ok: true }));
|
|
32
|
+
});
|
|
33
|
+
const timer = setTimeout(() => server.listen(socketPath), delayMs);
|
|
34
|
+
return { server, cancel: () => clearTimeout(timer) };
|
|
35
|
+
}
|
|
36
|
+
// REGRESSION: `crtr sys daemon restart` acks, then tears the old server down and
|
|
37
|
+
// hands over to a successor — a request crossing that window dies with a hang-up
|
|
38
|
+
// (ECONNRESET), not a refused connect. That used to surface as
|
|
39
|
+
// `daemon_unavailable: socket hang up` telling the caller to run
|
|
40
|
+
// `crtr sys daemon start` against a daemon that was already coming back up.
|
|
41
|
+
function startHandoverServer(socketPath) {
|
|
42
|
+
let dropped = false;
|
|
43
|
+
const server = createServer((req, res) => {
|
|
44
|
+
if (!dropped) {
|
|
45
|
+
dropped = true; // model the old server dying mid-request
|
|
46
|
+
req.socket.destroy();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
50
|
+
res.end(JSON.stringify({ ok: true, method: req.method }));
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
server,
|
|
54
|
+
ready: new Promise((resolvePromise) => server.listen(socketPath, resolvePromise)),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
test('a hang-up mid-request rides out the daemon handover and replays the idempotent call', async () => {
|
|
58
|
+
const dir = mkdtempSync(join(tmpdir(), 'crtr-client-handover-'));
|
|
59
|
+
const socketPath = join(dir, 'crtrd.sock');
|
|
60
|
+
const { server, ready } = startHandoverServer(socketPath);
|
|
61
|
+
try {
|
|
62
|
+
await ready;
|
|
63
|
+
const client = new CrtrClient({ socketPath, autostart: false, coldStartPollWindowMs: 2_000 });
|
|
64
|
+
assert.deepEqual(await client.request('GET', '/v1/nodes'), { ok: true, method: 'GET' });
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
server.close();
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
test('a hang-up mid-mutation reports daemon_restarting, not daemon_unavailable', async () => {
|
|
72
|
+
const dir = mkdtempSync(join(tmpdir(), 'crtr-client-handover-'));
|
|
73
|
+
const socketPath = join(dir, 'crtrd.sock');
|
|
74
|
+
const { server, ready } = startHandoverServer(socketPath);
|
|
75
|
+
try {
|
|
76
|
+
await ready;
|
|
77
|
+
const client = new CrtrClient({ socketPath, autostart: false, coldStartPollWindowMs: 2_000 });
|
|
78
|
+
await assert.rejects(() => client.request('POST', '/v1/nodes', {}),
|
|
79
|
+
// Replaying is unsafe (the daemon may have applied it), so this one is the
|
|
80
|
+
// caller's call — but it must not be reported as a daemon that is down.
|
|
81
|
+
(err) => err instanceof ApiError && err.code === 'daemon_restarting');
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
server.close();
|
|
85
|
+
rmSync(dir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
test('cliClient-style cold start fails loud when the injected poll window is shorter than the (valid) startup delay', async () => {
|
|
89
|
+
const dir = mkdtempSync(join(tmpdir(), 'crtr-client-coldstart-'));
|
|
90
|
+
const socketPath = join(dir, 'crtrd.sock');
|
|
91
|
+
const { server, cancel } = startDelayedHealthzServer(socketPath, 300);
|
|
92
|
+
try {
|
|
93
|
+
const client = new CrtrClient({
|
|
94
|
+
socketPath,
|
|
95
|
+
autostart: true,
|
|
96
|
+
onColdSocket: async () => {
|
|
97
|
+
/* fire-and-forget: the delayed listen() above models ensureDaemon() */
|
|
98
|
+
},
|
|
99
|
+
coldStartPollWindowMs: 100, // shorter than the 300ms the daemon actually needs
|
|
100
|
+
});
|
|
101
|
+
await assert.rejects(() => client.healthz(), (err) => err instanceof ApiError && err.code === 'daemon_unavailable');
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
cancel(); // the client already timed out at 100ms; never let listen() fire at 300ms
|
|
105
|
+
server.close();
|
|
106
|
+
rmSync(dir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
test('cliClient-style cold start succeeds when the injected poll window covers the (valid) startup delay', async () => {
|
|
110
|
+
const dir = mkdtempSync(join(tmpdir(), 'crtr-client-coldstart-'));
|
|
111
|
+
const socketPath = join(dir, 'crtrd.sock');
|
|
112
|
+
const { server, cancel } = startDelayedHealthzServer(socketPath, 300);
|
|
113
|
+
try {
|
|
114
|
+
const client = new CrtrClient({
|
|
115
|
+
socketPath,
|
|
116
|
+
autostart: true,
|
|
117
|
+
onColdSocket: async () => {
|
|
118
|
+
/* fire-and-forget: the delayed listen() above models ensureDaemon() */
|
|
119
|
+
},
|
|
120
|
+
coldStartPollWindowMs: 2_000, // long enough to see the 300ms-delayed daemon come up
|
|
121
|
+
});
|
|
122
|
+
const health = await client.healthz();
|
|
123
|
+
assert.deepEqual(health, { ok: true });
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
cancel(); // already fired by the time we get here; harmless no-op
|
|
127
|
+
server.close();
|
|
128
|
+
rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
test('safeColdStartDiagnostic returns undefined for an absent hook', () => {
|
|
132
|
+
assert.equal(safeColdStartDiagnostic(undefined), undefined);
|
|
133
|
+
});
|
|
134
|
+
test('safeColdStartDiagnostic returns the hook result when it succeeds', () => {
|
|
135
|
+
assert.equal(safeColdStartDiagnostic(() => 'crtrd.err (tail):\nboom'), 'crtrd.err (tail):\nboom');
|
|
136
|
+
});
|
|
137
|
+
test('safeColdStartDiagnostic treats a THROWING hook as absent, not a propagated error', () => {
|
|
138
|
+
assert.equal(safeColdStartDiagnostic(() => {
|
|
139
|
+
throw new Error('custom hook blew up');
|
|
140
|
+
}), undefined);
|
|
141
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export declare const MEMORY_SCOPES: readonly ["node", "project", "profile", "user", "builtin"];
|
|
2
|
+
export type MemoryScopeName = typeof MEMORY_SCOPES[number];
|
|
3
|
+
/** A canonical document address: slash-joined safe segments, carried as one percent-encoded path segment. */
|
|
4
|
+
export declare function isMemoryDocName(name: string): boolean;
|
|
5
|
+
export interface MemoryOriginConversationDTO {
|
|
6
|
+
session_file: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MemoryScope {
|
|
9
|
+
node?: string;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
profile?: string;
|
|
12
|
+
store?: MemoryScopeName;
|
|
13
|
+
}
|
|
14
|
+
export interface MemoryDocSummaryDTO {
|
|
15
|
+
name: string;
|
|
16
|
+
short_form: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
privacy: 'public' | 'friends' | 'private';
|
|
19
|
+
scope: MemoryScopeName;
|
|
20
|
+
extensions: Record<string, Record<string, string | boolean | number>>;
|
|
21
|
+
path: string;
|
|
22
|
+
store_root: string;
|
|
23
|
+
physical_relative_path: string;
|
|
24
|
+
candidates?: MemoryCandidateDTO[];
|
|
25
|
+
}
|
|
26
|
+
export interface MemoryCandidateDTO {
|
|
27
|
+
winner: boolean;
|
|
28
|
+
scope: MemoryScopeName;
|
|
29
|
+
store_root: string;
|
|
30
|
+
physical_relative_path: string;
|
|
31
|
+
path: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MemoryDocDTO extends MemoryDocSummaryDTO {
|
|
34
|
+
body: string;
|
|
35
|
+
frontmatter: Record<string, unknown> | null;
|
|
36
|
+
representation: 'leaf-file' | 'directory-index';
|
|
37
|
+
links: string[];
|
|
38
|
+
findings?: MemoryLintFindingDTO[];
|
|
39
|
+
}
|
|
40
|
+
export interface MemoryLintFindingDTO {
|
|
41
|
+
severity: string;
|
|
42
|
+
rule: string;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
export interface MemoryListQuery extends MemoryScope {
|
|
46
|
+
kind?: string;
|
|
47
|
+
limit?: number;
|
|
48
|
+
after?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface MemoryDocCreateRequest extends MemoryScope {
|
|
51
|
+
name: string;
|
|
52
|
+
kind: string;
|
|
53
|
+
when_and_why_to_read: string;
|
|
54
|
+
body: string;
|
|
55
|
+
frontmatter?: Record<string, unknown>;
|
|
56
|
+
extensions?: Record<string, Record<string, string | boolean | number>>;
|
|
57
|
+
}
|
|
58
|
+
export interface MemoryDocUpdateRequest extends MemoryScope {
|
|
59
|
+
rationale: string;
|
|
60
|
+
body?: string;
|
|
61
|
+
frontmatter?: Record<string, unknown>;
|
|
62
|
+
extensions?: Record<string, Record<string, string | boolean | number>>;
|
|
63
|
+
unset?: string[];
|
|
64
|
+
}
|
|
65
|
+
export interface MemoryDocDeletedDTO {
|
|
66
|
+
name: string;
|
|
67
|
+
scope: MemoryScopeName;
|
|
68
|
+
path: string;
|
|
69
|
+
log_path: string;
|
|
70
|
+
deleted: true;
|
|
71
|
+
}
|
|
72
|
+
export interface MemoryDocMoveRequest extends MemoryScope {
|
|
73
|
+
to: string;
|
|
74
|
+
}
|
|
75
|
+
export interface MemoryDocMovedDTO {
|
|
76
|
+
name: string;
|
|
77
|
+
previous_name: string;
|
|
78
|
+
scope: MemoryScopeName;
|
|
79
|
+
path: string;
|
|
80
|
+
log_path: string;
|
|
81
|
+
refs_rewritten: Array<{
|
|
82
|
+
name: string;
|
|
83
|
+
scope: MemoryScopeName;
|
|
84
|
+
path: string;
|
|
85
|
+
refs: number;
|
|
86
|
+
}>;
|
|
87
|
+
follow_up: string;
|
|
88
|
+
}
|
|
89
|
+
export interface MemorySearchRequest extends MemoryScope {
|
|
90
|
+
query: string;
|
|
91
|
+
kind?: string;
|
|
92
|
+
grep?: boolean;
|
|
93
|
+
body?: boolean;
|
|
94
|
+
limit?: number;
|
|
95
|
+
after?: string;
|
|
96
|
+
min_score?: number;
|
|
97
|
+
}
|
|
98
|
+
export type MemorySearchHitDTO = MemoryRankedSearchHitDTO | MemoryGrepSearchHitDTO;
|
|
99
|
+
export interface MemoryRankedSearchHitDTO {
|
|
100
|
+
name: string;
|
|
101
|
+
kind: string;
|
|
102
|
+
privacy: 'public' | 'friends' | 'private';
|
|
103
|
+
scope: MemoryScopeName;
|
|
104
|
+
score: number;
|
|
105
|
+
short_form: string;
|
|
106
|
+
path: string;
|
|
107
|
+
store_root: string;
|
|
108
|
+
physical_relative_path: string;
|
|
109
|
+
candidates?: MemoryCandidateDTO[];
|
|
110
|
+
}
|
|
111
|
+
export interface MemoryGrepSearchHitDTO {
|
|
112
|
+
name: string;
|
|
113
|
+
privacy: 'public' | 'friends' | 'private';
|
|
114
|
+
scope: MemoryScopeName;
|
|
115
|
+
line: number;
|
|
116
|
+
text: string;
|
|
117
|
+
path: string;
|
|
118
|
+
store_root: string;
|
|
119
|
+
physical_relative_path: string;
|
|
120
|
+
candidates?: MemoryCandidateDTO[];
|
|
121
|
+
}
|
|
122
|
+
export interface MemoryHistoryQuery extends MemoryScope {
|
|
123
|
+
limit?: number;
|
|
124
|
+
revision?: number;
|
|
125
|
+
diff?: boolean;
|
|
126
|
+
}
|
|
127
|
+
export interface MemoryHistoryRevisionDTO {
|
|
128
|
+
revision: number;
|
|
129
|
+
op: 'create' | 'edit' | 'delete' | 'move';
|
|
130
|
+
at: string;
|
|
131
|
+
node?: string;
|
|
132
|
+
rationale?: string;
|
|
133
|
+
verbatim?: true;
|
|
134
|
+
body_changed: boolean;
|
|
135
|
+
frontmatter_changed: string[];
|
|
136
|
+
diff?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface MemoryHistoryRecordDTO {
|
|
139
|
+
revision: number;
|
|
140
|
+
op: 'create' | 'edit' | 'delete' | 'move';
|
|
141
|
+
at: string;
|
|
142
|
+
node?: string;
|
|
143
|
+
cwd: string;
|
|
144
|
+
from?: string;
|
|
145
|
+
rationale?: string;
|
|
146
|
+
verbatim?: boolean;
|
|
147
|
+
before: string;
|
|
148
|
+
after: string;
|
|
149
|
+
}
|
|
150
|
+
export interface MemoryHistoryDTO {
|
|
151
|
+
name: string;
|
|
152
|
+
scope: MemoryScopeName;
|
|
153
|
+
log_path: string;
|
|
154
|
+
total: number;
|
|
155
|
+
deleted?: unknown;
|
|
156
|
+
revisions?: MemoryHistoryRevisionDTO[];
|
|
157
|
+
record?: MemoryHistoryRecordDTO;
|
|
158
|
+
}
|
|
159
|
+
export interface MemoryDocRefDTO {
|
|
160
|
+
name: string;
|
|
161
|
+
scope: string;
|
|
162
|
+
path: string;
|
|
163
|
+
store_root: string;
|
|
164
|
+
physical_relative_path: string;
|
|
165
|
+
plugin?: string;
|
|
166
|
+
}
|
|
167
|
+
export interface MemoryListDTO<T> {
|
|
168
|
+
object: 'list';
|
|
169
|
+
data: T[];
|
|
170
|
+
has_more: boolean;
|
|
171
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Memory-document DTOs. Every memory request carries an explicit target; crtrd
|
|
2
|
+
// never resolves documents from its own cwd or environment.
|
|
3
|
+
export const MEMORY_SCOPES = ['node', 'project', 'profile', 'user', 'builtin'];
|
|
4
|
+
/** A canonical document address: slash-joined safe segments, carried as one percent-encoded path segment. */
|
|
5
|
+
export function isMemoryDocName(name) {
|
|
6
|
+
return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(name);
|
|
7
|
+
}
|