mixdog 1.4.1 → 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 (326) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/gate-local.mjs +172 -61
  4. package/scripts/lib/gate-scheduler.mjs +138 -32
  5. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  6. package/scripts/lib/run-node-tests.mjs +54 -29
  7. package/scripts/lib/scoped-temp.mjs +1 -0
  8. package/scripts/lib/test-failure-records.mjs +14 -1
  9. package/scripts/lib/test-failure-summary.mjs +16 -4
  10. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  11. package/scripts/pptx-kit-index.mjs +5 -6
  12. package/scripts/suite-health.mjs +110 -28
  13. package/scripts/test.mjs +9 -14
  14. package/src/agents/advisor/AGENT.md +4 -4
  15. package/src/agents/advisor/agent.json +1 -1
  16. package/src/agents/explore/AGENT.md +22 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/cli.mjs +7 -0
  19. package/src/defaults/agents.json +7 -0
  20. package/src/defaults/shipped-history.json +18 -2
  21. package/src/defaults/skills/browser-use/SKILL.md +4 -1
  22. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  23. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  24. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  25. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  26. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  27. package/src/defaults/skills/computer-use/SKILL.md +12 -2
  28. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  29. package/src/defaults/skills/setup/references/actions.md +12 -6
  30. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  31. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  32. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  33. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  34. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  35. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  36. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  37. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  38. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  39. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  40. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  41. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  42. package/src/defaults/skills/visualize/SKILL.md +6 -2
  43. package/src/lib/rules-builder.cjs +6 -30
  44. package/src/rules/lead/LEAD.md +3 -3
  45. package/src/rules/shared/20-research.md +3 -6
  46. package/src/rules/shared/40-editing.md +1 -0
  47. package/src/rules/shared/60-verification.md +10 -9
  48. package/src/rules/shared/70-delivery.md +4 -3
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
  52. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  54. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  55. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
  56. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  57. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  58. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  59. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  60. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  61. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  63. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
  64. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  65. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
  66. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
  67. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  69. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  70. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  74. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  75. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  78. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  80. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
  81. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  82. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  83. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  85. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
  88. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
  93. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  94. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  95. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  96. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  97. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  98. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
  99. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  100. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  102. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  103. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  104. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  105. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
  106. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
  107. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
  108. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
  109. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
  110. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  111. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  112. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  116. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  117. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  118. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  123. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
  124. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
  125. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  126. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  127. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
  128. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
  129. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  130. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  131. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
  132. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
  133. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  134. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  135. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  136. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  137. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
  138. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
  139. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  143. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  144. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  145. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  154. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  155. package/src/runtime/channels/lib/whisper-port.mjs +19 -10
  156. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  157. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  158. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  159. package/src/runtime/explore/explorer.mjs +93 -0
  160. package/src/runtime/explore/tool-defs.mjs +35 -0
  161. package/src/runtime/explore/tool.mjs +123 -0
  162. package/src/runtime/memory/index.mjs +4 -0
  163. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  164. package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
  165. package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
  166. package/src/runtime/shared/agent-route-config.mjs +17 -2
  167. package/src/runtime/shared/atomic-file.mjs +103 -2
  168. package/src/runtime/shared/background-tasks.mjs +15 -6
  169. package/src/runtime/shared/connector-accounts.mjs +5 -5
  170. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  171. package/src/runtime/shared/connector-catalog.mjs +60 -52
  172. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  173. package/src/runtime/shared/file-has-data.mjs +26 -2
  174. package/src/runtime/shared/file-lock.mjs +13 -15
  175. package/src/runtime/shared/git-launch.d.mts +9 -0
  176. package/src/runtime/shared/git-launch.mjs +213 -0
  177. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  178. package/src/runtime/shared/llm/cost.mjs +6 -1
  179. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  180. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  181. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  182. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  183. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  184. package/src/runtime/shared/provider-accounts.mjs +52 -2
  185. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  186. package/src/runtime/shared/staged-update.mjs +47 -2
  187. package/src/runtime/shared/tool-surface.mjs +110 -36
  188. package/src/runtime/shared/tool-work-units.mjs +4 -0
  189. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  190. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  191. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  192. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  193. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  194. package/src/runtime/shared/worktrees/git.mjs +57 -0
  195. package/src/runtime/shared/worktrees/manager.mjs +205 -137
  196. package/src/runtime/tidy/install.mjs +95 -15
  197. package/src/runtime/tidy/languages.mjs +17 -15
  198. package/src/runtime/tidy/process.mjs +7 -4
  199. package/src/runtime/tidy/resolve.mjs +19 -16
  200. package/src/runtime/tidy/runners/biome.mjs +3 -3
  201. package/src/runtime/tidy/tool.mjs +1 -1
  202. package/src/runtime/tidy/worktree.mjs +10 -6
  203. package/src/runtime/visualize-bridge/client.mjs +4 -1
  204. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  205. package/src/runtime/web-search/index.mjs +1 -1
  206. package/src/session-runtime/agent-task-sessions.mjs +410 -353
  207. package/src/session-runtime/boot/tools.mjs +2 -0
  208. package/src/session-runtime/bot-status.mjs +103 -103
  209. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  210. package/src/session-runtime/conversation-import/discovery.mjs +159 -40
  211. package/src/session-runtime/conversation-import/service.mjs +9 -3
  212. package/src/session-runtime/handoff-attachments.mjs +1 -1
  213. package/src/session-runtime/held-requests.mjs +3 -1
  214. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  215. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  216. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
  217. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  218. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  219. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  220. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  221. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  222. package/src/session-runtime/provider-auth/login.mjs +2 -2
  223. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  224. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  225. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  226. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  227. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  228. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  229. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  230. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  231. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  232. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  233. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  234. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  235. package/src/session-runtime/services/provider-admin.mjs +281 -10
  236. package/src/session-runtime/session-turn-api.mjs +2 -0
  237. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  238. package/src/session-runtime/settings-system-api.mjs +1 -1
  239. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  240. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  241. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  242. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  243. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  244. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  245. package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
  246. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  247. package/src/session-runtime/workspace/session-view.mjs +149 -0
  248. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  249. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  250. package/src/session-runtime/workspace/transitions.mjs +108 -1066
  251. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  252. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  253. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  254. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  255. package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
  256. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  257. package/src/standalone/daemon-session-control.mjs +151 -136
  258. package/src/standalone/daemon.mjs +7 -0
  259. package/src/standalone/desktop-service-registry.mjs +9 -0
  260. package/src/standalone/local-session-runtime.mjs +5 -1
  261. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  262. package/src/standalone/session-client.mjs +5 -1
  263. package/src/standalone/session-frame-stream.mjs +83 -11
  264. package/src/standalone/session-protocol.mjs +4 -0
  265. package/src/standalone/session-service/agent-tree.mjs +45 -9
  266. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  267. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  268. package/src/standalone/session-service/stored-reader.mjs +127 -40
  269. package/src/tui/App.jsx +154 -164
  270. package/src/tui/app/app-view/floating-panel.jsx +3 -0
  271. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  272. package/src/tui/app/clipboard.mjs +21 -10
  273. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  274. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  275. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  276. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  277. package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
  278. package/src/tui/app/provider-setup-picker.mjs +20 -13
  279. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  280. package/src/tui/app/slash-commands.mjs +1 -0
  281. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  282. package/src/tui/app/slash-dispatch.mjs +1 -1
  283. package/src/tui/app/use-btw-panel.mjs +29 -0
  284. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  285. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  286. package/src/tui/app/use-mouse-input.mjs +11 -0
  287. package/src/tui/app/use-onboarding-start.mjs +8 -3
  288. package/src/tui/app/use-panel-transition.mjs +10 -1
  289. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  290. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  291. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  292. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  293. package/src/tui/app/use-selection-focus.mjs +11 -2
  294. package/src/tui/app/use-selection-geometry.mjs +18 -9
  295. package/src/tui/app/use-selection-paint.mjs +4 -4
  296. package/src/tui/app/use-selection-stitch.mjs +3 -3
  297. package/src/tui/app/use-slash-palette.mjs +62 -0
  298. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  299. package/src/tui/app/use-transcript-activity.mjs +47 -32
  300. package/src/tui/app/use-transcript-follow.mjs +12 -4
  301. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  302. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  303. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  304. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  305. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  306. package/src/tui/app/use-transcript-window.mjs +21 -11
  307. package/src/tui/app/use-tui-ready.mjs +18 -0
  308. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  309. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  310. package/src/tui/components/Picker.jsx +27 -3
  311. package/src/tui/components/SignInUrl.jsx +68 -0
  312. package/src/tui/components/TextEntryPanel.jsx +76 -46
  313. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  314. package/src/tui/dist/index.mjs +1896 -1341
  315. package/src/tui/session/goal-turn-state.mjs +6 -2
  316. package/src/tui/session/session-api/controls.mjs +4 -0
  317. package/src/tui/session/session-api/extensions.mjs +8 -0
  318. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  319. package/src/tui/session/session-api/intake.mjs +2 -0
  320. package/src/tui/session/session-api/integrations.mjs +2 -2
  321. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  322. package/src/tui/session/session-api/settings.mjs +2 -2
  323. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  324. package/src/tui/session/transcript-spill.mjs +92 -4
  325. package/src/tui/session/transcript-store.mjs +2 -0
  326. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -6,60 +6,26 @@ import { SESSION_READ_ACTION_SET } from '../../session-protocol.mjs';
