@ccmsg/protocol 1.18.0 → 1.20.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.18.0",
3
+ "version": "1.20.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",
@@ -7,6 +7,7 @@ export const PLAIN_TOPICS = [
7
7
  "inbox",
8
8
  "notify",
9
9
  "peers",
10
+ "instances",
10
11
  "agents",
11
12
  "session_errors",
12
13
  "llm_requests",
@@ -89,8 +90,17 @@ export interface TopicAttributes {
89
90
  export const TOPIC_ATTRIBUTES = {
90
91
  inbox: { roles: ["session", "user"], granularity: "element" },
91
92
  notify: { roles: ["session", "user"], granularity: "event" },
92
- peers: { roles: ["session", "user"], granularity: "per_instance_whole" },
93
- agents: { roles: ["user"], granularity: "per_instance_whole" },
93
+ // A row here changes on its own — one session becomes busy while the rest
94
+ // stand still so a frame carries the rows that changed rather than every
95
+ // row the instance knows.
96
+ peers: { roles: ["session", "user"], granularity: "element" },
97
+ // One instance's whole reading of its links, taken together, which is why it
98
+ // is apart from the rows of `peers`.
99
+ instances: { roles: ["session", "user"], granularity: "per_instance_whole" },
100
+ agents: { roles: ["user"], granularity: "element" },
101
+ // A set the instance derives whole, by folding one error pattern over its
102
+ // sessions: it learns which sessions are stopped, not that one of them
103
+ // changed, so each frame is that reading entire.
94
104
  session_errors: { roles: ["user"], granularity: "per_instance_whole" },
95
105
  llm_requests: {
96
106
  roles: ["user"],
@@ -50,15 +50,34 @@ export const AgentInfo = Type.Object(
50
50
  );
51
51
  export type AgentInfo = Static<typeof AgentInfo>;
52
52
 
53
- /** The `agents` topic. Whole-value per instance, like `peers`.
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. */
56
+ export const AgentRemoved = Type.Object(
57
+ {
58
+ sid: Sid,
59
+ instance: InstanceId,
60
+ removed: Type.Literal(true),
61
+ },
62
+ { $id: "AgentRemoved" },
63
+ );
64
+ export type AgentRemoved = Static<typeof AgentRemoved>;
65
+
66
+ export const AgentElement = Type.Union([AgentInfo, AgentRemoved], { $id: "AgentElement" });
67
+ export type AgentElement = Static<typeof AgentElement>;
68
+
69
+ /** The `agents` topic. Elements, like `peers`: the rows that changed since the
70
+ * last frame, matched by their `instance` and `sid`.
54
71
  *
55
72
  * The instance polls the harness only while somebody is listening here, so the
56
- * list is as fresh as the subscription is old. */
73
+ * list is as fresh as the subscription is old — and a poll that finds one
74
+ * session's status changed says that, rather than restating every row it read.
75
+ */
57
76
  export const AgentsFrame = topicFrame(
58
77
  "agents",
59
78
  Type.Object({
60
- agents: Type.Array(AgentInfo),
61
- /** When the poll behind this list ran. Absent before the first one. */
79
+ agents: Type.Array(AgentElement),
80
+ /** When the poll behind these rows ran. Absent before the first one. */
62
81
  polled_at: Type.Optional(Timestamp),
63
82
  }),
64
83
  );
@@ -79,6 +79,27 @@ export const TranscriptItemSelector = Type.String({
79
79
  });
80
80
  export type TranscriptItemSelector = Static<typeof TranscriptItemSelector>;
81
81
 
82
+ /** Whose transcript an item was read from, said as a standing rather than a
83
+ * name: `main` is a session's own transcript, `sub` a throwaway agent's — one
84
+ * started to answer once — and `team` a teammate's, an agent that was named and
85
+ * goes on standing.
86
+ *
87
+ * It is what the relations are read from. `message:parent:in` under a `sub` is
88
+ * an errand's brief and under a `team` is what its lead wrote, and an item that
89
+ * does not say which of the two it stood in can only be placed by whoever
90
+ * remembers the request that fetched it. Carrying it on the item is what lets
91
+ * several transcripts be drawn as one, and what lets the table of relations be
92
+ * checked against items rather than against the request that asked for them.
93
+ *
94
+ * It is not `harness_name`: that is the other party's literal name, this is the
95
+ * subject's own standing, and a subject has one whether or not anything it
96
+ * talked to was named. */
97
+ export const TranscriptSubject = Type.Union(
98
+ [Type.Literal("main"), Type.Literal("team"), Type.Literal("sub")],
99
+ { $id: "TranscriptSubject" },
100
+ );
101
+ export type TranscriptSubject = Static<typeof TranscriptSubject>;
102
+
82
103
  /** Where in the transcript the record an item was read from begins, and how far
83
104
  * it runs.
84
105
  *
@@ -115,10 +136,16 @@ export type TranscriptItemId = Static<typeof TranscriptItemId>;
115
136
  * `id` is what the links below point with — never a position in the array,
116
137
  * since a selection or a range decides which items exist in a given read — and
117
138
  * `uuid` stays beside it as the record the item came out of, which is what a
118
- * reader groups by when it wants the whole of one line. */
139
+ * reader groups by when it wants the whole of one line.
140
+ *
141
+ * `subject` is on every item because an item is read out of one transcript and
142
+ * ends up among items from others: a client draws a session beside the agents
143
+ * below it, and the standing it was read from is then the item's own to state
144
+ * and not something to be inferred from the call that fetched it. */
119
145
  const BASE_FIELDS = {
120
146
  id: TranscriptItemId,
121
147
  uuid: Type.String({ minLength: 1 }),
148
+ subject: TranscriptSubject,
122
149
  source: TranscriptItemSource,
123
150
  /** The item's own instant. A call and its result each keep their own. */
124
151
  at: Timestamp,
@@ -0,0 +1,21 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { InstanceInfo } from "../common/hello.ts";
3
+ import { topicFrame } from "../envelope.ts";
4
+
5
+ /** The `instances` topic: the mesh as one instance sees it, itself included,
6
+ * as `hello` answers it.
7
+ *
8
+ * Carried as a topic so that a link going down is something a subscriber
9
+ * learns where it is already listening, rather than by greeting again to find
10
+ * out.
11
+ *
12
+ * Whole-value per instance: `reachable` is one instance's reading of every
13
+ * link it has, taken together, and two instances may legitimately disagree
14
+ * about the same link — so a frame states one sender's whole view and leaves
15
+ * every other sender's alone. It is apart from `peers` for the same reason it
16
+ * is whole: a mesh view is one value, while a session row is a row, and an
17
+ * entry here may also stand before its instance has an id to be matched by. */
18
+ export const InstancesFrame = topicFrame(
19
+ "instances",
20
+ Type.Object({ instances: Type.Array(InstanceInfo) }),
21
+ );
@@ -1,5 +1,4 @@
1
1
  import { type Static, Type } from "@sinclair/typebox";
2
- import { InstanceInfo } from "../common/hello.ts";
3
2
  import { topicFrame } from "../envelope.ts";
4
3
  import { InstanceId, Sid, Timestamp } from "../identifiers.ts";
5
4
  import { SessionMetaFields } from "../session-meta.ts";
@@ -31,8 +30,9 @@ export const SessionState = Type.Union(
31
30
  );
32
31
  export type SessionState = Static<typeof SessionState>;
33
32
 
34
- /** How long a session stays in `last_live` after it was last seen. The same
35
- * window the inbox keeps undelivered messages for: what a person comes back to
33
+ /** How long a lost session's row is kept after it was last seen, before the
34
+ * instance forgets it and the row leaves as a removal. The same window the inbox
35
+ * keeps undelivered messages for: what a person comes back to
36
36
  * is one thing — the session and what was said to it — so the two cannot expire
37
37
  * at different times. */
38
38
  export const LAST_LIVE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
@@ -57,11 +57,20 @@ export const StaleClientInfo = Type.Object(
57
57
  );
58
58
  export type StaleClientInfo = Static<typeof StaleClientInfo>;
59
59
 
60
- /** One session an instance can call live (connected or not).
60
+ /** One session an instance knows of, connected or lost.
61
61
  *
62
62
  * The paths are the host's, so the entry names the instance holding them:
63
63
  * `peers` carries every instance's sessions in one list, and two hosts' paths
64
- * would otherwise be indistinguishable. */
64
+ * would otherwise be indistinguishable. The pair `instance` and `sid` is also
65
+ * what a later row is matched against, since one session lives on one
66
+ * instance.
67
+ *
68
+ * Connected and lost sessions are one kind of row rather than two lists: a
69
+ * session registering or going quiet moves it between the two, and a row that
70
+ * changed lists while keeping its identity is an update of that row. Which it
71
+ * is now is the `state` below, and the fields that only a lost session has
72
+ * (`last_seen_at`, `stopped_at`, and what it must resume as) are stated
73
+ * alongside the connection fields it kept. */
65
74
  export const PeerInfo = Type.Object(
66
75
  {
67
76
  sid: Sid,
@@ -81,10 +90,10 @@ export const PeerInfo = Type.Object(
81
90
  * notification's `sid_label` — so the material for those is on the row that
82
91
  * every client already holds. */
83
92
  title: Type.Optional(SessionMetaFields.title),
84
- /** How this session stands. One of `waiting`, `live` or `live_unmanaged`:
85
- * a session in this list is connected, so it is by definition not gone.
86
- * Absent from an instance that states no classification, and a client then
87
- * shows the session without grouping it rather than guessing one. */
93
+ /** How this session stands, which is also what separates a connected row
94
+ * from one its instance has lost. Absent from an instance that states no
95
+ * classification, and a client then shows the session without grouping it
96
+ * rather than guessing one. */
88
97
  state: Type.Optional(SessionState),
89
98
  /** Set while a person has pinned this session. Absent means not pinned. */
90
99
  pinned: Type.Optional(Type.Boolean()),
@@ -129,77 +138,52 @@ export const PeerInfo = Type.Object(
129
138
  protocol_version: Type.Optional(Type.Integer({ minimum: 1 })),
130
139
  /** Set while some client of this session is being refused. */
131
140
  stale_client: Type.Optional(StaleClientInfo),
141
+ /** The newest instant this session is known to have been alive. Stated on a
142
+ * row its instance has lost, where it is what the retention window above is
143
+ * measured from; a connected row is alive now and states none. */
144
+ last_seen_at: Type.Optional(Timestamp),
145
+ /** When the session said it was stopping. Its presence is what makes a lost
146
+ * session a pause rather than a disappearance: one that goes without a word
147
+ * leaves nothing to stamp here. */
148
+ stopped_at: Type.Optional(Timestamp),
149
+ /** What its last turn ran as, in the transcript's own spelling, read back
150
+ * from the transcript rather than copied from the connection: what a lost
151
+ * session must resume as is a property of where it actually stopped. A
152
+ * resume must not quietly switch the session to something else. */
153
+ model: Type.Optional(SessionMetaFields.model),
154
+ effort: Type.Optional(SessionMetaFields.effort),
132
155
  },
133
156
  { $id: "PeerInfo" },
134
157
  );
135
158
  export type PeerInfo = Static<typeof PeerInfo>;
136
159
 
137
- /** One session that was connected when its instance last saw it, and has not
138
- * come back.
160
+ /** A row that is gone: the session has been forgotten by the instance that
161
+ * held it, whether its retention window ran out or a person dropped it.
139
162
  *
140
- * The connection fields are a frozen copy of what that session looked like at
141
- * the last snapshot, not a live reading by definition it is not connected
142
- * while it appears here. The model and effort are the exception: they are read
143
- * back from the transcript's last turn, because what the session must resume as
144
- * is a property of where it actually stopped, not of when the snapshot was
145
- * written. An entry leaves this list the moment its session registers again, so
146
- * a fully recovered host shows none. */
147
- export const LastLiveSession = Type.Object(
163
+ * A removal has to be a marked element rather than an absence, since a frame
164
+ * carries only what changed and an absence in it says nothing. It names the
165
+ * same pair every row is matched by and nothing else there is no row left to
166
+ * describe. */
167
+ export const PeerRemoved = Type.Object(
148
168
  {
149
169
  sid: Sid,
150
170
  instance: InstanceId,
151
- repo: SessionMetaFields.repo,
152
- ws: SessionMetaFields.ws,
153
- cwd: SessionMetaFields.cwd,
154
- /** Also where the model and effort below were read from. */
155
- transcript_path: Type.Optional(SessionMetaFields.transcript_path),
156
- repo_root: Type.Optional(SessionMetaFields.repo_root),
157
- branch: Type.Optional(SessionMetaFields.branch),
158
- title: Type.Optional(SessionMetaFields.title),
159
- /** How this session stands: `paused` or `disappeared`, which the
160
- * `stopped_at` below is what separates. Absent from an instance that states
161
- * no classification. */
162
- state: Type.Optional(SessionState),
163
- /** Set while a person has pinned this session. Absent means not pinned. */
164
- pinned: Type.Optional(Type.Boolean()),
165
- connected_at: Type.Optional(Timestamp),
166
- /** The newest instant this session is known to have been alive. */
167
- last_seen_at: Timestamp,
168
- /** When the session said it was stopping. Its presence is what makes this a
169
- * pause rather than a disappearance: a session that goes without a word
170
- * leaves nothing to stamp here. */
171
- stopped_at: Type.Optional(Timestamp),
172
- /** What its last turn ran as, in the transcript's own spelling. A resume
173
- * must not quietly switch the session to something else. */
174
- model: Type.Optional(SessionMetaFields.model),
175
- effort: Type.Optional(SessionMetaFields.effort),
171
+ removed: Type.Literal(true),
176
172
  },
177
- { $id: "LastLiveSession" },
173
+ { $id: "PeerRemoved" },
178
174
  );
179
- export type LastLiveSession = Static<typeof LastLiveSession>;
175
+ export type PeerRemoved = Static<typeof PeerRemoved>;
176
+
177
+ export const PeerElement = Type.Union([PeerInfo, PeerRemoved], { $id: "PeerElement" });
178
+ export type PeerElement = Static<typeof PeerElement>;
180
179
 
181
180
  /** The `peers` topic.
182
181
  *
183
- * Whole-value per instance: a frame replaces everything previously known from
184
- * the instance that sent it and leaves other instances' entries alone, which is
185
- * what lets several instances each state their whole list without colliding.
182
+ * Elements: each frame carries the rows that changed, matched by their
183
+ * `instance` and `sid`, and rows it does not name are left as they were. A row
184
+ * changes on its own one session becoming busy, another going quiet — and
185
+ * restating every row an instance knows would send a list to report one field.
186
186
  *
187
- * Both lists travel together because a session registering is exactly what
188
- * moves it from one to the other. */
189
- export const PeersFrame = topicFrame(
190
- "peers",
191
- Type.Object({
192
- peers: Type.Array(PeerInfo),
193
- last_live: Type.Array(LastLiveSession),
194
- /** The instances the sending one can see, itself included, as `hello`
195
- * answers it. Carried here so that a link going down is something a
196
- * subscriber learns from the topic it is already on, rather than by
197
- * greeting again to find out. It is the sender's own view like the two
198
- * lists above — `reachable` says whether that instance can reach the one it
199
- * names, which two instances may legitimately disagree about.
200
- *
201
- * Absent from an instance that states no view; a client then keeps what it
202
- * last knew rather than reading the omission as nothing being reachable. */
203
- instances: Type.Optional(Type.Array(InstanceInfo)),
204
- }),
205
- );
187
+ * The opening `snapshot: true` frame carries every row the instance holds,
188
+ * connected and lost alike. */
189
+ export const PeersFrame = topicFrame("peers", Type.Object({ peers: Type.Array(PeerElement) }));
@@ -230,6 +230,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
230
230
  {
231
231
  id: "3f9a21c4:0",
232
232
  uuid: "3f9a21c4",
233
+ subject: "main",
233
234
  source: { offset: 180_000, bytes: 420 },
234
235
  type: "message:user:in",
235
236
  at: FIXTURE_NOW - 3_600_000,
@@ -239,6 +240,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
239
240
  {
240
241
  id: "f10b6d43:0",
241
242
  uuid: "f10b6d43",
243
+ subject: "main",
242
244
  source: { offset: 180_420, bytes: 980 },
243
245
  type: "thinking",
244
246
  at: FIXTURE_NOW - 3_500_000,
@@ -247,6 +249,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
247
249
  {
248
250
  id: "f10b6d43:1",
249
251
  uuid: "f10b6d43",
252
+ subject: "main",
250
253
  source: { offset: 180_420, bytes: 980 },
251
254
  type: "tool:Bash",
252
255
  at: FIXTURE_NOW - 3_400_000,
@@ -259,6 +262,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
259
262
  {
260
263
  id: "18d6f2c9:0",
261
264
  uuid: "18d6f2c9",
265
+ subject: "main",
262
266
  source: { offset: 181_400, bytes: 260 },
263
267
  type: "tool:Bash",
264
268
  at: FIXTURE_NOW - 3_399_000,
@@ -271,6 +275,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
271
275
  {
272
276
  id: "b7e41d09:0",
273
277
  uuid: "b7e41d09",
278
+ subject: "main",
274
279
  source: { offset: 181_660, bytes: 640 },
275
280
  type: "message:sub:out",
276
281
  at: FIXTURE_NOW - 3_300_000,
@@ -284,6 +289,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
284
289
  {
285
290
  id: "92e6d4f5:0",
286
291
  uuid: "92e6d4f5",
292
+ subject: "main",
287
293
  source: { offset: 182_300, bytes: 310 },
288
294
  type: "hook:PreToolUse",
289
295
  at: FIXTURE_NOW - 3_200_000,
@@ -295,6 +301,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
295
301
  {
296
302
  id: "81d5c3e4:0",
297
303
  uuid: "81d5c3e4",
304
+ subject: "main",
298
305
  source: { offset: 182_610, bytes: 190 },
299
306
  type: "system:attachment:queued_command",
300
307
  at: FIXTURE_NOW - 3_100_000,
@@ -303,6 +310,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
303
310
  {
304
311
  id: "c8a2f371:0",
305
312
  uuid: "c8a2f371",
313
+ subject: "main",
306
314
  source: { offset: 182_800, bytes: 520 },
307
315
  type: "message:team:out",
308
316
  at: FIXTURE_NOW - 3_000_000,
@@ -317,6 +325,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
317
325
  {
318
326
  id: "e5b70c93:0",
319
327
  uuid: "e5b70c93",
328
+ subject: "main",
320
329
  source: { offset: 183_320, bytes: 300 },
321
330
  type: "message:team:in",
322
331
  at: FIXTURE_NOW - 2_900_000,
@@ -326,6 +335,7 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
326
335
  {
327
336
  id: "d4c1a0b2:0",
328
337
  uuid: "d4c1a0b2",
338
+ subject: "main",
329
339
  source: { offset: 183_620, bytes: 410 },
330
340
  type: "message:team:in",
331
341
  at: FIXTURE_NOW - 2_800_000,
@@ -339,15 +349,18 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
339
349
  },
340
350
  ];
341
351
 
342
- /** The same vocabulary read with an agent as the subject.
352
+ /** The same vocabulary read with a throwaway agent as the subject.
343
353
  *
344
354
  * Nothing here is a new type: the brief an agent opens with and the answer it
345
355
  * closes with are what `message:parent` names from wherever it is read, and the
346
- * answer comes as prose with no call behind it. */
356
+ * answer comes as prose with no call behind it. What says these were read from
357
+ * an errand's transcript rather than a session's is `subject`, which every item
358
+ * carries. */
347
359
  export const TRANSCRIPT_ITEMS_AGENT_SUBJECT: Static<typeof TranscriptItem>[] = [
348
360
  {
349
361
  id: "a9f30d15:0",
350
362
  uuid: "a9f30d15",
363
+ subject: "sub",
351
364
  source: { offset: 0, bytes: 1_240 },
352
365
  type: "message:parent:in",
353
366
  at: FIXTURE_NOW - 3_290_000,
@@ -357,6 +370,7 @@ export const TRANSCRIPT_ITEMS_AGENT_SUBJECT: Static<typeof TranscriptItem>[] = [
357
370
  {
358
371
  id: "b0e41c26:0",
359
372
  uuid: "b0e41c26",
373
+ subject: "sub",
360
374
  source: { offset: 1_240, bytes: 380 },
361
375
  type: "message:parent:out",
362
376
  at: FIXTURE_NOW - 3_260_000,
@@ -369,6 +383,7 @@ export const TRANSCRIPT_ITEMS_AGENT_SUBJECT: Static<typeof TranscriptItem>[] = [
369
383
  {
370
384
  id: "c1f52d37:0",
371
385
  uuid: "c1f52d37",
386
+ subject: "sub",
372
387
  source: { offset: 1_620, bytes: 690 },
373
388
  type: "message:parent:out",
374
389
  at: FIXTURE_NOW - 3_200_000,
@@ -376,6 +391,57 @@ export const TRANSCRIPT_ITEMS_AGENT_SUBJECT: Static<typeof TranscriptItem>[] = [
376
391
  },
377
392
  ];
378
393
 
394
+ /** The same vocabulary again, read with a teammate as the subject.
395
+ *
396
+ * A teammate stands where neither of the other two does: it answers a lead, as
397
+ * an errand does, and a person can also type at it directly, as a session's own
398
+ * transcript has. The relations are the ones already spelled out — what tells
399
+ * these apart from the errand's above is `subject`, and it is what makes the
400
+ * three columns of the table checkable against items. */
401
+ export const TRANSCRIPT_ITEMS_TEAM_SUBJECT: Static<typeof TranscriptItem>[] = [
402
+ {
403
+ id: "d2a63e48:0",
404
+ uuid: "d2a63e48",
405
+ subject: "team",
406
+ source: { offset: 0, bytes: 1_480 },
407
+ type: "message:parent:in",
408
+ at: FIXTURE_NOW - 3_000_000,
409
+ turn: 1,
410
+ text: "契約に message:parent と message:team を足して",
411
+ harness_name: "main",
412
+ },
413
+ {
414
+ id: "e3b74f59:0",
415
+ uuid: "e3b74f59",
416
+ subject: "team",
417
+ source: { offset: 1_480, bytes: 260 },
418
+ type: "message:user:in",
419
+ at: FIXTURE_NOW - 2_950_000,
420
+ text: "fixtures も忘れずに",
421
+ },
422
+ {
423
+ id: "f4c8506a:0",
424
+ uuid: "f4c8506a",
425
+ subject: "team",
426
+ source: { offset: 1_740, bytes: 340 },
427
+ type: "message:team:out",
428
+ at: FIXTURE_NOW - 2_920_000,
429
+ role: "use",
430
+ tool_use_id: "toolu_01Wc7Zdq",
431
+ text: "daemon 側の分類はどこまで進んでいる",
432
+ harness_name: "daemon-dump-items",
433
+ },
434
+ {
435
+ id: "a5d9617b:0",
436
+ uuid: "a5d9617b",
437
+ subject: "team",
438
+ source: { offset: 2_080, bytes: 410 },
439
+ type: "message:parent:out",
440
+ at: FIXTURE_NOW - 2_800_000,
441
+ text: "4 型を足して 1.17.0 を切った",
442
+ },
443
+ ];
444
+
379
445
  export const TRANSCRIPT_READ_REQUEST: Static<typeof TranscriptReadRequest> = {
380
446
  request_id,
381
447
  op: "transcript_read",
@@ -163,6 +163,7 @@ export const TOPIC_FIXTURES = {
163
163
  inbox: topics.INBOX_FRAME,
164
164
  notify: topics.NOTIFY_FRAME,
165
165
  peers: topics.PEERS_FRAME,
166
+ instances: topics.INSTANCES_FRAME,
166
167
  agents: topics.AGENTS_FRAME,
167
168
  session_status: topics.SESSION_STATUS_FRAME,
168
169
  transcript: topics.TRANSCRIPT_FRAME,
@@ -1,6 +1,7 @@
1
1
  import type { Static } from "@sinclair/typebox";
2
2
  import type { AuthRecordsFrame } from "../common/auth.ts";
3
3
  import type { AgentsFrame } from "../control/agents.ts";
4
+ import type { InstancesFrame } from "../control/instances.ts";
4
5
  import type { KvFrame } from "../control/kv.ts";
5
6
  import type { LlmRequestsFrame, LlmStatusFrame } from "../control/llm.ts";
6
7
  import type { PeersFrame } from "../control/peers.ts";
@@ -94,16 +95,6 @@ export const PEERS_FRAME: Static<typeof PeersFrame> = {
94
95
  repo: "ccmsg",
95
96
  ws: "daemon-v2",
96
97
  cwd: "/repos/kawaz/ccmsg/daemon-v2",
97
- state: "live_unmanaged",
98
- },
99
- ],
100
- last_live: [
101
- {
102
- sid: other_sid,
103
- instance,
104
- repo: "ccmsg",
105
- ws: "daemon-v2",
106
- cwd: "/repos/kawaz/ccmsg/daemon-v2",
107
98
  state: "paused",
108
99
  last_seen_at: FIXTURE_NOW - 1_200_000,
109
100
  stopped_at: FIXTURE_NOW - 1_000_000,
@@ -111,6 +102,28 @@ export const PEERS_FRAME: Static<typeof PeersFrame> = {
111
102
  effort: "high",
112
103
  },
113
104
  ],
105
+ },
106
+ };
107
+
108
+ /** A later frame: one row moved on, and one the instance forgot. */
109
+ export const PEERS_CHANGE_FRAME: Static<typeof PeersFrame> = {
110
+ ev: "topic",
111
+ topic: "peers",
112
+ instance,
113
+ data: {
114
+ peers: [
115
+ { ...PEER, state: "live", gateway_active_at: FIXTURE_NOW + 1_000 },
116
+ { sid: "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", instance, removed: true },
117
+ ],
118
+ },
119
+ };
120
+
121
+ export const INSTANCES_FRAME: Static<typeof InstancesFrame> = {
122
+ ev: "topic",
123
+ topic: "instances",
124
+ snapshot: true,
125
+ instance,
126
+ data: {
114
127
  instances: [
115
128
  { id: instance, endpoint, host: "mba", reachable: true },
116
129
  { id: other_instance, endpoint: other_endpoint, host: "nuc", reachable: false },
@@ -144,6 +157,14 @@ export const AGENTS_FRAME: Static<typeof AgentsFrame> = {
144
157
  },
145
158
  };
146
159
 
160
+ /** A later frame: the harness no longer reports one of the rows above. */
161
+ export const AGENTS_CHANGE_FRAME: Static<typeof AgentsFrame> = {
162
+ ev: "topic",
163
+ topic: "agents",
164
+ instance,
165
+ data: { agents: [{ sid: other_sid, instance, removed: true }], polled_at: FIXTURE_NOW + 5_000 },
166
+ };
167
+
147
168
  export const SESSION_STATUS_FRAME: Static<typeof SessionStatusFrame> = {
148
169
  ev: "topic",
149
170
  topic: `session_status:${sid}`,
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./common/topics.ts";
7
7
  export * from "./control/agents.ts";
8
8
  export * from "./control/dump.ts";
9
9
  export * from "./control/files.ts";
10
+ export * from "./control/instances.ts";
10
11
  export * from "./control/kv.ts";
11
12
  export * from "./control/launcher.ts";
12
13
  export * from "./control/llm.ts";
package/src/schemas.ts CHANGED
@@ -77,6 +77,7 @@ import {
77
77
  LlmUsageReadRequest,
78
78
  LlmUsageReadResponse,
79
79
  } from "./control/llm.ts";
80
+ import { InstancesFrame } from "./control/instances.ts";
80
81
  import { PeersFrame } from "./control/peers.ts";
81
82
  import {
82
83
  SandboxGrantRequest,
@@ -201,6 +202,7 @@ export const TOPIC_SCHEMAS = {
201
202
  inbox: InboxFrame,
202
203
  notify: NotifyFrame,
203
204
  peers: PeersFrame,
205
+ instances: InstancesFrame,
204
206
  agents: AgentsFrame,
205
207
  session_status: SessionStatusFrame,
206
208
  transcript: TranscriptFrame,