@ccmsg/protocol 1.22.1 → 2.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/protocol",
3
- "version": "1.22.1",
3
+ "version": "2.0.0",
4
4
  "description": "Wire contract (schema + types + op attribute table) shared by the ccmsg daemon and web UI",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
package/src/attributes.ts CHANGED
@@ -8,10 +8,10 @@ import type { Capability, Role } from "./identifiers.ts";
8
8
  * different op. */
9
9
  export type Plane = "common" | "messaging" | "control" | "mesh";
10
10
 
11
- /** `instance-local` ops answer for one instance's processes, paths and
11
+ /** `owner_instance` ops answer for one instance's processes, paths and
12
12
  * handles, so they are forwarded to the instance that owns the subject.
13
- * `cluster` ops are answerable by whichever instance is asked. */
14
- export type Locality = "instance-local" | "cluster";
13
+ * `any_instance` ops are answerable by whichever instance is asked. */
14
+ export type Locality = "owner_instance" | "any_instance";
15
15
 
16
16
  export interface OpAttributes {
17
17
  readonly plane: Plane;
@@ -65,47 +65,47 @@ export const OP_ATTRIBUTES = {
65
65
  //
66
66
  // The greetings and `instance.ping` address the instance the caller reached, so
67
67
  // there is nothing to forward and no unreachable instance to report — which
68
- // is why they are `cluster` despite answering about one instance.
68
+ // is why they are `any_instance` despite answering about one instance.
69
69
  "hello.session": {
70
70
  plane: "common",
71
71
  roles: ALL_ROLES,
72
72
  needs_hello: false,
73
- locality: "cluster",
73
+ locality: "any_instance",
74
74
  errors: [],
75
75
  },
76
76
  "hello.user": {
77
77
  plane: "common",
78
78
  roles: ALL_ROLES,
79
79
  needs_hello: false,
80
- locality: "cluster",
80
+ locality: "any_instance",
81
81
  errors: [],
82
82
  },
83
83
  "hello.instance": {
84
84
  plane: "common",
85
85
  roles: ALL_ROLES,
86
86
  needs_hello: false,
87
- locality: "cluster",
87
+ locality: "any_instance",
88
88
  errors: [],
89
89
  },
90
90
  "instance.ping": {
91
91
  plane: "common",
92
92
  roles: ALL_ROLES,
93
93
  needs_hello: true,
94
- locality: "cluster",
94
+ locality: "any_instance",
95
95
  errors: [],
96
96
  },
97
97
  "instance.shutdown": {
98
98
  plane: "common",
99
99
  roles: USER_ONLY,
100
100
  needs_hello: true,
101
- locality: "instance-local",
101
+ locality: "owner_instance",
102
102
  errors: [],
103
103
  },
104
104
  "session.stopping": {
105
105
  plane: "common",
106
106
  roles: SESSION_ONLY,
107
107
  needs_hello: true,
108
- locality: "instance-local",
108
+ locality: "owner_instance",
109
109
  errors: [],
110
110
  },
111
111
  // Open to every role, with which role may have which topic left to the topic
@@ -115,20 +115,20 @@ export const OP_ATTRIBUTES = {
115
115
  plane: "common",
116
116
  roles: ALL_ROLES,
117
117
  needs_hello: true,
118
- locality: "cluster",
118
+ locality: "any_instance",
119
119
  errors: ["topic_unknown"],
120
120
  },
121
121
  "topic.unsubscribe": {
122
122
  plane: "common",
123
123
  roles: ALL_ROLES,
124
124
  needs_hello: true,
125
- locality: "cluster",
125
+ locality: "any_instance",
126
126
  errors: ["topic_unknown"],
127
127
  },
128
128
 
129
129
  // The four ops that authenticate a person are open to every role for the
130
130
  // same reason the greetings are: they run before there is an identity to
131
- // check, and what they answer is what settles one. They are `cluster` because whichever
131
+ // check, and what they answer is what settles one. They are `any_instance` because whichever
132
132
  // instance is reached answers — behind a load balancer that is not a choice
133
133
  // the caller makes — and each asks the issuing instance itself for the parts
134
134
  // only it holds.
@@ -136,7 +136,7 @@ export const OP_ATTRIBUTES = {
136
136
  plane: "common",
137
137
  roles: ALL_ROLES,
138
138
  needs_hello: false,
139
- locality: "cluster",
139
+ locality: "any_instance",
140
140
  carrier: "http",
141
141
  errors: [],
142
142
  },
@@ -144,7 +144,7 @@ export const OP_ATTRIBUTES = {
144
144
  plane: "common",
145
145
  roles: ALL_ROLES,
146
146
  needs_hello: false,
147
- locality: "cluster",
147
+ locality: "any_instance",
148
148
  carrier: "http",
149
149
  errors: ["auth_invalid", "auth_expired", "auth_unknown_issuer"],
150
150
  },
@@ -152,7 +152,7 @@ export const OP_ATTRIBUTES = {
152
152
  plane: "common",
153
153
  roles: ALL_ROLES,
154
154
  needs_hello: false,
155
- locality: "cluster",
155
+ locality: "any_instance",
156
156
  carrier: "http",
157
157
  errors: ["auth_invalid", "auth_expired", "auth_unknown_issuer"],
158
158
  },
@@ -160,7 +160,7 @@ export const OP_ATTRIBUTES = {
160
160
  plane: "common",
161
161
  roles: ALL_ROLES,
162
162
  needs_hello: false,
163
- locality: "cluster",
163
+ locality: "any_instance",
164
164
  carrier: "http",
165
165
  errors: ["auth_invalid", "auth_expired", "auth_unknown_issuer"],
166
166
  },
@@ -170,24 +170,24 @@ export const OP_ATTRIBUTES = {
170
170
  plane: "common",
171
171
  roles: USER_ONLY,
172
172
  needs_hello: true,
173
- locality: "cluster",
173
+ locality: "any_instance",
174
174
  errors: ["auth_invalid", "auth_expired"],
175
175
  },
176
- // Between instances: what an issuer alone can answer. Instance-local by the
176
+ // Between instances: what an issuer alone can answer. `owner_instance` by the
177
177
  // usual rule — the subject belongs to one instance, and it is reached by
178
178
  // `to_instance` being that instance's id.
179
179
  "auth.resolve": {
180
180
  plane: "common",
181
181
  roles: INSTANCE_ONLY,
182
182
  needs_hello: true,
183
- locality: "instance-local",
183
+ locality: "owner_instance",
184
184
  errors: ["auth_invalid", "auth_expired"],
185
185
  },
186
186
  "auth.rotate": {
187
187
  plane: "common",
188
188
  roles: INSTANCE_ONLY,
189
189
  needs_hello: true,
190
- locality: "instance-local",
190
+ locality: "owner_instance",
191
191
  errors: ["auth_invalid", "auth_expired"],
192
192
  },
193
193
 
@@ -196,29 +196,29 @@ export const OP_ATTRIBUTES = {
196
196
  plane: "messaging",
197
197
  roles: AGENT_AND_USER,
198
198
  needs_hello: true,
199
- locality: "cluster",
200
- errors: ["session_not_found"],
199
+ locality: "any_instance",
200
+ errors: ["session_not_found", "session_duplicated"],
201
201
  },
202
202
  "say.post": {
203
203
  plane: "messaging",
204
204
  roles: SESSION_ONLY,
205
205
  needs_hello: true,
206
- locality: "cluster",
206
+ locality: "any_instance",
207
207
  errors: ["rate_limited"],
208
208
  },
209
209
  "say.unread.clear": {
210
210
  plane: "messaging",
211
211
  roles: USER_ONLY,
212
212
  needs_hello: true,
213
- locality: "cluster",
213
+ locality: "any_instance",
214
214
  errors: [],
215
215
  },
216
216
  "notify.send": {
217
217
  plane: "messaging",
218
218
  roles: AGENT_AND_USER,
219
219
  needs_hello: true,
220
- locality: "cluster",
221
- errors: ["rate_limited"],
220
+ locality: "any_instance",
221
+ errors: ["rate_limited", "session_duplicated"],
222
222
  },
223
223
 
224
224
  // --- control: session observation and operation (10) ---
@@ -226,50 +226,50 @@ export const OP_ATTRIBUTES = {
226
226
  plane: "control",
227
227
  roles: USER_ONLY,
228
228
  needs_hello: true,
229
- locality: "instance-local",
230
- errors: ["session_not_found"],
229
+ locality: "owner_instance",
230
+ errors: ["session_not_found", "ambiguous_run"],
231
231
  },
232
232
  "session.rename": {
233
233
  plane: "control",
234
234
  roles: USER_ONLY,
235
235
  needs_hello: true,
236
236
  capability: "terminal",
237
- locality: "instance-local",
237
+ locality: "owner_instance",
238
238
  errors: ["session_not_found"],
239
239
  },
240
240
  "session.env.read": {
241
241
  plane: "control",
242
242
  roles: USER_ONLY,
243
243
  needs_hello: true,
244
- locality: "instance-local",
244
+ locality: "owner_instance",
245
245
  errors: ["session_not_found"],
246
246
  },
247
247
  "session.search": {
248
248
  plane: "control",
249
249
  roles: USER_ONLY,
250
250
  needs_hello: true,
251
- locality: "instance-local",
251
+ locality: "owner_instance",
252
252
  errors: [],
253
253
  },
254
254
  "session.dump.write": {
255
255
  plane: "control",
256
256
  roles: USER_ONLY,
257
257
  needs_hello: true,
258
- locality: "instance-local",
259
- errors: ["not_found"],
258
+ locality: "owner_instance",
259
+ errors: ["not_found", "session_duplicated"],
260
260
  },
261
261
  "dump.presets.read": {
262
262
  plane: "control",
263
263
  roles: AGENT_AND_USER,
264
264
  needs_hello: true,
265
- locality: "instance-local",
265
+ locality: "owner_instance",
266
266
  errors: [],
267
267
  },
268
268
  "transcript.read": {
269
269
  plane: "control",
270
270
  roles: AGENT_AND_USER,
271
271
  needs_hello: true,
272
- locality: "instance-local",
272
+ locality: "owner_instance",
273
273
  scope: "role",
274
274
  errors: ["not_found"],
275
275
  },
@@ -277,7 +277,7 @@ export const OP_ATTRIBUTES = {
277
277
  plane: "control",
278
278
  roles: AGENT_AND_USER,
279
279
  needs_hello: true,
280
- locality: "instance-local",
280
+ locality: "owner_instance",
281
281
  scope: "role",
282
282
  errors: ["not_found"],
283
283
  },
@@ -286,14 +286,14 @@ export const OP_ATTRIBUTES = {
286
286
  roles: USER_ONLY,
287
287
  needs_hello: true,
288
288
  capability: "fork",
289
- locality: "instance-local",
289
+ locality: "owner_instance",
290
290
  errors: ["not_found"],
291
291
  },
292
292
  "session.forget": {
293
293
  plane: "control",
294
294
  roles: USER_ONLY,
295
295
  needs_hello: true,
296
- locality: "instance-local",
296
+ locality: "owner_instance",
297
297
  errors: [],
298
298
  },
299
299
 
@@ -302,66 +302,66 @@ export const OP_ATTRIBUTES = {
302
302
  plane: "control",
303
303
  roles: AGENT_AND_USER,
304
304
  needs_hello: true,
305
- locality: "instance-local",
305
+ locality: "owner_instance",
306
306
  scope: "role",
307
- errors: ["path_forbidden", "not_found"],
307
+ errors: ["path_forbidden", "not_found", "session_duplicated"],
308
308
  },
309
309
  "file.read": {
310
310
  plane: "control",
311
311
  roles: AGENT_AND_USER,
312
312
  needs_hello: true,
313
- locality: "instance-local",
313
+ locality: "owner_instance",
314
314
  scope: "role",
315
- errors: ["path_forbidden", "not_found"],
315
+ errors: ["path_forbidden", "not_found", "session_duplicated"],
316
316
  },
317
317
  "file.write": {
318
318
  plane: "control",
319
319
  roles: USER_ONLY,
320
320
  needs_hello: true,
321
- locality: "instance-local",
322
- errors: ["path_not_writable", "file_exists"],
321
+ locality: "owner_instance",
322
+ errors: ["path_not_writable", "file_exists", "session_duplicated"],
323
323
  },
324
324
  "file.create": {
325
325
  plane: "control",
326
326
  roles: USER_ONLY,
327
327
  needs_hello: true,
328
- locality: "instance-local",
329
- errors: ["file_exists", "path_forbidden"],
328
+ locality: "owner_instance",
329
+ errors: ["file_exists", "path_forbidden", "session_duplicated"],
330
330
  },
331
331
  "file.edit": {
332
332
  plane: "control",
333
333
  roles: USER_ONLY,
334
334
  needs_hello: true,
335
- locality: "instance-local",
336
- errors: ["file_conflict", "not_a_text_file"],
335
+ locality: "owner_instance",
336
+ errors: ["file_conflict", "not_a_text_file", "session_duplicated"],
337
337
  },
338
338
  "file.delete": {
339
339
  plane: "control",
340
340
  roles: USER_ONLY,
341
341
  needs_hello: true,
342
- locality: "instance-local",
343
- errors: ["path_forbidden", "not_found"],
342
+ locality: "owner_instance",
343
+ errors: ["path_forbidden", "not_found", "session_duplicated"],
344
344
  },
345
345
  "file.find": {
346
346
  plane: "control",
347
347
  roles: USER_ONLY,
348
348
  needs_hello: true,
349
- locality: "instance-local",
350
- errors: ["path_forbidden"],
349
+ locality: "owner_instance",
350
+ errors: ["path_forbidden", "session_duplicated"],
351
351
  },
352
352
  "file.stat": {
353
353
  plane: "control",
354
354
  roles: USER_ONLY,
355
355
  needs_hello: true,
356
- locality: "instance-local",
357
- errors: [],
356
+ locality: "owner_instance",
357
+ errors: ["session_duplicated"],
358
358
  },
359
359
  "dir.tree": {
360
360
  plane: "control",
361
361
  roles: USER_ONLY,
362
362
  needs_hello: true,
363
363
  capability: "launcher",
364
- locality: "instance-local",
364
+ locality: "owner_instance",
365
365
  errors: [],
366
366
  },
367
367
 
@@ -371,7 +371,7 @@ export const OP_ATTRIBUTES = {
371
371
  roles: USER_ONLY,
372
372
  needs_hello: true,
373
373
  capability: "launcher",
374
- locality: "instance-local",
374
+ locality: "owner_instance",
375
375
  errors: [],
376
376
  },
377
377
  "launcher.run": {
@@ -379,7 +379,7 @@ export const OP_ATTRIBUTES = {
379
379
  roles: USER_ONLY,
380
380
  needs_hello: true,
381
381
  capability: "launcher",
382
- locality: "instance-local",
382
+ locality: "owner_instance",
383
383
  errors: [],
384
384
  },
385
385
  "sandbox.grant": {
@@ -387,7 +387,7 @@ export const OP_ATTRIBUTES = {
387
387
  roles: USER_ONLY,
388
388
  needs_hello: true,
389
389
  capability: "sandbox",
390
- locality: "instance-local",
390
+ locality: "owner_instance",
391
391
  errors: ["path_forbidden"],
392
392
  },
393
393
  "sandbox.revoke": {
@@ -395,7 +395,7 @@ export const OP_ATTRIBUTES = {
395
395
  roles: USER_ONLY,
396
396
  needs_hello: true,
397
397
  capability: "sandbox",
398
- locality: "instance-local",
398
+ locality: "owner_instance",
399
399
  errors: [],
400
400
  },
401
401
  "translate.run": {
@@ -403,7 +403,7 @@ export const OP_ATTRIBUTES = {
403
403
  roles: USER_ONLY,
404
404
  needs_hello: true,
405
405
  capability: "translate",
406
- locality: "instance-local",
406
+ locality: "owner_instance",
407
407
  errors: ["translate_helper_failed"],
408
408
  },
409
409
  "llm.usage.read": {
@@ -411,7 +411,7 @@ export const OP_ATTRIBUTES = {
411
411
  roles: USER_ONLY,
412
412
  needs_hello: true,
413
413
  capability: "llm_usage",
414
- locality: "instance-local",
414
+ locality: "owner_instance",
415
415
  errors: [],
416
416
  },
417
417
  "llm.stats.read": {
@@ -419,32 +419,32 @@ export const OP_ATTRIBUTES = {
419
419
  roles: USER_ONLY,
420
420
  needs_hello: true,
421
421
  capability: "llm_stats",
422
- locality: "instance-local",
422
+ locality: "owner_instance",
423
423
  errors: [],
424
424
  },
425
425
 
426
426
  // --- control: the shared key-value store (3) ---
427
- // The only control ops that are not instance-local: a value is held by every
427
+ // The only control ops not answered by an owning instance: a value is held by every
428
428
  // instance rather than by one, so whichever is asked can answer.
429
429
  "kv.read": {
430
430
  plane: "control",
431
431
  roles: USER_ONLY,
432
432
  needs_hello: true,
433
- locality: "cluster",
433
+ locality: "any_instance",
434
434
  errors: ["not_found"],
435
435
  },
436
436
  "kv.write": {
437
437
  plane: "control",
438
438
  roles: USER_ONLY,
439
439
  needs_hello: true,
440
- locality: "cluster",
440
+ locality: "any_instance",
441
441
  errors: [],
442
442
  },
443
443
  "kv.delete": {
444
444
  plane: "control",
445
445
  roles: USER_ONLY,
446
446
  needs_hello: true,
447
- locality: "cluster",
447
+ locality: "any_instance",
448
448
  errors: [],
449
449
  },
450
450
  } as const satisfies Record<string, OpAttributes>;
@@ -466,7 +466,7 @@ export function opErrors(op: OpName): ErrorCode[] {
466
466
  if (attrs.needs_hello) codes.add("hello_required");
467
467
  if (attrs.roles.length < ALL_ROLES.length) codes.add("forbidden");
468
468
  if (attrs.capability !== undefined) codes.add("capability_unavailable");
469
- if (attrs.locality === "instance-local") codes.add("instance_unreachable");
469
+ if (attrs.locality === "owner_instance") codes.add("instance_unreachable");
470
470
  return [...codes];
471
471
  }
472
472
 
@@ -30,7 +30,7 @@ export const REGISTER_TTL_MS = 10 * 60 * 1000;
30
30
  * retention would otherwise carry the removed credential back as news. */
31
31
  export const FAMILY_TOMBSTONE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
32
32
 
33
- /** Who a person is to this cluster. Issued when the registration URL is made
33
+ /** Who a person is to this mesh. Issued when the registration URL is made
34
34
  * (`<unit>-<counter>` by default) and carried by every record they own. */
35
35
  export const Subject = Type.String({ $id: "Subject", minLength: 1, maxLength: 128 });
36
36
  export type Subject = Static<typeof Subject>;
@@ -337,11 +337,11 @@ export const AuthRotateResponse = response("auth.rotate", AuthRotateResult);
337
337
 
338
338
  // --- the replicated records ------------------------------------------------
339
339
 
340
- /** A registered passkey, as every instance in the cluster holds it.
340
+ /** A registered passkey, as every instance in the mesh holds it.
341
341
  *
342
342
  * Complete once it is written: the instance that registered it is not asked
343
343
  * about it again, which is what lets a person authenticate anywhere in the
344
- * cluster while the instance they registered at is down. */
344
+ * mesh while the instance they registered at is down. */
345
345
  export const CredentialRecord = Type.Object(
346
346
  {
347
347
  kind: Type.Literal("credential"),
@@ -503,7 +503,7 @@ export type AuthTombstone = Static<typeof AuthTombstone>;
503
503
  /** One entry of the replicated set, under the key it is matched by. */
504
504
  export const AuthRecord = Type.Object(
505
505
  {
506
- /** What this entry is, cluster-wide. Two instances writing one key hold the
506
+ /** What this entry is, mesh-wide. Two instances writing one key hold the
507
507
  * same thing, and the later `updated_at` is what stands. */
508
508
  key: Type.String({ minLength: 1, maxLength: 256 }),
509
509
  updated_at: Timestamp,
@@ -66,6 +66,16 @@ export const HelloSessionArgs = Type.Object({
66
66
  ...GREETING_FIELDS,
67
67
  /** The session the connection speaks for. */
68
68
  sid: Sid,
69
+ /** The harness process running this session. Where the greeting is carried by
70
+ * something standing in for the session — a hook, the CLI — it is the parent
71
+ * harness process's pid and not the helper's: what this names is the run, and
72
+ * the helper is not one.
73
+ *
74
+ * It is what ties this session to a process a launcher started before the
75
+ * harness had written anything of its own, and what lets two runs of one
76
+ * session be told apart. Optional: a session that cannot read it is greeted
77
+ * anyway, and the instance then knows the run by its connection alone. */
78
+ pid: Type.Optional(Type.Integer({ minimum: 1 })),
69
79
  /** What the session says about itself. All optional: a session states what it
70
80
  * knows, and the instance derives or leaves unknown what it is not told. The
71
81
  * instance repeats these on the `peers` topic, so they are the same fields
@@ -141,11 +151,11 @@ export const HelloResult = Type.Object({
141
151
  version: Type.String(),
142
152
  started_at: Timestamp,
143
153
  /** Where a person opens the terminal a session runs in: the base URL of the
144
- * gateway that fronts this instance's terminals. A session's terminal names
145
- * itself in `terminal_id` on the `agents` topic, and the gateway's URL for it
146
- * is `<terminal_gateway>/sessions/<terminal_id>` so the base URL carries no
147
- * trailing slash, the path below it being the gateway's spelling and not this
148
- * contract's.
154
+ * gateway that fronts this instance's terminals. A run's terminal names
155
+ * itself in `terminal_id`, and `terminalUrl` below composes the URL from the
156
+ * twoonly for a handle under the `hyoui` scheme, this gateway serving no
157
+ * other. The base URL carries no trailing slash, the path below it being the
158
+ * gateway's spelling and not this contract's.
149
159
  *
150
160
  * Stated by the instance because only it knows which gateway stands in front
151
161
  * of the machine its sessions run on; a client has no way to derive one from
@@ -164,6 +174,29 @@ export const HelloResult = Type.Object({
164
174
  });
165
175
  export type HelloResult = Static<typeof HelloResult>;
166
176
 
177
+ /** The terminal scheme the gateway in `terminal_gateway` serves. */
178
+ export const HYOUI_TERMINAL_SCHEME = "hyoui";
179
+
180
+ /** Where a person opens a run's terminal, or nothing.
181
+ *
182
+ * Nothing when the gateway is absent, and nothing when the handle is under
183
+ * another scheme: a handle names whose terminal it is, and composing this URL
184
+ * for one the gateway does not serve would hand a person a link to a page that
185
+ * is not there. A client holding such a handle opens it the way that system
186
+ * does, or leaves it unopened.
187
+ *
188
+ * Composed here rather than once per client, so the one place that knows both
189
+ * the scheme and the gateway's path is this contract. */
190
+ export function terminalUrl(
191
+ gateway: string | undefined,
192
+ terminalId: string | undefined,
193
+ ): string | undefined {
194
+ if (gateway === undefined || terminalId === undefined) return undefined;
195
+ const prefix = `${HYOUI_TERMINAL_SCHEME}:`;
196
+ if (!terminalId.startsWith(prefix)) return undefined;
197
+ return `${gateway}/sessions/${terminalId.slice(prefix.length)}`;
198
+ }
199
+
167
200
  export const HelloSessionRequest = request("hello.session", HelloSessionArgs);
168
201
  export const HelloSessionResponse = response("hello.session", HelloResult);
169
202
 
@@ -7,8 +7,8 @@ export type InstancePingArgs = Static<typeof InstancePingArgs>;
7
7
 
8
8
  /** How the answering daemon process is running.
9
9
  *
10
- * This is about one process, which is why the op is instance-local: the health
11
- * of the cluster is `hello`'s `instances[]`, not a ping fanned out. */
10
+ * This is about one process, which is why the op is `owner_instance`: the health
11
+ * of the mesh is `hello`'s `instances[]`, not a ping fanned out. */
12
12
  export const InstancePingResult = Type.Object({
13
13
  instance: InstanceId,
14
14
  version: Type.String(),
@@ -93,6 +93,12 @@ export interface TopicAttributes {
93
93
  * `forbidden`, and one naming a capability the instance lacks answers
94
94
  * `capability_unavailable`. */
95
95
  export const TOPIC_ATTRIBUTES = {
96
+ // The one topic where the same subscription means two things: a session is
97
+ // handed what was addressed to it, so what it reads leaves its inbox, while a
98
+ // person is looking at somebody else's mail and reading it moves nothing.
99
+ // Stated on the frame rather than as an attribute here — it is not a fold and
100
+ // not an authorization, and a subscriber does the same thing with the frames
101
+ // whichever it is.
96
102
  inbox: { roles: ["session", "user"], granularity: "element" },
97
103
  notify: { roles: ["session", "user"], granularity: "event" },
98
104
  // A row here changes on its own — one session becomes busy while the rest
@@ -1,19 +1,26 @@
1
1
  import { type Static, Type } from "@sinclair/typebox";
2
2
  import { topicFrame } from "../envelope.ts";
3
- import { InstanceId, Sid, Timestamp } from "../identifiers.ts";
3
+ import { InstanceId, Sid, TerminalId, Timestamp } from "../identifiers.ts";
4
4
  import { upstream } from "../upstream.ts";
5
5
 
6
- /** One session as the harness itself reports it, noted with which config home
7
- * it was found under.
6
+ /** One run of a session — one process — as the harness itself reports it, noted
7
+ * with which config home it was found under.
8
8
  *
9
9
  * This is the harness's view rather than the instance's: it covers sessions
10
10
  * that never connected here, and it carries what only the process knows — its
11
11
  * pid, its terminal, the title the session gave itself. The instance renames
12
12
  * the fields as it copies them in; the words inside them stay the harness's,
13
- * which is why the status-like fields are open sets. */
13
+ * which is why the status-like fields are open sets.
14
+ *
15
+ * A row is one process and is matched by `instance` and `pid`, never by `sid`:
16
+ * two processes may be running one session, and a launcher's process is here
17
+ * before it has a session at all. */
14
18
  export const AgentInfo = Type.Object(
15
19
  {
16
- sid: Sid,
20
+ /** The session this process is running. Absent on a process a launcher
21
+ * started that the harness has not yet named a session for — it has a
22
+ * terminal and a start, and nothing to attach them to yet. */
23
+ sid: Type.Optional(Sid),
17
24
  /** The instance that polled it, and whose host the pid belongs to. */
18
25
  instance: InstanceId,
19
26
  pid: Type.Integer({ minimum: 1 }),
@@ -39,7 +46,7 @@ export const AgentInfo = Type.Object(
39
46
  * into. Absent when the process does not name one or its environment could
40
47
  * not be read. Read from the running process rather than remembered from
41
48
  * when it started, since resuming a session gives it a new process. */
42
- terminal_id: Type.Optional(Type.String()),
49
+ terminal_id: Type.Optional(TerminalId),
43
50
  /** Which namespace that terminal lives in. Absent means the process set
44
51
  * none, which the multiplexer treats as its default — not the instance's
45
52
  * own namespace, which can differ. Typing into the wrong namespace reports a
@@ -50,13 +57,15 @@ export const AgentInfo = Type.Object(
50
57
  );
51
58
  export type AgentInfo = Static<typeof AgentInfo>;
52
59
 
53
- /** A row that is gone: the harness no longer reports this session, or the
54
- * instance that polled it stopped. Marked rather than absent, since a frame
55
- * carries only what changed. */
60
+ /** A row that is gone: the process ended, or the instance that polled it
61
+ * stopped. Marked rather than absent, since a frame carries only what changed.
62
+ *
63
+ * It names the pid and not the session: the session may well still be there,
64
+ * with another process running it. */
56
65
  export const AgentRemoved = Type.Object(
57
66
  {
58
- sid: Sid,
59
67
  instance: InstanceId,
68
+ pid: Type.Integer({ minimum: 1 }),
60
69
  removed: Type.Literal(true),
61
70
  },
62
71
  { $id: "AgentRemoved" },
@@ -67,7 +76,7 @@ export const AgentElement = Type.Union([AgentInfo, AgentRemoved], { $id: "AgentE
67
76
  export type AgentElement = Static<typeof AgentElement>;
68
77
 
69
78
  /** The `agents` topic. Elements, like `peers`: the rows that changed since the
70
- * last frame, matched by their `instance` and `sid`.
79
+ * last frame, matched by their `instance` and `pid`.
71
80
  *
72
81
  * The instance polls the harness only while somebody is listening here, so the
73
82
  * list is as fresh as the subscription is old — and a poll that finds one