@kangentic/protocol 0.13.1 → 0.15.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.
@@ -1,8 +1,37 @@
1
- import type { BridgeMessage } from './messages';
1
+ import type { BridgeMessage, CapabilityErrorCode } from './messages';
2
2
  export declare const MAX_FRAME_LENGTH: number;
3
3
  /** Upper bound on the JSON a compressed frame may declare/inflate to. */
4
4
  export declare const MAX_DECODED_LENGTH: number;
5
5
  /** JSON below this size ships raw - deflate overhead is not worth it. */
6
6
  export declare const COMPRESSION_THRESHOLD: number;
7
+ /** The `code` a desktop puts on its refusal of a verb its build does not know. */
8
+ export declare const UNSUPPORTED_VERB_ERROR_CODE: CapabilityErrorCode;
9
+ /**
10
+ * Thrown by `decodeMessage` for a capability-request whose envelope is
11
+ * well-formed (a string `requestId`, a string `verb`, a JSON `payload`) but
12
+ * whose verb this build's `CAPABILITY_VERBS` does not carry. It is the one
13
+ * decode failure that names its request, so the receiver can ANSWER it
14
+ * (`{ ok: false, code: 'unsupported-verb' }`) instead of dropping the frame.
15
+ *
16
+ * Without this a desktop that predates a verb sent nothing back at all, and
17
+ * the phone that sent it timed out, unable to tell an old desktop from an
18
+ * unreachable one. Every verb added to the protocol has that failure mode on
19
+ * a mixed-version pair. A frame that is NOT a well-formed request (a missing
20
+ * `requestId`, a non-string `verb`, a non-JSON `payload`) still throws a
21
+ * plain `Error` and stays a silent rejection: there is nothing to answer, and
22
+ * answering unstructured input would hand a probe to whoever sent it.
23
+ */
24
+ export declare class UnsupportedVerbError extends Error {
25
+ readonly requestId: string;
26
+ readonly verb: string;
27
+ constructor(requestId: string, verb: string);
28
+ }
29
+ /**
30
+ * Keyed on `name`, not `instanceof`: the desktop consumes this package from
31
+ * source through a workspace alias while the mobile app resolves the published
32
+ * `dist`, and a bundler can hold two copies of the class, which `instanceof`
33
+ * would tell apart.
34
+ */
35
+ export declare function isUnsupportedVerbError(error: unknown): error is UnsupportedVerbError;
7
36
  export declare function encodeMessage(message: BridgeMessage): Uint8Array;
8
37
  export declare function decodeMessage(bytes: Uint8Array): BridgeMessage;
@@ -30,6 +30,15 @@ export interface CapabilityRequestMessage {
30
30
  verb: CapabilityVerb;
31
31
  payload: JsonValue;
32
32
  }
33
+ /**
34
+ * A stable key a client can branch on, beside the human-readable `error`.
35
+ * Today the one member is the refusal a desktop sends for a verb its build
36
+ * does not know (see `UnsupportedVerbError` in framing.ts), so a newer phone
37
+ * can tell "desktop is old" from "desktop is unreachable" and show the right
38
+ * copy. A union rather than a bare string so a second code is a deliberate
39
+ * addition here, not a typo at a call site.
40
+ */
41
+ export type CapabilityErrorCode = 'unsupported-verb';
33
42
  export interface CapabilityResponseMessage {
34
43
  type: 'capability-response';
35
44
  requestId: string;
@@ -37,6 +46,12 @@ export interface CapabilityResponseMessage {
37
46
  payload?: JsonValue;
38
47
  /** Present only when ok is false. */
39
48
  error?: string;
49
+ /**
50
+ * Present only when ok is false AND the refusal has a stable key. Additive
51
+ * on the wire: a peer whose decoder predates the field rebuilds the message
52
+ * from the keys it knows and keeps `error`, so the text still shows.
53
+ */
54
+ code?: CapabilityErrorCode;
40
55
  }
41
56
  export interface EventMessage {
42
57
  type: 'event';
@@ -278,6 +278,38 @@ export interface MoveTaskRequestPayload {
278
278
  export interface MoveTaskResponsePayload {
279
279
  ok: boolean;
280
280
  }
281
+ /**
282
+ * Keyed by task, not by session: the state this verb exists for is a task
283
+ * whose session has ENDED, so there is no live session id to name. The
284
+ * desktop spawns or resumes in the task's current column with that column's
285
+ * settings; a live session is an idempotent no-op.
286
+ */
287
+ export interface StartSessionRequestPayload {
288
+ taskId: string;
289
+ projectId: string;
290
+ }
291
+ /**
292
+ * What the desktop did with an accepted start. `starting`: the worktree and
293
+ * PTY work run behind this response and the successor's arrival reaches the
294
+ * phone as a board / stream event, the same way a column move's does. `live`:
295
+ * the task already had a running session, nothing was spawned, and NO event
296
+ * is coming, so a phone that tapped Start from a stale screen must refresh
297
+ * its board and stream itself rather than wait.
298
+ */
299
+ export type StartSessionOutcome = 'starting' | 'live';
300
+ /**
301
+ * `ok` means the start was ACCEPTED (the task exists, is in a column that may
302
+ * run an agent, and either has no live session or already has one), not that
303
+ * a successor is up: the desktop answers before the worktree and PTY work so
304
+ * the response fits the phone's per-verb budget. `outcome` says which of the
305
+ * two accepted shapes this was.
306
+ */
307
+ export interface StartSessionResponsePayload {
308
+ ok: boolean;
309
+ outcome: StartSessionOutcome;
310
+ }
311
+ /** Phone-side guard: narrows a decoded start-session response before the phone acts on it. */
312
+ export declare function parseStartSessionResponsePayload(payload: JsonValue): StartSessionResponsePayload;
281
313
  export interface AnswerPermissionPromptRequestPayload {
282
314
  sessionId: string;
283
315
  /** The prompt id the phone believes is outstanding - the handler rejects a stale/replayed answer whose id no longer matches the live awaited prompt. */
@@ -363,6 +395,7 @@ export interface CapabilityRequestPayloadMap {
363
395
  'board-tool-read': BoardToolRequestPayload;
364
396
  'board-tool-write': BoardToolRequestPayload;
365
397
  'register-push': RegisterPushRequestPayload;
398
+ 'start-session': StartSessionRequestPayload;
366
399
  }
367
400
  export interface CapabilityResponsePayloadMap {
368
401
  'read-stream': ReadStreamResponsePayload | TranscriptWindowResponsePayload;
@@ -375,6 +408,7 @@ export interface CapabilityResponsePayloadMap {
375
408
  'board-tool-read': BoardToolResponsePayload;
376
409
  'board-tool-write': BoardToolResponsePayload;
377
410
  'register-push': RegisterPushResponsePayload;
411
+ 'start-session': StartSessionResponsePayload;
378
412
  }
379
413
  /**
380
414
  * Validates and narrows a decoded capability-request's generic JsonValue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Kangentic mobile bridge wire protocol: framing, Noise-based pairing and session handshakes, signed device roster, and capability verbs shared between desktop and mobile.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",