@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
@@ -0,0 +1,19 @@
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ export const nodeCompactStorageIo = {
3
+ writeFile: (path, data) => writeFile(path, data, { flag: "wx", mode: 0o600 }),
4
+ readFile: (path) => readFile(path),
5
+ unlink: (path) => unlink(path),
6
+ };
7
+ export function describeCause(cause) {
8
+ return cause instanceof Error ? cause.message : String(cause);
9
+ }
10
+ /** Committed storage that can no longer be read: a real error, never empty content. */
11
+ export class CompactStorageReadError extends Error {
12
+ constructor(operation, path, cause) {
13
+ super(`compact storage ${operation} failed for ${path}: ${describeCause(cause)}`, { cause });
14
+ this.name = "CompactStorageReadError";
15
+ this.operation = operation;
16
+ this.path = path;
17
+ }
18
+ }
19
+ //# sourceMappingURL=compact-storage-io.js.map
@@ -48,6 +48,8 @@ export interface RpcConnectionOptions {
48
48
  */
49
49
  export interface RpcConnectionSink {
50
50
  writeRaw(chunk: string): void;
51
+ /** Shared bindings accept correlated records synchronously, before JSONL buffering. */
52
+ writeRecord?(record: object): void;
51
53
  waitForBackpressure(): Promise<void>;
52
54
  /** Tears the transport down once its event queue can no longer deliver (overflow, write failure). */
53
55
  close?(): void;
@@ -42,13 +42,13 @@ import { ClientAdmissions } from "./client-admissions.js";
42
42
  import { handleClientInput } from "./client-input-handler.js";
43
43
  import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } from "./connection-question-bridge.js";
44
44
  import { CONNECTION_ROLE_FIXED, ConnectionRoles, roleConnection } from "./connection-roles.js";
45
- import { AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, INTERACTIVE_USER_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, TOOL_BARRIER_CAPABILITY, TASK_PARENT_CAPABILITY, } from "./custom-capability.js";
45
+ import { AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, ENTRIES_TAIL_CAPABILITY, INTERACTIVE_USER_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, TOOL_BARRIER_CAPABILITY, TASK_PARENT_CAPABILITY, } from "./custom-capability.js";
46
46
  import { runAbort, runGetEffectStatus, runQueuedInputChange, runSetModel, runSetThinkingLevel, } from "./effect-commands.js";
47
47
  import { EffectLedger } from "./effect-ledger.js";
48
48
  import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
49
49
  import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
50
50
  import { settleExtensionUiResponse } from "./extension-ui-response.js";
51
- import { currentResponseByteBudget, endEntryWalk, entryPageBounds, holdEntryWalk, pageEntries, } from "./get-entries-page.js";
51
+ import { answerGetEntries } from "./get-entries-command.js";
52
52
  import { runHoldInput } from "./held-input-commands.js";
53
53
  import { HostSessionControl } from "./host-session-control.js";
54
54
  import { createRpcLoginPromptCallbacks } from "./login-prompts.js";
@@ -57,7 +57,9 @@ import { protocolIdentity } from "./protocol-identity.js";
57
57
  import { buildRpcCommandsForSession, createCommandsChangedEvent, rpcCommandListDigest } from "./rpc-command-surface.js";
58
58
  import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
59
59
  import { buildRpcSessionState } from "./rpc-session-state.js";
60
- import { RPC_ERROR_ENTRY_NOT_FOUND, RPC_ERROR_EVAL_CELL_NOT_FOUND, RPC_ERROR_EVAL_CELLS_UNAVAILABLE, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_INVALID_EVAL_CELL_REQUEST, RPC_ERROR_MEDIA_NOT_FOUND, RPC_ERROR_MODEL_NOT_FOUND, RPC_ERROR_RESPONSE_TOO_LARGE, STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY, } from "./rpc-types.js";
60
+ import { getSnapshot } from "./get-snapshot.js";
61
+ import { sessionUiAnswerLedger } from "./ui-answer-ledger.js";
62
+ import { RPC_ERROR_EVAL_CELL_NOT_FOUND, RPC_ERROR_EVAL_CELLS_UNAVAILABLE, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_INVALID_EVAL_CELL_REQUEST, RPC_ERROR_MEDIA_NOT_FOUND, RPC_ERROR_MODEL_NOT_FOUND, STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY, } from "./rpc-types.js";
61
63
  import { runPauseForRun, runResumeForRun } from "./run-hold-commands.js";
62
64
  import { availableModelsData, interruptRunningTurn } from "./session-control-actions-data.js";
63
65
  import { SessionExtensionUiRequests, sessionDialogRequests } from "./session-extension-ui-requests.js";
@@ -197,7 +199,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
197
199
  let loadedSurfacesDigest;
198
200
  let rpcCommandsDigest;
199
201
  let suppressLoadedSurfaceEvents = false;
200
- const eventOutput = createRpcEventOutputBuffer(sink.writeRaw, options.eventFlushScheduler);
202
+ const eventOutput = sink.writeRecord
203
+ ? undefined
204
+ : createRpcEventOutputBuffer(sink.writeRaw, options.eventFlushScheduler);
201
205
  const effectContext = () => ({
202
206
  session,
203
207
  by: { connectionId: options.clientInfo?.connectionId() ?? "local" },
@@ -205,10 +209,18 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
205
209
  });
206
210
  const tagSessionRecord = (value) => routingSessionId === undefined ? value : { ...value, sessionId: routingSessionId };
207
211
  const output = (obj) => {
208
- eventOutput.writeImmediate(tagSessionRecord(obj));
212
+ const record = tagSessionRecord(obj);
213
+ if (sink.writeRecord)
214
+ sink.writeRecord(record);
215
+ else
216
+ eventOutput?.writeImmediate(record);
209
217
  };
210
218
  const outputEvent = (event) => {
211
- eventOutput.enqueueEvent(tagSessionRecord(event));
219
+ const record = tagSessionRecord(event);
220
+ if (sink.writeRecord)
221
+ sink.writeRecord(record);
222
+ else
223
+ eventOutput?.enqueueEvent(record);
212
224
  };
213
225
  const currentLoadedSurfaces = () => {
214
226
  const extensions = loadedExtensions(session);
@@ -268,7 +280,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
268
280
  });
