@kangentic/protocol 0.17.0 → 0.18.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.
@@ -16,9 +16,15 @@
16
16
  * `start-session` is the second lifecycle verb beside `move-task`: it spawns
17
17
  * or resumes a task's session in the column the task is already in, so a
18
18
  * phone's "Session ended" state can bring the agent back without moving the
19
- * task. A live session is an idempotent no-op; nothing is ever stopped.
19
+ * task. A live session is an idempotent no-op; `start-session` never stops
20
+ * anything.
21
+ *
22
+ * `pause-session` is the third lifecycle verb: it suspends a task's live
23
+ * session exactly as the desktop's Pause button does, without moving the task,
24
+ * and keeps the conversation for a later `start-session` Resume. Gate it on
25
+ * `BoardTaskWire.pausable`.
20
26
  */
21
- export declare const CAPABILITY_VERBS: readonly ["read-stream", "read-board", "read-diff", "send-user-message", "move-task", "answer-permission-prompt", "interactive-terminal", "board-tool-read", "board-tool-write", "register-push", "start-session"];
27
+ export declare const CAPABILITY_VERBS: readonly ["read-stream", "read-board", "read-diff", "send-user-message", "move-task", "answer-permission-prompt", "interactive-terminal", "board-tool-read", "board-tool-write", "register-push", "start-session", "pause-session"];
22
28
  export type CapabilityVerb = (typeof CAPABILITY_VERBS)[number];
23
29
  export declare function isCapabilityVerb(value: string): value is CapabilityVerb;
24
30
  /** Deny-by-default: a verb absent from the set is not authorized. */
@@ -409,9 +409,14 @@ export interface BoardTaskWire {
409
409
  * a Command Terminal) and no live one. No column or archive gate applies. It
410
410
  * is the fact the desktop card's "Paused" footer reads, so a paused task in
411
411
  * Done or on the archived page reads true here while `resumable` reads
412
- * false. A move into To Do removes the task's session rows, so a task there
413
- * normally reads false. Gate Resume on `resumable` only; `resumable: true`
414
- * always comes with `paused: true`.
412
+ * false. The desktop draws an archived task as a compact card with no
413
+ * footer, so a client that matches the desktop card draws "Paused" only
414
+ * when `archived_at` is null. A move from another column into Done
415
+ * archives the task in the same step, so only a task that sits in Done
416
+ * unarchived gets the full card and its footer. A move into To Do removes
417
+ * the task's session rows, so a task there normally reads false. Gate
418
+ * Resume on `resumable` only; `resumable: true` always comes with
419
+ * `paused: true`.
415
420
  *
416
421
  * A desktop pause clears the task's `session_id`, so find a paused session
417
422
  * from this field, not from `session_id`. A respawn queued behind the
@@ -429,6 +434,37 @@ export interface BoardTaskWire {
429
434
  * the key from every real wire response.
430
435
  */
431
436
  paused?: boolean | null;
437
+ /**
438
+ * True when the task has a live session (`running` or `queued`) and is not
439
+ * in To Do. It is the desktop's promise that `pause-session` for this task
440
+ * pauses that session exactly as its Pause button does: the conversation is
441
+ * kept for a later Resume, and the pause sticks, so a column with auto-spawn
442
+ * on does not start the task again by itself. Gate a phone's Pause control on
443
+ * THIS field.
444
+ *
445
+ * Done and archived do not clear it, matching the desktop, whose Pause stays
446
+ * available there because it is the only in-window stop for a live agent. A
447
+ * move into Done suspends the task's session in the same step, so a task
448
+ * there normally has no live session and reads false. It reads true only
449
+ * when that suspend failed and left the agent running.
450
+ *
451
+ * Never true while `paused` is true: a paused task has no live session. While
452
+ * `spawn_progress` is non-null and no session is live, the desktop card still
453
+ * shows a Pause control, but its click only cancels an in-flight resume or
454
+ * start, which `pause-session` does not promise. This field reads false
455
+ * then, and the verb refuses a task with no live session.
456
+ *
457
+ * A pause, a session's start and its end each reach the phone as a
458
+ * `task-updated` board event, after which the next snapshot carries the new
459
+ * value. Null or absent means a desktop that predates the field
460
+ * (pre-0.18.0), and a client offers no Pause then: a 0.15.0 to 0.17.0
461
+ * desktop refuses `pause-session` with the `unsupported-verb` code, and an
462
+ * older one does not answer it at all.
463
+ *
464
+ * Declared OPTIONAL (`?`) like `paused`; `parseBoardTaskWire` populates the
465
+ * key from every real wire response.
466
+ */
467
+ pausable?: boolean | null;
432
468
  }
433
469
  /** Phone-needed subset of the desktop's BacklogTask row. */
