@kangentic/protocol 0.16.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. */
@@ -397,11 +397,74 @@ export interface BoardTaskWire {
397
397
  * Resume control; a client applies the same precedence. `start-session`
398
398
  * re-checks the column and refuses a To Do, Done or archived task with the
399
399
  * desktop's Resume copy, so a value overtaken by a move is safe to act on.
400
+ * A paused task that offers no Resume reads false here. From 0.17.0 `paused`
401
+ * tells it apart from a task with no paused session.
400
402
  *
401
403
  * Declared OPTIONAL (`?`) like `spawns_session`; `parseBoardTaskWire`
402
404
  * populates the key from every real wire response.
403
405
  */
404
406
  resumable?: boolean | null;
407
+ /**
408
+ * True when the task has a paused session (a `suspended` session that is not
409
+ * a Command Terminal) and no live one. No column or archive gate applies. It
410
+ * is the fact the desktop card's "Paused" footer reads, so a paused task in
411
+ * Done or on the archived page reads true here while `resumable` reads
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`.
420
+ *
421
+ * A desktop pause clears the task's `session_id`, so find a paused session
422
+ * from this field, not from `session_id`. A respawn queued behind the
423
+ * desktop's concurrency limit leaves the task holding a paused session and a
424
+ * queued one; that task reads false, as the desktop card shows the queued
425
+ * session. While `spawn_progress` is non-null the desktop card shows that
426
+ * label instead of "Paused"; a client applies the same precedence.
427
+ *
428
+ * Under `view: 'sessions'` a paused task that offers no Resume stays out of
429
+ * the snapshot, because an agent feed has nothing to act on for it. Null or
430
+ * absent means a desktop that predates the field (pre-0.17.0); a client
431
+ * then reads a paused task from `resumable` alone, as before.
432
+ *
433
+ * Declared OPTIONAL (`?`) like `resumable`; `parseBoardTaskWire` populates
434
+ * the key from every real wire response.
435
+ */
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;
405
468
  }
406
469
  /** Phone-needed subset of the desktop's BacklogTask row. */
407
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);
@@ -5465,7 +5467,9 @@ function parseBoardTaskWire(value) {
5465
5467
  created_at: requireString(value, "created_at", "board task"),
5466
5468
  updated_at: requireString(value, "updated_at", "board task"),
5467
5469
  spawn_progress: nullableString(value, "spawn_progress"),
5468
- resumable: nullableBoolean(value, "resumable")
5470
+ resumable: nullableBoolean(value, "resumable"),
5471
+ paused: nullableBoolean(value, "paused"),
5472
+ pausable: nullableBoolean(value, "pausable")
5469
5473
  };
5470
5474
  }