6
6
  import { SESSION_ID_PATTERN } from '../agent-tree.mjs';
7
7
  import {
8
8
  loadLiveTranscriptHead,
9
+ readTranscriptHistory,
9
10
  readTranscriptRange,
10
11
  rebudgetTranscriptWindow,
12
+ requestedTranscriptHistory,
11
13
  requestedHeldTranscript,
12
14
  requestedTranscriptWindow,
13
15
  requestLiveTranscriptWindow,
16
+ restartTranscriptWindow,
17
+ transcriptAnswerBounds,
14
18
  transcriptPageBody,
19
+ transcriptWindowOversized,
15
20
  } from '../projection/transcript-window.mjs';
16
21
 
17
- export function createSessionViewCalls(ctx) {
18
- const { log, readStoredSession, externalViewEntries, runSessionAction, sessionResult, advanceForCaller } = ctx;
19
- const { currentSessionId, externalEntryForView, projectionResult, unchangedProjectionResult, sessionOwner } =
20
- ctx.projection;
21
- /** The owner a concurrent materialization may have produced while a disk
22
- * read was in flight: a daemon-owned runtime outranks an external view. */
23
- const lateOwnerFor = (id) => sessionOwner(id) || externalEntryForView(id);
24
- const {
25
- subscriberToken,
26
- addSubscriber,
27
- trackPendingViewer,
28
- dropPendingViewer,
29
- adoptPendingViewers,
30
- notePrependViewer,
31
- } = ctx.viewers;
32
- const { sessionBusy, retainUnwatched, releaseProjection, startEvictionSweep } = ctx.retention;
33
- const {
34
- assertAvailable,
35
- createEntry,
36
- getOrCreateSessionEntry,
37
- entryForSession,
38
- liveEntryForView,
39
- bindExternalSessionView,
40
- destroy,
41
- } = ctx.entries;
42
- const {
43
- storedSessionProjection,
44
- storedProjectionUnchanged,
45
- requestedMessageSlice,
46
- forgetStoredSession,
47
- releaseStoredSession,
48
- } = ctx.storedReader;
49
-
50
- /** A read/subscribe names the transcript window its view wants (none: the
51
- * whole transcript). A window grown past what a resumed runtime holds is
52
- * completed from durable history, which a live session never keeps cached. */
53
- async function applyTranscriptRequest(entry, params, sessionId) {
54
- const request = requestedTranscriptWindow(params);
55
- // A model-message read (messageStart) is not a view and leaves the window.
56
- if (request || !Number.isInteger(params.messageStart)) requestLiveTranscriptWindow(entry, request);
57
- // No baseline the next step can patch: this caller receives the window
58
- // whole, so it goes out within its byte budget.
59
- if (request && params.baseRevision !== entry.revision) rebudgetTranscriptWindow(entry);
60
- if (await loadLiveTranscriptHead(entry, sessionId, readStoredSession)) forgetStoredSession(sessionId);
61
- assertAvailable(entry);
62
- }
22
+ /** Creating (reserving) a session. */
23
+ function createSessionCreation(ctx) {
24
+ const { log, sessionResult, advanceForCaller } = ctx;
25
+ const { currentSessionId } = ctx.projection;
26
+ const { addSubscriber, adoptPendingViewers } = ctx.viewers;
27
+ const { retainUnwatched } = ctx.retention;
28
+ const { assertAvailable, createEntry, getOrCreateSessionEntry, destroy } = ctx.entries;
63
29
 
64
30
  async function createReservedSession(params, viewer, reservedSessionId) {
65
31
  const entry = await createEntry({ ...params, sessionId: reservedSessionId }, viewer);
@@ -103,6 +69,138 @@ export function createSessionViewCalls(ctx) {
103
69
  return sessionResult(entry, step);
104
70
  }
105
71
 
72
+ return { createSession };
73
+ }
74
+
75
+ /** The transcript window a live view is answered through: requesting it,
76
+ * bounding the answer, and attaching a subscriber to a live entry. */
77
+ function createLiveTranscriptAnswers(ctx) {
78
+ const { readStoredSession, sessionResult, advanceForCaller } = ctx;
79
+ const { currentSessionId } = ctx.projection;
80
+ const { addSubscriber } = ctx.viewers;
81
+ const { assertAvailable } = ctx.entries;
82
+
83
+ /** A read/subscribe names the transcript window its view wants (none: the
84
+ * whole transcript). A window grown past what a resumed runtime holds is
85
+ * completed from durable history, sliced from the store's cached display
86
+ * history of the record's content. */
87
+ async function applyTranscriptRequest(entry, params, sessionId) {
88
+ const request = requestedTranscriptWindow(params);
89
+ // A model-message read (messageStart) is not a view and leaves the window.
90
+ if (request || !Number.isInteger(params.messageStart)) requestLiveTranscriptWindow(entry, request);
91
+ // No baseline the next step can patch: this caller receives the window
92
+ // whole, so it goes out within its byte budget.
93
+ if (request && params.baseRevision !== entry.revision) rebudgetTranscriptWindow(entry);
94
+ await loadLiveTranscriptHead(entry, sessionId, readStoredSession);
95
+ assertAvailable(entry);
96
+ }
97
+
98
+ /** The caller's answer for `step`, reduced to a page when it proves the rows
99
+ * it holds and they fit one answer. A window that would go out whole past
100
+ * the request's bounds (transcriptAnswerBounds) does not: a caller already
101
+ * at this revision needs no body; any other gets a bounded recovery
102
+ * snapshot — the window restarts from a fresh tail, which the views
103
+ * attached receive as a step too. */
104
+ function boundedSessionResult(entry, step, params, baseRevision, extra) {
105
+ const prepend = params.transcriptPrepend === true;
106
+ const bounds = transcriptAnswerBounds(requestedTranscriptWindow(params));
107
+ const result = transcriptPageBody(
108
+ sessionResult(entry, step, baseRevision, extra, { prepend }),
109
+ requestedHeldTranscript(params),
110
+ { byteBudget: bounds.byteBudget }
111
+ );
112
+ if (!transcriptWindowOversized(entry, result.full, bounds)) return result;
113
+ if (baseRevision === step.revision) {
114
+ const { full: _full, ...bodiless } = result;
115
+ return bodiless;
116
+ }
117
+ restartTranscriptWindow(entry, bounds.maxItems);
118
+ return sessionResult(entry, advanceForCaller(entry), baseRevision, extra, { prepend });
119
+ }
120
+
121
+ /** Publish BEFORE attaching: the views already there get the frame, the new
122
+ * one gets the same revision once in its reply. */
123
+ const subscribeLive = async (entry, params, viewer, baseRevision) => {
124
+ await applyTranscriptRequest(entry, params, currentSessionId(entry) || String(params.sessionId || ''));
125
+ const prepend = params.transcriptPrepend === true;
126
+ let step = advanceForCaller(entry);
127
+ // Bounded like a read (boundedSessionResult), before attaching: the new
128
+ // view receives the restarted tail once, in its reply.
129
+ const bounds = transcriptAnswerBounds(requestedTranscriptWindow(params));
130
+ if (transcriptWindowOversized(entry, sessionResult(entry, step, baseRevision, {}, { prepend }).full, bounds)) {
131
+ restartTranscriptWindow(entry, bounds.maxItems);
132
+ step = advanceForCaller(entry);
133
+ }
134
+ addSubscriber(entry, viewer);
135
+ return sessionResult(entry, step, baseRevision, { subscribed: true }, { prepend });
136
+ };
137
+
138
+ return { applyTranscriptRequest, boundedSessionResult, subscribeLive };
139
+ }
140
+
141
+ /** Detaching a view from a live or cold session. */
142
+ function createSessionUnsubscribe(ctx) {
143
+ const { log, externalViewEntries } = ctx;
144
+ const { externalEntryForView, sessionOwner } = ctx.projection;
145
+ const { subscriberToken, dropPendingViewer } = ctx.viewers;
146
+ const { sessionBusy, releaseProjection, startEvictionSweep } = ctx.retention;
147
+ const { destroy } = ctx.entries;
148
+ const { releaseStoredSession } = ctx.storedReader;
149
+
150
+ async function unsubscribeSession({ sessionId } = {}, viewer = null) {
151
+ const id = String(sessionId || '');
152
+ if (!id) throw new TypeError('sessionId is required');
153
+ const coldViewers = dropPendingViewer(id, viewer);
154
+ const owner = sessionOwner(id);
155
+ if (!owner) {
156
+ const external = externalEntryForView(id);
157
+ if (external) {
158
+ const token = subscriberToken(viewer);
159
+ if (token) external.subscribers?.delete(token);
160
+ if ((external.subscribers?.size || 0) === 0) externalViewEntries.delete(id);
161
+ }
162
+ // The last cold view left: its disk projection turns idle, kept within
163
+ // the store's idle budget so a revisit skips the parse.
164
+ if (!coldViewers) releaseStoredSession(id);
165
+ return { sessionId: id, unsubscribed: true };
166
+ }
167
+ const entry = owner;
168
+ const token = subscriberToken(viewer);
169
+ if (token) entry.subscribers?.delete(token);
170
+ if ((entry.subscribers?.size || 0) === 0) {
171
+ entry.transcriptView = null;
172
+ if (entry.reservedOnly && !sessionBusy(entry)) {
173
+ await destroy(entry, 'unclaimed session reservation', {
174
+ keepBackgroundWork: true,
175
+ });
176
+ return { sessionId: id, unsubscribed: true };
177
+ }
178
+ entry.retainedAt = Date.now();
179
+ releaseProjection(entry);
180
+ startEvictionSweep();
181
+ }
182
+ log(`session unsubscribed session=${id}${token ? ` client=${token}` : ''}`);
183
+ return { sessionId: id, unsubscribed: true };
184
+ }
185
+
186
+ return { unsubscribeSession };
187
+ }
188
+
189
+ export function createSessionViewCalls(ctx) {
190
+ const { readStoredSession, runSessionAction, advanceForCaller } = ctx;
191
+ const { externalEntryForView, projectionResult, unchangedProjectionResult, sessionOwner } = ctx.projection;
192
+ /** The owner a concurrent materialization may have produced while a disk
193
+ * read was in flight: a daemon-owned runtime outranks an external view. */
194
+ const lateOwnerFor = (id) => sessionOwner(id) || externalEntryForView(id);
195
+ const { trackPendingViewer, dropPendingViewer, notePrependViewer } = ctx.viewers;
196
+ const { retainUnwatched } = ctx.retention;
197
+ const { assertAvailable, entryForSession, liveEntryForView, bindExternalSessionView } = ctx.entries;
198
+ const { storedSessionProjection, storedProjectionUnchanged, storedPageOnly, requestedMessageSlice } =
199
+ ctx.storedReader;
200
+ const { createSession } = createSessionCreation(ctx);
201
+ const { applyTranscriptRequest, boundedSessionResult, subscribeLive } = createLiveTranscriptAnswers(ctx);
202
+ const { unsubscribeSession } = createSessionUnsubscribe(ctx);
203
+
106
204
  async function liveSessionReadResult(entry, params, sessionId, baseRevision) {
107
205
  assertAvailable(entry);
108
206
  await applyTranscriptRequest(entry, params, sessionId);
@@ -110,11 +208,7 @@ export function createSessionViewCalls(ctx) {
110
208
  retainUnwatched(entry, 'headless session read');
111
209
  const messages = await requestedMessageSlice(params, sessionId);
112
210
  assertAvailable(entry);
113
- const prepend = params.transcriptPrepend === true;
114
- return transcriptPageBody(
115
- sessionResult(entry, step, baseRevision, messages, { prepend }),
116
- requestedHeldTranscript(params)
117
- );
211
+ return boundedSessionResult(entry, step, params, baseRevision, messages);
118
212
  }
119
213
 
120
214
  /** The live entry a view can attach to: the owner, an external view, or a
@@ -146,6 +240,13 @@ export function createSessionViewCalls(ctx) {
146
240
  assertAvailable(live);
147
241
  return { range };
148
242
  }
243
+ const historyRequest = requestedTranscriptHistory(params);
244
+ if (historyRequest) {
245
+ // Read-only: no window, baseline or revision of any view moves.
246
+ const history = await readTranscriptHistory(live, id, readStoredSession, historyRequest);
247
+ assertAvailable(live);
248
+ return { history };
249
+ }
149
250
  if (live) {
150
251
  return liveSessionReadResult(live, params, id, baseRevision);
151
252
  }
@@ -162,7 +263,8 @@ export function createSessionViewCalls(ctx) {
162
263
  const lateOwner = lateOwnerFor(id);
163
264
  return lateOwner ? liveSessionReadResult(lateOwner, params, id, baseRevision) : held;
164
265
  }
165
- const projection = await storedSessionProjection(id, openHints, window);
266
+ const heldRows = requestedHeldTranscript(params);
267
+ const projection = await storedSessionProjection(id, openHints, window, heldRows, baseProjectionStamp);
166
268
  assertAvailable();
167
269
  const lateOwner = lateOwnerFor(id);
168
270
  if (lateOwner) {
@@ -179,28 +281,18 @@ export function createSessionViewCalls(ctx) {
179
281
  });
180
282
  const messages = await requestedMessageSlice(params, id);
181
283
  assertAvailable();
182
- return { ...transcriptPageBody(result, requestedHeldTranscript(params)), ...messages };
284
+ // The stored reader already proved (or disproved) the held rows, and
285
+ // its projection then carries only the page above them.
286
+ const page = storedPageOnly(projection)
287
+ ? transcriptPageBody(result, heldRows, { proven: true, byteBudget: window?.byteBudget, pageOnly: true })
288
+ : result;
289
+ return { ...page, ...messages };
183
290
  }
184
291
  // Embedders without a store reader keep the legacy load-on-read seam.
185
292
  const entry = await entryForSession(id, openHints || {});
186
293
  return liveSessionReadResult(entry, params, id, baseRevision);
187
294
  }
188
295
 
189
- /** Publish BEFORE attaching: the views already there get the frame, the new
190
- * one gets the same revision once in its reply. */
191
- const subscribeLive = async (entry, params, viewer, baseRevision) => {
192
- await applyTranscriptRequest(entry, params, currentSessionId(entry) || String(params.sessionId || ''));
193
- const step = advanceForCaller(entry);
194
- addSubscriber(entry, viewer);
195
- return sessionResult(
196
- entry,
197
- step,
198
- baseRevision,
199
- { subscribed: true },
200
- { prepend: params.transcriptPrepend === true }
201
- );
202
- };
203
-
204
296
  async function subscribeSession(params = {}, viewer = null) {
205
297
  const { sessionId, open: openHints = {}, baseRevision = null } = params;
206
298
  assertAvailable();
@@ -236,41 +328,5 @@ export function createSessionViewCalls(ctx) {
236
328
  return subscribeLive(entry, params, viewer, baseRevision);
237
329
  }
238
330
 
239
- async function unsubscribeSession({ sessionId } = {}, viewer = null) {
240
- const id = String(sessionId || '');
241
- if (!id) throw new TypeError('sessionId is required');
242
- const coldViewers = dropPendingViewer(id, viewer);
243
- const owner = sessionOwner(id);
244
- if (!owner) {
245
- const external = externalEntryForView(id);
246
- if (external) {
247
- const token = subscriberToken(viewer);
248
- if (token) external.subscribers?.delete(token);
249
- if ((external.subscribers?.size || 0) === 0) externalViewEntries.delete(id);
250
- }
251
- // The last cold view left: its disk projection turns idle, kept within
252
- // the store's idle budget so a revisit skips the parse.
253
- if (!coldViewers) releaseStoredSession(id);
254
- return { sessionId: id, unsubscribed: true };
255
- }
256
- const entry = owner;
257
- const token = subscriberToken(viewer);
258
- if (token) entry.subscribers?.delete(token);
259
- if ((entry.subscribers?.size || 0) === 0) {
260
- entry.transcriptView = null;
261
- if (entry.reservedOnly && !sessionBusy(entry)) {
262
- await destroy(entry, 'unclaimed session reservation', {
263
- keepBackgroundWork: true,
264
- });
265
- return { sessionId: id, unsubscribed: true };
266
- }
267
- entry.retainedAt = Date.now();
268
- releaseProjection(entry);
269
- startEvictionSweep();
270
- }
271
- log(`session unsubscribed session=${id}${token ? ` client=${token}` : ''}`);
272
- return { sessionId: id, unsubscribed: true };
273
- }
274
-
275
331
  return { createSession, readSession, subscribeSession, unsubscribeSession };
276
332
  }
@@ -1,9 +1,72 @@
1
1
  import { sanitizeForWire } from '../session-wire-values.mjs';
2
- import { budgetStoredWindow } from './projection/transcript-window.mjs';
2
+ import {
3
+ clampStoredPage,
4
+ clampStoredWindow,
5
+ holdsTranscriptTail,
6
+ TRANSCRIPT_RESPONSE_MAX_ITEMS,
7
+ } from './projection/transcript-window.mjs';
3
8
 
4
9
  const SLOW_STORED_PROJECTION_MS = 250;
5
10
  const PROJECTED_FILE_LIMIT = 256;
6
11
 
12
+ // Every window of a stored session is a slice of the store's one cached
13
+ // display history, so its rows are the same objects page after page: each
14
+ // is sanitized once (keyed weakly — dropping the store's cache entry
15
+ // releases the clones) and a page costs a slice, not a whole-window clone.
16
+ const wireItems = new WeakMap();
17
+ function wireItem(item) {
18
+ if (!item || typeof item !== 'object') return sanitizeForWire(item, 2) ?? null;
19
+ let wire = wireItems.get(item);
20
+ if (wire === undefined) {
21
+ wire = sanitizeForWire(item, 2) ?? null;
22
+ wireItems.set(item, wire);
23
+ }
24
+ return wire;
25
+ }
26
+ // The store returns one object per unchanged window, so a repeated read
27
+ // reuses its wire clone (the 1s cold-view refresh would re-clone it).
28
+ const wireSnapshots = new WeakMap();
29
+ function wireSnapshot(snapshot) {
30
+ let wire = wireSnapshots.get(snapshot);
31
+ if (!wire) {
32
+ // The slice's own proof fields stay here.
33
+ const { items, heldProven: _heldProven, skippedFirstId: _skippedFirstId, ...fields } = snapshot;
34
+ wire = sanitizeForWire(fields);
35
+ if (Array.isArray(items)) wire = { ...wire, items: items.map(wireItem) };
36
+ wireSnapshots.set(snapshot, wire);
37
+ }
38
+ return wire;
39
+ }
40
+
41
+ // A slice's stamp is `<history stamp>/<slice>` (storedHistorySlice); an
42
+ // answer cut from it is served as `<slice stamp>#<rows delivered>`.
43
+ const historyOf = (stamp) => {
44
+ const window = stamp.includes('#') ? stamp.slice(0, stamp.lastIndexOf('#')) : stamp;
45
+ return window.includes('/') ? window.slice(0, window.lastIndexOf('/')) : window;
46
+ };
47
+ const deliveredOf = (stamp) => (stamp.includes('#') ? Number(stamp.slice(stamp.lastIndexOf('#') + 1)) : -1);
48
+ /** The stamp an answer of `delivered` rows is served under. Every answer cut
49
+ * from one display history is a tail of it, so the rows a caller received
50
+ * are named by that history and their count, byte-budget cuts included.
51
+ * A request that pages nothing is answered under the caller's own stamp
52
+ * (unchanged) when the tail it received already covers this whole answer —
53
+ * a host that paged deeper than this window keeps its rows on a refresh;
54
+ * any answer that would reveal more is served. */
55
+ function servedStamp(stamp, window, baseProjectionStamp, delivered) {
56
+ if (typeof stamp !== 'string' || !stamp.includes('/')) return stamp;
57
+ const own = `${stamp}#${delivered}`;
58
+ if (window?.paged || typeof baseProjectionStamp !== 'string') return own;
59
+ const covers = historyOf(baseProjectionStamp) === historyOf(own) && deliveredOf(baseProjectionStamp) >= delivered;
60
+ return covers ? baseProjectionStamp : own;
61
+ }
62
+
63
+ const itemLimitFor = (hints, window) => {
64
+ const requested = window ? window.limit : Number(hints?.resumeOptions?.transcriptItemLimit);
65
+ return Number.isFinite(requested) && requested > 0 ? requested : 512;
66
+ };
67
+ const projectedKey = (sessionId, hints, window) =>
68
+ `${sessionId}|${itemLimitFor(hints, window)}:${window?.byteBudget ?? ''}:${window?.pageBase ?? ''}:${window?.paged ? 'p' : ''}`;
69
+
7
70
  export function createStoredSessionReader({
8
71
  readStoredSession,
9
72
  readStoredGoal,
@@ -38,21 +101,9 @@ export function createStoredSessionReader({
38
101
  // that guarantee once several 512-item views exceed its byte budget: it
39
102
  // evicts each one just before its next refresh.
40
103
  const projectedFiles = new Map();
41
- // The stored reader returns its cached projection object for unchanged
42
- // content and callers never mutate it, so identity keys the wire clone. The
43
- // 1s cold-view refresh otherwise re-sanitized the whole transcript per tick.
44
- // Keyed weakly: dropping the store's cache entry releases the clone too.
45
- const wireSnapshots = new WeakMap();
46
- function wireSnapshot(snapshot, window) {
47
- const budget = `${window?.byteBudget ?? ''}:${window?.pageBase ?? ''}`;
48
- let cached = wireSnapshots.get(snapshot);
49
- if (!cached || cached.budget !== budget) {
50
- const wire = cached?.wire ?? sanitizeForWire(snapshot);
51
- cached = { wire, budget, value: budgetStoredWindow(wire, window) };
52
- wireSnapshots.set(snapshot, cached);
53
- }
54
- return cached.value;
55
- }
104
+ /** Projections that carry only the page above their caller's proven rows
105
+ * (storedPageOnly). */
106
+ const pageOnly = new WeakSet();
56
107
 
57
108
  /** Drop every cold projection the store retains for this session. */
58
109
  function forgetStoredProjection(sessionId) {
@@ -87,13 +138,6 @@ export function createStoredSessionReader({
87
138
  );
88
139
  }
89
140
 
90
- const itemLimitFor = (hints, window) => {
91
- const requested = window ? window.limit : Number(hints?.resumeOptions?.transcriptItemLimit);
92
- return Number.isFinite(requested) && requested > 0 ? requested : 512;
93
- };
94
- const projectedKey = (sessionId, hints, window) =>
95
- `${sessionId}|${itemLimitFor(hints, window)}:${window?.byteBudget ?? ''}:${window?.pageBase ?? ''}`;
96
-
97
141
  async function fileStampOf(sessionId) {
98
142
  if (typeof statStoredSession !== 'function') return null;
99
143
  let stamp;
@@ -125,32 +169,68 @@ export function createStoredSessionReader({
125
169
  }
126
170
 
127
171
  /** `window` (from requestedTranscriptWindow) selects a paged tail; without
128
- * one, the resume hint (default 512 items) keeps the legacy page. */
129
- async function storedSessionProjection(sessionId, hints, window = null) {
172
+ * one, the resume hint (default 512 items) keeps the legacy page. A window
173
+ * answers within clampStoredWindow's bounds, above the rows `held`
174
+ * (requestedHeldTranscript) when the caller proves it holds them. */
175
+ async function storedSessionProjection(sessionId, hints, window = null, held = null, baseProjectionStamp = null) {
130
176
  if (typeof readStoredSession !== 'function') return null;
131
177
  const key = projectedKey(sessionId, hints, window);
132
178
  // stat -> read -> stat: the projection is attributed to a file identity
133
179
  // only when no write can have landed while it was read.
134
180
  const fileStamp = await fileStampOf(sessionId);
135
- let snapshot = null;
136
- try {
137
- snapshot = await readStoredSession(sessionId, {
138
- transcriptItemLimit: itemLimitFor(hints, window),
139
- trace: traceStoredProjectionRead,
140
- });
141
- } catch (err) {
142
- log(`stored session projection failed session=${sessionId}: ${err?.message || err}`);
143
- return null;
181
+ const read = async (options) => {
182
+ try {
183
+ const value = await readStoredSession(sessionId, { trace: traceStoredProjectionRead, ...options });
184
+ return value && typeof value === 'object' ? value : null;
185
+ } catch (err) {
186
+ log(`stored session projection failed session=${sessionId}: ${err?.message || err}`);
187
+ return undefined;
188
+ }
189
+ };
190
+ const limit = itemLimitFor(hints, window);
191
+ const pagedHeld = window?.paged ? held : null;
192
+ // A paged caller's held rows are proven where the history is (by the
193
+ // history stamp they were served under, or their digest), at any depth:
194
+ // only the page above them crosses, never the rows held. Unproven, a
195
+ // fresh tail of at most one answer's rows.
196
+ const snapshot = await read({
197
+ transcriptItemLimit: window?.paged && !pagedHeld ? Math.min(limit, TRANSCRIPT_RESPONSE_MAX_ITEMS) : limit,
198
+ ...(pagedHeld
199
+ ? {
200
+ transcriptHeld: pagedHeld,
201
+ transcriptBaseHistory: typeof baseProjectionStamp === 'string' ? historyOf(baseProjectionStamp) : null,
202
+ transcriptPage: { maxRows: TRANSCRIPT_RESPONSE_MAX_ITEMS, byteBudget: window.byteBudget },
203
+ }
204
+ : {}),
205
+ paging: Boolean(pagedHeld),
206
+ });
207
+ if (!snapshot) return null;
208
+ const whole = wireSnapshot(snapshot);
209
+ let paged = Boolean(pagedHeld) && snapshot.heldProven === true;
210
+ let wire;
211
+ if (pagedHeld && snapshot.heldProven === undefined && Array.isArray(whole.items)) {
212
+ // A reader that predates held rows answers the window itself: they are
213
+ // proven against it here, by digest.
214
+ const proven = holdsTranscriptTail(whole.items, pagedHeld);
215
+ wire = clampStoredWindow(whole, window, proven ? pagedHeld.count : 0);
216
+ if (proven) {
217
+ wire = { ...wire, items: wire.items.slice(0, wire.items.length - pagedHeld.count) };
218
+ paged = true;
219
+ }
220
+ } else {
221
+ wire = paged ? clampStoredPage(whole, window, pagedHeld.count) : clampStoredWindow(whole, window, 0);
144
222
  }
145
- if (!snapshot || typeof snapshot !== 'object') return null;
223
+ const heldRows = paged ? pagedHeld.count : 0;
224
+ const delivered = (Array.isArray(wire.items) ? wire.items.length : 0) + heldRows;
225
+ const projectionStamp = servedStamp(snapshot.projectionStamp, window, baseProjectionStamp, delivered);
146
226
  projectedFiles.delete(key);
147
227
  if (
148
228
  fileStamp !== null &&
149
- typeof snapshot.projectionStamp === 'string' &&
150
- snapshot.projectionStamp &&
229
+ typeof projectionStamp === 'string' &&
230
+ projectionStamp &&
151
231
  (await fileStampOf(sessionId)) === fileStamp
152
232
  ) {
153
- projectedFiles.set(key, { projectionStamp: snapshot.projectionStamp, fileStamp });
233
+ projectedFiles.set(key, { projectionStamp, fileStamp });
154
234
  if (projectedFiles.size > PROJECTED_FILE_LIMIT) projectedFiles.delete(projectedFiles.keys().next().value);
155
235
  }
156
236
  let goal;
@@ -162,9 +242,9 @@ export function createStoredSessionReader({
162
242
  goal = null;
163
243
  }
164
244
  }
165
- const wire = wireSnapshot(snapshot, window);
166
- return {
245
+ const projection = {
167
246
  ...wire,
247
+ ...(typeof projectionStamp === 'string' ? { projectionStamp } : {}),
168
248
  sessionId,
169
249
  ...(typeof readStoredGoal === 'function' ? { goal: sanitizeForWire(goal, 1) ?? null } : {}),
170
250
  ...(typeof readQuotaResume === 'function'
@@ -172,8 +252,14 @@ export function createStoredSessionReader({
172
252
  : {}),
173
253
  queued: Array.isArray(snapshot.queued) ? wire.queued : [],
174
254
  };
255
+ if (paged) pageOnly.add(projection);
256
+ return projection;
175
257
  }
176
258
 
259
+ /** Whether `projection` carries only the rows above its caller's proven
260
+ * ones (the page itself), not those rows. */
261
+ const storedPageOnly = (projection) => Boolean(projection) && pageOnly.has(projection);
262
+
177
263
  async function requestedMessageSlice(params, sessionId) {
178
264
  if (!Number.isInteger(params?.messageStart)) return {};
179
265
  const start = Math.max(0, params.messageStart);
@@ -204,6 +290,7 @@ export function createStoredSessionReader({
204
290
  return {
205
291
  storedSessionProjection,
206
292
  storedProjectionUnchanged,
293
+ storedPageOnly,
207
294
  requestedMessageSlice,
208
295
  forgetStoredSession: forgetStoredProjection,
209
296
  releaseStoredSession: releaseStoredProjection,