@code-yeongyu/senpi 2026.10.25-3 → 2026.10.26-1

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.
Files changed (218) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/bundle/chunks/{anthropic-messages-WBASBTIW.js → anthropic-messages-M235FJQV.js} +3 -3
  3. package/dist/bundle/chunks/{args-B444OQBK.js → args-FBNJOJSR.js} +1 -1
  4. package/dist/bundle/chunks/azure-openai-responses-YQRTQ7H6.js +2 -0
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +3 -3
  6. package/dist/bundle/chunks/{chunk-FFQOEC76.js → chunk-3A3XCLNH.js} +3 -3
  7. package/dist/bundle/chunks/{chunk-EHQXLHRB.js → chunk-3LPWILUZ.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-3BTN337S.js → chunk-3WP5RFQR.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-WLHNEUGD.js → chunk-5FQMNHXV.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-HVJI5T75.js → chunk-5KZH6EYQ.js} +2 -2
  11. package/dist/bundle/chunks/{chunk-4P7WARQZ.js → chunk-5LWUUXMH.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-ODTGC6YU.js → chunk-6U3TBFBA.js} +1 -1
  13. package/dist/bundle/chunks/chunk-6VWIMHMH.js +2 -0
  14. package/dist/bundle/chunks/{chunk-BWA4FVFW.js → chunk-7KOUQSBX.js} +2 -2
  15. package/dist/bundle/chunks/{chunk-7CNOCJCN.js → chunk-ADQAPDPV.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-ITGYN22E.js → chunk-CDQPA4FP.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-WDPQ6CZJ.js → chunk-DMFAVE5N.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-XF3LJC3O.js → chunk-DYWIFF2Q.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-YRF4I43T.js → chunk-EZP6AYW2.js} +37 -31
  20. package/dist/bundle/chunks/chunk-GQJYNFMO.js +2 -0
  21. package/dist/bundle/chunks/{chunk-O3Q2ET6W.js → chunk-GWGNVYPB.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-Z3UYNSVR.js → chunk-H4WJ4LY3.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-UQNRW3VU.js → chunk-HWTD4QF3.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-WD6WBYC5.js → chunk-I7QWTWVZ.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-SWJXRC2I.js → chunk-J2YAYTB2.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-TTU2RNVX.js → chunk-J5FCH7TI.js} +1 -1
  27. package/dist/bundle/chunks/chunk-JFQUWEN7.js +13 -0
  28. package/dist/bundle/chunks/{chunk-JA2R2ZSE.js → chunk-L2O6K3SJ.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-4AOMNOGY.js → chunk-LO5MYLGF.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-P3M2PF6K.js → chunk-MSHRIZGT.js} +8 -6
  31. package/dist/bundle/chunks/{chunk-T36LJ6MW.js → chunk-MY4ZD3EW.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-UFJ2K7Q7.js → chunk-OQ3WDMB3.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-7GZUFOM3.js → chunk-PH3JDRB2.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-E6WYH3ZL.js → chunk-QREKAXKX.js} +7 -5
  35. package/dist/bundle/chunks/{chunk-MJ5NNWX4.js → chunk-QYFOHFPS.js} +1 -1
  36. package/dist/bundle/chunks/chunk-SOXQ42VS.js +2 -0
  37. package/dist/bundle/chunks/{chunk-FHXR4ZAC.js → chunk-UDM7VO6O.js} +1 -1
  38. package/dist/bundle/chunks/{chunk-OLCDIITS.js → chunk-UW6M3EXY.js} +1 -1
  39. package/dist/bundle/chunks/chunk-VICDRG3J.js +410 -0
  40. package/dist/bundle/chunks/chunk-VL7O5PO4.js +5 -0
  41. package/dist/bundle/chunks/chunk-XMJKZK5H.js +2 -0
  42. package/dist/bundle/chunks/{chunk-C5UFJ7B3.js → chunk-XMNPK472.js} +1 -1
  43. package/dist/bundle/chunks/{chunk-GSSD3R5X.js → chunk-XQ45JMGN.js} +1 -1
  44. package/dist/bundle/chunks/{client-WTYEICLH.js → chunk-Y3TMJV3J.js} +3 -3
  45. package/dist/bundle/chunks/{chunk-KXB5WIS5.js → chunk-Z6SAQ5O3.js} +1 -1
  46. package/dist/bundle/chunks/{chunk-CNL6QSUB.js → chunk-ZAUVY6ZC.js} +3 -3
  47. package/dist/bundle/chunks/{chunk-E4LBKOZG.js → chunk-ZLIVZQDY.js} +4 -4
  48. package/dist/bundle/chunks/{chunk-6AOVUTUR.js → chunk-ZXHLH4MW.js} +2 -2
  49. package/dist/bundle/chunks/cli-main-55EQA5PS.js +3 -0
  50. package/dist/bundle/chunks/client-LOS6GSEW.js +2 -0
  51. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-K5H6BJP3.js → cloudflare-workers-ai-system-one-ZJQGIIRC.js} +1 -1
  52. package/dist/bundle/chunks/cursor-agent.js +3 -3
  53. package/dist/bundle/chunks/devin-agent.js +3 -3
  54. package/dist/bundle/chunks/dispatch-C2IBP5VV.js +9 -0
  55. package/dist/bundle/chunks/google-generative-ai-5G4RIQA6.js +2 -0
  56. package/dist/bundle/chunks/google-vertex-ALD4P27O.js +2 -0
  57. package/dist/bundle/chunks/{help-fast-path-3Z26DQSV.js → help-fast-path-PEIKGCKZ.js} +1 -1
  58. package/dist/bundle/chunks/host-command-657VCQ34.js +2 -0
  59. package/dist/bundle/chunks/{host-lifecycle-ONQIMWMX.js → host-lifecycle-HX3EGRRW.js} +1 -1
  60. package/dist/bundle/chunks/host-lifecycle.js +2 -2
  61. package/dist/bundle/chunks/{host-session-command-J65ENSV5.js → host-session-command-OV5XDOJK.js} +1 -1
  62. package/dist/bundle/chunks/{host-socket-request-3CWE4265.js → host-socket-request-K4LCUOUD.js} +1 -1
  63. package/dist/bundle/chunks/interactive-mode-MLXSYK2H.js +2 -0
  64. package/dist/bundle/chunks/{mcp-presenter-bootstrap-XUSCQVRC.js → mcp-presenter-bootstrap-FOMKF4TU.js} +1 -1
  65. package/dist/bundle/chunks/{mistral-conversations-DH44NOYV.js → mistral-conversations-RAE4K2XY.js} +1 -1
  66. package/dist/bundle/chunks/{multi-session-host-YI2WZOET.js → multi-session-host-774MACJU.js} +5 -6
  67. package/dist/bundle/chunks/{openai-codex-responses-SERAFKAS.js → openai-codex-responses-3CW4R7TG.js} +1 -1
  68. package/dist/bundle/chunks/{openai-completions-BZB5O45A.js → openai-completions-5UIJXQUF.js} +1 -1
  69. package/dist/bundle/chunks/openai-responses-2BO44MRW.js +3 -0
  70. package/dist/bundle/chunks/{package-manager-cli-GHZPJMRF.js → package-manager-cli-MZHVCX2S.js} +1 -1
  71. package/dist/bundle/chunks/{pi-messages-VXATX45B.js → pi-messages-RF6GER4R.js} +2 -2
  72. package/dist/bundle/chunks/{role-JVSBNKGE.js → role-ETSFSDTS.js} +1 -1
  73. package/dist/bundle/chunks/rotation-stream-ZMQB4GPO.js +2 -0
  74. package/dist/bundle/chunks/rpc-mode-UJXOIZCW.js +2 -0
  75. package/dist/bundle/chunks/{rpc-monitor-source-FFEQS744.js → rpc-monitor-source-DRZXJHTL.js} +1 -1
  76. package/dist/bundle/chunks/{schedule-command-YWG6D6ZF.js → schedule-command-KA7LFFNK.js} +1 -1
  77. package/dist/bundle/chunks/session-control-endpoint-MFWXUR7Z.js +3 -0
  78. package/dist/bundle/chunks/session-picker-BP33NRSL.js +2 -0
  79. package/dist/bundle/chunks/session-worker.js +212 -203
  80. package/dist/bundle/chunks/{typesafe-system-one-P75WJ767.js → typesafe-system-one-GNNZ5O5U.js} +1 -1
  81. package/dist/bundle/cli.js +2 -2
  82. package/dist/bundle/host-supervisor.js +2 -2
  83. package/dist/bundle/index.js +1 -1
  84. package/dist/bundle/rpc-entry.js +1 -1
  85. package/dist/bundle/runtime-manifest.json +1 -1
  86. package/dist/cli/host-command-args.d.ts +1 -0
  87. package/dist/cli/host-command-args.js +5 -0
  88. package/dist/cli/host-command.js +7 -1
  89. package/dist/cli.js +21 -3
  90. package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +2 -1
  91. package/dist/core/extensions/builtin/mcp/catalog-cache.js +19 -6
  92. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -2
  93. package/dist/core/extensions/builtin/mcp/catalog.js +20 -3
  94. package/dist/core/extensions/builtin/mcp/elicitation.js +2 -4
  95. package/dist/core/extensions/builtin/mcp/expose/call.d.ts +2 -1
  96. package/dist/core/extensions/builtin/mcp/expose/call.js +2 -1
  97. package/dist/core/extensions/builtin/mcp/expose/register.js +1 -1
  98. package/dist/core/extensions/builtin/mcp/expose/schema-compat.d.ts +2 -2
  99. package/dist/core/extensions/builtin/mcp/expose/schema-compat.js +11 -1
  100. package/dist/core/extensions/builtin/mcp/invocation.js +16 -2
  101. package/dist/core/extensions/builtin/mcp/modern/request.d.ts +1 -1
  102. package/dist/core/extensions/builtin/mcp/modern/request.js +1 -1
  103. package/dist/core/extensions/builtin/mcp/modern/sdk-bridge.js +18 -3
  104. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +23 -0
  105. package/dist/core/extensions/builtin/mcp/modern/subscriptions.d.ts +14 -0
  106. package/dist/core/extensions/builtin/mcp/modern/subscriptions.js +226 -0
  107. package/dist/core/extensions/builtin/mcp/modern/tool-schema.d.ts +13 -0
  108. package/dist/core/extensions/builtin/mcp/modern/tool-schema.js +75 -0
  109. package/dist/core/extensions/builtin/mcp/modern/tools.d.ts +107 -0
  110. package/dist/core/extensions/builtin/mcp/modern/tools.js +53 -0
  111. package/dist/core/extensions/builtin/mcp/resources.js +4 -0
  112. package/dist/core/extensions/builtin/mcp/tool-client.d.ts +13 -0
  113. package/dist/core/extensions/builtin/mcp/tool-client.js +17 -0
  114. package/dist/core/extensions/builtin/mcp/tool-contracts.d.ts +19 -0
  115. package/dist/core/extensions/builtin/mcp/tool-contracts.js +2 -0
  116. package/dist/core/extensions/builtin/mcp/wrap.js +2 -2
  117. package/dist/core/extensions/builtin/permission-system/bash-command-segments.d.ts +37 -0
  118. package/dist/core/extensions/builtin/permission-system/bash-command-segments.js +899 -0
  119. package/dist/core/extensions/builtin/permission-system/parsers.d.ts +2 -0
  120. package/dist/core/extensions/builtin/permission-system/parsers.js +44 -10
  121. package/dist/core/session-control-actions.js +0 -2
  122. package/dist/modes/interactive/session-control-attach.d.ts +20 -0
  123. package/dist/modes/interactive/session-control-attach.js +13 -0
  124. package/dist/modes/interactive/session-control-commands.js +37 -10
  125. package/dist/modes/interactive/session-control-registry.d.ts +2 -1
  126. package/dist/modes/interactive/session-control-registry.js +3 -2
  127. package/dist/modes/interactive/session-control-rpc-commands.d.ts +4 -2
  128. package/dist/modes/interactive/session-control-rpc-commands.js +133 -4
  129. package/dist/modes/interactive/session-control-server.js +9 -3
  130. package/dist/modes/rpc/compact-content-store.d.ts +96 -0
  131. package/dist/modes/rpc/compact-content-store.js +269 -0
  132. package/dist/modes/rpc/compact-session-state.d.ts +111 -0
  133. package/dist/modes/rpc/compact-session-state.js +206 -0
  134. package/dist/modes/rpc/compact-spool-dir.d.ts +5 -0
  135. package/dist/modes/rpc/compact-spool-dir.js +61 -0
  136. package/dist/modes/rpc/compact-storage-io.d.ts +27 -0
  137. package/dist/modes/rpc/compact-storage-io.js +19 -0
  138. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  139. package/dist/modes/rpc/connection-handler.js +45 -47
  140. package/dist/modes/rpc/connection-question-bridge.d.ts +2 -1
  141. package/dist/modes/rpc/connection-question-bridge.js +6 -2
  142. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  143. package/dist/modes/rpc/custom-capability.js +6 -0
  144. package/dist/modes/rpc/extension-ui-response.d.ts +7 -2
  145. package/dist/modes/rpc/extension-ui-response.js +27 -7
  146. package/dist/modes/rpc/get-entries-command.d.ts +13 -0
  147. package/dist/modes/rpc/get-entries-command.js +61 -0
  148. package/dist/modes/rpc/get-entries-page.d.ts +22 -0
  149. package/dist/modes/rpc/get-entries-page.js +41 -2
  150. package/dist/modes/rpc/get-snapshot.d.ts +18 -0
  151. package/dist/modes/rpc/get-snapshot.js +43 -0
  152. package/dist/modes/rpc/host-capabilities.js +7 -1
  153. package/dist/modes/rpc/host-ensure-start.js +1 -0
  154. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  155. package/dist/modes/rpc/host-ensure.js +1 -0
  156. package/dist/modes/rpc/host-handoff.d.ts +2 -0
  157. package/dist/modes/rpc/host-handover-request.d.ts +1 -1
  158. package/dist/modes/rpc/host-handover-request.js +4 -3
  159. package/dist/modes/rpc/host-handover-wire.js +6 -0
  160. package/dist/modes/rpc/host-idle-handover.d.ts +2 -0
  161. package/dist/modes/rpc/host-idle-handover.js +3 -1
  162. package/dist/modes/rpc/host-observe-request.d.ts +2 -1
  163. package/dist/modes/rpc/host-observe-request.js +5 -1
  164. package/dist/modes/rpc/host-runner.d.ts +1 -0
  165. package/dist/modes/rpc/host-runner.js +9 -7
  166. package/dist/modes/rpc/host-successor.js +4 -0
  167. package/dist/modes/rpc/json-normal-value.d.ts +6 -0
  168. package/dist/modes/rpc/json-normal-value.js +100 -0
  169. package/dist/modes/rpc/mcp-presenter-bootstrap.js +5 -0
  170. package/dist/modes/rpc/media-placeholders.d.ts +9 -3
  171. package/dist/modes/rpc/media-placeholders.js +12 -4
  172. package/dist/modes/rpc/rpc-host-command-types.d.ts +2 -0
  173. package/dist/modes/rpc/rpc-question-types.d.ts +1 -0
  174. package/dist/modes/rpc/rpc-read-response-types.d.ts +9 -0
  175. package/dist/modes/rpc/rpc-session-command-types.d.ts +14 -1
  176. package/dist/modes/rpc/rpc-session-state-types.d.ts +3 -0
  177. package/dist/modes/rpc/rpc-session-state.js +2 -0
  178. package/dist/modes/rpc/rpc-types.d.ts +1 -0
  179. package/dist/modes/rpc/session-attachment-gate.js +1 -0
  180. package/dist/modes/rpc/session-binding.js +21 -18
  181. package/dist/modes/rpc/session-command-activity.js +1 -0
  182. package/dist/modes/rpc/session-event-fanout.d.ts +1 -1
  183. package/dist/modes/rpc/session-event-fanout.js +17 -9
  184. package/dist/modes/rpc/session-event-writer.js +22 -5
  185. package/dist/modes/rpc/session-extension-ui-requests.d.ts +2 -1
  186. package/dist/modes/rpc/session-extension-ui-requests.js +15 -10
  187. package/dist/modes/rpc/session-held.js +1 -0
  188. package/dist/modes/rpc/session-status-stream.d.ts +2 -1
  189. package/dist/modes/rpc/session-status-stream.js +8 -2
  190. package/dist/modes/rpc/session-status-subscriptions.d.ts +2 -1
  191. package/dist/modes/rpc/session-status-subscriptions.js +4 -0
  192. package/dist/modes/rpc/session-worker.js +5 -2
  193. package/dist/modes/rpc/snapshot-types.d.ts +29 -0
  194. package/dist/modes/rpc/snapshot-types.js +2 -0
  195. package/dist/modes/rpc/ui-answer-ledger.d.ts +13 -0
  196. package/dist/modes/rpc/ui-answer-ledger.js +34 -0
  197. package/dist/senpi +21 -3
  198. package/docs/mcp.md +20 -0
  199. package/docs/rpc.md +87 -11
  200. package/package.json +7 -6
  201. package/dist/bundle/chunks/azure-openai-responses-7I45DI72.js +0 -2
  202. package/dist/bundle/chunks/chunk-3LNSLJ7N.js +0 -2
  203. package/dist/bundle/chunks/chunk-6O4SEW2W.js +0 -5
  204. package/dist/bundle/chunks/chunk-7W7OJDIY.js +0 -410
  205. package/dist/bundle/chunks/chunk-DMI55M2N.js +0 -2
  206. package/dist/bundle/chunks/chunk-H3HNMY2W.js +0 -13
  207. package/dist/bundle/chunks/chunk-MSQCTH6V.js +0 -2
  208. package/dist/bundle/chunks/cli-main-HEKFFB5S.js +0 -3
  209. package/dist/bundle/chunks/dispatch-NYAULNVW.js +0 -9
  210. package/dist/bundle/chunks/google-generative-ai-UM5Y7T6B.js +0 -2
  211. package/dist/bundle/chunks/google-vertex-OKU2VQHL.js +0 -2
  212. package/dist/bundle/chunks/host-command-Z3WPHJOW.js +0 -2
  213. package/dist/bundle/chunks/interactive-mode-DEPQC6TX.js +0 -2
  214. package/dist/bundle/chunks/openai-responses-TNHQ6SJS.js +0 -3
  215. package/dist/bundle/chunks/rotation-stream-ZPVCFOVD.js +0 -2
  216. package/dist/bundle/chunks/rpc-mode-ORHBGBBV.js +0 -2
  217. package/dist/bundle/chunks/session-control-endpoint-36HAQSQO.js +0 -3
  218. package/dist/bundle/chunks/session-picker-25H5KNYP.js +0 -2
