mixdog 0.9.96 → 0.9.99

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 (193) hide show
  1. package/README.md +0 -17
  2. package/package.json +16 -8
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/agent/30-explorer.md +43 -32
  9. package/src/rules/lead/01-general.md +1 -2
  10. package/src/rules/lead/lead-tool.md +0 -3
  11. package/src/rules/shared/01-tool.md +11 -10
  12. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  13. package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  15. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
  17. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
  18. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  19. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
  20. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  22. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  24. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  25. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  26. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
  27. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  28. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
  29. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  30. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
  31. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  32. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
  33. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  34. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  35. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  36. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  37. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  38. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  39. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  41. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
  42. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  43. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  44. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
  45. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  46. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  47. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  48. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  49. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  50. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  51. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  52. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
  55. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  58. package/src/runtime/attachments/pdf-extract.mjs +83 -0
  59. package/src/runtime/attachments/store.mjs +515 -0
  60. package/src/runtime/attachments/store.test.mjs +188 -0
  61. package/src/runtime/channels/lib/config.mjs +33 -33
  62. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  63. package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
  64. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  65. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  66. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  67. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  68. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  69. package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
  70. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  72. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  73. package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
  74. package/src/runtime/channels/lib/worker-main.mjs +39 -58
  75. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  76. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  77. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  78. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  79. package/src/runtime/channels/tool-defs.mjs +1 -1
  80. package/src/runtime/memory/index.mjs +72 -211
  81. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  82. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  83. package/src/runtime/memory/lib/http-router.mjs +1 -6
  84. package/src/runtime/memory/lib/http-wire.mjs +39 -4
  85. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  86. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  87. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  88. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  89. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  90. package/src/runtime/memory/lib/memory.mjs +1 -1
  91. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  92. package/src/runtime/search/index.mjs +2 -2
  93. package/src/runtime/shared/abort-race.mjs +70 -0
  94. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  95. package/src/runtime/shared/config.mjs +11 -24
  96. package/src/runtime/shared/json-metrics.mjs +94 -0
  97. package/src/runtime/shared/llm/index.mjs +1 -1
  98. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  99. package/src/runtime/shared/resource-admission.mjs +6 -5
  100. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  101. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  102. package/src/runtime/shared/tool-surface.mjs +91 -22
  103. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  104. package/src/runtime/shared/turn-snapshot.mjs +30 -2
  105. package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
  106. package/src/session-runtime/channel-config-api.mjs +5 -5
  107. package/src/session-runtime/config-lifecycle.mjs +27 -27
  108. package/src/session-runtime/lifecycle-api.mjs +22 -3
  109. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  110. package/src/session-runtime/prewarm.mjs +1 -1
  111. package/src/session-runtime/provider-models.mjs +12 -2
  112. package/src/session-runtime/quick-model-rows.mjs +38 -7
  113. package/src/session-runtime/quick-search-models.mjs +15 -17
  114. package/src/session-runtime/remote-control.mjs +6 -7
  115. package/src/session-runtime/runtime-core.mjs +61 -32
  116. package/src/session-runtime/runtime-tunables.mjs +9 -6
  117. package/src/session-runtime/self-update.mjs +1 -1
  118. package/src/session-runtime/session-lifecycle.mjs +36 -15
  119. package/src/session-runtime/session-title.mjs +28 -4
  120. package/src/session-runtime/session-turn-api.mjs +55 -15
  121. package/src/session-runtime/settings-api.mjs +7 -7
  122. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  123. package/src/standalone/channel-admin.mjs +42 -53
  124. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  125. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
  126. package/src/standalone/channel-worker.mjs +124 -647
  127. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
  128. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  129. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  130. package/src/standalone/session-protocol.mjs +9 -4
  131. package/src/standalone/session-runtime-pool.mjs +393 -0
  132. package/src/standalone/session-runtime-record.mjs +20 -0
  133. package/src/standalone/session-runtime-worker.mjs +203 -0
  134. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
  135. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
  136. package/src/standalone/session-wire.mjs +55 -0
  137. package/src/tui/App.jsx +4 -4
  138. package/src/tui/app/app-format.mjs +1 -1
  139. package/src/tui/app/channel-pickers.mjs +41 -41
  140. package/src/tui/app/input-parsers.mjs +2 -2
  141. package/src/tui/app/onboarding-steps.mjs +2 -2
  142. package/src/tui/app/project-picker.mjs +8 -8
  143. package/src/tui/app/prompt-submit.mjs +15 -17
  144. package/src/tui/app/settings-picker.mjs +25 -25
  145. package/src/tui/app/use-prompt-handlers.mjs +7 -3
  146. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  147. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  148. package/src/tui/components/ToolExecution.jsx +1 -1
  149. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  150. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  151. package/src/tui/dist/index.mjs +403 -170
  152. package/src/tui/hooks/useSession.mjs +16 -0
  153. package/src/tui/index.jsx +11 -11
  154. package/src/tui/paste-attachments.mjs +13 -7
  155. package/src/tui/prompt-history-store.mjs +52 -4
  156. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  157. package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
  158. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  159. package/src/tui/{engine → session}/context-state.mjs +3 -3
  160. package/src/tui/{engine → session}/labels.mjs +2 -2
  161. package/src/tui/{engine → session}/live-share.mjs +28 -14
  162. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  163. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  164. package/src/tui/session/oauth-flows.mjs +140 -0
  165. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  166. package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
  167. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  168. package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
  169. package/src/tui/{engine → session}/session-api.mjs +17 -61
  170. package/src/tui/{engine → session}/session-flow.mjs +5 -3
  171. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  172. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  173. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  174. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  175. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  176. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  177. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  178. package/src/tui/{engine → session}/turn.mjs +29 -107
  179. package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
  180. package/src/tui/session.mjs +22 -0
  181. package/src/ui/statusline-segments.mjs +1 -1
  182. package/src/ui/statusline.mjs +1 -1
  183. package/vendor/ink/build/components/App.js +10 -1
  184. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  185. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  186. package/src/tui/engine/turn-watchdog.mjs +0 -92
  187. package/src/tui/engine.mjs +0 -22
  188. package/src/tui/hooks/useEngine.mjs +0 -16
  189. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  190. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  191. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  192. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  193. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
