@ccmsg/cli 0.8.1 → 0.9.0

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 (46) hide show
  1. package/package.json +2 -2
  2. package/src/auth/auth.ts +14 -14
  3. package/src/auth/http.ts +6 -6
  4. package/src/auth/records.ts +1 -1
  5. package/src/auth/topic.ts +1 -1
  6. package/src/cli.ts +21 -24
  7. package/src/daemon/control.ts +2 -2
  8. package/src/daemon/registry.ts +7 -7
  9. package/src/daemon/snapshot.ts +12 -10
  10. package/src/daemon/supervise.ts +1 -1
  11. package/src/files/containment.ts +5 -5
  12. package/src/files/files.ts +12 -12
  13. package/src/files/sandbox.ts +0 -0
  14. package/src/greeting/meta.ts +5 -2
  15. package/src/instance/config.ts +21 -9
  16. package/src/instance/instance.ts +15 -13
  17. package/src/kv/store.ts +4 -3
  18. package/src/launcher/launcher.ts +3 -3
  19. package/src/mesh/mesh.ts +7 -8
  20. package/src/mesh/relay.ts +1 -1
  21. package/src/messaging/delivery.ts +4 -4
  22. package/src/messaging/direct.ts +2 -2
  23. package/src/messaging/handlers.ts +4 -4
  24. package/src/messaging/inbox.ts +1 -1
  25. package/src/messaging/notify.ts +11 -11
  26. package/src/sessions/dump.ts +1 -1
  27. package/src/sessions/handlers.ts +17 -17
  28. package/src/sessions/harness.ts +1 -1
  29. package/src/sessions/last-live.ts +1 -1
  30. package/src/sessions/registry.ts +60 -77
  31. package/src/sessions/search.ts +1 -1
  32. package/src/sessions/status.ts +7 -7
  33. package/src/topics/handlers.ts +2 -2
  34. package/src/topics/topics.ts +0 -0
  35. package/src/transcript/items/classify.ts +50 -39
  36. package/src/transcript/items/document.ts +2 -2
  37. package/src/transcript/items/ids.ts +0 -0
  38. package/src/transcript/items/render.ts +24 -24
  39. package/src/transcript/items/select.ts +5 -5
  40. package/src/transcript/transcripts.ts +3 -3
  41. package/src/translate/translate.ts +1 -1
  42. package/src/transport/driver.ts +13 -11
  43. package/src/upstream/gateway.ts +3 -3
  44. package/src/upstream/requests.ts +1 -1
  45. package/src/upstream/status.ts +1 -1
  46. package/src/version.ts +1 -1
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute } from "node:path";
3
- import type { DumpPreset, Endpoint } from "@ccmsg/protocol";
3
+ import { type DumpPreset, type Endpoint, TranscriptItemSelector } from "@ccmsg/protocol";
4
4
  import { DEFAULT_HARNESS, type Harness, HARNESSES, isHarness } from "../harness/index.ts";
5
5
  import { parseCidr } from "./client.ts";
6
6
 
