@ccmsg/protocol 1.20.0 → 1.22.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.
@@ -3,7 +3,7 @@ import { request, response } from "../envelope.ts";
3
3
  import { Capability, Endpoint, InstanceId, Role, Sid, Timestamp } from "../identifiers.ts";
4
4
  import { SessionMetaFields } from "../session-meta.ts";
5
5
 
6
- /** The mesh handshake's opening claim, carried by a `role: "instance"` hello.
6
+ /** The mesh handshake's opening claim, carried by `hello.instance`.
7
7
  *
8
8
  * It is not signed and proves nothing on its own: it names the peer and says
9
9
  * where its one-off key can be fetched. The proof that binds this connection to
@@ -36,42 +36,39 @@ export const MeshHello = Type.Object(
36
36
  );
37
37
  export type MeshHello = Static<typeof MeshHello>;
38
38
 
39
- /** The greeting that settles what a connection is.
40
- *
41
- * Which of the fields below are required is decided by `role`, which no single
42
- * object schema can state so the instance checks it, and a greeting that
43
- * breaks one of these is refused with `invalid_args`:
44
- *
45
- * - `role: "session"` carries `sid`.
46
- * - `role: "user"` carries no `sid`: a person speaks for no one session, and a
47
- * greeting that names one is refused rather than quietly ignored.
48
- * - `role: "instance"` carries `mesh`.
39
+ /** What every greeting carries, whichever of the three it is. */
40
+ const GREETING_FIELDS = {
41
+ /** The generation the caller speaks. A hello announcing another generation
42
+ * is refused with `bad_request`; there is no path that serves it anyway. */
43
+ protocol_version: Type.Integer({ minimum: 1 }),
44
+ /** The client build, for display in diagnostics. Nothing gates on it. */
45
+ client_version: Type.Optional(Type.String()),
46
+ };
47
+
48
+ /** The greeting that settles what a connection is. There is one op per role,
49
+ * so what a greeting must carry is the schema's to state rather than a check
50
+ * the instance runs against a `role` field: a session names its `sid`, an
51
+ * instance its mesh claim, and a person neither.
49
52
  *
50
- * A field that belongs to another role is as much a refusal as a missing one:
53
+ * A field that belongs to another role is as much a refusal as a missing one
51
54
  * `mesh` on a session greeting says the caller has confused which handshake it
52
- * is in, and accepting it would leave the connection settled as something
53
- * neither side meant.
55
+ * is in which is what the three ops keep apart: a greeting arrives under the
56
+ * name of the thing it settles, and there is no state in which the name and the
57
+ * fields disagree.
54
58
  *
55
59
  * A session's meta (`cwd`, `repo_root`, `transcript_path`, `title`, ...) is
56
60
  * taken field by field: a greeting that leaves a field out does not withdraw
57
61
  * it, and the instance keeps what it already knows for that `sid`. One session
58
62
  * reaches an instance as a run of short-lived processes (a session-start hook,
59
63
  * a `post`, a session-end hook), none of which knows every field. */
