mixdog 0.9.62 → 0.9.64

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 (403) hide show
  1. package/README.md +3 -0
  2. package/package.json +9 -2
  3. package/scripts/code-graph-description-contract.mjs +3 -3
  4. package/scripts/verify-release-assets.mjs +2 -2
  5. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +11 -14
  6. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -6
  7. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  8. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
  9. package/src/runtime/agent/orchestrator/config.mjs +19 -124
  10. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -6
  11. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +2 -2
  12. package/src/runtime/agent/orchestrator/internal-agents.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +4 -4
  15. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +2 -2
  16. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +4 -4
  17. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +2 -2
  18. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +4 -4
  19. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +333 -0
  20. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -4
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -277
  24. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +2 -2
  27. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -6
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +196 -0
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +460 -0
  30. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +14 -593
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +4 -4
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -280
  37. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +357 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +216 -0
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +2 -188
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/registry.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -2
  46. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +3 -3
  49. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +2 -2
  50. package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +2 -2
  54. package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -4
  55. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +1 -1
  56. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +17 -0
  57. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +18 -26
  59. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +1 -1
  60. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +3 -3
  61. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -0
  62. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +1 -1
  64. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +49 -0
  66. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +260 -0
  68. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +72 -0
  69. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +163 -0
  70. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +4 -1
  73. package/src/runtime/agent/orchestrator/session/store.mjs +9 -561
  74. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/stall-policy.mjs +11 -11
  77. package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -1
  78. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -154
  83. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/tools/builtin/cwd-utils.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/path-locks.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -2
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +2 -3
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +4 -4
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -5
  97. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +2 -2
  99. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +519 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +5 -449
  102. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +212 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -1
  105. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +1 -7
  106. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +9 -36
  107. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +15 -3
  108. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +429 -0
  109. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -395
  110. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +2 -2
  111. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +1 -1
  112. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +3 -3
  115. package/src/runtime/agent/orchestrator/tools/mutation-planner.mjs +5 -5
  116. package/src/runtime/agent/orchestrator/tools/next-call-utils.mjs +1 -1
  117. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +7 -7
  119. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +72 -9
  120. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +4 -4
  121. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  123. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -4
  124. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  125. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +3 -3
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -444
  127. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +505 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +2 -2
  129. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +1 -1
  130. package/src/runtime/channels/lib/backend-dispatch.mjs +20 -5
  131. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  132. package/src/runtime/channels/lib/telegram-format.mjs +1 -1
  133. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +4 -4
  134. package/src/runtime/channels/tool-defs.mjs +2 -3
  135. package/src/runtime/memory/index.mjs +1 -5
  136. package/src/runtime/memory/lib/core-memory-candidates.mjs +357 -0
  137. package/src/runtime/memory/lib/core-memory-store.mjs +7 -357
  138. package/src/runtime/memory/lib/cycle-signatures.mjs +1 -1
  139. package/src/runtime/memory/lib/embedding-model-config.mjs +1 -1
  140. package/src/runtime/memory/lib/embedding-provider.mjs +1 -5
  141. package/src/runtime/memory/lib/embedding-worker.mjs +1 -5
  142. package/src/runtime/memory/lib/ko-morph.mjs +2 -2
  143. package/src/runtime/memory/lib/memory-config-flags.mjs +2 -5
  144. package/src/runtime/memory/lib/memory-cycle-requests.mjs +3 -7
  145. package/src/runtime/memory/lib/memory-cycle1.mjs +4 -8
  146. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -1
  147. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -1
  148. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +2 -5
  149. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -5
  150. package/src/runtime/memory/lib/memory-embed.mjs +2 -6
  151. package/src/runtime/memory/lib/memory-log.mjs +8 -0
  152. package/src/runtime/memory/lib/memory-ops-policy.mjs +4 -8
  153. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +1 -1
  154. package/src/runtime/memory/lib/memory-recall-store.mjs +2 -193
  155. package/src/runtime/memory/lib/memory-score.mjs +1 -1
  156. package/src/runtime/memory/lib/memory-text-utils.mjs +2 -2
  157. package/src/runtime/memory/lib/memory.mjs +2 -6
  158. package/src/runtime/memory/lib/pg/adapter.mjs +2 -6
  159. package/src/runtime/memory/lib/pg/process.mjs +1 -5
  160. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -5
  161. package/src/runtime/memory/lib/recall-format.mjs +2 -2
  162. package/src/runtime/memory/lib/recall-scoring.mjs +195 -0
  163. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -5
  164. package/src/runtime/memory/lib/trace-store.mjs +2 -6
  165. package/src/runtime/search/lib/http-fetch.mjs +6 -6
  166. package/src/runtime/search/lib/web-tools.mjs +2 -2
  167. package/src/runtime/shared/buffered-appender.mjs +1 -1
  168. package/src/runtime/shared/child-guardian.mjs +1 -1
  169. package/src/runtime/shared/child-spawn-gate.mjs +1 -1
  170. package/src/runtime/shared/config.mjs +4 -4
  171. package/src/runtime/shared/launcher-control.mjs +9 -10
  172. package/src/runtime/shared/llm/index.mjs +5 -20
  173. package/src/runtime/shared/markdown-frontmatter.mjs +1 -1
  174. package/src/runtime/shared/memory-snapshot.mjs +4 -4
  175. package/src/runtime/shared/pristine-execution.mjs +1 -1
  176. package/src/runtime/shared/process-lifecycle.mjs +0 -1
  177. package/src/runtime/shared/process-shutdown.mjs +1 -1
  178. package/src/runtime/shared/resource-admission.mjs +2 -2
  179. package/src/runtime/shared/schedule-session-run.mjs +1 -1
  180. package/src/runtime/shared/schedules-db.mjs +1 -1
  181. package/src/runtime/shared/service-discovery.mjs +2 -2
  182. package/src/runtime/shared/staged-update.mjs +6 -6
  183. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  184. package/src/runtime/shared/tool-primitives.mjs +1 -1
  185. package/src/runtime/shared/tool-surface.mjs +2 -2
  186. package/src/runtime/shared/update-checker.mjs +1 -1
  187. package/src/runtime/shared/user-cwd.mjs +3 -3
  188. package/src/runtime/shared/user-data-guard.mjs +4 -4
  189. package/src/runtime/shared/workspace-router.mjs +5 -5
  190. package/src/session-runtime/config-helpers.mjs +2 -3
  191. package/src/session-runtime/config-lifecycle.mjs +1 -1
  192. package/src/session-runtime/hitch-profile.mjs +1 -1
  193. package/src/session-runtime/lifecycle-api.mjs +6 -4
  194. package/src/session-runtime/model-recency.mjs +5 -5
  195. package/src/session-runtime/plugin-mcp.mjs +1 -1
  196. package/src/session-runtime/provider-request-snapshot.mjs +338 -0
  197. package/src/session-runtime/session-text.mjs +4 -4
  198. package/src/session-runtime/tool-catalog-schema.mjs +197 -0
  199. package/src/session-runtime/tool-catalog.mjs +6 -483
  200. package/src/session-runtime/tool-defs.mjs +1 -1
  201. package/src/standalone/agent-task-status.mjs +1 -1
  202. package/src/standalone/agent-tool/helpers.mjs +2 -2
  203. package/src/standalone/channel-admin.mjs +4 -4
  204. package/src/standalone/explore-tool.mjs +3 -3
  205. package/src/standalone/hook-bus/config.mjs +1 -1
  206. package/src/standalone/hook-bus/constants.mjs +1 -1
  207. package/src/standalone/hook-bus/rules.mjs +1 -1
  208. package/src/standalone/projects.mjs +1 -1
  209. package/src/standalone/provider-admin.mjs +2 -2
  210. package/src/tui/app/app-format.mjs +2 -2
  211. package/src/tui/app/clipboard.mjs +3 -3
  212. package/src/tui/app/input-parsers.mjs +1 -1
  213. package/src/tui/app/text-layout.mjs +1 -1
  214. package/src/tui/app/transcript-row-estimate.mjs +360 -0
  215. package/src/tui/app/transcript-window.mjs +5 -347
  216. package/src/tui/app/use-mouse-input.mjs +1 -1
  217. package/src/tui/app/use-prompt-handlers.mjs +1 -1
  218. package/src/tui/components/tool-execution/surface-detail.mjs +9 -9
  219. package/src/tui/components/tool-execution/text-format.mjs +2 -2
  220. package/src/tui/components/tool-output-format.mjs +7 -7
  221. package/src/tui/display-width.mjs +4 -4
  222. package/src/tui/dist/index.mjs +63 -97
  223. package/src/tui/engine/agent-envelope.mjs +5 -5
  224. package/src/tui/engine/notification-plan.mjs +1 -1
  225. package/src/tui/engine/queue-helpers.mjs +3 -3
  226. package/src/tui/engine/render-timing.mjs +4 -5
  227. package/src/tui/engine/session-api-ext.mjs +3 -3
  228. package/src/tui/engine/tool-result-status.mjs +5 -5
  229. package/src/tui/engine/tool-result-text.mjs +1 -1
  230. package/src/tui/engine/transcript-spill.mjs +585 -0
  231. package/src/tui/engine/tui-steering-persist.mjs +1 -1
  232. package/src/tui/engine.mjs +2 -474
  233. package/src/tui/lib/voice-setup.mjs +1 -1
  234. package/src/tui/markdown/format-token.mjs +1 -1
  235. package/src/tui/markdown/render-ansi.mjs +1 -1
  236. package/src/tui/markdown/table-layout.mjs +4 -4
  237. package/src/tui/paste-attachments.mjs +1 -1
  238. package/src/tui/prompt-history-store.mjs +3 -3
  239. package/src/tui/statusline-ansi-bridge.mjs +2 -2
  240. package/src/tui/theme.mjs +1 -1
  241. package/src/tui/transcript-tool-failures.mjs +2 -2
  242. package/src/ui/ansi.mjs +0 -9
  243. package/src/ui/statusline-format.mjs +2 -4
  244. package/scripts/_bench-cwc.json +0 -20
  245. package/scripts/_jitter-fuzz.jsx +0 -42
  246. package/scripts/_jitter-fuzz2.jsx +0 -30
  247. package/scripts/_jitter-probe.jsx +0 -35
  248. package/scripts/_jp2.jsx +0 -16
  249. package/scripts/_smoke_wd.mjs +0 -7
  250. package/scripts/abort-recovery-test.mjs +0 -175
  251. package/scripts/agent-dispatch-abort-compose-test.mjs +0 -31
  252. package/scripts/agent-live-arg-guard-smoke.mjs +0 -20
  253. package/scripts/agent-live-path-suffix-smoke.mjs +0 -34
  254. package/scripts/agent-live-toolcall-args-smoke.mjs +0 -45
  255. package/scripts/agent-loop-policy-test.mjs +0 -37
  256. package/scripts/agent-model-liveness-test.mjs +0 -763
  257. package/scripts/agent-parallel-smoke.mjs +0 -435
  258. package/scripts/agent-route-batch-test.mjs +0 -40
  259. package/scripts/agent-tag-reuse-smoke.mjs +0 -441
  260. package/scripts/agent-terminal-reap-test.mjs +0 -252
  261. package/scripts/agent-trace-io-test.mjs +0 -133
  262. package/scripts/ansi-color-capability-test.mjs +0 -90
  263. package/scripts/anthropic-admission-retry-integration-test.mjs +0 -119
  264. package/scripts/anthropic-maxtokens-test.mjs +0 -119
  265. package/scripts/anthropic-oauth-refresh-race-test.mjs +0 -397
  266. package/scripts/anthropic-transport-policy-test.mjs +0 -726
  267. package/scripts/apply-patch-edit-smoke.mjs +0 -71
  268. package/scripts/arg-guard-test.mjs +0 -93
  269. package/scripts/async-notify-settlement-test.mjs +0 -99
  270. package/scripts/atomic-lock-tryonce-test.mjs +0 -125
  271. package/scripts/background-task-meta-smoke.mjs +0 -38
  272. package/scripts/bench-run.mjs +0 -508
  273. package/scripts/boot-smoke.mjs +0 -137
  274. package/scripts/build-runtime-windows.ps1 +0 -242
  275. package/scripts/channel-daemon-smoke.mjs +0 -1103
  276. package/scripts/code-graph-aggregate-cwd-test.mjs +0 -158
  277. package/scripts/code-graph-disk-hit-test.mjs +0 -290
  278. package/scripts/code-graph-dispatch-test.mjs +0 -96
  279. package/scripts/code-graph-root-federation-test.mjs +0 -273
  280. package/scripts/compact-active-turn-test.mjs +0 -68
  281. package/scripts/compact-file-reattach-test.mjs +0 -88
  282. package/scripts/compact-pressure-test.mjs +0 -788
  283. package/scripts/compact-prior-context-flatten-test.mjs +0 -252
  284. package/scripts/compact-recall-digest-test.mjs +0 -57
  285. package/scripts/compact-smoke.mjs +0 -1017
  286. package/scripts/compact-trigger-migration-smoke.mjs +0 -261
  287. package/scripts/compacted-placeholder-scrub-test.mjs +0 -63
  288. package/scripts/context-mcp-metering-test.mjs +0 -1406
  289. package/scripts/debounced-skills-async-save-test.mjs +0 -57
  290. package/scripts/deferred-tool-loading-test.mjs +0 -250
  291. package/scripts/desktop-session-bridge-test.mjs +0 -1013
  292. package/scripts/dispatch-persist-recovery-test.mjs +0 -141
  293. package/scripts/embedding-worker-exit-test.mjs +0 -76
  294. package/scripts/execution-completion-dedup-test.mjs +0 -205
  295. package/scripts/execution-pending-resume-kick-test.mjs +0 -151
  296. package/scripts/execution-resume-esc-integration-test.mjs +0 -176
  297. package/scripts/explore-bench-tmp.mjs +0 -36
  298. package/scripts/explore-bench.mjs +0 -248
  299. package/scripts/explore-prompt-policy-test.mjs +0 -256
  300. package/scripts/explore-timeout-cancel-test.mjs +0 -345
  301. package/scripts/find-fuzzy-hidden-test.mjs +0 -267
  302. package/scripts/forwarder-rebind-tail-test.mjs +0 -67
  303. package/scripts/freevar-smoke.mjs +0 -98
  304. package/scripts/gemini-provider-test.mjs +0 -1448
  305. package/scripts/generate-oc-icons.mjs +0 -11
  306. package/scripts/grok-oauth-refresh-race-test.mjs +0 -273
  307. package/scripts/headless-pristine-execution-test.mjs +0 -614
  308. package/scripts/hook-bus-test.mjs +0 -552
  309. package/scripts/ingest-pure-conversation-smoke.mjs +0 -175
  310. package/scripts/internal-comms-bench-test.mjs +0 -226
  311. package/scripts/internal-comms-bench.mjs +0 -853
  312. package/scripts/internal-comms-smoke.mjs +0 -277
  313. package/scripts/internal-tools-normalization-test.mjs +0 -62
  314. package/scripts/interrupted-turn-history-test.mjs +0 -399
  315. package/scripts/lead-workflow-smoke.mjs +0 -592
  316. package/scripts/lifecycle-api-test.mjs +0 -137
  317. package/scripts/live-share-test.mjs +0 -148
  318. package/scripts/live-worker-smoke.mjs +0 -333
  319. package/scripts/log-writer-guard-smoke.mjs +0 -131
  320. package/scripts/maintenance-default-routes-test.mjs +0 -164
  321. package/scripts/max-output-recovery-persist-test.mjs +0 -81
  322. package/scripts/max-output-recovery-test.mjs +0 -368
  323. package/scripts/mcp-client-normalization-test.mjs +0 -45
  324. package/scripts/mcp-grace-deferred-test.mjs +0 -225
  325. package/scripts/memory-core-input-test.mjs +0 -167
  326. package/scripts/memory-cycle-routing-test.mjs +0 -111
  327. package/scripts/memory-meta-concurrency-test.mjs +0 -20
  328. package/scripts/memory-pg-recovery-test.mjs +0 -59
  329. package/scripts/memory-rule-contract-test.mjs +0 -93
  330. package/scripts/model-list-sanitize-test.mjs +0 -37
  331. package/scripts/mouse-tracking-restore-smoke.mjs +0 -107
  332. package/scripts/native-edit-wire-test.mjs +0 -152
  333. package/scripts/notify-completion-mirror-test.mjs +0 -73
  334. package/scripts/openai-oauth-refresh-race-test.mjs +0 -120
  335. package/scripts/openai-oauth-ws-1006-retry-test.mjs +0 -954
  336. package/scripts/openai-ws-early-settle-test.mjs +0 -216
  337. package/scripts/output-style-bench.mjs +0 -292
  338. package/scripts/output-style-smoke.mjs +0 -145
  339. package/scripts/parent-abort-link-test.mjs +0 -90
  340. package/scripts/patch-binary-cache-test.mjs +0 -181
  341. package/scripts/path-suffix-test.mjs +0 -57
  342. package/scripts/pending-completion-drop-test.mjs +0 -239
  343. package/scripts/pending-messages-lock-nonblocking-test.mjs +0 -62
  344. package/scripts/pretool-ask-runtime-test.mjs +0 -54
  345. package/scripts/process-lifecycle-test.mjs +0 -461
  346. package/scripts/prompt-immediate-render-test.mjs +0 -89
  347. package/scripts/prompt-input-parity-test.mjs +0 -145
  348. package/scripts/provider-admission-scheduler-test.mjs +0 -681
  349. package/scripts/provider-contract-test.mjs +0 -632
  350. package/scripts/provider-stream-stall-test.mjs +0 -276
  351. package/scripts/provider-toolcall-test.mjs +0 -3063
  352. package/scripts/reactive-compact-persist-smoke.mjs +0 -203
  353. package/scripts/recall-bench.mjs +0 -359
  354. package/scripts/resource-admission-test.mjs +0 -792
  355. package/scripts/result-classification-test.mjs +0 -75
  356. package/scripts/rg-runner-test.mjs +0 -240
  357. package/scripts/routing-corpus-test.mjs +0 -349
  358. package/scripts/sanitize-tool-pairs-test.mjs +0 -260
  359. package/scripts/session-bench-cache-break-test.mjs +0 -102
  360. package/scripts/session-bench.mjs +0 -1708
  361. package/scripts/session-context-bench.mjs +0 -344
  362. package/scripts/session-heartbeat-lifecycle-test.mjs +0 -68
  363. package/scripts/session-ingest-compaction-smoke.mjs +0 -279
  364. package/scripts/session-ingest-smoke.mjs +0 -177
  365. package/scripts/session-orphan-sweep-test.mjs +0 -109
  366. package/scripts/set-effort-config-test.mjs +0 -41
  367. package/scripts/shell-failure-diagnostics-test.mjs +0 -280
  368. package/scripts/shell-hardening-test.mjs +0 -209
  369. package/scripts/shell-jobs-windows-hide-test.mjs +0 -164
  370. package/scripts/ship-mode-test.mjs +0 -98
  371. package/scripts/smoke-loop-failure-summary-test.mjs +0 -38
  372. package/scripts/smoke-loop-failure-summary.mjs +0 -16
  373. package/scripts/smoke-loop-report.mjs +0 -221
  374. package/scripts/smoke-loop.mjs +0 -202
  375. package/scripts/smoke-runtime-negative.ps1 +0 -106
  376. package/scripts/statusline-quota-hysteresis-test.mjs +0 -62
  377. package/scripts/steering-drain-buckets-test.mjs +0 -467
  378. package/scripts/steering-fold-provenance-test.mjs +0 -71
  379. package/scripts/streaming-tail-window-test.mjs +0 -175
  380. package/scripts/submit-commandbusy-race-test.mjs +0 -257
  381. package/scripts/task-bench.mjs +0 -205
  382. package/scripts/terminal-bench-isolation-guards-test.mjs +0 -102
  383. package/scripts/termio-input-smoke.mjs +0 -199
  384. package/scripts/tool-result-hook-test.mjs +0 -48
  385. package/scripts/tool-smoke.mjs +0 -2453
  386. package/scripts/tool-tui-presentation-test.mjs +0 -286
  387. package/scripts/toolcall-args-test.mjs +0 -239
  388. package/scripts/tui-ambiguous-width-test.mjs +0 -113
  389. package/scripts/tui-background-failure-smoke.mjs +0 -73
  390. package/scripts/tui-perf-run.ps1 +0 -26
  391. package/scripts/tui-render-smoke.mjs +0 -90
  392. package/scripts/tui-runtime-load-bench-entry.jsx +0 -608
  393. package/scripts/tui-runtime-load-bench.mjs +0 -50
  394. package/scripts/tui-store-frame-batch-test.mjs +0 -99
  395. package/scripts/tui-transcript-jitter-harness-entry.jsx +0 -383
  396. package/scripts/tui-transcript-perf-test.mjs +0 -934
  397. package/scripts/usage-metrics-epoch-smoke.mjs +0 -114
  398. package/scripts/verify-release-assets-test.mjs +0 -281
  399. package/scripts/web-fetch-routing-test.mjs +0 -158
  400. package/scripts/webhook-smoke.mjs +0 -201
  401. package/scripts/windows-hide-spawn-options-test.mjs +0 -19
  402. package/scripts/worker-notify-rejection-test.mjs +0 -51
  403. package/scripts/write-backpressure-test.mjs +0 -147