package/README.md CHANGED
@@ -65,23 +65,6 @@ measured cache writes; the archived OpenAI runs did not retain
65
65
  artifacts, the exact run commands, and the metric scripts that recompute
66
66
  every number above live under `benchmarks/terminal-bench-2.1/`.
67
67
 
68
- ## Boot overhead — measured, not estimated
69
-
70
- Fixed cost of the default system prompt + tool schemas, measured with a
71
- minimal one-turn ping ("hi"). Every number below is what the provider's own
72
- usage accounting reported for the first API call — same tokenizer on both
73
- sides of each row, no offline estimates (2026-08).
74
-
75
- | Tokenizer | Baseline harness | First-call prompt | mixdog Lead | mixdog worker agent |
76
- |-----------|------------------|------------------:|------------:|--------------------:|
77
- | Anthropic — pristine containers, TB2.1 harness | Claude Code 2.1.220 | 19,149 tok (median, n=89) | **7,295 (−62%)** | 4,856 (−75%) |
78
- | OpenAI — same host, same model (`gpt-5.6-sol`) | Codex CLI 0.145.0 | 17,706 tok | **7,488 (−58%)** | 4,616 (−74%) |
79
-
80
- The Lead surface carries the full default rule set, skills manifest, and
81
- per-user profile; spawning a sub-agent costs a flat ~4.6–4.9k tokens. Low
82
- boot overhead compounds: it is re-read on every request, so it is the
83
- baseline of every cache write, compaction, and long-session turn.
84
-
85
68
  ## Why mixdog
86
69
 
87
70
  **Maximum performance at minimum cost**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.96",
3
+ "version": "0.9.99",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -71,36 +71,42 @@
71
71
  "test:agent-job-views": "node --test scripts/agent-job-terminal-view-test.mjs",
72
72
  "test:agent-fanout": "node scripts/agent-parallel-smoke.mjs && node --test scripts/agent-route-batch-test.mjs scripts/execution-completion-dedup-test.mjs",
73
73
  "test:toolcall": "node --test scripts/toolcall-args-test.mjs",
74
- "test:tool-batching": "node --test scripts/eager-patch-shell-order-test.mjs scripts/session-bench-batching-test.mjs",
74
+ "test:tool-batching": "node --test scripts/eager-patch-shell-order-test.mjs scripts/session-bench-batching-test.mjs scripts/tool-cwd-rebind-test.mjs",
75
75
  "test:shipmode": "node --test scripts/ship-mode-test.mjs",
76
76
  "test:shellhardening": "node --test scripts/shell-hardening-test.mjs scripts/shell-failure-diagnostics-test.mjs scripts/windows-hide-spawn-options-test.mjs scripts/shell-job-session-scope-test.mjs",
77
77
  "test:placeholder": "node --test scripts/compacted-placeholder-scrub-test.mjs",
78
78
  "test:providers": "node --test scripts/provider-toolcall-test.mjs scripts/provider-contract-test.mjs scripts/provider-stream-stall-test.mjs scripts/provider-stream-outcome-test.mjs scripts/stream-frame-fault-matrix-test.mjs scripts/gemini-provider-test.mjs scripts/anthropic-transport-policy-test.mjs scripts/anthropic-native-block-replay-test.mjs scripts/openai-oauth-ws-1006-retry-test.mjs scripts/stream-close-retry-test.mjs scripts/openai-end-turn-signal-test.mjs",
79
79
  "test:provider-admission": "node --test scripts/provider-admission-scheduler-test.mjs scripts/provider-stream-json-test.mjs",
80
- "test:resource-admission": "node --test scripts/resource-admission-test.mjs scripts/child-spawn-isolation-test.mjs",
80
+ "test:resource-admission": "node --test scripts/resource-admission-test.mjs scripts/child-spawn-isolation-test.mjs scripts/owner-fair-gate-test.mjs",
81
81
  "test:deferred-tools": "node --test scripts/deferred-tool-loading-test.mjs",
82
82
  "test:anthropic-oauth-race": "node --test scripts/anthropic-oauth-refresh-race-test.mjs",
83
83
  "test:grok-oauth-race": "node --test scripts/grok-oauth-refresh-race-test.mjs",
84
84
  "test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
85
85
  "test:keychain": "node --test scripts/keychain-prewarm-test.mjs",