269
281
  });
270
282
  const waitForRpcBackpressure = async () => {
271
- eventOutput.flushEvents();
283
+ eventOutput?.flushEvents();
272
284
  await sink.waitForBackpressure();
273
285
  };
274
286
  const success = (id, command, data) => {
@@ -292,10 +304,15 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
292
304
  };
293
305
  };
294
306
  // Pending extension UI requests waiting for response
295
- const pendingExtensionRequests = new SessionExtensionUiRequests(() => session.refreshLifecycle());
307
+ const uiRequestChanged = (change) => {
308
+ session.refreshLifecycle();
309
+ if (change && routingSessionId !== undefined)
310
+ output(change);
311
+ };
312
+ const pendingExtensionRequests = new SessionExtensionUiRequests(uiRequestChanged);
296
313
  const grantRequests = new Map();
297
314
  sessionDialogRequests.set(session, pendingExtensionRequests);
298
- const questions = new ConnectionQuestionBridge(output, () => session.refreshLifecycle());
315
+ const questions = new ConnectionQuestionBridge(output, uiRequestChanged);
299
316
  let shutdownRequested = false;
300
317
  // In-flight OAuth logins, keyed by provider. login_start registers one; the
301
318
  // login promise clears it on completion; login_cancel aborts it. The flow is
