@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/protocol",
3
- "version": "0.4.0",
3
+ "version": "0.5.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
@@ -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 / subscribe), which every face uses. `mesh` holds no ops: an
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 (5) ---
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,
@@ -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
@@ -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);
@@ -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),
@@ -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 { InstanceShutdownRequest, InstanceShutdownResponse } from "./common/shutdown.ts";
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 },