60
- export const HelloArgs = Type.Object({
61
- role: Role,
62
- /** The generation the caller speaks. A hello announcing another generation
63
- * is refused with `bad_request`; there is no path that serves it anyway. */
64
- protocol_version: Type.Integer({ minimum: 1 }),
65
- /** Required for `role: "session"`: the session the connection speaks for. */
66
- sid: Type.Optional(Sid),
67
- /** Required for `role: "instance"`: the mesh handshake claim. */
68
- mesh: Type.Optional(MeshHello),
69
- /** The client build, for display in diagnostics. Nothing gates on it. */
70
- client_version: Type.Optional(Type.String()),
71
- /** What a `role: "session"` connection says about itself. All optional: a
72
- * session states what it knows, and the instance derives or leaves unknown
73
- * what it is not told. The instance repeats these on the `peers` topic, so
74
- * they are the same fields under the same names there. */
64
+ export const HelloSessionArgs = Type.Object({
65
+ ...GREETING_FIELDS,
66
+ /** The session the connection speaks for. */
67
+ sid: Sid,
68
+ /** What the session says about itself. All optional: a session states what it
69
+ * knows, and the instance derives or leaves unknown what it is not told. The
70
+ * instance repeats these on the `peers` topic, so they are the same fields
71
+ * under the same names there. */
75
72
  repo: Type.Optional(SessionMetaFields.repo),
76
73
  ws: Type.Optional(SessionMetaFields.ws),
77
74
  cwd: Type.Optional(SessionMetaFields.cwd),
@@ -82,9 +79,22 @@ export const HelloArgs = Type.Object({
82
79
  model: Type.Optional(SessionMetaFields.model),
83
80
  effort: Type.Optional(SessionMetaFields.effort),
84
81
  });
85
- export type HelloArgs = Static<typeof HelloArgs>;
82
+ export type HelloSessionArgs = Static<typeof HelloSessionArgs>;
83
+
84
+ /** A person greeting. It names no session: a person speaks for none of them,
85
+ * and the sessions they may act on are decided by the op table rather than by
86
+ * anything settled here. */
87
+ export const HelloUserArgs = Type.Object(GREETING_FIELDS);
88
+ export type HelloUserArgs = Static<typeof HelloUserArgs>;
89
+
90
+ /** A peer instance greeting, which opens the mesh handshake. */
91
+ export const HelloInstanceArgs = Type.Object({
92
+ ...GREETING_FIELDS,
93
+ mesh: MeshHello,
94
+ });
95
+ export type HelloInstanceArgs = Static<typeof HelloInstanceArgs>;
86
96
 
87
- /** One instance as seen from the instance answering `hello`. */
97
+ /** One instance as seen from the instance answering a greeting. */
88
98
  export const InstanceInfo = Type.Object(
89
99
  {
90
100
  /** Absent until the handshake with it has settled: an endpoint an operator
@@ -148,10 +158,38 @@ export const HelloResult = Type.Object({
148
158
  * closes it. Present on a connection an access token opened; absent where
149
159
  * reaching the instance is itself the permission (the Unix socket) or where
150
160
  * the connection is a mesh link. The person's client renews before this
151
- * instant with `auth_refresh` rather than reconnecting. */
161
+ * instant with `auth.extend` rather than reconnecting. */
152
162
  auth_expires_at: Type.Optional(Timestamp),
153
163
  });
154
164
  export type HelloResult = Static<typeof HelloResult>;
155
165
 
156
- export const HelloRequest = request("hello", HelloArgs);
157
- export const HelloResponse = response("hello", HelloResult);
166
+ export const HelloSessionRequest = request("hello.session", HelloSessionArgs);
167
+ export const HelloSessionResponse = response("hello.session", HelloResult);
168
+
169
+ export const HelloUserRequest = request("hello.user", HelloUserArgs);
170
+ export const HelloUserResponse = response("hello.user", HelloResult);
171
+
172
+ export const HelloInstanceRequest = request("hello.instance", HelloInstanceArgs);
173
+ export const HelloInstanceResponse = response("hello.instance", HelloResult);
174
+
175
+ /** The role a greeting settles, read from the op that carried it.
176
+ *
177
+ * The role itself stays — the op table and the topic table are written against
178
+ * it, and a forwarded request names it in `caller` — and what a greeting no
179
+ * longer carries is a second place to say it. */
180
+ export const HELLO_OPS = {
181
+ "hello.session": "session",
182
+ "hello.user": "user",
183
+ "hello.instance": "instance",
184
+ } as const satisfies Record<string, Role>;
185
+
186
+ export type HelloOp = keyof typeof HELLO_OPS;
187
+
188
+ export function isHelloOp(op: string): op is HelloOp {
189
+ return op in HELLO_OPS;
190
+ }
191
+
192
+ /** The role a connection speaks once `op` has been answered. */
193
+ export function helloRole(op: HelloOp): Role {
194
+ return HELLO_OPS[op];
195
+ }
@@ -33,5 +33,5 @@ export const InstancePingResult = Type.Object({
33
33
  });
34
34
  export type InstancePingResult = Static<typeof InstancePingResult>;
35
35
 
36
- export const InstancePingRequest = request("instance_ping", InstancePingArgs);
37
- export const InstancePingResponse = response("instance_ping", InstancePingResult);
36
+ export const InstancePingRequest = request("instance.ping", InstancePingArgs);
37
+ export const InstancePingResponse = response("instance.ping", InstancePingResult);
@@ -11,8 +11,8 @@ export type InstanceShutdownArgs = Static<typeof InstanceShutdownArgs>;
11
11
  export const InstanceShutdownResult = Type.Object({});
12
12
  export type InstanceShutdownResult = Static<typeof InstanceShutdownResult>;
13
13
 
14
- export const InstanceShutdownRequest = request("instance_shutdown", InstanceShutdownArgs);
15
- export const InstanceShutdownResponse = response("instance_shutdown", InstanceShutdownResult);
14
+ export const InstanceShutdownRequest = request("instance.shutdown", InstanceShutdownArgs);
15
+ export const InstanceShutdownResponse = response("instance.shutdown", InstanceShutdownResult);
16
16
 
17
17
  /** A session saying it is about to go, so that its disconnection reads as a
18
18
  * pause rather than a loss.
@@ -44,5 +44,5 @@ export const SessionStoppingResult = Type.Object({
44
44
  });
45
45
  export type SessionStoppingResult = Static<typeof SessionStoppingResult>;
46
46
 
47
- export const SessionStoppingRequest = request("session_stopping", SessionStoppingArgs);
48
- export const SessionStoppingResponse = response("session_stopping", SessionStoppingResult);
47
+ export const SessionStoppingRequest = request("session.stopping", SessionStoppingArgs);
48
+ export const SessionStoppingResponse = response("session.stopping", SessionStoppingResult);
@@ -9,14 +9,14 @@ export const PLAIN_TOPICS = [
9
9
  "peers",
10
10
  "instances",
11
11
  "agents",
12
- "session_errors",
13
- "llm_requests",
14
- "llm_status",
15
- "auth_records",
12
+ "session.errors",
13
+ "llm.requests",
14
+ "llm.status",
15
+ "auth.records",
16
16
  ] as const;
17
17
 
18
18
  /** Topics naming one session, written `<topic>:<sid>`. */
19
- export const SESSION_SCOPED_TOPICS = ["session_status", "transcript", "transcript_items"] as const;
19
+ export const SESSION_SCOPED_TOPICS = ["session.status", "transcript", "transcript.items"] as const;
20
20
 
21
21
  /** Topics naming one namespace, written `<topic>:<ns>`. The parameter is a name
22
22
  * its users choose rather than an identifier this contract issues, so it is
@@ -36,13 +36,18 @@ export type TopicName =
36
36
  | `${SessionScopedTopic}:${string}`
37
37
  | `${NamespaceScopedTopic}:${string}`;
38
38
 
39
+ /** A topic name is a `.`-separated hierarchy, so its dots are literal here
40
+ * rather than the regexp's any-character. */
41
+ const alternation = (topics: readonly string[]): string =>
42
+ topics.map((topic) => topic.replace(/\./g, "\\.")).join("|");
43
+
39
44
  export const Topic = Type.String({
40
45
  $id: "Topic",
41
46
  pattern: [
42
47
  "^(?:",
43
- PLAIN_TOPICS.join("|"),
44
- `|(?:${SESSION_SCOPED_TOPICS.join("|")}):[0-9a-f-]{36}`,
45
- `|(?:${NAMESPACE_SCOPED_TOPICS.join("|")}):${NAMESPACE_PATTERN}`,
48
+ alternation(PLAIN_TOPICS),
49
+ `|(?:${alternation(SESSION_SCOPED_TOPICS)}):[0-9a-f-]{36}`,
50
+ `|(?:${alternation(NAMESPACE_SCOPED_TOPICS)}):${NAMESPACE_PATTERN}`,
46
51
  ")$",
47
52
  ].join(""),
48
53
  });
@@ -101,27 +106,27 @@ export const TOPIC_ATTRIBUTES = {
101
106
  // A set the instance derives whole, by folding one error pattern over its
102
107
  // sessions: it learns which sessions are stopped, not that one of them
103
108
  // changed, so each frame is that reading entire.
104
- session_errors: { roles: ["user"], granularity: "per_instance_whole" },
105
- llm_requests: {
109
+ "session.errors": { roles: ["user"], granularity: "per_instance_whole" },
110
+ "llm.requests": {
106
111
  roles: ["user"],
107
112
  capability: "llm_events",
108
113
  granularity: "per_instance_whole",
109
114
  },
110
- llm_status: { roles: ["user"], capability: "llm_status", granularity: "per_instance_whole" },
115
+ "llm.status": { roles: ["user"], capability: "llm_status", granularity: "per_instance_whole" },
111
116
  // One session lives on one instance, so its status has no other instance's
112
117
  // half to leave alone: the frame is simply the whole of it.
113
- session_status: { roles: ["user"], granularity: "whole" },
118
+ "session.status": { roles: ["user"], granularity: "whole" },
114
119
  transcript: { roles: ["user"], granularity: "append" },
115
120
  // The same appending, in items rather than in bytes. Both are offered because
116
121
  // they answer different needs: one draws the conversation, the other shows a
117
122
  // record as it was written.
118
- transcript_items: { roles: ["user"], granularity: "append" },
123
+ "transcript.items": { roles: ["user"], granularity: "append" },
119
124
  kv: { roles: ["user"], granularity: "element" },
120
125
  // The only topic no person may subscribe to: its elements are the secrets
121
126
  // that authenticate them. A relay carries it as the instance it is, not on a
122
127
  // caller's behalf, so there is no path by which a person's subscription
123
128
  // reaches it.
124
- auth_records: { roles: ["instance"], granularity: "element" },
129
+ "auth.records": { roles: ["instance"], granularity: "element" },
125
130
  } as const satisfies Record<
126
131
  PlainTopic | SessionScopedTopic | NamespaceScopedTopic,
127
132
  TopicAttributes
@@ -136,7 +141,7 @@ export function topicKind(topic: string): TopicKind | undefined {
136
141
  }
137
142
 
138
143
  /** How the frames of a topic name fold, taken from the name a subscriber
139
- * actually uses — `session_status:<sid>` rather than the kind behind it.
144
+ * actually uses — `session.status:<sid>` rather than the kind behind it.
140
145
  * `undefined` for a name this generation does not define. */
141
146
  export function topicGranularity(topic: string): TopicGranularity | undefined {
142
147
  const kind = topicKind(topic);
@@ -152,8 +157,8 @@ export type TopicSubscribeArgs = Static<typeof TopicSubscribeArgs>;
152
157
  export const TopicSubscribeResult = Type.Object({ topic: Topic });
153
158
  export type TopicSubscribeResult = Static<typeof TopicSubscribeResult>;
154
159
 
155
- export const TopicSubscribeRequest = request("topic_subscribe", TopicSubscribeArgs);
156
- export const TopicSubscribeResponse = response("topic_subscribe", TopicSubscribeResult);
160
+ export const TopicSubscribeRequest = request("topic.subscribe", TopicSubscribeArgs);
161
+ export const TopicSubscribeResponse = response("topic.subscribe", TopicSubscribeResult);
157
162
 
158
163
  export const TopicUnsubscribeArgs = Type.Object({ topic: Topic });
159
164
  export type TopicUnsubscribeArgs = Static<typeof TopicUnsubscribeArgs>;
@@ -161,5 +166,5 @@ export type TopicUnsubscribeArgs = Static<typeof TopicUnsubscribeArgs>;
161
166
  export const TopicUnsubscribeResult = Type.Object({ topic: Topic });
162
167
  export type TopicUnsubscribeResult = Static<typeof TopicUnsubscribeResult>;
163
168
 
164
- export const TopicUnsubscribeRequest = request("topic_unsubscribe", TopicUnsubscribeArgs);
165
- export const TopicUnsubscribeResponse = response("topic_unsubscribe", TopicUnsubscribeResult);
169
+ export const TopicUnsubscribeRequest = request("topic.unsubscribe", TopicUnsubscribeArgs);
170
+ export const TopicUnsubscribeResponse = response("topic.unsubscribe", TopicUnsubscribeResult);
@@ -13,8 +13,8 @@ import { Sid, Timestamp } from "../identifiers.ts";
13
13
  * down here. That split is what lets a harness change its file, or a second
14
14
  * harness be read at all, without the contract moving.
15
15
  *
16
- * A type name is `:`-separated and read left to right, so a prefix names
17
- * everything below it: `tool` is every tool, `message:user` is both directions
16
+ * A type name is `.`-separated and read left to right, so a prefix names
17
+ * everything below it: `tool` is every tool, `message.user` is both directions
18
18
  * of what a person and a session said. Three families stay open, because their
19
19
  * last segment is a name someone else coins — a tool, an attachment kind, a
20
20
  * hook event — and closing them would turn every newcomer into `unknown`. */
@@ -22,49 +22,56 @@ import { Sid, Timestamp } from "../identifiers.ts";
22
22
  /** A type name as written on the wire and in a selection.
23
23
  *
24
24
  * Segments after the first carry the spelling of whatever named them, which is
25
- * why they are not held to snake_case: `tool:Bash` and `hook:PreToolUse` are
25
+ * why they are not held to snake_case: `tool.Bash` and `hook.PreToolUse` are
26
26
  * the harness's words, and rewriting them would leave the reader unable to
27
- * match what it sees against what it ran.
27
+ * match what it sees against what it ran. That segment carries no `.` of its
28
+ * own — a harness name holding one is written with `_` by whoever coins the
29
+ * type — so a reader may split any type name on `.` and get the hierarchy.
30
+ *
31
+ * `tool.unknown` is the reserved name for a call whose tool the reader could
32
+ * not name. It is a coined segment like any other, so a harness tool actually
33
+ * called `unknown` lands on it; what is lost is that distinction and nothing
34
+ * else, where dropping the item would lose the call.
28
35
  *
29
36
  * Under `message`, the second segment is **a relation read from the subject**:
30
37
  * `parent` is whoever started this agent, `sub` a throwaway agent it started,
31
38
  * `team` a named counterpart that goes on standing, `session` another session
32
39
  * over ccmsg. The one exception is `user` — a person is not a relation to
33
40
  * anyone, but the user, standing alone. A harness's own name for a party is
34
- * never a type: `message:main` would read as the main session's traffic being
41
+ * never a type: `message.main` would read as the main session's traffic being
35
42
  * overheard wherever it happens, when what is meant is the party this subject
36
43
  * answers to — which is what `parent` says. The literal names (`main`, a
37
44
  * lead's, a teammate's) are kept in `harness_name` on the item. */
38
45
  export const TranscriptItemType = Type.String({
39
- pattern: "^[a-z]+(?::[A-Za-z0-9_.-]+)*$",
46
+ pattern: "^[a-z][a-z0-9_]*(?:\\.[A-Za-z0-9_-]+)*$",
40
47
  $id: "TranscriptItemType",
41
48
  });
42
49
  export type TranscriptItemType = Static<typeof TranscriptItemType>;
43
50
 
44
51
  /** The type names that are fully spelled out here. The three open families
45
- * (`tool:<Name>`, `system:attachment:<kind>`, `hook:<Event>`) are not in the
52
+ * (`tool.<Name>`, `system.attachment.<kind>`, `hook.<Event>`) are not in the
46
53
  * list: their last segment is coined elsewhere, and a name absent from this
47
54
  * list is a newcomer rather than an error. */
48
55
  export const TRANSCRIPT_ITEM_TYPES = [
49
- "message:user:in",
50
- "message:user:out",
51
- "message:parent:in",
52
- "message:parent:out",
53
- "message:sub:in",
54
- "message:sub:out",
55
- "message:team:in",
56
- "message:team:out",
57
- "message:session:in",
58
- "message:session:out",
56
+ "message.user.in",
57
+ "message.user.out",
58
+ "message.parent.in",
59
+ "message.parent.out",
60
+ "message.sub.in",
61
+ "message.sub.out",
62
+ "message.team.in",
63
+ "message.team.out",
64
+ "message.session.in",
65
+ "message.session.out",
59
66
  "thinking",
60
- "notice:slash",
61
- "notice:interrupt",
62
- "system:compact",
63
- "system:api-error",
64
- "system:task",
65
- "system:caveat",
66
- "system:resume",
67
- "system:unknown",
67
+ "notice.slash",
68
+ "notice.interrupt",
69
+ "system.compact",
70
+ "system.api.error",
71
+ "system.task",
72
+ "system.caveat",
73
+ "system.resume",
74
+ "system.unknown",
68
75
  ] as const;
69
76
  export type KnownTranscriptItemType = (typeof TRANSCRIPT_ITEM_TYPES)[number];
70
77
 
@@ -74,7 +81,7 @@ export type KnownTranscriptItemType = (typeof TRANSCRIPT_ITEM_TYPES)[number];
74
81
  * Elements apply left to right, so an exclusion reaches whatever a prefix or an
75
82
  * expansion before it brought in. */
76
83
  export const TranscriptItemSelector = Type.String({
77
- pattern: "^-?(?:@[A-Za-z0-9][A-Za-z0-9_-]*|[a-z]+(?::[A-Za-z0-9_.-]+)*)$",
84
+ pattern: "^-?(?:@[A-Za-z0-9][A-Za-z0-9_-]*|[a-z][a-z0-9_]*(?:\\.[A-Za-z0-9_-]+)*)$",
78
85
  $id: "TranscriptItemSelector",
79
86
  });
80
87
  export type TranscriptItemSelector = Static<typeof TranscriptItemSelector>;
@@ -84,7 +91,7 @@ export type TranscriptItemSelector = Static<typeof TranscriptItemSelector>;
84
91
  * started to answer once — and `team` a teammate's, an agent that was named and
85
92
  * goes on standing.
86
93
  *
87
- * It is what the relations are read from. `message:parent:in` under a `sub` is
94
+ * It is what the relations are read from. `message.parent.in` under a `sub` is
88
95
  * an errand's brief and under a `team` is what its lead wrote, and an item that
89
96
  * does not say which of the two it stood in can only be placed by whoever
90
97
  * remembers the request that fetched it. Carrying it on the item is what lets
@@ -105,7 +112,7 @@ export type TranscriptSubject = Static<typeof TranscriptSubject>;
105
112
  *
106
113
  * An item is what a reader made of a record, and a reader is fallible: the one
107
114
  * question it cannot answer is what the record actually said. These two numbers
108
- * are that answer's address — `transcript_read` bounded to end at `offset +
115
+ * are that answer's address — `transcript.read` bounded to end at `offset +
109
116
  * bytes` and to carry `bytes` returns the record itself — which is what lets a
110
117
  * client show items and still let a person open the line behind one. Several
111
118
  * items read out of a single record share the address, so what comes back is
@@ -208,24 +215,24 @@ const Text = Type.String();
208
215
  * the one above: an agent's parent is a session or another agent, and calling it
209
216
  * `user` would have a reader take a machine for a person.
210
217
  *
211
- * `message:user` is a person and nobody else. Both directions occur under a
218
+ * `message.user` is a person and nobody else. Both directions occur under a
212
219
  * session and under a teammate, which someone can type at directly; under a
213
220
  * throwaway agent neither does. A combination a subject is not expected to show
214
221
  * — `team` below an agent, say — is not refused: an unexpected line is still a
215
222
  * line, and it is emitted under the name it fits. */
216
- const MessageUserIn = item(Type.Literal("message:user:in"), { text: Text });
217
- const MessageUserOut = item(Type.Literal("message:user:out"), { text: Text });
223
+ const MessageUserIn = item(Type.Literal("message.user.in"), { text: Text });
224
+ const MessageUserOut = item(Type.Literal("message.user.out"), { text: Text });
218
225
 
219
226
  /** What the one above said, and what was said back to it.
220
227
  *
221
228
  * An agent's first line is the brief it was started with, and its last is the
222
229
  * answer that brief is discharged by; in between it may hand its parent
223
230
  * something mid-flight. The answer is plain prose the harness collects, with no
224
- * call behind it, so `parent:out` is prose-or-call and not a call alone:
231
+ * call behind it, so `parent.out` is prose-or-call and not a call alone:
225
232
  * addressed to the parent is what the two forms have in common, and requiring a
226
233
  * `tool_use_id` would leave the one message an agent is certain to send
227
234
  * unnameable. */
228
- const MessageParentIn = item(Type.Literal("message:parent:in"), {
235
+ const MessageParentIn = item(Type.Literal("message.parent.in"), {
229
236
  text: Text,
230
237
  /** The harness's own name for the parent — `main`, a lead's name, the agent
231
238
  * above. Left out when the record says only that it came from above, which is
@@ -236,7 +243,7 @@ const MessageParentIn = item(Type.Literal("message:parent:in"), {
236
243
 
237
244
  /** Sent to the parent through a call, which the parent's own transcript has the
238
245
  * other half of. */
239
- const MessageParentOutSent = item(Type.Literal("message:parent:out"), {
246
+ const MessageParentOutSent = item(Type.Literal("message.parent.out"), {
240
247
  ...USE_FIELDS,
241
248
  text: Text,
242
249
  /** The harness's own name for the parent, as the subject addressed it. */
@@ -245,7 +252,7 @@ const MessageParentOutSent = item(Type.Literal("message:parent:out"), {
245
252
  });
246
253
 
247
254
  /** Answered to the parent as prose — the agent's reply, final or interim. */
248
- const MessageParentOutSaid = item(Type.Literal("message:parent:out"), { text: Text });
255
+ const MessageParentOutSaid = item(Type.Literal("message.parent.out"), { text: Text });
249
256
 
250
257
  /** A teammate is an agent that was given a name and goes on standing, so what
251
258
  * passes between the subject and one is a correspondence rather than an errand:
@@ -253,10 +260,10 @@ const MessageParentOutSaid = item(Type.Literal("message:parent:out"), { text: Te
253
260
  * written, and not as the answer to the call that sent it.
254
261
  *
255
262
  * That is the whole of what separates `team` from `sub`. A throwaway agent is
256
- * started, answers once and is done, which is why `sub:in` is the result of the
257
- * `sub:out` that started it. Here the two halves of a round trip are two
263
+ * started, answers once and is done, which is why `sub.in` is the result of the
264
+ * `sub.out` that started it. Here the two halves of a round trip are two
258
265
  * messages, and only the start of a teammate has a result to pair with. */
259
- const MessageTeamOut = item(Type.Literal("message:team:out"), {
266
+ const MessageTeamOut = item(Type.Literal("message.team.out"), {
260
267
  ...USE_FIELDS,
261
268
  text: Text,
262
269
  /** The teammate addressed, by the name it stands under. */
@@ -271,7 +278,7 @@ const MessageTeamOut = item(Type.Literal("message:team:out"), {
271
278
 
272
279
  /** A teammate writing to the subject, arriving under its own name whenever it
273
280
  * was written. */
274
- const MessageTeamInSaid = item(Type.Literal("message:team:in"), {
281
+ const MessageTeamInSaid = item(Type.Literal("message.team.in"), {
275
282
  text: Text,
276
283
  /** The name the teammate stands under. */
277
284
  harness_name: Type.Optional(Type.String()),
@@ -279,7 +286,7 @@ const MessageTeamInSaid = item(Type.Literal("message:team:in"), {
279
286
  });
280
287
 
281
288
  /** A teammate's run ending, which answers the call that started it. */
282
- const MessageTeamInDone = item(Type.Literal("message:team:in"), {
289
+ const MessageTeamInDone = item(Type.Literal("message.team.in"), {
283
290
  ...RESULT_FIELDS,
284
291
  text: Text,
285
292
  harness_name: Type.Optional(Type.String()),
@@ -288,7 +295,7 @@ const MessageTeamInDone = item(Type.Literal("message:team:in"), {
288
295
  duration_ms: Type.Optional(Type.Integer({ minimum: 0 })),
289
296
  });
290
297
 
291
- const MessageSubOut = item(Type.Literal("message:sub:out"), {
298
+ const MessageSubOut = item(Type.Literal("message.sub.out"), {
292
299
  ...USE_FIELDS,
293
300
  prompt: Text,
294
301
  agent_id: Type.Optional(Type.String()),
@@ -298,7 +305,7 @@ const MessageSubOut = item(Type.Literal("message:sub:out"), {
298
305
  description: Type.Optional(Type.String()),
299
306
  });
300
307
 
301
- const MessageSubIn = item(Type.Literal("message:sub:in"), {
308
+ const MessageSubIn = item(Type.Literal("message.sub.in"), {
302
309
  ...RESULT_FIELDS,
303
310
  text: Text,
304
311
  agent_id: Type.Optional(Type.String()),
@@ -306,7 +313,7 @@ const MessageSubIn = item(Type.Literal("message:sub:in"), {
306
313
  duration_ms: Type.Optional(Type.Integer({ minimum: 0 })),
307
314
  });
308
315
 
309
- const MessageSessionOut = item(Type.Literal("message:session:out"), {
316
+ const MessageSessionOut = item(Type.Literal("message.session.out"), {
310
317
  text: Text,
311
318
  /** The addressee as the subject wrote it: a sid, or a name that was resolved
312
319
  * to one. Kept unresolved when that is all the transcript says. */
@@ -315,7 +322,7 @@ const MessageSessionOut = item(Type.Literal("message:session:out"), {
315
322
  reply_to: Type.Optional(Type.String()),
316
323
  });
317
324
 
318
- const MessageSessionIn = item(Type.Literal("message:session:in"), {
325
+ const MessageSessionIn = item(Type.Literal("message.session.in"), {
319
326
  text: Text,
320
327
  from: Type.Optional(Type.String()),
321
328
  msg_id: Type.Optional(Type.String()),
@@ -325,52 +332,52 @@ const Thinking = item(Type.Literal("thinking"), { text: Text });
325
332
 
326
333
  // --- notice: a person operated the harness ---
327
334
 
328
- /** Kept apart from `system:*` because these explain a break in the
335
+ /** Kept apart from `system.*` because these explain a break in the
329
336
  * conversation: someone typed a command or stopped a turn. A reader skimming
330
337
  * for why the thread jumps needs them, and can skip what the harness injected
331
338
  * for its own reasons. */
332
- const NoticeSlash = item(Type.Literal("notice:slash"), {
339
+ const NoticeSlash = item(Type.Literal("notice.slash"), {
333
340
  command: Type.String(),
334
341
  args: Type.Optional(Type.String()),
335
342
  stdout: Type.Optional(Type.String()),
336
343
  });
337
- const NoticeInterrupt = item(Type.Literal("notice:interrupt"), {
344
+ const NoticeInterrupt = item(Type.Literal("notice.interrupt"), {
338
345
  text: Type.Optional(Text),
339
346
  });
340
347
 
341
348
  // --- system: the harness talking in someone else's voice ---
342
349
 
343
- const SystemCompact = item(Type.Literal("system:compact"), { text: Text });
344
- const SystemApiError = item(Type.Literal("system:api-error"), { text: Text });
345
- const SystemTask = item(Type.Literal("system:task"), {
350
+ const SystemCompact = item(Type.Literal("system.compact"), { text: Text });
351
+ const SystemApiError = item(Type.Literal("system.api.error"), { text: Text });
352
+ const SystemTask = item(Type.Literal("system.task"), {
346
353
  text: Text,
347
354
  /** The background task or monitor the event came from. */
348
355
  task_id: Type.Optional(Type.String()),
349
356
  event: Type.Optional(Type.String()),
350
357
  });
351
- const SystemCaveat = item(Type.Literal("system:caveat"), { text: Text });
352
- const SystemResume = item(Type.Literal("system:resume"), { text: Text });
358
+ const SystemCaveat = item(Type.Literal("system.caveat"), { text: Text });
359
+ const SystemResume = item(Type.Literal("system.resume"), { text: Text });
353
360
 
354
- /** `system:attachment:<kind>` — the kind is the harness's own word for what it
361
+ /** `system.attachment.<kind>` — the kind is the harness's own word for what it
355
362
  * attached, taken through unchanged so an attachment nobody has seen before
356
363
  * still arrives under its own name instead of collapsing into `unknown`. */
357
- const SystemAttachment = item(Type.String({ pattern: "^system:attachment:[A-Za-z0-9_.-]+$" }), {
364
+ const SystemAttachment = item(Type.String({ pattern: "^system\\.attachment\\.[A-Za-z0-9_-]+$" }), {
358
365
  attachment: Type.Record(Type.String(), Type.Unknown()),
359
366
  });
360
367
 
361
368
  /** What the reader could not place. It is still an item: a line that vanishes
362
369
  * silently is the one failure a dump cannot be read around. */
363
- const SystemUnknown = item(Type.Literal("system:unknown"), {
370
+ const SystemUnknown = item(Type.Literal("system.unknown"), {
364
371
  record: Type.Record(Type.String(), Type.Unknown()),
365
372
  });
366
373
 
367
374
  // --- hook: code the operator installed ---
368
375
 
369
- /** `hook:<Event>` — the event alone, never the matcher. The name a hook runs
376
+ /** `hook.<Event>` — the event alone, never the matcher. The name a hook runs
370
377
  * under is `PreToolUse:Bash`, whose `:` would read as a level of the hierarchy
371
- * and make `hook:PreToolUse` select nothing; the full name is a field instead,
378
+ * and make `hook.PreToolUse` select nothing; the full name is a field instead,
372
379
  * and prefix selection keeps meaning what it says. */
373
- const Hook = item(Type.String({ pattern: "^hook:[A-Za-z0-9_.-]+$" }), {
380
+ const Hook = item(Type.String({ pattern: "^hook\\.[A-Za-z0-9_-]+$" }), {
374
381
  /** The hook's full name, matcher included. */
375
382
  hook_name: Type.String(),
376
383
  outcome: Type.Union([
@@ -387,19 +394,19 @@ const Hook = item(Type.String({ pattern: "^hook:[A-Za-z0-9_.-]+$" }), {
387
394
  tool_use_id: Type.Optional(Type.String()),
388
395
  });
389
396
 
390
- // --- tool: `tool:<Name>`, one item for the call and one for the result ---
397
+ // --- tool: `tool.<Name>`, one item for the call and one for the result ---
391
398
 
392
- const ToolType = Type.String({ pattern: "^tool:[A-Za-z0-9_.-]+$" });
399
+ const ToolType = Type.String({ pattern: "^tool\\.[A-Za-z0-9_-]+$" });
393
400
 
394
401
  function toolUse<N extends string, F extends Record<string, TSchema>>(name: N, fields: F) {
395
- return item(Type.Literal(`tool:${name}` as const), {
402
+ return item(Type.Literal(`tool.${name}` as const), {
396
403
  ...USE_FIELDS,
397
404
  ...fields,
398
405
  });
399
406
  }
400
407
 
401
408
  function toolResult<N extends string, F extends Record<string, TSchema>>(name: N, fields: F) {
402
- return item(Type.Literal(`tool:${name}` as const), {
409
+ return item(Type.Literal(`tool.${name}` as const), {
403
410
  ...RESULT_FIELDS,
404
411
  ...fields,
405
412
  });
@@ -453,7 +460,7 @@ const TOOL_ITEMS = [
453
460
  toolResult("WebFetch", { text: OptText }),
454
461
  toolUse("WebSearch", { query: Type.String() }),
455
462
  toolResult("WebSearch", { results: OptCount }),
456
- /** The same exchange `message:sub:*` carries, seen from the calling side:
463
+ /** The same exchange `message.sub.*` carries, seen from the calling side:
457
464
  * this pair states that an agent was started and how it ended, and what it
458
465
  * answered stays with the message. */
459
466
  toolUse("Agent", {
@@ -493,7 +500,7 @@ const TOOL_ITEMS = [
493
500
  * Items are finer than lines: one assistant record becomes the thinking, the
494
501
  * text and each tool call it held. A reader that does not recognise a record
495
502
  * still emits one — as a tool it has no fields for, an attachment under its own
496
- * kind, or `system:unknown` — so nothing in the file goes missing without
503
+ * kind, or `system.unknown` — so nothing in the file goes missing without
497
504
  * saying so. */
498
505
  export const TranscriptItem = Type.Union(
499
506
  [
@@ -628,5 +635,5 @@ export const DumpPresetsReadResult = Type.Object({
628
635
  });
629
636
  export type DumpPresetsReadResult = Static<typeof DumpPresetsReadResult>;
630
637
 
631
- export const DumpPresetsReadRequest = request("dump_presets_read", DumpPresetsReadArgs);
632
- export const DumpPresetsReadResponse = response("dump_presets_read", DumpPresetsReadResult);
638
+ export const DumpPresetsReadRequest = request("dump.presets.read", DumpPresetsReadArgs);
639
+ export const DumpPresetsReadResponse = response("dump.presets.read", DumpPresetsReadResult);