@@ -858,6 +875,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
858
875
  capabilities: [
859
876
  ...new Set([
860
877
  "multi_session",
878
+ "snapshot_cursor",
879
+ "ui_answer_ledger",
861
880
  "session_lifecycle",
862
881
  AUTO_TITLE_SESSIONS_CAPABILITY,
863
882
  MEDIA_PLACEHOLDERS_CAPABILITY,
@@ -871,6 +890,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
871
890
  TOOL_BARRIER_CAPABILITY,
872
891
  USER_ACTIVITY_CAPABILITY,
873
892
  RESUME_SESSION_CAPABILITY,
893
+ ENTRIES_TAIL_CAPABILITY,
874
894
  ...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
875
895
  ...(options.toolRuntime ? toolRuntimeCapabilities(options.toolRuntime) : []),
876
896
  // Client flags cannot advertise a host guard this classic binding does not run, nor a tool runtime.
@@ -1079,6 +1099,18 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1079
1099
  refreshGrantPresentation();
1080
1100
  return success(id, "set_client_info");
1081
1101
  }
1102
+ case "get_snapshot": {
1103
+ const snapshot = getSnapshot(session, command, {
1104
+ lastAbortSource,
1105
+ enqueue: (markerId) => {
1106
+ if (routingSessionId !== undefined)
1107
+ output({ type: "snapshot_marker", markerId });
1108
+ },
1109
+ });
1110
+ return "error" in snapshot
1111
+ ? error(id, "get_snapshot", snapshot.error, RPC_ERROR_INVALID_ARGUMENT)
1112
+ : success(id, "get_snapshot", snapshot.data);
1113
+ }
1082
1114
  case "get_state":
1083
1115
  return success(id, "get_state", buildRpcSessionState(session, lastAbortSource));
1084
1116
  case "request_self_settle":
@@ -1457,43 +1489,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1457
1489
  const messages = session.getUserMessagesForForking();
1458
1490
  return success(id, "get_fork_messages", { messages });
1459
1491
  }
1460
- case "get_entries": {
1461
- const sessionManager = session.sessionManager;
1462
- let entries = sessionManager.getEntries();
1463
- if (command.since !== undefined) {
1464
- const sinceIndex = entries.findIndex((e) => e.id === command.since);
1465
- if (sinceIndex === -1) {
1466
- return error(id, "get_entries", `Entry not found: ${command.since}`, RPC_ERROR_ENTRY_NOT_FOUND);
1467
- }
1468
- entries = entries.slice(sinceIndex + 1);
1469
- }
1470
- const bounds = entryPageBounds(command);
1471
- if (!bounds.ok)
1472
- return error(id, "get_entries", bounds.message, RPC_ERROR_INVALID_ARGUMENT);
1473
- const page = pageEntries(entries, describeUsageLimitEntry, bounds);
1474
- // A client walking pages asks again at once; holding the view until the last page spares each page a
1475
- // full re-read of the file. The hold expires on its own, so a walk the client abandons on an idle
1476
- // session does not pin the whole history; any other answer lets the view go now.
1477
- if (bounds.paged && page.hasMore)
1478
- holdEntryWalk(session, () => session.releaseFullHistoryView());
1479
- else {
1480
- endEntryWalk(session);
1481
- session.releaseFullHistoryView();
1482
- }
1483
- const budget = currentResponseByteBudget();
1484
- if (page.firstEntryBytes > budget) {
1485
- return error(id, "get_entries", `Entry ${entries[0]?.id ?? ""} alone is larger than one answer can carry (${budget} bytes), so it cannot be sent over RPC.`, RPC_ERROR_RESPONSE_TOO_LARGE);
1486
- }
1487
- if (!bounds.paged && page.hasMore) {
1488
- return error(id, "get_entries", `The session history is larger than one answer can carry (${budget} bytes). Read it in pages: pass limit or maxBytes, then since: nextSince until hasMore is false.`, RPC_ERROR_RESPONSE_TOO_LARGE);
1489
- }
1490
- return success(id, "get_entries", {
1491
- entries: page.entries,
1492
- leafId: sessionManager.getLeafId(),
1493
- ...(bounds.paged ? { hasMore: page.hasMore } : {}),
1494
- ...(page.nextSince === undefined ? {} : { nextSince: page.nextSince }),
1495
- });
1496
- }
1492
+ case "get_entries":
1493
+ return answerGetEntries(session, command);
1497
1494
  case "get_tree": {
1498
1495
  const sessionManager = session.sessionManager;
1499
1496
  const tree = sessionManager.getTree();
@@ -1722,6 +1719,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1722
1719
  questions,
1723
1720
  dialogs: pendingExtensionRequests,
1724
1721
  routed: routingSessionId !== undefined,
1722
+ ledger: sessionUiAnswerLedger(session),
1725
1723
  // An answer is a person's input by its entry point, whatever the connection role.
1726
1724
  beforeAnswer: () => session.admitHumanAnswer(),
1727
1725
  });
@@ -1753,7 +1751,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1753
1751
  // A single-session connection hears its account's new limit windows as they are reported. A host
1754
1752
  // session's handler (it has a routing `sessionId`) does not: the host broadcasts the record once.
1755
1753
  const stopLimitEvents = options.sessionId === undefined
1756
- ? subscriptionLimits.subscribe((limits) => eventOutput.enqueueEvent({ type: "subscription_limits", ...limits }))
1754
+ ? subscriptionLimits.subscribe((limits) => outputEvent({ type: "subscription_limits", ...limits }))
1757
1755
  : () => { };
