@ccmsg/cli 0.11.3 → 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.
Files changed (67) hide show
  1. package/README.md +8 -12
  2. package/package.json +1 -1
  3. package/src/auth/auth.ts +23 -23
  4. package/src/auth/http.ts +3 -3
  5. package/src/auth/records.ts +2 -2
  6. package/src/auth/webauthn.ts +1 -1
  7. package/src/cli.ts +10 -10
  8. package/src/daemon/control.ts +1 -1
  9. package/src/daemon/registry.ts +11 -11
  10. package/src/daemon/snapshot.ts +1 -1
  11. package/src/daemon/supervise.ts +4 -4
  12. package/src/dispatch/caller.ts +1 -1
  13. package/src/dispatch/dispatch.ts +4 -4
  14. package/src/dispatch/handler.ts +4 -4
  15. package/src/dispatch/identity.ts +1 -1
  16. package/src/dispatch/result.ts +2 -2
  17. package/src/files/containment.ts +2 -2
  18. package/src/harness/index.ts +1 -1
  19. package/src/instance/client.ts +1 -1
  20. package/src/instance/config.ts +13 -13
  21. package/src/instance/instance.ts +42 -42
  22. package/src/instance/lock.ts +2 -2
  23. package/src/instance/log.ts +1 -1
  24. package/src/instance/paths.ts +6 -6
  25. package/src/instance/socket.ts +1 -1
  26. package/src/kv/store.ts +1 -1
  27. package/src/mesh/instances.ts +2 -2
  28. package/src/mesh/keys.ts +7 -7
  29. package/src/mesh/mesh.ts +67 -67
  30. package/src/mesh/relay.ts +8 -8
  31. package/src/mesh/wire.ts +2 -2
  32. package/src/messaging/delivery.ts +24 -24
  33. package/src/messaging/direct.ts +23 -23
  34. package/src/messaging/inbox.ts +6 -6
  35. package/src/messaging/notify.ts +5 -5
  36. package/src/plugin/codex.ts +1 -1
  37. package/src/sessions/classify.ts +6 -6
  38. package/src/sessions/dump.ts +1 -1
  39. package/src/sessions/handlers.ts +2 -2
  40. package/src/sessions/harness.ts +12 -12
  41. package/src/sessions/last-live.ts +4 -4
  42. package/src/sessions/registry.ts +32 -32
  43. package/src/sessions/status.ts +7 -7
  44. package/src/sessions/terminals.ts +1 -1
  45. package/src/transcript/files.ts +4 -4
  46. package/src/transcript/fold.ts +9 -9
  47. package/src/transcript/items/classify.ts +56 -8
  48. package/src/transcript/read.ts +1 -1
  49. package/src/transcript/tail.ts +3 -3
  50. package/src/transcript/transcripts.ts +7 -7
  51. package/src/translate/helper.ts +2 -2
  52. package/src/transport/conn.ts +3 -3
  53. package/src/transport/dial.ts +1 -1
  54. package/src/transport/driver.ts +2 -2
  55. package/src/transport/entry.ts +1 -1
  56. package/src/transport/framing.ts +1 -1
  57. package/src/transport/listener.ts +3 -3
  58. package/src/transport/uds.ts +2 -2
  59. package/src/transport/ws.ts +2 -2
  60. package/src/upstream/events.ts +3 -3
  61. package/src/upstream/gateway.ts +6 -6
  62. package/src/upstream/json.ts +1 -1
  63. package/src/upstream/requests.ts +6 -6
  64. package/src/upstream/stats.ts +1 -1
  65. package/src/upstream/status.ts +4 -4
  66. package/src/upstream/usage.ts +1 -1
  67. 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 (§3.6): losing it loses the Paused
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 (§5.2). */
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
  }
@@ -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 (§3.8).
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 (§3.8). It decides what says a
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 (§5.1). Absent while
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 (§5.1). Absent on an instance with
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
- * (§3.3 — the current value lives with whoever owns it). */
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 (§3.3). */
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 (§5.3). */
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 (§3.3): what
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 §5.2 is derived from those three whenever a payload is
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, daemon-v2 §3.2 step 7). This is
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 (§5.2). Undefined for a sid this instance has
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 (§5.1) — the watch of §6.3 exists
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 (§5.1). Whoever
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 (§4.2).
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 (§5.2).
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 (§5.2). A session the harness names is
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 (§5.1).
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 (§5.3): the one
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 (§5.1). Absent from an
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 (§5.1).
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 (§5.2). What it cannot carry is everything a greeting
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 (§5.1).
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 §4.2 says to derive from `cwd` when it is not
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(
@@ -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 (§5.2), what
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 (§5.1). The same two values the file
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
- * (§5.1). A session it cannot follow has no error to fold. */
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 (§5.2, `live_unmanaged`). Both want the value on every
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.
@@ -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 (§3.8).
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 (§3.8). */
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 (§5.1). A session that
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 (§3.6).
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
@@ -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 (§3.3).
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 §5.2 calls Waiting. */
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 (§5.3). */
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 (§3.3, M5).
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 (§3.8).
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 (§5.2). A row the
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 (§3.5). */
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 §5 asks a transcript for and a rollout answers: when a
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 (§3.5). */
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 type { TranscriptSubject } from "@ccmsg/protocol";
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 (§3.8). So
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 (§3.6).
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
- make("message.session.in", {
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. */
@@ -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 (§3.3): the end of the
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.
@@ -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 (§3.3: a transcript of any size is read from its end).
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 (§5.1): the watch is the route and the poll is
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
- * (§5.1), or the `<sid>.jsonl` under this instance's `projects/` that
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 (§5.1), so the domain that
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 (§3.3).
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[] {
@@ -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 (DV-Q9): an instance without the capability looks
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 {