@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 +1 -1
- package/src/attributes.ts +68 -68
- package/src/common/auth.ts +4 -4
- package/src/common/hello.ts +38 -5
- package/src/common/ping.ts +2 -2
- package/src/common/topics.ts +6 -0
- package/src/control/agents.ts +20 -11
- package/src/control/dump.ts +22 -1
- package/src/control/peers.ts +124 -34
- package/src/control/session.ts +24 -9
- package/src/envelope.ts +2 -2
- package/src/errors.ts +13 -3
- package/src/fixtures/common.ts +5 -4
- package/src/fixtures/control.ts +1 -1
- package/src/fixtures/messaging.ts +1 -0
- package/src/fixtures/topics.ts +67 -9
- package/src/identifiers.ts +16 -1
- package/src/messaging/message.ts +65 -4
- package/src/messaging/notify.ts +12 -1
package/src/control/dump.ts
CHANGED
|
@@ -576,7 +576,28 @@ export type DumpIdEntry = Static<typeof DumpIdEntry>;
|
|
|
576
576
|
export const DumpIds = Type.Array(DumpIdEntry, { $id: "DumpIds" });
|
|
577
577
|
export type DumpIds = Static<typeof DumpIds>;
|
|
578
578
|
|
|
579
|
-
/**
|
|
579
|
+
/** How the selected items are written out.
|
|
580
|
+
*
|
|
581
|
+
* The selection is one thing and its rendering another: which items a dump is
|
|
582
|
+
* of follows from the range and the `types`, and a format decides only what the
|
|
583
|
+
* file then says about them. So all three are dumps of the same items, and the
|
|
584
|
+
* reply describes that selection whichever was asked for.
|
|
585
|
+
*
|
|
586
|
+
* `items` is this contract's own vocabulary, the typed items as
|
|
587
|
+
* `SessionDumpFile`. `records` writes the transcript records those items were
|
|
588
|
+
* read from, unchanged, one JSON document per line — for a tool that already
|
|
589
|
+
* reads the harness's file and wants the classifying alone, which is why
|
|
590
|
+
* nothing of ours is added around them. An item names its record, so several
|
|
591
|
+
* items out of one record are one record here and the line count is not the
|
|
592
|
+
* item count. `text` renders the items for a person to read. */
|
|
593
|
+
export const SessionDumpFormat = Type.Union(
|
|
594
|
+
[Type.Literal("items"), Type.Literal("records"), Type.Literal("text")],
|
|
595
|
+
{ $id: "SessionDumpFormat" },
|
|
596
|
+
);
|
|
597
|
+
export type SessionDumpFormat = Static<typeof SessionDumpFormat>;
|
|
598
|
+
|
|
599
|
+
/** The file an `items` dump is written to. The other two formats are not this
|
|
600
|
+
* shape: `records` is the harness's own lines and `text` is prose.
|
|
580
601
|
*
|
|
581
602
|
* The reply to a dump names a path rather than carrying the items, so the file
|
|
582
603
|
* is where they actually travel — which makes its shape as much a part of the
|
package/src/control/peers.ts
CHANGED
|
@@ -1,34 +1,60 @@
|
|
|
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 { SessionMetaFields } from "../session-meta.ts";
|
|
5
5
|
|
|
6
|
-
/** How
|
|
6
|
+
/** How recently the gateway must have seen inference for a session for that
|
|
7
|
+
* alone to say the session is alive. A session whose processes are all gone but
|
|
8
|
+
* whose inference is still running is alive: the request outlives the terminal
|
|
9
|
+
* it was typed in. */
|
|
10
|
+
export const GATEWAY_LIVE_WINDOW_MS = 5 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
/** One process running a session.
|
|
7
13
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
14
|
+
* A session and a run of it are two things: the session is the transcript and
|
|
15
|
+
* the folded state, and lives whether nothing or two processes are running it.
|
|
16
|
+
* This is the process — what a signal reaches, what a terminal shows, what a
|
|
17
|
+
* connection speaks over. */
|
|
18
|
+
export const SessionRun = Type.Object(
|
|
19
|
+
{
|
|
20
|
+
/** The harness process. Present when the harness's state file names one or
|
|
21
|
+
* a launcher started it; a run known only by its connection has none, and
|
|
22
|
+
* nothing in this contract can signal such a run. */
|
|
23
|
+
pid: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
24
|
+
/** When that process started, which is what tells a pid the OS has handed
|
|
25
|
+
* to something else from the run it was read for. Stated wherever `pid`
|
|
26
|
+
* is. */
|
|
27
|
+
started_at: Type.Optional(Timestamp),
|
|
28
|
+
terminal_id: Type.Optional(TerminalId),
|
|
29
|
+
/** Whether a connection of this run is open to the instance right now. */
|
|
30
|
+
connected: Type.Boolean(),
|
|
31
|
+
},
|
|
32
|
+
{ $id: "SessionRun" },
|
|
33
|
+
);
|
|
34
|
+
export type SessionRun = Static<typeof SessionRun>;
|
|
35
|
+
|
|
36
|
+
/** What the `session.status` fold for this session is worth.
|
|
12
37
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
38
|
+
* A client reads this before it reads the fold: the values below say whether
|
|
39
|
+
* there is a transcript at all, whether the instance has caught up with it, and
|
|
40
|
+
* whether anything it says can still be trusted. */
|
|
41
|
+
export const SessionStatusStanding = Type.Union(
|
|
16
42
|
[
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
Type.Literal("
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Type.Literal("
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Type.Literal("
|
|
43
|
+
/** No transcript, so there is nothing to fold. A session that has just
|
|
44
|
+
* started stands here until the harness writes its first record. */
|
|
45
|
+
Type.Literal("absent"),
|
|
46
|
+
/** The transcript is being read from the top; what the fold says so far is
|
|
47
|
+
* incomplete. */
|
|
48
|
+
Type.Literal("folding"),
|
|
49
|
+
Type.Literal("ready"),
|
|
50
|
+
/** Two or more runs are writing the same transcript, so the instance stops
|
|
51
|
+
* updating the fold and stops carrying the transcript's additions. What it
|
|
52
|
+
* states is the last value it could trust. */
|
|
53
|
+
Type.Literal("frozen"),
|
|
28
54
|
],
|
|
29
|
-
{ $id: "
|
|
55
|
+
{ $id: "SessionStatusStanding" },
|
|
30
56
|
);
|
|
31
|
-
export type
|
|
57
|
+
export type SessionStatusStanding = Static<typeof SessionStatusStanding>;
|
|
32
58
|
|
|
33
59
|
/** How long a lost session's row is kept after it was last seen, before the
|
|
34
60
|
* instance forgets it and the row leaves as a removal. The same window the inbox
|
|
@@ -68,9 +94,9 @@ export type StaleClientInfo = Static<typeof StaleClientInfo>;
|
|
|
68
94
|
* Connected and lost sessions are one kind of row rather than two lists: a
|
|
69
95
|
* session registering or going quiet moves it between the two, and a row that
|
|
70
96
|
* changed lists while keeping its identity is an update of that row. Which it
|
|
71
|
-
* is now is
|
|
72
|
-
* (`last_seen_at`, `stopped_at`, and what
|
|
73
|
-
* alongside the connection fields it kept. */
|
|
97
|
+
* is now is read off `runs` and `stopped_at` by `liveness` below, and the
|
|
98
|
+
* fields that only a lost session has (`last_seen_at`, `stopped_at`, and what
|
|
99
|
+
* it must resume as) are stated alongside the connection fields it kept. */
|
|
74
100
|
export const PeerInfo = Type.Object(
|
|
75
101
|
{
|
|
76
102
|
sid: Sid,
|
|
@@ -90,11 +116,19 @@ export const PeerInfo = Type.Object(
|
|
|
90
116
|
* notification's `sid_label` — so the material for those is on the row that
|
|
91
117
|
* every client already holds. */
|
|
92
118
|
title: Type.Optional(SessionMetaFields.title),
|
|
93
|
-
/**
|
|
94
|
-
* from one its instance has lost.
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
119
|
+
/** Every process the instance can see running this session, which is also
|
|
120
|
+
* what separates a connected row from one its instance has lost. Empty
|
|
121
|
+
* means none is running, which is a row a person can still resume; two or
|
|
122
|
+
* more mean the harness let the same session be resumed while it was
|
|
123
|
+
* running, and nothing here picks one of them.
|
|
124
|
+
*
|
|
125
|
+
* The observation itself rather than a count or a verdict: which process,
|
|
126
|
+
* where it can be opened, and whether it is connected are each what a
|
|
127
|
+
* person decides on, and a session role holds this row without ever seeing
|
|
128
|
+
* the `agents` topic. */
|
|
129
|
+
runs: Type.Array(SessionRun),
|
|
130
|
+
/** What this session's `session.status` fold is worth just now. */
|
|
131
|
+
session_status: SessionStatusStanding,
|
|
98
132
|
/** Set while a person has pinned this session. Absent means not pinned. */
|
|
99
133
|
pinned: Type.Optional(Type.Boolean()),
|
|
100
134
|
/** When this session first registered with the instance. Stable across its
|
|
@@ -112,9 +146,10 @@ export const PeerInfo = Type.Object(
|
|
|
112
146
|
/** When inference last ran for this session, as the gateway saw it.
|
|
113
147
|
*
|
|
114
148
|
* How busy a session is, carried as an attribute of the row rather than
|
|
115
|
-
* folded into
|
|
116
|
-
*
|
|
117
|
-
*
|
|
149
|
+
* folded into how it stands: a session is busy whether one process or none
|
|
150
|
+
* is running it, so the two answer different questions and collapsing them
|
|
151
|
+
* would lose one. It is also what says a session with no run left is still
|
|
152
|
+
* alive, the request outliving the process. It is an instant rather than a flag
|
|
118
153
|
* because there is no moment a request stops being in flight that anything
|
|
119
154
|
* observes — a client reads recency and decides its own threshold.
|
|
120
155
|
*
|
|
@@ -144,7 +179,9 @@ export const PeerInfo = Type.Object(
|
|
|
144
179
|
last_seen_at: Type.Optional(Timestamp),
|
|
145
180
|
/** When the session said it was stopping. Its presence is what makes a lost
|
|
146
181
|
* session a pause rather than a disappearance: one that goes without a word
|
|
147
|
-
* leaves nothing to stamp here.
|
|
182
|
+
* leaves nothing to stamp here. It says something only while `runs` is
|
|
183
|
+
* empty — a session that declared it was stopping and is running again has
|
|
184
|
+
* a stamp older than the run. */
|
|
148
185
|
stopped_at: Type.Optional(Timestamp),
|
|
149
186
|
/** What its last turn ran as, in the transcript's own spelling, read back
|
|
150
187
|
* from the transcript rather than copied from the connection: what a lost
|
|
@@ -177,6 +214,59 @@ export type PeerRemoved = Static<typeof PeerRemoved>;
|
|
|
177
214
|
export const PeerElement = Type.Union([PeerInfo, PeerRemoved], { $id: "PeerElement" });
|
|
178
215
|
export type PeerElement = Static<typeof PeerElement>;
|
|
179
216
|
|
|
217
|
+
/** Where a session stands as a thing that is or is not running.
|
|
218
|
+
*
|
|
219
|
+
* `duplicated` is not a degree of aliveness but the answer to a different
|
|
220
|
+
* question — how many processes — which is why it wins over the rest: a session
|
|
221
|
+
* two processes are writing is one nothing should be read from, however alive
|
|
222
|
+
* it looks. */
|
|
223
|
+
export type Liveness = "alive" | "duplicated" | "paused" | "disappeared";
|
|
224
|
+
|
|
225
|
+
/** How a session stands, read off the row.
|
|
226
|
+
*
|
|
227
|
+
* Derived here rather than stated on the wire, and here rather than once per
|
|
228
|
+
* side: an instance and a client that each wrote this arithmetic would show the
|
|
229
|
+
* same row two ways. */
|
|
230
|
+
export function liveness(
|
|
231
|
+
row: {
|
|
232
|
+
runs: readonly { connected: boolean }[];
|
|
233
|
+
stopped_at?: number;
|
|
234
|
+
gateway_active_at?: number;
|
|
235
|
+
},
|
|
236
|
+
now: number,
|
|
237
|
+
): Liveness {
|
|
238
|
+
if (row.runs.length >= 2) return "duplicated";
|
|
239
|
+
const running =
|
|
240
|
+
(row.runs.length > 0 && row.stopped_at === undefined) ||
|
|
241
|
+
(row.gateway_active_at !== undefined && now - row.gateway_active_at <= GATEWAY_LIVE_WINDOW_MS);
|
|
242
|
+
if (running) return "alive";
|
|
243
|
+
return row.stopped_at === undefined ? "disappeared" : "paused";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Whether anything here can act on the session: some run of it is connected or
|
|
247
|
+
* names a terminal. A session alive with neither is one nothing can be handed
|
|
248
|
+
* to and nothing can be typed into. */
|
|
249
|
+
export function reachable(row: {
|
|
250
|
+
runs: readonly { connected: boolean; terminal_id?: string }[];
|
|
251
|
+
}): boolean {
|
|
252
|
+
return row.runs.some((run) => run.connected || run.terminal_id !== undefined);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Whether something is out that a person has to answer: a dialog the harness
|
|
256
|
+
* is holding open, or a turn that ended on an upstream error.
|
|
257
|
+
*
|
|
258
|
+
* Both materials belong to the `user` role — the `agents` row and the status
|
|
259
|
+
* fold — which is why this takes them rather than a `peers` row: a session role
|
|
260
|
+
* asking how another session stands has `runs` and `stopped_at` and no way to
|
|
261
|
+
* see this one. Either argument may be missing, which says only that its
|
|
262
|
+
* material was not there to read. */
|
|
263
|
+
export function waiting(
|
|
264
|
+
agentsRow: { waiting_for?: string } | undefined,
|
|
265
|
+
status: { api_error?: unknown } | undefined,
|
|
266
|
+
): boolean {
|
|
267
|
+
return agentsRow?.waiting_for !== undefined || status?.api_error !== undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
180
270
|
/** The `peers` topic.
|
|
181
271
|
*
|
|
182
272
|
* Elements: each frame carries the rows that changed, matched by their
|
package/src/control/session.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
import { request, response } from "../envelope.ts";
|
|
3
|
-
import { InstanceId, Sid, Timestamp } from "../identifiers.ts";
|
|
4
|
-
import { DumpIds, TranscriptItemSelector, TranscriptItemType } from "./dump.ts";
|
|
3
|
+
import { InstanceId, Sid, TerminalId, Timestamp } from "../identifiers.ts";
|
|
4
|
+
import { DumpIds, SessionDumpFormat, TranscriptItemSelector, TranscriptItemType } from "./dump.ts";
|
|
5
5
|
|
|
6
6
|
/** Ends the OS process behind a session.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* A session with one run is named by its sid alone: the instance resolves it to
|
|
9
|
+
* a pid at the moment it signals, which is a better basis than anything a
|
|
10
|
+
* caller asserts. A session with two is not resolvable that way, and a kill
|
|
11
|
+
* that named only the sid is refused with `ambiguous_run` — the caller picks a
|
|
12
|
+
* run from `peers.runs` and names it below. */
|
|
11
13
|
export const SessionKillArgs = Type.Object({
|
|
12
14
|
sid: Sid,
|
|
15
|
+
/** Which run to end, where the session has more than one. The instance
|
|
16
|
+
* signals it only after finding it in the session's own runs, pid and start
|
|
17
|
+
* together, so a pid the caller got wrong ends nothing rather than ending
|
|
18
|
+
* whatever the OS has since given that number to. */
|
|
19
|
+
pid: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
13
20
|
/** Escalate to an unconditional kill. The instance never chooses this on its
|
|
14
21
|
* own, because it forfeits the session's chance to flush its transcript; a
|
|
15
22
|
* caller asks for it after watching a graceful attempt go unconfirmed. */
|
|
@@ -55,7 +62,7 @@ export type SessionRenameArgs = Static<typeof SessionRenameArgs>;
|
|
|
55
62
|
export const SessionRenameResult = Type.Object({
|
|
56
63
|
/** The terminal handle the keystrokes went to. A host-local handle, so it
|
|
57
64
|
* travels with the instance that owns it. */
|
|
58
|
-
terminal_id:
|
|
65
|
+
terminal_id: TerminalId,
|
|
59
66
|
instance: InstanceId,
|
|
60
67
|
/** The trimmed title actually typed, which is what a caller should report
|
|
61
68
|
* rather than the draft it sent. */
|
|
@@ -203,6 +210,8 @@ export const SessionDumpWriteArgs = Type.Object({
|
|
|
203
210
|
/** Leave out the machinery of in-process agents, which
|
|
204
211
|
* `["-message.sub", "-tool.Agent"]` also says. */
|
|
205
212
|
no_agent: Type.Optional(Type.Boolean()),
|
|
213
|
+
/** What the file says about the items selected. Absent is `items`. */
|
|
214
|
+
format: Type.Optional(SessionDumpFormat),
|
|
206
215
|
});
|
|
207
216
|
export type SessionDumpWriteArgs = Static<typeof SessionDumpWriteArgs>;
|
|
208
217
|
|
|
@@ -210,12 +219,18 @@ export const SessionDumpWriteResult = Type.Object({
|
|
|
210
219
|
/** Absolute path on the writing instance's host. */
|
|
211
220
|
path: Type.String(),
|
|
212
221
|
instance: InstanceId,
|
|
213
|
-
/** How many items of each type were
|
|
222
|
+
/** How many items of each type were selected, keyed by type name. A single
|
|
214
223
|
* total leaves the caller unable to tell a dump that kept what it asked for
|
|
215
|
-
* from one whose selection matched almost nothing.
|
|
224
|
+
* from one whose selection matched almost nothing.
|
|
225
|
+
*
|
|
226
|
+
* It counts items and not what the file holds, whatever the `format`: the
|
|
227
|
+
* selection is what a caller asked for and what it reads this against, and a
|
|
228
|
+
* count that moved with the rendering would answer a different question each
|
|
229
|
+
* time. */
|
|
216
230
|
entries: Type.Record(TranscriptItemType, Type.Integer({ minimum: 0 })),
|
|
217
231
|
/** The ids those items carried, so the next dump — of an agent named here —
|
|
218
|
-
* can be asked for without opening the file.
|
|
232
|
+
* can be asked for without opening the file. Read off the selection like
|
|
233
|
+
* `entries`, whatever the `format`. */
|
|
219
234
|
ids: DumpIds,
|
|
220
235
|
bytes: Type.Integer({ minimum: 0 }),
|
|
221
236
|
});
|
package/src/envelope.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { InstanceId, Role, Sid } from "./identifiers.ts";
|
|
|
6
6
|
* fields and whole new ops may be added; a removal or a change of meaning
|
|
7
7
|
* raises it. Peers announcing another generation are refused, on client
|
|
8
8
|
* connections and on mesh links alike. */
|
|
9
|
-
export const PROTOCOL_VERSION =
|
|
9
|
+
export const PROTOCOL_VERSION = 4;
|
|
10
10
|
|
|
11
11
|
/** The largest a single frame — one newline-delimited line, request, reply or
|
|
12
12
|
* topic frame alike — may be, in bytes.
|
|
@@ -62,7 +62,7 @@ export const RequestEnvelope = Type.Object(
|
|
|
62
62
|
*
|
|
63
63
|
* A forwarded request that names none is dispatched as the `instance` role
|
|
64
64
|
* it arrived on, which the attribute table already answers: an
|
|
65
|
-
*
|
|
65
|
+
* an `owner_instance` op called by an instance is `forbidden`. */
|
|
66
66
|
caller: Type.Optional(CallerIdentity),
|
|
67
67
|
},
|
|
68
68
|
{ $id: "RequestEnvelope" },
|
package/src/errors.ts
CHANGED
|
@@ -35,17 +35,27 @@ export const ERROR_CODES = [
|
|
|
35
35
|
"invalid_args",
|
|
36
36
|
/** The op declares a `capability` this instance does not have. */
|
|
37
37
|
"capability_unavailable",
|
|
38
|
-
/** An `
|
|
38
|
+
/** An `owner_instance` op could not be forwarded to the instance that owns
|
|
39
39
|
* the subject. */
|
|
40
40
|
"instance_unreachable",
|
|
41
41
|
// --- subscription ---
|
|
42
42
|
/** The topic name is not one this protocol generation defines. */
|
|
43
43
|
"topic_unknown",
|
|
44
44
|
// --- subject lookup ---
|
|
45
|
-
/** The `sid` names no session anywhere in the
|
|
45
|
+
/** The `sid` names no session anywhere in the mesh. */
|
|
46
46
|
"session_not_found",
|
|
47
47
|
/** The path, transcript, or record named by the arguments does not exist. */
|
|
48
48
|
"not_found",
|
|
49
|
+
// --- a session two processes are running ---
|
|
50
|
+
/** Two or more processes are running the session the call names, so what it
|
|
51
|
+
* would act on cannot be settled and what the transcript says cannot be
|
|
52
|
+
* trusted. Nothing is held back for later: what was to be sent is still with
|
|
53
|
+
* the caller, and a person decides which run to end before anything here
|
|
54
|
+
* resumes. */
|
|
55
|
+
"session_duplicated",
|
|
56
|
+
/** The session has more than one run and the call named none. The caller
|
|
57
|
+
* picks one from `peers.runs` and asks again naming its pid. */
|
|
58
|
+
"ambiguous_run",
|
|
49
59
|
// --- file access ---
|
|
50
60
|
"path_forbidden",
|
|
51
61
|
"path_not_writable",
|
|
@@ -66,7 +76,7 @@ export const ERROR_CODES = [
|
|
|
66
76
|
* and `msg` says no more than the instance's own log would want. */
|
|
67
77
|
"auth_invalid",
|
|
68
78
|
/** The instance that issued the challenge or registration, and alone can
|
|
69
|
-
* spend it, is not one this
|
|
79
|
+
* spend it, is not one this mesh knows or could reach just now. The client
|
|
70
80
|
* asks for a fresh one, which the instance it is talking to can issue. */
|
|
71
81
|
"auth_unknown_issuer",
|
|
72
82
|
// --- translate ---
|
package/src/fixtures/common.ts
CHANGED
|
@@ -41,8 +41,9 @@ const { sid, instance, other_instance, endpoint, other_endpoint, request_id } =
|
|
|
41
41
|
export const HELLO_SESSION_REQUEST: Static<typeof HelloSessionRequest> = {
|
|
42
42
|
request_id,
|
|
43
43
|
op: "hello.session",
|
|
44
|
-
protocol_version:
|
|
44
|
+
protocol_version: 4,
|
|
45
45
|
sid,
|
|
46
|
+
pid: 4821,
|
|
46
47
|
client_version: "0.1.0",
|
|
47
48
|
repo: "ccmsg-protocol",
|
|
48
49
|
ws: "main",
|
|
@@ -59,7 +60,7 @@ export const HELLO_SESSION_REQUEST: Static<typeof HelloSessionRequest> = {
|
|
|
59
60
|
export const HELLO_USER_REQUEST: Static<typeof HelloUserRequest> = {
|
|
60
61
|
request_id,
|
|
61
62
|
op: "hello.user",
|
|
62
|
-
protocol_version:
|
|
63
|
+
protocol_version: 4,
|
|
63
64
|
client_version: "0.1.0",
|
|
64
65
|
};
|
|
65
66
|
|
|
@@ -68,7 +69,7 @@ export const HELLO_USER_REQUEST: Static<typeof HelloUserRequest> = {
|
|
|
68
69
|
export const HELLO_INSTANCE_REQUEST: Static<typeof HelloInstanceRequest> = {
|
|
69
70
|
request_id,
|
|
70
71
|
op: "hello.instance",
|
|
71
|
-
protocol_version:
|
|
72
|
+
protocol_version: 4,
|
|
72
73
|
mesh: {
|
|
73
74
|
ver: 1,
|
|
74
75
|
iss: other_endpoint,
|
|
@@ -83,7 +84,7 @@ export const HELLO_INSTANCE_REQUEST: Static<typeof HelloInstanceRequest> = {
|
|
|
83
84
|
export const HELLO_RESPONSE: Static<typeof HelloSessionResponse> = {
|
|
84
85
|
ok: true,
|
|
85
86
|
request_id,
|
|
86
|
-
protocol_version:
|
|
87
|
+
protocol_version: 4,
|
|
87
88
|
instance,
|
|
88
89
|
endpoint,
|
|
89
90
|
auth_expires_at: FIXTURE_NOW + 10_000_000,
|
package/src/fixtures/control.ts
CHANGED
|
@@ -105,7 +105,7 @@ export const SESSION_RENAME_REQUEST: Static<typeof SessionRenameRequest> = {
|
|
|
105
105
|
export const SESSION_RENAME_RESPONSE: Static<typeof SessionRenameResponse> = {
|
|
106
106
|
ok: true,
|
|
107
107
|
request_id,
|
|
108
|
-
terminal_id: "
|
|
108
|
+
terminal_id: "hyoui:%17",
|
|
109
109
|
instance,
|
|
110
110
|
title: "contract fixtures",
|
|
111
111
|
};
|
package/src/fixtures/topics.ts
CHANGED
|
@@ -29,6 +29,7 @@ export const INBOX_FRAME: Static<typeof InboxFrame> = {
|
|
|
29
29
|
from_label: "contract-fixtures",
|
|
30
30
|
text: "fixture を export した",
|
|
31
31
|
sent_at: FIXTURE_NOW,
|
|
32
|
+
to: sid,
|
|
32
33
|
},
|
|
33
34
|
{
|
|
34
35
|
mid: `${instance}/1842`,
|
|
@@ -37,15 +38,34 @@ export const INBOX_FRAME: Static<typeof InboxFrame> = {
|
|
|
37
38
|
text: "確認する",
|
|
38
39
|
reply_to: mid,
|
|
39
40
|
sent_at: FIXTURE_NOW + 1_000,
|
|
41
|
+
to: sid,
|
|
40
42
|
},
|
|
41
43
|
],
|
|
42
44
|
};
|
|
43
45
|
|
|
46
|
+
/** A later frame, where one message has been handed over and another was never
|
|
47
|
+
* taken. */
|
|
48
|
+
export const INBOX_REMOVED_FRAME: Static<typeof InboxFrame> = {
|
|
49
|
+
ev: "topic",
|
|
50
|
+
topic: "inbox",
|
|
51
|
+
instance,
|
|
52
|
+
data: [
|
|
53
|
+
{ mid, removed: true, reason: "delivered" },
|
|
54
|
+
{ mid: `${instance}/1842`, removed: true, reason: "expired" },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
44
58
|
export const NOTIFY_FRAME: Static<typeof NotifyFrame> = {
|
|
45
59
|
ev: "topic",
|
|
46
60
|
topic: "notify",
|
|
47
61
|
instance,
|
|
48
|
-
data: {
|
|
62
|
+
data: {
|
|
63
|
+
sid,
|
|
64
|
+
sid_label: "contract-fixtures",
|
|
65
|
+
text: "確認して",
|
|
66
|
+
reply_to: mid,
|
|
67
|
+
sent_at: FIXTURE_NOW,
|
|
68
|
+
},
|
|
49
69
|
};
|
|
50
70
|
|
|
51
71
|
const PEER = {
|
|
@@ -54,7 +74,7 @@ const PEER = {
|
|
|
54
74
|
repo: "ccmsg-protocol",
|
|
55
75
|
ws: "main",
|
|
56
76
|
cwd: WORKSPACE,
|
|
57
|
-
protocol_version:
|
|
77
|
+
protocol_version: 4,
|
|
58
78
|
};
|
|
59
79
|
|
|
60
80
|
export const PEERS_FRAME: Static<typeof PeersFrame> = {
|
|
@@ -70,7 +90,15 @@ export const PEERS_FRAME: Static<typeof PeersFrame> = {
|
|
|
70
90
|
repo_root: "/repos/kawaz/ccmsg-protocol",
|
|
71
91
|
branch: "main",
|
|
72
92
|
title: "contract fixtures",
|
|
73
|
-
|
|
93
|
+
runs: [
|
|
94
|
+
{
|
|
95
|
+
pid: 4821,
|
|
96
|
+
started_at: FIXTURE_NOW - 600_000,
|
|
97
|
+
terminal_id: "hyoui:%17",
|
|
98
|
+
connected: true,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
session_status: "ready",
|
|
74
102
|
pinned: true,
|
|
75
103
|
connected_at: FIXTURE_NOW - 600_000,
|
|
76
104
|
last_activity_at: FIXTURE_NOW,
|
|
@@ -82,7 +110,8 @@ export const PEERS_FRAME: Static<typeof PeersFrame> = {
|
|
|
82
110
|
{
|
|
83
111
|
...PEER,
|
|
84
112
|
sid: other_sid,
|
|
85
|
-
|
|
113
|
+
runs: [{ pid: 7314, started_at: FIXTURE_NOW - 120_000, connected: false }],
|
|
114
|
+
session_status: "folding",
|
|
86
115
|
stale_client: {
|
|
87
116
|
last_seen_at: FIXTURE_NOW - 300_000,
|
|
88
117
|
version: "0.0.9",
|
|
@@ -95,7 +124,8 @@ export const PEERS_FRAME: Static<typeof PeersFrame> = {
|
|
|
95
124
|
repo: "ccmsg",
|
|
96
125
|
ws: "daemon-v2",
|
|
97
126
|
cwd: "/repos/kawaz/ccmsg/daemon-v2",
|
|
98
|
-
|
|
127
|
+
runs: [],
|
|
128
|
+
session_status: "ready",
|
|
99
129
|
last_seen_at: FIXTURE_NOW - 1_200_000,
|
|
100
130
|
stopped_at: FIXTURE_NOW - 1_000_000,
|
|
101
131
|
model: "claude-opus-5",
|
|
@@ -105,14 +135,33 @@ export const PEERS_FRAME: Static<typeof PeersFrame> = {
|
|
|
105
135
|
},
|
|
106
136
|
};
|
|
107
137
|
|
|
108
|
-
/** A later frame: one row
|
|
138
|
+
/** A later frame: one row is being run by two processes at once, and one the
|
|
139
|
+
* instance forgot. */
|
|
109
140
|
export const PEERS_CHANGE_FRAME: Static<typeof PeersFrame> = {
|
|
110
141
|
ev: "topic",
|
|
111
142
|
topic: "peers",
|
|
112
143
|
instance,
|
|
113
144
|
data: {
|
|
114
145
|
peers: [
|
|
115
|
-
{
|
|
146
|
+
{
|
|
147
|
+
...PEER,
|
|
148
|
+
runs: [
|
|
149
|
+
{
|
|
150
|
+
pid: 4821,
|
|
151
|
+
started_at: FIXTURE_NOW - 600_000,
|
|
152
|
+
terminal_id: "hyoui:%17",
|
|
153
|
+
connected: true,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
pid: 9022,
|
|
157
|
+
started_at: FIXTURE_NOW - 30_000,
|
|
158
|
+
terminal_id: "hyoui:%23",
|
|
159
|
+
connected: true,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
session_status: "frozen",
|
|
163
|
+
gateway_active_at: FIXTURE_NOW + 1_000,
|
|
164
|
+
},
|
|
116
165
|
{ sid: "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", instance, removed: true },
|
|
117
166
|
],
|
|
118
167
|
},
|
|
@@ -149,9 +198,18 @@ export const AGENTS_FRAME: Static<typeof AgentsFrame> = {
|
|
|
149
198
|
status: "working",
|
|
150
199
|
state: "busy",
|
|
151
200
|
config_dir: "/config/claude-personal",
|
|
152
|
-
terminal_id: "
|
|
201
|
+
terminal_id: "hyoui:%17",
|
|
153
202
|
terminal_namespace: "personal",
|
|
154
203
|
},
|
|
204
|
+
{
|
|
205
|
+
instance,
|
|
206
|
+
pid: 9022,
|
|
207
|
+
cwd: WORKSPACE,
|
|
208
|
+
kind: "claude",
|
|
209
|
+
started_at: FIXTURE_NOW - 30_000,
|
|
210
|
+
config_dir: "/config/claude-personal",
|
|
211
|
+
terminal_id: "hyoui:%23",
|
|
212
|
+
},
|
|
155
213
|
],
|
|
156
214
|
polled_at: FIXTURE_NOW,
|
|
157
215
|
},
|
|
@@ -162,7 +220,7 @@ export const AGENTS_CHANGE_FRAME: Static<typeof AgentsFrame> = {
|
|
|
162
220
|
ev: "topic",
|
|
163
221
|
topic: "agents",
|
|
164
222
|
instance,
|
|
165
|
-
data: { agents: [{
|
|
223
|
+
data: { agents: [{ instance, pid: 7314, removed: true }], polled_at: FIXTURE_NOW + 5_000 },
|
|
166
224
|
};
|
|
167
225
|
|
|
168
226
|
export const SESSION_STATUS_FRAME: Static<typeof SessionStatusFrame> = {
|
package/src/identifiers.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
|
|
3
3
|
/** A session id: the uuid Claude Code gives its own session. Globally unique,
|
|
4
|
-
* so it names a session across the whole
|
|
4
|
+
* so it names a session across the whole mesh without an instance prefix. */
|
|
5
5
|
export const Sid = Type.String({
|
|
6
6
|
$id: "Sid",
|
|
7
7
|
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
@@ -81,6 +81,21 @@ export const Mid = Type.String({
|
|
|
81
81
|
});
|
|
82
82
|
export type Mid = Static<typeof Mid>;
|
|
83
83
|
|
|
84
|
+
/** A terminal a run lives in: `<scheme>:<id>`, where the scheme says whose
|
|
85
|
+
* handle the id is and the id is that system's own spelling.
|
|
86
|
+
*
|
|
87
|
+
* The scheme is what lets a client tell a handle it can open from one it
|
|
88
|
+
* cannot: `hyoui:<id>` is a terminal the gateway named in `terminal_gateway`
|
|
89
|
+
* serves, and `terminalUrl` composes the URL for it. A handle under any other
|
|
90
|
+
* scheme travels unchanged and is opened only by a client that knows that
|
|
91
|
+
* system — which is the point of naming the scheme rather than leaving a bare
|
|
92
|
+
* id every reader would have to guess the owner of. */
|
|
93
|
+
export const TerminalId = Type.String({
|
|
94
|
+
$id: "TerminalId",
|
|
95
|
+
pattern: "^[a-z][a-z0-9_-]*:[^\\s]+$",
|
|
96
|
+
});
|
|
97
|
+
export type TerminalId = Static<typeof TerminalId>;
|
|
98
|
+
|
|
84
99
|
/** Who a connection speaks as. Settled once by the greeting that opened it —
|
|
85
100
|
* `hello.session`, `hello.user` or `hello.instance`, the op being what says
|
|
86
101
|
* which — and fixed for the connection's life; the op attribute table's
|