mixdog 1.4.0 → 1.4.2

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 (493) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/check-voice-runtime-inputs.mjs +25 -0
  4. package/scripts/gate-local.mjs +183 -63
  5. package/scripts/latest-guard.mjs +21 -9
  6. package/scripts/lib/gate-scheduler.mjs +138 -32
  7. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  8. package/scripts/lib/isolated-test-env.mjs +10 -5
  9. package/scripts/lib/run-node-tests.mjs +91 -66
  10. package/scripts/lib/scoped-temp.d.mts +16 -0
  11. package/scripts/lib/scoped-temp.mjs +124 -0
  12. package/scripts/lib/stale-temp.mjs +44 -0
  13. package/scripts/lib/test-failure-records.mjs +14 -1
  14. package/scripts/lib/test-failure-summary.mjs +16 -4
  15. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  16. package/scripts/post-release-check.mjs +1 -6
  17. package/scripts/pptx-kit-index.mjs +17 -5
  18. package/scripts/suite-health.mjs +115 -24
  19. package/scripts/test-data-dir.mjs +10 -7
  20. package/scripts/test.mjs +9 -14
  21. package/src/agents/advisor/AGENT.md +4 -4
  22. package/src/agents/advisor/agent.json +1 -1
  23. package/src/agents/explore/AGENT.md +22 -0
  24. package/src/agents/explore/agent.json +6 -0
  25. package/src/app.mjs +4 -0
  26. package/src/cli.mjs +7 -0
  27. package/src/defaults/agents.json +7 -0
  28. package/src/defaults/shipped-history.json +23 -2
  29. package/src/defaults/skills/browser-use/SKILL.md +10 -5
  30. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  31. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  32. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  33. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  34. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  35. package/src/defaults/skills/computer-use/SKILL.md +16 -4
  36. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  37. package/src/defaults/skills/setup/references/actions.md +12 -6
  38. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  39. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  40. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  41. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  42. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  43. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  44. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  45. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  46. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  47. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  48. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  49. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  50. package/src/defaults/skills/visualize/SKILL.md +6 -2
  51. package/src/lib/rules-builder.cjs +35 -42
  52. package/src/output-styles/common.md +2 -2
  53. package/src/output-styles/simple.md +1 -1
  54. package/src/rules/agent/AGENT.md +5 -4
  55. package/src/rules/lead/LEAD.md +12 -7
  56. package/src/rules/shared/10-tool-workflow.md +3 -1
  57. package/src/rules/shared/20-research.md +3 -6
  58. package/src/rules/shared/40-editing.md +1 -0
  59. package/src/rules/shared/45-destructive.md +0 -2
  60. package/src/rules/shared/50-execution.md +2 -0
  61. package/src/rules/shared/60-verification.md +10 -9
  62. package/src/rules/shared/70-delivery.md +8 -9
  63. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  64. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  65. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +156 -24
  66. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  67. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
  69. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  70. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +8 -8
  71. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  72. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
  73. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  74. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  75. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  76. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  77. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  78. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  79. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +88 -54
  80. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  81. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +54 -8
  82. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +37 -31
  84. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
  85. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
  86. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
  87. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  89. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
  90. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
  91. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  92. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  94. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  96. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
  97. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  100. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  101. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  102. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  103. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  104. package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
  105. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  106. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  107. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +248 -41
  108. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
  109. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
  110. package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
  111. package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
  112. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  113. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  114. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
  115. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  117. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  119. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  120. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
  121. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
  123. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
  124. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  125. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
  126. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
  127. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  128. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
  129. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
  130. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
  131. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
  132. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
  133. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +160 -31
  134. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +22 -12
  135. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
  136. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +49 -22
  137. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  139. package/src/runtime/agent/orchestrator/providers/registry.mjs +45 -2
  140. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  141. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  142. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  143. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  144. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
  145. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +8 -6
  147. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
  148. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  149. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +6 -1
  150. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  151. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  152. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
  153. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  154. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  155. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
  156. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +102 -12
  157. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  158. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
  159. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
  160. package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
  161. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
  162. package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
  163. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +164 -23
  164. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +139 -19
  165. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
  166. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
  167. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +100 -22
  168. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  169. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  170. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
  171. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +9 -2
  172. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
  173. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
  174. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
  175. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
  176. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +23 -4
  177. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  178. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  179. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
  180. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  181. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  182. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
  183. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
  184. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
  185. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  186. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
  187. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +30 -13
  188. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +63 -6
  189. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  190. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  191. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +35 -13
  192. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +24 -14
  193. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  194. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  195. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +10 -6
  196. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
  197. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
  198. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +56 -29
  199. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  200. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  201. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
  202. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
  203. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
  204. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
  205. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  206. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
  207. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
  208. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +252 -39
  209. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  210. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  211. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  212. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
  213. package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
  214. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  215. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  216. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
  217. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
  218. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
  219. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
  220. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +67 -31
  221. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +25 -13
  222. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
  223. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  224. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
  225. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
  226. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
  227. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
  228. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
  229. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
  230. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  231. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  232. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  233. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  234. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  235. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  236. package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
  237. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  238. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  239. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  240. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  241. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  242. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  243. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  244. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +28 -4
  245. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  246. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  247. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  248. package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
  249. package/src/runtime/channels/lib/whisper-port.mjs +29 -11
  250. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  251. package/src/runtime/computer-bridge/action-schema.mjs +2 -2
  252. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  253. package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
  254. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
  255. package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
  256. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  257. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  258. package/src/runtime/explore/explorer.mjs +93 -0
  259. package/src/runtime/explore/tool-defs.mjs +35 -0
  260. package/src/runtime/explore/tool.mjs +123 -0
  261. package/src/runtime/local-provider/server-process.mjs +2 -1
  262. package/src/runtime/media/adapters/gemini-video.mjs +4 -1
  263. package/src/runtime/media/adapters/xai-media.mjs +2 -1
  264. package/src/runtime/memory/index.mjs +4 -0
  265. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  266. package/src/runtime/memory/lib/embedding-worker.mjs +10 -3
  267. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
  268. package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
  269. package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
  270. package/src/runtime/office/design/design-art-direction.mjs +29 -27
  271. package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
  272. package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
  273. package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
  274. package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
  275. package/src/runtime/office/quality/assurance-structure-docx.mjs +4 -2
  276. package/src/runtime/office/quality/design-review.mjs +1 -5
  277. package/src/runtime/office/quality/document-brief.mjs +1 -5
  278. package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
  279. package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
  280. package/src/runtime/shared/agent-route-config.mjs +17 -2
  281. package/src/runtime/shared/atomic-file.mjs +103 -2
  282. package/src/runtime/shared/background-tasks.mjs +15 -6
  283. package/src/runtime/shared/bot-owned-work.mjs +56 -0
  284. package/src/runtime/shared/connector-accounts.mjs +5 -5
  285. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  286. package/src/runtime/shared/connector-catalog.mjs +60 -52
  287. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  288. package/src/runtime/shared/defaults-separation.mjs +1 -4
  289. package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
  290. package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
  291. package/src/runtime/shared/file-has-data.mjs +26 -2
  292. package/src/runtime/shared/file-lock.mjs +13 -15
  293. package/src/runtime/shared/git-launch.d.mts +9 -0
  294. package/src/runtime/shared/git-launch.mjs +213 -0
  295. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  296. package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
  297. package/src/runtime/shared/handoff-artifacts.mjs +18 -8
  298. package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
  299. package/src/runtime/shared/llm/cost.mjs +11 -2
  300. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  301. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  302. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  303. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  304. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  305. package/src/runtime/shared/loopback-listener.mjs +25 -2
  306. package/src/runtime/shared/model-route-chain.d.mts +7 -2
  307. package/src/runtime/shared/model-route-chain.mjs +22 -4
  308. package/src/runtime/shared/open-url.mjs +10 -0
  309. package/src/runtime/shared/provider-accounts.mjs +52 -2
  310. package/src/runtime/shared/session-activity-signal.mjs +14 -0
  311. package/src/runtime/shared/session-scratch.mjs +37 -0
  312. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  313. package/src/runtime/shared/staged-update.mjs +47 -2
  314. package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
  315. package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
  316. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  317. package/src/runtime/shared/tool-surface.mjs +113 -36
  318. package/src/runtime/shared/tool-work-units.mjs +6 -0
  319. package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
  320. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  321. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  322. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  323. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  324. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  325. package/src/runtime/shared/worktrees/contract.mjs +4 -1
  326. package/src/runtime/shared/worktrees/git.mjs +72 -0
  327. package/src/runtime/shared/worktrees/manager.mjs +285 -168
  328. package/src/runtime/shared/worktrees/test-support.mjs +6 -10
  329. package/src/runtime/tidy/install.mjs +95 -15
  330. package/src/runtime/tidy/languages.mjs +17 -15
  331. package/src/runtime/tidy/process.mjs +7 -4
  332. package/src/runtime/tidy/resolve.mjs +19 -16
  333. package/src/runtime/tidy/runners/biome.mjs +3 -3
  334. package/src/runtime/tidy/tool.mjs +1 -1
  335. package/src/runtime/tidy/worktree.mjs +10 -6
  336. package/src/runtime/visualize-bridge/client.mjs +6 -1
  337. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  338. package/src/runtime/web-search/index.mjs +1 -1
  339. package/src/session-runtime/agent-home-delivery.mjs +41 -0
  340. package/src/session-runtime/agent-task-sessions.mjs +458 -312
  341. package/src/session-runtime/agent-tool-routing.mjs +8 -41
  342. package/src/session-runtime/boot/tools.mjs +7 -1
  343. package/src/session-runtime/bot-status.mjs +107 -101
  344. package/src/session-runtime/bot-target.mjs +38 -0
  345. package/src/session-runtime/channel-config-api.mjs +6 -1
  346. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  347. package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
  348. package/src/session-runtime/conversation-import/discovery.mjs +401 -36
  349. package/src/session-runtime/conversation-import/service.mjs +35 -8
  350. package/src/session-runtime/handoff-attachments.mjs +82 -31
  351. package/src/session-runtime/held-requests.mjs +3 -1
  352. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
  353. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
  354. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  355. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  356. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
  357. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +145 -112
  358. package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
  359. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
  360. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  361. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  362. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  363. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  364. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  365. package/src/session-runtime/model-route/set-route.mjs +11 -2
  366. package/src/session-runtime/provider-auth/login.mjs +2 -2
  367. package/src/session-runtime/provider-catalog-cache.mjs +82 -36
  368. package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
  369. package/src/session-runtime/provider-models.mjs +4 -2
  370. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  371. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  372. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  373. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  374. package/src/session-runtime/resource-recall-api.mjs +6 -1
  375. package/src/session-runtime/runtime-feature-gates.mjs +5 -1
  376. package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
  377. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
  378. package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
  379. package/src/session-runtime/services/agent-tool/render.mjs +71 -17
  380. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  381. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  382. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  383. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
  384. package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
  385. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  386. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  387. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  388. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  389. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  390. package/src/session-runtime/services/provider-admin.mjs +283 -11
  391. package/src/session-runtime/session-turn-api.mjs +2 -0
  392. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  393. package/src/session-runtime/settings-system-api.mjs +1 -1
  394. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  395. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  396. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  397. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  398. package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
  399. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  400. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  401. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
  402. package/src/session-runtime/workflow-agents-api/agent-route.mjs +25 -5
  403. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
  404. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  405. package/src/session-runtime/workspace/session-view.mjs +149 -0
  406. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  407. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  408. package/src/session-runtime/workspace/transitions.mjs +108 -1041
  409. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  410. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  411. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  412. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  413. package/src/session-runtime/workspace/worktree-tool.mjs +73 -56
  414. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  415. package/src/standalone/daemon-session-control.mjs +169 -138
  416. package/src/standalone/daemon.mjs +7 -0
  417. package/src/standalone/desktop-service-registry.mjs +9 -0
  418. package/src/standalone/local-session-runtime.mjs +5 -1
  419. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  420. package/src/standalone/session-client.mjs +5 -1
  421. package/src/standalone/session-frame-stream.mjs +83 -11
  422. package/src/standalone/session-protocol.mjs +4 -0
  423. package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
  424. package/src/standalone/session-service/agent-tree.mjs +45 -9
  425. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  426. package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
  427. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  428. package/src/standalone/session-service/stored-reader.mjs +127 -40
  429. package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
  430. package/src/tui/App.jsx +154 -164
  431. package/src/tui/app/app-view/floating-panel.jsx +10 -4
  432. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  433. package/src/tui/app/clipboard.mjs +21 -10
  434. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
  435. package/src/tui/app/model-picker/route-selection.mjs +5 -6
  436. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  437. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  438. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  439. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  440. package/src/tui/app/provider-setup/oauth-actions.mjs +58 -21
  441. package/src/tui/app/provider-setup/provider-items.mjs +8 -2
  442. package/src/tui/app/provider-setup-picker.mjs +29 -18
  443. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  444. package/src/tui/app/slash-commands.mjs +1 -0
  445. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  446. package/src/tui/app/slash-dispatch.mjs +1 -1
  447. package/src/tui/app/use-btw-panel.mjs +29 -0
  448. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  449. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  450. package/src/tui/app/use-mouse-input.mjs +11 -0
  451. package/src/tui/app/use-onboarding-start.mjs +8 -3
  452. package/src/tui/app/use-panel-transition.mjs +10 -1
  453. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  454. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  455. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  456. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  457. package/src/tui/app/use-selection-focus.mjs +11 -2
  458. package/src/tui/app/use-selection-geometry.mjs +18 -9
  459. package/src/tui/app/use-selection-paint.mjs +4 -4
  460. package/src/tui/app/use-selection-stitch.mjs +3 -3
  461. package/src/tui/app/use-slash-palette.mjs +62 -0
  462. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  463. package/src/tui/app/use-transcript-activity.mjs +47 -32
  464. package/src/tui/app/use-transcript-follow.mjs +12 -4
  465. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  466. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  467. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  468. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  469. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  470. package/src/tui/app/use-transcript-window.mjs +21 -11
  471. package/src/tui/app/use-tui-ready.mjs +18 -0
  472. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  473. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  474. package/src/tui/components/Picker.jsx +27 -3
  475. package/src/tui/components/QuestionPanel.jsx +13 -4
  476. package/src/tui/components/SignInUrl.jsx +68 -0
  477. package/src/tui/components/TextEntryPanel.jsx +76 -46
  478. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  479. package/src/tui/dist/index.mjs +1963 -1361
  480. package/src/tui/session/goal-turn-state.mjs +6 -2
  481. package/src/tui/session/session-api/controls.mjs +4 -0
  482. package/src/tui/session/session-api/extensions.mjs +8 -0
  483. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  484. package/src/tui/session/session-api/intake.mjs +2 -0
  485. package/src/tui/session/session-api/integrations.mjs +2 -2
  486. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  487. package/src/tui/session/session-api/settings.mjs +2 -2
  488. package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
  489. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  490. package/src/tui/session/transcript-spill.mjs +92 -4
  491. package/src/tui/session/transcript-store.mjs +2 -0
  492. package/src/ui/statusline-agents.mjs +1 -3
  493. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -15,6 +15,13 @@ import { sanitizeForWire } from '../../session-wire-values.mjs';
