@ccmsg/cli 0.11.3 → 0.11.5
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 +10 -10
- 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
|
@@ -26,13 +26,13 @@ import type { DirectRoute } from "./direct.ts";
|
|
|
26
26
|
import type { Inbox } from "./inbox.ts";
|
|
27
27
|
|
|
28
28
|
/** The topic a message reaches its session on, and the only route in use while
|
|
29
|
-
* route (a) waits for confirmation (§
|
|
29
|
+
* route (a) waits for confirmation (DESIGN §6.5). */
|
|
30
30
|
const INBOX = "inbox";
|
|
31
31
|
|
|
32
32
|
/** What delivery reads about a session. Two questions, both answered by the
|
|
33
|
-
* sessions domain from the inputs of §
|
|
33
|
+
* sessions domain from the inputs of DESIGN §4.2: where a session stands, and which
|
|
34
34
|
* sessions are around — neither is asked of anything else, which is what keeps
|
|
35
|
-
* the reasons of §
|
|
35
|
+
* the reasons of DESIGN §6.6 from growing a source per reason. */
|
|
36
36
|
export interface SessionLookup {
|
|
37
37
|
classify(sid: Sid): SessionState | undefined;
|
|
38
38
|
peerRows(): PeerInfo[];
|
|
@@ -43,7 +43,7 @@ export interface SessionLookup {
|
|
|
43
43
|
* `message.send` is a `cluster` op — any instance may be asked — but a message
|
|
44
44
|
* reaches a session through the session's own connections, which are held by
|
|
45
45
|
* the instance it greeted. So the op is answered here by carrying it there
|
|
46
|
-
* (§
|
|
46
|
+
* (DESIGN §2.2 step 6 is about `instance-local` ops; this is the same forwarding for
|
|
47
47
|
* the one op whose subject is elsewhere while its op is not). */
|
|
48
48
|
export interface Cluster {
|
|
49
49
|
/** Which instance holds this session, or nothing when the cluster has not
|
|
@@ -67,20 +67,20 @@ export interface DeliveryDeps {
|
|
|
67
67
|
readonly cluster?: Cluster;
|
|
68
68
|
readonly inbox: Inbox;
|
|
69
69
|
/** Route (a). Off until it is confirmed against a running harness, which is
|
|
70
|
-
* condition 0 of §
|
|
70
|
+
* condition 0 of DESIGN §6.5 and is why this is handed in rather than built here. */
|
|
71
71
|
readonly direct: DirectRoute;
|
|
72
|
-
/** The one way a value reaches subscribers (§6.1), narrowed to the session a
|
|
72
|
+
/** The one way a value reaches subscribers (DESIGN §6.1), narrowed to the session a
|
|
73
73
|
* message is for. */
|
|
74
74
|
readonly publish: (topic: string, data: unknown, instance: InstanceId, to: Sid) => PublishOutcome;
|
|
75
75
|
/** How many of that session's connections are listening on `inbox`. */
|
|
76
76
|
readonly listeners: (topic: string, to: Sid) => number;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** Delivery, and the inbox topic it delivers on (§
|
|
79
|
+
/** Delivery, and the inbox topic it delivers on (DESIGN §6.5-6.8).
|
|
80
80
|
*
|
|
81
|
-
* Two things, as §
|
|
81
|
+
* Two things, as DESIGN §6.5-6.8 splits them: the route a message takes, and what the sender
|
|
82
82
|
* is told when it took none. The second reads the classification and the inbox
|
|
83
|
-
* and nothing else (§
|
|
83
|
+
* and nothing else (DESIGN §6.6) — a reason is a name for a state that was already
|
|
84
84
|
* there, never a state of its own. */
|
|
85
85
|
export class Delivery implements UpstreamResource {
|
|
86
86
|
#counter: number;
|
|
@@ -92,7 +92,7 @@ export class Delivery implements UpstreamResource {
|
|
|
92
92
|
/** The messages an offer has taken responsibility for, per session. They are
|
|
93
93
|
* still in the inbox — an offer that does not reach the end leaves them
|
|
94
94
|
* there — but they are spoken for, so the snapshot below hands them to
|
|
95
|
-
* nobody: one message goes out on one route (§
|
|
95
|
+
* nobody: one message goes out on one route (DESIGN §6.7). */
|
|
96
96
|
readonly #claimed = new Map<Sid, Set<Mid>>();
|
|
97
97
|
|
|
98
98
|
constructor(private readonly deps: DeliveryDeps) {
|
|
@@ -115,14 +115,14 @@ export class Delivery implements UpstreamResource {
|
|
|
115
115
|
const direct = await this.deps.direct.send(to, message);
|
|
116
116
|
if (direct === "delivered") {
|
|
117
117
|
// Route (a) reaching this session is the session being able to receive,
|
|
118
|
-
// which is what the inbox waits for (§
|
|
118
|
+
// which is what the inbox waits for (DESIGN §6.7). Whatever is still held for it
|
|
119
119
|
// is offered now, on the route that just worked.
|
|
120
120
|
await this.#offer(to);
|
|
121
121
|
return { delivered: true };
|
|
122
122
|
}
|
|
123
123
|
if (direct === "refused") {
|
|
124
124
|
// Turned away for now, which is neither delivered nor undeliverable: it
|
|
125
|
-
// waits in the inbox and is offered again (§
|
|
125
|
+
// waits in the inbox and is offered again (DESIGN §6.8).
|
|
126
126
|
this.deps.inbox.hold(to, message);
|
|
127
127
|
return { delivered: false, reason: "throttled" };
|
|
128
128
|
}
|
|
@@ -133,7 +133,7 @@ export class Delivery implements UpstreamResource {
|
|
|
133
133
|
}
|
|
134
134
|
// The session is listening but is behind on what it has already been
|
|
135
135
|
// offered, which is the same standing as route (a) turning the message
|
|
136
|
-
// away: it waits in the inbox and is offered again (§
|
|
136
|
+
// away: it waits in the inbox and is offered again (DESIGN §6.8).
|
|
137
137
|
this.deps.inbox.hold(to, message);
|
|
138
138
|
return { delivered: false, reason: "throttled" };
|
|
139
139
|
}
|
|
@@ -146,11 +146,11 @@ export class Delivery implements UpstreamResource {
|
|
|
146
146
|
* or named as one the cluster cannot answer for right now.
|
|
147
147
|
*
|
|
148
148
|
* Nothing when the cluster has no such session anywhere and every instance
|
|
149
|
-
* could be asked — which is the only case `session_not_found` covers (§
|
|
149
|
+
* could be asked — which is the only case `session_not_found` covers (DESIGN §6.6).
|
|
150
150
|
* While an instance is out of reach the sid may well be its, so the sender is
|
|
151
151
|
* told the reason rather than that the session does not exist. The message is
|
|
152
152
|
* not held here either: the inbox that would offer it again is the one on the
|
|
153
|
-
* instance that owns the session (§
|
|
153
|
+
* instance that owns the session (DESIGN §6.7). */
|
|
154
154
|
async #elsewhere(to: Sid, input: HandlerInput): Promise<MessageSendResult | undefined> {
|
|
155
155
|
const cluster = this.deps.cluster;
|
|
156
156
|
if (cluster === undefined) return undefined;
|
|
@@ -162,7 +162,7 @@ export class Delivery implements UpstreamResource {
|
|
|
162
162
|
}
|
|
163
163
|
// The sender, as the owning instance will run the op as: the identity the
|
|
164
164
|
// connection greeted with, which is the same thing `message.send` reads to
|
|
165
|
-
// decide who a message is from (§
|
|
165
|
+
// decide who a message is from (DESIGN §6.5).
|
|
166
166
|
const answer = await cluster.forward(owner, input.args, callerOf(input));
|
|
167
167
|
if (answer.kind === "reply") {
|
|
168
168
|
const { ok: _ok, request_id: _id, ...body } = answer.response;
|
|
@@ -197,7 +197,7 @@ export class Delivery implements UpstreamResource {
|
|
|
197
197
|
/** Hand a session what it is owed, oldest first, over route (a).
|
|
198
198
|
*
|
|
199
199
|
* Stops at the first message the route does not carry, whatever it answered:
|
|
200
|
-
* a refusal means the session is taking nothing more for now (§
|
|
200
|
+
* a refusal means the session is taking nothing more for now (DESIGN §6.8), and an
|
|
201
201
|
* unavailable route means route (b) is the one that applies — either way the
|
|
202
202
|
* rest stay held, in order, for the next time this session becomes able to
|
|
203
203
|
* receive. */
|
|
@@ -223,7 +223,7 @@ export class Delivery implements UpstreamResource {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
// --- UpstreamResource (§6.3)
|
|
226
|
+
// --- UpstreamResource (DESIGN §6.3)
|
|
227
227
|
|
|
228
228
|
/** Nothing upstream to run: what is undelivered is already in hand, and the
|
|
229
229
|
* messages that arrive later come through `send`. */
|
|
@@ -232,12 +232,12 @@ export class Delivery implements UpstreamResource {
|
|
|
232
232
|
stop(): void {}
|
|
233
233
|
|
|
234
234
|
/** The current value of `inbox` for whoever subscribed: everything still
|
|
235
|
-
* undelivered for that session (§6.2, element granularity — the snapshot is
|
|
235
|
+
* undelivered for that session (DESIGN §6.2, element granularity — the snapshot is
|
|
236
236
|
* every element, a later frame is one).
|
|
237
237
|
*
|
|
238
238
|
* Subscribing is receiving, so the snapshot empties the inbox: the frame is
|
|
239
239
|
* queued on the connection before this returns, and a message the session has
|
|
240
|
-
* been handed is not one that is still waiting for it (§
|
|
240
|
+
* been handed is not one that is still waiting for it (DESIGN §6.7). A connection
|
|
241
241
|
* with no session — a person watching — is handed nothing, because the topic
|
|
242
242
|
* carries what was said to a session and they are not one.
|
|
243
243
|
*
|
|
@@ -260,7 +260,7 @@ export class Delivery implements UpstreamResource {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/** The reason a message is waiting, named from the classification alone
|
|
263
|
-
* (§
|
|
263
|
+
* (DESIGN §6.6). `preparing` is the live session with nowhere to put it: it is there,
|
|
264
264
|
* route (a) did not carry it, and nothing of its is listening yet.
|
|
265
265
|
*
|
|
266
266
|
* `instance_unreachable` is not here: it is the mesh's answer about an
|
|
@@ -280,7 +280,7 @@ export class Delivery implements UpstreamResource {
|
|
|
280
280
|
|
|
281
281
|
/** The answer for a message that went to the inbox. Candidates ride along
|
|
282
282
|
* when the addressee is gone, since that is when sending somewhere else is
|
|
283
|
-
* the sender's next move (§
|
|
283
|
+
* the sender's next move (DESIGN §6.6). */
|
|
284
284
|
#undelivered(to: Sid, reason: UndeliveredReason): MessageSendResult {
|
|
285
285
|
if (reason !== "paused" && reason !== "disappeared") return { delivered: false, reason };
|
|
286
286
|
const candidates = this.#candidates(to);
|
|
@@ -289,7 +289,7 @@ export class Delivery implements UpstreamResource {
|
|
|
289
289
|
: { delivered: false, reason, candidates };
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
/** Sessions live now in the repository the addressee belongs to (§
|
|
292
|
+
/** Sessions live now in the repository the addressee belongs to (DESIGN §6.6).
|
|
293
293
|
*
|
|
294
294
|
* The rows are one list of sessions, connected and lost alike, so which of
|
|
295
295
|
* them can be written to is the classification — asked of the domain, the
|
|
@@ -319,7 +319,7 @@ export class Delivery implements UpstreamResource {
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
/** Who the message is from: the identity the connection greeted as, never
|
|
322
|
-
* anything the caller put in the arguments (§
|
|
322
|
+
* anything the caller put in the arguments (DESIGN §6.5).
|
|
323
323
|
*
|
|
324
324
|
* A session names itself with its sid. A person greets without one, which is
|
|
325
325
|
* what the sender literal stands for — spelled out so a reader tells "a
|
package/src/messaging/direct.ts
CHANGED
|
@@ -5,30 +5,30 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { type InboxMessage, renderDirectDelivery, type Sid } from "@ccmsg/protocol";
|
|
6
6
|
import { HARNESS, HARNESSES } from "../harness/index.ts";
|
|
7
7
|
|
|
8
|
-
/** What route (a) answered (§
|
|
8
|
+
/** What route (a) answered (DESIGN §6.5).
|
|
9
9
|
*
|
|
10
10
|
* `unavailable` is every way the route does not apply — the flag is off, the
|
|
11
11
|
* harness names no messaging socket, the generation is one we do not speak, the
|
|
12
|
-
* key cannot be read, the acknowledgement did not come. §
|
|
12
|
+
* key cannot be read, the acknowledgement did not come. DESIGN §6.5 gives them one
|
|
13
13
|
* outcome on purpose: the route either carried the message or it did not, and
|
|
14
14
|
* route (b) is tried the same way in each case.
|
|
15
15
|
*
|
|
16
16
|
* `refused` is separate because it is not "the route does not apply": the
|
|
17
17
|
* session is there and turned this message away for now, which is the one
|
|
18
|
-
* outcome that reaches the sender as its own reason (§
|
|
18
|
+
* outcome that reaches the sender as its own reason (DESIGN §6.8). */
|
|
19
19
|
export type DirectOutcome = "delivered" | "unavailable" | "refused";
|
|
20
20
|
|
|
21
21
|
/** Route (a): the harness's own messaging socket. */
|
|
22
22
|
export interface DirectRoute {
|
|
23
23
|
send(sid: Sid, message: InboxMessage): Promise<DirectOutcome>;
|
|
24
24
|
/** Let go of what the route holds open. The status inbox below is a bound
|
|
25
|
-
* socket with a name on disk, and it leaves when the instance does (§8.5). */
|
|
25
|
+
* socket with a name on disk, and it leaves when the instance does (DESIGN §8.5). */
|
|
26
26
|
close(): void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** Route (a) turned off by config (§
|
|
29
|
+
/** Route (a) turned off by config (DESIGN §6.5 condition 0). Delivery is unchanged by
|
|
30
30
|
* this: route (b) is the fallback, and a fallback that always runs is still the
|
|
31
|
-
* same semantics (§
|
|
31
|
+
* same semantics (DESIGN §6.5). */
|
|
32
32
|
export class DisabledDirectRoute implements DirectRoute {
|
|
33
33
|
send(): Promise<DirectOutcome> {
|
|
34
34
|
return Promise.resolve("unavailable");
|
|
@@ -39,7 +39,7 @@ export class DisabledDirectRoute implements DirectRoute {
|
|
|
39
39
|
|
|
40
40
|
/** The `peerProtocol` generation this speaks. One value, because one is what
|
|
41
41
|
* has been read off a running harness (2.1.263); any other generation is a
|
|
42
|
-
* protocol nobody here has seen, which is condition 1 of §
|
|
42
|
+
* protocol nobody here has seen, which is condition 1 of DESIGN §6.5. */
|
|
43
43
|
export const PEER_PROTOCOL = 1;
|
|
44
44
|
|
|
45
45
|
/** How long one attempt has to reach the point where the harness holds our
|
|
@@ -52,7 +52,7 @@ export const PEER_PROTOCOL = 1;
|
|
|
52
52
|
export const DIRECT_ACK_MS = 2_000;
|
|
53
53
|
|
|
54
54
|
/** How long the status inbox is watched for word about this message before the
|
|
55
|
-
* send is taken to have landed (§
|
|
55
|
+
* send is taken to have landed (DESIGN §6.5 condition 3).
|
|
56
56
|
*
|
|
57
57
|
* Provisional. What is known from the harness (2.1.263) is where the receipt
|
|
58
58
|
* is raised, not how long it takes to arrive: the receiving session decides a
|
|
@@ -74,11 +74,11 @@ export const DIRECT_STATUS_MS = 250;
|
|
|
74
74
|
*
|
|
75
75
|
* `held` is among them because a parked message is not delivered yet: it waits
|
|
76
76
|
* on somebody's approval there, which is the same "there, and not taking it
|
|
77
|
-
* now" that §
|
|
77
|
+
* now" that DESIGN §6.8 keeps in our inbox and offers again. */
|
|
78
78
|
const REFUSING = new Set(["refused", "denied", "dropped", "expired", "held"]);
|
|
79
79
|
|
|
80
80
|
/** The socket this daemon offers so the receiving session can say what became
|
|
81
|
-
* of a message (§
|
|
81
|
+
* of a message (DESIGN §6.5 condition 3).
|
|
82
82
|
*
|
|
83
83
|
* It lives in the directory the target's own socket is in, and not in this
|
|
84
84
|
* instance's state directory, because the receiving harness vets the address it
|
|
@@ -213,7 +213,7 @@ export interface SocketRouteOptions {
|
|
|
213
213
|
readonly statusMs?: number;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
/** Route (a) against the harness's messaging socket (§
|
|
216
|
+
/** Route (a) against the harness's messaging socket (DESIGN §6.5).
|
|
217
217
|
*
|
|
218
218
|
* The path is `sessions/<pid>.json` of this instance's own config home, which
|
|
219
219
|
* is also the answer to condition 2: a key beside it that this uid can read is
|
|
@@ -222,7 +222,7 @@ export interface SocketRouteOptions {
|
|
|
222
222
|
* instance cannot see a state file for is simply not reachable this way.
|
|
223
223
|
*
|
|
224
224
|
* The directory is read per send rather than taken from the sessions domain's
|
|
225
|
-
* watch: that watch runs only while a topic is subscribed (§6.3), and route (a)
|
|
225
|
+
* watch: that watch runs only while a topic is subscribed (DESIGN §6.3), and route (a)
|
|
226
226
|
* exists precisely for the session that subscribes to nothing. */
|
|
227
227
|
export class ClaudeCodeSocketRoute implements DirectRoute {
|
|
228
228
|
readonly #sessionsDir: string;
|
|
@@ -243,7 +243,7 @@ export class ClaudeCodeSocketRoute implements DirectRoute {
|
|
|
243
243
|
*
|
|
244
244
|
* The message is written, and then the receipt channel is watched for word
|
|
245
245
|
* about it. What can arrive is a session saying it did not take the message
|
|
246
|
-
* (§
|
|
246
|
+
* (DESIGN §6.8); what cannot is a session saying it did, because none is sent for
|
|
247
247
|
* the ordinary case. So the outcome is refusal if it says so in time, and
|
|
248
248
|
* delivery if it says nothing — which is the same shape as the acknowledged
|
|
249
249
|
* send it stands in for, decided on a channel that carries the refusals
|
|
@@ -281,7 +281,7 @@ export class ClaudeCodeSocketRoute implements DirectRoute {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
/** The state file naming this session, if it names a socket of a generation
|
|
284
|
-
* we speak (§
|
|
284
|
+
* we speak (DESIGN §6.5 conditions 1). */
|
|
285
285
|
async #target(sid: Sid): Promise<HarnessTarget | undefined> {
|
|
286
286
|
let names: string[];
|
|
287
287
|
try {
|
|
@@ -303,7 +303,7 @@ export class ClaudeCodeSocketRoute implements DirectRoute {
|
|
|
303
303
|
return undefined;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
/** The `peerToken` the harness wrote for this session (§
|
|
306
|
+
/** The `peerToken` the harness wrote for this session (DESIGN §6.5 condition 2).
|
|
307
307
|
*
|
|
308
308
|
* Found by the pid the key is named after rather than by rebuilding the rest
|
|
309
309
|
* of the name: the digest in `<pid>.<digest>.key` is stated to be over the
|
|
@@ -341,7 +341,7 @@ type Env = Record<string, string>;
|
|
|
341
341
|
* Passed through, they would tell the Codex CLI about a config home this
|
|
342
342
|
* instance is not about and a session that is not the one being written to.
|
|
343
343
|
* The home this route means is named explicitly, and the rest is dropped
|
|
344
|
-
* (§
|
|
344
|
+
* (DESIGN §4.1). */
|
|
345
345
|
const DROPPED = HARNESSES.filter((harness) => harness !== "codex").flatMap((harness) => [
|
|
346
346
|
HARNESS[harness].homeEnv,
|
|
347
347
|
...HARNESS[harness].sessionEnv,
|
|
@@ -360,7 +360,7 @@ const DROPPED = HARNESSES.filter((harness) => harness !== "codex").flatMap((harn
|
|
|
360
360
|
* The budget is here for what is not being predicted: a child that never
|
|
361
361
|
* answers would hold `message.send` open for as long as it lived, and route
|
|
362
362
|
* (b) exists exactly so a route that does not come through costs a message
|
|
363
|
-
* nothing (§
|
|
363
|
+
* nothing (DESIGN §6.5). It is generous next to a call that has been measured to
|
|
364
364
|
* return at once. */
|
|
365
365
|
export const QUEUE_MS = 10_000;
|
|
366
366
|
|
|
@@ -380,7 +380,7 @@ const runCodex: RunCodex = async (args, env) => {
|
|
|
380
380
|
});
|
|
381
381
|
} catch {
|
|
382
382
|
// No `codex` on `PATH`, which is the same as the route not applying: the
|
|
383
|
-
// message goes by route (b) and nothing about it is lost (§
|
|
383
|
+
// message goes by route (b) and nothing about it is lost (DESIGN §6.5).
|
|
384
384
|
return { code: 127 };
|
|
385
385
|
}
|
|
386
386
|
// The timer is held so it can be cleared: a send that answered in a
|
|
@@ -408,7 +408,7 @@ export interface QueueRouteOptions {
|
|
|
408
408
|
readonly run?: RunCodex;
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
/** Route (a) against a Codex thread's queue (§
|
|
411
|
+
/** Route (a) against a Codex thread's queue (DESIGN §6.5).
|
|
412
412
|
*
|
|
413
413
|
* Codex has no socket a message can be written to: what it has is a queue per
|
|
414
414
|
* thread, held by the app-server the thread belongs to, and `codex queue` is
|
|
@@ -460,7 +460,7 @@ export class CodexQueueRoute implements DirectRoute {
|
|
|
460
460
|
* into a message nobody receives rather than one the wrong session does.
|
|
461
461
|
*
|
|
462
462
|
* `from` is the address of our own status inbox, and is fixed by ccmsg rather
|
|
463
|
-
* than taken from the caller (§
|
|
463
|
+
* than taken from the caller (DESIGN §6.5). It is what the receiving session answers
|
|
464
464
|
* to about this message, and the message's `mid` is what it answers about — so
|
|
465
465
|
* the two travel together, and a route with no inbox to offer sends neither
|
|
466
466
|
* rather than naming an address nothing is listening on. */
|
|
@@ -476,7 +476,7 @@ function frames(sid: Sid, token: string, message: InboxMessage, from?: string):
|
|
|
476
476
|
return `${JSON.stringify(auth)}\n${JSON.stringify(user)}\n`;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
/** Connect and write, and answer whether the harness holds our bytes (§
|
|
479
|
+
/** Connect and write, and answer whether the harness holds our bytes (DESIGN §6.5
|
|
480
480
|
* condition 3).
|
|
481
481
|
*
|
|
482
482
|
* That is the whole of what this can decide. The connection carries nothing
|
|
@@ -520,7 +520,7 @@ async function write(path: string, payload: string, ackMs: number): Promise<Dire
|
|
|
520
520
|
});
|
|
521
521
|
} catch {
|
|
522
522
|
// No socket at the path, or nothing listening on it: the session ended and
|
|
523
|
-
// took its socket with it, or never had one (§
|
|
523
|
+
// took its socket with it, or never had one (DESIGN §6.5 condition 1).
|
|
524
524
|
return "unavailable";
|
|
525
525
|
}
|
|
526
526
|
|
|
@@ -543,7 +543,7 @@ async function readJson(path: string): Promise<Record<string, unknown> | undefin
|
|
|
543
543
|
return document as Record<string, unknown>;
|
|
544
544
|
} catch {
|
|
545
545
|
// Missing, unreadable by this uid, or half written — all of them are
|
|
546
|
-
// "route (a) does not apply here" (§
|
|
546
|
+
// "route (a) does not apply here" (DESIGN §6.5 conditions 1 and 2).
|
|
547
547
|
return undefined;
|
|
548
548
|
}
|
|
549
549
|
}
|
package/src/messaging/inbox.ts
CHANGED
|
@@ -33,13 +33,13 @@ type Record_ =
|
|
|
33
33
|
|
|
34
34
|
/** What was said to a session and has not reached it.
|
|
35
35
|
*
|
|
36
|
-
* The one thing here that nothing else can reconstruct (§
|
|
36
|
+
* The one thing here that nothing else can reconstruct (DESIGN §2.5): the sender's
|
|
37
37
|
* `message.send` has already been answered, no transcript holds a message that
|
|
38
38
|
* was never handed over, and the text lives nowhere else. Losing this file
|
|
39
39
|
* loses the words.
|
|
40
40
|
*
|
|
41
41
|
* One file rather than one per sid. Both are append-only and both mean the same
|
|
42
|
-
* thing for removal and expiry (§
|
|
42
|
+
* thing for removal and expiry (DESIGN §6.7); a single file makes the write path one
|
|
43
43
|
* open handle and makes "what is undelivered right now" one replay. */
|
|
44
44
|
export class Inbox {
|
|
45
45
|
readonly #held = new Map<Sid, InboxMessage[]>();
|
|
@@ -100,7 +100,7 @@ export class Inbox {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** Note that messages reached their session, which is what takes them out of
|
|
103
|
-
* the inbox (§
|
|
103
|
+
* the inbox (DESIGN §6.7). */
|
|
104
104
|
delivered(sid: Sid, mids: readonly string[]): void {
|
|
105
105
|
const held = this.#held.get(sid);
|
|
106
106
|
if (held === undefined || mids.length === 0) return;
|
|
@@ -112,7 +112,7 @@ export class Inbox {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/** Every session something is waiting for. What reads it is the offer of
|
|
115
|
-
* §
|
|
115
|
+
* DESIGN §6.7: when a session becomes able to receive, what it is owed has to be
|
|
116
116
|
* findable without asking about each sid in turn. */
|
|
117
117
|
sids(): Sid[] {
|
|
118
118
|
return [...this.#held.keys()];
|
|
@@ -146,7 +146,7 @@ export class Inbox {
|
|
|
146
146
|
else this.#held.set(record.sid, left);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/** Drop what is past the window the contract sets (
|
|
149
|
+
/** Drop what is past the window the contract sets (DR-0008). Nothing is
|
|
150
150
|
* appended for an expiry: the same clock reaches the same verdict on the next
|
|
151
151
|
* replay, so writing it down would record a conclusion rather than an event. */
|
|
152
152
|
#expire(now: Timestamp, only?: Sid): void {
|
|
@@ -185,7 +185,7 @@ export class Inbox {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
/** Where the inbox lives for an instance whose state directory is `stateDir`
|
|
188
|
-
* (§8.1: every per-instance path is derived from its config home). */
|
|
188
|
+
* (DESIGN §8.1: every per-instance path is derived from its config home). */
|
|
189
189
|
export function inboxPath(stateDir: string): string {
|
|
190
190
|
return join(stateDir, INBOX_FILE);
|
|
191
191
|
}
|
package/src/messaging/notify.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface NotifyDeps {
|
|
|
21
21
|
/** How a session is shown. The contract has the issuing instance resolve it,
|
|
22
22
|
* so the label is decided here rather than carried in the arguments. */
|
|
23
23
|
readonly label: (sid: Sid) => string;
|
|
24
|
-
/** The one way a value reaches subscribers (§6.1). No `to`: a notification is
|
|
24
|
+
/** The one way a value reaches subscribers (DESIGN §6.1). No `to`: a notification is
|
|
25
25
|
* for whoever is watching, not for one session. */
|
|
26
26
|
readonly publish: (topic: string, data: unknown, instance: InstanceId) => PublishOutcome;
|
|
27
27
|
}
|
|
@@ -34,7 +34,7 @@ export interface NotifyDeps {
|
|
|
34
34
|
* as the same frame, which is what keeps "a notification" from meaning two
|
|
35
35
|
* shapes depending on which op raised it.
|
|
36
36
|
*
|
|
37
|
-
* The topic is `event` granularity (§6.2): nothing is held, so there is no
|
|
37
|
+
* The topic is `event` granularity (DESIGN §6.2): nothing is held, so there is no
|
|
38
38
|
* snapshot and no suppression — two identical notifications are two things
|
|
39
39
|
* that happened. */
|
|
40
40
|
export class Notify implements UpstreamResource {
|
|
@@ -79,7 +79,7 @@ export class Notify implements UpstreamResource {
|
|
|
79
79
|
return [...this.#unread];
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// --- UpstreamResource (§6.3)
|
|
82
|
+
// --- UpstreamResource (DESIGN §6.3)
|
|
83
83
|
|
|
84
84
|
/** Nothing upstream to run: a notification exists because an op raised it. */
|
|
85
85
|
start(): void {}
|
|
@@ -87,7 +87,7 @@ export class Notify implements UpstreamResource {
|
|
|
87
87
|
stop(): void {}
|
|
88
88
|
|
|
89
89
|
/** An event topic has no current value, so a subscriber starts at the next
|
|
90
|
-
* thing that happens (§6.2). */
|
|
90
|
+
* thing that happens (DESIGN §6.2). */
|
|
91
91
|
snapshot(): readonly TopicValue[] {
|
|
92
92
|
return [];
|
|
93
93
|
}
|
|
@@ -102,7 +102,7 @@ export class Notify implements UpstreamResource {
|
|
|
102
102
|
// A notification is an occurrence, so nothing folds it away and a watcher
|
|
103
103
|
// that cannot keep up is what stops it. The caller hears that rather than
|
|
104
104
|
// the notification going nowhere: it is the one that decides whether to
|
|
105
|
-
// raise another (§6.4).
|
|
105
|
+
// raise another (DESIGN §6.4).
|
|
106
106
|
if (this.deps.publish(NOTIFY, notification, this.deps.self) === "rate_limited") {
|
|
107
107
|
throw new OpError(
|
|
108
108
|
"rate_limited",
|
package/src/plugin/codex.ts
CHANGED
|
@@ -86,7 +86,7 @@ const SKILL_FILE = join("skills", "ccmsg", "SKILL.md");
|
|
|
86
86
|
* started against the default home has no variable saying so, and a Codex
|
|
87
87
|
* session started from inside a Claude Code session inherits that session's
|
|
88
88
|
* `CLAUDE_CONFIG_DIR` and session id — so a hook that only added its own would
|
|
89
|
-
* still greet the other instance, as the other session (§
|
|
89
|
+
* still greet the other instance, as the other session (DESIGN §4.1, measured). What
|
|
90
90
|
* is dropped is named here rather than left to the shell: the hook has to
|
|
91
91
|
* speak for the session it fired for.
|
|
92
92
|
*
|
package/src/sessions/classify.ts
CHANGED
|
@@ -2,23 +2,23 @@ import type { SessionState, Timestamp } from "@ccmsg/protocol";
|
|
|
2
2
|
|
|
3
3
|
/** What the harness's own row says, for a session that has one. */
|
|
4
4
|
export interface HarnessPresence {
|
|
5
|
-
/** Its status is `waiting`, so a dialog is open (§
|
|
5
|
+
/** Its status is `waiting`, so a dialog is open (DESIGN §4.2). */
|
|
6
6
|
waiting: boolean;
|
|
7
7
|
/** The terminal it runs in, when one could be read. Absent means unknown,
|
|
8
8
|
* which is what makes a live session unmanaged. */
|
|
9
9
|
terminal_id?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/** Everything the classification reads, and nothing else (§
|
|
12
|
+
/** Everything the classification reads, and nothing else (DESIGN §4.2). */
|
|
13
13
|
export interface SessionInputs {
|
|
14
14
|
/** A connection of this session is open to us right now. */
|
|
15
15
|
connected: boolean;
|
|
16
16
|
/** Present when the harness's `sessions/` has a row for it. */
|
|
17
17
|
harness?: HarnessPresence;
|
|
18
|
-
/** The last time the gateway saw inference for it (§
|
|
18
|
+
/** The last time the gateway saw inference for it (DESIGN §4.2). Absent on an
|
|
19
19
|
* instance with no gateway configured, and for a session it has not seen. */
|
|
20
20
|
gateway_active_at?: Timestamp;
|
|
21
|
-
/** Its transcript's last turn ended on an API error (§
|
|
21
|
+
/** Its transcript's last turn ended on an API error (DESIGN §4.2, from the fold).
|
|
22
22
|
* Absent for a session whose transcript nothing is following. */
|
|
23
23
|
api_error_stopped?: boolean;
|
|
24
24
|
/** Present when it is in `last_live`. */
|
|
@@ -30,12 +30,12 @@ export interface SessionInputs {
|
|
|
30
30
|
export const GATEWAY_LIVE_WINDOW_MS = 5 * 60 * 1000;
|
|
31
31
|
|
|
32
32
|
/** The classification is the contract's `SessionState`, derived here rather
|
|
33
|
-
* than by whoever displays it (§
|
|
33
|
+
* than by whoever displays it (DESIGN §4.3): a client combining raw values of its own
|
|
34
34
|
* would read two instances' lists by two rules.
|
|
35
35
|
*
|
|
36
36
|
* Pinned is not one of them. A person pins a row and the instance holds the
|
|
37
37
|
* mark beside the classification, but the mark never decides which state the
|
|
38
|
-
* row is in (§
|
|
38
|
+
* row is in (DESIGN §4.3).
|
|
39
39
|
*
|
|
40
40
|
* Busy and idle are not among them either, and not by omission: a live session
|
|
41
41
|
* carries how busy it is as an attribute of its row, so an instance with no
|
package/src/sessions/dump.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type { TranscriptFiles } from "../transcript/index.ts";
|
|
|
21
21
|
|
|
22
22
|
/** Where dumps land: one directory under this instance's own state, named
|
|
23
23
|
* after the config home it answers for like every other per-instance path
|
|
24
|
-
* (§8.1). The caller never supplies a path, so there is none to contain. */
|
|
24
|
+
* (DESIGN §8.1). The caller never supplies a path, so there is none to contain. */
|
|
25
25
|
export const DUMPS = "dumps";
|
|
26
26
|
|
|
27
27
|
/** What a dump file is called. Two extensions rather than one so that a reader
|
package/src/sessions/handlers.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface SessionOpsDeps {
|
|
|
62
62
|
* instance last saw them. The sessions domain owns the list; this op only
|
|
63
63
|
* asks it to forget a row. */
|
|
64
64
|
readonly forget: (sid: Sid) => boolean;
|
|
65
|
-
/** The named selections this instance is configured with (§
|
|
65
|
+
/** The named selections this instance is configured with (DESIGN §2.5). */
|
|
66
66
|
readonly presets: readonly DumpPreset[];
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -138,7 +138,7 @@ export function sessionHandlers(deps: SessionOpsDeps) {
|
|
|
138
138
|
"transcript.read": (input: HandlerInput): TranscriptReadResult => {
|
|
139
139
|
const args = input.args as unknown as TranscriptReadArgs;
|
|
140
140
|
if (!sees(args.sid, viewer(input))) {
|
|
141
|
-
// The role sets the visible range, not the permission (§
|
|
141
|
+
// The role sets the visible range, not the permission (DESIGN §2.2): outside
|
|
142
142
|
// it there is no transcript to speak of, which is the one code this op
|
|
143
143
|
// declares. A refusal that named the session would answer a question
|
|
144
144
|
// the caller was not entitled to ask.
|
package/src/sessions/harness.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Harness } from "../harness/index.ts";
|
|
|
7
7
|
* an answer, alongside a `waitingFor` naming what it waits on.
|
|
8
8
|
*
|
|
9
9
|
* Read out of the harness binary (2.1.263): `{status:"waiting",waitingFor:…}`.
|
|
10
|
-
* This is the one thing the raw status decides (§
|
|
10
|
+
* This is the one thing the raw status decides (DESIGN §4.2 / DR-0009) — busy and idle
|
|
11
11
|
* are the gateway's to say, so no other value of it is read here. */
|
|
12
12
|
const WAITING = "waiting";
|
|
13
13
|
|
|
@@ -16,9 +16,9 @@ const WAITING = "waiting";
|
|
|
16
16
|
* `fs.watch` is the route; this is not. macOS/Bun delivers FSEvents tens of
|
|
17
17
|
* seconds late under load (measured in the old daemon while many test children
|
|
18
18
|
* ran), and the poll exists so a change the watch is sitting on is picked up
|
|
19
|
-
* before a person notices it is missing (§
|
|
19
|
+
* before a person notices it is missing (DESIGN §4.2). Five seconds is the interval
|
|
20
20
|
* the old daemon's `claude agents` poller ran at as its only route, and this
|
|
21
|
-
* one replaces it as a backstop (
|
|
21
|
+
* one replaces it as a backstop (DR-0009), so it cannot be the slower of the
|
|
22
22
|
* two. */
|
|
23
23
|
export const CONFIRM_POLL_MS = 5_000;
|
|
24
24
|
|
|
@@ -32,11 +32,11 @@ const STATE_FILE = /^\d+\.json$/;
|
|
|
32
32
|
* states and what the `agents` topic is; Codex says only that a thread has a
|
|
33
33
|
* live writer, which answers "is it there" and nothing else. So `rows` is what
|
|
34
34
|
* can be reported and `present` is what the classification reads, and a harness
|
|
35
|
-
* that reports nothing still has its sessions classified (§
|
|
35
|
+
* that reports nothing still has its sessions classified (DESIGN §4.2). */
|
|
36
36
|
export interface OwnSessions {
|
|
37
37
|
readonly running: boolean;
|
|
38
38
|
/** Begins watching. Called when the first subscriber arrives and not before
|
|
39
|
-
* (§6.3 / §8.3: no upstream is read until somebody is listening). */
|
|
39
|
+
* (DESIGN §6.3 / §8.3: no upstream is read until somebody is listening). */
|
|
40
40
|
start(): void;
|
|
41
41
|
stop(): void;
|
|
42
42
|
/** The harness's own rows, as `agents` answers with them. Empty for a
|
|
@@ -46,7 +46,7 @@ export interface OwnSessions {
|
|
|
46
46
|
present(): ReadonlySet<Sid>;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** The one this config home runs (§
|
|
49
|
+
/** The one this config home runs (DESIGN §4.1). */
|
|
50
50
|
export function ownSessions(
|
|
51
51
|
harness: Harness,
|
|
52
52
|
configHome: string,
|
|
@@ -117,11 +117,11 @@ class CodexThreads implements OwnSessions {
|
|
|
117
117
|
/** The sessions the harness itself reports, read from one config home.
|
|
118
118
|
*
|
|
119
119
|
* The directory is the whole input: it says which sessions exist and which is
|
|
120
|
-
* waiting on a dialog (§
|
|
120
|
+
* waiting on a dialog (DESIGN §4.2). Only the config home this instance was given is
|
|
121
121
|
* ever opened (M6) — the path is handed in, and nothing here searches for
|
|
122
122
|
* another one.
|
|
123
123
|
*
|
|
124
|
-
* Two things live here, and §6.3 separates them. Reading the directory answers
|
|
124
|
+
* Two things live here, and DESIGN §6.3 separates them. Reading the directory answers
|
|
125
125
|
* a question, and is done whenever one is asked. Watching it says the answer
|
|
126
126
|
* may have changed, which is only worth knowing while somebody is subscribed —
|
|
127
127
|
* so the watch is what the subscription drives, and no answer waits on it. */
|
|
@@ -163,7 +163,7 @@ export class HarnessSessions implements OwnSessions {
|
|
|
163
163
|
/** The directory as it is at this instant.
|
|
164
164
|
*
|
|
165
165
|
* Every answer comes from here rather than from anything the watch left
|
|
166
|
-
* behind. Which sessions exist is an input to the classification (§
|
|
166
|
+
* behind. Which sessions exist is an input to the classification (DESIGN §4.2), and
|
|
167
167
|
* classifying happens inside `message.send`'s decision and inside the
|
|
168
168
|
* recompute that writes `last_live` — neither of which can hand back a
|
|
169
169
|
* promise without changing what it means, and neither of which may depend on
|
|
@@ -209,7 +209,7 @@ export class HarnessSessions implements OwnSessions {
|
|
|
209
209
|
/** One directory that says what the harness's sessions are, watched while
|
|
210
210
|
* somebody is subscribed and read whenever an answer is wanted.
|
|
211
211
|
*
|
|
212
|
-
* The two things §6.3 separates live here. Reading the directory answers a
|
|
212
|
+
* The two things DESIGN §6.3 separates live here. Reading the directory answers a
|
|
213
213
|
* question, and is done whenever one is asked. Watching it says the answer may
|
|
214
214
|
* have changed, which is only worth knowing while somebody is listening — so
|
|
215
215
|
* the watch is what the subscription drives, and no answer waits on it. */
|
|
@@ -274,7 +274,7 @@ type RowResult =
|
|
|
274
274
|
| { readonly complete: true; readonly row?: AgentInfo };
|
|
275
275
|
|
|
276
276
|
/** The conversion of one upstream document into the contract's spelling
|
|
277
|
-
* (§
|
|
277
|
+
* (DESIGN §2.4): renamed to snake_case, instants in Unix ms, and nothing carried over
|
|
278
278
|
* that the contract does not name.
|
|
279
279
|
*
|
|
280
280
|
* A row whose process is gone is dropped: the file outlives a session that did
|
|
@@ -316,7 +316,7 @@ function alive(pid: number): boolean {
|
|
|
316
316
|
return true;
|
|
317
317
|
} catch {
|
|
318
318
|
// EPERM would mean alive but ours to signal — impossible here, since the
|
|
319
|
-
// daemon and the sessions of its config home run as one uid (§
|
|
319
|
+
// daemon and the sessions of its config home run as one uid (DESIGN §1.4 A4).
|
|
320
320
|
return false;
|
|
321
321
|
}
|
|
322
322
|
}
|