5471
5475
  function parseBacklogItemWire(value) {
@@ -5777,6 +5781,20 @@ function parseStartSessionResponsePayload(payload) {
5777
5781
  }
5778
5782
  return { ok: payload.ok, outcome: payload.outcome };
5779
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
+ }
5780
5798
  function parseAnswerPermissionPromptRequestPayload(payload) {
5781
5799
  if (!isRecord(payload)) throw new Error("answer-permission-prompt payload must be an object");
5782
5800
  if (typeof payload.sessionId !== "string") throw new Error('answer-permission-prompt payload missing "sessionId"');
@@ -5874,6 +5892,8 @@ function parseCapabilityRequestPayload(verb, payload) {
5874
5892
  return parseRegisterPushRequestPayload(payload);
5875
5893
  case "start-session":
5876
5894
  return parseStartSessionRequestPayload(payload);
5895
+ case "pause-session":
5896
+ return parsePauseSessionRequestPayload(payload);
5877
5897
  default: {
5878
5898
  const exhaustiveCheck = verb;
5879
5899
  throw new Error(`Unknown capability verb: ${String(exhaustiveCheck)}`);
@@ -6277,6 +6297,7 @@ function isBridgeEvent(value) {
6277
6297
  parseCapabilityRequestPayload,
6278
6298
  parseDiffFileContentWire,
6279
6299
  parseDiffFileListWire,
6300
+ parsePauseSessionResponsePayload,
6280
6301
  parsePushEnvelopePlaintext,
6281
6302
  parseReadBoardResponsePayload,
6282
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);
@@ -5339,7 +5340,9 @@ function parseBoardTaskWire(value) {
5339
5340
  created_at: requireString(value, "created_at", "board task"),
5340
5341
  updated_at: requireString(value, "updated_at", "board task"),
5341
5342
  spawn_progress: nullableString(value, "spawn_progress"),
5342
- resumable: nullableBoolean(value, "resumable")
5343
+ resumable: nullableBoolean(value, "resumable"),
5344
+ paused: nullableBoolean(value, "paused"),
5345
+ pausable: nullableBoolean(value, "pausable")
5343
5346
  };
5344
5347
  }
5345
5348
  function parseBacklogItemWire(value) {
@@ -5651,6 +5654,20 @@ function parseStartSessionResponsePayload(payload) {
5651
5654
  }
5652
5655
  return { ok: payload.ok, outcome: payload.outcome };
5653
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
+ }
5654
5671
  function parseAnswerPermissionPromptRequestPayload(payload) {
5655
5672
  if (!isRecord(payload)) throw new Error("answer-permission-prompt payload must be an object");
5656
5673
  if (typeof payload.sessionId !== "string") throw new Error('answer-permission-prompt payload missing "sessionId"');
@@ -5748,6 +5765,8 @@ function parseCapabilityRequestPayload(verb, payload) {
5748
5765
  return parseRegisterPushRequestPayload(payload);
5749
5766
  case "start-session":
5750
5767
  return parseStartSessionRequestPayload(payload);
5768
+ case "pause-session":
5769
+ return parsePauseSessionRequestPayload(payload);
5751
5770
  default: {
5752
5771
  const exhaustiveCheck = verb;
5753
5772
  throw new Error(`Unknown capability verb: ${String(exhaustiveCheck)}`);
@@ -6150,6 +6169,7 @@ export {
6150
6169
  parseCapabilityRequestPayload,
6151
6170
  parseDiffFileContentWire,
6152
6171
  parseDiffFileListWire,
6172
+ parsePauseSessionResponsePayload,
6153
6173
  parsePushEnvelopePlaintext,
6154
6174
  parseReadBoardResponsePayload,
6155
6175
  parseReadDiffResponsePayload,
@@ -136,6 +136,8 @@ export declare function parseTranscriptWindowResponsePayload(payload: JsonValue)
136
136
  * `spawn_progress` label in flight (a first start, or a respawn that has
137
137
  * nulled `session_id`) and a `resumable` paused task (a desktop pause
138
138
  * nulls `session_id` too), so the feed can draw "Paused" with a Resume.
139
+ * A paused task that offers no Resume (in Done, say) stays out even though
140
+ * its row reads `paused: true` from 0.17.0 (see `BoardTaskWire.paused`).
139
141
  * - 'full': every non-archived task, for the one project whose board the user
140
142
  * actually has open.
141
143
  *
@@ -340,6 +342,29 @@ export interface StartSessionResponsePayload {
340
342
  }
341
343
  /** Phone-side guard: narrows a decoded start-session response before the phone acts on it. */
342
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;
343
368
  export interface AnswerPermissionPromptRequestPayload {
344
369
  sessionId: string;
345
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. */
@@ -426,6 +451,7 @@ export interface CapabilityRequestPayloadMap {
426
451
  'board-tool-write': BoardToolRequestPayload;
427
452
  'register-push': RegisterPushRequestPayload;
428
453
  'start-session': StartSessionRequestPayload;
454
+ 'pause-session': PauseSessionRequestPayload;
429
455
  }
430
456
  export interface CapabilityResponsePayloadMap {
431
457
  'read-stream': ReadStreamResponsePayload | TranscriptWindowResponsePayload;
@@ -439,6 +465,7 @@ export interface CapabilityResponsePayloadMap {
439
465
  'board-tool-write': BoardToolResponsePayload;
440
466
  'register-push': RegisterPushResponsePayload;
441
467
  'start-session': StartSessionResponsePayload;
468
+ 'pause-session': PauseSessionResponsePayload;
442
469
  }
443
470
  /**
444
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.16.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",