15
15
  * whatever their size: a window always shows something and paging always
16
16
  * progresses. A viewport the rows do not fill pages further on its own. */
17
17
  const TRANSCRIPT_WINDOW_MIN_ITEMS = 8;
18
+ /** The most rows one answer to a paged request (one naming the rows it holds,
19
+ * `transcriptHeld`) carries, whatever window it names: a window sent whole,
20
+ * or the rows one page reveals above the ones it holds. History itself has
21
+ * no limit; it pages in steps of at most this many. */
22
+ export const TRANSCRIPT_RESPONSE_MAX_ITEMS = 512;
23
+ /** A request naming no held rows keeps its earlier contract: a window of at
24
+ * most this many rows, answered whole. */
18
25
  const TRANSCRIPT_WINDOW_MAX_ITEMS = 8_192;
19
26
  // Deterministic restore ids: hist_<sessionId>_<messageIndex>_<part>.
20
27
  const RESTORED_ITEM_ID = /^hist_.+_(\d+)_\d+$/;
@@ -31,18 +38,26 @@ function itemBytes(item) {
31
38
  }
32
39
 
33
40
  /** The window a read/subscribe asked for, or null for a legacy view.
34
- * `transcriptPageBase` names how many newest items the reader already holds:
35
- * the byte budget then bounds only the older rows revealed above them. */
41
+ * A paged request (`paged`: it names the rows it holds, `transcriptHeld`)
42
+ * names how far back it wants history, without bound; what one answer
43
+ * carries is bounded by TRANSCRIPT_RESPONSE_MAX_ITEMS and the byte budget.
44
+ * Any other request keeps its earlier contract: at most
45
+ * TRANSCRIPT_WINDOW_MAX_ITEMS rows, answered whole, and `transcriptPageBase`
46
+ * names how many newest items the reader already holds — the byte budget
47
+ * then bounds only the older rows revealed above them. */
36
48
  export function requestedTranscriptWindow(params) {
37
- const limit = Number(params?.transcriptItemLimit);
38
- if (!Number.isInteger(limit) || limit < 1) return null;
49
+ const requested = Number(params?.transcriptItemLimit);
50
+ if (!Number.isSafeInteger(requested) || requested < 1) return null;
39
51
  const budget = Number(params?.transcriptByteBudget);
52
+ const byteBudget = Number.isSafeInteger(budget) && budget > 0 ? budget : null;
53
+ if (requestedHeldTranscript(params)) return { limit: requested, byteBudget, pageBase: 0, paged: true };
54
+ const limit = Math.min(TRANSCRIPT_WINDOW_MAX_ITEMS, requested);
40
55
  const pageBase = Number(params?.transcriptPageBase);
41
- const cappedLimit = Math.min(TRANSCRIPT_WINDOW_MAX_ITEMS, limit);
42
56
  return {
43
- limit: cappedLimit,
44
- byteBudget: Number.isInteger(budget) && budget > 0 ? budget : null,
45
- pageBase: Number.isInteger(pageBase) && pageBase > 0 ? Math.min(pageBase, cappedLimit) : 0,
57
+ limit,
58
+ byteBudget,
59
+ pageBase: Number.isInteger(pageBase) && pageBase > 0 ? Math.min(pageBase, limit) : 0,
60
+ paged: false,
46
61
  };
47
62
  }