86
+ "test:memory-leaks": "node --expose-gc --test scripts/memory-retention-test.mjs scripts/memory-worker-stability-test.mjs scripts/session-transport-test.mjs apps/desktop/src/renderer/memory-retention.test.mjs",
86
87
  "test:memory-routing": "node --test scripts/memory-cycle-routing-test.mjs scripts/maintenance-default-routes-test.mjs scripts/embedding-worker-exit-test.mjs scripts/embedding-runtime-prune-test.mjs",
87
88
  "test:embedding-runtime": "node --test scripts/embedding-runtime-prune-test.mjs scripts/memory-pg-recovery-test.mjs && node scripts/verify-embedding-runtime.mjs",
88
89
  "test:embedding-runtime:core": "node --test scripts/embedding-runtime-prune-test.mjs scripts/memory-pg-recovery-test.mjs && node scripts/verify-embedding-runtime.mjs --core",
89
90
  "test:embedding-runtime:warmup": "node scripts/verify-embedding-runtime.mjs --warmup",
91
+ "prepare:code-graph-test": "node scripts/prepare-code-graph-test.mjs",
90
92
  "test:code-graph-dispatch": "node --test scripts/code-graph-dispatch-test.mjs",
91
93
  "test:tui-queue": "node --test scripts/submit-commandbusy-race-test.mjs scripts/steering-drain-buckets-test.mjs scripts/abort-recovery-test.mjs scripts/message-rewind-test.mjs scripts/execution-pending-resume-kick-test.mjs scripts/execution-resume-esc-integration-test.mjs scripts/pending-stale-injection-test.mjs",
92
94
  "test:tui-input-render": "node --test scripts/prompt-immediate-render-test.mjs scripts/prompt-key-parity-test.mjs",
93
95
  "test:tui-streaming-window": "node --test scripts/streaming-tail-window-test.mjs scripts/tui-store-frame-batch-test.mjs && node scripts/tui-transcript-jitter-harness.mjs",
94
96
  "test:tui-ambiguous-width": "node --test scripts/tui-ambiguous-width-test.mjs",
95
97
  "test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/verify-release-assets-test.mjs && node --check scripts/deploy-workflow-test.mjs && node --check scripts/release-version-discipline-test.mjs && node --test scripts/verify-release-assets-test.mjs scripts/deploy-workflow-test.mjs scripts/release-version-discipline-test.mjs",
96
- "test:release-focused": "npm run test:release-assets && npm run test:tool-contracts && npm run test:tool-batching && npm run test:placeholder && npm run smoke:patch && npm run test:patch-binary-cache && npm run test:providers && npm run test:deferred-tools && npm run smoke:compact && npm run test:compact && npm run test:context && node --test scripts/code-graph-root-federation-test.mjs scripts/code-graph-aggregate-cwd-test.mjs && npm run test:code-graph-dispatch && node --test scripts/code-graph-disk-hit-test.mjs && npm run test:shellhardening && npm run test:project-registry && npm run test:session && npm run test:workflow-editor && npm run test:embedding-runtime && node --test scripts/tui-transcript-perf-test.mjs",
98
+ "test:release-focused": "npm run test:release-focused:contracts && npm run test:release-focused:providers && npm run test:release-focused:compact && npm run test:release-focused:session",
99
+ "test:release-focused:contracts": "npm run test:release-assets && npm run test:tool-contracts && npm run test:tool-batching && npm run test:placeholder && npm run smoke:patch && npm run test:patch-binary-cache",
100
+ "test:release-focused:providers": "npm run test:providers && npm run test:deferred-tools && npm run test:compact && npm run test:context && npm run prepare:code-graph-test && node --test scripts/code-graph-root-federation-test.mjs scripts/code-graph-aggregate-cwd-test.mjs && npm run test:code-graph-dispatch && node --test scripts/code-graph-disk-hit-test.mjs",
101
+ "test:release-focused:compact": "npm run smoke:compact && npm run test:shellhardening",
102
+ "test:release-focused:session": "npm run test:project-registry && npm run test:session && npm run test:workflow-editor && npm run test:embedding-runtime && node --test scripts/tui-transcript-perf-test.mjs",
97
103
  "test:native-edit-wire": "node --test scripts/native-edit-wire-test.mjs",
98
104
  "test:patch-binary-cache": "node --test scripts/patch-binary-cache-test.mjs",
99
105
  "test:patch-parity": "node --test scripts/v4a-parity-test.mjs",
100
106
  "test:project-registry": "node --test scripts/project-registry-isolation-test.mjs",
