@kangentic/protocol 0.15.0 → 0.16.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.
@@ -27,6 +27,7 @@
27
27
  * event before its router dispatches on `kind`.
28
28
  */
29
29
  import type { JsonValue } from '../wire/messages';
30
+ import { type ReadStreamSessionStatusWire } from '../wire/payloads';
30
31
  import { type ActivityReasonWire, type ActivityStateWire, type SessionEventWire, type SessionUsageWire, type TerminalDimensionsWire, type TranscriptEventPayload } from './payloads';
31
32
  export interface TranscriptEvent {
32
33
  kind: 'transcript';
@@ -60,11 +61,25 @@ export type ActivityEventPayload = {
60
61
  options?: string[];
61
62
  }
62
63
  /**
63
- * The streamed session's PTY exited. Pushed once by the desktop's
64
- * read-stream subscription right before it tears itself down, so the
65
- * phone learns "this session is over" from the feed itself instead of
66
- * inferring it from silence. `intentional` distinguishes a deliberate
67
- * stop (desktop Stop button, suspend, shutdown) from a crash.
64
+ * The feed's last word: the desktop's read-stream subscription pushes this
65
+ * once, right before it tears itself down, so the phone learns "this
66
+ * session is over" from the feed itself instead of inferring it from
67
+ * silence. Sent when the streamed session's PTY exits. From 0.16.0 it is
68
+ * also sent when a paused row, which the feed holds without a PTY, is
69
+ * replaced or removed. `intentional` distinguishes a deliberate stop
70
+ * (desktop Stop button, suspend, shutdown, a resume replacing a paused row)
71
+ * from a crash.
72
+ *
73
+ * `successorSessionId`, when present, names the session that replaced this
74
+ * one for the same task. A desktop resume never reuses the paused session's
75
+ * id: it spawns a new session and drops the paused row, so a feed held on
76
+ * the paused session can only end, never turn back into `running`. The task
77
+ * continues as the successor; subscribe to it. Its snapshot carries
78
+ * `sessionStatus` (`queued` when the desktop's concurrency limit holds it)
79
+ * and `resuming: true`. Absent when no successor exists (a removed paused
80
+ * row, or a successor whose own spawn failed), and from pre-0.16.0
81
+ * desktops, which never end a feed this way and leave it silent instead. An
82
+ * older phone ignores the field and still sees an ended session.
68
83
  *
69
84
  * `spawnProgressLabel`, when present, means the desktop had a respawn in
70
85
  * flight for this task when the session ended - a same-column respawn
@@ -81,7 +96,8 @@ export type ActivityEventPayload = {
81
96
  * the desktop's own display text for what it is doing ("Switching
82
97
  * model...", "Starting agent...", a live git-queue wait count) - treat it
83
98
  * as untrusted display text, not a key to switch on, cap its length, and
84
- * fall back to generic copy rather than parsing it.
99
+ * fall back to generic copy rather than parsing it. From 0.16.0 the desktop
100
+ * sends it sanitized as `BoardTaskWire.spawn_progress` describes.
85
101
  *
86
102
  * INTENT, not a guarantee, on the same terms `BoardColumnWire.spawns_session`
87
103
  * documents: the desktop can suspend without a successor ever landing (a
@@ -107,6 +123,41 @@ export type ActivityEventPayload = {
107
123
  type: 'session-ended';
108
124
  intentional: boolean;
109
125
  spawnProgressLabel?: string;
126
+ successorSessionId?: string;
127
+ }
128
+ /**
129
+ * The streamed session's lifecycle status changed after the subscribe
130
+ * snapshot: `queued` -> `running` when the desktop's concurrency limit
131
+ * frees a slot, -> `suspended` on a pause or a respawn, -> `exited` when the
132
+ * desktop retires a session whose agent already ended. Sent only on an
133
+ * actual change, for the same session id only. A resume is a NEW session
134
+ * id, so suspended -> running never arrives here: see `session-ended`'s
135
+ * `successorSessionId`.
136
+ *
137
+ * A `suspended` push precedes the PTY's `session-ended` when a running
138
+ * session is suspended, so for an instant both say the session is parked.
139
+ *
140
+ * Precedence, matching the desktop card: a non-null `spawn_progress` on the
141
+ * task's board row overrides `suspended` (the desktop suspends a session
142
+ * before every model, agent or effort respawn), and never `running` or
143
+ * `queued`. Read `suspended` alone as "Paused"; read it next to a label as
144
+ * that label.
145
+ *
146
+ * `resuming` and `resumable` repeat the snapshot fields so the payload is
147
+ * complete on its own. The desktop re-reads all three on each lifecycle
148
+ * edge of this session and sends the payload when any of them differs. A
149
+ * move to Done or an archive of a paused task is no edge of its row, so
150
+ * `resumable` here can stay true until the feed ends; gate Resume on
151
+ * `BoardTaskWire.resumable`, which the board event for that move refreshes.
152
+ * Absent from pre-0.16.0 desktops, whose phones drop the unknown
153
+ * type through `isBridgeEvent` and keep inferring status from activity
154
+ * events.
155
+ */
156
+ | {
157
+ type: 'status';
158
+ status: ReadStreamSessionStatusWire;
159
+ resuming: boolean;
160
+ resumable: boolean;
110
161
  }
111
162
  /**
112
163
  * The agent's most recent assistant message, already collapsed to a short
@@ -160,6 +160,12 @@ export interface SessionUsageWire {
160
160
  totalCostUsd: number;
161
161
  totalDurationMs: number;
162
162
  };
163
+ /**
164
+ * Completed tool calls across every run of the session. A resume (an app
165
+ * restart, a pause and resume) is a new run, and the desktop adds the
166
+ * session's earlier runs to the live count. Optional: an older desktop
167
+ * omits it.
168
+ */
163
169
  toolCallCount?: number;
164
170
  model: {
165
171
  id: string;
@@ -325,6 +331,77 @@ export interface BoardTaskWire {
325
331
  archived_at: string | null;
326
332
  created_at: string;
327
333
  updated_at: string;
334
+ /**
335
+ * The task's in-flight spawn-progress label, the text of the desktop card's
336
+ * preparing footer: "Creating worktree...", "Fetching latest...",
337
+ * "Waiting (2 ahead)", "Switching model...", "Starting agent...", a git-queue
338
+ * wait with its elapsed time, and any staleness note the desktop appends
339
+ * ("Starting agent... (base 3 behind)"). The desktop announces every change
340
+ * with a `task-updated` board event, coalesced to at most about one a
341
+ * second per task.
342
+ *
343
+ * Untrusted display text, the same terms as `session-ended`'s
344
+ * `spawnProgressLabel`: never parse it or switch on it, and fall back to
345
+ * generic copy. It can carry text the desktop did not write: a column
346
+ * automation's name, in `Running "<name>"...`, from a config a team may
347
+ * share. From 0.16.0 the desktop strips escape sequences, control characters,
348
+ * invisible format characters (bidi controls, the zero-width space, the soft
349
+ * hyphen, the byte order mark) and tag characters outside the England,
350
+ * Scotland and Wales flag emoji, turns line breaks and tabs into spaces, and
351
+ * caps it at
352
+ * 120 characters ending in "...", but a client still caps it for its own
353
+ * layout.
354
+ *
355
+ * Precedence, matching the desktop card: a label overrides a `suspended`
356
+ * session status (a respawn is in flight behind a session that was just
357
+ * suspended for it), and never a `running` or `queued` one. A client that
358
+ * reads the read-stream feed's `status: 'suspended'` during a model or
359
+ * effort switch shows this label, not "Paused".
360
+ *
361
+ * A spawn path that dies without clearing its label leaves it in place for
362
+ * up to the desktop's 120s spawn-progress TTL, and the TTL expiry sends no
363
+ * board event, so a stale label can outlive its spawn until the next
364
+ * snapshot. Null or absent means no spawn is in flight, or a desktop that
365
+ * predates the field (pre-0.16.0). Under `view: 'sessions'` a task with a
366
+ * label is kept even while its `session_id` is null, so a first start and
367
+ * a respawn's gap stay visible.
368
+ *
369
+ * Declared OPTIONAL (`?`) like `pr_merge_readiness`; `parseBoardTaskWire`
370
+ * populates the key from every real wire response.
371
+ */
372
+ spawn_progress?: string | null;
373
+ /**
374
+ * True when the task's session is paused and the desktop's own task view
375
+ * would offer Resume for it: the task is not in To Do or Done and not
376
+ * archived. It is the desktop's promise that `start-session` for this task
377
+ * resumes the paused session exactly as its Resume button does: the same
378
+ * conversation, the column's model, effort and permission, a "Resuming
379
+ * session..." label, and NO enter automations or column message. Gate a
380
+ * phone's Resume control on THIS field.
381
+ *
382
+ * On the board row because the phone always holds the board: a desktop
383
+ * pause clears the task's `session_id`, so a phone keeps no read-stream on
384
+ * a paused session (the stream's `resumable` covers only an open session
385
+ * screen through the suspend itself). A pause, a resume, a column move and
386
+ * an archive each reach the phone as a `task-updated` board event, after
387
+ * which the next snapshot carries the new value. Under `view: 'sessions'` a
388
+ * resumable task is kept even though its `session_id` is null, so an agent
389
+ * feed can draw it as Paused with a Resume control.
390
+ *
391
+ * Explicit on purpose, never inferred from another field: a pre-0.16.0
392
+ * desktop answers `start-session` by STARTING the column, which re-runs its
393
+ * enter automations and resends its message (a paused task in Code Review
394
+ * would get `/code-review` again). Null or absent means such a desktop; a
395
+ * client offers no Resume then. While `spawn_progress` is non-null a spawn
396
+ * is already in flight and the desktop card shows that label instead of a
397
+ * Resume control; a client applies the same precedence. `start-session`
398
+ * re-checks the column and refuses a To Do, Done or archived task with the
399
+ * desktop's Resume copy, so a value overtaken by a move is safe to act on.
400
+ *
401
+ * Declared OPTIONAL (`?`) like `spawns_session`; `parseBoardTaskWire`
402
+ * populates the key from every real wire response.
403
+ */
404
+ resumable?: boolean | null;
328
405
  }
329
406
  /** Phone-needed subset of the desktop's BacklogTask row. */
330
407
  export interface BacklogItemWire {
package/dist/index.cjs CHANGED
@@ -75,6 +75,7 @@ __export(index_exports, {
75
75
  isDoneRole: () => isDoneRole,
76
76
  isJsonValue: () => isJsonValue,
77
77
  isPushCategory: () => isPushCategory,
78
+ isReadStreamSessionStatusWire: () => isReadStreamSessionStatusWire,
78
79
  isRecord: () => isRecord,
79
80
  isRosterEntryExpired: () => isRosterEntryExpired,
80
81
  isSecureRelayAddress: () => isSecureRelayAddress,
@@ -5462,7 +5463,9 @@ function parseBoardTaskWire(value) {
5462
5463
  attachment_count: typeof value.attachment_count === "number" ? value.attachment_count : 0,
5463
5464
  archived_at: nullableString(value, "archived_at"),
5464
5465
  created_at: requireString(value, "created_at", "board task"),
5465
- updated_at: requireString(value, "updated_at", "board task")
5466
+ updated_at: requireString(value, "updated_at", "board task"),
5467
+ spawn_progress: nullableString(value, "spawn_progress"),
5468
+ resumable: nullableBoolean(value, "resumable")
5466
5469
  };
5467
5470
  }
5468
5471
  function parseBacklogItemWire(value) {
@@ -5514,6 +5517,9 @@ function parseDiffFileContentWire(payload) {
5514
5517
 
5515
5518
  // src/wire/payloads.ts
5516
5519
  var READ_STREAM_SESSION_STATUSES = ["running", "queued", "suspended", "exited"];
5520
+ function isReadStreamSessionStatusWire(value) {
5521
+ return typeof value === "string" && READ_STREAM_SESSION_STATUSES.includes(value);
5522
+ }
5517
5523
  function parseReadStreamResponsePayload(payload) {
5518
5524
  if (!isRecord(payload)) throw new Error("read-stream response must be an object");
5519
5525
  if (typeof payload.scrollback !== "string") throw new Error('read-stream response is missing "scrollback"');
@@ -5544,11 +5550,19 @@ function parseReadStreamResponsePayload(payload) {
5544
5550
  response.ptyDimensions = parseTerminalDimensionsWire(payload.ptyDimensions);
5545
5551
  }
5546
5552
  if (payload.sessionStatus !== void 0) {
5547
- if (typeof payload.sessionStatus !== "string" || !READ_STREAM_SESSION_STATUSES.includes(payload.sessionStatus)) {
5553
+ if (!isReadStreamSessionStatusWire(payload.sessionStatus)) {
5548
5554
  throw new Error('read-stream response has an invalid "sessionStatus"');
5549
5555
  }
5550
5556
  response.sessionStatus = payload.sessionStatus;
5551
5557
  }
5558
+ if (payload.resuming !== void 0) {
5559
+ if (typeof payload.resuming !== "boolean") throw new Error('read-stream response has an invalid "resuming"');
5560
+ response.resuming = payload.resuming;
5561
+ }
5562
+ if (payload.resumable !== void 0) {
5563
+ if (typeof payload.resumable !== "boolean") throw new Error('read-stream response has an invalid "resumable"');
5564
+ response.resumable = payload.resumable;
5565
+ }
5552
5566
  return response;
5553
5567
  }
5554
5568
  function parseTranscriptWindowResponsePayload(payload) {
@@ -6116,11 +6130,24 @@ function parseActivityEventPayload(payload) {
6116
6130
  }
6117
6131
  case "session-ended": {
6118
6132
  if (typeof payload.intentional !== "boolean") throw new Error('session-ended payload is missing "intentional"');
6119
- if (payload.spawnProgressLabel === void 0) {
6120
- return { type: "session-ended", intentional: payload.intentional };
6133
+ if (payload.spawnProgressLabel !== void 0 && typeof payload.spawnProgressLabel !== "string") {
6134
+ throw new Error('session-ended payload has an invalid "spawnProgressLabel"');
6135
+ }
6136
+ if (payload.successorSessionId !== void 0 && typeof payload.successorSessionId !== "string") {
6137
+ throw new Error('session-ended payload has an invalid "successorSessionId"');
6121
6138
  }
6122
- if (typeof payload.spawnProgressLabel !== "string") throw new Error('session-ended payload has an invalid "spawnProgressLabel"');
6123
- return { type: "session-ended", intentional: payload.intentional, spawnProgressLabel: payload.spawnProgressLabel };
6139
+ return {
6140
+ type: "session-ended",
6141
+ intentional: payload.intentional,
6142
+ ...payload.spawnProgressLabel !== void 0 ? { spawnProgressLabel: payload.spawnProgressLabel } : {},
6143
+ ...payload.successorSessionId !== void 0 ? { successorSessionId: payload.successorSessionId } : {}
6144
+ };
6145
+ }
6146
+ case "status": {
6147
+ if (!isReadStreamSessionStatusWire(payload.status)) throw new Error('status payload has an invalid "status"');
6148
+ if (typeof payload.resuming !== "boolean") throw new Error('status payload is missing "resuming"');
6149
+ if (typeof payload.resumable !== "boolean") throw new Error('status payload is missing "resumable"');
6150
+ return { type: "status", status: payload.status, resuming: payload.resuming, resumable: payload.resumable };
6124
6151
  }
6125
6152
  case "message-preview": {
6126
6153
  if (typeof payload.text !== "string") throw new Error('message-preview payload is missing "text"');
@@ -6235,6 +6262,7 @@ function isBridgeEvent(value) {
6235
6262
  isDoneRole,
6236
6263
  isJsonValue,
6237
6264
  isPushCategory,
6265
+ isReadStreamSessionStatusWire,
6238
6266
  isRecord,
6239
6267
  isRosterEntryExpired,
6240
6268
  isSecureRelayAddress,
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export type { JsonValue, BridgeMessage, HeartbeatMessage, CapabilityRequestMessa
12
12
  export { encodeMessage, decodeMessage, UnsupportedVerbError, isUnsupportedVerbError, UNSUPPORTED_VERB_ERROR_CODE, MAX_FRAME_LENGTH, MAX_DECODED_LENGTH, COMPRESSION_THRESHOLD, } from './wire/framing';
13
13
  export { isJsonValue, isRecord } from './wire/json-value';
14
14
  export { SessionFrameKind, wrapSessionFrame, unwrapSessionFrame } from './wire/session-frame';
15
- export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, parseRegisterPushRequestPayload, parseStartSessionResponsePayload, type CapabilityRequestPayloadMap, type CapabilityResponsePayloadMap, type ReadStreamRequestPayload, type ReadStreamResponsePayload, type ReadStreamSessionStatusWire, type TranscriptWindowResponsePayload, type ReadBoardRequestPayload, type ReadBoardView, type ReadBoardProjectSummary, type ReadBoardProjectGroup, type ReadBoardProjectListResponsePayload, type ReadBoardSnapshotResponsePayload, type ReadBoardArchivedResponsePayload, type ReadBoardResponsePayload, type ReadDiffScope, type ReadDiffRequestPayload, type ReadDiffResponsePayload, type SendUserMessageRequestPayload, type SendUserMessageResponsePayload, type MoveTaskRequestPayload, type MoveTaskResponsePayload, type AnswerPermissionPromptRequestPayload, type AnswerPermissionPromptResponsePayload, type InteractiveTerminalRequestPayload, type InteractiveTerminalResponsePayload, type BoardToolRequestPayload, type BoardToolResponsePayload, type RegisterPushRequestPayload, type RegisterPushResponsePayload, type StartSessionRequestPayload, type StartSessionResponsePayload, type StartSessionOutcome, } from './wire/payloads';
15
+ export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, parseRegisterPushRequestPayload, parseStartSessionResponsePayload, isReadStreamSessionStatusWire, type CapabilityRequestPayloadMap, type CapabilityResponsePayloadMap, type ReadStreamRequestPayload, type ReadStreamResponsePayload, type ReadStreamSessionStatusWire, type TranscriptWindowResponsePayload, type ReadBoardRequestPayload, type ReadBoardView, type ReadBoardProjectSummary, type ReadBoardProjectGroup, type ReadBoardProjectListResponsePayload, type ReadBoardSnapshotResponsePayload, type ReadBoardArchivedResponsePayload, type ReadBoardResponsePayload, type ReadDiffScope, type ReadDiffRequestPayload, type ReadDiffResponsePayload, type SendUserMessageRequestPayload, type SendUserMessageResponsePayload, type MoveTaskRequestPayload, type MoveTaskResponsePayload, type AnswerPermissionPromptRequestPayload, type AnswerPermissionPromptResponsePayload, type InteractiveTerminalRequestPayload, type InteractiveTerminalResponsePayload, type BoardToolRequestPayload, type BoardToolResponsePayload, type RegisterPushRequestPayload, type RegisterPushResponsePayload, type StartSessionRequestPayload, type StartSessionResponsePayload, type StartSessionOutcome, } from './wire/payloads';
16
16
  export { PUSH_CATEGORIES, isPushCategory, parsePushEnvelopePlaintext, sealPushEnvelope, openPushEnvelope, PUSH_ENVELOPE_MAX_AGE_MS, PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS, type PushCategory, type PushEnvelopePlaintext, } from './crypto/push-envelope';
17
17
  export { PAIRING_URI_SCHEME, encodePairingQrPayload, decodePairingQrPayload, type PairingQrPayload, } from './pairing/qr';
18
18
  export { sealPairingConfirm, openPairingConfirm } from './pairing/confirm';
package/dist/index.mjs CHANGED
@@ -5337,7 +5337,9 @@ function parseBoardTaskWire(value) {
5337
5337
  attachment_count: typeof value.attachment_count === "number" ? value.attachment_count : 0,
5338
5338
  archived_at: nullableString(value, "archived_at"),
5339
5339
  created_at: requireString(value, "created_at", "board task"),
5340
- updated_at: requireString(value, "updated_at", "board task")
5340
+ updated_at: requireString(value, "updated_at", "board task"),
5341
+ spawn_progress: nullableString(value, "spawn_progress"),
5342
+ resumable: nullableBoolean(value, "resumable")
5341
5343
  };
5342
5344
  }
5343
5345
  function parseBacklogItemWire(value) {
@@ -5389,6 +5391,9 @@ function parseDiffFileContentWire(payload) {
5389
5391
 
5390
5392
  // src/wire/payloads.ts
5391
5393
  var READ_STREAM_SESSION_STATUSES = ["running", "queued", "suspended", "exited"];
5394
+ function isReadStreamSessionStatusWire(value) {
5395
+ return typeof value === "string" && READ_STREAM_SESSION_STATUSES.includes(value);
5396
+ }
5392
5397
  function parseReadStreamResponsePayload(payload) {
5393
5398
  if (!isRecord(payload)) throw new Error("read-stream response must be an object");
5394
5399
  if (typeof payload.scrollback !== "string") throw new Error('read-stream response is missing "scrollback"');
@@ -5419,11 +5424,19 @@ function parseReadStreamResponsePayload(payload) {
5419
5424
  response.ptyDimensions = parseTerminalDimensionsWire(payload.ptyDimensions);
5420
5425
  }
5421
5426
  if (payload.sessionStatus !== void 0) {
5422
- if (typeof payload.sessionStatus !== "string" || !READ_STREAM_SESSION_STATUSES.includes(payload.sessionStatus)) {
5427
+ if (!isReadStreamSessionStatusWire(payload.sessionStatus)) {
5423
5428
  throw new Error('read-stream response has an invalid "sessionStatus"');
5424
5429
  }
5425
5430
  response.sessionStatus = payload.sessionStatus;
5426
5431
  }
5432
+ if (payload.resuming !== void 0) {
5433
+ if (typeof payload.resuming !== "boolean") throw new Error('read-stream response has an invalid "resuming"');
5434
+ response.resuming = payload.resuming;
5435
+ }
5436
+ if (payload.resumable !== void 0) {
5437
+ if (typeof payload.resumable !== "boolean") throw new Error('read-stream response has an invalid "resumable"');
5438
+ response.resumable = payload.resumable;
5439
+ }
5427
5440
  return response;
5428
5441
  }
5429
5442
  function parseTranscriptWindowResponsePayload(payload) {
@@ -5991,11 +6004,24 @@ function parseActivityEventPayload(payload) {
5991
6004
  }
5992
6005
  case "session-ended": {
5993
6006
  if (typeof payload.intentional !== "boolean") throw new Error('session-ended payload is missing "intentional"');
5994
- if (payload.spawnProgressLabel === void 0) {
5995
- return { type: "session-ended", intentional: payload.intentional };
6007
+ if (payload.spawnProgressLabel !== void 0 && typeof payload.spawnProgressLabel !== "string") {
6008
+ throw new Error('session-ended payload has an invalid "spawnProgressLabel"');
6009
+ }
6010
+ if (payload.successorSessionId !== void 0 && typeof payload.successorSessionId !== "string") {
6011
+ throw new Error('session-ended payload has an invalid "successorSessionId"');
5996
6012
  }
5997
- if (typeof payload.spawnProgressLabel !== "string") throw new Error('session-ended payload has an invalid "spawnProgressLabel"');
5998
- return { type: "session-ended", intentional: payload.intentional, spawnProgressLabel: payload.spawnProgressLabel };
6013
+ return {
6014
+ type: "session-ended",
6015
+ intentional: payload.intentional,
6016
+ ...payload.spawnProgressLabel !== void 0 ? { spawnProgressLabel: payload.spawnProgressLabel } : {},
6017
+ ...payload.successorSessionId !== void 0 ? { successorSessionId: payload.successorSessionId } : {}
6018
+ };
6019
+ }
6020
+ case "status": {
6021
+ if (!isReadStreamSessionStatusWire(payload.status)) throw new Error('status payload has an invalid "status"');
6022
+ if (typeof payload.resuming !== "boolean") throw new Error('status payload is missing "resuming"');
6023
+ if (typeof payload.resumable !== "boolean") throw new Error('status payload is missing "resumable"');
6024
+ return { type: "status", status: payload.status, resuming: payload.resuming, resumable: payload.resumable };
5999
6025
  }
6000
6026
  case "message-preview": {
6001
6027
  if (typeof payload.text !== "string") throw new Error('message-preview payload is missing "text"');
@@ -6109,6 +6135,7 @@ export {
6109
6135
  isDoneRole,
6110
6136
  isJsonValue,
6111
6137
  isPushCategory,
6138
+ isReadStreamSessionStatusWire,
6112
6139
  isRecord,
6113
6140
  isRosterEntryExpired,
6114
6141
  isSecureRelayAddress,
@@ -53,6 +53,8 @@ export interface ReadStreamRequestPayload {
53
53
  * snapshot races the session's teardown.
54
54
  */
55
55
  export type ReadStreamSessionStatusWire = 'running' | 'queued' | 'suspended' | 'exited';
56
+ /** True for a value that is one of the four session lifecycle statuses. */
57
+ export declare function isReadStreamSessionStatusWire(value: unknown): value is ReadStreamSessionStatusWire;
56
58
  /** Initial snapshot returned on subscribe; live updates arrive as TerminalEvent/ActivityEvent/TranscriptEvent. */
57
59
  export interface ReadStreamResponsePayload {
58
60
  scrollback: string;
@@ -80,9 +82,34 @@ export interface ReadStreamResponsePayload {
80
82
  ptyDimensions?: TerminalDimensionsWire;
81
83
  /**
82
84
  * The session's lifecycle status at snapshot time. Absent from
83
- * pre-0.5.0 desktops; the phone then assumes 'running'.
85
+ * pre-0.5.0 desktops; the phone then assumes 'running'. Later changes for
86
+ * the same session id arrive as the `status` activity payload.
84
87
  */
85
88
  sessionStatus?: ReadStreamSessionStatusWire;
89
+ /**
90
+ * True when the desktop spawned this session as a resume of an earlier
91
+ * conversation. The desktop card then reads "Resuming agent..." where it
92
+ * would read "Starting agent...", for as long as a running session has no
93
+ * model name yet. Fixed for the life of a session id. Absent from pre-0.16.0
94
+ * desktops; read absent as false.
95
+ */
96
+ resuming?: boolean;
97
+ /**
98
+ * The streamed session's copy of `BoardTaskWire.resumable`: true when this
99
+ * session is paused (`suspended`) and the desktop's own task view would
100
+ * offer Resume for it. Gate a Resume control on the BOARD ROW's field, not
101
+ * this one: a desktop pause clears the task's `session_id`, so a phone holds
102
+ * no stream on a paused session once its board refreshes. This copy keeps a
103
+ * session screen that is open during the suspend current until then, and it
104
+ * carries the same promise: `start-session` resumes rather than starts.
105
+ *
106
+ * Recomputed on each lifecycle edge of this session, and a `status` push
107
+ * carries it even when it is the only value that changed. A move to Done or
108
+ * an archive of a paused task is no edge of its row, so this copy can stay
109
+ * true after the board row's turns false. Absent from pre-0.16.0 desktops,
110
+ * which a client reads as "offer no Resume".
111
+ */
112
+ resumable?: boolean;
86
113
  }
87
114
  /** Phone-side narrowing of a read-stream subscribe response. Throws on a malformed required field. */
88
115
  export declare function parseReadStreamResponsePayload(payload: JsonValue): ReadStreamResponsePayload;
@@ -105,7 +132,10 @@ export declare function parseTranscriptWindowResponsePayload(payload: JsonValue)
105
132
  * - 'sessions': columns, per-column task counts, and only the non-archived
106
133
  * tasks that carry a `session_id`. This is what an agent feed renders: it
107
134
  * watches every project at once but only ever draws the handful of tasks
108
- * with a live agent on them.
135
+ * with a live agent on them. From 0.16.0 it also keeps a task with a
136
+ * `spawn_progress` label in flight (a first start, or a respawn that has
137
+ * nulled `session_id`) and a `resumable` paused task (a desktop pause
138
+ * nulls `session_id` too), so the feed can draw "Paused" with a Resume.
109
139
  * - 'full': every non-archived task, for the one project whose board the user
110
140
  * actually has open.
111
141
  *
@@ -195,7 +225,7 @@ export interface ReadBoardProjectListResponsePayload {
195
225
  export interface ReadBoardSnapshotResponsePayload {
196
226
  projectId: string;
197
227
  columns: BoardColumnWire[];
198
- /** Non-archived tasks; filtered to the ones carrying a `session_id` when `view` is 'sessions'. */
228
+ /** Non-archived tasks; filtered when `view` is 'sessions' (see ReadBoardView for which tasks it keeps). */
199
229
  tasks: BoardTaskWire[];
200
230
  /** Absent whenever the request carried a `view` (protocol 0.9.0) - no phone has ever rendered it. */
201
231
  backlog?: BacklogItemWire[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",