@ccmsg/protocol 0.4.0 → 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/control/peers.ts +4 -1
- package/src/identifiers.ts +5 -1
- 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/control/peers.ts
CHANGED
|
@@ -68,7 +68,10 @@ 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),
|
package/src/identifiers.ts
CHANGED
|
@@ -44,7 +44,11 @@ export const Mid = Type.String({
|
|
|
44
44
|
export type Mid = Static<typeof Mid>;
|
|
45
45
|
|
|
46
46
|
/** Who a connection speaks as. Set once by `hello` and fixed for the
|
|
47
|
-
* 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. */
|
|
48
52
|
export const Role = Type.Union(
|
|
49
53
|
[Type.Literal("session"), Type.Literal("user"), Type.Literal("instance")],
|
|
50
54
|
{ $id: "Role" },
|
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 },
|