101
- "test:engine-daemon": "node --test scripts/fair-call-scheduler-test.mjs scripts/engine-daemon-test.mjs scripts/engine-daemon-tui-parity-test.mjs scripts/engine-daemon-recovery-test.mjs scripts/engine-daemon-submit-delivery-test.mjs scripts/engine-daemon-process-lifetime-test.mjs scripts/engine-daemon-multipane-stress-test.mjs && node --test scripts/runtime-isolation-stress-test.mjs",
102
- "smoke:engine-daemon": "node scripts/engine-daemon-smoke.mjs",
103
- "test:session": "node --test scripts/session-orphan-sweep-test.mjs scripts/interrupted-turn-history-test.mjs scripts/turn-checkpoint-crash-test.mjs scripts/turn-outcome-fault-matrix-test.mjs scripts/session-save-fault-store-test.mjs scripts/session-disk-authority-test.mjs scripts/session-load-cache-race-test.mjs scripts/agent-loop-complete-turn-test.mjs scripts/session-heartbeat-lifecycle-test.mjs scripts/remote-transition-order-test.mjs scripts/session-new-reset-test.mjs scripts/turn-ttft-critical-path-test.mjs",
107
+ "test:session-transport": "node --test scripts/fair-call-scheduler-test.mjs scripts/session-transport-test.mjs scripts/session-tui-parity-test.mjs scripts/session-recovery-test.mjs scripts/session-submit-delivery-test.mjs scripts/daemon-process-lifetime-test.mjs scripts/session-multipane-stress-test.mjs && node --test scripts/runtime-isolation-stress-test.mjs",
108
+ "smoke:session": "node scripts/session-smoke.mjs",
109
+ "test:session": "node --test scripts/runtime-turn-contract-test.mjs scripts/session-orphan-sweep-test.mjs scripts/interrupted-turn-history-test.mjs scripts/turn-checkpoint-crash-test.mjs scripts/turn-outcome-fault-matrix-test.mjs scripts/session-save-fault-store-test.mjs scripts/session-disk-authority-test.mjs scripts/session-load-cache-race-test.mjs scripts/agent-loop-complete-turn-test.mjs scripts/session-heartbeat-lifecycle-test.mjs scripts/remote-transition-order-test.mjs scripts/session-new-reset-test.mjs scripts/turn-ttft-critical-path-test.mjs",
104
110
  "test:live-canary": "node --test scripts/live-canary-test.mjs",
105
111
  "test:rebindtail": "node --test scripts/forwarder-rebind-tail-test.mjs scripts/channel-remote-format-test.mjs",
106
112
  "test:workflow-editor": "node --test scripts/workflow-id-test.mjs scripts/workflow-pack-editor-test.mjs",
@@ -122,7 +128,7 @@
122
128
  "bench:explore": "node scripts/explore-bench.mjs",
123
129
  "bench:output-style": "node scripts/output-style-bench.mjs",
124
130
  "bench:session-context": "node scripts/session-context-bench.mjs",
125
- "bench:engine-daemon": "node scripts/engine-daemon-bench.mjs",
131
+ "bench:session-transport": "node scripts/session-transport-bench.mjs",
126
132
  "bench:tool-overhead": "node scripts/tool-overhead-microbench.mjs",
127
133
  "bench:internal-comms": "node scripts/internal-comms-bench.mjs",
128
134
  "audit:models": "node scripts/model-catalog-audit.mjs",
@@ -154,9 +160,11 @@
154
160
  "pg": "^8.22.0",
155
161
  "puppeteer-core": "^25.2.0",
156
162
  "react": "^19.2.7",
163
+ "sharp": "^0.35.3",
157
164
  "string-width": "^8.2.1",
158
165
  "strip-ansi": "^7.2.0",
159
166
  "tiktoken": "^1.0.22",
167
+ "unpdf": "^1.8.0",
160
168
  "undici": "^8.5.0",
161
169
  "wrap-ansi": "^10.0.0",
162
170
  "ws": "^8.21.0",
@@ -41,13 +41,13 @@ const sharedRuntimeExternalPlugin = {
41
41
  },
42
42
  };
43
43
 