@@ -1,7 +1,7 @@
1
1
  import { isHostOwnedToolRuntimeCapability, toolRuntimeCapabilities, } from "../../cli/tool-runtime.js";
2
2
  import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY, AUTO_PERMISSION_PRESET_CAPABILITY, } from "../../core/extensions/builtin/permission-system/config.js";
3
3
  import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.js";
4
- import { ATTENDED_CAPABILITY, AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DETACH_CONNECTION_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, EVAL_CELLS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, MOVED_PATH_GUARD_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_HELD_CAPABILITY, SESSION_KIND_CAPABILITY, SESSION_RELOCATION_CAPABILITY, SESSION_STATUS_CAPABILITY, TOOL_BARRIER_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
4
+ import { ATTENDED_CAPABILITY, AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DETACH_CONNECTION_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, ENTRIES_TAIL_CAPABILITY, EVAL_CELLS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, MOVED_PATH_GUARD_CAPABILITY, OBSERVE_READS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_HELD_CAPABILITY, SESSION_KIND_CAPABILITY, SESSION_RELOCATION_CAPABILITY, SESSION_STATUS_CAPABILITY, TOOL_BARRIER_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
5
5
  import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
6
6
  import { STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY } from "./rpc-types.js";
7
7
  import { MOVE_SESSION_CAPABILITY, MOVE_SESSION_REVOKE_CAPABILITY } from "./session-move-wire.js";
@@ -20,6 +20,9 @@ export function multiSessionHostCapabilities(options) {
20
20
  return [
21
21
  ...new Set([
22
22
  "multi_session",
23
+ "snapshot_cursor",
24
+ "ui_answer_ledger",
25
+ "dialog_observer",
23
26
  ...(options.mcpPresenters ? ["mcp_form_presenter_v1"] : []),
24
27
  "session_lifecycle",
25
28
  "expected_host_instance",
@@ -44,6 +47,9 @@ export function multiSessionHostCapabilities(options) {
44
47
  SESSION_HELD_CAPABILITY,
45
48
  RESUME_SESSION_CAPABILITY,
46
49
  ATTENDED_CAPABILITY,
50
+ // `get_state`/`get_entries` marked `observe` never attach, and history reads page backward too (senpi#4267).
51
+ OBSERVE_READS_CAPABILITY,
52
+ ENTRIES_TAIL_CAPABILITY,
47
53
  // Host capabilities, not client opt-ins: only a multi-session host owns the
48
54
  // attachment refcount `open_session.retain_on_disconnect` detaches from, the
49
55
  // per-session launch profile `context`/`auto_title` travel on, and the session
@@ -63,6 +63,7 @@ export async function startHost(paths, socket, options, generation = 0) {
63
63
  socket,
64
64
  ...(options.owner ? ["--owner-fd", String(OWNER_WATCH_FD)] : []),
65
65
  ...(options.presenterBootstrap ? ["--mcp-presenter-fd", "4"] : []),
66
+ ...(options.mcpPresenterEnforcement ? ["--mcp-presenter-enforcement"] : []),
66
67
  ...(options.hostArgs ?? []),
67
68
  ];
68
69
  const launch = testOptions?.spawn ?? testOptions?.launch?.(supervisorArgs) ?? defaultHostLaunch(supervisorArgs);
@@ -12,6 +12,8 @@ import type { Duplex } from "node:stream";
12
12
  export type HostUpgradePolicy = "never" | "if-engine-differs";
13
13
  export interface EnsureHostOptions {
14
14
  readonly presenterBootstrap?: Duplex;
15
+ /** Deployment opt-in, never a project setting or proof of enrollment. Defaults off for a new host. */
16
+ readonly mcpPresenterEnforcement?: boolean;
15
17
  readonly socket: string;
16
18
  readonly agentDir?: string;
17
19
  readonly keyRoot?: string;
@@ -206,6 +206,7 @@ async function upgradeGeneration(paths, socket, options, attachedPid) {
206
206
  agentDir: options.agentDir ?? getAgentDir(),
207
207
  hostArgs: options.hostArgs ?? [],
208
208
  presenterBootstrap: options.presenterBootstrap,
209
+ mcpPresenterEnforcement: options.mcpPresenterEnforcement,
209
210
  ...(options.env ? { env: options.env } : {}),
210
211
  ...(options.policy ? { policy: options.policy } : {}),
211
212
  _test: {
@@ -7,6 +7,8 @@ import type { HostLifecyclePolicyInput } from "./host-lifecycle.ts";
7
7
  export declare const HANDOFF_LOCK_HOLD_MS: number;
8
8
  export interface HandoffHostOptions {
9
9
  readonly presenterBootstrap?: Duplex;
10
+ /** Deployment opt-in. An enforcing predecessor never downgrades during replacement. */
11
+ readonly mcpPresenterEnforcement?: boolean;
10
12
  readonly socket: string;
11
13
  readonly agentDir?: string;
12
14
  /** Extra CLI arguments the successor's host child is launched with (provider pinning, extensions). */
@@ -9,5 +9,5 @@ export interface IdleHandoverTerms {
9
9
  export declare function idleHandoverOutcome(target: {
10
10
  readonly socket: string;
11
11
  readonly agentDir: string;
12
- }, spec: ResolvedHostLaunchSpec, terms: IdleHandoverTerms): Promise<HostOutcome>;
12
+ }, spec: ResolvedHostLaunchSpec, terms: IdleHandoverTerms, mcpPresenterEnforcement?: boolean): Promise<HostOutcome>;
13
13
  //# sourceMappingURL=host-handover-request.d.ts.map
@@ -28,7 +28,7 @@ import { RUNTIME_IDENTITY_HANDOVER_CAPABILITY } from "./runtime-build-id.js";
28
28
  const PROBE_TIMEOUT_MS = 10_000;
29
29
  /** A host idle at once hands over before answering: its ensure-lock wait plus the handoff itself. */
30
30
  const BEGIN_HANDOVER_TIMEOUT_MS = 2 * HANDOFF_LOCK_HOLD_MS + 20_000;
31
- export async function idleHandoverOutcome(target, spec, terms) {
31
+ export async function idleHandoverOutcome(target, spec, terms, mcpPresenterEnforcement) {
32
32
  const { socket } = target;
33
33
  if (process.platform === "win32") {
34
34
  return refusal("refuse", undefined, { reason: "upgrade_unsupported", socket, operationId: terms.operationId });
@@ -57,7 +57,7 @@ export async function idleHandoverOutcome(target, spec, terms) {
57
57
  }
58
58
  if (!before.capabilities.includes(RUNTIME_IDENTITY_HANDOVER_CAPABILITY))
59
59
  return refuse("handover_unsupported");
60
- const reply = await requestOnSocket(socket, beginHandoverCommand(target, spec, terms), BEGIN_HANDOVER_TIMEOUT_MS);
60
+ const reply = await requestOnSocket(socket, beginHandoverCommand(target, spec, terms, mcpPresenterEnforcement), BEGIN_HANDOVER_TIMEOUT_MS);
61
61
  if (!isRecord(reply))
62
62
  return reconcileLostReply(target, terms, before);
63
63
  if (typeof reply.refused === "string") {
@@ -71,7 +71,7 @@ export async function idleHandoverOutcome(target, spec, terms) {
71
71
  }
72
72
  return answered(target, "handover_pending", before, terms, reply);
73
73
  }
74
- function beginHandoverCommand(target, spec, terms) {
74
+ function beginHandoverCommand(target, spec, terms, mcpPresenterEnforcement) {
75
75
  const env = {};
76
76
  for (const [name, value] of Object.entries(daemonEnvironment(process.env, daemonEnvOverrides(process.env, spec.env)))) {
77
77
  if (value !== undefined)
@@ -88,6 +88,7 @@ function beginHandoverCommand(target, spec, terms) {
88
88
  launch: { command: launch.command, args: launch.args },
89
89
  host_args: spec.hostArgs,
90
90
  env,
91
+ ...(mcpPresenterEnforcement === true && { mcp_presenter_enforcement: true }),
91
92
  ...(spec.policy !== undefined && { policy: spec.policy }),
92
93
  };
93
94
  }
@@ -33,6 +33,10 @@ export function parseBeginHandover(record) {
33
33
  }
34
34
  const policy = isRecord(record.policy) ? record.policy : undefined;
35
35
  const { agent_dir } = record;
36
+ const mcpPresenterEnforcement = record.mcp_presenter_enforcement;
37
+ if (mcpPresenterEnforcement !== undefined && typeof mcpPresenterEnforcement !== "boolean") {
38
+ return "mcp_presenter_enforcement must be a boolean";
39
+ }
36
40
  if (agent_dir !== undefined && (typeof agent_dir !== "string" || agent_dir === "")) {
37
41
  return "agent_dir must be a non-empty string";
38
42
  }
@@ -44,6 +48,7 @@ export function parseBeginHandover(record) {
44
48
  launch: { command: launch.command, args: launch.args },
45
49
  hostArgs: host_args,
46
50
  env: env,
51
+ ...(mcpPresenterEnforcement !== undefined && { mcpPresenterEnforcement }),
47
52
  ...(policy !== undefined && { policy }),
48
53
  ...(agent_dir !== undefined && { agentDir: agent_dir }),
49
54
  };
@@ -121,6 +126,7 @@ export async function performIdleHandover(context) {
121
126
  Object.assign(env, request.env);
122
127
  const result = await handoffHost({
123
128
  presenterBootstrap: context.presenterBootstrap,
129
+ mcpPresenterEnforcement: request.mcpPresenterEnforcement,
124
130
  socket,
125
131
  agentDir,
126
132
  hostArgs: request.hostArgs,
@@ -36,6 +36,8 @@ export interface HandoverLaunch {
36
36
  readonly args: readonly string[];
37
37
  }
38
38
  export interface IdleHandoverRequest {
39
+ /** Successor deployment policy only; never enrollment authority. */
40
+ readonly mcpPresenterEnforcement?: boolean;
39
41
  readonly operationId: string;
40
42
  readonly ifInstanceId: string;
41
43
  readonly ifGeneration: number;
@@ -40,7 +40,8 @@ export class HostIdleHandover {
40
40
  : { kind: "refused", reason: "operation_conflict", detail: "this operation id names other terms" };
41
41
  }
42
42
  if (current !== undefined && current.view.state !== "handover_blocked") {
43
- return current.request.targetRuntimeBuildId === request.targetRuntimeBuildId
43
+ return current.request.targetRuntimeBuildId === request.targetRuntimeBuildId &&
44
+ Boolean(current.request.mcpPresenterEnforcement) === Boolean(request.mcpPresenterEnforcement)
44
45
  ? answer(current)
45
46
  : { kind: "refused", reason: "handover_in_progress", detail: current.view.operationId };
46
47
  }
@@ -112,6 +113,7 @@ export class HostIdleHandover {
112
113
  }
113
114
  function sameTerms(a, b) {
114
115
  return (a.targetRuntimeBuildId === b.targetRuntimeBuildId &&
116
+ Boolean(a.mcpPresenterEnforcement) === Boolean(b.mcpPresenterEnforcement) &&
115
117
  a.ifInstanceId === b.ifInstanceId &&
116
118
  a.ifGeneration === b.ifGeneration);
117
119
  }
@@ -7,7 +7,8 @@
7
7
  * itself with `observe: true`; the supervisor classifies a connection by the requests it sends, and
8
8
  * only a connection that sends something other than an observing read becomes an attachment.
9
9
  *
10
- * Only the side-effect-free reads can observe (`get_effect_status` answers a receipt and changes nothing): `observe: true` on any other command is ignored
10
+ * Only the side-effect-free reads can observe (`get_effect_status` answers a receipt and changes nothing; `get_state` and
11
+ * `get_entries` read a session's state and history, senpi#4267): `observe: true` on any other command is ignored
11
12
  * and the connection attaches, so a client cannot hold a session while its host idles out under it.
12
13
  * `warm` never attaches, marked or not: it opens nothing, so a host that only received it idles out
13
14
  * on its normal deadline (senpi#2314).
@@ -7,7 +7,8 @@
7
7
  * itself with `observe: true`; the supervisor classifies a connection by the requests it sends, and
8
8
  * only a connection that sends something other than an observing read becomes an attachment.
9
9
  *
10
- * Only the side-effect-free reads can observe (`get_effect_status` answers a receipt and changes nothing): `observe: true` on any other command is ignored
10
+ * Only the side-effect-free reads can observe (`get_effect_status` answers a receipt and changes nothing; `get_state` and
11
+ * `get_entries` read a session's state and history, senpi#4267): `observe: true` on any other command is ignored
11
12
  * and the connection attaches, so a client cannot hold a session while its host idles out under it.
12
13
  * `warm` never attaches, marked or not: it opens nothing, so a host that only received it idles out
13
14
  * on its normal deadline (senpi#2314).
@@ -27,12 +28,15 @@ export const OBSERVE_REQUEST_FIELD = "observe";
27
28
  export const ORIGIN_HOST_INSTANCE_FIELD = "origin_host_instance";
28
29
  const OBSERVING_COMMANDS = new Set([
29
30
  "get_protocol_info",
31
+ "get_snapshot",
30
32
  "list_sessions",
31
33
  "subscribe_sessions",
32
34
  "unsubscribe_sessions",
33
35
  "get_effect_status",
34
36
  "get_pending_inputs",
35
37
  "get_subscription_limits",
38
+ "get_state",
39
+ "get_entries",
36
40
  ]);
37
41
  export function isObservingRequest(line) {
38
42
  let request;
@@ -52,5 +52,6 @@ export type HostRequest = {
52
52
  export declare function runHostRequest(request: HostRequest, options?: {
53
53
  readonly keyRoot?: string;
54
54
  readonly presenterBootstrap?: Duplex;
55
+ readonly mcpPresenterEnforcement?: boolean;
55
56
  }): Promise<HostOutcome>;
56
57
  //# sourceMappingURL=host-runner.d.ts.map
@@ -37,9 +37,9 @@ import { hostRequestKeyRoot } from "./host-request-key-root.js";
37
37
  import { withRpcKeyRoot } from "./rpc-auth-client.js";
38
38
  const PROBE_TIMEOUT_MS = 10_000;
39
39
  export async function runHostRequest(request, options = {}) {
40
- return withRpcKeyRoot(hostRequestKeyRoot(request, options.keyRoot), () => runHostRequestInKeyRoot(request, options.presenterBootstrap));
40
+ return withRpcKeyRoot(hostRequestKeyRoot(request, options.keyRoot), () => runHostRequestInKeyRoot(request, options.presenterBootstrap, options.mcpPresenterEnforcement));
41
41
  }
42
- async function runHostRequestInKeyRoot(request, presenterBootstrap) {
42
+ async function runHostRequestInKeyRoot(request, presenterBootstrap, mcpPresenterEnforcement) {
43
43
  if (request.action === "ensure" || request.action === "handoff" || request.action === "stop") {
44
44
  const { socket, agentDir } = request.target;
45
45
  if ((await endpointKindOfSocket(socket, agentDir)) === "tui") {
@@ -48,7 +48,7 @@ async function runHostRequestInKeyRoot(request, presenterBootstrap) {
48
48
  }
49
49
  switch (request.action) {
50
50
  case "ensure":
51
- return ensureOutcome(request.target, request.spec, request.policy, presenterBootstrap);
51
+ return ensureOutcome(request.target, request.spec, request.policy, presenterBootstrap, mcpPresenterEnforcement);
52
52
  case "status":
53
53
  return request.all === true
54
54
  ? statusAllOutcome(request.target, request.includeWorkers)
@@ -57,8 +57,8 @@ async function runHostRequestInKeyRoot(request, presenterBootstrap) {
57
57
  return stopOutcome(request);
58
58
  case "handoff":
59
59
  return request.handover === undefined
60
- ? handoffOutcome(request.target, request.spec, presenterBootstrap)
61
- : idleHandoverOutcome(request.target, request.spec, request.handover);
60
+ ? handoffOutcome(request.target, request.spec, presenterBootstrap, mcpPresenterEnforcement)
61
+ : idleHandoverOutcome(request.target, request.spec, request.handover, mcpPresenterEnforcement);
62
62
  case "shard_path":
63
63
  return shardPathOutcome(request.kind, request.owner, request.root);
64
64
  case "gc":
@@ -70,7 +70,7 @@ async function runHostRequestInKeyRoot(request, presenterBootstrap) {
70
70
  return assertNever(request);
71
71
  }
72
72
  }
73
- async function ensureOutcome(target, spec, policy, presenterBootstrap) {
73
+ async function ensureOutcome(target, spec, policy, presenterBootstrap, mcpPresenterEnforcement) {
74
74
  const before = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
75
75
  if (policy === "fallback") {
76
76
  // `fallback` is the caller saying it can live without a host. The decision is made HERE
@@ -85,6 +85,7 @@ async function ensureOutcome(target, spec, policy, presenterBootstrap) {
85
85
  ensured = await ensureHost({
86
86
  socket: target.socket,
87
87
  presenterBootstrap,
88
+ mcpPresenterEnforcement,
88
89
  agentDir: target.agentDir,
89
90
  hostArgs: spec.hostArgs,
90
91
  env: daemonEnvOverrides(process.env, spec.env),
@@ -182,11 +183,12 @@ async function stopOutcome(request) {
182
183
  },
183
184
  };
184
185
  }
185
- async function handoffOutcome(target, spec, presenterBootstrap) {
186
+ async function handoffOutcome(target, spec, presenterBootstrap, mcpPresenterEnforcement) {
186
187
  const before = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
187
188
  const result = await handoffHost({
188
189
  socket: target.socket,
189
190
  presenterBootstrap,
191
+ mcpPresenterEnforcement,
190
192
  agentDir: target.agentDir,
191
193
  hostArgs: spec.hostArgs,
192
194
  env: daemonEnvOverrides(process.env, spec.env),
@@ -83,6 +83,10 @@ export async function startSuccessor(context) {
83
83
  `${replaced.dev}:${replaced.ino}`,
84
84
  ...(inheritOwnerPipe ? ["--owner-fd", String(OWNER_WATCH_FD)] : []),
85
85
  ...(options.presenterBootstrap ? ["--mcp-presenter-fd", "4"] : []),
86
+ // Preserve the running host's policy even when an older caller omits the deployment option.
87
+ ...(options.mcpPresenterEnforcement || host.capabilities.includes("mcp_form_presenter_v1")
88
+ ? ["--mcp-presenter-enforcement"]
89
+ : []),
86
90
  ...(options.hostArgs ?? []),
87
91
  ];
88
92
  const launch = (options._test?.launch ?? options.launch)?.(argv) ?? defaultHostLaunch(argv);
@@ -0,0 +1,6 @@
1
+ /** JSON.stringify's value semantics, without serializing or parsing a record. */
2
+ export declare function normalizeJsonValue(input: unknown): {
3
+ value: unknown;
4
+ changed: boolean;
5
+ };
6
+ //# sourceMappingURL=json-normal-value.d.ts.map
@@ -0,0 +1,100 @@
1
+ import { isBigIntObject, isBooleanObject, isNumberObject, isStringObject } from "node:util/types";
2
+ /** JSON.stringify's value semantics, without serializing or parsing a record. */
3
+ export function normalizeJsonValue(input) {
4
+ const ancestors = new Set();
5
+ let changed = false;
6
+ const visit = (input, key) => {
7
+ let value = input;
8
+ if ((typeof value === "object" && value !== null) || typeof value === "function" || typeof value === "bigint") {
9
+ const toJSON = value.toJSON;
10
+ if (typeof toJSON === "function") {
11
+ value = toJSON.call(value, key);
12
+ changed = true;
13
+ }
14
+ }
15
+ if (typeof value === "object" && value !== null) {
16
+ if (isNumberObject(value)) {
17
+ value = +value;
18
+ changed = true;
19
+ }
20
+ else if (isStringObject(value)) {
21
+ value = String(value);
22
+ changed = true;
23
+ }
24
+ else if (isBooleanObject(value)) {
25
+ value = Boolean.prototype.valueOf.call(value);
26
+ changed = true;
27
+ }
28
+ else if (isBigIntObject(value)) {
29
+ throw new TypeError("Do not know how to serialize a BigInt");
30
+ }
31
+ }
32
+ if (typeof value === "bigint")
33
+ throw new TypeError("Do not know how to serialize a BigInt");
34
+ if (typeof value === "number") {
35
+ const normalized = Number.isFinite(value) ? (Object.is(value, -0) ? 0 : value) : null;
36
+ if (!Object.is(normalized, value))
37
+ changed = true;
38
+ return normalized;
39
+ }
40
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
41
+ changed = true;
42
+ return undefined;
43
+ }
44
+ if (typeof value !== "object" || value === null)
45
+ return value;
46
+ if (ancestors.has(value))
47
+ throw new TypeError("Converting circular structure to JSON");
48
+ ancestors.add(value);
49
+ try {
50
+ // A non-callable toJSON getter must not be read again by later serialization.
51
+ if ("toJSON" in value)
52
+ changed = true;
53
+ if (Array.isArray(value)) {
54
+ const array = [];
55
+ const length = value.length;
56
+ for (let index = 0; index < length; index++) {
57
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
58
+ if (!descriptor || descriptor.get)
59
+ changed = true;
60
+ array.push(visit(value[index], String(index)) ?? null);
61
+ }
62
+ return array;
63
+ }
64
+ const result = {};
65
+ const names = Object.keys(value);
66
+ if (Object.getPrototypeOf(value) !== Object.prototype || Reflect.ownKeys(value).length !== names.length)
67
+ changed = true;
68
+ for (const name of names) {
69
+ if (Object.getOwnPropertyDescriptor(value, name)?.get)
70
+ changed = true;
71
+ const child = visit(value[name], name);
72
+ if (child !== undefined)
73
+ Object.defineProperty(result, name, {
74
+ value: child,
75
+ enumerable: true,
76
+ configurable: true,
77
+ writable: true,
78
+ });
79
+ }
80
+ return result;
81
+ }
82
+ finally {
83
+ ancestors.delete(value);
84
+ }
85
+ };
86
+ let value;
87
+ try {
88
+ value = visit(input, "");
89
+ }
90
+ catch (error) {
91
+ // The recursive walk overflows the stack before native JSON does; let native JSON decide.
92
+ if (!(error instanceof RangeError))
93
+ throw error;
94
+ return { value: JSON.parse(JSON.stringify(input)), changed: true };
95
+ }
96
+ if (value === undefined)
97
+ throw new SyntaxError("JSON input has no representation");
98
+ return { value, changed };
99
+ }
100
+ //# sourceMappingURL=json-normal-value.js.map
@@ -11,6 +11,11 @@ export function presenterBootstrapStream(fd) {
11
11
  const stat = fstatSync(fd);
12
12
  if (!stat.isSocket() && !stat.isFIFO())
13
13
  return undefined;
14
+ // Node's fs read blocks a worker until peer EOF, even after destroy(), and process.exit()
15
+ // joins that worker. A socket handle can cancel the pending read after enrollment.
16
+ // Bun does not support inherited net.Socket fds; it retains the file-stream adapter below.
17
+ if (stat.isSocket() && !process.versions.bun)
18
+ return new Socket({ fd, readable: true, writable: true });
14
19
  }
15
20
  catch {
16
21
  // Bootstrap is optional authority, never a reason to terminate the shared host.
@@ -11,17 +11,18 @@
11
11
  * `SessionEventWriter.enqueue`.
12
12
  *
13
13
  * Two hard rules:
14
- * - Type-gate FIRST. `message_update` is the hot path (one record per token) and must
15
- * never be walked.
14
+ * - Type-gate the media walk. JSON-value normalization is shared with writer
15
+ * acceptance; media projection itself never walks `message_update` partials.
16
16
  * - Copy-on-write. Responses hand out live agent state by reference
17
17
  * (`success(id, "get_messages", { messages: session.messages })`), so the input graph
18
- * is never mutated and unchanged sub-trees keep their identity. When nothing changed
18
+ * is never mutated and unchanged JSON sub-trees keep their identity. When nothing changed
19
19
  * the SAME record reference comes back, which lets the caller reuse the line it
20
20
  * already serialized.
21
21
  *
22
22
  * User-authored images (`prompt.images`) stay intact: they are bounded by the client
23
23
  * that sent them; the fan-out hazard is tool output.
24
24
  */
25
+ type RpcRecord = Record<string, unknown>;
25
26
  /** Why a host could not keep a tool image on disk for the client to read. */
26
27
  export type MediaUnavailableReason = "image_too_large" | "session_limit" | "storage_error";
27
28
  /** Where a persisted image lives (absolute), or why there is no file. */
@@ -61,6 +62,8 @@ export interface ImageRefBlock {
61
62
  * omitting it.
62
63
  */
63
64
  export declare function base64ByteLength(data: string): number;
65
+ /** True when this record type can carry a tool result at all (cheap gate). */
66
+ export declare function carriesMedia(record: RpcRecord): boolean;
64
67
  /**
65
68
  * Replace inline tool-result images with `image_ref` placeholders.
66
69
  *
@@ -68,4 +71,7 @@ export declare function base64ByteLength(data: string): number;
68
71
  * serialization entirely.
69
72
  */
70
73
  export declare function omitInlineMedia<T extends object>(record: T, persist?: MediaPersister): T;
74
+ /** `omitInlineMedia` for a value that is already JSON-normal: type gate first, no normalization walk. */
75
+ export declare function omitNormalInlineMedia<T extends object>(record: T, persist?: MediaPersister): T;
76
+ export {};
71
77
  //# sourceMappingURL=media-placeholders.d.ts.map
@@ -11,17 +11,18 @@
11
11
  * `SessionEventWriter.enqueue`.
12
12
  *
13
13
  * Two hard rules:
14
- * - Type-gate FIRST. `message_update` is the hot path (one record per token) and must
15
- * never be walked.
14
+ * - Type-gate the media walk. JSON-value normalization is shared with writer
15
+ * acceptance; media projection itself never walks `message_update` partials.
16
16
  * - Copy-on-write. Responses hand out live agent state by reference
17
17
  * (`success(id, "get_messages", { messages: session.messages })`), so the input graph
18
- * is never mutated and unchanged sub-trees keep their identity. When nothing changed
18
+ * is never mutated and unchanged JSON sub-trees keep their identity. When nothing changed
19
19
  * the SAME record reference comes back, which lets the caller reuse the line it
20
20
  * already serialized.
21
21
  *
22
22
  * User-authored images (`prompt.images`) stay intact: they are bounded by the client
23
23
  * that sent them; the fan-out hazard is tool output.
24
24
  */
25
+ import { normalizeJsonValue } from "./json-normal-value.js";
25
26
  /** Record types that can carry a tool result somewhere in their payload. */
26
27
  const MEDIA_BEARING_RECORD_TYPES = new Set([
27
28
  "tool_execution_end",
@@ -128,7 +129,7 @@ function omitToolExecutionEndImages(record, persist) {
128
129
  return omitted === result.content ? record : { ...record, result: { ...result, content: omitted } };
129
130
  }
130
131
  /** True when this record type can carry a tool result at all (cheap gate). */
131
- function carriesMedia(record) {
132
+ export function carriesMedia(record) {
132
133
  if (typeof record.type !== "string")
133
134
  return false;
134
135
  if (MEDIA_BEARING_RECORD_TYPES.has(record.type))
@@ -144,6 +145,13 @@ function carriesMedia(record) {
144
145
  * serialization entirely.
145
146
  */
146
147
  export function omitInlineMedia(record, persist) {
148
+ const { value, changed } = normalizeJsonValue(record);
149
+ if (!changed)
150
+ return omitNormalInlineMedia(record, persist);
151
+ return omitNormalInlineMedia(value, persist);
152
+ }
153
+ /** `omitInlineMedia` for a value that is already JSON-normal: type gate first, no normalization walk. */
154
+ export function omitNormalInlineMedia(record, persist) {
147
155
  const typed = record;
148
156
  if (!carriesMedia(typed))
149
157
  return record;
@@ -192,6 +192,8 @@ export type RpcCommand = RpcAttachmentCommand | (RpcSessionCommand & {
192
192
  * caused the transition (default: true for `childrenOf`, false otherwise).
193
193
  */
194
194
  effects?: boolean;
195
+ /** Observe dialog registration and settlement without receiving command replies. */
196
+ dialogs?: boolean;
195
197
  /** Also deliver these content record types of matched sessions (default `"none"`); opt-in, never an attachment. */
196
198
  mirror?: "none" | "all" | string[];
197
199
  /** Mark the request activity-neutral for the supervisor's idle exit, as on `list_sessions`. */
@@ -116,6 +116,7 @@ export type RpcExtensionUIResponseFrame = {
116
116
  type: "extension_ui_response";
117
117
  id: string;
118
118
  uiRequestId?: string;
119
+ answerId?: string;
119
120
  };
120
121
  export type RpcExtensionUIResponse = (RpcExtensionUIResponseFrame & {
121
122
  value: string;
@@ -10,7 +10,14 @@ import type { EffectStatus } from "./effect-record.ts";
10
10
  import type { RpcSlashCommand } from "./rpc-command-surface.ts";
11
11
  import type { RpcEvalCell, RpcEvalCellSnapshot } from "./rpc-result-types.ts";
12
12
  import type { RpcAuthProvider, RpcLoadedExtension, RpcLoadedMcpServer, RpcProviderAccount, RpcSessionState } from "./rpc-session-state-types.ts";
13
+ import type { RpcSnapshot } from "./snapshot-types.ts";
13
14
  export type RpcReadResponse = {
15
+ id?: string;
16
+ type: "response";
17
+ command: "get_snapshot";
18
+ success: true;
19
+ data: RpcSnapshot;
20
+ } | {
14
21
  id?: string;
15
22
  type: "response";
16
23
  command: "get_state";
@@ -120,6 +127,8 @@ export type RpcReadResponse = {
120
127
  leafId: string | null;
121
128
  hasMore?: boolean;
122
129
  nextSince?: string;
130
+ /** A `tail` page's oldest entry id while older entries remain (senpi#4267). */
131
+ next_before?: string;
123
132
  };
124
133
  } | {
125
134
  id?: string;
@@ -182,6 +182,12 @@ export type RpcSessionCommand = {
182
182
  } | {
183
183
  id?: string;
184
184
  type: "get_state";
185
+ } | {
186
+ id?: string;
187
+ type: "get_snapshot";
188
+ tail?: number;
189
+ maxBytes?: number;
190
+ observe?: boolean;
185
191
  } | {
186
192
  id?: string;
187
193
  type: "request_self_settle";
@@ -388,11 +394,18 @@ export type RpcSessionCommand = {
388
394
  } | {
389
395
  id?: string;
390
396
  type: "get_fork_messages";
391
- } | {
397
+ }
398
+ /**
399
+ * `since`/`before` bound the range (strictly after / strictly before an entry id); `limit` pages it forward from
400
+ * its start, `tail` backward from its end (`next_before` names the next page, senpi#4267).
401
+ */
402
+ | {
392
403
  id?: string;
393
404
  type: "get_entries";
394
405
  since?: string;
406
+ before?: string;
395
407
  limit?: number;
408
+ tail?: number;
396
409
  maxBytes?: number;
397
410
  } | {
398
411
  id?: string;
@@ -9,6 +9,7 @@ import type { SessionEntry, UsageTotals } from "../../core/session-manager.ts";
9
9
  import type { ThinkingLevelSource } from "../../core/session-thinking-memory.ts";
10
10
  import type { SourceInfo } from "../../core/source-info.ts";
11
11
  import type { RpcExtensionUIRequest, RpcQuestionUiRequest } from "./rpc-question-types.ts";
12
+ import type { UiAnswerReceipt } from "./ui-answer-ledger.ts";
12
13
  /** One provider row for the /login and /logout selectors. */
13
14
  export interface RpcAuthProvider {
14
15
  /** Provider id (e.g. "anthropic", "openai"). */
@@ -61,6 +62,8 @@ export interface RpcSessionModelEntry {
61
62
  serviceTier?: ServiceTier;
62
63
  }
63
64
  export interface RpcSessionState {
65
+ /** Latest 64 UI-answer receipts in this session generation, oldest first. */
66
+ answeredUiRequests?: UiAnswerReceipt[];
64
67
  paused: boolean;
65
68
  pausedNoticeCount: number;
66
69
  holds: {
@@ -7,6 +7,7 @@ import { sanitizeProviderDiagnostic } from "@earendil-works/pi-ai";
7
7
  import { ProjectTrustStore } from "../../core/trust-manager.js";
8
8
  import { clientFacingIdentity } from "./client-admission-record.js";
9
9
  import { sessionQuestionBridges } from "./connection-question-bridge.js";
10
+ import { sessionUiAnswerLedger } from "./ui-answer-ledger.js";
10
11
  import { sessionDialogRequests } from "./session-extension-ui-requests.js";
11
12
  /**
12
13
  * Project one session into the wire state shape.
@@ -29,6 +30,7 @@ export function buildRpcSessionState(session, lastAbortSource) {
29
30
  const projectTrusted = new ProjectTrustStore(session.agentDir).get(cwd) === true;
30
31
  const lastProviderDiagnostic = sanitizeProviderDiagnostic(session.agent.state.providerDiagnostic);
31
32
  return {
33
+ answeredUiRequests: sessionUiAnswerLedger(session).snapshot(),
32
34
  paused: session.paused ?? false,
33
35
  pausedNoticeCount: session.pausedNoticeCount ?? 0,
34
36
  holds: session.holds ?? [],
@@ -1,4 +1,5 @@
1
1
  /** Public RPC protocol surface. Definitions are grouped by responsibility. */
2
+ export type { RpcSnapshot, RpcSnapshotMarker, RpcUiRequestEvent } from "./snapshot-types.ts";
2
3
  export type { SessionContext, SessionKind } from "../../core/extensions/types.ts";
3
4
  export type { RpcCommandInvocationEvent } from "./rpc-command-invocation.ts";
4
5
  export type { RpcCommandsChangedEvent, RpcSlashCommand } from "./rpc-command-surface.ts";
@@ -13,6 +13,7 @@ export const ATTACH_GATE_EXEMPT = new Set([
13
13
  "reclaim_session",
14
14
  "get_effect_status",
15
15
  "get_state",
16
+ "get_snapshot",
16
17
  "get_messages",
17
18
  "get_entries",
18
19
  "get_tree",