434
470
  export interface BacklogItemWire {
package/dist/index.cjs CHANGED
@@ -90,6 +90,7 @@ __export(index_exports, {
90
90
  parseCapabilityRequestPayload: () => parseCapabilityRequestPayload,
91
91
  parseDiffFileContentWire: () => parseDiffFileContentWire,
92
92
  parseDiffFileListWire: () => parseDiffFileListWire,
93
+ parsePauseSessionResponsePayload: () => parsePauseSessionResponsePayload,
93
94
  parsePushEnvelopePlaintext: () => parsePushEnvelopePlaintext,
94
95
  parseReadBoardResponsePayload: () => parseReadBoardResponsePayload,
95
96
  parseReadDiffResponsePayload: () => parseReadDiffResponsePayload,
@@ -4971,7 +4972,8 @@ var CAPABILITY_VERBS = [
4971
4972
  "board-tool-read",
4972
4973
  "board-tool-write",
4973
4974
  "register-push",
4974
- "start-session"
4975
+ "start-session",
4976
+ "pause-session"
4975
4977
  ];
4976
4978
  function isCapabilityVerb(value) {
4977
4979
  return CAPABILITY_VERBS.includes(value);
@@ -5466,7 +5468,8 @@ function parseBoardTaskWire(value) {
5466
5468
  updated_at: requireString(value, "updated_at", "board task"),
5467
5469
  spawn_progress: nullableString(value, "spawn_progress"),
5468
5470
  resumable: nullableBoolean(value, "resumable"),
5469
- paused: nullableBoolean(value, "paused")
5471
+ paused: nullableBoolean(value, "paused"),
5472
+ pausable: nullableBoolean(value, "pausable")
5470
5473
  };
5471
5474
  }
5472
5475
  function parseBacklogItemWire(value) {
@@ -5778,6 +5781,20 @@ function parseStartSessionResponsePayload(payload) {
5778
5781
  }
5779
5782
  return { ok: payload.ok, outcome: payload.outcome };
5780
5783
  }
5784
+ function parsePauseSessionRequestPayload(payload) {
5785
+ if (!isRecord(payload)) throw new Error("pause-session payload must be an object");
5786
+ if (typeof payload.taskId !== "string") throw new Error('pause-session payload missing "taskId"');
5787
+ if (typeof payload.projectId !== "string") throw new Error('pause-session payload missing "projectId"');
5788
+ return {
5789
+ taskId: payload.taskId,
5790
+ projectId: payload.projectId
5791
+ };
5792
+ }
5793
+ function parsePauseSessionResponsePayload(payload) {
5794
+ if (!isRecord(payload)) throw new Error("pause-session response must be an object");
5795
+ if (typeof payload.ok !== "boolean") throw new Error('pause-session response missing "ok"');
5796
+ return { ok: payload.ok };
5797
+ }
5781
5798
  function parseAnswerPermissionPromptRequestPayload(payload) {
5782
5799
  if (!isRecord(payload)) throw new Error("answer-permission-prompt payload must be an object");
5783
5800
  if (typeof payload.sessionId !== "string") throw new Error('answer-permission-prompt payload missing "sessionId"');
@@ -5875,6 +5892,8 @@ function parseCapabilityRequestPayload(verb, payload) {
5875
5892
  return parseRegisterPushRequestPayload(payload);
5876
5893
  case "start-session":
5877
5894
  return parseStartSessionRequestPayload(payload);
5895
+ case "pause-session":
5896
+ return parsePauseSessionRequestPayload(payload);
5878
5897
  default: {
5879
5898
  const exhaustiveCheck = verb;
5880
5899
  throw new Error(`Unknown capability verb: ${String(exhaustiveCheck)}`);
@@ -6278,6 +6297,7 @@ function isBridgeEvent(value) {
6278
6297
  parseCapabilityRequestPayload,
6279
6298
  parseDiffFileContentWire,
6280
6299
  parseDiffFileListWire,
6300
+ parsePauseSessionResponsePayload,
6281
6301
  parsePushEnvelopePlaintext,
6282
6302
  parseReadBoardResponsePayload,
6283
6303
  parseReadDiffResponsePayload,
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, 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';
15
+ export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, parseRegisterPushRequestPayload, parseStartSessionResponsePayload, parsePauseSessionResponsePayload, 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, type PauseSessionRequestPayload, type PauseSessionResponsePayload, } 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
@@ -4845,7 +4845,8 @@ var CAPABILITY_VERBS = [
4845
4845
  "board-tool-read",
4846
4846
  "board-tool-write",
4847
4847
  "register-push",
4848
- "start-session"
4848
+ "start-session",
4849
+ "pause-session"
4849
4850
  ];
4850
4851
  function isCapabilityVerb(value) {
4851
4852
  return CAPABILITY_VERBS.includes(value);
@@ -5340,7 +5341,8 @@ function parseBoardTaskWire(value) {
5340
5341
  updated_at: requireString(value, "updated_at", "board task"),
5341
5342
  spawn_progress: nullableString(value, "spawn_progress"),
5342
5343
  resumable: nullableBoolean(value, "resumable"),
5343
- paused: nullableBoolean(value, "paused")
5344
+ paused: nullableBoolean(value, "paused"),
5345
+ pausable: nullableBoolean(value, "pausable")
5344
5346
  };
5345
5347
  }
5346
5348
  function parseBacklogItemWire(value) {
@@ -5652,6 +5654,20 @@ function parseStartSessionResponsePayload(payload) {
5652
5654
  }
5653
5655
  return { ok: payload.ok, outcome: payload.outcome };
5654
5656
  }
5657
+ function parsePauseSessionRequestPayload(payload) {
5658
+ if (!isRecord(payload)) throw new Error("pause-session payload must be an object");
5659
+ if (typeof payload.taskId !== "string") throw new Error('pause-session payload missing "taskId"');
5660
+ if (typeof payload.projectId !== "string") throw new Error('pause-session payload missing "projectId"');
5661
+ return {
5662
+ taskId: payload.taskId,
5663
+ projectId: payload.projectId
5664
+ };
5665
+ }
5666
+ function parsePauseSessionResponsePayload(payload) {
5667
+ if (!isRecord(payload)) throw new Error("pause-session response must be an object");
5668
+ if (typeof payload.ok !== "boolean") throw new Error('pause-session response missing "ok"');
5669
+ return { ok: payload.ok };
5670
+ }
5655
5671
  function parseAnswerPermissionPromptRequestPayload(payload) {
5656
5672
  if (!isRecord(payload)) throw new Error("answer-permission-prompt payload must be an object");
5657
5673
  if (typeof payload.sessionId !== "string") throw new Error('answer-permission-prompt payload missing "sessionId"');
@@ -5749,6 +5765,8 @@ function parseCapabilityRequestPayload(verb, payload) {
5749
5765
  return parseRegisterPushRequestPayload(payload);
5750
5766
  case "start-session":
5751
5767
  return parseStartSessionRequestPayload(payload);
5768
+ case "pause-session":
5769
+ return parsePauseSessionRequestPayload(payload);
5752
5770
  default: {
5753
5771
  const exhaustiveCheck = verb;
5754
5772
  throw new Error(`Unknown capability verb: ${String(exhaustiveCheck)}`);
@@ -6151,6 +6169,7 @@ export {
6151
6169
  parseCapabilityRequestPayload,
6152
6170
  parseDiffFileContentWire,
6153
6171
  parseDiffFileListWire,
6172
+ parsePauseSessionResponsePayload,
6154
6173
  parsePushEnvelopePlaintext,
6155
6174
  parseReadBoardResponsePayload,
6156
6175
  parseReadDiffResponsePayload,
@@ -342,6 +342,29 @@ export interface StartSessionResponsePayload {
342
342
  }
343
343
  /** Phone-side guard: narrows a decoded start-session response before the phone acts on it. */
344
344
  export declare function parseStartSessionResponsePayload(payload: JsonValue): StartSessionResponsePayload;
345
+ /**
346
+ * Keyed by task, like `start-session`: the desktop resolves the task's live
347
+ * session itself (a pause clears the task's `session_id`, and a stale phone
348
+ * view may name a session that already ended). It suspends that session the
349
+ * way the desktop's Pause button does, in the column the task is already in.
350
+ */
351
+ export interface PauseSessionRequestPayload {
352
+ taskId: string;
353
+ projectId: string;
354
+ }
355
+ /**
356
+ * `ok` means the pause was ACCEPTED: the task had a live session, and the
357
+ * desktop has recorded it as paused by the user. The agent's own shutdown runs
358
+ * behind this response, because it can outlast the phone's per-verb budget,
359
+ * and its end reaches the phone as the board event that carries
360
+ * `paused: true`. A task with no live session is refused (`ok: false` on the
361
+ * capability response), so there is no idempotent shape to report here.
362
+ */
363
+ export interface PauseSessionResponsePayload {
364
+ ok: boolean;
365
+ }
366
+ /** Phone-side guard: narrows a decoded pause-session response before the phone acts on it. */
367
+ export declare function parsePauseSessionResponsePayload(payload: JsonValue): PauseSessionResponsePayload;
345
368
  export interface AnswerPermissionPromptRequestPayload {
346
369
  sessionId: string;
347
370
  /** The prompt id the phone believes is outstanding - the handler rejects a stale/replayed answer whose id no longer matches the live awaited prompt. */
@@ -428,6 +451,7 @@ export interface CapabilityRequestPayloadMap {
428
451
  'board-tool-write': BoardToolRequestPayload;
429
452
  'register-push': RegisterPushRequestPayload;
430
453
  'start-session': StartSessionRequestPayload;
454
+ 'pause-session': PauseSessionRequestPayload;
431
455
  }
432
456
  export interface CapabilityResponsePayloadMap {
433
457
  'read-stream': ReadStreamResponsePayload | TranscriptWindowResponsePayload;
@@ -441,6 +465,7 @@ export interface CapabilityResponsePayloadMap {
441
465
  'board-tool-write': BoardToolResponsePayload;
442
466
  'register-push': RegisterPushResponsePayload;
443
467
  'start-session': StartSessionResponsePayload;
468
+ 'pause-session': PauseSessionResponsePayload;
444
469
  }
445
470
  /**
446
471
  * 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.17.0",
3
+ "version": "0.18.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",