1758
1756
  const dispose = async (disposeOptions = {}) => {
1759
1757
  unbindMcpForms?.();
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionUIContext, QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
2
+ import type { UiRequestChange } from "./snapshot-types.ts";
2
3
  import type { RpcExtensionUIProgress, RpcExtensionUIResponse, RpcQuestionUiRequest } from "./rpc-types.ts";
3
4
  type Options = Parameters<NonNullable<ExtensionUIContext["question"]>>[1];
4
5
  export declare const sessionQuestionBridges: WeakMap<object, ConnectionQuestionBridge>;
@@ -7,7 +8,7 @@ export declare class ConnectionQuestionBridge {
7
8
  private readonly resolved;
8
9
  private readonly output;
9
10
  private readonly changed;
10
- constructor(output: (record: object) => void, changed?: () => void);
11
+ constructor(output: (record: object) => void, changed?: (change?: UiRequestChange) => void);
11
12
  pendingQuestions(): RpcQuestionUiRequest[];
12
13
  ask(request: QuestionRequest, opts?: Options): Promise<QuestionResponse>;
13
14
  respond(response: RpcExtensionUIResponse, beforeAnswer?: () => void): boolean | "question_incomplete" | "question_already_resolved";
@@ -43,7 +43,11 @@ export class ConnectionQuestionBridge {
43
43
  clearTimeout(timer);
44
44
  opts?.signal?.removeEventListener("abort", cancel);
45
45
  this.pending.delete(id);
46
- this.changed?.();
46
+ this.changed?.({
47
+ type: "ui_request_settled",
48
+ uiRequestId: id,
49
+ disposition: status === "answered" || status === "comment-submitted" ? "answered" : "cancelled",
50
+ });
47
51
  this.resolved.add(id);
48
52
  const result = {
49
53
  status,
@@ -121,7 +125,7 @@ export class ConnectionQuestionBridge {
121
125
  },
122
126
  });
123
127
  arm();
124
- this.changed?.();
128
+ this.changed?.({ type: "ui_request_pending", uiRequestId: id, method: "question", request: frame });
125
129
  this.output(frame);
126
130
  opts?.signal?.addEventListener("abort", cancel, { once: true });
127
131
  if (opts?.signal?.aborted)
@@ -60,6 +60,12 @@ export declare const DETACH_CONNECTION_CAPABILITY = "detach_connection";
60
60
  * client's own `set_client_info` and fixed at that first declaration (see `connection-roles.ts`).
61
61
  */
62
62
  export declare const INTERACTIVE_USER_CAPABILITY = "interactive_user";
63
+ /** `get_state` and `get_entries` accept `observe: true` and then never count as an attachment (senpi#4267). */
64
+ export declare const OBSERVE_READS_CAPABILITY = "observe_reads";
65
+ /** `get_entries` accepts `before` and `tail` and answers `next_before` (senpi#4267). */
66
+ export declare const ENTRIES_TAIL_CAPABILITY = "entries_tail";
67
+ /** A terminal control endpoint serves the host's observe-and-drive command set (senpi#4267). */
68
+ export declare const TUI_OBSERVER_CAPABILITY = "tui_observer";
63
69
  /** The connection owns this session as a task child; only it may pause or resume the session for its run (senpi#3811). */
64
70
  export declare const TASK_PARENT_CAPABILITY = "task_parent";
65
71
  /**
@@ -60,6 +60,12 @@ export const DETACH_CONNECTION_CAPABILITY = "detach_connection";
60
60
  * client's own `set_client_info` and fixed at that first declaration (see `connection-roles.ts`).
61
61
  */
62
62
  export const INTERACTIVE_USER_CAPABILITY = "interactive_user";
63
+ /** `get_state` and `get_entries` accept `observe: true` and then never count as an attachment (senpi#4267). */
64
+ export const OBSERVE_READS_CAPABILITY = "observe_reads";
65
+ /** `get_entries` accepts `before` and `tail` and answers `next_before` (senpi#4267). */
66
+ export const ENTRIES_TAIL_CAPABILITY = "entries_tail";
67
+ /** A terminal control endpoint serves the host's observe-and-drive command set (senpi#4267). */
68
+ export const TUI_OBSERVER_CAPABILITY = "tui_observer";
63
69
  /** The connection owns this session as a task child; only it may pause or resume the session for its run (senpi#3811). */
64
70
  export const TASK_PARENT_CAPABILITY = "task_parent";
65
71
  /**
@@ -1,5 +1,6 @@
1
1
  import type { QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
2
2
  import type { RpcExtensionUIResponse } from "./rpc-types.ts";
3
+ import type { UiAnswerLedger } from "./ui-answer-ledger.ts";
3
4
  /**
4
5
  * An `extension_ui_response` carries two ids: `id`, the frame's own correlation id that its reply
5
6
  * echoes, and `uiRequestId`, the extension UI request it answers. The short form every older client
@@ -23,6 +24,8 @@ export type ExtensionUiResponseReply = {
23
24
  readonly id: string;
24
25
  readonly type: "response";
25
26
  readonly command: "extension_ui_response";
27
+ /** The answer's outcome for its request; absent when a malformed answer was refused before settling anything. */
28
+ readonly disposition?: "applied" | "replay" | "already_answered" | "unknown";
26
29
  } & ({
27
30
  readonly success: true;
28
31
  } | {
@@ -37,13 +40,15 @@ type DialogAnswers = {
37
40
  };
38
41
  /**
39
42
  * Settles one response on a host connection and returns its reply, keyed by the frame `id`.
40
- * `undefined` only on an unrouted (single-session stdio) connection for a response no request here
41
- * matches: that connection has always ignored such a response, and still does.
43
+ * `undefined` only on an unrouted (single-session stdio) connection for an unnamed response no
44
+ * request here matches: that connection has always ignored such a response, and still does. A named
45
+ * response (`answerId`) asks for a receipt, so every connection answers it.
42
46
  */
43
47
  export declare function settleExtensionUiResponse(response: RpcExtensionUIResponse, targets: {
44
48
  readonly questions: QuestionAnswers;
45
49
  readonly dialogs: DialogAnswers;
46
50
  readonly routed: boolean;
51
+ readonly ledger?: UiAnswerLedger;
47
52
  readonly beforeAnswer?: () => void;
48
53
  }): ExtensionUiResponseReply | undefined;
49
54
  export {};
@@ -28,25 +28,45 @@ export function unansweredQuestionIds(questions, answers) {
28
28
  }
29
29
  /**
30
30
  * Settles one response on a host connection and returns its reply, keyed by the frame `id`.
31
- * `undefined` only on an unrouted (single-session stdio) connection for a response no request here
32
- * matches: that connection has always ignored such a response, and still does.
31
+ * `undefined` only on an unrouted (single-session stdio) connection for an unnamed response no
32
+ * request here matches: that connection has always ignored such a response, and still does. A named
33
+ * response (`answerId`) asks for a receipt, so every connection answers it.
33
34
  */
34
35
  export function settleExtensionUiResponse(response, targets) {
36
+ if (response.answerId !== undefined &&
37
+ (typeof response.answerId !== "string" || response.answerId.length < 1 || response.answerId.length > 256))
38
+ return refused(response.id, "invalid_answer_id");
35
39
  const requestId = answeredUiRequestId(response) ?? response.id;
40
+ const receipt = response.answerId === undefined ? undefined : targets.ledger?.lookup(requestId, response.answerId);
41
+ if (receipt === "replay")
42
+ return { ...reply(response.id), success: true, disposition: "replay" };
43
+ if (receipt === "already_answered")
44
+ return { ...reply(response.id), success: false, error: "already_answered", disposition: receipt };
36
45
  const answer = requestId === response.id ? response : { ...response, id: requestId };
37
46
  const question = targets.questions.respond(answer, targets.beforeAnswer);
38
47
  if (typeof question === "string")
39
- return refused(response.id, question);
40
- if (question || targets.dialogs.resolve(answer, targets.beforeAnswer))
41
- return { ...reply(response.id), success: true };
48
+ return refused(response.id, response.answerId !== undefined && question === "question_already_resolved"
49
+ ? "unknown_extension_ui_request"
50
+ : question);
51
+ if (question || targets.dialogs.resolve(answer, targets.beforeAnswer)) {
52
+ targets.ledger?.record(requestId, response.answerId);
53
+ return { ...reply(response.id), success: true, disposition: "applied" };
54
+ }
42
55
  // A routed binding owns exactly one session's request map: an unmatched response is a protocol
43
56
  // error for its sender, never a cross-session match.
44
- return targets.routed ? refused(response.id, "unknown_extension_ui_request") : undefined;
57
+ return targets.routed || response.answerId !== undefined
58
+ ? refused(response.id, "unknown_extension_ui_request")
59
+ : undefined;
45
60
  }
46
61
  function reply(id) {
47
62
  return { id, type: "response", command: "extension_ui_response" };
48
63
  }
49
64
  function refused(id, error) {
50
- return { ...reply(id), success: false, error };
65
+ const disposition = error === "question_already_resolved"
66
+ ? "already_answered"
67
+ : error === "unknown_extension_ui_request"
68
+ ? "unknown"
69
+ : undefined;
70
+ return { ...reply(id), success: false, error, ...(disposition === undefined ? {} : { disposition }) };
51
71
  }
52
72
  //# sourceMappingURL=extension-ui-response.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The one `get_entries` answer: a host connection and a terminal control endpoint both read history through it, so
3
+ * the range rules (`since`, `before`), the forward (`limit`) and backward (`tail`, senpi#4267) pages and the byte
4
+ * budget (senpi#3534) are the same on every surface.
5
+ */
6
+ import type { AgentSession } from "../../core/agent-session.ts";
7
+ import { type RpcCommand, type RpcResponse } from "./rpc-types.ts";
8
+ type GetEntries = Extract<RpcCommand, {
9
+ type: "get_entries";
10
+ }>;
11
+ export declare function answerGetEntries(session: AgentSession, command: GetEntries): RpcResponse;
12
+ export {};
13
+ //# sourceMappingURL=get-entries-command.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { describeUsageLimitEntry } from "../../core/retry-fallback/usage-limit.js";
2
+ import { currentResponseByteBudget, endEntryWalk, entryPageBounds, holdEntryWalk, pageEntries, pageEntriesFromEnd, } from "./get-entries-page.js";
3
+ import { RPC_ERROR_ENTRY_NOT_FOUND, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_RESPONSE_TOO_LARGE, } from "./rpc-types.js";
4
+ function refuse(id, message, errorCode) {
5
+ return { id, type: "response", command: "get_entries", success: false, error: message, errorCode };
6
+ }
7
+ export function answerGetEntries(session, command) {
8
+ const { id } = command;
9
+ const sessionManager = session.sessionManager;
10
+ let entries = sessionManager.getEntries();
11
+ if (command.before !== undefined) {
12
+ const beforeIndex = entries.findIndex((e) => e.id === command.before);
13
+ if (beforeIndex === -1)
14
+ return refuse(id, `Entry not found: ${command.before}`, RPC_ERROR_ENTRY_NOT_FOUND);
15
+ entries = entries.slice(0, beforeIndex);
16
+ }
17
+ if (command.since !== undefined) {
18
+ const sinceIndex = sessionManager.getEntries().findIndex((e) => e.id === command.since);
19
+ if (sinceIndex === -1)
20
+ return refuse(id, `Entry not found: ${command.since}`, RPC_ERROR_ENTRY_NOT_FOUND);
21
+ entries = entries.slice(sinceIndex + 1);
22
+ }
23
+ const bounds = entryPageBounds(command);
24
+ if (!bounds.ok)
25
+ return refuse(id, bounds.message, RPC_ERROR_INVALID_ARGUMENT);
26
+ const backward = bounds.tail !== undefined;
27
+ const page = backward
28
+ ? pageEntriesFromEnd(entries, describeUsageLimitEntry, bounds)
29
+ : pageEntries(entries, describeUsageLimitEntry, bounds);
30
+ // A client walking pages asks again at once; holding the view until the last page spares each page a
31
+ // full re-read of the file. The hold expires on its own, so a walk the client abandons on an idle
32
+ // session does not pin the whole history; any other answer lets the view go now.
33
+ if (bounds.paged && page.hasMore)
34
+ holdEntryWalk(session, () => session.releaseFullHistoryView());
35
+ else {
36
+ endEntryWalk(session);
37
+ session.releaseFullHistoryView();
38
+ }
39
+ const budget = currentResponseByteBudget();
40
+ if (page.firstEntryBytes > budget) {
41
+ const entry = backward ? entries.at(-1) : entries[0];
42
+ return refuse(id, `Entry ${entry?.id ?? ""} alone is larger than one answer can carry (${budget} bytes), so it cannot be sent over RPC.`, RPC_ERROR_RESPONSE_TOO_LARGE);
43
+ }
44
+ if (!bounds.paged && page.hasMore) {
45
+ return refuse(id, `The session history is larger than one answer can carry (${budget} bytes). Read it in pages: pass limit or maxBytes, then since: nextSince until hasMore is false.`, RPC_ERROR_RESPONSE_TOO_LARGE);
46
+ }
47
+ return {
48
+ id,
49
+ type: "response",
50
+ command: "get_entries",
51
+ success: true,
52
+ data: {
53
+ entries: page.entries,
54
+ leafId: sessionManager.getLeafId(),
55
+ ...(bounds.paged ? { hasMore: page.hasMore } : {}),
56
+ ...("nextSince" in page && page.nextSince !== undefined ? { nextSince: page.nextSince } : {}),
57
+ ...("nextBefore" in page && page.nextBefore !== undefined ? { next_before: page.nextBefore } : {}),
58
+ },
59
+ };
60
+ }
61
+ //# sourceMappingURL=get-entries-command.js.map
@@ -14,6 +14,8 @@ export declare function currentResponseByteBudget(): number;
14
14
  export declare function setResponseByteBudgetForTesting(bytes: number): () => void;
15
15
  export interface EntryPageRequest {
16
16
  readonly limit?: number;
17
+ /** The newest `tail` entries of the range, read backwards (senpi#4267); never combined with `limit`. */
18
+ readonly tail?: number;
17
19
  readonly maxBytes?: number;
18
20
  }
19
21
  export interface EntryPage<T> {
@@ -27,6 +29,7 @@ export type EntryPageBounds = {
27
29
  readonly ok: true;
28
30
  readonly paged: boolean;
29
31
  readonly limit?: number;
32
+ readonly tail?: number;
30
33
  readonly maxBytes: number;
31
34
  } | {
32
35
  readonly ok: false;
@@ -44,6 +47,25 @@ export declare function pageEntries<S extends {
44
47
  readonly limit?: number;
45
48
  readonly maxBytes: number;
46
49
  }): EntryPage<T>;
50
+ export interface TailEntryPage<T> {
51
+ /** In append order: the oldest of the page first. */
52
+ readonly entries: T[];
53
+ readonly hasMore: boolean;
54
+ /** The oldest entry id the page returned, while older entries remain: pass it as the next `before`. */
55
+ readonly nextBefore?: string;
56
+ /** Serialized size of the page's newest entry (0 for an empty page). */
57
+ readonly firstEntryBytes: number;
58
+ }
59
+ /**
60
+ * The newest entries first, until the count or the byte size would pass the bounds, answered in append order. The
61
+ * newest entry is always taken, so a backward walk always moves even when one entry alone is larger than `maxBytes`.
62
+ */
63
+ export declare function pageEntriesFromEnd<S extends {
64
+ readonly id: string;
65
+ }, T>(entries: readonly S[], project: (entry: S) => T, bounds: {
66
+ readonly tail?: number;
67
+ readonly maxBytes: number;
68
+ }): TailEntryPage<T>;
47
69
  /**
48
70
  * Keeps a page walk's full-history view for the next page, and lets it go after `ENTRY_WALK_HOLD_MS` without
49
71
  * one: a client that stops mid-walk must not pin the whole history of a session that is already idle.
@@ -22,17 +22,22 @@ export function setResponseByteBudgetForTesting(bytes) {
22
22
  };
23
23
  }
24
24
  export function entryPageBounds(request) {
25
- const { limit, maxBytes } = request;
25
+ const { limit, tail, maxBytes } = request;
26
26
  if (limit !== undefined && !(Number.isInteger(limit) && limit > 0))
27
27
  return { ok: false, message: "limit must be a positive integer" };
28
+ if (tail !== undefined && !(Number.isInteger(tail) && tail > 0))
29
+ return { ok: false, message: "tail must be a positive integer" };
30
+ if (limit !== undefined && tail !== undefined)
31
+ return { ok: false, message: "limit and tail cannot be combined: limit pages forward, tail backward" };
28
32
  if (maxBytes !== undefined && !(Number.isInteger(maxBytes) && maxBytes > 0))
29
33
  return { ok: false, message: "maxBytes must be a positive integer" };
30
- if (limit === undefined && maxBytes === undefined)
34
+ if (limit === undefined && tail === undefined && maxBytes === undefined)
31
35
  return { ok: true, paged: false, maxBytes: responseByteBudget };
32
36
  return {
33
37
  ok: true,
34
38
  paged: true,
35
39
  ...(limit === undefined ? {} : { limit }),
40
+ ...(tail === undefined ? {} : { tail }),
36
41
  maxBytes: Math.min(maxBytes ?? DEFAULT_ENTRY_PAGE_BYTES, responseByteBudget),
37
42
  };
38
43
  }
@@ -68,6 +73,40 @@ export function pageEntries(entries, project, bounds) {
68
73
  firstEntryBytes,
69
74
  };
70
75
  }
76
+ /**
77
+ * The newest entries first, until the count or the byte size would pass the bounds, answered in append order. The
78
+ * newest entry is always taken, so a backward walk always moves even when one entry alone is larger than `maxBytes`.
79
+ */
80
+ export function pageEntriesFromEnd(entries, project, bounds) {
81
+ const page = [];
82
+ let bytes = 0;
83
+ let firstEntryBytes = 0;
84
+ let index = entries.length - 1;
85
+ for (; index >= 0; index--) {
86
+ if (bounds.tail !== undefined && page.length >= bounds.tail)
87
+ break;
88
+ const entry = entries[index];
89
+ if (entry === undefined)
90
+ break;
91
+ const projected = project(entry);
92
+ const size = Buffer.byteLength(JSON.stringify(projected)) + 1;
93
+ if (page.length > 0 && bytes + size > bounds.maxBytes)
94
+ break;
95
+ if (page.length === 0)
96
+ firstEntryBytes = size;
97
+ page.push(projected);
98
+ bytes += size;
99
+ }
100
+ page.reverse();
101
+ const oldest = entries[index + 1];
102
+ const hasMore = index >= 0;
103
+ return {
104
+ entries: page,
105
+ hasMore,
106
+ ...(hasMore && oldest !== undefined ? { nextBefore: oldest.id } : {}),
107
+ firstEntryBytes,
108
+ };
109
+ }
71
110
  const heldWalks = new WeakMap();
72
111
  /**
73
112
  * Keeps a page walk's full-history view for the next page, and lets it go after `ENTRY_WALK_HOLD_MS` without
@@ -0,0 +1,18 @@
1
+ import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
2
+ import type { AgentSession } from "../../core/agent-session.ts";
3
+ import type { RpcSnapshot } from "./snapshot-types.ts";
4
+ type SnapshotRequest = {
5
+ readonly tail?: unknown;
6
+ readonly maxBytes?: unknown;
7
+ };
8
+ /** Capture and mark on the source loop, before any response-writing await. */
9
+ export declare function getSnapshot(session: AgentSession, request: SnapshotRequest, cursor: {
10
+ readonly enqueue: (markerId: string) => void;
11
+ readonly lastAbortSource?: AgentAbortSource;
12
+ }): {
13
+ readonly data: RpcSnapshot;
14
+ } | {
15
+ readonly error: string;
16
+ };
17
+ export {};
18
+ //# sourceMappingURL=get-snapshot.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { describeUsageLimitEntry } from "../../core/retry-fallback/usage-limit.js";
3
+ import { currentResponseByteBudget, DEFAULT_ENTRY_PAGE_BYTES } from "./get-entries-page.js";
4
+ import { buildRpcSessionState } from "./rpc-session-state.js";
5
+ /** Capture and mark on the source loop, before any response-writing await. */
6
+ export function getSnapshot(session, request, cursor) {
7
+ const tail = request.tail === undefined ? 100 : request.tail;
8
+ const maxBytes = request.maxBytes === undefined ? DEFAULT_ENTRY_PAGE_BYTES : request.maxBytes;
9
+ if (typeof tail !== "number" || !Number.isSafeInteger(tail) || tail < 0)
10
+ return { error: "tail must be a non-negative integer" };
11
+ if (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes <= 0)
12
+ return { error: "maxBytes must be a positive integer" };
13
+ const state = buildRpcSessionState(session, cursor.lastAbortSource);
14
+ const history = session.sessionManager.getEntries();
15
+ const entries = [];
16
+ let bytes = 2;
17
+ for (let i = history.length - 1; i >= 0 && entries.length < tail; i--) {
18
+ const entry = history[i];
19
+ if (entry === undefined)
20
+ break;
21
+ const projected = describeUsageLimitEntry(entry);
22
+ const size = Buffer.byteLength(JSON.stringify(projected)) + (entries.length > 0 ? 1 : 0);
23
+ if (bytes + size > Math.min(maxBytes, currentResponseByteBudget()))
24
+ break;
25
+ entries.push(projected);
26
+ bytes += size;
27
+ }
28
+ entries.reverse();
29
+ const history_truncated = entries.length < history.length;
30
+ const markerId = randomUUID();
31
+ // Some state fields and entry payloads are live references: detach them before yielding, too.
32
+ const data = structuredClone({
33
+ state,
34
+ entries,
35
+ history_truncated,
36
+ next_before: history_truncated ? (entries[0]?.id ?? null) : null,
37
+ markerId,
38
+ });
39
+ session.releaseFullHistoryView();
40
+ cursor.enqueue(markerId);
41
+ return { data };
42
+ }
43
+ //# sourceMappingURL=get-snapshot.js.map