@@ -1,1017 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from 'node:module';
3
- import { semanticCompactMessages, recallFastTrackCompactMessages, SUMMARY_PREFIX, COMPACT_TYPE_SEMANTIC, COMPACT_TYPE_RECALL_FASTTRACK, normalizeCompactType } from '../src/runtime/agent/orchestrator/session/compact.mjs';
4
- import { agentLoop } from '../src/runtime/agent/orchestrator/session/loop.mjs';
5
- import { estimateMessagesTokens, estimateToolSchemaTokens, providerTokenCalibration } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
6
- import { runSessionCompaction } from '../src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs';
7
- import { autoCompactWindowForRoute, summarizeGatewayUsage } from '../src/vendor/statusline/src/gateway/route-meta.mjs';
8
-
9
- function assert(condition, message) {
10
- if (!condition) throw new Error(message);
11
- }
12
-
13
- function findSummary(messages) {
14
- return messages.find((m) => m?.role === 'user' && typeof m.content === 'string' && m.content.startsWith(SUMMARY_PREFIX));
15
- }
16
-
17
- function previousSummaryBlock(prompt) {
18
- const open = '<previous-summary>';
19
- const close = '</previous-summary>';
20
- const start = String(prompt || '').indexOf(open);
21
- if (start < 0) return '';
22
- const end = String(prompt || '').indexOf(close, start + open.length);
23
- if (end < 0) return '';
24
- return String(prompt).slice(start + open.length, end);
25
- }
26
-
27
- function criticalContextBody(summaryText) {
28
- const text = String(summaryText || '');
29
- const idx = text.indexOf('## Critical Context');
30
- if (idx < 0) return '';
31
- const after = text.slice(idx + '## Critical Context'.length);
32
- const next = after.search(/\n##\s+/);
33
- return (next < 0 ? after : after.slice(0, next)).trim();
34
- }
35
-
36
- function hasNonPlaceholderCriticalContext(summaryText) {
37
- const body = criticalContextBody(summaryText);
38
- if (!body) return false;
39
- return body.split('\n').map((line) => line.trim()).filter(Boolean).some((line) => !/^-\s*\(none\)\s*$/i.test(line));
40
- }
41
-
42
- function priorBlockRetainsSentinelOrContext(prompt, sentinel) {
43
- const block = previousSummaryBlock(prompt);
44
- if (!block) return false;
45
- return block.includes(sentinel) || hasNonPlaceholderCriticalContext(block);
46
- }
47
-
48
- const bootstrapContextMessages = [
49
- { role: 'system', content: 'base rules stay exact' },
50
- { role: 'system', content: 'role/system rules stay exact' },
51
- { role: 'system', content: 'project/session memory/meta stay exact', cacheTier: 'tier3' },
52
- { role: 'user', content: '<system-reminder>\nvolatile cwd C:\\Project\\mixdog stays exact\n</system-reminder>' },
53
- { role: 'assistant', content: '.' },
54
- { role: 'user', content: 'fresh real task has no prior compactable history' },
55
- ];
56
- let bootstrapSemanticCalls = 0;
57
- try {
58
- await semanticCompactMessages({ name: 'bootstrap-smoke', async send() { bootstrapSemanticCalls += 1; return { content: 'bad' }; } }, bootstrapContextMessages, 'fake-model', 1_000, { force: true });
59
- } catch (err) {
60
- assert(/no compactable prior history/.test(String(err?.message || err)), 'semantic compact should reject fresh bootstrap-only history before provider call');
61
- }
62
- assert(bootstrapSemanticCalls === 0, 'semantic compact must not spend a provider call compacting bootstrap reminders');
63
-
64
- const statusRoute = {
65
- provider: 'openai',
66
- model: 'gpt-5.5',
67
- contextWindow: 950_000,
68
- rawContextWindow: 1_000_000,
69
- autoCompactTokenLimit: 950_000,
70
- };
71
- assert(autoCompactWindowForRoute(statusRoute) === 950_000, 'auto compact window should match the effective compact capacity');
72
- const usageSummary = summarizeGatewayUsage(statusRoute, { usage: { inputTokens: 900_000, outputTokens: 1 } });
73
- assert(usageSummary.contextUsedPct === 94.74, `statusline usage should use effective compact capacity: ${usageSummary.contextUsedPct}`);
74
-
75
- let semanticCalls = 0;
76
- const semanticProvider = {
77
- name: 'semantic-smoke',
78
- async send(_messages, _model, _tools, opts) {
79
- semanticCalls += 1;
80
- assert(opts?.maxOutputTokens === 4_096, 'semantic compact should request the summary output cap');
81
- return { content: '## Goal\n- continue compact smoke\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- older turn summarized\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- continue\n\n## Critical Context\n- src/runtime/agent/orchestrator/session/compact.mjs\n\n## Relevant Files\n- src/runtime/agent/orchestrator/session/compact.mjs: compact logic' };
82
- },
83
- };
84
- const semanticMessages = [
85
- { role: 'system', content: 'system rules stay mandatory' },
86
- { role: 'user', content: 'older request about src/runtime/agent/orchestrator/session/compact.mjs' },
87
- { role: 'assistant', content: 'older answer with useful details' },
88
- { role: 'user', content: 'current request should remain verbatim' },
89
- ];
90
- const semanticNoop = await semanticCompactMessages(semanticProvider, semanticMessages, 'fake-model', 5_000, { tailTurns: 1 });
91
- assert(semanticNoop.semantic === false && semanticCalls === 0, 'semantic compact should still no-op below budget unless forced');
92
- const semanticForced = await semanticCompactMessages(semanticProvider, semanticMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
93
- assert(semanticForced.semantic === true && semanticCalls === 1, 'forced semantic compact should run even when the local estimate fits');
94
- assert(semanticForced.compactType === COMPACT_TYPE_SEMANTIC, 'semantic compact should report compact type 1');
95
- assert(findSummary(semanticForced.messages), 'forced semantic compact should insert an anchored summary');
96
-
97
- let wholeTranscriptForcedCalls = 0;
98
- const wholeTranscriptProvider = {
99
- name: 'whole-transcript-forced-smoke',
100
- async send() {
101
- wholeTranscriptForcedCalls += 1;
102
- return { content: '## Goal\n- continue whole-transcript forced compact\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- oldest turn summarized\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- continue\n\n## Critical Context\n- compact.mjs\n\n## Relevant Files\n- compact.mjs' };
103
- },
104
- };
105
- const wholeTranscriptForced = await semanticCompactMessages(wholeTranscriptProvider, semanticMessages, 'fake-model', 5_000, { force: true });
106
- assert(wholeTranscriptForced.semantic === true && wholeTranscriptForcedCalls === 1, 'forced semantic compact must summarize when tail budget would preserve the whole live transcript (default tailTurns)');
107
- assert(findSummary(wholeTranscriptForced.messages), 'whole-transcript forced semantic compact should insert an anchored summary');
108
-
109
- // Manual /compact must reduce the active session transcript directly through
110
- // semantic compaction even when main-session policy is recall-fasttrack. If the
111
- // override regresses, this test enters Memory first (unregistered in this smoke)
112
- // and reports recall-fasttrack metadata before falling back.
113
- let manualDirectCalls = 0;
114
- let manualDirectPrompt = '';
115
- const manualDirectMessages = [
116
- { role: 'system', content: 'MANUAL_SYSTEM_RULE_STAYS_EXACT' },
117
- {
118
- role: 'user',
119
- content: '# Session\nCwd: C:\\Injected\\Noise\nModel: fake-model\nWorkflow: Solo\n\n# Additional context\nOLD_ADDITIONAL_CONTEXT_INJECTION\n\n# Prefetch\nOLD_PREFETCH_INJECTION\n\n# Task\nREAL_OLD_USER_REQUEST\n<system-reminder>OLD_SYSTEM_REMINDER_INJECTION</system-reminder>\n<memory-context>OLD_MEMORY_CONTEXT_INJECTION</memory-context>',
120
- },
121
- {
122
- role: 'assistant',
123
- content: 'REAL_OLD_ASSISTANT_REPLY\n```text\nOLD_CODE_FENCE_TRACE\n```',
124
- toolCalls: [{ id: 'OLD_TOOL_CALL_ID', name: 'OLD_TOOL_CALL_TRACE', arguments: '{"password":"OLD_TOOL_SECRET"}' }],
125
- },
126
- { role: 'tool', toolCallId: 'OLD_TOOL_CALL_ID', content: 'OLD_TOOL_RESULT_TRACE' },
127
- { role: 'developer', content: 'OLD_DEVELOPER_INJECTION' },
128
- { role: 'user', content: 'Reference files:\nOLD_REFERENCE_FILE_INJECTION' },
129
- { role: 'user', content: '[mixdog-runtime] OLD_RUNTIME_INJECTION' },
130
- { role: 'user', content: 'CURRENT_REQUEST_STAYS_VERBATIM' },
131
- { role: 'assistant', content: 'CURRENT_ASSISTANT_STAYS_VERBATIM' },
132
- ];
133
- const manualDirectSession = {
134
- id: `compact-smoke-manual-direct-${process.pid}`,
135
- provider: 'manual-direct-smoke',
136
- model: 'fake-model',
137
- contextWindow: 100_000,
138
- compactBoundaryTokens: 100_000,
139
- messages: manualDirectMessages.map((m) => ({ ...m })),
140
- tools: [],
141
- compaction: {
142
- type: COMPACT_TYPE_RECALL_FASTTRACK,
143
- compactType: COMPACT_TYPE_RECALL_FASTTRACK,
144
- tailTurns: 1,
145
- },
146
- };
147
- const manualDirect = await runSessionCompaction(manualDirectSession, {
148
- mode: 'manual',
149
- force: true,
150
- compactType: COMPACT_TYPE_SEMANTIC,
151
- filterOldHistoryForIngest: true,
152
- provider: {
153
- name: 'manual-direct-smoke',
154
- async send(sentMessages) {
155
- manualDirectCalls += 1;
156
- manualDirectPrompt = sentMessages.map((m) => String(m?.content || '')).join('\n');
157
- return { content: '## Goal\n- compact only the active session\n\n## Constraints & Preferences\n- preserve the recent tail\n\n## Progress\n### Done\n- reduced old transcript content\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- use direct semantic compaction\n\n## Next Steps\n- continue from the preserved tail\n\n## Critical Context\n- active session only\n\n## Relevant Files\n- session-crud.mjs' };
158
- },
159
- },
160
- model: 'fake-model',
161
- });
162
- assert(manualDirectCalls === 1, 'manual direct compact should make exactly one semantic provider call');
163
- assert(manualDirect.compactType === COMPACT_TYPE_SEMANTIC, 'manual direct compact must bypass recall-fasttrack policy');
164
- assert(manualDirect.semanticCompact === true, 'manual direct compact should produce a semantic summary');
165
- assert(manualDirect.recallFastTrackError == null, 'manual direct compact must not touch Memory recall');
166
- assert(findSummary(manualDirectSession.messages), 'manual direct compact should replace old active-session history with a handoff summary');
167
- assert(manualDirectPrompt.includes('REAL_OLD_USER_REQUEST'), 'manual compact prompt should retain genuine old user prose');
168
- assert(manualDirectPrompt.includes('REAL_OLD_ASSISTANT_REPLY'), 'manual compact prompt should retain genuine old assistant prose');
169
- for (const excluded of [
170
- 'OLD_ADDITIONAL_CONTEXT_INJECTION',
171
- 'OLD_PREFETCH_INJECTION',
172
- 'OLD_SYSTEM_REMINDER_INJECTION',
173
- 'OLD_MEMORY_CONTEXT_INJECTION',
174
- 'OLD_CODE_FENCE_TRACE',
175
- 'OLD_TOOL_CALL_TRACE',
176
- 'OLD_TOOL_SECRET',
177
- 'OLD_TOOL_RESULT_TRACE',
178
- 'OLD_DEVELOPER_INJECTION',
179
- 'OLD_REFERENCE_FILE_INJECTION',
180
- 'OLD_RUNTIME_INJECTION',
181
- ]) {
182
- assert(!manualDirectPrompt.includes(excluded), `manual compact old-history filter must exclude ${excluded}`);
183
- }
184
- assert(manualDirectSession.messages.some((m) => m.role === 'system' && m.content === 'MANUAL_SYSTEM_RULE_STAYS_EXACT'), 'manual compact must preserve system rules separately');
185
- assert(manualDirectSession.messages.some((m) => m.role === 'user' && m.content === 'CURRENT_REQUEST_STAYS_VERBATIM'), 'manual compact must preserve the recent user turn verbatim');
186
- assert(manualDirectSession.messages.some((m) => m.role === 'assistant' && m.content === 'CURRENT_ASSISTANT_STAYS_VERBATIM'), 'manual compact must preserve the recent assistant turn verbatim');
187
-
188
- assert(normalizeCompactType('type1') === COMPACT_TYPE_SEMANTIC, 'type1 should resolve to semantic compact');
189
- assert(normalizeCompactType('recall-fasttrack') === COMPACT_TYPE_RECALL_FASTTRACK, 'type2 should resolve to recall fast-track compact');
190
- // Alias parity: snake_case + dash + no-dash spellings of fast-track must all
191
- // resolve to the same recall-fasttrack type, and unknown values fall back.
192
- for (const alias of ['fast-track', 'fast_track', 'fasttrack', 'recall_fasttrack', 'recall-fast-track', 'type-2', '2']) {
193
- assert(normalizeCompactType(alias) === COMPACT_TYPE_RECALL_FASTTRACK, `alias ${alias} should resolve to recall fast-track`);
194
- }
195
- for (const alias of ['1', 'type-1', 'semantic', 'summary', 'bench1']) {
196
- assert(normalizeCompactType(alias) === COMPACT_TYPE_SEMANTIC, `alias ${alias} should resolve to semantic`);
197
- }
198
- assert(normalizeCompactType('totally-unknown') === COMPACT_TYPE_SEMANTIC, 'unknown compact type should fall back to the default (semantic)');
199
- assert(normalizeCompactType('totally-unknown', COMPACT_TYPE_RECALL_FASTTRACK) === COMPACT_TYPE_RECALL_FASTTRACK, 'unknown compact type should honor the caller-provided fallback');
200
- const recallFastTrackForced = recallFastTrackCompactMessages(semanticMessages, 5_000, {
201
- tailTurns: 1,
202
- force: true,
203
- recallText: 'recall hit: src/runtime/agent/orchestrator/session/compact.mjs and next steps preserved',
204
- query: 'compact smoke recall fast-track',
205
- querySha: 'smoketest',
206
- });
207
- assert(recallFastTrackForced.recallFastTrack === true, 'recall fast-track compact should mark type2 result');
208
- assert(recallFastTrackForced.compactType === COMPACT_TYPE_RECALL_FASTTRACK, 'recall fast-track compact should report compact type 2');
209
- assert(findSummary(recallFastTrackForced.messages), 'recall fast-track compact should insert an anchored summary');
210
-
211
- // Recall fast-track must preserve recent STRUCTURED state (assistant + tool +
212
- // developer), not collapse the tail to user-only. The newest turn carries an
213
- // assistant tool_call, its tool_result, and a developer note — all three must
214
- // survive into the compacted tail (older turns are anchored by the recall text).
215
- const structuredTailMessages = [
216
- { role: 'system', content: 'system rules stay mandatory' },
217
- { role: 'user', content: 'older request that should be summarized away' },
218
- { role: 'assistant', content: 'older answer' },
219
- { role: 'user', content: 'apply the edit to src/runtime/agent/orchestrator/session/compact.mjs' },
220
- { role: 'assistant', content: 'editing now', toolCalls: [{ id: 'call_edit_1', name: 'apply_patch', arguments: '{"file":"compact.mjs"}' }] },
221
- { role: 'tool', toolCallId: 'call_edit_1', content: 'patch applied: +5 -1' },
222
- { role: 'developer', content: 'developer steering note: keep tail structured' },
223
- ];
224
- const structuredRecall = recallFastTrackCompactMessages(structuredTailMessages, 5_000, {
225
- tailTurns: 1,
226
- force: true,
227
- recallText: 'recall hit: older request summarized',
228
- query: 'structured tail recall',
229
- querySha: 'structuredtail',
230
- });
231
- assert(structuredRecall.recallFastTrack === true, 'structured recall fast-track should mark type2 result');
232
- const structuredRoles = structuredRecall.messages.map((m) => m.role);
233
- assert(structuredRoles.includes('assistant'), 'recall fast-track tail should preserve recent assistant turns');
234
- assert(structuredRoles.includes('tool'), 'recall fast-track tail should preserve recent tool results');
235
- assert(structuredRoles.includes('developer'), 'recall fast-track tail should preserve recent developer steering');
236
- const structuredToolMsg = structuredRecall.messages.find((m) => m.role === 'tool');
237
- assert(structuredToolMsg?.toolCallId === 'call_edit_1', 'preserved tool result should keep its tool_call pairing id');
238
- const structuredAssistant = structuredRecall.messages.find((m) => m.role === 'assistant' && Array.isArray(m.toolCalls));
239
- assert(structuredAssistant, 'preserved assistant turn should keep its tool_calls');
240
-
241
- // Semantic malformed-summary repair: a non-empty but unstructured provider
242
- // response must be deterministically repaired into the anchored template shape
243
- // instead of being injected verbatim as the only summary.
244
- let malformedCalls = 0;
245
- const malformedProvider = {
246
- name: 'malformed-smoke',
247
- async send() {
248
- malformedCalls += 1;
249
- return { content: 'just a freeform blob with no template sections at all' };
250
- },
251
- };
252
- const malformedResult = await semanticCompactMessages(malformedProvider, semanticMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
253
- assert(malformedCalls === 1, 'malformed semantic compact should still call the provider once');
254
- assert(malformedResult.summaryRepaired === true, 'malformed semantic summary should be flagged repaired');
255
- const malformedSummaryMsg = findSummary(malformedResult.messages);
256
- assert(malformedSummaryMsg, 'repaired semantic compact should still insert an anchored summary');
257
- for (const section of ['## Goal', '## Progress', '## Next Steps', '## Critical Context', '## Relevant Files']) {
258
- assert(malformedSummaryMsg.content.includes(section), `repaired summary should contain required section ${section}`);
259
- }
260
- assert(malformedSummaryMsg.content.includes('freeform blob'), 'repaired summary should retain the original provider content (routed into Critical Context)');
261
- // A well-formed provider summary must NOT be flagged as repaired.
262
- assert(semanticForced.summaryRepaired !== true, 'well-formed semantic summary should not be repaired');
263
-
264
- const REQUIRED_SECTIONS = ['## Goal', '## Constraints', '## Progress', '## Key Decisions', '## Next Steps', '## Critical Context', '## Relevant Files'];
265
-
266
- // Partial malformed summary: present some sections but omit Critical Context /
267
- // Relevant Files. Schema enforcement must now require ALL sections, so a
268
- // partial summary is repaired (not passed through) and every anchor is present.
269
- let partialCalls = 0;
270
- const partialProvider = {
271
- name: 'partial-smoke',
272
- async send() {
273
- partialCalls += 1;
274
- return { content: '## Goal\n- partial goal\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- did a thing\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- finish' };
275
- },
276
- };
277
- const partialResult = await semanticCompactMessages(partialProvider, semanticMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
278
- assert(partialCalls === 1, 'partial semantic compact should still call the provider once');
279
- assert(partialResult.summaryRepaired === true, 'partial semantic summary (missing sections) should be flagged repaired');
280
- const partialSummaryMsg = findSummary(partialResult.messages);
281
- assert(partialSummaryMsg, 'repaired partial semantic compact should still insert an anchored summary');
282
- for (const section of REQUIRED_SECTIONS) {
283
- assert(partialSummaryMsg.content.includes(section), `repaired partial summary must contain required section ${section}`);
284
- }
285
- assert(partialSummaryMsg.content.includes('partial goal'), 'repaired partial summary should preserve the provider-supplied Goal body');
286
- assert(partialSummaryMsg.content.includes('did a thing'), 'repaired partial summary should preserve the provider-supplied Progress body');
287
-
288
- // Low-budget post-fit schema preservation: even when token pressure forces the
289
- // fitter to shrink section bodies, the final injected SUMMARY_PREFIX message
290
- // must still carry EVERY required section anchor (no trailing section dropped).
291
- let lowBudgetCalls = 0;
292
- const lowBudgetProvider = {
293
- name: 'low-budget-smoke',
294
- async send() {
295
- lowBudgetCalls += 1;
296
- const longBody = 'detail '.repeat(400);
297
- return { content: `## Goal\n- ${longBody}\n\n## Constraints & Preferences\n- ${longBody}\n\n## Progress\n### Done\n- ${longBody}\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- ${longBody}\n\n## Next Steps\n- ${longBody}\n\n## Critical Context\n- ${longBody}\n\n## Relevant Files\n- src/runtime/agent/orchestrator/session/compact.mjs: ${longBody}` };
298
- },
299
- };
300
- const lowBudgetResult = await semanticCompactMessages(lowBudgetProvider, semanticMessages, 'fake-model', 4_500, { tailTurns: 1, force: true });
301
- assert(lowBudgetCalls === 1, 'low-budget semantic compact should call the provider once');
302
- const lowBudgetSummaryMsg = findSummary(lowBudgetResult.messages);
303
- assert(lowBudgetSummaryMsg, 'low-budget semantic compact should still insert an anchored summary');
304
- for (const section of REQUIRED_SECTIONS) {
305
- assert(lowBudgetSummaryMsg.content.includes(section), `all required summary anchors should survive low-budget fitting: missing ${section}`);
306
- }
307
-
308
- // Heading-anchor validation: a summary where a required anchor (## Relevant
309
- // Files) appears only as inline prose inside a body — never as a real heading —
310
- // must be treated as malformed and repaired, not passed through. Also confirm
311
- // `## Constraints & Preferences` satisfies the `## Constraints` anchor (no
312
- // false repair) when every other heading is a real heading.
313
- let proseAnchorCalls = 0;
314
- const proseAnchorProvider = {
315
- name: 'prose-anchor-smoke',
316
- async send() {
317
- proseAnchorCalls += 1;
318
- // All 7 real headings present, including `## Constraints & Preferences`.
319
- // A bullet mentions `## Relevant Files` as prose — substring matching would
320
- // wrongly double-count, but heading-based validation still sees the real
321
- // heading, so this is well-formed and must NOT be repaired.
322
- return { content: '## Goal\n- ship it\n\n## Constraints & Preferences\n- mention of ## Relevant Files in prose\n\n## Progress\n### Done\n- x\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- go\n\n## Critical Context\n- (none)\n\n## Relevant Files\n- compact.mjs' };
323
- },
324
- };
325
- const proseAnchorResult = await semanticCompactMessages(proseAnchorProvider, semanticMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
326
- assert(proseAnchorCalls === 1, 'prose-anchor semantic compact should call the provider once');
327
- assert(proseAnchorResult.summaryRepaired !== true, 'well-formed summary using "## Constraints & Preferences" heading should NOT be repaired');
328
-
329
- // Now drop the real `## Relevant Files` heading but keep an inline prose
330
- // mention of it; substring validation would pass, heading validation repairs.
331
- let missingHeadingCalls = 0;
332
- const missingHeadingProvider = {
333
- name: 'missing-heading-smoke',
334
- async send() {
335
- missingHeadingCalls += 1;
336
- return { content: '## Goal\n- ship it\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- x\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- see ## Relevant Files below\n\n## Critical Context\n- (none)' };
337
- },
338
- };
339
- const missingHeadingResult = await semanticCompactMessages(missingHeadingProvider, semanticMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
340
- assert(missingHeadingCalls === 1, 'missing-heading semantic compact should call the provider once');
341
- assert(missingHeadingResult.summaryRepaired === true, 'a summary missing the real ## Relevant Files heading (only inline prose) must be repaired');
342
-
343
- // Semantic compaction must redact RAW (non-JSON) tool-call argument strings so
344
- // secrets never reach preserved facts or the compaction prompt transcript
345
- // metadata. The compaction provider here echoes the prompt it receives, so any
346
- // leaked secret fragment would surface in the prompt and the output.
347
- const SECRET_FRAGMENTS = ['abc.def', 'abc def', 'sk-supersecret-123', 's3cr3tcookie', 'xtok-prefixed-789', 'bear-tok-456'];
348
- let secretSeenPrompt = '';
349
- const secretProvider = {
350
- name: 'secret-redaction-smoke',
351
- async send(sentMessages) {
352
- // Capture the exact user prompt the compaction call receives so the test
353
- // can assert no raw secret fragment reached the transcript metadata.
354
- secretSeenPrompt = sentMessages.map((mm) => (typeof mm?.content === 'string' ? mm.content : JSON.stringify(mm?.content ?? ''))).join('\n');
355
- return { content: '## Goal\n- redact secrets\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- (none)\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- (none)\n\n## Critical Context\n- (none)\n\n## Relevant Files\n- (none)' };
356
- },
357
- };
358
- const secretMessages = [
359
- { role: 'system', content: 'system rules stay mandatory' },
360
- { role: 'user', content: 'older request that calls a tool with secret args' },
361
- { role: 'assistant', content: 'calling curl', toolCalls: [
362
- { id: 'call_secret_1', name: 'curl', arguments: 'authorization: Bearer abc.def' },
363
- { id: 'call_secret_2', name: 'login', arguments: 'password="abc def"' },
364
- { id: 'call_secret_3', name: 'http', arguments: '{"url":"https://x","api_key":"sk-supersecret-123"}' },
365
- { id: 'call_secret_4', name: 'fetch', arguments: 'cookie: session=s3cr3tcookie; path=/' },
366
- // Prefixed sensitive key variants must be caught by the raw redactor too.
367
- { id: 'call_secret_5', name: 'a', arguments: 'access_token=abc.def' },
368
- { id: 'call_secret_6', name: 'b', arguments: 'access-token=abc.def' },
369
- { id: 'call_secret_7', name: 'c', arguments: 'x-api-key=xtok-prefixed-789' },
370
- { id: 'call_secret_8', name: 'd', arguments: 'bearer_token: Bearer bear-tok-456' },
371
- ] },
372
- { role: 'tool', toolCallId: 'call_secret_1', content: 'ok' },
373
- { role: 'tool', toolCallId: 'call_secret_2', content: 'ok' },
374
- { role: 'tool', toolCallId: 'call_secret_3', content: 'ok' },
375
- { role: 'tool', toolCallId: 'call_secret_4', content: 'ok' },
376
- { role: 'tool', toolCallId: 'call_secret_5', content: 'ok' },
377
- { role: 'tool', toolCallId: 'call_secret_6', content: 'ok' },
378
- { role: 'tool', toolCallId: 'call_secret_7', content: 'ok' },
379
- { role: 'tool', toolCallId: 'call_secret_8', content: 'ok' },
380
- { role: 'user', content: 'current request stays verbatim' },
381
- ];
382
- const secretResult = await semanticCompactMessages(secretProvider, secretMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
383
- // 1) No secret fragment may appear in the compaction prompt (preserved facts +
384
- // conversation-history tool_calls metadata are both built from the head).
385
- for (const frag of SECRET_FRAGMENTS) {
386
- assert(!secretSeenPrompt.includes(frag), `compaction prompt must not leak secret fragment: ${frag}`);
387
- }
388
- // Tool names must still be readable in the prompt (redaction is value-only).
389
- assert(/curl|login|http|fetch/.test(secretSeenPrompt), 'tool names should remain readable in the compaction prompt');
390
- // 2) No secret fragment may appear anywhere in the compacted output messages.
391
- const secretSerialized = JSON.stringify(secretResult.messages);
392
- for (const frag of SECRET_FRAGMENTS) {
393
- assert(!secretSerialized.includes(frag), `compacted output must not leak secret fragment: ${frag}`);
394
- }
395
-
396
- // CRITICAL: a sensitive assistant tool call in the PRESERVED RECENT TAIL (not
397
- // the compacted head) is appended verbatim — its toolCalls[].arguments must be
398
- // scrubbed too, or a recent `authorization: Bearer ...` survives in the output.
399
- // Build a transcript whose newest turn carries the secret tool call so it lands
400
- // in the preserved tail; assert the returned messages JSON has no secret
401
- // fragments while tool names/ids remain present.
402
- const tailSecretFragments = ['tail.secret.abc', 'tail-cookie-xyz', 'sk-tail-987'];
403
- const tailSecretMessages = [
404
- { role: 'system', content: 'system rules stay mandatory' },
405
- { role: 'user', content: 'older request to be summarized away' },
406
- { role: 'assistant', content: 'older answer' },
407
- { role: 'user', content: 'current request with a fresh tool call' },
408
- { role: 'assistant', content: 'invoking tools now', toolCalls: [
409
- { id: 'call_tail_1', name: 'curl', arguments: 'authorization: Bearer tail.secret.abc' },
410
- { id: 'call_tail_2', name: 'fetch', arguments: 'cookie: session=tail-cookie-xyz; path=/' },
411
- { id: 'call_tail_3', name: 'http', arguments: '{"url":"https://y","x-api-key":"sk-tail-987"}' },
412
- ] },
413
- { role: 'tool', toolCallId: 'call_tail_1', content: 'ok1' },
414
- { role: 'tool', toolCallId: 'call_tail_2', content: 'ok2' },
415
- { role: 'tool', toolCallId: 'call_tail_3', content: 'ok3' },
416
- ];
417
- // Semantic: tailTurns 1 keeps the newest user turn (and the secret assistant
418
- // turn that follows it) in the preserved tail, while the older turn stays in
419
- // the compacted head.
420
- const tailSemantic = await semanticCompactMessages(semanticProvider, tailSecretMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
421
- const tailSemanticJson = JSON.stringify(tailSemantic.messages);
422
- for (const frag of tailSecretFragments) {
423
- assert(!tailSemanticJson.includes(frag), `semantic preserved-tail must not leak secret fragment: ${frag}`);
424
- }
425
- // Tool names + ids must remain in the preserved-tail assistant message.
426
- for (const token of ['curl', 'fetch', 'http', 'call_tail_1', 'call_tail_2', 'call_tail_3']) {
427
- assert(tailSemanticJson.includes(token), `semantic preserved tail should retain tool name/id: ${token}`);
428
- }
429
- // The preserved assistant turn must still carry its toolCalls structure.
430
- const tailSemanticAsst = tailSemantic.messages.find((m) => m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length === 3);
431
- assert(tailSemanticAsst, 'semantic preserved tail should keep the assistant toolCalls structure intact');
432
-
433
- // Recall fast-track: same transcript; the secret tool call lands in the
434
- // structured preserved tail and must be scrubbed in the returned messages.
435
- const tailRecall = recallFastTrackCompactMessages(tailSecretMessages, 5_000, {
436
- tailTurns: 1,
437
- force: true,
438
- recallText: 'recall hit: older request summarized',
439
- query: 'tail secret recall',
440
- querySha: 'tailsecret',
441
- });
442
- const tailRecallJson = JSON.stringify(tailRecall.messages);
443
- for (const frag of tailSecretFragments) {
444
- assert(!tailRecallJson.includes(frag), `recall-fasttrack preserved-tail must not leak secret fragment: ${frag}`);
445
- }
446
- for (const token of ['curl', 'fetch', 'http', 'call_tail_1', 'call_tail_2', 'call_tail_3']) {
447
- assert(tailRecallJson.includes(token), `recall-fasttrack preserved tail should retain tool name/id: ${token}`);
448
- }
449
- const tailRecallAsst = tailRecall.messages.find((m) => m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length === 3);
450
- assert(tailRecallAsst, 'recall-fasttrack preserved tail should keep the assistant toolCalls structure intact');
451
-
452
- // Preserved-tail redaction must be REDACTION-ONLY: non-sensitive args (incl. a
453
- // long string and a deep, key-ordered object/array) must survive byte- and
454
- // structure-identical (no truncation / summarization / key sorting / caps),
455
- // the string-vs-object `arguments` shape must be preserved, and
456
- // `toolCalls[].function.arguments` must be redacted in both paths.
457
- const LONG_NON_SENSITIVE = `keep ${'x'.repeat(900)} end`; // > any TOOL_ARG cap
458
- const fnSecretFragments = ['fn.secret.aaa', 'fn-cookie-bbb'];
459
- const onlyTailMessages = [
460
- { role: 'system', content: 'system rules stay mandatory' },
461
- { role: 'user', content: 'older request to summarize away' },
462
- { role: 'assistant', content: 'older answer' },
463
- { role: 'user', content: 'current request with mixed tool calls' },
464
- { role: 'assistant', content: 'invoking', toolCalls: [
465
- // Non-sensitive long string arg — must be preserved EXACTLY.
466
- { id: 'call_keep_str', name: 'echo', arguments: LONG_NON_SENSITIVE },
467
- // Object-shape args: provider passes a real object. Non-sensitive nested
468
- // values + key order must survive; only the sensitive key is redacted.
469
- { id: 'call_keep_obj', name: 'cfg', arguments: { zeta: 1, alpha: 'aval', nested: { keepArr: [3, 2, 1], note: 'n' }, password: 'should-vanish-pw' } },
470
- // function.arguments (string JSON) carrying a sensitive key — must redact.
471
- { id: 'call_fn_1', name: 'h', function: { name: 'h', arguments: '{"url":"https://z","authorization":"Bearer fn.secret.aaa"}' } },
472
- // function.arguments (raw string) carrying a cookie secret — must redact.
473
- { id: 'call_fn_2', name: 'g', function: { name: 'g', arguments: 'cookie: sid=fn-cookie-bbb; path=/' } },
474
- ] },
475
- { role: 'tool', toolCallId: 'call_keep_str', content: 'o1' },
476
- { role: 'tool', toolCallId: 'call_keep_obj', content: 'o2' },
477
- { role: 'tool', toolCallId: 'call_fn_1', content: 'o3' },
478
- { role: 'tool', toolCallId: 'call_fn_2', content: 'o4' },
479
- ];
480
-
481
- function checkRedactionOnly(messages, label) {
482
- const asst = messages.find((m) => m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length === 4);
483
- assert(asst, `${label}: preserved tail should keep the 4-call assistant turn`);
484
- const byId = Object.fromEntries(asst.toolCalls.map((tc) => [tc.id, tc]));
485
- // 1) Non-sensitive long string arg unchanged (no truncation/middle-cut).
486
- const strCall = byId.call_keep_str;
487
- assert(typeof strCall.arguments === 'string', `${label}: string arg must stay a string`);
488
- assert(strCall.arguments === LONG_NON_SENSITIVE, `${label}: non-sensitive long string arg must be byte-identical (no truncation)`);
489
- // 2) Object arg keeps shape, insertion order, nested array order/values;
490
- // only the sensitive key value is redacted.
491
- const objCall = byId.call_keep_obj;
492
- assert(objCall.arguments && typeof objCall.arguments === 'object' && !Array.isArray(objCall.arguments), `${label}: object arg must stay an object`);
493
- assert(Object.keys(objCall.arguments).join(',') === 'zeta,alpha,nested,password', `${label}: object key insertion order must be preserved (no sorting)`);
494
- assert(objCall.arguments.zeta === 1 && objCall.arguments.alpha === 'aval', `${label}: non-sensitive primitive/string values preserved`);
495
- assert(JSON.stringify(objCall.arguments.nested.keepArr) === '[3,2,1]', `${label}: nested array order/values preserved (no caps/sort)`);
496
- assert(objCall.arguments.nested.note === 'n', `${label}: deep non-sensitive value preserved`);
497
- assert(objCall.arguments.password === '[redacted]', `${label}: sensitive object key value must be redacted`);
498
- // 3) function.arguments redacted (string-vs-object shape preserved).
499
- const fn1 = byId.call_fn_1;
500
- assert(typeof fn1.function.arguments === 'string', `${label}: function.arguments (JSON string) shape preserved`);
501
- const fn2 = byId.call_fn_2;
502
- assert(typeof fn2.function.arguments === 'string', `${label}: function.arguments (raw string) shape preserved`);
503
- const json = JSON.stringify(messages);
504
- for (const frag of fnSecretFragments) {
505
- assert(!json.includes(frag), `${label}: function.arguments secret must not leak: ${frag}`);
506
- }
507
- assert(!json.includes('should-vanish-pw'), `${label}: object sensitive value must not leak`);
508
- }
509
-
510
- const onlyTailSemantic = await semanticCompactMessages(semanticProvider, onlyTailMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
511
- checkRedactionOnly(onlyTailSemantic.messages, 'semantic');
512
-
513
- const onlyTailRecall = recallFastTrackCompactMessages(onlyTailMessages, 5_000, {
514
- tailTurns: 1,
515
- force: true,
516
- recallText: 'recall hit: older request summarized',
517
- query: 'redaction-only recall',
518
- querySha: 'redactonly',
519
- });
520
- checkRedactionOnly(onlyTailRecall.messages, 'recall-fasttrack');
521
-
522
- // A non-sensitive JSON-string `arguments` payload must be returned BYTE-EXACT
523
- // (no parse/JSON.stringify reformatting) when the redaction-only walk changes
524
- // nothing — whitespace/key-order/number formatting must survive untouched.
525
- const UNTOUCHED_JSON_ARG = '{ "url":"https://keep", "n": 1.50, "list":[3,2,1] }';
526
- const jsonShapeMessages = [
527
- { role: 'system', content: 'system rules stay mandatory' },
528
- { role: 'user', content: 'older request to summarize away' },
529
- { role: 'assistant', content: 'older answer' },
530
- { role: 'user', content: 'current request with a non-sensitive json arg' },
531
- { role: 'assistant', content: 'invoking', toolCalls: [
532
- { id: 'call_json_keep', name: 'req', arguments: UNTOUCHED_JSON_ARG },
533
- ] },
534
- { role: 'tool', toolCallId: 'call_json_keep', content: 'ok' },
535
- ];
536
- function checkJsonByteExact(messages, label) {
537
- const asst = messages.find((m) => m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.some((tc) => tc.id === 'call_json_keep'));
538
- assert(asst, `${label}: preserved tail should keep the json-arg assistant turn`);
539
- const tc = asst.toolCalls.find((c) => c.id === 'call_json_keep');
540
- assert(tc.arguments === UNTOUCHED_JSON_ARG, `${label}: non-sensitive JSON-string arg must be byte-exact (no reserialization)`);
541
- }
542
- const jsonSemantic = await semanticCompactMessages(semanticProvider, jsonShapeMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
543
- checkJsonByteExact(jsonSemantic.messages, 'semantic');
544
- const jsonRecall = recallFastTrackCompactMessages(jsonShapeMessages, 5_000, {
545
- tailTurns: 1, force: true, recallText: 'recall hit', query: 'json byte exact', querySha: 'jsonexact',
546
- });
547
- checkJsonByteExact(jsonRecall.messages, 'recall-fasttrack');
548
-
549
- // No-op fast path: when the transcript already fits and force !== true, the
550
- // returned messages must be the ORIGINAL sanitized transcript UNCHANGED — no
551
- // preserved-tail redaction is applied (prior no-compaction semantics). Here the
552
- // tool arg carries a secret but, because we are under budget and not forced,
553
- // the secret is preserved verbatim (redaction only runs when compaction runs).
554
- const noopMessages = [
555
- { role: 'system', content: 'system rules stay mandatory' },
556
- { role: 'user', content: 'tiny request' },
557
- { role: 'assistant', content: 'tiny', toolCalls: [
558
- { id: 'call_noop', name: 'curl', arguments: 'authorization: Bearer noop-secret-xyz' },
559
- ] },
560
- { role: 'tool', toolCallId: 'call_noop', content: 'ok' },
561
- ];
562
- const noopSemantic = await semanticCompactMessages(semanticProvider, noopMessages, 'fake-model', 1_000_000);
563
- assert(noopSemantic.semantic === false, 'under-budget unforced semantic compact should no-op');
564
- assert(JSON.stringify(noopSemantic.messages).includes('noop-secret-xyz'), 'semantic no-op path must return the original transcript unchanged (no redaction applied)');
565
- const noopRecall = recallFastTrackCompactMessages(noopMessages, 1_000_000, { recallText: 'unused' });
566
- assert(noopRecall.recallFastTrack === false, 'under-budget unforced recall-fasttrack should no-op');
567
- assert(JSON.stringify(noopRecall.messages).includes('noop-secret-xyz'), 'recall-fasttrack no-op path must return the original transcript unchanged (no redaction applied)');
568
-
569
- // Context overflow on send triggers ONE reactive compact-retry: the loop
570
- // marks the session over-threshold and re-enters the pre-send auto-compact
571
- // path, which runs a single semantic compaction (one compact-provider send)
572
- // and re-sends once. If the retry still overflows, the deterministic
573
- // AGENT_CONTEXT_OVERFLOW error is surfaced (main send count 2, compact
574
- // provider send count 1).
575
- const overflowRetryMessages = [{ role: 'system', content: 'system rules stay mandatory' }];
576
- let overflowIndex = 0;
577
- while (estimateMessagesTokens(overflowRetryMessages) + 512 < 8_800) {
578
- overflowRetryMessages.push({ role: 'user', content: `older overflow request ${overflowIndex}: ${'important detail '.repeat(90)}` });
579
- overflowRetryMessages.push({ role: 'assistant', content: `older overflow answer ${overflowIndex}: ${'implementation note '.repeat(90)}` });
580
- overflowIndex += 1;
581
- }
582
- overflowRetryMessages.push({ role: 'user', content: 'current overflow task must stay verbatim' });
583
- let overflowSendCount = 0;
584
- let overflowCompactSendCount = 0;
585
- const overflowProvider = {
586
- name: 'overflow-smoke',
587
- async send(_sentMessages, _model, _tools, opts = {}) {
588
- if (String(opts?.sessionId || '').endsWith(':compact')) {
589
- overflowCompactSendCount += 1;
590
- return { content: 'unexpected compact call' };
591
- }
592
- overflowSendCount += 1;
593
- throw new Error('input tokens exceeds the context window');
594
- },
595
- };
596
- const overflowSession = {
597
- id: 'compact-smoke-overflow',
598
- owner: 'agent',
599
- provider: 'overflow-smoke',
600
- model: 'fake-model',
601
- contextWindow: 12_000,
602
- rawContextWindow: 12_000,
603
- compactBoundaryTokens: 12_000,
604
- compaction: { auto: true, semantic: true },
605
- };
606
- let overflowError = null;
607
- try {
608
- await agentLoop(overflowProvider, overflowRetryMessages, 'fake-model', [], null, process.cwd(), { session: overflowSession, sessionId: overflowSession.id });
609
- } catch (err) {
610
- overflowError = err;
611
- }
612
- assert(overflowError, 'context overflow on send should surface an error, not be silently recovered');
613
- assert(overflowError?.code === 'AGENT_CONTEXT_OVERFLOW', `overflow should surface AGENT_CONTEXT_OVERFLOW, got ${overflowError?.code || overflowError?.message}`);
614
- assert(overflowSendCount === 2, `overflow should send twice (original + one reactive compact-retry), sent=${overflowSendCount}`);
615
- assert(overflowCompactSendCount === 1, `reactive compact-retry should run exactly one in-loop semantic compaction, compactSends=${overflowCompactSendCount}`);
616
-
617
-
618
- function countRealUserTail(messages) {
619
- return messages.filter((m) => m?.role === 'user' && typeof m.content === 'string' && !m.content.startsWith(SUMMARY_PREFIX)).length;
620
- }
621
-
622
- // Recall fast-track: 5 small turns with tailTurns:4 must preserve 4 real user turns.
623
- const fiveTurnMessages = [{ role: 'system', content: 'system rules stay mandatory' }];
624
- for (let i = 0; i < 5; i += 1) {
625
- fiveTurnMessages.push({ role: 'user', content: `recall turn ${i} small request` });
626
- fiveTurnMessages.push({ role: 'assistant', content: `recall turn ${i} small answer` });
627
- }
628
- const fiveTurnRecall = recallFastTrackCompactMessages(fiveTurnMessages, 50_000, {
629
- tailTurns: 4,
630
- force: true,
631
- recallText: 'recall hit: older turns summarized',
632
- query: 'five turn recall',
633
- querySha: 'fiveturn',
634
- });
635
- assert(fiveTurnRecall.recallFastTrack === true, 'five-turn recall fast-track should run');
636
- assert(countRealUserTail(fiveTurnRecall.messages) === 4, `recall tail should keep 4 real user turns, got ${countRealUserTail(fiveTurnRecall.messages)}`);
637
-
638
- // Repeated summary compaction: exactly one SUMMARY_PREFIX; old summary not in live tail.
639
- const priorSummaryBody = `${SUMMARY_PREFIX}\nmessages=1 sha256=abc roles=user:1\n## Goal\n- prior compacted goal\n`;
640
- const repeatedSummaryMessages = [
641
- { role: 'system', content: 'system rules stay mandatory' },
642
- { role: 'user', content: priorSummaryBody },
643
- { role: 'user', content: 'new live request after prior summary' },
644
- { role: 'assistant', content: 'new live answer' },
645
- ];
646
- const repeatedSemantic = await semanticCompactMessages(semanticProvider, repeatedSummaryMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
647
- const repeatedSummaries = repeatedSemantic.messages.filter((m) => m?.role === 'user' && typeof m.content === 'string' && m.content.startsWith(SUMMARY_PREFIX));
648
- assert(repeatedSummaries.length === 1, 'repeated compaction must emit exactly one summary message');
649
- assert(!repeatedSemantic.messages.some((m) => m?.role === 'user' && m.content === priorSummaryBody), 'old summary must not remain as a separate live user message');
650
-
651
- // Huge prior summary must degrade to fit the compaction provider prompt budget.
652
- const PRIOR_PROMPT_FIT_SENTINEL = 'PRIOR_PROMPT_FIT_SENTINEL_keep_me';
653
- const hugePriorSummaryBody = `${SUMMARY_PREFIX}\nmessages=1 sha256=hugeprior roles=user:1\n## Goal\n- huge prior goal\n## Critical Context\n- ${PRIOR_PROMPT_FIT_SENTINEL}\n` + '\n## Progress\n- ' + 'p'.repeat(250_000);
654
- let hugePriorPromptCalls = 0;
655
- let hugePriorPrompt = '';
656
- const hugePriorProvider = {
657
- name: 'huge-prior-prompt-smoke',
658
- async send(sentMessages) {
659
- hugePriorPromptCalls += 1;
660
- hugePriorPrompt = sentMessages.map((mm) => (typeof mm?.content === 'string' ? mm.content : JSON.stringify(mm?.content ?? ''))).join('\n');
661
- return { content: '## Goal\n- continue after huge prior\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- merged huge prior\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- continue\n\n## Critical Context\n- huge prior merged\n\n## Relevant Files\n- compact.mjs' };
662
- },
663
- };
664
- const hugePriorMessages = [
665
- { role: 'system', content: 'system rules stay mandatory' },
666
- { role: 'user', content: hugePriorSummaryBody },
667
- { role: 'user', content: 'fresh request after huge prior summary' },
668
- { role: 'assistant', content: 'fresh answer' },
669
- ];
670
- const hugePriorResult = await semanticCompactMessages(hugePriorProvider, hugePriorMessages, 'fake-model', 6_000, {
671
- tailTurns: 1,
672
- force: true,
673
- preserveRecentTokens: 400,
674
- });
675
- assert(hugePriorPromptCalls === 1, 'huge prior summary must not fail compaction prompt fit before provider call');
676
- assert(hugePriorResult.semantic === true, 'huge prior summary compaction should succeed');
677
- assert(
678
- priorBlockRetainsSentinelOrContext(hugePriorPrompt, PRIOR_PROMPT_FIT_SENTINEL),
679
- 'degraded <previous-summary> block must retain sentinel or non-placeholder Critical Context',
680
- );
681
- assert(findSummary(hugePriorResult.messages), 'huge prior summary compaction should insert an anchored summary');
682
-
683
- // Huge unstructured/legacy prior summary must repair into schema shape and retain
684
- // a sentinel in the fitted compaction provider prompt (not all - (none)).
685
- const UNSTRUCT_PRIOR_SENTINEL = 'UNSTRUCT_PRIOR_SENTINEL_keep';
686
- const hugeUnstructPriorBody = `${SUMMARY_PREFIX}\nmessages=1 sha256=unstruct roles=user:1\nrecall fasttrack legacy chunk ${UNSTRUCT_PRIOR_SENTINEL}\n` + 'blob '.repeat(80_000);
687
- let hugeUnstructPromptCalls = 0;
688
- let hugeUnstructPrompt = '';
689
- const hugeUnstructProvider = {
690
- name: 'huge-unstruct-prior-smoke',
691
- async send(sentMessages) {
692
- hugeUnstructPromptCalls += 1;
693
- hugeUnstructPrompt = sentMessages.map((mm) => (typeof mm?.content === 'string' ? mm.content : JSON.stringify(mm?.content ?? ''))).join('\n');
694
- return { content: '## Goal\n- continue after unstructured prior\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- merged unstructured prior\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- continue\n\n## Critical Context\n- unstructured prior merged\n\n## Relevant Files\n- compact.mjs' };
695
- },
696
- };
697
- const hugeUnstructMessages = [
698
- { role: 'system', content: 'system rules stay mandatory' },
699
- { role: 'user', content: hugeUnstructPriorBody },
700
- { role: 'user', content: 'fresh request after unstructured prior summary' },
701
- { role: 'assistant', content: 'fresh answer' },
702
- ];
703
- const hugeUnstructResult = await semanticCompactMessages(hugeUnstructProvider, hugeUnstructMessages, 'fake-model', 6_000, {
704
- tailTurns: 1,
705
- force: true,
706
- preserveRecentTokens: 400,
707
- });
708
- assert(hugeUnstructPromptCalls === 1, 'huge unstructured prior must not fail compaction prompt fit before provider call');
709
- assert(hugeUnstructResult.semantic === true, 'huge unstructured prior compaction should succeed');
710
- assert(
711
- priorBlockRetainsSentinelOrContext(hugeUnstructPrompt, UNSTRUCT_PRIOR_SENTINEL),
712
- 'unstructured <previous-summary> block must retain sentinel or non-placeholder Critical Context',
713
- );
714
-
715
- // Leading-spaced headings must parse like validation (not collapse to all - (none)).
716
- const LEADING_SPACE_SENTINEL = 'LEADING_SPACE_SENTINEL_keep';
717
- const leadingSpacePriorBody = `${SUMMARY_PREFIX}\nmessages=1 sha256=lead roles=user:1\n ## Goal\n- ${LEADING_SPACE_SENTINEL}\n ## Critical Context\n- leading-space context\n` + 'z'.repeat(120_000);
718
- let leadingSpacePromptCalls = 0;
719
- let leadingSpacePrompt = '';
720
- const leadingSpaceProvider = {
721
- name: 'leading-space-prior-smoke',
722
- async send(sentMessages) {
723
- leadingSpacePromptCalls += 1;
724
- leadingSpacePrompt = sentMessages.map((mm) => (typeof mm?.content === 'string' ? mm.content : JSON.stringify(mm?.content ?? ''))).join('\n');
725
- return { content: '## Goal\n- continue after leading-space prior\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- merged\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- continue\n\n## Critical Context\n- leading-space merged\n\n## Relevant Files\n- compact.mjs' };
726
- },
727
- };
728
- const leadingSpaceMessages = [
729
- { role: 'system', content: 'system rules stay mandatory' },
730
- { role: 'user', content: leadingSpacePriorBody },
731
- { role: 'user', content: 'fresh request after leading-space prior summary' },
732
- { role: 'assistant', content: 'fresh answer' },
733
- ];
734
- const leadingSpaceResult = await semanticCompactMessages(leadingSpaceProvider, leadingSpaceMessages, 'fake-model', 6_000, {
735
- tailTurns: 1,
736
- force: true,
737
- preserveRecentTokens: 400,
738
- });
739
- assert(leadingSpacePromptCalls === 1, 'leading-space prior must not fail compaction prompt fit before provider call');
740
- assert(leadingSpaceResult.semantic === true, 'leading-space prior compaction should succeed');
741
- assert(
742
- priorBlockRetainsSentinelOrContext(leadingSpacePrompt, LEADING_SPACE_SENTINEL),
743
- 'leading-space prior <previous-summary> block must retain sentinel or non-placeholder Critical Context',
744
- );
745
-
746
- // Extra unrecognized heading in an otherwise schema-complete provider summary must
747
- // be repaired and routed into Critical Context rather than dropped.
748
- const EXTRA_HEADING_SENTINEL = 'EXTRA_HEADING_SENTINEL_keep';
749
- let extraHeadingCalls = 0;
750
- const extraHeadingProvider = {
751
- name: 'extra-heading-smoke',
752
- async send() {
753
- extraHeadingCalls += 1;
754
- return { content: `## Goal\n- ship\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- x\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- go\n\n## Mystery Notes\n- ${EXTRA_HEADING_SENTINEL}\n\n## Critical Context\n- (none)\n\n## Relevant Files\n- compact.mjs` };
755
- },
756
- };
757
- const extraHeadingResult = await semanticCompactMessages(extraHeadingProvider, semanticMessages, 'fake-model', 5_000, { tailTurns: 1, force: true });
758
- assert(extraHeadingCalls === 1, 'extra-heading semantic compact should call the provider once');
759
- assert(extraHeadingResult.summaryRepaired === true, 'extra unrecognized heading should force summary repair');
760
- const extraHeadingSummaryMsg = findSummary(extraHeadingResult.messages);
761
- assert(extraHeadingSummaryMsg, 'extra-heading repair should still insert an anchored summary');
762
- assert(extraHeadingSummaryMsg.content.includes(EXTRA_HEADING_SENTINEL), 'unrecognized heading body must route into repaired Critical Context');
763
-
764
- // Recall fast-track merges prior compacted summary when recallText lacks prior facts.
765
- const OLD_CRITICAL_CONTEXT = 'OLD_CRITICAL_CONTEXT_from_prior_summary';
766
- const priorRecallSummary = `${SUMMARY_PREFIX}\nmessages=2 sha256=def roles=user:1\n## Critical Context\n- ${OLD_CRITICAL_CONTEXT}\n`;
767
- const priorSummaryMessages = [
768
- { role: 'system', content: 'system rules stay mandatory' },
769
- { role: 'user', content: priorRecallSummary },
770
- { role: 'user', content: 'fresh request after prior summary' },
771
- { role: 'assistant', content: 'fresh answer' },
772
- ];
773
- const priorMergedRecall = recallFastTrackCompactMessages(priorSummaryMessages, 8_000, {
774
- tailTurns: 1,
775
- force: true,
776
- recallText: 'recall hit without old critical context',
777
- query: 'prior summary merge',
778
- querySha: 'priorctx',
779
- });
780
- const priorMergedSummary = findSummary(priorMergedRecall.messages);
781
- assert(priorMergedSummary, 'recall fast-track should still insert summary');
782
- assert(priorMergedSummary.content.includes(OLD_CRITICAL_CONTEXT), 'merged recall summary must retain prior compacted critical context');
783
-
784
- // Semantic oversized latest turn: huge sentinel summarized away, budget respected when reducible.
785
- const HUGE_SENTINEL = 'HUGE_SENTINEL_' + 'z'.repeat(120_000);
786
- let oversizedPrompt = '';
787
- const oversizedProvider = {
788
- name: 'oversized-smoke',
789
- async send(sentMessages) {
790
- oversizedPrompt = sentMessages.map((mm) => (typeof mm?.content === 'string' ? mm.content : JSON.stringify(mm?.content ?? ''))).join('\n');
791
- return { content: '## Goal\n- shrink huge tail\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- summarized huge turn\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- continue\n\n## Critical Context\n- huge turn summarized\n\n## Relevant Files\n- compact.mjs' };
792
- },
793
- };
794
- const oversizedMessages = [
795
- { role: 'system', content: 'system rules stay mandatory' },
796
- { role: 'user', content: 'older small request' },
797
- { role: 'assistant', content: 'older small answer' },
798
- { role: 'user', content: `latest turn with sentinel ${HUGE_SENTINEL}` },
799
- { role: 'assistant', content: 'latest small ack' },
800
- ];
801
- const oversizedBudget = 6_000;
802
- const oversizedResult = await semanticCompactMessages(oversizedProvider, oversizedMessages, 'fake-model', oversizedBudget, {
803
- tailTurns: 1,
804
- preserveRecentTokens: 900,
805
- force: true,
806
- });
807
- const oversizedTailJson = JSON.stringify(oversizedResult.messages.filter((m) => !String(m?.content || '').startsWith(SUMMARY_PREFIX)));
808
- assert(!oversizedTailJson.includes(HUGE_SENTINEL), 'oversized sentinel must not survive verbatim in preserved tail');
809
- assert(oversizedPrompt.includes('HUGE_SENTINEL_'), 'compaction prompt should include summarized oversized portion from head');
810
- assert(estimateMessagesTokens(oversizedResult.messages) <= oversizedBudget + 200, 'reducible semantic compact should stay near requested budget');
811
-
812
- // Semantic small recent regression: two small turns with tailTurns:2 both preserved.
813
- const twoTurnMessages = [
814
- { role: 'system', content: 'system rules stay mandatory' },
815
- { role: 'user', content: 'older turn to summarize away' },
816
- { role: 'assistant', content: 'older turn answer' },
817
- { role: 'user', content: 'first small recent request' },
818
- { role: 'assistant', content: 'first small recent answer' },
819
- { role: 'user', content: 'second small recent request' },
820
- { role: 'assistant', content: 'second small recent answer' },
821
- ];
822
- const twoTurnSemantic = await semanticCompactMessages(semanticProvider, twoTurnMessages, 'fake-model', 8_000, { tailTurns: 2, force: true });
823
- const twoTurnTailUsers = twoTurnSemantic.messages.filter((m) => m?.role === 'user' && typeof m.content === 'string' && !m.content.startsWith(SUMMARY_PREFIX));
824
- assert(twoTurnTailUsers.length === 2, 'two small recent turns should both remain in semantic tail');
825
- assert(twoTurnTailUsers.some((m) => m.content.includes('first small recent request')), 'first recent user turn should be preserved');
826
- assert(twoTurnTailUsers.some((m) => m.content.includes('second small recent request')), 'second recent user turn should be preserved');
827
-
828
-
829
- const RECALL_TAIL_TRUNCATION_MARKER = '[... truncated during recall tail preservation ...]';
830
- const RECALL_TAIL_SHORT_TRUNCATION_MARKER = '[truncated]';
831
-
832
- // Semantic: latest user-only oversized turn must appear in provider prompt or explicit tail marker (never vanish).
833
- const USER_ONLY_SENTINEL = 'USER_ONLY_SENTINEL_' + 'q'.repeat(140_000);
834
- let userOnlyPrompt = '';
835
- const userOnlyProvider = {
836
- name: 'user-only-oversized-smoke',
837
- async send(sentMessages) {
838
- userOnlyPrompt = sentMessages.map((mm) => (typeof mm?.content === 'string' ? mm.content : JSON.stringify(mm?.content ?? ''))).join('\n');
839
- return { content: '## Goal\n- summarize user-only huge turn\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- summarized\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- continue\n\n## Critical Context\n- user-only huge\n\n## Relevant Files\n- compact.mjs' };
840
- },
841
- };
842
- const userOnlyMessages = [
843
- { role: 'system', content: 'system rules stay mandatory' },
844
- { role: 'user', content: 'older small request' },
845
- { role: 'assistant', content: 'older small answer' },
846
- { role: 'user', content: `latest user-only sentinel ${USER_ONLY_SENTINEL}` },
847
- ];
848
- const userOnlyResult = await semanticCompactMessages(userOnlyProvider, userOnlyMessages, 'fake-model', 5_500, {
849
- tailTurns: 1,
850
- preserveRecentTokens: 700,
851
- force: true,
852
- });
853
- const userOnlyTailJson = JSON.stringify(userOnlyResult.messages.filter((m) => !String(m?.content || '').startsWith(SUMMARY_PREFIX)));
854
- const userOnlyInPrompt = userOnlyPrompt.includes('USER_ONLY_SENTINEL_');
855
- const userOnlyInTailMarker = userOnlyTailJson.includes(RECALL_TAIL_TRUNCATION_MARKER) || userOnlyTailJson.includes('USER_ONLY_SENTINEL_');
856
- assert(userOnlyInPrompt || userOnlyInTailMarker, 'user-only oversized turn must be in compaction prompt or explicit tail truncation/sentinel, never absent from both');
857
- assert(!userOnlyTailJson.includes(USER_ONLY_SENTINEL), 'user-only sentinel must not survive verbatim in preserved tail');
858
-
859
- // Recall fast-track: latest huge user + small assistant must keep user anchor (sentinel prefix or truncation marker).
860
- const RECALL_USER_SENTINEL = 'RECALL_USER_SENTINEL_' + 'r'.repeat(90_000);
861
- const recallHugeUserMessages = [
862
- { role: 'system', content: 'system rules stay mandatory' },
863
- { role: 'user', content: 'older request to summarize away' },
864
- { role: 'assistant', content: 'older answer' },
865
- { role: 'user', content: `latest recall user ${RECALL_USER_SENTINEL}` },
866
- { role: 'assistant', content: 'latest recall assistant ack' },
867
- ];
868
- const recallHugeUserResult = recallFastTrackCompactMessages(recallHugeUserMessages, 12_000, {
869
- tailTurns: 1,
870
- force: true,
871
- recallText: 'recall hit without latest user sentinel',
872
- query: 'recall huge user',
873
- querySha: 'recallhugeuser',
874
- recallTailTokenCap: 1_200,
875
- });
876
- const recallHugeTailUsers = recallHugeUserResult.messages.filter((m) => m?.role === 'user' && typeof m.content === 'string' && !m.content.startsWith(SUMMARY_PREFIX));
877
- assert(recallHugeTailUsers.length >= 1, 'recall fast-track must preserve a real user tail anchor');
878
- const recallHugeUserText = recallHugeTailUsers.map((m) => m.content).join('\n');
879
- assert(
880
- recallHugeUserText.includes('RECALL_USER_SENTINEL_') || recallHugeUserText.includes(RECALL_TAIL_TRUNCATION_MARKER),
881
- 'recall fast-track must keep latest user via sentinel prefix or explicit truncation marker',
882
- );
883
- assert(!recallHugeUserResult.messages.some((m) => m?.role === 'assistant' && m.content === 'latest recall assistant ack' && recallHugeTailUsers.length === 0), 'recall fast-track must not emit assistant-only tail without latest user');
884
-
885
- // Prior recall summary must survive huge recallText fitting.
886
- const PRIOR_FIT_SENTINEL = 'PRIOR_FIT_SENTINEL_keep_me';
887
- const priorFitSummary = `${SUMMARY_PREFIX}\nmessages=1 sha256=prior roles=user:1\n## Critical Context\n- ${PRIOR_FIT_SENTINEL}\n`;
888
- const priorFitMessages = [
889
- { role: 'system', content: 'system rules stay mandatory' },
890
- { role: 'user', content: priorFitSummary },
891
- { role: 'user', content: 'fresh request' },
892
- { role: 'assistant', content: 'fresh answer' },
893
- ];
894
- const hugeRecallText = 'recall blob ' + 'x'.repeat(120_000);
895
- const priorFitRecall = recallFastTrackCompactMessages(priorFitMessages, 9_000, {
896
- tailTurns: 1,
897
- force: true,
898
- recallText: hugeRecallText,
899
- query: 'prior fit',
900
- querySha: 'priorfit',
901
- });
902
- const priorFitSummaryMsg = findSummary(priorFitRecall.messages);
903
- assert(priorFitSummaryMsg, 'prior-fit recall should insert summary');
904
- assert(priorFitSummaryMsg.content.includes(PRIOR_FIT_SENTINEL), 'prior compacted sentinel must survive huge recallText fitting');
905
-
906
- // Very small recall tail cap still yields user anchor with explicit truncation marker.
907
- const tinyCapMessages = [
908
- { role: 'system', content: 'system rules stay mandatory' },
909
- { role: 'user', content: 'tiny cap user ' + 'y'.repeat(8_000) },
910
- { role: 'assistant', content: 'tiny cap assistant' },
911
- ];
912
- const tinyCapRecall = recallFastTrackCompactMessages(tinyCapMessages, 20_000, {
913
- tailTurns: 1,
914
- force: true,
915
- recallText: 'recall hit tiny cap',
916
- query: 'tiny cap',
917
- querySha: 'tinycap',
918
- recallTailTokenCap: 12,
919
- });
920
- const tinyCapUsers = tinyCapRecall.messages.filter((m) => m?.role === 'user' && typeof m.content === 'string' && !m.content.startsWith(SUMMARY_PREFIX));
921
- assert(tinyCapUsers.length === 1, 'tiny cap recall must keep exactly one user anchor');
922
- const tinyCapUserContent = tinyCapUsers[0].content;
923
- assert(
924
- tinyCapUserContent.includes(RECALL_TAIL_TRUNCATION_MARKER) || tinyCapUserContent.includes(RECALL_TAIL_SHORT_TRUNCATION_MARKER),
925
- 'tiny cap recall must use full or short explicit truncation marker on oversized user',
926
- );
927
- assert(!tinyCapUserContent.startsWith('[...'), 'tiny cap recall must not use a partial long-marker prefix');
928
-
929
- // ---------------------------------------------------------------------------
930
- // Conservative Unicode-aware estimator (strict-fit hardening).
931
- //
932
- // The estimator now encodes the provider-visible projection with the real
933
- // o200k_base BPE (tiktoken), so local estimates equal actual tokenizer counts
934
- // for every script (Korean/CJK/emoji/ASCII alike). Provider-specific billing
935
- // deltas (e.g. Anthropic ~1.7x o200k) are applied separately via
936
- // providerTokenCalibration at the pressure/gauge aggregation boundary.
937
- const requireForTokenizer = createRequire(import.meta.url);
938
- const { Tiktoken: SmokeTiktoken } = requireForTokenizer('tiktoken/lite');
939
- const smokeO200k = requireForTokenizer('tiktoken/encoders/o200k_base.json');
940
- const smokeEncoder = new SmokeTiktoken(smokeO200k.bpe_ranks, smokeO200k.special_tokens, smokeO200k.pat_str);
941
- const bpeCount = (text) => smokeEncoder.encode(String(text), undefined, []).length;
942
-
943
- for (const [label, probe] of [
944
- ['korean', '\uD55C\uAD6D\uC5B4 \uCEF4\uD329\uC158 \uACBD\uACC4 \uD14C\uC2A4\uD2B8 '.repeat(200)],
945
- ['cjk', '上下文压缩边界测试令牌预算'.repeat(200)],
946
- ['emoji', '😀🚀🔥✅'.repeat(200)],
947
- ['ascii', 'plain ascii sentence with normal words. '.repeat(200)],
948
- ]) {
949
- const est = estimateMessagesTokens([{ role: 'user', content: probe }]);
950
- const expected = bpeCount(probe) + 4;
951
- assert(
952
- est === expected,
953
- `${label} message estimate must equal the real o200k count plus framing (est=${est}, expected=${expected})`,
954
- );
955
- }
956
-
957
- // Provider calibration reconciles o200k counts with actual billing.
958
- assert(
959
- providerTokenCalibration('anthropic-oauth') > 1.5 && providerTokenCalibration('anthropic-oauth') < 2,
960
- 'anthropic calibration must reflect the measured ~1.7x billed/o200k ratio',
961
- );
962
- assert(providerTokenCalibration('openai-oauth') === 1.0, 'openai calibration must stay neutral');
963
- assert(providerTokenCalibration(undefined) === 1.0, 'unknown provider calibration must stay neutral');
964
-
965
- // Tool schemas are serialized into the request body, so their estimate must
966
- // track the real tokenizer over the wire-shape serialization.
967
- const toolSchema = [{
968
- name: 'apply_patch',
969
- description: '\uD328\uCE58\uB97C \uD30C\uC77C\uC5D0 \uC801\uC6A9 (\uD55C\uAD6D\uC5B4 \uC124\uBA85) — applies a patch',
970
- parameters: { type: 'object', properties: { patch: { type: 'string', description: '\uD328\uCE58 \uBCF8\uBB38 \uD14D\uC2A4\uD2B8' } } },
971
- }];
972
- const toolSchemaEst = estimateToolSchemaTokens(toolSchema);
973
- const toolWireJson = JSON.stringify(toolSchema.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters })));
974
- assert(
975
- toolSchemaEst === bpeCount(toolWireJson),
976
- `tool schema estimate must equal the o200k count of the serialized wire JSON (est=${toolSchemaEst}, expected=${bpeCount(toolWireJson)})`,
977
- );
978
- smokeEncoder.free();
979
-
980
- // Strict-fit smoke: a Korean/CJK-heavy newest turn far larger than the preserve
981
- // budget must be summarized/truncated safely — never preserved verbatim — and
982
- // the final estimated tokens must stay within budget.
983
- const KO_HUGE_SENTINEL = 'KO_HUGE_SENTINEL_' + '\uC555\uCD95\uACBD\uACC4\uD14C\uC2A4\uD2B8'.repeat(20_000);
984
- let strictFitPrompt = '';
985
- const strictFitProvider = {
986
- name: 'strict-fit-cjk-smoke',
987
- async send(sentMessages) {
988
- strictFitPrompt = sentMessages.map((mm) => (typeof mm?.content === 'string' ? mm.content : JSON.stringify(mm?.content ?? ''))).join('\n');
989
- return { content: '## Goal\n- summarize CJK-heavy huge turn\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- summarized\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n- continue\n\n## Critical Context\n- cjk huge\n\n## Relevant Files\n- compact.mjs' };
990
- },
991
- };
992
- const strictFitMessages = [
993
- { role: 'system', content: 'system rules stay mandatory' },
994
- { role: 'user', content: '\uC774\uC804 \uC791\uC740 \uC694\uCCAD' },
995
- { role: 'assistant', content: '\uC774\uC804 \uC791\uC740 \uC751\uB2F5' },
996
- { role: 'user', content: `\uCD5C\uC2E0 \uAC70\uB300\uD55C \uD55C\uAD6D\uC5B4 \uC694\uCCAD ${KO_HUGE_SENTINEL}` },
997
- ];
998
- const STRICT_FIT_BUDGET = 6_000;
999
- const strictFitResult = await semanticCompactMessages(strictFitProvider, strictFitMessages, 'fake-model', STRICT_FIT_BUDGET, {
1000
- tailTurns: 1,
1001
- preserveRecentTokens: 700,
1002
- force: true,
1003
- });
1004
- const strictFitTailJson = JSON.stringify(strictFitResult.messages.filter((m) => !String(m?.content || '').startsWith(SUMMARY_PREFIX)));
1005
- assert(!strictFitTailJson.includes(KO_HUGE_SENTINEL), 'CJK huge newest turn must not survive verbatim in preserved tail');
1006
- const strictFitInPrompt = strictFitPrompt.includes('KO_HUGE_SENTINEL_');
1007
- const strictFitInTailMarker = strictFitTailJson.includes('truncated') || strictFitTailJson.includes('KO_HUGE_SENTINEL_');
1008
- assert(strictFitInPrompt || strictFitInTailMarker, 'CJK huge turn must appear in compaction prompt or explicit tail truncation marker, never absent from both');
1009
- // Final estimated tokens (under the now-conservative estimator) must fit the
1010
- // effective budget — strict final-fit, no reducible-tail bypass.
1011
- const strictFitFinalTokens = estimateMessagesTokens(strictFitResult.messages);
1012
- assert(
1013
- strictFitFinalTokens <= STRICT_FIT_BUDGET,
1014
- `strict-fit compacted result must fit budget (final=${strictFitFinalTokens}, budget=${STRICT_FIT_BUDGET})`,
1015
- );
1016
-
1017
- process.stdout.write('compact smoke passed ✓\n');