48
63
 
@@ -57,17 +72,46 @@ export function requestedHeldTranscript(params) {
57
72
  return { firstId: held.firstId, count, digest: held.digest };
58
73
  }
59
74
 
75
+ /** Whether the window's newest `held.count` rows are exactly the rows the
76
+ * caller holds: same first id and, unless the caller already proved them
77
+ * (`proven`), the same content digest. */
78
+ export function holdsTranscriptTail(items, held, proven = false) {
79
+ const start = items.length - held.count;
80
+ if (start < 0 || items[start]?.id !== held.firstId) return false;
81
+ return proven || transcriptItemsDigest(start ? items.slice(start) : items) === held.digest;
82
+ }
83
+
84
+ /** Whether rows one answer carries are within its bounds: at most
85
+ * TRANSCRIPT_RESPONSE_MAX_ITEMS, and within `byteBudget` past
86
+ * TRANSCRIPT_WINDOW_MIN_ITEMS. */
87
+ function withinAnswerBounds(items, byteBudget, maxItems = TRANSCRIPT_RESPONSE_MAX_ITEMS) {
88
+ if (items.length > maxItems) return false;
89
+ return (
90
+ !byteBudget ||
91
+ items.length <= TRANSCRIPT_WINDOW_MIN_ITEMS ||
92
+ items.reduce((bytes, item) => bytes + itemBytes(item), 0) <= byteBudget
93
+ );
94
+ }
95
+
60
96
  /** A FULL answer to a paged read, reduced to what the caller lacks: the rows
61
97
  * above the ones it holds, the identity of its first held row, and every
62
- * other field. Only when the window's tail is exactly those held rows (same
63
- * first id, count and content digest); otherwise the full answer stands. */
64
- export function transcriptPageBody(result, held) {
98
+ * other field. Only when the window's tail is exactly those held rows
99
+ * (holdsTranscriptTail; `proven` when the reader already verified them) and
100
+ * the rows above them fit one answer (withinAnswerBounds, `byteBudget`);
101
+ * otherwise the full answer stands. */
102
+ export function transcriptPageBody(result, held, { proven = false, byteBudget = null, pageOnly = false } = {}) {
65
103
  const snapshot = result?.full;
66
104
  if (!held || !snapshot || !Array.isArray(snapshot.items)) return result;
105
+ if (pageOnly) {
106
+ // Already only the (bounded) rows above the proven held ones.
107
+ const { full: _full, ...body } = result;
108
+ const { items, ...state } = snapshot;
109
+ return { ...body, page: { firstHeldId: held.firstId, heldCount: held.count, items, state } };
110
+ }
67
111
  const items = snapshot.items;
112
+ if (!holdsTranscriptTail(items, held, proven)) return result;
68
113
  const start = items.length - held.count;
69
- if (start < 0 || items[start]?.id !== held.firstId) return result;
70
- if (transcriptItemsDigest(start ? items.slice(start) : items) !== held.digest) return result;
114
+ if (!withinAnswerBounds(items.slice(0, start), byteBudget)) return result;
71
115
  const { full: _full, ...body } = result;
72
116
  const { items: _items, ...state } = snapshot;
73
117
  return {
@@ -91,25 +135,44 @@ function budgetedStart(items, end, maxItems, byteBudget, minItems) {
91
135
  return start;
92
136
  }
93
137
 
94
- /** First index of the kept tail: at most `limit` items. A first window spends
95
- * `byteBudget` on the whole tail; a page keeps the `pageBase` newest items
96
- * the reader holds and spends it on the rows revealed above them. Neither
97
- * goes below TRANSCRIPT_WINDOW_MIN_ITEMS. */
138
+ /** First index of the kept tail: at most `limit` items (never more than
139
+ * TRANSCRIPT_WINDOW_MAX_ITEMS). A first window spends `byteBudget` on the
140
+ * whole tail; a page keeps the `pageBase` newest items the reader holds and
141
+ * spends it on the rows revealed above them. Neither goes below
142
+ * TRANSCRIPT_WINDOW_MIN_ITEMS. */
98
143
  function tailWindowStart(items, { limit, byteBudget = null, pageBase = 0 }) {
99
- const held = Math.min(pageBase || 0, limit, items.length);
100
- return budgetedStart(items, items.length - held, limit - held, byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
101
- }
102
-
103
- /** A stored projection already holds at most `window.limit` items; the byte
104
- * budget may cut it further. */
105
- export function budgetStoredWindow(wire, window) {
106
- if (!window?.byteBudget || !Array.isArray(wire?.items)) return wire;
107
- const start = tailWindowStart(wire.items, {
108
- limit: wire.items.length,
109
- byteBudget: window.byteBudget,
110
- pageBase: window.pageBase,
111
- });
112
- return start === 0 ? wire : { ...wire, items: wire.items.slice(start), transcriptHasOlder: true };
144
+ const maxItems = Math.min(limit, TRANSCRIPT_WINDOW_MAX_ITEMS);
145
+ const held = Math.min(pageBase || 0, maxItems, items.length);
146
+ return budgetedStart(items, items.length - held, maxItems - held, byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
147
+ }
148
+
149
+ /** The answer a stored window (at most `window.limit` newest rows) gives. A
150
+ * paged request: the `held` newest rows the caller is proven to hold, and
151
+ * above them at most TRANSCRIPT_RESPONSE_MAX_ITEMS more within the byte
152
+ * budget — or, proving none, a tail within the same bounds. Any other
153
+ * request: the whole window, byte-budgeted above its `pageBase`. */
154
+ export function clampStoredWindow(wire, window, held = 0) {
155
+ if (!window || !Array.isArray(wire?.items)) return wire;
156
+ const items = wire.items;
157
+ let start = 0;
158
+ if (window.paged) {
159
+ const maxItems = Math.min(window.limit - held, TRANSCRIPT_RESPONSE_MAX_ITEMS);
160
+ start = budgetedStart(items, items.length - held, maxItems, window.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
161
+ } else if (window.byteBudget) {
162
+ start = tailWindowStart(items, { limit: items.length, byteBudget: window.byteBudget, pageBase: window.pageBase });
163
+ }
164
+ return start === 0 ? wire : { ...wire, items: items.slice(start), transcriptHasOlder: true };
165
+ }
166
+
167
+ /** A stored page (only the rows above the `held` ones a paged caller proved)
168
+ * cut to one answer: at most TRANSCRIPT_RESPONSE_MAX_ITEMS (and the window's
169
+ * limit) newest of them within the byte budget. */
170
+ export function clampStoredPage(wire, window, held) {
171
+ if (!Array.isArray(wire?.items)) return wire;
172
+ const items = wire.items;
173
+ const maxItems = Math.max(0, Math.min(window.limit - held, TRANSCRIPT_RESPONSE_MAX_ITEMS));
174
+ const start = budgetedStart(items, items.length, maxItems, window.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
175
+ return start === 0 ? wire : { ...wire, items: items.slice(start), transcriptHasOlder: true };
113
176
  }
114
177
 
115
178
  /** A restored runtime transcript that starts after message 0 may have older
@@ -128,11 +191,68 @@ function runtimeArchiveRange(state) {
128
191
  return Number.isInteger(after) && Number.isInteger(before) && before - 1 > after ? { after, before } : null;
129
192
  }
130
193
 
194
+ /** The rows the runtime spilled out of memory above its first row
195
+ * (`transcriptSpilled`: `{ rows, epoch }`), published with the rows
196
+ * themselves and independent of the TUI's reading cursor; null when none. */
197
+ function spilledOf(state) {
198
+ const spilled = spillStateOf(state);
199
+ return spilled && spilled.rows > 0 ? spilled : null;
200
+ }
201
+
202
+ /** The published spill state, zero rows included (null: none published). */
203
+ function spillStateOf(state) {
204
+ const spilled = state?.transcriptSpilled;
205
+ const rows = Number(spilled?.rows);
206
+ const epoch = Number(spilled?.epoch);
207
+ return Number.isSafeInteger(rows) && rows >= 0 && Number.isSafeInteger(epoch) ? { rows, epoch } : null;
208
+ }
209
+
210
+ const sameSpill = (left, right) => left?.rows === right?.rows && left?.epoch === right?.epoch;
211
+
212
+ /** Whether spilled rows sit above the runtime's first row, readable through
213
+ * its spill accessor. A loaded head answers for itself. */
214
+ function spilledAbove(entry, state, view) {
215
+ return !view.head.length && spilledOf(state) !== null && typeof entry.runtime?.readSpilledTranscript === 'function';
216
+ }
217
+
131
218
  function resetLoadedHead(view) {
132
219
  view.head = [];
133
220
  view.headOlder = null;
134
221
  view.headSource = null;
135
222
  view.archiveHeld = 0;
223
+ // Absolute index of the oldest spilled row the head holds (null: none).
224
+ view.spillFrom = null;
225
+ }
226
+
227
+ /** Older rows a spilled head leaves above itself: more spilled rows, or the
228
+ * source above the first spilled row. */
229
+ function spilledHeadOlder(view, archive) {
230
+ return view.spillFrom > 0 || (view.head.length > 0 && headSourceFor(view.head[0], archive) !== null);
231
+ }
232
+
233
+ /** The runtime spilled its oldest rows while the window showed them (its
234
+ * anchor among them): those rows join the head exactly as spilled, so the
235
+ * window keeps its rows and its place. False when the spill does not
236
+ * continue the rows this window last saw. */
237
+ function followSpilledAnchor(entry, view, wire) {
238
+ const spilled = spilledOf(wire);
239
+ const before = view.spilled;
240
+ const read = entry.runtime?.readSpilledTranscript;
241
+ if (!spilled || typeof read !== 'function' || view.start === null) return false;
242
+ // Rows spilled when the window last looked: none, or a count in this epoch.
243
+ if (before && before.epoch !== spilled.epoch) return false;
244
+ const from = (before?.rows ?? 0) + view.start;
245
+ if (from >= spilled.rows) return false;
246
+ const page = read({ epoch: spilled.epoch, end: spilled.rows, limit: spilled.rows - from });
247
+ if (!page || page.broken || page.start !== from || page.items[0]?.id !== view.anchorId) return false;
248
+ view.head = view.head.concat(sanitizeForWire(page.items));
249
+ if (view.headSource !== 'stored' && view.headSource !== 'archive') {
250
+ view.spillFrom ??= from;
251
+ view.headSource = 'spill';
252
+ view.headOlder = spilledHeadOlder(view, runtimeArchiveRange(wire));
253
+ }
254
+ view.start = 0;
255
+ return true;
136
256
  }
137
257
 
138
258
  /** Fold a read/subscribe request into a live entry's window. */
@@ -145,9 +265,11 @@ export function requestLiveTranscriptWindow(entry, request) {
145
265
  if (view?.full) return;
146
266
  if (!view) {
147
267
  entry.transcriptView = {
148
- limit: request.limit,
268
+ // Its first window goes out whole: for a paged request,
269
+ // TRANSCRIPT_RESPONSE_MAX_ITEMS at most.
270
+ limit: request.paged ? Math.min(request.limit, TRANSCRIPT_RESPONSE_MAX_ITEMS) : request.limit,
149
271
  byteBudget: request.byteBudget,
150
- pageBase: request.pageBase,
272
+ pageBase: request.paged ? 0 : request.pageBase || 0,
151
273
  start: null,
152
274
  anchorId: undefined,
153
275
  grow: false,
@@ -156,16 +278,29 @@ export function requestLiveTranscriptWindow(entry, request) {
156
278
  paged: false,
157
279
  head: [],
158
280
  headOlder: null,
159
- // Where the loaded head came from: 'stored' (durable history aligned by
160
- // restore ids) or 'archive' (compaction-archive pages).
281
+ // Where the loaded head came from: 'spill' (the rows the runtime spilled
282
+ // out of memory, exactly), 'stored' (durable history aligned by restore
283
+ // ids) or 'archive' (compaction-archive pages).
161
284
  headSource: null,
162
285
  archiveHeld: 0,
286
+ spillFrom: null,
287
+ // The spilled rows (`{ rows, epoch }`) when the window last looked.
288
+ spilled: null,
289
+ // Rows the window last showed.
290
+ size: 0,
163
291
  cache: null,
164
292
  };
165
293
  return;
166
294
  }
167
- if (request.limit > view.limit) {
168
- view.limit = request.limit;
295
+ if (request.limit > view.limit && view.limit < TRANSCRIPT_WINDOW_MAX_ITEMS) {
296
+ // One paged request reveals at most TRANSCRIPT_RESPONSE_MAX_ITEMS older
297
+ // rows; any other names at most TRANSCRIPT_WINDOW_MAX_ITEMS. The shared
298
+ // window never grows past TRANSCRIPT_WINDOW_MAX_ITEMS: deeper history is
299
+ // a historical-position read (readTranscriptHistory), which no view shares.
300
+ view.limit = Math.min(
301
+ TRANSCRIPT_WINDOW_MAX_ITEMS,
302
+ request.paged ? Math.min(request.limit, view.size + TRANSCRIPT_RESPONSE_MAX_ITEMS) : request.limit
303
+ );
169
304
  // The page's own budget bounds the rows it reveals.
170
305
  view.grow = { byteBudget: request.byteBudget };
171
306
  view.paged = true;
@@ -188,6 +323,36 @@ export function inheritTranscriptWindow(entry, view) {
188
323
  );
189
324
  }
190
325
 
326
+ /** The most rows the answer to `window` (requestedTranscriptWindow) carries
327
+ * whole, and its byte bound: a paged request's TRANSCRIPT_RESPONSE_MAX_ITEMS
328
+ * within its budget, any other's TRANSCRIPT_WINDOW_MAX_ITEMS. */
329
+ export function transcriptAnswerBounds(window) {
330
+ return window?.paged
331
+ ? { maxItems: TRANSCRIPT_RESPONSE_MAX_ITEMS, byteBudget: window.byteBudget }
332
+ : { maxItems: TRANSCRIPT_WINDOW_MAX_ITEMS, byteBudget: null };
333
+ }
334
+
335
+ /** Whether `snapshot`, the entry's window about to go out whole, exceeds
336
+ * `bounds` (transcriptAnswerBounds). A legacy whole view has no window and
337
+ * never does. */
338
+ export function transcriptWindowOversized(entry, snapshot, bounds) {
339
+ const view = entry.transcriptView;
340
+ const items = snapshot?.items;
341
+ if (!view || view.full || !Array.isArray(items)) return false;
342
+ return !withinAnswerBounds(items, bounds.byteBudget, bounds.maxItems);
343
+ }
344
+
345
+ /** The window restarts from a fresh tail of at most `maxItems` rows at its
346
+ * next snapshot, as a first window: its readers page older rows again. */
347
+ export function restartTranscriptWindow(entry, maxItems) {
348
+ const view = entry.transcriptView;
349
+ if (!view || view.full) return;
350
+ view.start = null;
351
+ view.paged = true;
352
+ view.limit = Math.min(view.limit, maxItems);
353
+ view.pageBase = 0;
354
+ }
355
+
191
356
  /** The entry's wire snapshot seen through its window. Identity-stable for an
192
357
  * unchanged source and window, so revision steps still detect "unchanged". */
193
358
  export function windowTranscriptSnapshot(entry, wire) {
@@ -195,17 +360,34 @@ export function windowTranscriptSnapshot(entry, wire) {
195
360
  if (!view || view.full || !wire || !Array.isArray(wire.items)) return wire;
196
361
  const items = wire.items;
197
362
  let start = view.start;
363
+ const spill = spillStateOf(wire);
364
+ // The spill store was reset or restored (another epoch, or no spill state):
365
+ // spilled and older rows the window loaded name another page list, so it
366
+ // restarts from the tail whether or not its anchor is still live.
367
+ if (view.spilled && spill?.epoch !== view.spilled.epoch) start = null;
198
368
  if (start !== null && view.anchorId !== undefined && items[start]?.id !== view.anchorId) {
199
- // The runtime rewrote its transcript before the anchor (compaction,
200
- // rewind, clear): follow the anchor, or restart from the tail.
201
369
  const moved = items.findIndex((item) => item?.id === view.anchorId);
202
- start = moved < 0 ? null : moved;
203
- resetLoadedHead(view);
370
+ if (moved < 0 && followSpilledAnchor(entry, view, wire)) {
371
+ // The runtime spilled the rows the window starts with: they are head now.
372
+ start = 0;
373
+ } else {
374
+ // The runtime rewrote its transcript before the anchor (compaction,
375
+ // rewind, clear): follow the anchor, or restart from the tail.
376
+ start = moved < 0 ? null : moved;
377
+ resetLoadedHead(view);
378
+ }
204
379
  }
205
380
  if (start !== null && start > items.length) start = null;
206
381
  if (start === null) {
207
382
  start = tailWindowStart(items, view);
208
383
  resetLoadedHead(view);
384
+ if (view.paged) {
385
+ // A paged window restarted from a fresh tail is a first window again:
386
+ // its limit is what it shows, so the reader's next page grows it.
387
+ view.paged = false;
388
+ view.grow = false;
389
+ view.limit = items.length - start;
390
+ }
209
391
  }
210
392
  // The sticky start lets appends travel as suffix patches, so an unpaged
211
393
  // tail grows past its byte budget while views stay attached (a worker
@@ -223,10 +405,19 @@ export function windowTranscriptSnapshot(entry, wire) {
223
405
  start = budgetedStart(items, start, view.limit - held, view.grow.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
224
406
  view.grow = false;
225
407
  }
408
+ if (!view.private && view.head.length + items.length - start > TRANSCRIPT_WINDOW_MAX_ITEMS) {
409
+ // Appends under a sticky start outgrew the shared window: its oldest rows
410
+ // go (a page's worth below the bound, so this does not recur per append).
411
+ resetLoadedHead(view);
412
+ start = Math.max(start, items.length - (TRANSCRIPT_WINDOW_MAX_ITEMS - TRANSCRIPT_RESPONSE_MAX_ITEMS));
413
+ }
226
414
  view.start = start;
227
415
  view.anchorId = items[start]?.id;
416
+ view.spilled = spill;
228
417
  const older =
229
- start > 0 || (view.headOlder ?? headSourceFor(view.head[0] ?? items[0], runtimeArchiveRange(wire)) !== null);
418
+ start > 0 ||
419
+ (view.headOlder ??
420
+ headSourceFor(view.head[0] ?? items[0], runtimeArchiveRange(wire), spilledAbove(entry, wire, view)) !== null);
230
421
  const cache = view.cache;
231
422
  if (cache && cache.wire === wire && cache.start === start && cache.head === view.head && cache.older === older) {
232
423
  return cache.value;
@@ -236,14 +427,17 @@ export function windowTranscriptSnapshot(entry, wire) {
236
427
  else if (start) windowed = items.slice(start);
237
428
  const value = { ...wire, items: windowed, transcriptHasOlder: older };
238
429
  view.cache = { wire, start, head: view.head, older, value };
430
+ view.size = windowed.length;
239
431
  return value;
240
432
  }
241
433
 
242
- /** Where older rows above `first` come from: durable history aligned by
243
- * restore ids (which continues into the compaction archive), or — once the
244
- * runtime's rows are not restored ones (a live compaction archived them, or
245
- * nothing was restored) — compaction-archive pages alone. */
246
- function headSourceFor(first, archive) {
434
+ /** Where older rows above `first` come from: the rows the runtime spilled out
435
+ * of memory above it (`spilled`), then durable history aligned by restore
436
+ * ids (which continues into the compaction archive), or — once the runtime's
437
+ * rows are not restored ones (a live compaction archived them, or nothing was
438
+ * restored) — compaction-archive pages alone. */
439
+ function headSourceFor(first, archive, spilled = false) {
440
+ if (spilled) return 'spill';
247
441
  const restoredId = RESTORED_ITEM_ID.test(String(first?.id ?? ''));
248
442
  if (restoredId && first?.archived !== true) return restoredAfterFirstMessage(first) || archive ? 'stored' : null;
249
443
  if (archive) return 'archive';
@@ -324,11 +518,12 @@ function verifiedIdentityIndex(history, first, live) {
324
518
  }
325
519
 
326
520
  /** Load history OLDER than the runtime's first item when a grown window
327
- * reaches past what the runtime holds (a resumed runtime restores only a
328
- * bounded tail; a compaction moved earlier rows to the display-only
329
- * archive). Durable items align by their deterministic restore ids; archive
330
- * pages continue below the archived rows already held. Returns whether
331
- * durable history was read. */
521
+ * reaches past what the runtime holds (the runtime spilled its oldest rows
522
+ * out of memory; a resumed runtime restores only a bounded tail; a compaction
523
+ * moved earlier rows to the display-only archive). Spilled rows come first,
524
+ * exactly as spilled; then durable items align by their deterministic
525
+ * restore ids; archive pages continue below the archived rows already held.
526
+ * Returns whether durable history was read. */
332
527
  export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession) {
333
528
  const view = entry.transcriptView;
334
529
  if (!view || view.full || !view.paged || typeof readStoredSession !== 'function' || view.headOlder === false) {
@@ -340,12 +535,19 @@ export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession
340
535
  const state = entry.runtime?.getState?.();
341
536
  const runtimeItems = Array.isArray(state?.items) ? state.items : [];
342
537
  const missing = view.limit - runtimeItems.length - view.head.length;
538
+ // The rows were spilled since the window last looked: its next snapshot
539
+ // moves them into the head first, so spilled indices stay its own.
540
+ if (!sameSpill(spillStateOf(state), view.spilled)) return false;
541
+ const spilled = spilledOf(state);
343
542
  const first = view.head[0] ?? runtimeItems[0];
344
543
  const archive = runtimeArchiveRange(state);
345
- const source = view.headSource || headSourceFor(first, archive);
544
+ const spilling = view.headSource === 'spill' ? view.spillFrom > 0 : spilledAbove(entry, state, view);
545
+ const source = spilling ? 'spill' : (view.headSource !== 'spill' && view.headSource) || headSourceFor(first, archive);
346
546
  if (missing <= 0 || !source) return false;
347
547
  const generation = compactionGeneration(runtimeItems);
348
- if (view.head.length && view.headGeneration !== generation) {
548
+ // Spilled rows are the runtime's own, joined exactly whatever compacted
549
+ // since; a durable head is valid only for its generation.
550
+ if (view.head.length && view.headSource !== 'spill' && view.headGeneration !== generation) {
349
551
  // A compaction since the head was loaded: keep the rows as they are, and
350
552
  // never join older rows across the generation.
351
553
  view.headOlder = false;
@@ -357,9 +559,26 @@ export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession
357
559
  entry.transcriptView === view &&
358
560
  (view.head[0] ?? currentItems()?.[0]) === first &&
359
561
  compactionGeneration(Array.isArray(currentItems()) ? currentItems() : []) === generation;
562
+ if (source === 'spill') {
563
+ // Synchronous: the rows read are exactly the ones above `first` now.
564
+ const end = view.spillFrom ?? spilled.rows;
565
+ const page = entry.runtime.readSpilledTranscript({ epoch: spilled.epoch, end, limit: missing });
566
+ if (!page) return false;
567
+ const rows = page.items;
568
+ const from = budgetedStart(rows, rows.length, missing, pageBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
569
+ view.head = sanitizeForWire(rows.slice(from)).concat(view.head);
570
+ view.spillFrom = end - (rows.length - from);
571
+ view.headSource = 'spill';
572
+ // An unreadable page ends the history there: nothing above it joins
573
+ // without a gap. Past the spilled rows, the first of them names the next
574
+ // source.
575
+ view.headOlder = page.broken && from === 0 ? false : spilledHeadOlder(view, archive);
576
+ return false;
577
+ }
360
578
  if (source === 'archive') {
361
579
  const page = await readStoredSession(sessionId, {
362
580
  compactionArchive: { ...archive, skip: view.archiveHeld || 0, limit: missing },
581
+ paging: true,
363
582
  });
364
583
  if (!stillCurrent()) return true;
365
584
  const rows = Array.isArray(page?.items) ? page.items : [];
@@ -371,10 +590,15 @@ export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession
371
590
  view.headOlder = from > 0 || page?.hasOlder === true;
372
591
  return true;
373
592
  }
374
- const stored = await readStoredSession(sessionId, { transcriptItemLimit: view.limit + runtimeItems.length });
593
+ const stored = await readStoredSession(sessionId, {
594
+ transcriptItemLimit: view.limit + runtimeItems.length,
595
+ paging: true,
596
+ });
375
597
  if (!stillCurrent()) return true;
376
598
  const history = Array.isArray(stored?.items) ? stored.items : [];
377
- const overlap = alignedHistoryIndex(history, first, view.head.concat(runtimeItems), view.head.length > 0);
599
+ // Only a head read from this durable history aligns by its ids; spilled
600
+ // rows are live rows like the runtime's own.
601
+ const overlap = alignedHistoryIndex(history, first, view.head.concat(runtimeItems), view.headSource === 'stored');
378
602
  if (overlap < 0) {
379
603
  view.headOlder = false;
380
604
  return true;
@@ -387,6 +611,402 @@ export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession
387
611
  return true;
388
612
  }
389
613
 
614
+ /** The newest `requested` rows of a live session through a private window
615
+ * (never the entry's own, which its views share): spilled rows, durable
616
+ * history and archive load above the runtime's rows as for a view. */
617
+ async function isolatedLiveRows(entry, sessionId, readStoredSession, requested) {
618
+ const state = entry.runtime.getState();
619
+ const isolated = {
620
+ runtime: { getState: () => state, readSpilledTranscript: entry.runtime.readSpilledTranscript },
621
+ };
622
+ requestLiveTranscriptWindow(isolated, { limit: requested, byteBudget: null, pageBase: 0 });
623
+ const view = isolated.transcriptView;
624
+ // Not shared: the shared window's row cap does not apply.
625
+ view.private = true;
626
+ windowTranscriptSnapshot(isolated, state);
627
+ view.limit = requested;
628
+ view.paged = true;
629
+ view.grow = { byteBudget: null };
630
+ let snapshot = windowTranscriptSnapshot(isolated, state);
631
+ // One load reads one source (spilled rows, durable history, archive): keep
632
+ // loading until the rows are covered, history ends, or a load adds nothing.
633
+ while (snapshot.items.length < requested && snapshot.transcriptHasOlder) {
634
+ const held = snapshot.items.length;
635
+ await loadLiveTranscriptHead(isolated, sessionId, readStoredSession);
636
+ snapshot = windowTranscriptSnapshot(isolated, state);
637
+ if (snapshot.items.length <= held) break;
638
+ }
639
+ privateViews.set(snapshot, view);
640
+ return snapshot;
641
+ }
642
+
643
+ /** The private window each isolatedLiveRows snapshot was read through. */
644
+ const privateViews = new WeakMap();
645
+
646
+ /** Positions (see HISTORY_POSITION) of rows `[from, to)` of a private
647
+ * window's snapshot: its loaded head is archive rows (when it reached them)
648
+ * then spilled rows from `spillFrom`; other rows need none. */
649
+ function privateRowPositions(snapshot, state, from, to) {
650
+ const view = privateViews.get(snapshot);
651
+ const spilled = spilledOf(state);
652
+ const archive = runtimeArchiveRange(state);
653
+ const head = view?.head.length ?? 0;
654
+ const spillRows = view && view.spillFrom !== null && spilled ? spilled.rows - view.spillFrom : 0;
655
+ const archiveRows = view?.headSource === 'archive' ? head - spillRows : 0;
656
+ const positions = [];
657
+ for (let index = from; index < to; index += 1) {
658
+ if (index < archiveRows && archive) positions.push(archivePosition(archive, archiveRows - 1 - index));
659
+ else if (index >= head - spillRows && index < head && spilled) {
660
+ positions.push(spillPosition(spilled.epoch, view.spillFrom + index - (head - spillRows)));
661
+ } else positions.push(null);
662
+ }
663
+ return positions;
664
+ }
665
+
666
+ /** The most rows one historical-position read carries. */
667
+ export const TRANSCRIPT_HISTORY_MAX_ITEMS = TRANSCRIPT_RESPONSE_MAX_ITEMS;
668
+
669
+ /** A historical-position read (`transcriptHistory`: `{ before | after: row
670
+ * id, limit, byteBudget? }`), validated; null when the params name none. */
671
+ export function requestedTranscriptHistory(params) {
672
+ const request = params?.transcriptHistory;
673
+ if (request == null) return null;
674
+ const limit = Number(request?.limit);
675
+ const older = request?.before != null;
676
+ const id = older ? request.before : request?.after;
677
+ const budget = Number(request?.byteBudget);
678
+ if (
679
+ (typeof id !== 'string' && typeof id !== 'number') ||
680
+ (older && request.after != null) ||
681
+ !Number.isInteger(limit) ||
682
+ limit < 1 ||
683
+ limit > TRANSCRIPT_HISTORY_MAX_ITEMS
684
+ ) {
685
+ throw new TypeError('invalid transcript history request');
686
+ }
687
+ const position = request.position;
688
+ if (position != null && (typeof position !== 'string' || !HISTORY_POSITION.test(position))) {
689
+ throw new TypeError('invalid transcript history request');
690
+ }
691
+ return {
692
+ ...(older ? { before: id } : { after: id }),
693
+ limit,
694
+ byteBudget: Number.isSafeInteger(budget) && budget > 0 ? budget : null,
695
+ ...(position != null ? { position } : {}),
696
+ };
697
+ }
698
+
699
+ const historyAnswer = (sessionId, items, hasOlder, hasNewer, positions = null) => ({
700
+ sessionId,
701
+ items: sanitizeForWire(items),
702
+ hasOlder: hasOlder === true,
703
+ hasNewer: hasNewer === true,
704
+ ...(positions?.some((position) => position !== null) ? { positions } : {}),
705
+ });
706
+
707
+ /** Where a row of a live session's history sits in a store addressed by
708
+ * position — `s:<epoch>:<index>`: a spilled row (absolute index in that spill
709
+ * epoch); `a:<after>:<before>:<skip>`: a compaction-archive row (`skip` rows
710
+ * newer than it in that archive range). An answer names its rows' positions
711
+ * (`positions`) and a request may name its boundary row's (`position`): the
712
+ * row is then read where it is — one page, verified by id — never searched
713
+ * for. Rows of the runtime and of durable history need none (found by id). */
714
+ const HISTORY_POSITION = /^(?:s:\d{1,15}:\d{1,15}|a:\d{1,15}:\d{1,15}:\d{1,15})$/;
715
+
716
+ function parseHistoryPosition(position) {
717
+ if (typeof position !== 'string' || !HISTORY_POSITION.test(position)) return null;
718
+ const [kind, ...numbers] = position.split(':');
719
+ const values = numbers.map(Number);
720
+ return kind === 's'
721
+ ? { kind, epoch: values[0], index: values[1] }
722
+ : { kind, after: values[0], before: values[1], skip: values[2] };
723
+ }
724
+
725
+ const spillPosition = (epoch, index) => `s:${epoch}:${index}`;
726
+ const archivePosition = (range, skip) => `a:${range.after}:${range.before}:${skip}`;
727
+
728
+ /** Older (`before`) or newer rows than one positioned row, read where they
729
+ * are: spilled rows by absolute index, archive rows by skip, past either end
730
+ * into the next source (spill → archive or durable history above it; archive
731
+ * → spill or runtime below it). Null when the position no longer names that
732
+ * row (another epoch or range, or another row there): the caller looks up
733
+ * the row instead. Every read is one bounded page. */
734
+ async function readAtHistoryPosition(entry, state, sessionId, readStoredSession, request, older, id, position) {
735
+ const items = Array.isArray(state?.items) ? state.items : [];
736
+ const spilled = spilledOf(state);
737
+ const readSpill =
738
+ typeof entry.runtime?.readSpilledTranscript === 'function'
739
+ ? (range) => entry.runtime.readSpilledTranscript(range)
740
+ : null;
741
+ const archive = runtimeArchiveRange(state);
742
+ const readArchive = async (skip, limit) => {
743
+ if (!archive || limit <= 0 || typeof readStoredSession !== 'function') return null;
744
+ const page = await readStoredSession(sessionId, { compactionArchive: { ...archive, skip, limit }, paging: true });
745
+ return Array.isArray(page?.items) ? page : null;
746
+ };
747
+ const { limit, byteBudget } = request;
748
+ // Rows below the archive or the spill: the spill's first rows, else the runtime's.
749
+ const newerFromSpillStart = () => {
750
+ if (spilled && readSpill) {
751
+ const page = readSpill({ epoch: spilled.epoch, end: Math.min(spilled.rows, limit), limit });
752
+ // An unreadable spill page is a gap: never skipped to the runtime's rows.
753
+ if (!page || page.broken || page.start !== 0) return historyReset(sessionId);
754
+ const end = budgetedEnd(page.items, 0, limit, byteBudget);
755
+ const rows = page.items.slice(0, end);
756
+ return historyAnswer(
757
+ sessionId,
758
+ rows,
759
+ true,
760
+ true,
761
+ rows.map((_, i) => spillPosition(spilled.epoch, i))
762
+ );
763
+ }
764
+ const end = budgetedEnd(items, 0, limit, byteBudget);
765
+ return historyAnswer(sessionId, items.slice(0, end), true, end < items.length);
766
+ };
767
+ // Rows above the spill's first row (or the runtime's, when nothing spilled).
768
+ const olderThanSpillStart = async (first) => {
769
+ const source = headSourceFor(first, archive);
770
+ if (source === 'archive') return olderFromArchive(-1);
771
+ if (source === 'stored' && typeof readStoredSession === 'function' && first?.id != null) {
772
+ const page = await readStoredSession(sessionId, {
773
+ transcriptCursor: { before: first.id, maxRows: limit, byteBudget },
774
+ paging: true,
775
+ });
776
+ if (page && !page.cursorMissing && typeof page.transcriptHasNewer === 'boolean') {
777
+ return historyAnswer(sessionId, page.items, page.transcriptHasOlder, true);
778
+ }
779
+ }
780
+ return historyAnswer(sessionId, [], false, true);
781
+ };
782
+ // Archive rows older than the one `skip` rows from its newest (-1: from the newest).
783
+ const olderFromArchive = async (skip) => {
784
+ const page = await readArchive(skip + 1, limit);
785
+ if (!page) return historyAnswer(sessionId, [], false, true);
786
+ const start = budgetedStart(page.items, page.items.length, limit, byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
787
+ const rows = page.items.slice(start);
788
+ const positions = rows.map((_, i) => archivePosition(archive, skip + rows.length - i));
789
+ return historyAnswer(sessionId, rows, start > 0 || page.hasOlder === true, true, positions);
790
+ };
791
+ // Spilled rows ending just above index `end`.
792
+ const olderFromSpill = (end) => {
793
+ const page = readSpill({ epoch: spilled.epoch, end, limit });
794
+ if (!page || page.broken) return null;
795
+ const start = budgetedStart(page.items, page.items.length, limit, byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
796
+ const first = page.start + start;
797
+ const rows = page.items.slice(start);
798
+ const above = first > 0 || headSourceFor(rows[0], archive) !== null;
799
+ return historyAnswer(
800
+ sessionId,
801
+ rows,
802
+ above,
803
+ true,
804
+ rows.map((_, i) => spillPosition(spilled.epoch, first + i))
805
+ );
806
+ };
807
+ if (position.kind === 'runtime-start') {
808
+ // Older than the runtime's first row: the spill's last rows, else what
809
+ // lies above the runtime.
810
+ if (spilled && readSpill) return olderFromSpill(spilled.rows);
811
+ return olderThanSpillStart(items[0]);
812
+ }
813
+ if (position.kind === 's') {
814
+ if (!spilled || !readSpill || position.epoch !== spilled.epoch || position.index >= spilled.rows) return null;
815
+ const at = readSpill({ epoch: spilled.epoch, end: position.index + 1, limit: 1 });
816
+ if (!at || at.broken || at.items[0]?.id !== id) return null;
817
+ if (older) return position.index === 0 ? olderThanSpillStart(at.items[0]) : olderFromSpill(position.index);
818
+ const from = position.index + 1;
819
+ if (from >= spilled.rows) {
820
+ const end = budgetedEnd(items, 0, limit, byteBudget);
821
+ return historyAnswer(sessionId, items.slice(0, end), true, end < items.length);
822
+ }
823
+ const page = readSpill({ epoch: spilled.epoch, end: Math.min(spilled.rows, from + limit), limit });
824
+ if (!page || page.broken || page.start !== from) return null;
825
+ const end = budgetedEnd(page.items, 0, limit, byteBudget);
826
+ const rows = page.items.slice(0, end);
827
+ return historyAnswer(
828
+ sessionId,
829
+ rows,
830
+ true,
831
+ true,
832
+ rows.map((_, i) => spillPosition(spilled.epoch, from + i))
833
+ );
834
+ }
835
+ if (!archive || position.after !== archive.after || position.before !== archive.before) return null;
836
+ const at = await readArchive(position.skip, 1);
837
+ if (!at || at.items.at(-1)?.id !== id) return null;
838
+ if (older) return olderFromArchive(position.skip);
839
+ if (position.skip === 0) return newerFromSpillStart();
840
+ const take = Math.min(limit, position.skip);
841
+ const page = await readArchive(position.skip - take, take);
842
+ if (!page) return null;
843
+ const end = budgetedEnd(page.items, 0, limit, byteBudget);
844
+ const rows = page.items.slice(0, end);
845
+ return historyAnswer(
846
+ sessionId,
847
+ rows,
848
+ true,
849
+ true,
850
+ rows.map((_, i) => archivePosition(archive, position.skip - 1 - i))
851
+ );
852
+ }
853
+ const historyReset = (sessionId) => ({ sessionId, items: [], hasOlder: false, hasNewer: false, reset: true });
854
+
855
+ /** Rows next to one row of the session's whole history — the rows directly
856
+ * older than `before`, or newer than `after` — at most `limit` within
857
+ * `byteBudget` (never fewer than TRANSCRIPT_WINDOW_MIN_ITEMS), with whether
858
+ * more exist each way. Stateless and read-only: no view's window, baseline or
859
+ * revision moves, so a reader deep in history never grows what live views
860
+ * share. Rows are found by their stable ids (append-only history keeps
861
+ * them); a row no longer there answers `reset` and the reader returns to the
862
+ * live tail. */
863
+ export async function readTranscriptHistory(entry, sessionId, readStoredSession, request) {
864
+ const older = request.before != null;
865
+ const id = older ? request.before : request.after;
866
+ const stored = async (rowId, before) => {
867
+ if (typeof readStoredSession !== 'function') return null;
868
+ const page = await readStoredSession(sessionId, {
869
+ transcriptCursor: {
870
+ ...(before ? { before: rowId } : { after: rowId }),
871
+ maxRows: request.limit,
872
+ byteBudget: request.byteBudget,
873
+ },
874
+ paging: true,
875
+ });
876
+ // A reader without historical positions answers no cursor fields.
877
+ if (!page || page.cursorMissing || typeof page.transcriptHasNewer !== 'boolean') return null;
878
+ return page;
879
+ };
880
+ if (!entry) {
881
+ const page = await stored(id, older);
882
+ return page
883
+ ? historyAnswer(sessionId, page.items, page.transcriptHasOlder, page.transcriptHasNewer)
884
+ : historyReset(sessionId);
885
+ }
886
+ const state = entry.runtime.getState();
887
+ const items = Array.isArray(state?.items) ? state.items : [];
888
+ const at = items.findIndex((item) => item?.id === id);
889
+ const spilled = spilledOf(state) !== null && typeof entry.runtime?.readSpilledTranscript === 'function';
890
+ const olderSource = headSourceFor(items[0], runtimeArchiveRange(state), spilled);
891
+ if (older && at > 0) {
892
+ const start = budgetedStart(items, at, request.limit, request.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
893
+ return historyAnswer(sessionId, items.slice(start, at), start > 0 || olderSource !== null, true);
894
+ }
895
+ if (!older && at >= 0) {
896
+ const end = budgetedEnd(items, at + 1, request.limit, request.byteBudget);
897
+ return historyAnswer(sessionId, items.slice(at + 1, end), true, end < items.length);
898
+ }
899
+ // A row whose position the reader names (spilled or archived) is read
900
+ // where it is, and so is what lies above the runtime's first row.
901
+ const position = parseHistoryPosition(request.position);
902
+ if (position) {
903
+ const answer = await readAtHistoryPosition(
904
+ entry,
905
+ state,
906
+ sessionId,
907
+ readStoredSession,
908
+ request,
909
+ older,
910
+ id,
911
+ position
912
+ );
913
+ if (answer) return answer;
914
+ }
915
+ if (older && at === 0 && (olderSource === 'spill' || olderSource === 'archive')) {
916
+ const answer = await readAtHistoryPosition(entry, state, sessionId, readStoredSession, request, true, id, {
917
+ kind: 'runtime-start',
918
+ });
919
+ if (answer) return answer;
920
+ }
921
+ // Above the runtime's rows. Durable history aligns with restored rows by
922
+ // their ids while no compaction rewrote them: read it there directly.
923
+ if (olderSource === 'stored' && !items.some(isCompactionMark)) {
924
+ const page = await stored(older && at === 0 ? items[0].id : id, older);
925
+ if (!page) return historyReset(sessionId);
926
+ if (older) return historyAnswer(sessionId, page.items, page.transcriptHasOlder, true);
927
+ // Newer rows run into the runtime's own rows: continue there.
928
+ const joined = page.items.findIndex((item) => item?.id === items[0]?.id);
929
+ let rows = joined >= 0 ? page.items.slice(0, joined) : page.items;
930
+ if (rows.length === 0 && (joined >= 0 || !page.transcriptHasNewer)) {
931
+ rows = items.slice(0, budgetedEnd(items, 0, request.limit, request.byteBudget));
932
+ return historyAnswer(sessionId, rows, true, rows.length < items.length);
933
+ }
934
+ return historyAnswer(sessionId, rows, true, true);
935
+ }
936
+ // Spilled or archived rows (a live compaction): a private window loaded
937
+ // down to the row, deeper on each pass — but never more than
938
+ // TRANSCRIPT_HISTORY_LOOKUP_MAX_ITEMS rows above the runtime's own, nor on
939
+ // after a pass that loaded nothing new. Past that the row answers `reset`:
940
+ // no lookup here scales with the history's depth.
941
+ const bound = items.length + TRANSCRIPT_HISTORY_LOOKUP_MAX_ITEMS;
942
+ let requested = Math.min(bound, items.length + Math.max(request.limit * 2, 64));
943
+ let loaded = -1;
944
+ for (;;) {
945
+ const snapshot = await isolatedLiveRows(entry, sessionId, readStoredSession, requested);
946
+ const rows = snapshot.items;
947
+ const index = rows.findIndex((item) => item?.id === id);
948
+ // Found where a position names it: the page is read there.
949
+ const [own] = index >= 0 ? privateRowPositions(snapshot, state, index, index + 1) : [];
950
+ if (own) {
951
+ const answer = await readAtHistoryPosition(
952
+ entry,
953
+ state,
954
+ sessionId,
955
+ readStoredSession,
956
+ request,
957
+ older,
958
+ id,
959
+ parseHistoryPosition(own)
960
+ );
961
+ if (answer) return answer;
962
+ }
963
+ if (index >= 0 && (!older || index >= request.limit || !snapshot.transcriptHasOlder || requested >= bound)) {
964
+ if (older) {
965
+ const start = budgetedStart(rows, index, request.limit, request.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
966
+ return historyAnswer(
967
+ sessionId,
968
+ rows.slice(start, index),
969
+ start > 0 || snapshot.transcriptHasOlder,
970
+ true,
971
+ privateRowPositions(snapshot, state, start, index)
972
+ );
973
+ }
974
+ const end = budgetedEnd(rows, index + 1, request.limit, request.byteBudget);
975
+ return historyAnswer(
976
+ sessionId,
977
+ rows.slice(index + 1, end),
978
+ true,
979
+ end < rows.length,
980
+ privateRowPositions(snapshot, state, index + 1, end)
981
+ );
982
+ }
983
+ if (!snapshot.transcriptHasOlder || rows.length < requested || rows.length <= loaded || requested >= bound) {
984
+ return historyReset(sessionId);
985
+ }
986
+ loaded = rows.length;
987
+ requested = Math.min(bound, requested * 2);
988
+ }
989
+ }
990
+
991
+ /** The most rows a live historical-position lookup loads above the runtime's
992
+ * own (spilled or archived ones), as the stored reader's own bound. */
993
+ const TRANSCRIPT_HISTORY_LOOKUP_MAX_ITEMS = TRANSCRIPT_WINDOW_MAX_ITEMS;
994
+
995
+ /** Last index (exclusive) of the rows kept from `start`: at most `maxItems`
996
+ * and, past TRANSCRIPT_WINDOW_MIN_ITEMS, within `byteBudget`. */
997
+ function budgetedEnd(items, start, maxItems, byteBudget) {
998
+ let end = start;
999
+ let bytes = 0;
1000
+ while (end < items.length && end - start < maxItems) {
1001
+ const next = itemBytes(items[end]);
1002
+ if (byteBudget && end - start >= Math.min(maxItems, TRANSCRIPT_WINDOW_MIN_ITEMS) && bytes + next > byteBudget)
1003
+ break;
1004
+ bytes += next;
1005
+ end += 1;
1006
+ }
1007
+ return end;
1008
+ }
1009
+
390
1010
  /** A one-shot range must not grow or rebudget an attached desktop's window. */
391
1011
  export async function readTranscriptRange(entry, sessionId, readStoredSession, { offset, limit }) {
392
1012
  if (
@@ -401,15 +1021,31 @@ export async function readTranscriptRange(entry, sessionId, readStoredSession, {
401
1021
  const requested = offset + limit;
402
1022
  let snapshot;
403
1023
  if (entry) {
404
- const state = entry.runtime.getState();
405
- const isolated = { runtime: { getState: () => state } };
406
- requestLiveTranscriptWindow(isolated, { limit: requested, byteBudget: null, pageBase: 0 });
407
- windowTranscriptSnapshot(isolated, state);
408
- isolated.transcriptView.paged = true;
409
- await loadLiveTranscriptHead(isolated, sessionId, readStoredSession);
410
- snapshot = windowTranscriptSnapshot(isolated, state);
1024
+ snapshot = await isolatedLiveRows(entry, sessionId, readStoredSession, requested);
411
1025
  } else {
412
- snapshot = await readStoredSession(sessionId, { transcriptItemLimit: requested });
1026
+ // Only the range itself is sliced from the stored history.
1027
+ const slice = await readStoredSession(sessionId, {
1028
+ transcriptItemLimit: requested,
1029
+ transcriptSkip: offset,
1030
+ paging: true,
1031
+ });
1032
+ if (!slice) throw new Error(`session ${sessionId} is not available`);
1033
+ const items = slice.items || [];
1034
+ // A reader that ignores `transcriptSkip` answers the whole window — and
1035
+ // one that holds less than the range reaches says so, rather than an
1036
+ // empty page that never advances.
1037
+ const honored = Object.hasOwn(slice, 'skippedFirstId');
1038
+ if (!honored && offset >= items.length && slice.transcriptHasOlder) {
1039
+ throw new Error('session: requested history is not available in the retained projection');
1040
+ }
1041
+ const end = honored ? items.length : Math.max(0, items.length - offset);
1042
+ const start = Math.max(0, end - limit);
1043
+ return {
1044
+ sessionId,
1045
+ offset,
1046
+ items: sanitizeForWire(items.slice(start, end)),
1047
+ hasOlder: start > 0 || slice.transcriptHasOlder === true,
1048
+ };
413
1049
  }
414
1050
  if (!snapshot) throw new Error(`session ${sessionId} is not available`);
415
1051
  const source = snapshot.items || [];