@@ -47,7 +47,7 @@ export interface LauncherTemplateConfig {
47
47
  /** What the launcher may start, and where.
48
48
  *
49
49
  * Structured rather than a string because it is a form: the roots bound where a
50
- * session may run, and the recipes are what `launcher_config_read` answers
50
+ * session may run, and the recipes are what `launcher.config.read` answers
51
51
  * with. Present is what gives this instance the `launcher` capability. */
52
52
  export interface LauncherConfig {
53
53
  /** Absolute directories a session may be started in. A launch or a walk
@@ -55,7 +55,7 @@ export interface LauncherConfig {
55
55
  readonly root_dirs: readonly string[];
56
56
  /** In configured order; the first is the default recipe. */
57
57
  readonly templates: readonly LauncherTemplateConfig[];
58
- /** How deep `dir_tree` walks when a request names no depth. */
58
+ /** How deep `dir.tree` walks when a request names no depth. */
59
59
  readonly depth: number;
60
60
  /** How long a launch may run before it is stopped. */
61
61
  readonly timeout_secs: number;
@@ -98,7 +98,7 @@ export interface UpstreamConfig {
98
98
  * not a property of the wire. A type name stays one to one with what a record
99
99
  * is, and the groupings people reach for are made by naming a set of them. */
100
100
  export interface DumpConfig {
101
- /** In configured order, which is the order `dump_presets_read` answers in. */
101
+ /** In configured order, which is the order `dump.presets.read` answers in. */
102
102
  readonly presets: readonly DumpPreset[];
103
103
  }
104
104
 
@@ -330,9 +330,18 @@ export function parseConfig(file: string, fields: Record<string, unknown>): Inst
330
330
  };
331
331
  }
332
332
 
333
- /** One element of a selection, as the contract spells it: a type name, a
334
- * prefix of one, either negated with `-`, or `@name` for a preset. */
335
- const SELECTOR = /^-?(?:@[A-Za-z0-9][A-Za-z0-9_-]*|[a-z]+(?::[A-Za-z0-9_.-]+)*)$/;
333
+ /** One element of a selection: a type name, a prefix of one, either negated
334
+ * with `-`, or `@name` for a preset. Taken from the contract's own schema
335
+ * rather than written again here, so a config file and a request are held to
336
+ * the one spelling. */
337
+ const SELECTOR = new RegExp(
338
+ TranscriptItemSelector.pattern ??
339
+ // A selector schema with no pattern would let every string through here,
340
+ // which is the one outcome worse than refusing the config file.
341
+ (() => {
342
+ throw new Error("the contract's item selector states no pattern");
343
+ })(),
344
+ );
336
345
 
337
346
  function dumpOf(file: string, raw: unknown): DumpConfig {
338
347
  if (raw === undefined) return { presets: [] };
@@ -368,9 +377,12 @@ function presetsOf(file: string, raw: unknown): DumpPreset[] {
368
377
  const types = stringsOf(file, `${at}.opts.types`, opts["types"]);
369
378
  const wrong = types.filter((element) => !SELECTOR.test(element));
370
379
  if (wrong.length > 0) {
380
+ // Named rather than dropped: a selection nothing can match would dump an
381
+ // empty file and say why nowhere. The preset is named beside the strings
382
+ // so the line to edit is the one the message points at.
371
383
  throw new ConfigError(
372
384
  file,
373
- `${at}.opts.types must be item types, prefixes, exclusions or @presets, got ${wrong.join(", ")}`,
385
+ `dump.presets[${name}].opts.types must be item types, prefixes, exclusions or @presets, got ${wrong.join(", ")}`,
374
386
  );
375
387
  }
376
388
  return {
@@ -515,7 +527,7 @@ function upstreamOf(file: string, raw: unknown): UpstreamConfig {
515
527
  };
516
528
  }
517
529
 
518
- /** How deep `dir_tree` walks, and how long a launch may take, when the config
530
+ /** How deep `dir.tree` walks, and how long a launch may take, when the config
519
531
  * says neither. */
520
532
  const DEFAULT_DEPTH = 2;
521
533
  const DEFAULT_TIMEOUT_SECS = 10;
@@ -394,7 +394,7 @@ export class Instance {
394
394
  publish: (topic, data, instance) => {
395
395
  this.#topics.publish(topic, data, instance);
396
396
  },
397
- // What a peer wrote on `auth_records`, folded into the set this instance
397
+ // What a peer wrote on `auth.records`, folded into the set this instance
398
398
  // holds. It is not relayed onward: every instance subscribes to every
399
399
  // peer, so a record reaches all of them without anyone repeating it, and
400
400
  // what this instance writes travels as its own (DR-0001 §2.6).
@@ -552,11 +552,11 @@ export class Instance {
552
552
  this.#topics.attach("transcript", this.#transcripts);
553
553
  // One tail feeds both: the bytes as they are appended, and what those
554
554
  // bytes were read as.
555
- this.#topics.attach("transcript_items", this.#transcripts);
556
- this.#topics.attach("session_status", this.#status);
557
- this.#topics.attach("session_errors", this.#status);
558
- this.#topics.attach("llm_requests", this.#gateway.requests);
559
- this.#topics.attach("llm_status", this.#gateway.statusResource);
555
+ this.#topics.attach("transcript.items", this.#transcripts);
556
+ this.#topics.attach("session.status", this.#status);
557
+ this.#topics.attach("session.errors", this.#status);
558
+ this.#topics.attach("llm.requests", this.#gateway.requests);
559
+ this.#topics.attach("llm.status", this.#gateway.statusResource);
560
560
 
561
561
  // The one thing here that is written down and is nobody's derived value
562
562
  // (§3.6): what a person saved through a client, which no other party holds
@@ -574,7 +574,7 @@ export class Instance {
574
574
  self: this.self,
575
575
  ...(now === undefined ? {} : { now }),
576
576
  publish: (written) => {
577
- this.#topics.publish("auth_records", { records: written });
577
+ this.#topics.publish("auth.records", { records: written });
578
578
  },
579
579
  });
580
580
  this.#auth = new Auth({
@@ -590,7 +590,7 @@ export class Instance {
590
590
  this.log.write(msg, fields);
591
591
  },
592
592
  });
593
- this.#topics.attach("auth_records", new AuthTopic(this.self, records));
593
+ this.#topics.attach("auth.records", new AuthTopic(this.self, records));
594
594
 
595
595
  // The upstreams that answer a question rather than hold a value. Each is
596
596
  // built only where its config named one, and dispatch has already refused
@@ -618,8 +618,10 @@ export class Instance {
618
618
  const origin = config.upstream.sandbox_origin;
619
619
 
620
620
  this.#handlers = completeHandlers({
621
- hello: this.#sessions.hello,
622
- session_stopping: this.#sessions.stopping,
621
+ "hello.session": this.#sessions.helloSession,
622
+ "hello.user": this.#sessions.helloUser,
623
+ "hello.instance": this.#sessions.helloInstance,
624
+ "session.stopping": this.#sessions.stopping,
623
625
  ...topicHandlers(this.#topics),
624
626
  ...messagingHandlers(this.#delivery, this.#notify),
625
627
  ...fileHandlers(files),
@@ -643,8 +645,8 @@ export class Instance {
643
645
  ...gatewayHandlers(setup),
644
646
  ...kvHandlers(kv),
645
647
  ...authHandlers(this.#auth),
646
- instance_ping: (): InstancePingResult => this.ping(),
647
- instance_shutdown: () => {
648
+ "instance.ping": (): InstancePingResult => this.ping(),
649
+ "instance.shutdown": () => {
648
650
  // The reply goes out when this handler's value reaches the driver, so
649
651
  // stopping is deferred past that turn of the loop rather than run
650
652
  // here — the caller is told the request was accepted, which is what
@@ -852,7 +854,7 @@ export class Instance {
852
854
  }
853
855
  // Let in as a peer rather than on the entry token, and still unproven:
854
856
  // the greeting is the one thing it was admitted to make.
855
- if (this.#mesh.unproven(conn) && opOf(frame) !== "hello") {
857
+ if (this.#mesh.unproven(conn) && opOf(frame) !== "hello.instance") {
856
858
  conn.close();
857
859
  return failure(
858
860
  requestIdOf(frame),
package/src/kv/store.ts CHANGED
@@ -186,10 +186,11 @@ function forget(entries: Map<string, Held>, now: Timestamp): Map<string, Held> {
186
186
 
187
187
  export function kvHandlers(store: KvStore) {
188
188
  return {
189
- kv_read: (input: HandlerInput): KvReadResult => store.read(input.args as unknown as KvReadArgs),
190
- kv_write: (input: HandlerInput): KvWriteResult =>
189
+ "kv.read": (input: HandlerInput): KvReadResult =>
190
+ store.read(input.args as unknown as KvReadArgs),
191
+ "kv.write": (input: HandlerInput): KvWriteResult =>
191
192
  store.write(input.args as unknown as KvWriteArgs),
192
- kv_delete: (input: HandlerInput): KvDeleteResult =>
193
+ "kv.delete": (input: HandlerInput): KvDeleteResult =>
193
194
  store.delete(input.args as unknown as KvDeleteArgs),
194
195
  };
195
196
  }
@@ -126,10 +126,10 @@ export class Launcher {
126
126
 
127
127
  export function launcherHandlers(launcher: Launcher) {
128
128
  return {
129
- launcher_config_read: (): LauncherConfigReadResult => launcher.configRead(),
130
- launcher_run: (input: HandlerInput): Promise<LauncherRunResult> =>
129
+ "launcher.config.read": (): LauncherConfigReadResult => launcher.configRead(),
130
+ "launcher.run": (input: HandlerInput): Promise<LauncherRunResult> =>
131
131
  launcher.run(input.args as unknown as LauncherRunArgs),
132
- dir_tree: (input: HandlerInput): DirTreeResult =>
132
+ "dir.tree": (input: HandlerInput): DirTreeResult =>
133
133
  launcher.tree(input.args as unknown as DirTreeArgs),
134
134
  };
135
135
  }
package/src/mesh/mesh.ts CHANGED
@@ -122,7 +122,7 @@ export interface MeshHost {
122
122
  publish(topic: string, data: unknown, instance: InstanceId): void;
123
123
  /** Take a frame on a topic the relay does not carry.
124
124
  *
125
- * `auth_records` is the one: its granularity is `element`, so a frame states
125
+ * `auth.records` is the one: its granularity is `element`, so a frame states
126
126
  * the entries that moved rather than a whole value per instance, and there is
127
127
  * nothing for the relay's last-value-per-instance table to hold. What
128
128
  * receives it is the set itself, which merges by key (DR-0001 §2.6). */
@@ -469,8 +469,8 @@ export class Mesh {
469
469
 
470
470
  /** Ask another instance one op, as this instance rather than for anybody.
471
471
  *
472
- * What the person's authentication needs of a peer (`auth_resolve`,
473
- * `auth_rotate`) is a fact only its issuer holds, asked for by the instance
472
+ * What the person's authentication needs of a peer (`auth.resolve`,
473
+ * `auth.rotate`) is a fact only its issuer holds, asked for by the instance
474
474
  * that needs it — so the `caller` is this instance's own role, and the
475
475
  * request travels the ordinary forwarding path (§7.3, DR-0001 §2.6).
476
476
  *
@@ -537,7 +537,7 @@ export class Mesh {
537
537
  * instance asks of every peer, and the frames come back unchanged (§7.4).
538
538
  * `peers` is never given up, because it is also the routing table. */
539
539
  demand(topic: string, wanted: boolean): void {
540
- // `auth_records` is never given up and never asked for on demand: every
540
+ // `auth.records` is never given up and never asked for on demand: every
541
541
  // instance holds the whole set whether or not anything local is watching
542
542
  // it, the way `peers` is also the routing table (§7.4, DR-0001 §2.6).
543
543
  if (topic === AUTH_TOPIC || !isClusterTopic(topic)) return;
@@ -561,7 +561,7 @@ export class Mesh {
561
561
  * the ordering is the connection's rather than a delay chosen here. */
562
562
  #ask(conn: Requester, topic: string, wanted: boolean, afterAck = false): void {
563
563
  const frame = {
564
- op: wanted ? "topic_subscribe" : "topic_unsubscribe",
564
+ op: wanted ? "topic.subscribe" : "topic.unsubscribe",
565
565
  request_id: `mesh-sub-${randomId()}`,
566
566
  topic,
567
567
  // The instance asks on behalf of whoever subscribed to it, and what they
@@ -569,7 +569,7 @@ export class Mesh {
569
569
  // any one session: a cluster topic is the same value for all of them
570
570
  // (§6.2), so there is nothing narrower to name.
571
571
  //
572
- // `auth_records` is the exception, and the one topic no person may hear:
572
+ // `auth.records` is the exception, and the one topic no person may hear:
573
573
  // it carries the tokens that authenticate them, so the instance asks for
574
574
  // it as itself (DR-0001 §2.6).
575
575
  caller: (topic === AUTH_TOPIC
@@ -893,9 +893,8 @@ export class Mesh {
893
893
  }
894
894
  minted.conn = conn;
895
895
  conn.send({
896
- op: "hello",
896
+ op: "hello.instance",
897
897
  request_id: `mesh-hello-${key.kid}`,
898
- role: "instance",
899
898
  protocol_version: PROTOCOL_VERSION,
900
899
  mesh: { ver: MESH_VER, iss: self, aud: peer, id: this.deps.id, kid: key.kid },
901
900
  });
package/src/mesh/relay.ts CHANGED
@@ -35,7 +35,7 @@ export const CLUSTER_TOPICS: readonly string[] = [
35
35
  * It is `element`-granular and its elements are the instances' own, so it is
36
36
  * asked for as the instance rather than on a person's behalf, and folded into
37
37
  * the set this instance holds rather than held here (DR-0001 §2.6). */
38
- export const AUTH_TOPIC = "auth_records";
38
+ export const AUTH_TOPIC = "auth.records";
39
39
 
40
40
  export function isClusterTopic(topic: string): boolean {
41
41
  return CLUSTER_TOPICS.includes(topic);
@@ -40,7 +40,7 @@ export interface SessionLookup {
40
40
 
41
41
  /** The rest of the cluster, for a message addressed outside this instance.
42
42
  *
43
- * `message_send` is a `cluster` op — any instance may be asked — but a message
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
  * (§3.2 step 6 is about `instance-local` ops; this is the same forwarding for
@@ -99,7 +99,7 @@ export class Delivery implements UpstreamResource {
99
99
  this.#counter = deps.inbox.lastCounter(`${deps.self}/`);
100
100
  }
101
101
 
102
- /** `message_send`. The op fails only for a sid nobody knows; every other
102
+ /** `message.send`. The op fails only for a sid nobody knows; every other
103
103
  * outcome is a success carrying what became of the message. */
104
104
  send = async (input: HandlerInput): Promise<MessageSendResult> => {
105
105
  const args = input.args as unknown as MessageSendArgs;
@@ -161,7 +161,7 @@ export class Delivery implements UpstreamResource {
161
161
  : undefined;
162
162
  }
163
163
  // The sender, as the owning instance will run the op as: the identity the
164
- // connection greeted with, which is the same thing `message_send` reads to
164
+ // connection greeted with, which is the same thing `message.send` reads to
165
165
  // decide who a message is from (§4.1).
166
166
  const answer = await cluster.forward(owner, input.args, callerOf(input));
167
167
  if (answer.kind === "reply") {
@@ -366,7 +366,7 @@ export class Delivery implements UpstreamResource {
366
366
  }
367
367
 
368
368
  /** Who is asking, as another instance is told it (contract, `CallerIdentity`).
369
- * A connection with no settled greeting names nobody, and `message_send`
369
+ * A connection with no settled greeting names nobody, and `message.send`
370
370
  * refuses it before this is reached. */
371
371
  function callerOf(input: HandlerInput): CallerIdentity | undefined {
372
372
  const identity = input.identity;
@@ -59,7 +59,7 @@ export const DIRECT_ACK_MS = 2_000;
59
59
  * peer message at its inbound gate and reports the outcome from that same
60
60
  * decision, so a receipt for a message we have finished writing is one connect
61
61
  * and one line away on a socket of this same host. A quarter second is far
62
- * more than that costs and far less than a person waits for `message_send` to
62
+ * more than that costs and far less than a person waits for `message.send` to
63
63
  * answer. Nothing measured stands behind the number itself. */
64
64
  export const DIRECT_STATUS_MS = 250;
65
65
 
@@ -358,7 +358,7 @@ const DROPPED = HARNESSES.filter((harness) => harness !== "codex").flatMap((harn
358
358
  * belong to the interactive interface.
359
359
  *
360
360
  * The budget is here for what is not being predicted: a child that never
361
- * answers would hold `message_send` open for as long as it lived, and route
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
363
  * nothing (§4.1). It is generous next to a call that has been measured to
364
364
  * return at once. */
@@ -6,9 +6,9 @@ import type { Notify } from "./notify.ts";
6
6
  * own entry point under the contract's name. */
7
7
  export function messagingHandlers(delivery: Delivery, notify: Notify) {
8
8
  return {
9
- message_send: delivery.send,
10
- notify_send: notify.send,
11
- say_post: notify.post,
12
- say_mark_read: notify.markRead,
9
+ "message.send": delivery.send,
10
+ "notify.send": notify.send,
11
+ "say.post": notify.post,
12
+ "say.unread.clear": notify.markRead,
13
13
  };
14
14
  }
@@ -34,7 +34,7 @@ type Record_ =
34
34
  /** What was said to a session and has not reached it.
35
35
  *
36
36
  * The one thing here that nothing else can reconstruct (§3.6): the sender's
37
- * `message_send` has already been answered, no transcript holds a message that
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
  *
@@ -3,8 +3,8 @@ import type {
3
3
  Notification,
4
4
  NotifySendArgs,
5
5
  NotifySendResult,
6
- SayMarkReadArgs,
7
- SayMarkReadResult,
6
+ SayUnreadClearArgs,
7
+ SayUnreadClearResult,
8
8
  SayPostArgs,
9
9
  SayPostResult,
10
10
  Sid,
@@ -29,8 +29,8 @@ export interface NotifyDeps {
29
29
  /** The `notify` topic and the three ops that speak on it.
30
30
  *
31
31
  * One object for all three because they are one thing seen from two sides: a
32
- * line reaching a person watching. `notify_send` is somebody telling a person
33
- * about a session; `say_post` is a session saying that it just spoke. Both end
32
+ * line reaching a person watching. `notify.send` is somebody telling a person
33
+ * about a session; `say.post` is a session saying that it just spoke. Both end
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
  *
@@ -44,7 +44,7 @@ export class Notify implements UpstreamResource {
44
44
 
45
45
  constructor(private readonly deps: NotifyDeps) {}
46
46
 
47
- /** `notify_send`. The subject is the argument when it names one and the
47
+ /** `notify.send`. The subject is the argument when it names one and the
48
48
  * caller otherwise, so a session notifying about itself says only the text. */
49
49
  send = (input: HandlerInput): NotifySendResult => {
50
50
  const args = input.args as unknown as NotifySendArgs;
@@ -52,7 +52,7 @@ export class Notify implements UpstreamResource {
52
52
  return {};
53
53
  };
54
54
 
55
- /** `say_post`. What was said is already in the caller's transcript, so this
55
+ /** `say.post`. What was said is already in the caller's transcript, so this
56
56
  * pushes the occurrence and raises the unread mark; the instance keeps no log
57
57
  * of its own. The op is open to sessions alone, so the subject is the caller
58
58
  * and there is nothing to address. */
@@ -64,9 +64,9 @@ export class Notify implements UpstreamResource {
64
64
  return { posted_at };
65
65
  };
66
66
 
67
- /** `say_mark_read`. One session's mark, or every one when none is named. */
68
- markRead = (input: HandlerInput): SayMarkReadResult => {
69
- const { sid } = input.args as unknown as SayMarkReadArgs;
67
+ /** `say.unread.clear`. One session's mark, or every one when none is named. */
68
+ markRead = (input: HandlerInput): SayUnreadClearResult => {
69
+ const { sid } = input.args as unknown as SayUnreadClearArgs;
70
70
  if (sid === undefined) this.#unread.clear();
71
71
  else this.#unread.delete(sid);
72
72
  return {};
@@ -74,7 +74,7 @@ export class Notify implements UpstreamResource {
74
74
 
75
75
  /** The sessions that have spoken unheard. Nothing in this generation of the
76
76
  * contract carries the mark on the wire, so this is how the instance's own
77
- * side reads what `say_mark_read` clears. */
77
+ * side reads what `say.unread.clear` clears. */
78
78
  unread(): readonly Sid[] {
79
79
  return [...this.#unread];
80
80
  }
@@ -113,7 +113,7 @@ export class Notify implements UpstreamResource {
113
113
  }
114
114
 
115
115
  /** The session the caller is. A person's connection names none, which is why
116
- * `notify_send` takes the subject as an argument — one that omits it from a
116
+ * `notify.send` takes the subject as an argument — one that omits it from a
117
117
  * connection with no session has named nobody for the notification to be
118
118
  * about. */
119
119
  #caller(input: HandlerInput): Sid {
@@ -47,7 +47,7 @@ export interface DumpDeps {
47
47
  *
48
48
  * The subject is the session, or one agent below it when the request names
49
49
  * one. Every item type is read from wherever the subject stands — an agent's
50
- * `message:user:in` is the brief its parent gave it — so one selection carries
50
+ * `message.user.in` is the brief its parent gave it — so one selection carries
51
51
  * unchanged down a chain of agents, which is what makes the ledger's agent ids
52
52
  * a way to descend rather than just a list. */
53
53
  export function dumpWrite(args: SessionDumpWriteArgs, deps: DumpDeps): SessionDumpWriteResult {
@@ -6,12 +6,12 @@ import {
6
6
  type SessionDumpWriteArgs,
7
7
  type SessionEnvReadArgs,
8
8
  type SessionEnvReadResult,
9
- type SessionForkOriginArgs,
10
- type SessionForkOriginResult,
9
+ type SessionForkOriginReadArgs,
10
+ type SessionForkOriginReadResult,
11
11
  type SessionKillArgs,
12
12
  type SessionKillResult,
13
- type SessionLastLiveRemoveArgs,
14
- type SessionLastLiveRemoveResult,
13
+ type SessionForgetArgs,
14
+ type SessionForgetResult,
15
15
  type SessionRenameArgs,
16
16
  type SessionRenameResult,
17
17
  type SessionSearchArgs,
@@ -69,7 +69,7 @@ export interface SessionOpsDeps {
69
69
  /** The ops that observe and operate on sessions.
70
70
  *
71
71
  * None of them decides who may call it: dispatch has settled that from the
72
- * attribute table. The one that narrows by role is `transcript_read`, and it
72
+ * attribute table. The one that narrows by role is `transcript.read`, and it
73
73
  * narrows through the same `sees` the file ops narrow through — the visible
74
74
  * range of a `scope: "role"` op is one rule, in one place, whatever it is
75
75
  * a range over. */
@@ -77,12 +77,12 @@ export function sessionHandlers(deps: SessionOpsDeps) {
77
77
  const viewer = (input: HandlerInput): Viewer => ({ role: input.role, sid: input.identity?.sid });
78
78
 
79
79
  return {
80
- session_kill: async (input: HandlerInput): Promise<SessionKillResult> => {
80
+ "session.kill": async (input: HandlerInput): Promise<SessionKillResult> => {
81
81
  const args = input.args as unknown as SessionKillArgs;
82
82
  return await deps.processes.kill(args.sid, args.force === true);
83
83
  },
84
84
 
85
- session_rename: async (input: HandlerInput): Promise<SessionRenameResult> => {
85
+ "session.rename": async (input: HandlerInput): Promise<SessionRenameResult> => {
86
86
  const args = input.args as unknown as SessionRenameArgs;
87
87
  const title = validTitle(args.title);
88
88
  const terminal = await deps.processes.terminal(args.sid);
@@ -93,20 +93,20 @@ export function sessionHandlers(deps: SessionOpsDeps) {
93
93
  return { terminal_id: terminal.id, instance: deps.self, title };
94
94
  },
95
95
 
96
- session_env_read: async (input: HandlerInput): Promise<SessionEnvReadResult> => {
96
+ "session.env.read": async (input: HandlerInput): Promise<SessionEnvReadResult> => {
97
97
  const args = input.args as unknown as SessionEnvReadArgs;
98
98
  const { pid, env } = await deps.processes.environment(args.sid);
99
99
  return { pid, instance: deps.self, env };
100
100
  },
101
101
 
102
- session_search: (input: HandlerInput) =>
102
+ "session.search": (input: HandlerInput) =>
103
103
  search(input.args as unknown as SessionSearchArgs, {
104
104
  self: deps.self,
105
105
  configHome: deps.configHome,
106
106
  files: deps.files,
107
107
  }),
108
108
 
109
- session_dump_write: (input: HandlerInput) =>
109
+ "session.dump.write": (input: HandlerInput) =>
110
110
  dumpWrite(input.args as unknown as SessionDumpWriteArgs, {
111
111
  self: deps.self,
112
112
  stateDir: deps.stateDir,
@@ -120,22 +120,22 @@ export function sessionHandlers(deps: SessionOpsDeps) {
120
120
  * free-text field and let the instance refuse. A preset that references
121
121
  * another is answered as written: the expansion, and the refusal of a
122
122
  * cycle, happen where the config is read. */
123
- dump_presets_read: (): DumpPresetsReadResult => ({ presets: [...deps.presets] }),
123
+ "dump.presets.read": (): DumpPresetsReadResult => ({ presets: [...deps.presets] }),
124
124
 
125
- session_fork_origin: (input: HandlerInput): SessionForkOriginResult => {
126
- const args = input.args as unknown as SessionForkOriginArgs;
125
+ "session.fork.origin.read": (input: HandlerInput): SessionForkOriginReadResult => {
126
+ const args = input.args as unknown as SessionForkOriginReadArgs;
127
127
  const origin = forkOrigin(args.sid, deps.files);
128
128
  return origin === undefined ? {} : { origin };
129
129
  },
130
130
 
131
- session_last_live_remove: (input: HandlerInput): SessionLastLiveRemoveResult => {
132
- const args = input.args as unknown as SessionLastLiveRemoveArgs;
131
+ "session.forget": (input: HandlerInput): SessionForgetResult => {
132
+ const args = input.args as unknown as SessionForgetArgs;
133
133
  // An unknown session is not an error: two clients pressing the same
134
134
  // button is the ordinary case, and the caller's goal holds either way.
135
135
  return { removed: deps.forget(args.sid) };
136
136
  },
137
137
 
138
- transcript_read: (input: HandlerInput): TranscriptReadResult => {
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
141
  // The role sets the visible range, not the permission (§3.2): outside
@@ -153,7 +153,7 @@ export function sessionHandlers(deps: SessionOpsDeps) {
153
153
  * The role narrows it the way it narrows the raw read: what a role may see
154
154
  * is one rule whatever is being read, and a caller that cannot see a
155
155
  * session cannot see it in either vocabulary. */
156
- transcript_items_read: (input: HandlerInput): TranscriptItemsReadResult => {
156
+ "transcript.items.read": (input: HandlerInput): TranscriptItemsReadResult => {
157
157
  const args = input.args as unknown as TranscriptItemsReadArgs;
158
158
  if (!sees(args.sid, viewer(input))) {
159
159
  throw new OpError("not_found", `no transcript is known for ${args.sid}`);
@@ -164,7 +164,7 @@ export class HarnessSessions implements OwnSessions {
164
164
  *
165
165
  * Every answer comes from here rather than from anything the watch left
166
166
  * behind. Which sessions exist is an input to the classification (§5.1), and
167
- * classifying happens inside `message_send`'s decision and inside the
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
170
170
  * somebody being subscribed. The ops that signal a session's process read it
@@ -106,7 +106,7 @@ export class LastLiveStore {
106
106
  this.#save();
107
107
  }
108
108
 
109
- /** Drop one entry: `session_last_live_remove`, and a session registering
109
+ /** Drop one entry: `session.forget`, and a session registering
110
110
  * again, which is what moves it back to the connected list. */
111
111
  remove(sid: Sid): boolean {
112
112
  if (!this.#entries.delete(sid)) return false;