44
- const engineDaemonClientExternalPlugin = {
45
- name: 'mixdog-engine-daemon-client-external',
44
+ const sessionClientExternalPlugin = {
45
+ name: 'mixdog-session-client-external',
46
46
  setup(build) {
47
- build.onResolve({ filter: /^\.\.\/standalone\/engine-daemon-client\.mjs$/ }, () => ({
47
+ build.onResolve({ filter: /^\.\.\/standalone\/session-client\.mjs$/ }, () => ({
48
48
  // Keep this module outside the TUI bundle so its import.meta.url stays
49
- // anchored in src/standalone, where backend-daemon.mjs actually lives.
50
- path: '../../standalone/engine-daemon-client.mjs',
49
+ // anchored in src/standalone, where daemon.mjs actually lives.
50
+ path: '../../standalone/session-client.mjs',
51
51
  external: true,
52
52
  }));
53
53
  },
@@ -90,7 +90,7 @@ await build({
90
90
  plugins: [
91
91
  mixdogInkAliasPlugin,
92
92
  sharedRuntimeExternalPlugin,
93
- engineDaemonClientExternalPlugin,
93
+ sessionClientExternalPlugin,
94
94
  ],
95
95
  logLevel: 'info',
96
96
  });
@@ -1,17 +1,18 @@
1
- // Stub channels daemon for the flip smoke: same lifecycle contract as
2
- // src/standalone/channel-daemon.mjs (pid-verified singleton claim, HTTP+SSE
1
+ // Stub channel service for the flip smoke: same lifecycle contract as
2
+ // src/standalone/daemon.mjs (pid-verified singleton claim, HTTP+SSE
3
3
  // transport, discovery file, ready handshake, client-grace self-shutdown) but
4
4
  // with a STUB runtime instead of worker-main — no Discord token needed. The
5
5
  // real channel-worker.mjs spawn-or-attach path forks THIS via
6
- // MIXDOG_CHANNEL_DAEMON_ENTRY, so the flip is exercised end to end.
6
+ // MIXDOG_DAEMON_ENTRY, so the flip is exercised end to end.
7
7
  process.env.MIXDOG_WORKER_MODE = process.env.MIXDOG_WORKER_MODE || '1';
8
8
 
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- import { mkdirSync, writeFileSync } from 'node:fs';
11
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { writeJsonAtomicSync } from '../src/runtime/shared/atomic-file.mjs';
12
13
  import { claimSingletonOwner, releaseSingletonOwner } from '../src/runtime/shared/singleton-owner.mjs';
13
14
  import { safeIpcSend } from '../src/runtime/shared/safe-ipc-send.mjs';
14
- import { createChannelDaemonTransport } from '../src/standalone/channel-daemon-transport.mjs';
15
+ import { createChannelTransport } from '../src/standalone/channel-transport.mjs';
15
16
 
16
17
  function runtimeRoot() {
17
18
  return process.env.MIXDOG_RUNTIME_ROOT
@@ -20,8 +21,8 @@ function runtimeRoot() {
20
21
  }
21
22
  const RUNTIME_ROOT = runtimeRoot();
22
23
  const DATA_DIR = process.env.MIXDOG_DATA_DIR ? path.resolve(process.env.MIXDOG_DATA_DIR) : RUNTIME_ROOT;
23
- const DISCOVERY_PATH = path.join(RUNTIME_ROOT, 'channel-daemon.json');
24
- const OWNER_PATH = path.join(DATA_DIR, 'channel-daemon-owner.json');
24
+ const DISCOVERY_PATH = path.join(RUNTIME_ROOT, 'daemon.json');
25
+ const OWNER_PATH = path.join(DATA_DIR, 'daemon-owner.json');
25
26
 
26
27
  function log(line) { if (process.env.DAEMON_SMOKE_VERBOSE) process.stderr.write(`[stub-daemon] ${line}\n`); }
27
28
 
@@ -31,23 +32,24 @@ async function shutdown(reason, code = 0) {
31
32
  if (shuttingDown) return;
32
33
  shuttingDown = true;
33
34
  try { await transport?.stop?.(); } catch {}
35
+ try { rmSync(DISCOVERY_PATH, { force: true }); } catch {}
34
36
  try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {}
35
37
  process.exit(code);
36
38
  }
37
39
 
38
40
  async function main() {
39
41
  try { mkdirSync(RUNTIME_ROOT, { recursive: true }); } catch {}
40
- const claim = claimSingletonOwner(OWNER_PATH, { kind: 'channel-runtime-daemon', pid: process.pid, meta: { cwd: process.cwd() } });
42
+ const claim = claimSingletonOwner(OWNER_PATH, { kind: 'mixdog-daemon', pid: process.pid, meta: { cwd: process.cwd() } });
41
43
  if (!claim.owned) { process.exit(0); } // race loser → spawner attaches to winner
42
44
  process.on('exit', () => { try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {} });
43
45
 
44
46
  // Optional env dump for the tool smoke: proves daemonEnv() spawned us with
45
- // the daemon-mode flags (MIXDOG_CHANNEL_DAEMON=1, MIXDOG_CLI_OWNED=0).
47
+ // the host-mode flags (MIXDOG_DAEMON_HOST=1, MIXDOG_CLI_OWNED=0).
46
48
  if (process.env.SMOKE_CHANNEL_ENV_OUT) {
47
49
  try {
48
50
  writeFileSync(process.env.SMOKE_CHANNEL_ENV_OUT, JSON.stringify({
49
51
  cliOwned: process.env.MIXDOG_CLI_OWNED,
50
- daemon: process.env.MIXDOG_CHANNEL_DAEMON,
52
+ host: process.env.MIXDOG_DAEMON_HOST,
51
53
  }));
52
54
  } catch { /* smoke-only, best-effort */ }
53
55
  }
@@ -61,15 +63,19 @@ async function main() {
61
63
  return { ok: true, name, args, leadPid: ctx.leadPid };
62
64
  };
63
65
 
64
- transport = createChannelDaemonTransport({
66
+ transport = createChannelTransport({
65
67
  handleCall,
66
- discoveryPath: DISCOVERY_PATH,
67
68
  clientGraceMs: 250,
68
69
  sweepMs: 1000,
69
70
  log,
70
71
  onClientsEmpty: () => { void shutdown('no live clients'); },
71
72
  });
72
73
  const { port, token } = await transport.start();
74
+ writeJsonAtomicSync(DISCOVERY_PATH, {
75
+ pid: process.pid,
76
+ startedAt: Date.now(),
77
+ endpoints: { channel: { port, token } },
78
+ }, { compact: true });
73
79
  safeIpcSend(process, { type: 'ready', port, token });
74
80
  log(`ready port=${port} pid=${process.pid}`);
75
81
  }
@@ -132,7 +132,7 @@ apply it, and verify the result.
132
132
  ### Discord / Telegram / voice channels
133
133
 
134
134
  1. Check `/channels` (or `/setting` → Channel) and runtime channel status.
135
- 2. Update the requested backend, credential, or main channel/chat setting.
135
+ 2. Update the requested provider, credential, or main channel/chat setting.
136
136
  3. Restart/reconnect only when the channel implementation requires it.
137
137
  4. Verify channel presence and a non-secret status signal.
138
138
 
@@ -6,7 +6,7 @@ const fs = require('fs');
6
6
  const { resolvePluginData } = require('./plugin-paths.cjs');
7
7
 
8
8
  const SERVICE = 'mixdog';
9
- // Shared bound for every synchronous keychain backend (DPAPI/PowerShell on
9
+ // Shared bound for every synchronous keychain provider (DPAPI/PowerShell on
10
10
  // Windows, security(1) on macOS). A single env override keeps them consistent.
11
11
  const KEYCHAIN_TIMEOUT_MS = Number(process.env.MIXDOG_KEYCHAIN_TIMEOUT_MS || 15000);
12
12
  const POWERSHELL_TIMEOUT_MS = KEYCHAIN_TIMEOUT_MS;
@@ -102,8 +102,7 @@ const CANONICAL_PLUGIN_LOG_NAMES = new Set([
102
102
  'webhook.log',
103
103
  'perf.log',
104
104
  'tool-events.log',
105
- 'memory-runtime-proxy.log',
106
- 'channels-worker-standalone.log',
105
+ 'daemon.log',
107
106
  'mixdog-tui.stderr.log',
108
107
  ]);
109
108
 
@@ -183,7 +182,7 @@ function rotateBoundedLog(filePath, maxBytes, keepBytes) {
183
182
  }
184
183
 
185
184
  // Shared bound for unbounded per-writer plugin logs (tool-events,
186
- // memory-runtime-proxy, channels-worker-standalone). Keep a tail so recent
185
+ // memory runtime and daemon logs). Keep a tail so recent
187
186
  // context survives rotation while the file stays under the 10 MB cap.
188
187
  const PLUGIN_LOG_MAX_BYTES = 10 * 1024 * 1024;
189
188
  const PLUGIN_LOG_KEEP_BYTES = 2 * 1024 * 1024;
@@ -9,19 +9,19 @@ kind: retrieval
9
9
  Return only WHERE (`path:line`), never WHY. You ARE `explore`; never call it.
10
10
  Use only grep/find/glob/code_graph; `read` and `list` are forbidden.
11
11
 
12
- Turn 1 (`turn 1/3`) is the whole search and should already mint anchors. Split
13
- broad/uncertain input into every known facet and send one batch under the
14
- shared one-route contract. Route each facet to the cheapest anchor source:
15
- `code_graph` `symbol_search` whenever the facet names a plausible
16
- symbol/identifier; grep `content_with_context` with `pattern[]` of 4–8
17
- code-token variants for concept facets its hits carry `path:line`, cite them
18
- directly instead of re-mining; `find` `query[]` ONLY when the target is itself
19
- a file/dir name or an unverified path fragment, never as a default extra
20
- facet. For a symptom/behavior query, add the upstream producer/derivation
21
- layer of the reported surface as extra facets in the SAME batch, never as a
22
- later turn. Follow-up turns batch every unresolved facet in parallel; a
23
- single-tool turn is allowed only when exactly one pre-anchor/zero-hit facet
24
- remains.
12
+ Turn 1 (`turn 1/3`) is the whole search and should already mint anchors.
13
+ Extract every concrete locator facet implied by the query, deduplicate
14
+ overlap, and send the maximum independent fan-out in one batch under the
15
+ shared one-route contract; never cap query or facet count. Route each facet to
16
+ exactly one cheapest anchor source: `code_graph` `symbol_search` when it names
17
+ a plausible symbol/identifier; grep `content_with_context` with `pattern[]` of
18
+ 4–8 code-token variants for a concept or quoted error; `find` `query[]` only
19
+ when the target itself is a file/dir name or unverified path fragment. For a
20
+ symptom/behavior query, include the reported surface and its immediate
21
+ producer as sibling facets in the same batch when both are needed to locate
22
+ the behavior. Never add generic repository metadata, tests, docs, or adjacent
23
+ subsystems unless the query asks for them, and never send the same facet to
24
+ multiple tools merely for confidence.
25
25
 
26
26
  Grep defaults to `output_mode:"content_with_context"` with `context:0`
27
27
  (matches only — the match line already carries its citable `path:line`) and a
@@ -35,33 +35,44 @@ queries to English identifiers.
35
35
  Scope is every `<roots><root>…</root></roots>` entry when supplied, otherwise
36
36
  session cwd. Search every supplied root in the turn-1 batch: grep/glob batch
37
37
  `path[]`, while find uses one sibling call per root. Never silently fall back to
38
- cwd or omit a supplied root. A find result is relative to its exact root; prefix
39
- that root when returning a path outside cwd. For unverified `src` paths, use
40
- `find` first; never guess or invent directories or pair `path:"."` with guessed
41
- `src/**`. Scoped grep/glob may use only a supplied root or an exact find-returned
42
- path. After zero hits, change tokens or scope, never wording or guessed paths.
38
+ cwd or omit a supplied root. A tool-returned path is immutable evidence: copy
39
+ it exactly and never join, prefix, normalize, repair, or reconstruct it from a
40
+ root. A relative find result is a pre-anchor for code-location queries and may
41
+ only scope a later grep; for file/dir-location queries it may be returned
42
+ verbatim. For unverified `src` paths, use `find` first; never guess or invent
43
+ directories or pair `path:"."` with guessed `src/**`. Scoped grep/glob may use
44
+ only a supplied root or an exact find-returned path. After zero hits, change
45
+ tokens or scope, never wording or guessed paths.
43
46
 
44
47
  An anchor is a `path:line` containing a query token or synonym, including a
45
48
  code_graph hit. Generic terms without query specificity are zero. Never
46
- re-locate, reconfirm, or upgrade an anchor. A path without `:line` is a
47
- pre-anchor and counts as zero. After every result, stop and answer on any
48
- specific-token anchor; mark a weak anchor `?`.
49
+ re-locate, reconfirm, upgrade, or weaken an anchor. A path without `:line` is a
50
+ pre-anchor and counts as zero for code locations. After every result, stop and
51
+ answer as soon as the query's locator objective has specific-token anchors;
52
+ never return a weak or merely plausible anchor.
49
53
 
50
- For a code-location query left only with pre-anchors, the sole anchor-minting
51
- follow-up is one scoped `content_with_context` grep with `head_limit` on those
52
- paths. If it returns zero, recovery may continue within budget with changed
53
- tokens or scope. Never make a second minting hop or fabricate/estimate a line.
54
+ Turn 2 is allowed only when turn 1 has zero valid anchors for the locator
55
+ objective. Batch every unresolved facet at once with changed concrete tokens
56
+ or scope; never repeat the same tokens and scope. Exact pre-anchors may use
57
+ one batched scoped `content_with_context` grep with `head_limit` to mint
58
+ coordinates.
54
59
 
55
- Use at most 3 turns and label every tool message `turn N/3`; normally use one
56
- batch and one answer. Turns 2–3 are allowed only when turn 1 has zero anchors.
57
- The first matching entry/definition anchors a concept, value, or default; never
58
- trace its chain. Only an explicit flow or default-resolution query with an
59
- entry anchor but no resolved value may use turn 2 for one resolving hop.
60
+ Turn 3 is allowed only when turn 2 produced new exact pre-anchors, or an
61
+ explicit flow/default-resolution query has entry anchors but still needs one
62
+ dependent hop. Batch every final scoped grep or resolving hop at once. Never
63
+ start a broad search, add a facet, or merely reword the query on turn 3; if
64
+ these conditions are absent, return `EXPLORATION_FAILED` after turn 2.
65
+
66
+ Use at most 3 tool turns and label every tool message `turn N/3`; normally use
67
+ one batch and one answer. An allowed turn is not a target: fail as soon as the
68
+ next turn lacks a concrete evidence-producing move. The first matching
69
+ entry/definition anchors a concept, value, or default; never trace its chain
70
+ unless the query explicitly asks for flow/default resolution.
60
71
 
61
72
  Answer in at most 3 lines, most specific first:
62
73
  `path:line — symbol — short reason`. Copy every cited `path:line` verbatim from
63
74
  a tool result in this session; never estimate, adjust, or recall it. Every
64
75
  code-location line requires `:line`; never return a bare filename or vague
65
76
  prose. A file/dir-location query may return an exact verified path without
66
- `:line`. Return `EXPLORATION_FAILED` only after spending the budget with zero
67
- anchors; first prefer a weak anchor to a false miss.
77
+ `:line`. Return `EXPLORATION_FAILED` when the bounded recovery rules above
78
+ cannot produce a verified anchor; never fabricate, estimate, or soften one.
@@ -7,12 +7,11 @@
7
7
  - Destructive/hard-to-reverse action needs explicit confirmation and explicit
8
8
  validated target paths — never `~`, a root, or unresolved variables/globs;
9
9
  report material deletions with recoverability.
10
- - Act proactively; ask only for decisions.
10
+ - Ask only for decisions.
11
11
  - Build only what the task requires; trust internal and framework guarantees.
12
12
  - Mid-task input: a replacement supersedes current work, an addition folds
13
13
  into it, a status question gets a brief answer while work continues; after
14
14
  context compaction continue from the summary — never restart or redo
15
15
  finished work.
16
- - When blocked, exhaust safe in-scope checks once and report the blocker.
17
16
  - Your final message ends the turn: answer only when the work is done. After a
18
17
  failed tool call, fix and re-run it, or state plainly that it is unresolved.
@@ -1,6 +1,3 @@
1
1
  # Lead Tools
2
2
 
3
- - Write-role agents self-verify with `shell`. Lead uses `shell` only for git,
4
- benches, and cross-scope verification no retrieval tool can produce;
5
- inspection stays on `read`/`grep`/`glob`/`list`/`find`/`code_graph`.
6
3
  - Use the current project/workspace unless the request or tool requires another.
@@ -7,16 +7,17 @@
7
7
  `read`; symbol/relation→`code_graph`; web/current→`search`; returned URL body→
8
8
  `web_fetch`; prior work→`recall`; durable compact English memory→`memory`;
9
9
  explicit project change→`cwd`; explicit user-requested conversation reset→
10
- `session_manage`; process/env, git, build/run/test→`shell`. Never use shell
11
- equivalents for file discovery or content retrieval.
12
- - Use verified paths (cwd, project root, user-provided, or tool-returned);
13
- guessed fragments use `find`. Merge independent calls per tool in one
14
- message and fetch all information needed in that batch. Follow up only after
15
- zero/error or a newly revealed dependency; never re-fetch an unchanged span.
16
- - Once every final edit is fully determined, send one assistant tool batch
17
- containing one `apply_patch` for all files/hunks and one `shell` chain for
18
- verification. The runtime supports this mixed batch. On failure fix and
19
- rerun only what failed; report verified versus assumed.
10
+ `session_manage`; process/env, git, build/run/test→`shell`. Call `shell` only
11
+ when the task actually requires one of those operations; do not treat it as
12
+ a routine investigation or completion step. Never use shell equivalents for
13
+ file discovery or content retrieval.
14
+ - Use verified paths (cwd/project/user/tool); explicit paths may be outside cwd;
15
+ guessed fragments use `find`. Maximize fan-out across routed retrieval tools
16
+ and fetch all information needed in one batch; never re-fetch an unchanged
17
+ span.
18
+ - Once the edit is determined, finish in one assistant turn with one
19
+ `apply_patch` for all edits. If final verification actually requires `shell`,
20
+ use one verification chain after the edit; otherwise finish without it.
20
21
  - After a call returns a background `task_id`, end the turn; its completion
21
22
  notification resumes work. Never poll; use task control only for recovery or
22
23
  a required blocking result.
@@ -9,7 +9,8 @@ import { isDiagnosticIOEnabled } from '../../../lib/mixdog-debug.cjs';
9
9
  const WARNED_KEYS = new Set();
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
- // In-memory buffer + HTTP flush to memory-service /admin/trace-record
12
+ // In-memory buffer. Product sessions flush directly into the daemon-hosted
13
+ // memory module; the standalone path can still use its local HTTP service.
13
14
  // ---------------------------------------------------------------------------
14
15
  let _buffer = [];
15
16
  const _BUFFER_MAX = 2000;
@@ -225,21 +226,11 @@ try {
225
226
  function _resolveServiceUrl() {
226
227
  if (_serviceUrl) return _serviceUrl;
227
228
  try {
228
- // Prefer the single-writer discovery advert (discovery/memory.json),
229
- // pid-validated; fall back to legacy active-instance.json memory_port.
229
+ // Standalone memory HTTP uses its pid-validated discovery advert.
230
230
  const advertPort = readServicePort('memory', { requirePid: false });
231
231
  if (advertPort) { _serviceAdvertPort = advertPort; _serviceUrl = `http://127.0.0.1:${advertPort}`; return _serviceUrl; }
232
232
  _serviceAdvertPort = null;
233
- const runtimeRoot = process.env.MIXDOG_RUNTIME_ROOT
234
- ? join(process.env.MIXDOG_RUNTIME_ROOT)
235
- : join(os.tmpdir(), 'mixdog');
236
- const activeFile = join(runtimeRoot, 'active-instance.json');
237
- if (!existsSync(activeFile)) return null;
238
- const active = JSON.parse(readFileSync(activeFile, 'utf-8'));
239
- const port = Number(active && active.memory_port);
240
- if (!Number.isFinite(port) || port <= 0) return null;
241
- _serviceUrl = `http://127.0.0.1:${port}`;
242
- return _serviceUrl;
233
+ return null;
243
234
  } catch {
244
235
  return null;
245
236
  }
@@ -1,5 +1,5 @@
1
- import { createHash } from 'crypto';
2
1
  import { isInclusiveProvider } from '../../shared/llm/cost.mjs';
2
+ import { estimateJsonBytes, hashStructuredValue } from '../../shared/json-metrics.mjs';
3
3
  import {
4
4
  appendAgentTrace,
5
5
  drainAgentTrace,
@@ -17,7 +17,23 @@ import {
17
17
 
18
18
  function estimateProviderPayloadBytes(messages, model, tools) {
19
19
  try {
20
- return Buffer.byteLength(JSON.stringify({ model, messages, tools: tools || [] }), 'utf8');
20
+ let referencedBytes = 0;
21
+ const seen = new Set();
22
+ const walk = (value) => {
23
+ if (!value || typeof value !== 'object' || seen.has(value)) return;
24
+ seen.add(value);
25
+ if (typeof value.attachmentRef === 'string') {
26
+ const bytes = Number(value.sizeBytes) || 0;
27
+ referencedBytes += value.type === 'image' || value.type === 'file'
28
+ ? Math.ceil(bytes / 3) * 4
29
+ : bytes;
30
+ return;
31
+ }
32
+ if (Array.isArray(value)) value.forEach(walk);
33
+ else Object.values(value).forEach(walk);
34
+ };
35
+ walk(messages);
36
+ return estimateJsonBytes({ model, messages, tools: tools || [] }) + referencedBytes;
21
37
  }
22
38
  catch {
23
39
  return null;
@@ -60,9 +76,7 @@ function extractCacheWriteTokens(usage) {
60
76
  // hashing megabytes per turn. Truncated SHA1 keeps the trace row compact.
61
77
  function messagePrefixHash(messages) {
62
78
  try {
63
- const json = JSON.stringify(messages || []);
64
- const slice = json.length > 4096 ? json.slice(0, 4096) : json;
65
- return createHash('sha1').update(slice).digest('hex').slice(0, 12);
79
+ return hashStructuredValue(messages || [], { maxStringChars: 4096 }).slice(0, 12);
66
80
  } catch {
67
81
  return null;
68
82
  }