@ccmsg/protocol 0.3.1 → 0.5.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 +10 -2
- package/src/common/hello.ts +15 -0
- package/src/common/shutdown.ts +35 -0
- package/src/common/topics.ts +59 -13
- package/src/control/llm.ts +15 -2
- package/src/control/peers.ts +22 -1
- package/src/errors.ts +7 -0
- package/src/identifiers.ts +21 -1
- package/src/messaging/direct-delivery.ts +12 -5
- package/src/messaging/message.ts +12 -6
- package/src/schemas.ts +7 -1
package/package.json
CHANGED
package/src/attributes.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { ErrorCode } from "./errors.ts";
|
|
|
2
2
|
import type { Capability, Role } from "./identifiers.ts";
|
|
3
3
|
|
|
4
4
|
/** Which face of the contract an op belongs to. `common` is the transport
|
|
5
|
-
* level (connect
|
|
5
|
+
* level (connect, declare the end of a connection, subscribe), which every
|
|
6
|
+
* face uses. `mesh` holds no ops: an
|
|
6
7
|
* op crosses instances by carrying the envelope's mesh fields, not by being a
|
|
7
8
|
* different op. */
|
|
8
9
|
export type Plane = "common" | "messaging" | "control" | "mesh";
|
|
@@ -39,7 +40,7 @@ const SESSION_ONLY = ["session"] as const;
|
|
|
39
40
|
* facts live: authorization, capability gating and forwarding all read it
|
|
40
41
|
* rather than each carrying their own copy. */
|
|
41
42
|
export const OP_ATTRIBUTES = {
|
|
42
|
-
// --- common: connect and subscribe (
|
|
43
|
+
// --- common: connect, declare the end, and subscribe (6) ---
|
|
43
44
|
// `hello` and `instance_ping` address the instance the caller reached, so
|
|
44
45
|
// there is nothing to forward and no unreachable instance to report — which
|
|
45
46
|
// is why they are `cluster` despite answering about one instance.
|
|
@@ -64,6 +65,13 @@ export const OP_ATTRIBUTES = {
|
|
|
64
65
|
locality: "instance-local",
|
|
65
66
|
errors: [],
|
|
66
67
|
},
|
|
68
|
+
session_stopping: {
|
|
69
|
+
plane: "common",
|
|
70
|
+
roles: SESSION_ONLY,
|
|
71
|
+
needs_hello: true,
|
|
72
|
+
locality: "instance-local",
|
|
73
|
+
errors: [],
|
|
74
|
+
},
|
|
67
75
|
topic_subscribe: {
|
|
68
76
|
plane: "common",
|
|
69
77
|
roles: AGENT_AND_USER,
|
package/src/common/hello.ts
CHANGED
|
@@ -26,6 +26,21 @@ export const MeshHello = Type.Object(
|
|
|
26
26
|
);
|
|
27
27
|
export type MeshHello = Static<typeof MeshHello>;
|
|
28
28
|
|
|
29
|
+
/** The greeting that settles what a connection is.
|
|
30
|
+
*
|
|
31
|
+
* Which of the fields below are required is decided by `role`, which no single
|
|
32
|
+
* object schema can state — so the instance checks it, and a greeting that
|
|
33
|
+
* breaks one of these is refused with `invalid_args`:
|
|
34
|
+
*
|
|
35
|
+
* - `role: "session"` carries `sid`.
|
|
36
|
+
* - `role: "user"` carries no `sid`: a person speaks for no one session, and a
|
|
37
|
+
* greeting that names one is refused rather than quietly ignored.
|
|
38
|
+
* - `role: "instance"` carries `mesh`.
|
|
39
|
+
*
|
|
40
|
+
* A field that belongs to another role is as much a refusal as a missing one:
|
|
41
|
+
* `mesh` on a session greeting says the caller has confused which handshake it
|
|
42
|
+
* is in, and accepting it would leave the connection settled as something
|
|
43
|
+
* neither side meant. */
|
|
29
44
|
export const HelloArgs = Type.Object({
|
|
30
45
|
role: Role,
|
|
31
46
|
/** The generation the caller speaks. A hello announcing another generation
|
package/src/common/shutdown.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
import { request, response } from "../envelope.ts";
|
|
3
|
+
import { Timestamp } from "../identifiers.ts";
|
|
4
|
+
import { upstream } from "../upstream.ts";
|
|
3
5
|
|
|
4
6
|
export const InstanceShutdownArgs = Type.Object({});
|
|
5
7
|
export type InstanceShutdownArgs = Static<typeof InstanceShutdownArgs>;
|
|
@@ -11,3 +13,36 @@ export type InstanceShutdownResult = Static<typeof InstanceShutdownResult>;
|
|
|
11
13
|
|
|
12
14
|
export const InstanceShutdownRequest = request("instance_shutdown", InstanceShutdownArgs);
|
|
13
15
|
export const InstanceShutdownResponse = response("instance_shutdown", InstanceShutdownResult);
|
|
16
|
+
|
|
17
|
+
/** A session saying it is about to go, so that its disconnection reads as a
|
|
18
|
+
* pause rather than a loss.
|
|
19
|
+
*
|
|
20
|
+
* The calling session is recorded as `paused` — carrying the `stopped_at`
|
|
21
|
+
* below — once the connection closes; a session that vanishes without this
|
|
22
|
+
* call is `disappeared`. The instance holds the declaration until the
|
|
23
|
+
* disconnection arrives, so the two are one event in that order however long
|
|
24
|
+
* the session takes to actually exit, and a session that carries on regardless
|
|
25
|
+
* stays connected and unchanged. */
|
|
26
|
+
export const SessionStoppingArgs = Type.Object({
|
|
27
|
+
/** Why it is stopping, for display. An open set: this is normally called
|
|
28
|
+
* from the harness's own end-of-session hook, which passes its reason
|
|
29
|
+
* through, and nothing here gates on the word. */
|
|
30
|
+
reason: Type.Optional(
|
|
31
|
+
Type.String({
|
|
32
|
+
minLength: 1,
|
|
33
|
+
...upstream("claude", "the session-end reason, in the harness's spelling"),
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
export type SessionStoppingArgs = Static<typeof SessionStoppingArgs>;
|
|
38
|
+
|
|
39
|
+
export const SessionStoppingResult = Type.Object({
|
|
40
|
+
/** The instant the instance recorded, and the one that will appear on the
|
|
41
|
+
* session's `last_live` entry. Stated rather than left to the caller so the
|
|
42
|
+
* pause is stamped by the clock the entry is read against. */
|
|
43
|
+
stopped_at: Timestamp,
|
|
44
|
+
});
|
|
45
|
+
export type SessionStoppingResult = Static<typeof SessionStoppingResult>;
|
|
46
|
+
|
|
47
|
+
export const SessionStoppingRequest = request("session_stopping", SessionStoppingArgs);
|
|
48
|
+
export const SessionStoppingResponse = response("session_stopping", SessionStoppingResult);
|
package/src/common/topics.ts
CHANGED
|
@@ -45,25 +45,63 @@ export const Topic = Type.String({
|
|
|
45
45
|
].join(""),
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
/** How a subscriber folds a frame into what it already holds.
|
|
49
|
+
*
|
|
50
|
+
* Snapshot and delta share one payload type, which leaves one question the
|
|
51
|
+
* shape cannot answer: what a later frame does to the value before it. Stating
|
|
52
|
+
* it here means the daemon and the web UI fold the same way instead of each
|
|
53
|
+
* keeping its own table of which topic behaves how. */
|
|
54
|
+
export const TOPIC_GRANULARITIES = [
|
|
55
|
+
/** The frame is the whole value; it replaces everything held. */
|
|
56
|
+
"whole",
|
|
57
|
+
/** The frame is the whole of what its `instance` knows; it replaces that
|
|
58
|
+
* instance's entries and leaves every other instance's alone. What the
|
|
59
|
+
* subscriber holds is the union across instances. */
|
|
60
|
+
"per_instance_whole",
|
|
61
|
+
/** The frame carries the elements that changed, keyed by their own id.
|
|
62
|
+
* Elements it does not mention are untouched, so a removal has to be a
|
|
63
|
+
* marked element rather than an absence. */
|
|
64
|
+
"element",
|
|
65
|
+
/** The frame carries what has been added since the last one; the subscriber
|
|
66
|
+
* appends and never rewrites what it already has. */
|
|
67
|
+
"append",
|
|
68
|
+
/** The frame is an occurrence, not a value. Nothing is held, so there is
|
|
69
|
+
* nothing to snapshot: subscribing yields the next occurrence, never a
|
|
70
|
+
* current state. */
|
|
71
|
+
"event",
|
|
72
|
+
] as const;
|
|
73
|
+
|
|
74
|
+
export type TopicGranularity = (typeof TOPIC_GRANULARITIES)[number];
|
|
75
|
+
|
|
48
76
|
export interface TopicAttributes {
|
|
49
77
|
readonly roles: readonly Role[];
|
|
50
78
|
readonly capability?: Capability;
|
|
79
|
+
/** How a later frame relates to the value already held. Every topic but an
|
|
80
|
+
* `event` one opens with a `snapshot: true` frame. */
|
|
81
|
+
readonly granularity: TopicGranularity;
|
|
51
82
|
}
|
|
52
83
|
|
|
53
|
-
/** Who may subscribe to what. The same question the
|
|
54
|
-
* a subscribe from a role outside the set answers
|
|
55
|
-
* a capability the instance lacks answers
|
|
84
|
+
/** Who may subscribe to what, and how the frames fold. The same question the
|
|
85
|
+
* op table answers for ops: a subscribe from a role outside the set answers
|
|
86
|
+
* `forbidden`, and one naming a capability the instance lacks answers
|
|
87
|
+
* `capability_unavailable`. */
|
|
56
88
|
export const TOPIC_ATTRIBUTES = {
|
|
57
|
-
inbox: { roles: ["session", "user"] },
|
|
58
|
-
notify: { roles: ["session", "user"] },
|
|
59
|
-
peers: { roles: ["session", "user"] },
|
|
60
|
-
agents: { roles: ["user"] },
|
|
61
|
-
session_errors: { roles: ["user"] },
|
|
62
|
-
llm_requests: {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
inbox: { roles: ["session", "user"], granularity: "element" },
|
|
90
|
+
notify: { roles: ["session", "user"], granularity: "event" },
|
|
91
|
+
peers: { roles: ["session", "user"], granularity: "per_instance_whole" },
|
|
92
|
+
agents: { roles: ["user"], granularity: "per_instance_whole" },
|
|
93
|
+
session_errors: { roles: ["user"], granularity: "per_instance_whole" },
|
|
94
|
+
llm_requests: {
|
|
95
|
+
roles: ["user"],
|
|
96
|
+
capability: "llm_events",
|
|
97
|
+
granularity: "per_instance_whole",
|
|
98
|
+
},
|
|
99
|
+
llm_status: { roles: ["user"], capability: "llm_status", granularity: "per_instance_whole" },
|
|
100
|
+
// One session lives on one instance, so its status has no other instance's
|
|
101
|
+
// half to leave alone: the frame is simply the whole of it.
|
|
102
|
+
session_status: { roles: ["user"], granularity: "whole" },
|
|
103
|
+
transcript: { roles: ["user"], granularity: "append" },
|
|
104
|
+
kv: { roles: ["user"], granularity: "element" },
|
|
67
105
|
} as const satisfies Record<
|
|
68
106
|
PlainTopic | SessionScopedTopic | NamespaceScopedTopic,
|
|
69
107
|
TopicAttributes
|
|
@@ -77,6 +115,14 @@ export function topicKind(topic: string): TopicKind | undefined {
|
|
|
77
115
|
return head in TOPIC_ATTRIBUTES ? head : undefined;
|
|
78
116
|
}
|
|
79
117
|
|
|
118
|
+
/** How the frames of a topic name fold, taken from the name a subscriber
|
|
119
|
+
* actually uses — `session_status:<sid>` rather than the kind behind it.
|
|
120
|
+
* `undefined` for a name this generation does not define. */
|
|
121
|
+
export function topicGranularity(topic: string): TopicGranularity | undefined {
|
|
122
|
+
const kind = topicKind(topic);
|
|
123
|
+
return kind === undefined ? undefined : TOPIC_ATTRIBUTES[kind].granularity;
|
|
124
|
+
}
|
|
125
|
+
|
|
80
126
|
export const TopicSubscribeArgs = Type.Object({ topic: Topic });
|
|
81
127
|
export type TopicSubscribeArgs = Static<typeof TopicSubscribeArgs>;
|
|
82
128
|
|
package/src/control/llm.ts
CHANGED
|
@@ -249,8 +249,21 @@ export const LlmRequestInfo = Type.Object(
|
|
|
249
249
|
* sessions, which is the other half of why the pair is the key. */
|
|
250
250
|
prefix: Type.Optional(Type.String()),
|
|
251
251
|
/** True for the series the session's own turns keep warm, as opposed to a
|
|
252
|
-
* subagent's. The instance decides it
|
|
253
|
-
* window is the session's
|
|
252
|
+
* subagent's. The instance decides it, by the rule below, so that every
|
|
253
|
+
* client agrees on which window is the session's rather than each reading
|
|
254
|
+
* the same events into a different verdict.
|
|
255
|
+
*
|
|
256
|
+
* 1. `origin` decides it whenever the gateway states one. It watched the
|
|
257
|
+
* request go out and nothing here knows better.
|
|
258
|
+
* 2. Otherwise a `prefix` seen under two or more sessions is a subagent's:
|
|
259
|
+
* a series the session's own turns keep warm is not shared, so sharing
|
|
260
|
+
* is the evidence. Among the series left, the one that session used
|
|
261
|
+
* first wins — a session's own conversation starts before anything it
|
|
262
|
+
* spawns.
|
|
263
|
+
* 3. If every series of a session is shared, none of them is that session's
|
|
264
|
+
* own, so the newest is marked instead. It is the window a countdown
|
|
265
|
+
* would be about, and marking nothing would leave the session with no
|
|
266
|
+
* window at all. */
|
|
254
267
|
main: Type.Boolean(),
|
|
255
268
|
/** Whose turn issued the request, as the gateway read it. An open set; this
|
|
256
269
|
* is one of the inputs to `main`, and `main` is the verdict clients read. */
|
package/src/control/peers.ts
CHANGED
|
@@ -68,10 +68,18 @@ export const PeerInfo = Type.Object(
|
|
|
68
68
|
repo: SessionMetaFields.repo,
|
|
69
69
|
ws: SessionMetaFields.ws,
|
|
70
70
|
cwd: SessionMetaFields.cwd,
|
|
71
|
-
/** Present when the session announced one the instance accepted.
|
|
71
|
+
/** Present when the session announced one the instance accepted. What an
|
|
72
|
+
* instance accepts is its own rule — the reference one takes a path under
|
|
73
|
+
* the `projects/` tree of its own config home, so that a session cannot
|
|
74
|
+
* turn a transcript read into a read of any file it names. */
|
|
72
75
|
transcript_path: Type.Optional(SessionMetaFields.transcript_path),
|
|
73
76
|
repo_root: Type.Optional(SessionMetaFields.repo_root),
|
|
74
77
|
branch: Type.Optional(SessionMetaFields.branch),
|
|
78
|
+
/** The session's own title, as it named it. Also what resolves a display
|
|
79
|
+
* name for this session elsewhere — a message's `from_label`, a
|
|
80
|
+
* notification's `sid_label` — so the material for those is on the row that
|
|
81
|
+
* every client already holds. */
|
|
82
|
+
title: Type.Optional(SessionMetaFields.title),
|
|
75
83
|
/** How this session stands. One of `waiting`, `live` or `live_unmanaged`:
|
|
76
84
|
* a session in this list is connected, so it is by definition not gone.
|
|
77
85
|
* Absent from an instance that states no classification, and a client then
|
|
@@ -91,6 +99,19 @@ export const PeerInfo = Type.Object(
|
|
|
91
99
|
* while none has been found; a client orders such a session after every
|
|
92
100
|
* session that has one rather than treating it as long ago. */
|
|
93
101
|
last_user_input_at: Type.Optional(Timestamp),
|
|
102
|
+
/** When inference last ran for this session, as the gateway saw it.
|
|
103
|
+
*
|
|
104
|
+
* How busy a session is, carried as an attribute of the row rather than
|
|
105
|
+
* folded into `state`: a session is busy while it stands in any of the
|
|
106
|
+
* connected classifications, so the two answer different questions and
|
|
107
|
+
* collapsing them would lose one. It is an instant rather than a flag
|
|
108
|
+
* because there is no moment a request stops being in flight that anything
|
|
109
|
+
* observes — a client reads recency and decides its own threshold.
|
|
110
|
+
*
|
|
111
|
+
* Absent from an instance with no gateway configured, where nothing
|
|
112
|
+
* observes inference at all. That is not "idle": a client shows such a
|
|
113
|
+
* session without the mark rather than as quiet. */
|
|
114
|
+
gateway_active_at: Type.Optional(Timestamp),
|
|
94
115
|
/** Whether the asking session can reach this one with the harness's own
|
|
95
116
|
* cross-session messaging, which does not cross config homes. Computed
|
|
96
117
|
* against the asker, so it never appears on the asker's own entry nor for a
|
package/src/errors.ts
CHANGED
|
@@ -11,6 +11,13 @@ export const ERROR_CODES = [
|
|
|
11
11
|
"unknown_op",
|
|
12
12
|
/** An op with `needs_hello` arrived before `hello` settled the identity. */
|
|
13
13
|
"hello_required",
|
|
14
|
+
/** The op's implementation failed for a reason that is not the caller's: the
|
|
15
|
+
* arguments were right and the call was allowed. It belongs beside the other
|
|
16
|
+
* connection-level codes because no op owns it — any op can fail this way, and
|
|
17
|
+
* naming it apart from `bad_request` is what keeps a caller from re-reading
|
|
18
|
+
* arguments that were never the problem. Whether retrying helps is not stated;
|
|
19
|
+
* `msg` is the only thing that says more. */
|
|
20
|
+
"internal_error",
|
|
14
21
|
// --- rule-derived (op attribute table §0) ---
|
|
15
22
|
/** The connection's role is outside the op's `roles`. Argument problems stay
|
|
16
23
|
* on `invalid_args` / `bad_request`. */
|
package/src/identifiers.ts
CHANGED
|
@@ -8,6 +8,22 @@ export const Sid = Type.String({
|
|
|
8
8
|
});
|
|
9
9
|
export type Sid = Static<typeof Sid>;
|
|
10
10
|
|
|
11
|
+
/** Who sent a message: a session, or the person at the web UI.
|
|
12
|
+
*
|
|
13
|
+
* A person has no sid — the `user` role greets without one — so the sender of a
|
|
14
|
+
* message cannot be a `Sid` alone. The literal is spelled out rather than left
|
|
15
|
+
* as an absent field, because a reader has to tell "a person sent this" from "a
|
|
16
|
+
* session sent this and the id was lost". Every session id remains a valid
|
|
17
|
+
* sender, so a reader that only knew sids keeps working.
|
|
18
|
+
*
|
|
19
|
+
* There is one person per instance to a session's eye, so the literal carries
|
|
20
|
+
* no id of its own; which browser it was is not a thing this contract names. */
|
|
21
|
+
export const Sender = Type.Union([Sid, Type.Literal("user")], { $id: "Sender" });
|
|
22
|
+
export type Sender = Static<typeof Sender>;
|
|
23
|
+
|
|
24
|
+
/** The sender that is the person rather than a session. */
|
|
25
|
+
export const USER_SENDER = "user" as const;
|
|
26
|
+
|
|
11
27
|
/** An instance id: the endpoint URL other instances dial, compared as a whole
|
|
12
28
|
* string including its path (mesh-peer-auth §4.2 — one origin may host several
|
|
13
29
|
* instances, so origin-level comparison would confuse them). The display name
|
|
@@ -28,7 +44,11 @@ export const Mid = Type.String({
|
|
|
28
44
|
export type Mid = Static<typeof Mid>;
|
|
29
45
|
|
|
30
46
|
/** Who a connection speaks as. Set once by `hello` and fixed for the
|
|
31
|
-
* connection's life; the op attribute table's `roles` is checked against it.
|
|
47
|
+
* connection's life; the op attribute table's `roles` is checked against it.
|
|
48
|
+
*
|
|
49
|
+
* Once by `hello` means once: a second `hello` on a connection whose identity
|
|
50
|
+
* is already settled is refused with `bad_request`, whether it repeats the
|
|
51
|
+
* role or names another. */
|
|
32
52
|
export const Role = Type.Union(
|
|
33
53
|
[Type.Literal("session"), Type.Literal("user"), Type.Literal("instance")],
|
|
34
54
|
{ $id: "Role" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InboxMessage } from "./message.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type Mid, type Sender, USER_SENDER } from "../identifiers.ts";
|
|
3
3
|
|
|
4
4
|
/** How an `InboxMessage` is worded when it is handed to a session through the
|
|
5
5
|
* harness's own messaging socket rather than over this protocol.
|
|
@@ -39,15 +39,22 @@ export const DIRECT_DELIVERY_FROM_MODE = "prompting";
|
|
|
39
39
|
* the frame being answered — so nothing has to be looked up to use it. A `mid`
|
|
40
40
|
* does not name its sender, and the alternative to spelling the sid out here
|
|
41
41
|
* would be an op that resolves one, which means a sent-message index the
|
|
42
|
-
* daemon does not otherwise need.
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
* daemon does not otherwise need.
|
|
43
|
+
*
|
|
44
|
+
* A message from a person carries no addressee: `user` is not a sid and
|
|
45
|
+
* addressing it would be a send that fails. The line drops `--to` instead, and
|
|
46
|
+
* what an answer to a person becomes is the instance's to decide — it reaches
|
|
47
|
+
* them as a notification, which is a route the recipient does not have to know
|
|
48
|
+
* about to run this line. */
|
|
49
|
+
export function directDeliveryReplyLine(mid: Mid, from: Sender): string {
|
|
50
|
+
const to = from === USER_SENDER ? "" : ` --to ${from}`;
|
|
51
|
+
return `Reply with: ccmsg reply ${mid}${to} <text>`;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
/** An `InboxMessage` as it reaches a session through the messaging socket. */
|
|
48
55
|
export interface DirectDelivery {
|
|
49
56
|
mid: Mid;
|
|
50
|
-
from:
|
|
57
|
+
from: Sender;
|
|
51
58
|
from_label: string;
|
|
52
59
|
reply_to?: Mid;
|
|
53
60
|
/** The sender's text, exactly as it was sent. */
|
package/src/messaging/message.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
import { request, response, topicFrame } from "../envelope.ts";
|
|
3
|
-
import { InstanceId, Mid, Sid, Timestamp } from "../identifiers.ts";
|
|
3
|
+
import { InstanceId, Mid, Sender, Sid, Timestamp } from "../identifiers.ts";
|
|
4
4
|
|
|
5
5
|
/** Why a message was not handed to its recipient right away.
|
|
6
6
|
*
|
|
@@ -70,14 +70,20 @@ export const MessageSendResponse = response("message_send", MessageSendResult);
|
|
|
70
70
|
|
|
71
71
|
/** A message as the recipient receives it, on topic `inbox`.
|
|
72
72
|
*
|
|
73
|
-
* To answer it, send to `from`. The route is the sender
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
73
|
+
* To answer it, send to `from`. The route is the sender and nothing else, so no
|
|
74
|
+
* reply instructions travel on the wire: the wording a session sees belongs to
|
|
75
|
+
* whoever renders it — see `direct-delivery.ts` for the one route whose
|
|
76
|
+
* recipient reads text instead of this frame.
|
|
77
|
+
*
|
|
78
|
+
* A `from` of `user` is the exception: `message_send` addresses a sid, so there
|
|
79
|
+
* is no such thing as sending back to the person. An answer to one reaches them
|
|
80
|
+
* as a notification instead, which is the instance's to arrange — this contract
|
|
81
|
+
* only states that the sender can be a person, so a client stops treating one
|
|
82
|
+
* as a malformed message. */
|
|
77
83
|
export const InboxMessage = Type.Object(
|
|
78
84
|
{
|
|
79
85
|
mid: Mid,
|
|
80
|
-
from:
|
|
86
|
+
from: Sender,
|
|
81
87
|
/** How the sender should be shown, resolved by the issuing instance. */
|
|
82
88
|
from_label: Type.String(),
|
|
83
89
|
text: Type.String(),
|
package/src/schemas.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { TypeCompiler, type TypeCheck } from "@sinclair/typebox/compiler";
|
|
|
3
3
|
import type { OpName } from "./attributes.ts";
|
|
4
4
|
import { HelloRequest, HelloResponse } from "./common/hello.ts";
|
|
5
5
|
import { InstancePingRequest, InstancePingResponse } from "./common/ping.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
InstanceShutdownRequest,
|
|
8
|
+
InstanceShutdownResponse,
|
|
9
|
+
SessionStoppingRequest,
|
|
10
|
+
SessionStoppingResponse,
|
|
11
|
+
} from "./common/shutdown.ts";
|
|
7
12
|
import {
|
|
8
13
|
TopicSubscribeRequest,
|
|
9
14
|
TopicSubscribeResponse,
|
|
@@ -108,6 +113,7 @@ export const OP_SCHEMAS: Record<OpName, OpSchemas> = {
|
|
|
108
113
|
hello: { request: HelloRequest, response: HelloResponse },
|
|
109
114
|
instance_ping: { request: InstancePingRequest, response: InstancePingResponse },
|
|
110
115
|
instance_shutdown: { request: InstanceShutdownRequest, response: InstanceShutdownResponse },
|
|
116
|
+
session_stopping: { request: SessionStoppingRequest, response: SessionStoppingResponse },
|
|
111
117
|
topic_subscribe: { request: TopicSubscribeRequest, response: TopicSubscribeResponse },
|
|
112
118
|
topic_unsubscribe: { request: TopicUnsubscribeRequest, response: TopicUnsubscribeResponse },
|
|
113
119
|
message_send: { request: MessageSendRequest, response: MessageSendResponse },
|