@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
|
@@ -46,7 +46,7 @@ const VERSION = 1;
|
|
|
46
46
|
|
|
47
47
|
/** The sessions that were running when this instance last saw them.
|
|
48
48
|
*
|
|
49
|
-
* One of the three things written to disk (§
|
|
49
|
+
* One of the three things written to disk (DESIGN §2.5): losing it loses the Paused
|
|
50
50
|
* and Disappeared rows of the list entirely, and nothing else on the host
|
|
51
51
|
* remembers that a session used to be here. Only observations are stored — the
|
|
52
52
|
* classification is derived from them at read time, never written (M4). */
|
|
@@ -62,7 +62,7 @@ export class LastLiveStore {
|
|
|
62
62
|
private readonly id: InstanceId,
|
|
63
63
|
) {}
|
|
64
64
|
|
|
65
|
-
/** Read at startup (§8.3 step 4), before anything can ask for the list. A
|
|
65
|
+
/** Read at startup (DESIGN §8.3 step 4), before anything can ask for the list. A
|
|
66
66
|
* file that is missing or unreadable starts an empty list: the daemon has no
|
|
67
67
|
* way to recover it and refusing to start would cost more than the rows. */
|
|
68
68
|
load(now: Timestamp = Date.now()): void {
|
|
@@ -95,7 +95,7 @@ export class LastLiveStore {
|
|
|
95
95
|
/** Note a session as no longer live. A `stopped_at` already recorded for it
|
|
96
96
|
* survives, since the session being gone is what that stop led to; the entry
|
|
97
97
|
* carries one when the session declared it was going, which is what makes it
|
|
98
|
-
* Paused rather than Disappeared (§
|
|
98
|
+
* Paused rather than Disappeared (DESIGN §4.3). */
|
|
99
99
|
record(entry: StoredEntry): void {
|
|
100
100
|
const stopped = this.#entries.get(entry.sid)?.stopped_at ?? entry.stopped_at;
|
|
101
101
|
this.#entries.set(entry.sid, {
|
|
@@ -136,7 +136,7 @@ export class LastLiveStore {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/** Where the list lives for an instance whose state directory is `stateDir`
|
|
139
|
-
* (§8.1: every per-instance path is derived from its config home). */
|
|
139
|
+
* (DESIGN §8.1: every per-instance path is derived from its config home). */
|
|
140
140
|
export function lastLivePath(stateDir: string): string {
|
|
141
141
|
return join(stateDir, LAST_LIVE_FILE);
|
|
142
142
|
}
|
package/src/sessions/registry.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { stoppedOn } from "./status.ts";
|
|
|
32
32
|
import { TerminalCache, type TerminalReader } from "./terminals.ts";
|
|
33
33
|
|
|
34
34
|
/** What the harness says at one instant: the rows it reports, and which
|
|
35
|
-
* sessions it says are there (§
|
|
35
|
+
* sessions it says are there (DESIGN §4.1).
|
|
36
36
|
*
|
|
37
37
|
* Two readings of one moment, passed together so a caller answering several
|
|
38
38
|
* questions about that moment reads once. They are the same set for a harness
|
|
@@ -45,7 +45,7 @@ interface Own {
|
|
|
45
45
|
|
|
46
46
|
/** What the sessions domain needs from the instance around it. */
|
|
47
47
|
export interface SessionsDeps {
|
|
48
|
-
/** Which harness this config home runs (§
|
|
48
|
+
/** Which harness this config home runs (DESIGN §4.1). It decides what says a
|
|
49
49
|
* session is there and, through that, what `agents` can report. */
|
|
50
50
|
readonly harness: Harness;
|
|
51
51
|
readonly self: InstanceId;
|
|
@@ -58,30 +58,30 @@ export interface SessionsDeps {
|
|
|
58
58
|
* opened (DR-0001 §2.5). Absent on the unix socket, where reaching the
|
|
59
59
|
* instance is itself the permission, and on a mesh link. */
|
|
60
60
|
readonly authExpiresAt?: (conn: Requester) => Timestamp | undefined;
|
|
61
|
-
/** The one config home this instance answers for (§8.2). Its `sessions/` is
|
|
61
|
+
/** The one config home this instance answers for (DESIGN §8.2). Its `sessions/` is
|
|
62
62
|
* the only directory read, and no other config home is ever looked for (M6). */
|
|
63
63
|
readonly configHome: string;
|
|
64
64
|
/** Where `last_live` is written. Derived from the config home by the caller,
|
|
65
|
-
* which is where every per-instance path is decided (§8.1). */
|
|
65
|
+
* which is where every per-instance path is decided (DESIGN §8.1). */
|
|
66
66
|
readonly stateDir: string;
|
|
67
67
|
readonly capabilities: readonly Capability[];
|
|
68
68
|
/** The daemon build, reported by `hello` for display. */
|
|
69
69
|
readonly version: string;
|
|
70
70
|
readonly startedAt: Timestamp;
|
|
71
|
-
/** The one way a value reaches subscribers (§6.1). */
|
|
71
|
+
/** The one way a value reaches subscribers (DESIGN §6.1). */
|
|
72
72
|
readonly publish: (topic: string, data: unknown) => void;
|
|
73
|
-
/** What the transcript fold says about a session (§
|
|
73
|
+
/** What the transcript fold says about a session (DESIGN §4.2). Absent while
|
|
74
74
|
* nothing folds transcripts, in which case the two values it settles are
|
|
75
75
|
* simply unknown and every rule that reads them behaves as it does for a
|
|
76
76
|
* session whose transcript has said nothing. */
|
|
77
77
|
readonly transcript?: TranscriptSource;
|
|
78
|
-
/** What the gateway has seen of a session (§
|
|
78
|
+
/** What the gateway has seen of a session (DESIGN §4.2). Absent on an instance with
|
|
79
79
|
* no gateway configured, which costs the classification one of its five
|
|
80
80
|
* inputs and none of its states. */
|
|
81
81
|
readonly gateway?: GatewaySource;
|
|
82
82
|
/** The sessions this instance speaks about, or what the fold says about one,
|
|
83
83
|
* has changed. What rests on either — the topics whose value is derived from
|
|
84
|
-
* the same fold, and the tails they keep running (§6.3) — is told to catch
|
|
84
|
+
* the same fold, and the tails they keep running (DESIGN §6.3) — is told to catch
|
|
85
85
|
* up. Absent when nothing does. */
|
|
86
86
|
readonly onChanged?: () => void;
|
|
87
87
|
/** How often the confirmation poll runs, for a test that cannot wait. */
|
|
@@ -99,7 +99,7 @@ export interface SessionsDeps {
|
|
|
99
99
|
readonly terminals?: TerminalReader;
|
|
100
100
|
/** The mesh, on an instance that has one. It answers the one greeting this
|
|
101
101
|
* domain cannot judge: a peer's, whose claim is settled by an exchange of its
|
|
102
|
-
* own rather than by anything a session says (§7.2). */
|
|
102
|
+
* own rather than by anything a session says (DESIGN §7.2). */
|
|
103
103
|
readonly mesh?: MeshSource;
|
|
104
104
|
/** Where a person opens the terminal a session runs in, which `hello` states
|
|
105
105
|
* as `terminal_gateway`. The same value that gates the `terminal` capability
|
|
@@ -110,7 +110,7 @@ export interface SessionsDeps {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/** What `hello` needs of the mesh: verify the greeting of a peer, and say which
|
|
113
|
-
* instances there are and which of them can be reached (§7.5). */
|
|
113
|
+
* instances there are and which of them can be reached (DESIGN §7.5). */
|
|
114
114
|
export interface MeshSource {
|
|
115
115
|
greet(conn: Requester, claim: MeshClaim): Promise<void>;
|
|
116
116
|
instances(): InstanceInfo[];
|
|
@@ -121,13 +121,13 @@ type MeshClaim = HelloInstanceArgs["mesh"];
|
|
|
121
121
|
|
|
122
122
|
/** The fold, as the sessions domain reads it: two values about one session,
|
|
123
123
|
* asked for when a payload is built rather than copied here when they change
|
|
124
|
-
* (§
|
|
124
|
+
* (DESIGN §2.3 — the current value lives with whoever owns it). */
|
|
125
125
|
export interface TranscriptSource {
|
|
126
126
|
facts(sid: Sid): TranscriptFacts;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/** The gateway, as the sessions domain reads it: when it last saw inference
|
|
130
|
-
* for one session, asked for when a payload is built (§
|
|
130
|
+
* for one session, asked for when a payload is built (DESIGN §2.3). */
|
|
131
131
|
export interface GatewaySource {
|
|
132
132
|
activeAt(sid: Sid): Timestamp | undefined;
|
|
133
133
|
}
|
|
@@ -164,7 +164,7 @@ interface Connected {
|
|
|
164
164
|
readonly meta: SessionMeta;
|
|
165
165
|
/** The most recent request on any of its connections. Distinct from when a
|
|
166
166
|
* person last spoke to it, which is folded out of the transcript and is the
|
|
167
|
-
* one an attention-ordered list wants (§
|
|
167
|
+
* one an attention-ordered list wants (DESIGN §4.4). */
|
|
168
168
|
last_activity_at: Timestamp;
|
|
169
169
|
/** More than one client process of a session may hold a connection. */
|
|
170
170
|
conns: number;
|
|
@@ -173,10 +173,10 @@ interface Connected {
|
|
|
173
173
|
/** The sessions this instance can speak about, and the two topics that carry
|
|
174
174
|
* them.
|
|
175
175
|
*
|
|
176
|
-
* The current value lives here rather than in the topic mechanism (§
|
|
176
|
+
* The current value lives here rather than in the topic mechanism (DESIGN §2.3): what
|
|
177
177
|
* is connected is held in memory and dies with the process, what the harness
|
|
178
178
|
* reports is re-read from `sessions/`, and only `last_live` survives a restart.
|
|
179
|
-
* The classification of §
|
|
179
|
+
* The classification of DESIGN §4.3 is derived from those three whenever a payload is
|
|
180
180
|
* built, and never stored (M4). */
|
|
181
181
|
export class Sessions implements UpstreamResource {
|
|
182
182
|
readonly #connected = new Map<Sid, Connected>();
|
|
@@ -187,7 +187,7 @@ export class Sessions implements UpstreamResource {
|
|
|
187
187
|
* being live is what writes its `last_live` entry. */
|
|
188
188
|
#live = new Map<Sid, StoredEntry>();
|
|
189
189
|
/** The topic names currently subscribed. Both topics rest on the same
|
|
190
|
-
* directory watch, so it runs while either has a listener (§6.3). */
|
|
190
|
+
* directory watch, so it runs while either has a listener (DESIGN §6.3). */
|
|
191
191
|
readonly #wanted = new Set<string>();
|
|
192
192
|
/** What a session said about itself when it last greeted, kept for as long
|
|
193
193
|
* as the harness still names the session.
|
|
@@ -277,7 +277,7 @@ export class Sessions implements UpstreamResource {
|
|
|
277
277
|
/** `hello.instance`. A peer's greeting is answered only once the connection
|
|
278
278
|
* has been proven to be the endpoint it names. The verification rejects when
|
|
279
279
|
* it is not, and the connection stays anonymous because nothing settles an
|
|
280
|
-
* identity but a reply (mesh-peer-auth §5,
|
|
280
|
+
* identity but a reply (mesh-peer-auth §5, DESIGN §2.2 step 7). This is
|
|
281
281
|
* the one greeting that has to wait for something, which is why it is the one
|
|
282
282
|
* that answers with a promise. */
|
|
283
283
|
helloInstance = (input: HandlerInput): Promise<HelloResult> => {
|
|
@@ -327,7 +327,7 @@ export class Sessions implements UpstreamResource {
|
|
|
327
327
|
};
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
/** Where a session stands (§
|
|
330
|
+
/** Where a session stands (DESIGN §4.3). Undefined for a sid this instance has
|
|
331
331
|
* never seen live and does not hold in `last_live`. */
|
|
332
332
|
classify(
|
|
333
333
|
sid: Sid,
|
|
@@ -371,7 +371,7 @@ export class Sessions implements UpstreamResource {
|
|
|
371
371
|
*
|
|
372
372
|
* The harness's rows are read here rather than taken from the watch. Which
|
|
373
373
|
* sessions the harness has is a fact about this config home, true whether or
|
|
374
|
-
* not anybody subscribed to hear about it (§
|
|
374
|
+
* not anybody subscribed to hear about it (DESIGN §4.2) — the watch of DESIGN §6.3 exists
|
|
375
375
|
* to push a change to subscribers, and reading its cache instead would make
|
|
376
376
|
* "a session exists" mean "somebody is listening", which is how a live
|
|
377
377
|
* session becomes `session_not_found` to a sender and how a session that is
|
|
@@ -417,7 +417,7 @@ export class Sessions implements UpstreamResource {
|
|
|
417
417
|
if (held !== undefined) held.last_activity_at = at;
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
-
/** Where a session's transcript is, as it announced it (§
|
|
420
|
+
/** Where a session's transcript is, as it announced it (DESIGN §4.2). Whoever
|
|
421
421
|
* follows one needs the path, and the greeting is the only thing that
|
|
422
422
|
* states it. */
|
|
423
423
|
transcriptPath(sid: Sid): string | undefined {
|
|
@@ -431,7 +431,7 @@ export class Sessions implements UpstreamResource {
|
|
|
431
431
|
const meta = this.#connected.get(sid)?.meta;
|
|
432
432
|
const cwd = meta?.cwd ?? this.#own().rows.get(sid)?.cwd;
|
|
433
433
|
// The container when the session named one, the working directory
|
|
434
|
-
// otherwise — the same order `repo_root` is meant in (§
|
|
434
|
+
// otherwise — the same order `repo_root` is meant in (DESIGN §6.6).
|
|
435
435
|
const root = meta?.repo_root ?? cwd;
|
|
436
436
|
return {
|
|
437
437
|
...(root === undefined || root === "" ? {} : { root }),
|
|
@@ -441,7 +441,7 @@ export class Sessions implements UpstreamResource {
|
|
|
441
441
|
|
|
442
442
|
/** The harness's sessions as they are right now, read rather than taken
|
|
443
443
|
* from the watch's cache. What acts on a session's process resolves its pid
|
|
444
|
-
* through this: the watch runs only while somebody is subscribed (§6.3), and
|
|
444
|
+
* through this: the watch runs only while somebody is subscribed (DESIGN §6.3), and
|
|
445
445
|
* a pid from a poll that has not run is a number belonging to nobody. */
|
|
446
446
|
rowsNow(): ReadonlyMap<Sid, AgentInfo> {
|
|
447
447
|
return this.#own().rows;
|
|
@@ -463,7 +463,7 @@ export class Sessions implements UpstreamResource {
|
|
|
463
463
|
}
|
|
464
464
|
|
|
465
465
|
/** `session.stopping`: a session saying it is about to go, which is what
|
|
466
|
-
* makes it Paused rather than Disappeared once it is gone (§
|
|
466
|
+
* makes it Paused rather than Disappeared once it is gone (DESIGN §4.3).
|
|
467
467
|
*
|
|
468
468
|
* Nothing is recorded now and nothing is published: the session is still
|
|
469
469
|
* here, and the list this changes is the one it is not on yet. What the
|
|
@@ -481,7 +481,7 @@ export class Sessions implements UpstreamResource {
|
|
|
481
481
|
return { stopped_at: at };
|
|
482
482
|
};
|
|
483
483
|
|
|
484
|
-
// --- UpstreamResource (§6.3): the directory is read while, and only while,
|
|
484
|
+
// --- UpstreamResource (DESIGN §6.3): the directory is read while, and only while,
|
|
485
485
|
// somebody is subscribed to a topic that rests on it.
|
|
486
486
|
|
|
487
487
|
start(topic: string): void {
|
|
@@ -524,7 +524,7 @@ export class Sessions implements UpstreamResource {
|
|
|
524
524
|
* client that held two lists would have to move an entry between them to
|
|
525
525
|
* follow one field.
|
|
526
526
|
*
|
|
527
|
-
* Live is not the same as connected (§
|
|
527
|
+
* Live is not the same as connected (DESIGN §4.3). A session the harness names is
|
|
528
528
|
* live whether or not it ever greeted us, and it has to be on this list for
|
|
529
529
|
* the same reason it is classified at all: a restart forgets every greeting,
|
|
530
530
|
* and a list that showed only what had greeted this daemon would show a host
|
|
@@ -573,7 +573,7 @@ export class Sessions implements UpstreamResource {
|
|
|
573
573
|
};
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
-
/** The gateway saw inference for one session again (§
|
|
576
|
+
/** The gateway saw inference for one session again (DESIGN §4.2).
|
|
577
577
|
*
|
|
578
578
|
* What moved is one attribute of one row, so that row is what goes out. The
|
|
579
579
|
* sessions domain is not recomputed for it: which sessions there are has not
|
|
@@ -718,12 +718,12 @@ export class Sessions implements UpstreamResource {
|
|
|
718
718
|
}
|
|
719
719
|
|
|
720
720
|
#peer(session: Connected, now: Timestamp, own: Own): PeerInfo {
|
|
721
|
-
// The two "last activity" values are different questions (§
|
|
721
|
+
// The two "last activity" values are different questions (DESIGN §4.4): the one
|
|
722
722
|
// above moves on every request the session makes, this one only when a
|
|
723
723
|
// person speaks, and the fold is the only place that knows the second.
|
|
724
724
|
const userInput = this.deps.transcript?.facts(session.sid).last_user_input_at;
|
|
725
725
|
// What the gateway last saw run for this session: an attribute of the row
|
|
726
|
-
// beside the classification, not folded into it (§
|
|
726
|
+
// beside the classification, not folded into it (DESIGN §4.2). Absent from an
|
|
727
727
|
// instance with no gateway, where nothing observes inference at all.
|
|
728
728
|
const gatewayActiveAt = this.#gatewayActiveAt(session.sid, own.present.has(session.sid));
|
|
729
729
|
return {
|
|
@@ -741,11 +741,11 @@ export class Sessions implements UpstreamResource {
|
|
|
741
741
|
};
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
-
/** A session the harness names that holds no connection here (§
|
|
744
|
+
/** A session the harness names that holds no connection here (DESIGN §4.2).
|
|
745
745
|
*
|
|
746
746
|
* It is on the same list as the connected ones because it is live in the same
|
|
747
747
|
* sense: the classification is what separates them, and a client groups on
|
|
748
|
-
* that field alone (§
|
|
748
|
+
* that field alone (DESIGN §4.3). What it cannot carry is everything a greeting
|
|
749
749
|
* states — the session never said where it works, so the working directory
|
|
750
750
|
* comes from the harness's own row and the display names it does not know are
|
|
751
751
|
* simply absent.
|
|
@@ -772,7 +772,7 @@ export class Sessions implements UpstreamResource {
|
|
|
772
772
|
}
|
|
773
773
|
|
|
774
774
|
/** When the gateway last saw inference for a session, for a session this
|
|
775
|
-
* instance knows (§
|
|
775
|
+
* instance knows (DESIGN §4.2).
|
|
776
776
|
*
|
|
777
777
|
* The gateway sits above every config home and its events name only a session
|
|
778
778
|
* id, so what it reports is not by itself evidence about *this* instance's
|
|
@@ -807,7 +807,7 @@ export class Sessions implements UpstreamResource {
|
|
|
807
807
|
* `repo` and `ws` have no fallback: they are display names for a layout this
|
|
808
808
|
* instance has no stated way to read out of a path, so a session that does
|
|
809
809
|
* not name them is shown without them rather than with a guess. The same
|
|
810
|
-
* goes for `repo_root`, which §
|
|
810
|
+
* goes for `repo_root`, which DESIGN §6.6 says to derive from `cwd` when it is not
|
|
811
811
|
* given — no primary source states that derivation, so it is left unstated
|
|
812
812
|
* until one does. */
|
|
813
813
|
#where(
|
package/src/sessions/status.ts
CHANGED
|
@@ -13,10 +13,10 @@ import { workspaceFolders } from "./workspace.ts";
|
|
|
13
13
|
|
|
14
14
|
/** What the fold says stopped a session, read in one place.
|
|
15
15
|
*
|
|
16
|
-
* Three values rest on it: whether a live session is Waiting (§
|
|
16
|
+
* Three values rest on it: whether a live session is Waiting (DESIGN §4.3), what
|
|
17
17
|
* `session.errors` lists, and the `api_error` of `session.status:<sid>`. They
|
|
18
18
|
* ask this rather than each reading the fold's field, so the three cannot come
|
|
19
|
-
* to different answers about the same session (§7.4, M5). */
|
|
19
|
+
* to different answers about the same session (DESIGN §7.4, M5). */
|
|
20
20
|
export function stoppedOn(facts: TranscriptFacts): SessionApiError | undefined {
|
|
21
21
|
return facts.api_error;
|
|
22
22
|
}
|
|
@@ -60,7 +60,7 @@ export function sessionStatusOf(
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/** Where a session works, as it greeted (§
|
|
63
|
+
/** Where a session works, as it greeted (DESIGN §4.2). The same two values the file
|
|
64
64
|
* surfaces are decided against, asked for here so that what `session.status`
|
|
65
65
|
* says and what a read is admitted by come from one answer. */
|
|
66
66
|
export interface SessionWhere {
|
|
@@ -72,7 +72,7 @@ export interface SessionStatusDeps {
|
|
|
72
72
|
readonly self: InstanceId;
|
|
73
73
|
/** The sessions this instance can follow a transcript of: the ones that
|
|
74
74
|
* greeted, since a greeting is the only thing that names a transcript path
|
|
75
|
-
* (§
|
|
75
|
+
* (DESIGN §4.2). A session it cannot follow has no error to fold. */
|
|
76
76
|
readonly sessions: () => readonly Sid[];
|
|
77
77
|
readonly facts: (sid: Sid) => TranscriptFacts;
|
|
78
78
|
/** Where each session works, for the two fields the transcript does not
|
|
@@ -81,12 +81,12 @@ export interface SessionStatusDeps {
|
|
|
81
81
|
/** The tail behind a session's fold, asked for and let go by name. */
|
|
82
82
|
readonly hold: (sid: Sid) => void;
|
|
83
83
|
readonly release: (sid: Sid) => void;
|
|
84
|
-
/** The one way a value reaches subscribers (§6.1). */
|
|
84
|
+
/** The one way a value reaches subscribers (DESIGN §6.1). */
|
|
85
85
|
readonly publish: (topic: string, data: unknown) => void;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/** The two topics the fold's error state feeds, and the tails they keep
|
|
89
|
-
* running (§6.3).
|
|
89
|
+
* running (DESIGN §6.3).
|
|
90
90
|
*
|
|
91
91
|
* `session.errors` is one list for the instance and `session.status:<sid>` is
|
|
92
92
|
* one session, so what they hold differs: the first wants every session's fold
|
|
@@ -108,7 +108,7 @@ export class SessionStatus implements UpstreamResource {
|
|
|
108
108
|
|
|
109
109
|
constructor(private readonly deps: SessionStatusDeps) {}
|
|
110
110
|
|
|
111
|
-
// --- UpstreamResource (§6.3)
|
|
111
|
+
// --- UpstreamResource (DESIGN §6.3)
|
|
112
112
|
|
|
113
113
|
start(topic: string): void {
|
|
114
114
|
this.#wanted.add(topic);
|
|
@@ -9,7 +9,7 @@ export type TerminalReader = (pid: number) => Promise<Terminal | undefined>;
|
|
|
9
9
|
*
|
|
10
10
|
* `agents` states a session's terminal and the classification reads it: a live
|
|
11
11
|
* session that neither holds a connection here nor names a terminal is the one
|
|
12
|
-
* nothing can reach (§
|
|
12
|
+
* nothing can reach (DESIGN §4.3, `live_unmanaged`). Both want the value on every
|
|
13
13
|
* row, and neither may pay for it on every read — the harness's directory is
|
|
14
14
|
* scanned whenever any question is asked of it, and reading every session's
|
|
15
15
|
* environment there would spawn a child per session per question.
|
package/src/transcript/files.ts
CHANGED
|
@@ -13,7 +13,7 @@ const AGENT_PREFIX = "agent-";
|
|
|
13
13
|
const TEAMMATE_TASK = "in_process_teammate";
|
|
14
14
|
|
|
15
15
|
/** Where one harness keeps transcripts under its config home, and how a file
|
|
16
|
-
* there says which session it belongs to (§
|
|
16
|
+
* there says which session it belongs to (DESIGN §4.1).
|
|
17
17
|
*
|
|
18
18
|
* Two facts, because the two harnesses file the same thing differently. Claude
|
|
19
19
|
* Code keeps one directory per working directory and names the file after the
|
|
@@ -76,9 +76,9 @@ const TEAMMATE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
76
76
|
* (M6) — nothing searches for another one. */
|
|
77
77
|
export interface TranscriptFilesDeps {
|
|
78
78
|
readonly configHome: string;
|
|
79
|
-
/** Which harness's tree is under it (§
|
|
79
|
+
/** Which harness's tree is under it (DESIGN §4.1). */
|
|
80
80
|
readonly harness: Harness;
|
|
81
|
-
/** Where a connected session said its transcript is (§
|
|
81
|
+
/** Where a connected session said its transcript is (DESIGN §4.2). A session that
|
|
82
82
|
* never greeted has none, and the walk below answers for it. */
|
|
83
83
|
readonly announced: (sid: Sid) => string | undefined;
|
|
84
84
|
}
|
|
@@ -133,7 +133,7 @@ export class TranscriptFiles {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/** Which standing a transcript was written from, which every item read out
|
|
136
|
-
* of it states (§
|
|
136
|
+
* of it states (DESIGN §2.5).
|
|
137
137
|
*
|
|
138
138
|
* The file itself does not say whether an agent was a teammate or an errand:
|
|
139
139
|
* both are marked as sidechains and both are briefed the same way. What says
|
package/src/transcript/fold.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
Timestamp,
|
|
12
12
|
} from "@ccmsg/protocol";
|
|
13
13
|
|
|
14
|
-
/** Everything one session's transcript is folded into (§
|
|
14
|
+
/** Everything one session's transcript is folded into (DESIGN §2.3).
|
|
15
15
|
*
|
|
16
16
|
* One fold, not one per consumer: the same line settles whether the session is
|
|
17
17
|
* stopped, when a person last spoke to it, which files it named and what is
|
|
@@ -23,9 +23,9 @@ import type {
|
|
|
23
23
|
export interface TranscriptFacts {
|
|
24
24
|
/** The error the latest turn ended on. Present only while it stands: a real
|
|
25
25
|
* turn after it clears it, so this is the session's current state and not
|
|
26
|
-
* every error it ever hit. One of the two things §
|
|
26
|
+
* every error it ever hit. One of the two things DESIGN §4.3 calls Waiting. */
|
|
27
27
|
readonly api_error?: SessionApiError;
|
|
28
|
-
/** When a person last put something into the session (§
|
|
28
|
+
/** When a person last put something into the session (DESIGN §4.4). */
|
|
29
29
|
readonly last_user_input_at?: Timestamp;
|
|
30
30
|
/** What answered on the latest turn, and how hard it was asked to think.
|
|
31
31
|
*
|
|
@@ -66,7 +66,7 @@ export const NO_FACTS: TranscriptFacts = {
|
|
|
66
66
|
* Nothing outside this module parses a transcript record. A line arrives, the
|
|
67
67
|
* fold updates what it can from it, and the values the domain states are read
|
|
68
68
|
* off the result — so a value can never be derived by two different readings
|
|
69
|
-
* of the same file (§
|
|
69
|
+
* of the same file (DESIGN §2.3, M5).
|
|
70
70
|
*
|
|
71
71
|
* Feeding lines is order-dependent by design: the api error is the state of
|
|
72
72
|
* the latest turn, so a later line undoing an earlier one is the point.
|
|
@@ -135,7 +135,7 @@ export class TranscriptFold {
|
|
|
135
135
|
if (!isRecord(row)) return false;
|
|
136
136
|
// A Codex rollout line settles one of these facts and none of the others,
|
|
137
137
|
// so it is folded on its own rather than run past readers of records it
|
|
138
|
-
// does not have (§
|
|
138
|
+
// does not have (DESIGN §4.1).
|
|
139
139
|
const rollout = rolloutRecord(row, str(row["type"]));
|
|
140
140
|
if (rollout !== undefined) return this.#foldRollout(rollout);
|
|
141
141
|
// Every value this fold derives, derived from the one parse (M5).
|
|
@@ -155,7 +155,7 @@ export class TranscriptFold {
|
|
|
155
155
|
* The harness writes its own failures as assistant messages carrying
|
|
156
156
|
* `isApiErrorMessage: true` ("Prompt is too long", "API Error: 500 …",
|
|
157
157
|
* "Please run /login"): the turn stopped and the session sits idle until a
|
|
158
|
-
* person intervenes, which is why it counts as Waiting (§
|
|
158
|
+
* person intervenes, which is why it counts as Waiting (DESIGN §4.3). A row the
|
|
159
159
|
* model actually produced clears it — a row the harness wrote itself carries
|
|
160
160
|
* `model: "<synthetic>"` and does not, so the harness's own "No response
|
|
161
161
|
* requested." cannot pass for the agent answering again. A user row is not a
|
|
@@ -729,7 +729,7 @@ function merge(before: readonly string[] | undefined, added: unknown): string[]
|
|
|
729
729
|
* read one that has stopped growing. Both are the same act of interpretation,
|
|
730
730
|
* so both live here: nothing outside this module turns a transcript line into
|
|
731
731
|
* meaning, and the harness's own spellings — its record types, its block
|
|
732
|
-
* kinds, its ISO instants — stop at this boundary (§
|
|
732
|
+
* kinds, its ISO instants — stop at this boundary (DESIGN §2.4). */
|
|
733
733
|
export interface TranscriptRecord {
|
|
734
734
|
/** The record id a dump's bounds cut at. */
|
|
735
735
|
readonly uuid?: string;
|
|
@@ -787,7 +787,7 @@ export function readRecord(line: string): TranscriptRecord | undefined {
|
|
|
787
787
|
* uses appear in no Claude Code transcript — so the two formats are told apart
|
|
788
788
|
* by the line rather than by anything the reader was told beforehand.
|
|
789
789
|
*
|
|
790
|
-
* What is read is what §
|
|
790
|
+
* What is read is what DESIGN §4 asks a transcript for and a rollout answers: when a
|
|
791
791
|
* person last spoke, and where the session runs. The rest of the fold's facts —
|
|
792
792
|
* a session's todos, its teammates, the files it named — are Claude Code's own
|
|
793
793
|
* records, and a Codex session simply declares none of them.
|
|
@@ -887,7 +887,7 @@ function isHuman(text: string): boolean {
|
|
|
887
887
|
}
|
|
888
888
|
|
|
889
889
|
/** A transcript instant, in the contract's spelling. The harness writes ISO
|
|
890
|
-
* strings; the contract's `Timestamp` is Unix ms (§
|
|
890
|
+
* strings; the contract's `Timestamp` is Unix ms (DESIGN §2.4). */
|
|
891
891
|
function instant(value: unknown): Timestamp | undefined {
|
|
892
892
|
const text = str(value);
|
|
893
893
|
if (text === undefined) return undefined;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
DIRECT_DELIVERY_TAG,
|
|
3
|
+
type DirectDelivery,
|
|
4
|
+
parseDirectDelivery,
|
|
5
|
+
type TranscriptSubject,
|
|
6
|
+
USER_SENDER,
|
|
7
|
+
} from "@ccmsg/protocol";
|
|
2
8
|
import type { Item } from "./item.ts";
|
|
3
9
|
import {
|
|
4
10
|
count,
|
|
@@ -20,7 +26,7 @@ import { genericResult, resultFields, useFields } from "./tools.ts";
|
|
|
20
26
|
*
|
|
21
27
|
* The contract writes down the type names and what an item of each type
|
|
22
28
|
* carries, and says nothing about the file: the file is the harness's own, it
|
|
23
|
-
* changes without asking, and reading it is this instance's work (§
|
|
29
|
+
* changes without asking, and reading it is this instance's work (DESIGN §4.1). So
|
|
24
30
|
* everything that knows what a line looks like is here, and what leaves is
|
|
25
31
|
* only ever an item.
|
|
26
32
|
*
|
|
@@ -126,7 +132,7 @@ export class Classification {
|
|
|
126
132
|
*
|
|
127
133
|
* It is told rather than read out of the records, because what tells a
|
|
128
134
|
* teammate from an errand is not in the transcript at all: the harness states
|
|
129
|
-
* it beside the file, and whoever opened the file has already read that (§
|
|
135
|
+
* it beside the file, and whoever opened the file has already read that (DESIGN §2.5).
|
|
130
136
|
*
|
|
131
137
|
* A record marked as a sidechain inside a file opened as a session's own says
|
|
132
138
|
* the file is an agent's after all, and the reading moves to `sub` — the
|
|
@@ -490,11 +496,7 @@ export class Classification {
|
|
|
490
496
|
return;
|
|
491
497
|
}
|
|
492
498
|
if (said.includes("<cross-session-message")) {
|
|
493
|
-
|
|
494
|
-
text: said,
|
|
495
|
-
...optional("from", attribute(said, "from")),
|
|
496
|
-
...optional("msg_id", attribute(said, "mid")),
|
|
497
|
-
});
|
|
499
|
+
delivered(said, make);
|
|
498
500
|
return;
|
|
499
501
|
}
|
|
500
502
|
if (said.includes("<teammate-message")) {
|
|
@@ -590,6 +592,52 @@ function envelope(said: string): Record<string, unknown> {
|
|
|
590
592
|
};
|
|
591
593
|
}
|
|
592
594
|
|
|
595
|
+
/** What arrived in the envelope another session's message comes in.
|
|
596
|
+
*
|
|
597
|
+
* The harness writes prose around it — a line saying where it came from, and
|
|
598
|
+
* after it a standing reminder about messages from elsewhere — and both are
|
|
599
|
+
* the same on every one of these. What a person reads a transcript for is what
|
|
600
|
+
* was said, so the item carries the body alone and the envelope's own
|
|
601
|
+
* attributes say who said it.
|
|
602
|
+
*
|
|
603
|
+
* Who that is settles the type. A message this instance delivered on somebody's
|
|
604
|
+
* behalf names them: `user` is a person typing at a page, and their words are
|
|
605
|
+
* the same thing as words typed at the terminal (`message.user.in`). A sid is
|
|
606
|
+
* another session, which is a correspondent rather than the subject's own
|
|
607
|
+
* person (`message.session.in`). An envelope from somewhere else entirely —
|
|
608
|
+
* another harness's own cross-session traffic, which carries none of this
|
|
609
|
+
* protocol's attributes — is a session's message and is kept whole, since
|
|
610
|
+
* nothing here knows which part of it is the body. */
|
|
611
|
+
function delivered(said: string, make: Make): void {
|
|
612
|
+
const parsed = envelopeOf(said);
|
|
613
|
+
if (parsed === undefined) {
|
|
614
|
+
make("message.session.in", {
|
|
615
|
+
text: said,
|
|
616
|
+
...optional("from", attribute(said, "from")),
|
|
617
|
+
...optional("msg_id", attribute(said, "mid")),
|
|
618
|
+
});
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
make(parsed.from === USER_SENDER ? "message.user.in" : "message.session.in", {
|
|
622
|
+
text: parsed.text,
|
|
623
|
+
from: parsed.from,
|
|
624
|
+
msg_id: parsed.mid,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** The envelope out of whatever the harness wrapped around it.
|
|
629
|
+
*
|
|
630
|
+
* The contract reads one that starts where the text does, so the text is cut
|
|
631
|
+
* to it first: the tag opens it and the last closing tag ends it, which is the
|
|
632
|
+
* same rule the contract uses to let a body contain one. */
|
|
633
|
+
function envelopeOf(said: string): DirectDelivery | undefined {
|
|
634
|
+
const open = said.indexOf(`<${DIRECT_DELIVERY_TAG}`);
|
|
635
|
+
if (open < 0) return undefined;
|
|
636
|
+
const close = said.lastIndexOf(`</${DIRECT_DELIVERY_TAG}>`);
|
|
637
|
+
if (close < open) return undefined;
|
|
638
|
+
return parseDirectDelivery(said.slice(open, close + `</${DIRECT_DELIVERY_TAG}>`.length));
|
|
639
|
+
}
|
|
640
|
+
|
|
593
641
|
/** The type an agent's answer arrives under, which is the other half of
|
|
594
642
|
* whatever asked for it: a teammate's run ending answers the call that started
|
|
595
643
|
* it, an errand's answer is the errand's result. */
|
package/src/transcript/read.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const READ_LIMIT = 512 * 1024;
|
|
|
13
13
|
|
|
14
14
|
/** A slice of a transcript, read backwards from an offset.
|
|
15
15
|
*
|
|
16
|
-
* Paging is by byte offset aligned to line boundaries (§
|
|
16
|
+
* Paging is by byte offset aligned to line boundaries (DESIGN §2.3): the end of the
|
|
17
17
|
* file is read first, and each further page asks for what began before the
|
|
18
18
|
* slice just read. Nothing is scanned whole and no index is built, which is
|
|
19
19
|
* what lets a transcript of any size be read from its end.
|
package/src/transcript/tail.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { CONFIRM_POLL_MS } from "../sessions/harness.ts";
|
|
|
9
9
|
* is the recent end of the file, not its history. A megabyte is a few hundred
|
|
10
10
|
* records at the sizes the harness writes, which reaches back past the current
|
|
11
11
|
* turn by a wide margin while costing one read of fixed size however large the
|
|
12
|
-
* file has grown (§
|
|
12
|
+
* file has grown (DESIGN §2.3: a transcript of any size is read from its end).
|
|
13
13
|
*
|
|
14
14
|
* A person who has not spoken within it is reported as having no known input
|
|
15
15
|
* rather than as having spoken long ago, which is what the contract's absent
|
|
@@ -42,10 +42,10 @@ export interface TailDeps {
|
|
|
42
42
|
readonly pollMs?: number;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** One transcript file, followed while somebody wants it (§6.3).
|
|
45
|
+
/** One transcript file, followed while somebody wants it (DESIGN §6.3).
|
|
46
46
|
*
|
|
47
47
|
* Watch plus a low-rate confirmation poll, for the reason and at the interval
|
|
48
|
-
* the sessions directory uses (§
|
|
48
|
+
* the sessions directory uses (DESIGN §4.2): the watch is the route and the poll is
|
|
49
49
|
* the backstop for what a delayed FSEvents queue is still sitting on. The
|
|
50
50
|
* interval is shared rather than chosen again, so the two watches cannot
|
|
51
51
|
* drift into two different answers to the same question. */
|
|
@@ -23,14 +23,14 @@ function isItems(topic: string): boolean {
|
|
|
23
23
|
export interface TranscriptsDeps {
|
|
24
24
|
readonly self: InstanceId;
|
|
25
25
|
/** Where a session's transcript is: what it announced when it greeted
|
|
26
|
-
* (§
|
|
26
|
+
* (DESIGN §4.2), or the `<sid>.jsonl` under this instance's `projects/` that
|
|
27
27
|
* carries its name. A sid neither names nor is named by a file there has
|
|
28
28
|
* none, and nothing is guessed for it. */
|
|
29
29
|
readonly pathOf: (sid: Sid) => string | undefined;
|
|
30
|
-
/** The one way a value reaches subscribers (§6.1). */
|
|
30
|
+
/** The one way a value reaches subscribers (DESIGN §6.1). */
|
|
31
31
|
readonly publish: (topic: string, data: unknown) => void;
|
|
32
32
|
/** The fold now says something different about this session. What the fold
|
|
33
|
-
* settles is an input to the sessions domain (§
|
|
33
|
+
* settles is an input to the sessions domain (DESIGN §4.2), so the domain that
|
|
34
34
|
* states those values is told to state them again. */
|
|
35
35
|
readonly onFacts: (sid: Sid) => void;
|
|
36
36
|
/** Overrides the confirmation poll, for a test that cannot wait. */
|
|
@@ -38,14 +38,14 @@ export interface TranscriptsDeps {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** One tail and one fold per session, and the `transcript:<sid>` topic they
|
|
41
|
-
* feed (§
|
|
41
|
+
* feed (DESIGN §2.3).
|
|
42
42
|
*
|
|
43
43
|
* The fold is one per session, not one per consumer: a line is read once and
|
|
44
44
|
* every value it settles is settled from that read, so the api error, the last
|
|
45
45
|
* human input and the appended bytes are three uses of one pass rather than
|
|
46
46
|
* three passes (M5).
|
|
47
47
|
*
|
|
48
|
-
* A tail runs while something wants it and stops when nothing does (§6.3).
|
|
48
|
+
* A tail runs while something wants it and stops when nothing does (DESIGN §6.3).
|
|
49
49
|
* Subscription is one such want; a `hold` is the other, for the values the
|
|
50
50
|
* sessions domain states about a session nobody is watching the transcript of.
|
|
51
51
|
* They are counted together, so the last one to go is what stops the tail. */
|
|
@@ -54,7 +54,7 @@ export class Transcripts implements UpstreamResource {
|
|
|
54
54
|
|
|
55
55
|
constructor(private readonly deps: TranscriptsDeps) {}
|
|
56
56
|
|
|
57
|
-
// --- UpstreamResource (§6.3)
|
|
57
|
+
// --- UpstreamResource (DESIGN §6.3)
|
|
58
58
|
|
|
59
59
|
start(topic: string): void {
|
|
60
60
|
const sid = topicParam(topic);
|
|
@@ -68,7 +68,7 @@ export class Transcripts implements UpstreamResource {
|
|
|
68
68
|
|
|
69
69
|
/** Where the transcript ends as the subscription begins. What follows starts
|
|
70
70
|
* there, which is the whole of the snapshot for a topic whose frames are an
|
|
71
|
-
* append rather than a value (§6.2). A session whose transcript this
|
|
71
|
+
* append rather than a value (DESIGN §6.2). A session whose transcript this
|
|
72
72
|
* instance cannot find has nothing to state, and the subscriber begins at
|
|
73
73
|
* the first thing appended after one appears. */
|
|
74
74
|
snapshot(topic: string): readonly TopicValue[] {
|
package/src/translate/helper.ts
CHANGED
|
@@ -17,10 +17,10 @@ export interface HelperChannel {
|
|
|
17
17
|
kill(): void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** Read the helper the config names (§8.2).
|
|
20
|
+
/** Read the helper the config names (DESIGN §8.2).
|
|
21
21
|
*
|
|
22
22
|
* A helper that is named and cannot be run ends the start rather than leaving
|
|
23
|
-
* translation silently off (
|
|
23
|
+
* translation silently off (DR-0004): an instance without the capability looks
|
|
24
24
|
* exactly like one nobody configured, and the operator who named a program
|
|
25
25
|
* meant to have it. */
|
|
26
26
|
export function translateSetup(config: UpstreamConfig, file: string): string | undefined {
|