@ccmsg/cli 0.11.2 → 0.11.4
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/README.md +8 -12
- package/package.json +1 -1
- package/src/auth/auth.ts +23 -23
- package/src/auth/http.ts +3 -3
- package/src/auth/records.ts +2 -2
- package/src/auth/webauthn.ts +1 -1
- package/src/cli.ts +78 -23
- package/src/daemon/control.ts +1 -1
- package/src/daemon/registry.ts +11 -11
- package/src/daemon/snapshot.ts +1 -1
- package/src/daemon/supervise.ts +4 -4
- package/src/dispatch/caller.ts +1 -1
- package/src/dispatch/dispatch.ts +4 -4
- package/src/dispatch/handler.ts +4 -4
- package/src/dispatch/identity.ts +1 -1
- package/src/dispatch/result.ts +2 -2
- package/src/files/containment.ts +2 -2
- package/src/harness/index.ts +1 -1
- package/src/instance/client.ts +1 -1
- package/src/instance/config.ts +13 -13
- package/src/instance/instance.ts +42 -42
- package/src/instance/lock.ts +2 -2
- package/src/instance/log.ts +1 -1
- package/src/instance/paths.ts +6 -6
- package/src/instance/socket.ts +1 -1
- package/src/kv/store.ts +1 -1
- package/src/mesh/instances.ts +2 -2
- package/src/mesh/keys.ts +7 -7
- package/src/mesh/mesh.ts +67 -67
- package/src/mesh/relay.ts +8 -8
- package/src/mesh/wire.ts +2 -2
- package/src/messaging/delivery.ts +24 -24
- package/src/messaging/direct.ts +23 -23
- package/src/messaging/inbox.ts +6 -6
- package/src/messaging/notify.ts +5 -5
- package/src/plugin/codex.ts +1 -1
- package/src/sessions/classify.ts +6 -6
- package/src/sessions/dump.ts +1 -1
- package/src/sessions/handlers.ts +2 -2
- package/src/sessions/harness.ts +12 -12
- package/src/sessions/last-live.ts +4 -4
- package/src/sessions/registry.ts +32 -32
- package/src/sessions/status.ts +7 -7
- package/src/sessions/terminals.ts +1 -1
- package/src/transcript/files.ts +4 -4
- package/src/transcript/fold.ts +9 -9
- package/src/transcript/items/classify.ts +56 -8
- package/src/transcript/read.ts +1 -1
- package/src/transcript/tail.ts +3 -3
- package/src/transcript/transcripts.ts +7 -7
- package/src/translate/helper.ts +2 -2
- package/src/transport/conn.ts +3 -3
- package/src/transport/dial.ts +1 -1
- package/src/transport/driver.ts +2 -2
- package/src/transport/entry.ts +1 -1
- package/src/transport/framing.ts +1 -1
- package/src/transport/listener.ts +3 -3
- package/src/transport/uds.ts +2 -2
- package/src/transport/ws.ts +2 -2
- package/src/upstream/events.ts +3 -3
- package/src/upstream/gateway.ts +6 -6
- package/src/upstream/json.ts +1 -1
- package/src/upstream/requests.ts +6 -6
- package/src/upstream/stats.ts +1 -1
- package/src/upstream/status.ts +4 -4
- package/src/upstream/usage.ts +1 -1
- package/src/upstream/webhook.ts +1 -1
package/src/dispatch/caller.ts
CHANGED
|
@@ -10,7 +10,7 @@ export class CallerError extends Error {}
|
|
|
10
10
|
* The role is read here because it decides which fields the value has to
|
|
11
11
|
* carry — `sid` is present exactly when the role is `session` — and not
|
|
12
12
|
* whether anything is allowed: what the identity may do is decided afterwards,
|
|
13
|
-
* by dispatch, against the attribute table (§
|
|
13
|
+
* by dispatch, against the attribute table (DESIGN §2.2). It is the same question
|
|
14
14
|
* `hello` asks of a greeting, whose shape depends on its role in the same way
|
|
15
15
|
* and for the same reason: one schema covers all three roles, so the schema
|
|
16
16
|
* cannot state the rule and the instance does.
|
package/src/dispatch/dispatch.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface DispatchDeps {
|
|
|
20
20
|
readonly capabilities: ReadonlySet<Capability>;
|
|
21
21
|
/** The instance that owns the subject of an `instance-local` op, or
|
|
22
22
|
* `undefined` when no other instance owns it and we answer ourselves.
|
|
23
|
-
* The routing table behind this is the `peers` topic (
|
|
23
|
+
* The routing table behind this is the `peers` topic (DESIGN §7.3). */
|
|
24
24
|
readonly resolveInstance: (op: OpName, frame: Record<string, unknown>) => InstanceId | undefined;
|
|
25
25
|
readonly handlers: Handlers;
|
|
26
26
|
}
|
|
@@ -31,7 +31,7 @@ function isOpName(op: string): op is OpName {
|
|
|
31
31
|
|
|
32
32
|
/** Decide one frame.
|
|
33
33
|
*
|
|
34
|
-
* The six steps of
|
|
34
|
+
* The six steps of DESIGN §2.2 are written once, here, and read the op
|
|
35
35
|
* attribute table for every op. Adding an op is a row in the table plus a
|
|
36
36
|
* schema and an implementation — never a check in this function (M1). */
|
|
37
37
|
export async function dispatch(
|
|
@@ -101,7 +101,7 @@ export async function dispatch(
|
|
|
101
101
|
//
|
|
102
102
|
// A request that has already been here is dropped before that: a cycle in
|
|
103
103
|
// the routing would otherwise send it round the same instances until every
|
|
104
|
-
// deadline expired (§7.3). It is answered rather than left unanswered,
|
|
104
|
+
// deadline expired (DESIGN §7.3). It is answered rather than left unanswered,
|
|
105
105
|
// because the caller learns the same thing sooner and the code is the one
|
|
106
106
|
// the contract gives a destination that could not be reached.
|
|
107
107
|
const hops = fields["hops"];
|
|
@@ -123,7 +123,7 @@ export async function dispatch(
|
|
|
123
123
|
args: fields,
|
|
124
124
|
conn,
|
|
125
125
|
identity: identity.state === "settled" ? identity : undefined,
|
|
126
|
-
// The one route by which a role reaches an implementation (§
|
|
126
|
+
// The one route by which a role reaches an implementation (DESIGN §2.2).
|
|
127
127
|
role: attrs.scope === "role" && identity.state === "settled" ? identity.role : undefined,
|
|
128
128
|
});
|
|
129
129
|
return reply(requestId, body);
|
package/src/dispatch/handler.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ConnIdentity, SettledIdentity } from "./identity.ts";
|
|
|
5
5
|
*
|
|
6
6
|
* It is the `Conn` transport accepted, narrowed to what an op may do with it:
|
|
7
7
|
* read the identity, push frames, and learn that the connection is gone. A
|
|
8
|
-
* subscription is held by a connection and ends with it (
|
|
8
|
+
* subscription is held by a connection and ends with it (DESIGN §6.3), so
|
|
9
9
|
* this is what the topic mechanism keys its subscribers on. Declared here
|
|
10
10
|
* rather than imported from transport because dispatch sits below it. */
|
|
11
11
|
export interface Requester {
|
|
@@ -25,12 +25,12 @@ export interface Requester {
|
|
|
25
25
|
/** What an op implementation receives.
|
|
26
26
|
*
|
|
27
27
|
* The arguments are already validated and the caller is already allowed
|
|
28
|
-
* (
|
|
28
|
+
* (DESIGN §2.2): a handler starts from "this may be run", so it holds no
|
|
29
29
|
* check of its own. */
|
|
30
30
|
export interface HandlerInput {
|
|
31
31
|
readonly op: OpName;
|
|
32
32
|
/** The connection the request arrived on. Ops that hold something for the
|
|
33
|
-
* length of a connection — the subscriptions of
|
|
33
|
+
* length of a connection — the subscriptions of DESIGN §6.3 — need it;
|
|
34
34
|
* ops that only answer ignore it. */
|
|
35
35
|
readonly conn: Requester;
|
|
36
36
|
/** The whole request frame, validated against the op's request schema. */
|
|
@@ -40,7 +40,7 @@ export interface HandlerInput {
|
|
|
40
40
|
/** Set only for ops the attribute table marks `scope: "role"`, where the role
|
|
41
41
|
* changes what the reply may contain rather than whether the call is allowed.
|
|
42
42
|
* This is the only route by which a role reaches an implementation
|
|
43
|
-
* (
|
|
43
|
+
* (DESIGN §2.2). */
|
|
44
44
|
readonly role?: Role;
|
|
45
45
|
}
|
|
46
46
|
|
package/src/dispatch/identity.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Role, Sid } from "@ccmsg/protocol";
|
|
|
2
2
|
|
|
3
3
|
/** What dispatch knows about the connection a frame arrived on.
|
|
4
4
|
*
|
|
5
|
-
* transport settles this (
|
|
5
|
+
* transport settles this (DESIGN §2.1): a connection starts anonymous and
|
|
6
6
|
* becomes `settled` when `hello` binds a role, and a session's `sid`, to it.
|
|
7
7
|
* Nothing else about the connection reaches dispatch — the authorization steps
|
|
8
8
|
* read the op attribute table, not the connection. */
|
package/src/dispatch/result.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ErrorCode, ErrorResponse, InstanceId } from "@ccmsg/protocol";
|
|
|
3
3
|
/** What dispatch decided about one frame.
|
|
4
4
|
*
|
|
5
5
|
* `forward` is the only outcome that is not an answer: the op belongs to
|
|
6
|
-
* another instance and mesh has to carry it there (
|
|
6
|
+
* another instance and mesh has to carry it there (DESIGN §2.2 step 6).
|
|
7
7
|
* There is no mesh yet, so dispatch names the destination and stops. */
|
|
8
8
|
export type DispatchResult =
|
|
9
9
|
/** The frame was not a request and has no answer. The mesh handshake's own
|
|
@@ -32,7 +32,7 @@ export class OpError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** The reply envelope, built here and nowhere else so the wire shape stays in
|
|
35
|
-
* one place (
|
|
35
|
+
* one place (DESIGN §9.1). */
|
|
36
36
|
export function reply(
|
|
37
37
|
requestId: string,
|
|
38
38
|
body: unknown,
|
package/src/files/containment.ts
CHANGED
|
@@ -53,13 +53,13 @@ export interface Located {
|
|
|
53
53
|
* range a caller has are decided in one place and cannot come apart.
|
|
54
54
|
*
|
|
55
55
|
* `Viewer` is that visible range, and it is where the role of a `scope: "role"`
|
|
56
|
-
* op arrives (§
|
|
56
|
+
* op arrives (DESIGN §2.2): a session reads its own session's files, a person reads
|
|
57
57
|
* any session's, and a role the rule does not name reaches nothing rather than
|
|
58
58
|
* being guessed at. An op the attribute table gives no `scope` states no role
|
|
59
59
|
* here, and needs none — dispatch has already settled who may call it. */
|
|
60
60
|
export interface Viewer {
|
|
61
61
|
/** Present only for an op the attribute table marks `scope: "role"`, which
|
|
62
|
-
* is the one route by which a role reaches an implementation (§
|
|
62
|
+
* is the one route by which a role reaches an implementation (DESIGN §2.2). */
|
|
63
63
|
readonly role?: Role;
|
|
64
64
|
/** The session the connection speaks for, when it speaks for one. */
|
|
65
65
|
readonly sid?: Sid;
|
package/src/harness/index.ts
CHANGED
|
@@ -72,7 +72,7 @@ export const HARNESS: Record<Harness, HarnessFacts> = {
|
|
|
72
72
|
* thread to the commands of its own turn. The narrower claim is the truer one,
|
|
73
73
|
* so it is asked first. The reverse nesting — a Claude Code session started
|
|
74
74
|
* from inside a Codex turn — reads as Codex. `--sid` overrides only the sid a
|
|
75
|
-
* command speaks as, and not which instance it speaks to (§
|
|
75
|
+
* command speaks as, and not which instance it speaks to (DESIGN §4.1).
|
|
76
76
|
*
|
|
77
77
|
* A process no session runs inside — a person at a terminal, a supervisor —
|
|
78
78
|
* matches nothing here, and the caller falls back to what it would have done
|
package/src/instance/client.ts
CHANGED
package/src/instance/config.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { parseCidr } from "./client.ts";
|
|
|
14
14
|
|
|
15
15
|
/** Where the instance accepts WebSocket connections, and from whom.
|
|
16
16
|
*
|
|
17
|
-
* The two allowlists are the entry check of §
|
|
17
|
+
* The two allowlists are the entry check of DESIGN §2.1: what transport asks before a
|
|
18
18
|
* request is upgraded. They are config-driven because who may reach an
|
|
19
19
|
* instance is a deployment fact, not a property of the code. */
|
|
20
20
|
export interface EntryConfig {
|
|
@@ -111,14 +111,14 @@ export interface DumpConfig {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export interface InstanceConfig {
|
|
114
|
-
/** Which harness this config home runs (§
|
|
114
|
+
/** Which harness this config home runs (DESIGN §4.1).
|
|
115
115
|
*
|
|
116
116
|
* A setting rather than something discovered, because it decides where the
|
|
117
117
|
* instance looks before there is anything there to look at: an empty config
|
|
118
118
|
* home says nothing about the program it belongs to, and an instance that
|
|
119
119
|
* guessed would walk the wrong tree for the whole of its first session. */
|
|
120
120
|
readonly harness: Harness;
|
|
121
|
-
/** Every instance of the mesh, this one among them (§7.1).
|
|
121
|
+
/** Every instance of the mesh, this one among them (DESIGN §7.1).
|
|
122
122
|
*
|
|
123
123
|
* Data, and the same data on every host: a settings function is handed it
|
|
124
124
|
* and may read it — an instance that wants to know who else there is has it
|
|
@@ -127,7 +127,7 @@ export interface InstanceConfig {
|
|
|
127
127
|
* settles `endpoint` below. */
|
|
128
128
|
readonly endpoints: readonly EndpointRow[];
|
|
129
129
|
/** Where peers and people reach this instance: its own row of the mesh
|
|
130
|
-
* (§7.1).
|
|
130
|
+
* (DESIGN §7.1).
|
|
131
131
|
*
|
|
132
132
|
* Not something a settings file states — the row is, and two places to write
|
|
133
133
|
* one address is one place for it to be wrong. An instance behind a reverse
|
|
@@ -140,7 +140,7 @@ export interface InstanceConfig {
|
|
|
140
140
|
readonly entry?: EntryConfig;
|
|
141
141
|
readonly upstream: UpstreamConfig;
|
|
142
142
|
/** Whether delivery tries the harness's messaging socket before the `inbox`
|
|
143
|
-
* topic (§
|
|
143
|
+
* topic (DESIGN §6.5 condition 0). On, because the protocol has been read off a
|
|
144
144
|
* running harness; off is for a harness generation that turns out to speak
|
|
145
145
|
* something else, and costs only the reach route (b) never had. */
|
|
146
146
|
readonly direct_delivery: boolean;
|
|
@@ -157,7 +157,7 @@ export interface InstanceConfig {
|
|
|
157
157
|
*
|
|
158
158
|
* Its own class so startup can tell "the operator wrote something wrong" from
|
|
159
159
|
* any other failure, and refuse to run rather than continuing with the feature
|
|
160
|
-
* that setting was for silently off (§8.3,
|
|
160
|
+
* that setting was for silently off (DESIGN §8.3, DR-0004). */
|
|
161
161
|
export class ConfigError extends Error {
|
|
162
162
|
constructor(
|
|
163
163
|
readonly file: string,
|
|
@@ -184,7 +184,7 @@ export const DEFAULT_CONFIG: InstanceConfig = {
|
|
|
184
184
|
|
|
185
185
|
/** The file every instance's settings start from, and the directory holding
|
|
186
186
|
* one file per instance. Both are read from the config home a person edits
|
|
187
|
-
* (§8.2). The names are held here alone, so what the files are called is one
|
|
187
|
+
* (DESIGN §8.2). The names are held here alone, so what the files are called is one
|
|
188
188
|
* edit rather than a search. */
|
|
189
189
|
export const CONFIG_FILE = "config_v2.ts";
|
|
190
190
|
export const INSTANCES_DIR = "instances";
|
|
@@ -216,10 +216,10 @@ const INSTANCE_FIELDS = ["dir", "name"] as const;
|
|
|
216
216
|
* instance cannot work out for itself — which address of the several a host
|
|
217
217
|
* has is the one its peers dial, and which of the entries is this instance.
|
|
218
218
|
* Both are answered by the row carrying its own id, which is what settles
|
|
219
|
-
* `self` (§7.1) without asking the network anything.
|
|
219
|
+
* `self` (DESIGN §7.1) without asking the network anything.
|
|
220
220
|
*
|
|
221
221
|
* Every instance of the mesh is in it, this host's and the others', so one
|
|
222
|
-
* file can be copied to every host unchanged (§8.2). */
|
|
222
|
+
* file can be copied to every host unchanged (DESIGN §8.2). */
|
|
223
223
|
export const ENDPOINTS_FILE = "endpoints.json";
|
|
224
224
|
|
|
225
225
|
/** Which of them this host starts. An id here and not in the endpoints is a
|
|
@@ -233,7 +233,7 @@ export const SUPERVISOR_FILE = "supervisor.json";
|
|
|
233
233
|
* Apart from the files a person edits because the two answer different
|
|
234
234
|
* questions: what is being written, and what is running. A config that does
|
|
235
235
|
* not check out never reaches here, which is what lets a broken edit be
|
|
236
|
-
* reported without taking the host down (§8.3). */
|
|
236
|
+
* reported without taking the host down (DESIGN §8.3). */
|
|
237
237
|
export const STATE_CONFIG_DIR = "config";
|
|
238
238
|
export const SATISFIED_FILE = "satisfied.json";
|
|
239
239
|
export const REJECTED_DIR = "config.rejected";
|
|
@@ -292,7 +292,7 @@ export function instanceFileName(id: string): string {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
/** Read everything a person edits, call what has to be called, and check the
|
|
295
|
-
* whole of it (
|
|
295
|
+
* whole of it (DR-0004, DESIGN §8.3).
|
|
296
296
|
*
|
|
297
297
|
* One pass rather than a check per file, because what makes a config right is
|
|
298
298
|
* mostly between files: an id the supervisor starts has to be an entry of the
|
|
@@ -400,7 +400,7 @@ function readEndpoints(
|
|
|
400
400
|
if (rows.some((row) => row.id === id)) at(file, `${where}.id repeats ${id}`);
|
|
401
401
|
else if (rows.some((row) => row.endpoint === endpoint)) {
|
|
402
402
|
// Two entries at one address would each be this instance to whoever
|
|
403
|
-
// dialled it, and neither could be told from the other (§7.1).
|
|
403
|
+
// dialled it, and neither could be told from the other (DESIGN §7.1).
|
|
404
404
|
at(file, `${where}.endpoint repeats ${endpoint}`);
|
|
405
405
|
} else rows.push({ id, endpoint: endpoint as Endpoint });
|
|
406
406
|
}
|
|
@@ -468,7 +468,7 @@ async function instanceOf(
|
|
|
468
468
|
if (read === undefined) return undefined;
|
|
469
469
|
// Where this instance is reached: its own row of the mesh. An instance the
|
|
470
470
|
// data does not name could not be dialled by anybody and could not settle
|
|
471
|
-
// what a handshake calls it (§7.1), so it is a config error rather than an
|
|
471
|
+
// what a handshake calls it (DESIGN §7.1), so it is a config error rather than an
|
|
472
472
|
// instance with no address.
|
|
473
473
|
const mine = endpoints.find((row) => row.id === id);
|
|
474
474
|
if (mine === undefined) {
|
package/src/instance/instance.ts
CHANGED
|
@@ -111,7 +111,7 @@ export interface StartOptions {
|
|
|
111
111
|
*
|
|
112
112
|
* Passed by value rather than through the environment, because the
|
|
113
113
|
* environment is read for a different question: which session the process
|
|
114
|
-
* runs inside, and therefore which config home *that* means (§
|
|
114
|
+
* runs inside, and therefore which config home *that* means (DESIGN §4.1). A
|
|
115
115
|
* `daemon run <dir>` started from inside a session of another harness would
|
|
116
116
|
* otherwise answer for the config home of whoever started it. Absent means
|
|
117
117
|
* the environment decides, which is what a process nobody named a directory
|
|
@@ -120,7 +120,7 @@ export interface StartOptions {
|
|
|
120
120
|
/** Mirror the log to stderr. A foreground run wants it; a test does not. */
|
|
121
121
|
readonly echoLog?: boolean;
|
|
122
122
|
/** Whether this start is the one that reads the edited files and writes down
|
|
123
|
-
* what checked out (§8.2).
|
|
123
|
+
* what checked out (DESIGN §8.2).
|
|
124
124
|
*
|
|
125
125
|
* A supervisor does that for the instances it starts, so its children read
|
|
126
126
|
* what it applied and write nothing: one writer means no two processes
|
|
@@ -144,19 +144,19 @@ export interface StartOptions {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/** The mesh intervals a caller may shorten. The values themselves, and why they
|
|
147
|
-
* are what they are, belong to the mesh (§8.2, §8.3). */
|
|
147
|
+
* are what they are, belong to the mesh (DESIGN §8.2, §8.3). */
|
|
148
148
|
export interface MeshTiming {
|
|
149
149
|
readonly heartbeatMs?: number;
|
|
150
150
|
readonly heartbeatTimeoutMs?: number;
|
|
151
151
|
readonly reconnectMinMs?: number;
|
|
152
152
|
readonly forwardTimeoutMs?: number;
|
|
153
|
-
/** The clock the retention window of §7.5 is read against, so a test can
|
|
153
|
+
/** The clock the retention window of DESIGN §7.5 is read against, so a test can
|
|
154
154
|
* pass it without waiting a week. */
|
|
155
155
|
readonly now?: () => Timestamp;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/** Startup found another instance already serving this config home. Nothing
|
|
159
|
-
* was created and nothing has to be undone (§8.3 step 2). */
|
|
159
|
+
* was created and nothing has to be undone (DESIGN §8.3 step 2). */
|
|
160
160
|
export interface AlreadyRunning {
|
|
161
161
|
readonly kind: "already_running";
|
|
162
162
|
readonly pid: number;
|
|
@@ -168,7 +168,7 @@ export function isRunning(outcome: StartOutcome): outcome is Instance {
|
|
|
168
168
|
return outcome instanceof Instance;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
/** Start one instance, in the order of §8.3.
|
|
171
|
+
/** Start one instance, in the order of DESIGN §8.3.
|
|
172
172
|
*
|
|
173
173
|
* The order is the point of this function: the lock before anything is
|
|
174
174
|
* created, the config before anything is derived from it, the pid before the
|
|
@@ -191,15 +191,15 @@ export async function start(options: StartOptions = {}): Promise<StartOutcome> {
|
|
|
191
191
|
const log = new Log(paths.logFile, options.echoLog ?? true);
|
|
192
192
|
try {
|
|
193
193
|
// 3. the config. A broken one ends the start rather than turning the
|
|
194
|
-
// setting it carried silently off (
|
|
194
|
+
// setting it carried silently off (DR-0004).
|
|
195
195
|
const config = await configFor(paths, log, options.settle ?? true);
|
|
196
196
|
// What the config says of the gateway, resolved before anything is built
|
|
197
197
|
// from it: a webhook source whose secret cannot be read ends the start
|
|
198
|
-
// here, for the same reason a broken config does (
|
|
198
|
+
// here, for the same reason a broken config does (DR-0004).
|
|
199
199
|
const gateway = gatewaySetup(config.upstream, paths.configFile, env);
|
|
200
200
|
// The translation helper, checked the same way and for the same reason: a
|
|
201
201
|
// program that was named and cannot be run is a setting that cannot be
|
|
202
|
-
// honoured (
|
|
202
|
+
// honoured (DR-0004).
|
|
203
203
|
const helper = translateSetup(config.upstream, paths.configFile);
|
|
204
204
|
// 4. this instance's identity, written the first time it is asked for.
|
|
205
205
|
//
|
|
@@ -211,7 +211,7 @@ export async function start(options: StartOptions = {}): Promise<StartOutcome> {
|
|
|
211
211
|
// 5. the mesh, for an instance the data names an address for.
|
|
212
212
|
//
|
|
213
213
|
// Which entry of the list is this instance is its own row, so nothing has
|
|
214
|
-
// to be asked of the network to settle it (§7.1). The WebSocket is still
|
|
214
|
+
// to be asked of the network to settle it (DESIGN §7.1). The WebSocket is still
|
|
215
215
|
// bound here and handed over, because the instance does not exist yet and
|
|
216
216
|
// a peer may dial the moment the address is up.
|
|
217
217
|
const mesh = meshFor(id, config, log, options.meshTiming);
|
|
@@ -246,7 +246,7 @@ export async function start(options: StartOptions = {}): Promise<StartOutcome> {
|
|
|
246
246
|
* the same place: what this instance runs with is a value that checked out.
|
|
247
247
|
* A config that does not hold leaves the applied one standing and is written
|
|
248
248
|
* to the log, because an instance that was serving a session is not something
|
|
249
|
-
* a typo should take down (§8.3).
|
|
249
|
+
* a typo should take down (DESIGN §8.3).
|
|
250
250
|
*
|
|
251
251
|
* A config home nothing states settings for runs the built-in ones, which is
|
|
252
252
|
* the unix socket and no mesh: `daemon run` on a directory nobody registered
|
|
@@ -299,7 +299,7 @@ function meshFor(
|
|
|
299
299
|
*
|
|
300
300
|
* The connection registry is shared rather than copied: a connection accepted
|
|
301
301
|
* during self-identification is one of the instance's, and two registries would
|
|
302
|
-
* mean the stop order (§8.5 step 3) reaching only one of them. */
|
|
302
|
+
* mean the stop order (DESIGN §8.5 step 3) reaching only one of them. */
|
|
303
303
|
export interface MeshWiring {
|
|
304
304
|
readonly conns: ConnRegistry;
|
|
305
305
|
readonly ws: Listener;
|
|
@@ -343,11 +343,11 @@ async function bindForMesh(config: InstanceConfig, mesh: Mesh): Promise<MeshWiri
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
/** One running instance: the layers wired together, and the two lifecycle
|
|
346
|
-
* orders of §8.3 and §8.5. */
|
|
346
|
+
* orders of DESIGN §8.3 and §8.5. */
|
|
347
347
|
export class Instance {
|
|
348
348
|
readonly startedAt: Timestamp = Date.now();
|
|
349
349
|
readonly #conns: ConnRegistry;
|
|
350
|
-
/** The mesh, on an instance configured for one (§7). */
|
|
350
|
+
/** The mesh, on an instance configured for one (DESIGN §7). */
|
|
351
351
|
readonly #mesh: Mesh | undefined;
|
|
352
352
|
/** The WebSocket listener, when it had to be bound before this instance
|
|
353
353
|
* existed so that self-identification could reach it. */
|
|
@@ -372,7 +372,7 @@ export class Instance {
|
|
|
372
372
|
readonly #proxies: readonly Cidr[];
|
|
373
373
|
readonly #handlers: Handlers;
|
|
374
374
|
readonly #capabilities: ReadonlySet<Capability>;
|
|
375
|
-
/** Set the moment shutdown starts, which is the re-entry guard of §8.5 step
|
|
375
|
+
/** Set the moment shutdown starts, which is the re-entry guard of DESIGN §8.5 step
|
|
376
376
|
* 1: a request arriving after it is refused rather than half-served. */
|
|
377
377
|
#stopping = false;
|
|
378
378
|
#stopped: Promise<void> | undefined;
|
|
@@ -425,7 +425,7 @@ export class Instance {
|
|
|
425
425
|
]);
|
|
426
426
|
// The mesh is the rest of the cluster as the topic mechanism sees it: what
|
|
427
427
|
// the peers have stated, and where a local subscription has to travel to
|
|
428
|
-
// (§7.4). An instance without one has no other instance to hear from.
|
|
428
|
+
// (DESIGN §7.4). An instance without one has no other instance to hear from.
|
|
429
429
|
this.#topics = new Topics(this.self, this.#capabilities, this.#mesh);
|
|
430
430
|
this.#mesh?.bind({
|
|
431
431
|
handle: (frame, conn) => this.handle(frame, conn),
|
|
@@ -441,7 +441,7 @@ export class Instance {
|
|
|
441
441
|
if (Array.isArray(stated)) this.#auth.merge(stated);
|
|
442
442
|
},
|
|
443
443
|
// The mesh view is this instance's own, so the topic that carries it is
|
|
444
|
-
// restated when that view moves (§7.5).
|
|
444
|
+
// restated when that view moves (DESIGN §7.5).
|
|
445
445
|
changed: () => {
|
|
446
446
|
this.#instances.refresh();
|
|
447
447
|
this.#linkMoved();
|
|
@@ -449,7 +449,7 @@ export class Instance {
|
|
|
449
449
|
});
|
|
450
450
|
|
|
451
451
|
// What the gateway saw. It feeds two topics and one input of the sessions
|
|
452
|
-
// domain (§
|
|
452
|
+
// domain (DESIGN §4.2), so it is built before both.
|
|
453
453
|
this.#gateway = new Gateway({
|
|
454
454
|
self: this.self,
|
|
455
455
|
setup,
|
|
@@ -480,7 +480,7 @@ export class Instance {
|
|
|
480
480
|
|
|
481
481
|
// The transcript tails and their folds. Built before the sessions domain
|
|
482
482
|
// and reading from it lazily: the fold is one of the sessions domain's
|
|
483
|
-
// inputs (§
|
|
483
|
+
// inputs (DESIGN §4.2) while the path to follow is one of its outputs, and the
|
|
484
484
|
// two meet at the moment a tail starts rather than at construction.
|
|
485
485
|
this.#transcripts = new Transcripts({
|
|
486
486
|
self: this.self,
|
|
@@ -521,7 +521,7 @@ export class Instance {
|
|
|
521
521
|
onChanged: () => {
|
|
522
522
|
this.#status.refresh();
|
|
523
523
|
// A session that is live again is one route (a) can be tried against,
|
|
524
|
-
// which is what the inbox is waiting for (§
|
|
524
|
+
// which is what the inbox is waiting for (DESIGN §6.7).
|
|
525
525
|
void this.#delivery.retry();
|
|
526
526
|
},
|
|
527
527
|
...(pollMs === undefined ? {} : { pollMs }),
|
|
@@ -530,7 +530,7 @@ export class Instance {
|
|
|
530
530
|
// The topics whose value is the fold's error state, over the sessions the
|
|
531
531
|
// instance holds. They are the other thing that keeps a tail running: a
|
|
532
532
|
// subscriber watching the list of stopped sessions is watching every
|
|
533
|
-
// session's fold, and the tails behind it run only while it does (§6.3).
|
|
533
|
+
// session's fold, and the tails behind it run only while it does (DESIGN §6.3).
|
|
534
534
|
this.#status = new SessionStatus({
|
|
535
535
|
self: this.self,
|
|
536
536
|
sessions: () => this.#sessions.connectedSids(),
|
|
@@ -549,9 +549,9 @@ export class Instance {
|
|
|
549
549
|
|
|
550
550
|
const inbox = new Inbox(inboxPath(paths.stateDir));
|
|
551
551
|
inbox.load();
|
|
552
|
-
// Route (a) is the harness's own way in (§
|
|
552
|
+
// Route (a) is the harness's own way in (DESIGN §6.5): Claude Code's messaging
|
|
553
553
|
// socket, Codex's thread queue. Which one an instance speaks follows the
|
|
554
|
-
// config home it answers for (§
|
|
554
|
+
// config home it answers for (DESIGN §4.1), and the flag turns the route off for
|
|
555
555
|
// either.
|
|
556
556
|
this.#direct = !config.direct_delivery
|
|
557
557
|
? new DisabledDirectRoute()
|
|
@@ -597,7 +597,7 @@ export class Instance {
|
|
|
597
597
|
this.#topics.attach("llm.status", this.#gateway.statusResource);
|
|
598
598
|
|
|
599
599
|
// The one thing here that is written down and is nobody's derived value
|
|
600
|
-
// (§
|
|
600
|
+
// (DESIGN §2.5): what a person saved through a client, which no other party holds
|
|
601
601
|
// a copy of. It owns `kv:<ns>` and is the only publisher of it.
|
|
602
602
|
const kv = new KvStore(join(paths.stateDir, KV_DIR), this.self, (topic, data) => {
|
|
603
603
|
this.#topics.publish(topic, data);
|
|
@@ -606,7 +606,7 @@ export class Instance {
|
|
|
606
606
|
|
|
607
607
|
// The credentials, tokens and removals the cluster shares (DR-0001 §2.6).
|
|
608
608
|
// Written down beside the store and for the same reason: none of it is
|
|
609
|
-
// derived from anything else this instance holds (§
|
|
609
|
+
// derived from anything else this instance holds (DESIGN §2.5).
|
|
610
610
|
const records = new AuthRecords({
|
|
611
611
|
dir: recordsDir(paths.stateDir),
|
|
612
612
|
self: this.self,
|
|
@@ -695,7 +695,7 @@ export class Instance {
|
|
|
695
695
|
});
|
|
696
696
|
}
|
|
697
697
|
|
|
698
|
-
/** 7-8 of §8.3: the pid, then the listeners with the unix socket first, then
|
|
698
|
+
/** 7-8 of DESIGN §8.3: the pid, then the listeners with the unix socket first, then
|
|
699
699
|
* the peers. */
|
|
700
700
|
async listen(): Promise<void> {
|
|
701
701
|
// Before any listener: a client that can connect can always find the
|
|
@@ -742,13 +742,13 @@ export class Instance {
|
|
|
742
742
|
this.accepted(conn, info);
|
|
743
743
|
},
|
|
744
744
|
// The gateway posts to the address this instance already serves,
|
|
745
|
-
// behind the same entry check (§
|
|
745
|
+
// behind the same entry check (DESIGN §2.1).
|
|
746
746
|
route: (request, source) => this.route(request, source),
|
|
747
747
|
}),
|
|
748
748
|
);
|
|
749
749
|
}
|
|
750
750
|
// 8. the peers. Every instance dials every one of them, and one that is not
|
|
751
|
-
// there is retried rather than waited for (§7.2).
|
|
751
|
+
// there is retried rather than waited for (DESIGN §7.2).
|
|
752
752
|
this.#mesh?.connect();
|
|
753
753
|
await Promise.resolve();
|
|
754
754
|
this.log.write("started", {
|
|
@@ -810,17 +810,17 @@ export class Instance {
|
|
|
810
810
|
}
|
|
811
811
|
|
|
812
812
|
/** Whether the sessions watch is running. It is driven by subscription
|
|
813
|
-
* (§6.3), so this is how "the upstream watches stopped" is observable from
|
|
813
|
+
* (DESIGN §6.3), so this is how "the upstream watches stopped" is observable from
|
|
814
814
|
* outside the domain that owns them. */
|
|
815
815
|
get watching(): boolean {
|
|
816
816
|
return this.#sessions.watching;
|
|
817
817
|
}
|
|
818
818
|
|
|
819
|
-
/** When the gateway last saw inference for a session (§
|
|
819
|
+
/** When the gateway last saw inference for a session (DESIGN §4.2).
|
|
820
820
|
*
|
|
821
821
|
* The one input of the classification that arrives from outside this host,
|
|
822
822
|
* and the only place it is observable from: it is an attribute of a row
|
|
823
|
-
* rather than a state (§
|
|
823
|
+
* rather than a state (DESIGN §4.3), so nothing on the wire carries it yet. */
|
|
824
824
|
gatewayActiveAt(sid: Sid): Timestamp | undefined {
|
|
825
825
|
return this.#gateway.activeAt(sid);
|
|
826
826
|
}
|
|
@@ -845,7 +845,7 @@ export class Instance {
|
|
|
845
845
|
* the answer is read from: a peer that answers is the link working, and
|
|
846
846
|
* every configured peer silent at once is the link gone. Nothing else is
|
|
847
847
|
* probed — an instance does not dial the internet to have an opinion about
|
|
848
|
-
* it, and the peers are already being dialled for their own reasons (§8.3).
|
|
848
|
+
* it, and the peers are already being dialled for their own reasons (DESIGN §8.3).
|
|
849
849
|
*
|
|
850
850
|
* Two cases state no verdict rather than guessing one. `off` is an instance
|
|
851
851
|
* with no mesh: nothing here watches the link at all. `unknown` is a mesh
|
|
@@ -872,7 +872,7 @@ export class Instance {
|
|
|
872
872
|
for (const conn of this.#conns) conn.send(event);
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
-
/** One frame, from either transport. The re-entry guard of §8.5 step 1 sits
|
|
875
|
+
/** One frame, from either transport. The re-entry guard of DESIGN §8.5 step 1 sits
|
|
876
876
|
* here because this is the single door every request comes through. */
|
|
877
877
|
async handle(frame: unknown, conn: Requester): Promise<DispatchResult> {
|
|
878
878
|
if (this.#stopping) {
|
|
@@ -934,7 +934,7 @@ export class Instance {
|
|
|
934
934
|
});
|
|
935
935
|
if (decided.kind !== "forward") return decided;
|
|
936
936
|
// The op belongs to another instance. Mesh carries it and brings the
|
|
937
|
-
// answer back under the id the caller used (§7.3); without a mesh there is
|
|
937
|
+
// answer back under the id the caller used (DESIGN §7.3); without a mesh there is
|
|
938
938
|
// nothing that can reach it, which the driver names.
|
|
939
939
|
//
|
|
940
940
|
// Who it is forwarded as is stated here rather than copied from the
|
|
@@ -950,7 +950,7 @@ export class Instance {
|
|
|
950
950
|
* The subject is the session an op names, and an op that names none is about
|
|
951
951
|
* this instance and stays here. A session this instance holds is its own
|
|
952
952
|
* whatever the cluster last said; one it does not hold is looked for in the
|
|
953
|
-
* routing table the `peers` topic is (§7.3). */
|
|
953
|
+
* routing table the `peers` topic is (DESIGN §7.3). */
|
|
954
954
|
#owner(fields: Record<string, unknown>): InstanceId | undefined {
|
|
955
955
|
const sid = fields["sid"];
|
|
956
956
|
if (typeof sid !== "string" || this.#mesh === undefined) return undefined;
|
|
@@ -958,7 +958,7 @@ export class Instance {
|
|
|
958
958
|
return this.#mesh.ownerOf(sid as Sid);
|
|
959
959
|
}
|
|
960
960
|
|
|
961
|
-
/** Stop, in the order of §8.5. Repeating it waits for the first one. */
|
|
961
|
+
/** Stop, in the order of DESIGN §8.5. Repeating it waits for the first one. */
|
|
962
962
|
stop(): Promise<void> {
|
|
963
963
|
this.#stopped ??= this.#stop().finally(() => {
|
|
964
964
|
this.#done.resolve();
|
|
@@ -976,7 +976,7 @@ export class Instance {
|
|
|
976
976
|
// 1. refuse new work
|
|
977
977
|
this.#stopping = true;
|
|
978
978
|
// 2. stop the upstream watches. They run only while something is
|
|
979
|
-
// subscribed (§6.3), so dropping the subscriptions is what stops them.
|
|
979
|
+
// subscribed (DESIGN §6.3), so dropping the subscriptions is what stops them.
|
|
980
980
|
for (const conn of this.#conns) this.#topics.dropAll(conn);
|
|
981
981
|
// A tail may also be held for a value this instance states rather than for
|
|
982
982
|
// a subscriber, and those holds end here.
|
|
@@ -988,23 +988,23 @@ export class Instance {
|
|
|
988
988
|
// with it rather than outliving the daemon that has its pipe.
|
|
989
989
|
this.#translate?.stop();
|
|
990
990
|
// Route (a) holds a socket of its own, bound where the sessions' sockets
|
|
991
|
-
// are so their receipts can reach it (§
|
|
991
|
+
// are so their receipts can reach it (DESIGN §6.5). It has a name on disk, so it
|
|
992
992
|
// is taken down here rather than left for the next run to find.
|
|
993
993
|
this.#direct.close();
|
|
994
994
|
// The mesh's links and its timers, let go here for the same reason: they
|
|
995
|
-
// are this instance's and do not outlive it (§7).
|
|
995
|
+
// are this instance's and do not outlive it (DESIGN §7).
|
|
996
996
|
this.#mesh?.stop();
|
|
997
997
|
// 3. tell the connections, while they can still be told
|
|
998
998
|
const restarting: RestartingEvent = { ev: "restarting", instance: this.self };
|
|
999
999
|
for (const conn of this.#conns) conn.send(restarting);
|
|
1000
1000
|
// 4. settle what is persisted. `last_live` and the inbox are written as
|
|
1001
1001
|
// they change rather than at exit, so there is nothing held back to flush;
|
|
1002
|
-
// the log's writer is synchronous for the same reason (§
|
|
1002
|
+
// the log's writer is synchronous for the same reason (DESIGN §2.5).
|
|
1003
1003
|
this.log.write("stopping", { instance: this.self });
|
|
1004
1004
|
// 5. let the resources go, the unix socket last. Closing takes the path
|
|
1005
1005
|
// this process bound, and only that one: the stable address is a symlink
|
|
1006
1006
|
// nothing here touches, because a successor may have already pointed it at
|
|
1007
|
-
// itself (§8.5).
|
|
1007
|
+
// itself (DESIGN §8.5).
|
|
1008
1008
|
try {
|
|
1009
1009
|
await this.#transport.close();
|
|
1010
1010
|
} catch (cause) {
|
|
@@ -1023,7 +1023,7 @@ export class Instance {
|
|
|
1023
1023
|
}
|
|
1024
1024
|
}
|
|
1025
1025
|
|
|
1026
|
-
/** Who may reach the WebSocket at all (§
|
|
1026
|
+
/** Who may reach the WebSocket at all (DESIGN §2.1): an address the operator named.
|
|
1027
1027
|
*
|
|
1028
1028
|
* An empty `source_ips` leaves the addresses to the bind, which for the default
|
|
1029
1029
|
* loopback host is this machine. The `Origin` a request carries is not read:
|
package/src/instance/lock.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "no
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
|
|
5
|
-
/** The right to be the instance for one config home (§8.3 step 2).
|
|
5
|
+
/** The right to be the instance for one config home (DESIGN §8.3 step 2).
|
|
6
6
|
*
|
|
7
|
-
* A handle, not state (§
|
|
7
|
+
* A handle, not state (DESIGN §2.5): it says who is running right now and means
|
|
8
8
|
* nothing once the process is gone. */
|
|
9
9
|
export interface Lock {
|
|
10
10
|
release(): void;
|
package/src/instance/log.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { appendFileSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
/** The instance's log: one writer, and every line on disk before the call
|
|
5
|
-
* returns (§
|
|
5
|
+
* returns (DESIGN §2.5).
|
|
6
6
|
*
|
|
7
7
|
* The reason to read a log is to find out why a process stopped, so the line
|
|
8
8
|
* that matters most is the last one written before it did. A buffered writer
|