mixdog 0.9.155 → 0.9.157

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 (171) hide show
  1. package/package.json +6 -8
  2. package/scripts/build-ffmpeg-runtime.sh +141 -0
  3. package/scripts/generate-voice-runtime-manifest.mjs +70 -0
  4. package/scripts/lib/embedding-model-bench-core.mjs +353 -0
  5. package/scripts/lib/stage-postgres-runtime-windows.ps1 +224 -0
  6. package/scripts/native-tool-download.mjs +6 -7
  7. package/scripts/prepare-native-assets.mjs +13 -33
  8. package/scripts/prune-desktop-runtime.mjs +304 -0
  9. package/scripts/prune-embedding-runtime.mjs +19 -0
  10. package/scripts/run-office-live-tests.mjs +17 -21
  11. package/scripts/sync-voice-runtime-manifest.mjs +1 -0
  12. package/scripts/verify-release-assets.mjs +0 -63
  13. package/scripts/verify-voice-runtime-release.mjs +26 -8
  14. package/scripts/voice-runtime-config.json +2 -1
  15. package/src/app.mjs +0 -1
  16. package/src/headless-command.mjs +1 -2
  17. package/src/headless-exec.mjs +2 -5
  18. package/src/help.mjs +2 -1
  19. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +40 -15
  20. package/src/runtime/agent/orchestrator/providers/lib/anthropic-replay-blocks.mjs +98 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +14 -7
  22. package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +160 -0
  23. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +14 -8
  24. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +13 -1
  25. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +1 -28
  26. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +30 -2
  27. package/src/runtime/agent/orchestrator/session/context-utils.mjs +19 -277
  28. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +21 -1
  29. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +19 -5
  30. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +5 -7
  31. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -0
  32. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +48 -15
  33. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +203 -0
  34. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +15 -2
  35. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +29 -7
  36. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +0 -24
  37. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +38 -1
  38. package/src/runtime/agent/orchestrator/session/thinking-replay-recovery.mjs +113 -0
  39. package/src/runtime/agent/orchestrator/session/token-estimate.mjs +130 -0
  40. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +17 -22
  41. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +11 -0
  43. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +29 -5
  44. package/src/runtime/agent/orchestrator/tools/builtin/search-admission.mjs +33 -0
  45. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +9 -0
  46. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +57 -7
  47. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  48. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -6
  49. package/src/runtime/browser-bridge/action-schema.mjs +307 -15
  50. package/src/runtime/browser-bridge/browser-action-contract.d.mts +4 -0
  51. package/src/runtime/browser-bridge/browser-action-contract.mjs +25 -0
  52. package/src/runtime/browser-bridge/client.mjs +119 -35
  53. package/src/runtime/browser-bridge/tool-defs.mjs +80 -24
  54. package/src/runtime/channels/data/voice-runtime-manifest.json +35 -35
  55. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +13 -1
  56. package/src/runtime/computer-bridge/action-schema.mjs +228 -32
  57. package/src/runtime/computer-bridge/client.mjs +59 -11
  58. package/src/runtime/computer-bridge/tool-defs.mjs +7 -2
  59. package/src/runtime/memory/data/runtime-manifest.json +3 -3
  60. package/src/runtime/memory/index.mjs +43 -11
  61. package/src/runtime/memory/lib/embedding-cache-retention.mjs +55 -0
  62. package/src/runtime/memory/lib/embedding-model-cache-compression.mjs +50 -0
  63. package/src/runtime/memory/lib/embedding-model-config.mjs +16 -12
  64. package/src/runtime/memory/lib/embedding-provider.mjs +9 -1
  65. package/src/runtime/memory/lib/embedding-reindex.mjs +54 -0
  66. package/src/runtime/memory/lib/embedding-worker.mjs +18 -5
  67. package/src/runtime/memory/lib/http-router.mjs +10 -14
  68. package/src/runtime/memory/lib/light-ko-morph.mjs +88 -0
  69. package/src/runtime/memory/lib/memory-embed.mjs +12 -30
  70. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -167
  71. package/src/runtime/memory/lib/memory-text-utils.mjs +14 -56
  72. package/src/runtime/memory/lib/query-handlers.mjs +32 -39
  73. package/src/runtime/memory/lib/recall-embedding-readiness.mjs +78 -0
  74. package/src/runtime/memory/lib/recall-format.mjs +6 -2
  75. package/src/runtime/memory/lib/recall-fusion.mjs +53 -0
  76. package/src/runtime/memory/lib/recall-scoring.mjs +2 -140
  77. package/src/runtime/memory/lib/trace-mode.mjs +58 -0
  78. package/src/runtime/memory/lib/trace-store.mjs +75 -43
  79. package/src/runtime/office/assurance-benchmark.mjs +12 -0
  80. package/src/runtime/office/assurance.mjs +45 -5
  81. package/src/runtime/office/benchmark.mjs +6 -1
  82. package/src/runtime/office/capabilities.mjs +17 -13
  83. package/src/runtime/office/composition-system.mjs +33 -0
  84. package/src/runtime/office/content-model.mjs +0 -1
  85. package/src/runtime/office/design-docx-components.mjs +264 -0
  86. package/src/runtime/office/design-docx.mjs +203 -0
  87. package/src/runtime/office/design-library-core.mjs +252 -0
  88. package/src/runtime/office/design-library-pack.mjs +374 -0
  89. package/src/runtime/office/design-library.mjs +26 -1268
  90. package/src/runtime/office/design-pptx-executive.mjs +335 -0
  91. package/src/runtime/office/design-pptx-plan.mjs +891 -0
  92. package/src/runtime/office/design-pptx-primitives.mjs +87 -0
  93. package/src/runtime/office/design-pptx.mjs +776 -0
  94. package/src/runtime/office/design-review.mjs +373 -0
  95. package/src/runtime/office/design-system.mjs +44 -1994
  96. package/src/runtime/office/design-template-index.mjs +266 -0
  97. package/src/runtime/office/design-template-inspect.mjs +434 -0
  98. package/src/runtime/office/design-tokens.mjs +431 -0
  99. package/src/runtime/office/design-xlsx-components.mjs +171 -0
  100. package/src/runtime/office/design-xlsx.mjs +364 -0
  101. package/src/runtime/office/document-preview.mjs +139 -0
  102. package/src/runtime/office/index.mjs +14 -2213
  103. package/src/runtime/office/office-actions.mjs +940 -0
  104. package/src/runtime/office/office-com-host.ps1 +439 -73
  105. package/src/runtime/office/office-com-session-host.ps1 +57 -8
  106. package/src/runtime/office/office-core.mjs +270 -0
  107. package/src/runtime/office/office-documents.mjs +103 -0
  108. package/src/runtime/office/office-sessions.mjs +465 -0
  109. package/src/runtime/office/office-transactions.mjs +432 -0
  110. package/src/runtime/office/pdf-analysis.mjs +57 -0
  111. package/src/runtime/office/pdf-render.mjs +4 -1
  112. package/src/runtime/office/polish-benchmark.mjs +63 -15
  113. package/src/runtime/office/portable-cells.mjs +271 -0
  114. package/src/runtime/office/portable-chart.mjs +240 -0
  115. package/src/runtime/office/portable-docx-parts.mjs +326 -0
  116. package/src/runtime/office/portable-docx-xml.mjs +258 -0
  117. package/src/runtime/office/portable-docx.mjs +755 -0
  118. package/src/runtime/office/portable-ooxml.mjs +24 -2899
  119. package/src/runtime/office/portable-opc.mjs +350 -0
  120. package/src/runtime/office/portable-package.mjs +44 -0
  121. package/src/runtime/office/portable-pivot.mjs +229 -0
  122. package/src/runtime/office/portable-pptx-chart.mjs +101 -0
  123. package/src/runtime/office/portable-pptx-charts.mjs +292 -0
  124. package/src/runtime/office/portable-pptx-core.mjs +316 -0
  125. package/src/runtime/office/portable-pptx-deck.mjs +314 -0
  126. package/src/runtime/office/portable-pptx-package.mjs +439 -0
  127. package/src/runtime/office/portable-pptx-shapes.mjs +607 -0
  128. package/src/runtime/office/portable-pptx.mjs +66 -0
  129. package/src/runtime/office/portable-sheet-parts.mjs +152 -0
  130. package/src/runtime/office/portable-sheet-xml.mjs +320 -0
  131. package/src/runtime/office/portable-slide-shapes.mjs +0 -4
  132. package/src/runtime/office/portable-snapshot.mjs +528 -0
  133. package/src/runtime/office/portable-soffice.mjs +293 -0
  134. package/src/runtime/office/portable-validation.mjs +734 -0
  135. package/src/runtime/office/portable-xlsx.mjs +848 -0
  136. package/src/runtime/office/portable-xml.mjs +234 -0
  137. package/src/runtime/office/quality-live-benchmark.mjs +119 -16
  138. package/src/runtime/office/quality-pipeline.mjs +0 -5
  139. package/src/runtime/office/snapshot-contract.mjs +190 -0
  140. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  141. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +10 -0
  142. package/src/runtime/office/text-metrics.mjs +286 -0
  143. package/src/runtime/office/tool-defs.mjs +2 -2
  144. package/src/runtime/office/xlsx-contract.mjs +20 -0
  145. package/src/runtime/shared/bridge-tool-args.mjs +42 -0
  146. package/src/runtime/shared/heap-cap.mjs +56 -0
  147. package/src/runtime/shared/idle-gc.mjs +150 -0
  148. package/src/runtime/shared/native-tool-paths.mjs +0 -1
  149. package/src/runtime/shared/tool-surface.mjs +40 -6
  150. package/src/runtime/shared/tool-workload-gates.mjs +12 -0
  151. package/src/session-runtime/context-status.mjs +10 -1
  152. package/src/session-runtime/goal-runtime.mjs +38 -13
  153. package/src/session-runtime/prewarm.mjs +0 -6
  154. package/src/session-runtime/runtime-core.mjs +8 -0
  155. package/src/session-runtime/session-lifecycle.mjs +34 -3
  156. package/src/session-runtime/tool-profile.mjs +57 -0
  157. package/src/session-runtime/tool-surface.mjs +19 -3
  158. package/src/standalone/channel-worker.mjs +3 -1
  159. package/src/standalone/daemon.mjs +28 -4
  160. package/src/standalone/memory-runtime-proxy.mjs +2 -1
  161. package/src/standalone/session-client.mjs +5 -0
  162. package/src/standalone/session-runtime-host.mjs +2 -28
  163. package/src/standalone/session-runtime-worker.mjs +1 -2
  164. package/src/tui/dist/index.mjs +29 -6
  165. package/scripts/build-token-addon.mjs +0 -32
  166. package/src/runtime/agent/orchestrator/session/token-bpe.mjs +0 -42
  167. package/src/runtime/agent/orchestrator/session/token-native-worker.mjs +0 -35
  168. package/src/runtime/agent/orchestrator/session/token-native.mjs +0 -288
  169. package/src/runtime/agent/orchestrator/tools/token-addon-fetcher.mjs +0 -195
  170. package/src/runtime/agent/orchestrator/tools/token-manifest.json +0 -26
  171. package/src/runtime/memory/lib/ko-morph.mjs +0 -313
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.155",
3
+ "version": "0.9.157",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -54,7 +54,7 @@
54
54
  "smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:patch && npm run smoke:compact",
55
55
  "smoke:boot": "node scripts/boot-smoke.mjs",
56
56
  "smoke:compact": "node scripts/compact-smoke.mjs",
57
- "test:compact": "node --test scripts/suite-compact-test.mjs",
57
+ "test:compact": "node --test scripts/suite-compact-test.mjs src/runtime/agent/orchestrator/session/token-estimate.test.mjs",
58
58
  "test:markdown": "node --test scripts/markdown-surface-test.mjs",
59
59
  "smoke:loop": "node scripts/smoke-loop.mjs",
60
60
  "smoke:loop:final": "node scripts/smoke-loop-report.mjs --require-complete --min-elapsed 5h --min-iterations 400 --max-gap 60s --max-smoke-ms 10000 --max-avg-smoke-ms 8500 --max-step-ms smoke.mjs=4000 --max-step-ms boot-smoke.mjs=8000 --max-rss-mb 140 --max-rss-growth-mb 50",
@@ -72,7 +72,7 @@
72
72
  "test:session-transport": "node --test scripts/session-transport-test.mjs scripts/daemon-bootstrap-test.mjs src/runtime/shared/child-spawn-remote.test.mjs src/standalone/session-runtime-agent-control-client.test.mjs src/standalone/session-runtime-dispatch-cancel.test.mjs src/standalone/session-runtime-host-factory.test.mjs src/standalone/session-runtime-inline-host.test.mjs src/standalone/session-runtime-provider-cooldown.test.mjs src/standalone/session-runtime-shard-host.test.mjs src/standalone/session-runtime-shard-router.test.mjs",
73
73
  "test:session": "node --test scripts/runtime-turn-contract-test.mjs scripts/session-save-fault-store-test.mjs scripts/turn-review-revert-test.mjs src/runtime/shared/tool-surface.test.mjs src/runtime/bridge-clients.test.mjs",
74
74
  "test:quick": "npm run test:session && npm run test:session-transport && npm run test:desktop-main --prefix apps/desktop",
75
- "test:office": "node --test src/runtime/office/office-runtime.test.mjs src/runtime/office/office-assurance.test.mjs src/runtime/office/portable-authoring.test.mjs && npm run test:office-approval --prefix apps/desktop",
75
+ "test:office": "node --test scripts/run-office-live-tests.test.mjs src/runtime/office/office-runtime.test.mjs src/runtime/office/office-assurance.test.mjs src/runtime/office/portable-authoring.test.mjs && npm run test:office-approval --prefix apps/desktop",
76
76
  "test:office:live": "node scripts/run-office-live-tests.mjs",
77
77
  "test:media": "node --test src/runtime/media/store.test.mjs src/runtime/media/renditions.test.mjs src/runtime/media/adapters/codex-image.test.mjs",
78
78
  "failures": "node scripts/tool-failures.mjs",
@@ -125,7 +125,7 @@
125
125
  "dependencies": {
126
126
  "@anthropic-ai/sdk": "^0.105.0",
127
127
  "@google/generative-ai": "^0.24.1",
128
- "@huggingface/transformers": "^3.8.1",
128
+ "@huggingface/transformers": "^4.2.0",
129
129
  "@modelcontextprotocol/sdk": "^1.29.0",
130
130
  "@mozilla/readability": "^0.6.0",
131
131
  "@napi-rs/canvas": "^1.0.8",
@@ -136,10 +136,9 @@
136
136
  "ink": "^7.1.0",
137
137
  "jsdom": "^27.0.0",
138
138
  "jszip": "^3.10.1",
139
- "kiwi-nlp": "^0.21.0",
140
139
  "marked": "^14.1.4",
141
140
  "node-cron": "^4.5.0",
142
- "onnxruntime-node": "1.23.2",
141
+ "onnxruntime-node": "1.29.0",
143
142
  "openai": "^6.44.0",
144
143
  "pdf-lib": "^1.17.1",
145
144
  "pdfjs-dist": "^6.2.108",
@@ -150,7 +149,6 @@
150
149
  "string-width": "^8.2.1",
151
150
  "strip-ansi": "^7.2.0",
152
151
  "tesseract.js": "^7.0.0",
153
- "tiktoken": "^1.0.22",
154
152
  "undici": "^8.5.0",
155
153
  "unpdf": "^1.8.0",
156
154
  "wrap-ansi": "^10.0.0",
@@ -160,7 +158,7 @@
160
158
  },
161
159
  "overrides": {
162
160
  "onnxruntime-node": {
163
- ".": "1.23.2",
161
+ ".": "1.29.0",
164
162
  "global-agent": "^4.1.3"
165
163
  },
166
164
  "sharp": "^0.35.3"
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ FFMPEG_VERSION="${FFMPEG_VERSION:-6.1.1}"
5
+ TARGET_OS="${TARGET_OS:?TARGET_OS is required}"
6
+ TARGET_ARCH="${TARGET_ARCH:?TARGET_ARCH is required}"
7
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
+ BUILD_DIR="$ROOT_DIR/build/ffmpeg-runtime-${TARGET_OS}-${TARGET_ARCH}"
9
+ SOURCE_DIR="$BUILD_DIR/ffmpeg-${FFMPEG_VERSION}"
10
+ DIST_DIR="$ROOT_DIR/dist"
11
+
12
+ rm -rf "$BUILD_DIR"
13
+ mkdir -p "$BUILD_DIR" "$DIST_DIR"
14
+
15
+ echo "==> Downloading FFmpeg n${FFMPEG_VERSION}"
16
+ curl -fsSL --retry 3 \
17
+ "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" \
18
+ -o "$BUILD_DIR/ffmpeg.tar.xz"
19
+ tar -xJf "$BUILD_DIR/ffmpeg.tar.xz" -C "$BUILD_DIR"
20
+
21
+ CONFIGURE_FLAGS=(
22
+ --disable-everything
23
+ --disable-autodetect
24
+ --disable-debug
25
+ --disable-doc
26
+ --disable-network
27
+ --disable-shared
28
+ --enable-static
29
+ --enable-small
30
+ --enable-ffmpeg
31
+ --enable-avcodec
32
+ --enable-avfilter
33
+ --enable-avformat
34
+ --enable-swresample
35
+ --enable-protocol=file,pipe
36
+ --enable-demuxer=aac,ac3,aiff,amr,ape,asf,au,avi,caf,flac,flv,matroska,mov,mp3,mpegts,ogg,wav,wv
37
+ --enable-decoder=aac,aac_fixed,ac3,alac,amrnb,amrwb,ape,eac3,flac,mp1,mp1float,mp2,mp2float,mp3,mp3float,opus,pcm_alaw,pcm_f32be,pcm_f32le,pcm_f64be,pcm_f64le,pcm_mulaw,pcm_s16be,pcm_s16le,pcm_s24be,pcm_s24le,pcm_s32be,pcm_s32le,pcm_s8,pcm_u16be,pcm_u16le,pcm_u24be,pcm_u24le,pcm_u32be,pcm_u32le,pcm_u8,vorbis,wavpack,wmav1,wmav2,wmapro
38
+ --enable-encoder=pcm_s16le
39
+ --enable-parser=aac,aac_latm,ac3,flac,mpegaudio,opus,vorbis
40
+ --enable-filter=aformat,anull,aresample
41
+ --enable-muxer=wav
42
+ )
43
+
44
+ EXE_NAME=ffmpeg
45
+ STRIP_COMMAND=strip
46
+ if [[ "$TARGET_OS" == "win32" ]]; then
47
+ CONFIGURE_FLAGS+=(
48
+ --target-os=mingw32
49
+ --arch=x86_64
50
+ --cc=gcc
51
+ --ar=ar
52
+ --ranlib=ranlib
53
+ --nm=nm
54
+ --strip=strip
55
+ --extra-ldflags=-static
56
+ )
57
+ EXE_NAME=ffmpeg.exe
58
+ elif [[ "$TARGET_OS" == "darwin" ]]; then
59
+ STRIP_COMMAND="strip -x"
60
+ fi
61
+
62
+ echo "==> Configuring minimal audio-transcode runtime"
63
+ pushd "$SOURCE_DIR" >/dev/null
64
+ ./configure "${CONFIGURE_FLAGS[@]}"
65
+ make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.logicalcpu)"
66
+ popd >/dev/null
67
+
68
+ RUNTIME_EXE="$SOURCE_DIR/ffmpeg"
69
+ if [[ "$TARGET_OS" == "win32" ]]; then
70
+ RUNTIME_EXE="$SOURCE_DIR/ffmpeg.exe"
71
+ fi
72
+ if [[ ! -f "$RUNTIME_EXE" ]]; then
73
+ echo "FFmpeg build did not produce $RUNTIME_EXE" >&2
74
+ exit 1
75
+ fi
76
+
77
+ read -r -a STRIP_PARTS <<< "$STRIP_COMMAND"
78
+ "${STRIP_PARTS[@]}" "$RUNTIME_EXE"
79
+
80
+ echo "==> Smoke testing WAV normalization"
81
+ PYTHON_COMMAND=
82
+ for candidate in python3 python; do
83
+ if command -v "$candidate" >/dev/null 2>&1; then
84
+ PYTHON_COMMAND="$candidate"
85
+ break
86
+ fi
87
+ done
88
+ if [[ -z "$PYTHON_COMMAND" ]]; then
89
+ echo "Python 3 is required for the FFmpeg runtime smoke test" >&2
90
+ exit 1
91
+ fi
92
+
93
+ "$PYTHON_COMMAND" - "$BUILD_DIR/input.wav" <<'PY'
94
+ import math
95
+ import struct
96
+ import sys
97
+ import wave
98
+
99
+ with wave.open(sys.argv[1], "wb") as wav:
100
+ wav.setnchannels(2)
101
+ wav.setsampwidth(2)
102
+ wav.setframerate(48000)
103
+ frames = bytearray()
104
+ for index in range(4800):
105
+ sample = int(math.sin(index * 2 * math.pi * 440 / 48000) * 12000)
106
+ frames.extend(struct.pack("<hh", sample, sample))
107
+ wav.writeframes(frames)
108
+ PY
109
+ "$RUNTIME_EXE" -hide_banner -loglevel error \
110
+ -i "$BUILD_DIR/input.wav" -ar 16000 -ac 1 -threads 1 -y "$BUILD_DIR/output.wav"
111
+ "$PYTHON_COMMAND" - "$BUILD_DIR/output.wav" <<'PY'
112
+ import sys
113
+ import wave
114
+
115
+ with wave.open(sys.argv[1], "rb") as wav:
116
+ assert wav.getframerate() == 16000
117
+ assert wav.getnchannels() == 1
118
+ assert wav.getnframes() > 0
119
+ PY
120
+
121
+ RUNTIME_BYTES="$(wc -c < "$RUNTIME_EXE" | tr -d ' ')"
122
+ if (( RUNTIME_BYTES > 26214400 )); then
123
+ echo "Minimal FFmpeg exceeds 25 MiB: $RUNTIME_BYTES bytes" >&2
124
+ exit 1
125
+ fi
126
+
127
+ ASSET_NAME="ffmpeg-${TARGET_OS}-${TARGET_ARCH}.gz"
128
+ gzip -9 -n -c "$RUNTIME_EXE" > "$DIST_DIR/$ASSET_NAME"
129
+ ASSET_BYTES="$(wc -c < "$DIST_DIR/$ASSET_NAME" | tr -d ' ')"
130
+ if (( ASSET_BYTES > 15728640 )); then
131
+ echo "Compressed FFmpeg exceeds 15 MiB: $ASSET_BYTES bytes" >&2
132
+ exit 1
133
+ fi
134
+
135
+ if command -v sha256sum >/dev/null 2>&1; then
136
+ (cd "$DIST_DIR" && sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256")
137
+ else
138
+ (cd "$DIST_DIR" && shasum -a 256 "$ASSET_NAME" > "$ASSET_NAME.sha256")
139
+ fi
140
+
141
+ echo "Built $DIST_DIR/$ASSET_NAME ($RUNTIME_BYTES bytes raw, $ASSET_BYTES bytes compressed)"
@@ -10,6 +10,7 @@ const RELEASE_HASH_INPUTS = [
10
10
  new URL('../.github/workflows/build-voice-runtime.yml', import.meta.url),
11
11
  new URL('./build-voice-runtime-unix.sh', import.meta.url),
12
12
  new URL('./build-voice-runtime-windows.ps1', import.meta.url),
13
+ new URL('./build-ffmpeg-runtime.sh', import.meta.url),
13
14
  new URL('./generate-voice-runtime-manifest.mjs', import.meta.url),
14
15
  ]
15
16
 
@@ -21,6 +22,9 @@ function loadVoiceRuntimeConfig() {
21
22
  if (!/^[a-f0-9]{40}$/.test(config.whisperCommit || '')) {
22
23
  throw new Error('voice runtime config has an invalid whisperCommit')
23
24
  }
25
+ if (!/^\d+\.\d+\.\d+-mixdog\.\d+$/.test(config.ffmpegVersion || '')) {
26
+ throw new Error('voice runtime config has an invalid ffmpegVersion')
27
+ }
24
28
  if (!Array.isArray(config.platforms) || config.platforms.length === 0) {
25
29
  throw new Error('voice runtime config has no platforms')
26
30
  }
@@ -133,6 +137,38 @@ export function buildVoiceRuntimePlatforms(
133
137
  return platforms
134
138
  }
135
139
 
140
+ function ffmpegAssetName(spec) {
141
+ return `ffmpeg-${spec.os}-${spec.arch}.gz`
142
+ }
143
+
144
+ export function buildVoiceFfmpegPlatforms(
145
+ releaseAssets,
146
+ { repository = 'tribgames/mixdog', tag = VOICE_RUNTIME_TAG } = {},
147
+ ) {
148
+ const byName = new Map((releaseAssets || []).map((asset) => [asset.name, asset]))
149
+ const platforms = {}
150
+ for (const spec of VOICE_RUNTIME_CONFIG.platforms) {
151
+ const assetName = ffmpegAssetName(spec)
152
+ const asset = byName.get(assetName)
153
+ if (!asset) throw new Error(`release is missing required asset ${assetName}`)
154
+ if (!Number.isSafeInteger(asset.size) || asset.size <= 0) {
155
+ throw new Error(`release asset ${assetName} has invalid size`)
156
+ }
157
+ platforms[spec.key] = {
158
+ url: `https://github.com/${repository}/releases/download/${tag}/${assetName}`,
159
+ sha256: releaseAssetSha256(asset),
160
+ size: asset.size,
161
+ format: 'gz',
162
+ executable: spec.os === 'win32' ? 'ffmpeg.exe' : 'ffmpeg',
163
+ }
164
+ }
165
+ return {
166
+ version: VOICE_RUNTIME_CONFIG.ffmpegVersion,
167
+ source: 'FFmpeg n6.1.1 minimal audio-transcode build',
168
+ platforms,
169
+ }
170
+ }
171
+
136
172
  function githubHeaders(token, accept = 'application/vnd.github+json') {
137
173
  return {
138
174
  Accept: accept,
@@ -207,6 +243,7 @@ export function voiceRuntimeIdentity(manifest) {
207
243
  source: manifest?.source,
208
244
  release_tag: manifest?.release_tag,
209
245
  build_hash: manifest?.build_hash,
246
+ ffmpeg: manifest?.ffmpeg,
210
247
  platforms: manifest?.platforms,
211
248
  }
212
249
  }
@@ -258,6 +295,36 @@ export async function assertVoiceRuntimeManifestIdentity(
258
295
  throw new Error(`voice runtime manifest ${spec.key} does not match the release config`)
259
296
  }
260
297
  }
298
+ const expectedFfmpeg = buildVoiceFfmpegPlatforms(
299
+ VOICE_RUNTIME_CONFIG.platforms.map((spec, index) => ({
300
+ name: ffmpegAssetName(spec),
301
+ size: index + 1,
302
+ digest: `sha256:${String(index + 1).padStart(64, '0')}`,
303
+ })),
304
+ { repository, tag: VOICE_RUNTIME_TAG },
305
+ )
306
+ if (
307
+ manifest?.ffmpeg?.version !== expectedFfmpeg.version
308
+ || manifest?.ffmpeg?.source !== expectedFfmpeg.source
309
+ || JSON.stringify(Object.keys(manifest?.ffmpeg?.platforms || {}))
310
+ !== JSON.stringify(Object.keys(expectedFfmpeg.platforms))
311
+ ) {
312
+ throw new Error('voice runtime FFmpeg manifest does not match the release config')
313
+ }
314
+ for (const spec of VOICE_RUNTIME_CONFIG.platforms) {
315
+ const actual = manifest.ffmpeg.platforms[spec.key]
316
+ const expected = expectedFfmpeg.platforms[spec.key]
317
+ if (
318
+ actual?.url !== expected.url
319
+ || actual?.format !== expected.format
320
+ || actual?.executable !== expected.executable
321
+ || !/^[a-f0-9]{64}$/.test(actual?.sha256 || '')
322
+ || !Number.isSafeInteger(actual?.size)
323
+ || actual.size <= 0
324
+ ) {
325
+ throw new Error(`voice runtime FFmpeg ${spec.key} does not match the release config`)
326
+ }
327
+ }
261
328
  }
262
329
 
263
330
  async function main() {
@@ -269,11 +336,13 @@ async function main() {
269
336
  const manifestPath = resolve(process.argv[2] || 'src/runtime/channels/data/voice-runtime-manifest.json')
270
337
  const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
271
338
  let platforms
339
+ let ffmpeg
272
340
  let lastError
273
341
  for (let attempt = 1; attempt <= 5; attempt++) {
274
342
  try {
275
343
  const release = await fetchGithubRelease(repository, tag, process.env.GITHUB_TOKEN)
276
344
  platforms = buildVoiceRuntimePlatforms(release.assets, { repository, tag })
345
+ ffmpeg = buildVoiceFfmpegPlatforms(release.assets, { repository, tag })
277
346
  lastError = null
278
347
  break
279
348
  } catch (error) {
@@ -286,6 +355,7 @@ async function main() {
286
355
  manifest.source = `ggml-org/whisper.cpp@${VOICE_RUNTIME_COMMIT}`
287
356
  manifest.release_tag = tag
288
357
  manifest.build_hash = await voiceRuntimeBuildHash()
358
+ manifest.ffmpeg = ffmpeg
289
359
  manifest.platforms = platforms
290
360
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
291
361
  console.log(`updated ${manifestPath} from ${tag}`)
@@ -0,0 +1,353 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { parsePeriod } from '../../src/runtime/memory/lib/recall-format.mjs'
3
+
4
+ export const CASE_FILES = Object.freeze([
5
+ 'recall-bench-cases.json',
6
+ 'recall-quality-cases.json',
7
+ 'recall-repo-cases.json',
8
+ 'recall-usecase-cases.json',
9
+ 'recall-event-cases.json',
10
+ 'recall-history-cases.json',
11
+ ])
12
+
13
+ const RETRIEVAL_INSTRUCTION = 'Given a web search query, retrieve relevant passages that answer the query'
14
+
15
+ export const MODEL_SPECS = Object.freeze({
16
+ harrier: Object.freeze({
17
+ label: 'Harrier 270M q4',
18
+ modelId: 'onnx-community/harrier-oss-v1-270m-ONNX',
19
+ dtype: 'q4',
20
+ pooling: 'last_token',
21
+ outputName: 'sentence_embedding',
22
+ queryPrefix: `Instruct: ${RETRIEVAL_INSTRUCTION}\nQuery: `,
23
+ documentPrefix: '',
24
+ batchSize: 16,
25
+ }),
26
+ granite97: Object.freeze({
27
+ label: 'Granite 97M quint8',
28
+ modelId: 'ibm-granite/granite-embedding-97m-multilingual-r2',
29
+ dtype: 'fp32',
30
+ modelFileName: 'model_quint8_avx2',
31
+ pooling: 'cls',
32
+ outputName: '',
33
+ queryPrefix: '',
34
+ documentPrefix: '',
35
+ batchSize: 16,
36
+ }),
37
+ e5small: Object.freeze({
38
+ label: 'multilingual-e5-small q8',
39
+ modelId: 'Xenova/multilingual-e5-small',
40
+ dtype: 'q8',
41
+ pooling: 'mean',
42
+ outputName: '',
43
+ queryPrefix: 'query: ',
44
+ documentPrefix: 'passage: ',
45
+ batchSize: 16,
46
+ }),
47
+ embeddinggemma: Object.freeze({
48
+ label: 'EmbeddingGemma 300M q4',
49
+ modelId: 'onnx-community/embeddinggemma-300m-ONNX',
50
+ dtype: 'q4',
51
+ pooling: 'mean',
52
+ outputName: 'sentence_embedding',
53
+ queryPrefix: 'task: search result | query: ',
54
+ documentPrefix: 'title: none | text: ',
55
+ batchSize: 16,
56
+ }),
57
+ bgem3: Object.freeze({
58
+ label: 'BGE-M3 q4',
59
+ modelId: 'Xenova/bge-m3',
60
+ dtype: 'q4',
61
+ pooling: 'cls',
62
+ outputName: '',
63
+ queryPrefix: '',
64
+ documentPrefix: '',
65
+ batchSize: 16,
66
+ }),
67
+ qwen3: Object.freeze({
68
+ label: 'Qwen3-Embedding 0.6B q8',
69
+ modelId: 'onnx-community/Qwen3-Embedding-0.6B-ONNX',
70
+ dtype: 'q8',
71
+ pooling: 'last_token',
72
+ outputName: '',
73
+ queryPrefix: `Instruct: ${RETRIEVAL_INSTRUCTION}\nQuery:`,
74
+ documentPrefix: '',
75
+ batchSize: 16,
76
+ }),
77
+ })
78
+
79
+ export function documentText(row) {
80
+ return [row?.element, row?.summary].filter(Boolean).join(' — ').trim()
81
+ }
82
+
83
+ export function classifyQueryLanguage(query) {
84
+ const text = String(query || '')
85
+ const hasHangul = /[\uac00-\ud7a3]/u.test(text)
86
+ const hasLatin = /[A-Za-z]/u.test(text)
87
+ if (hasHangul && hasLatin) return 'mixed'
88
+ if (hasHangul) return 'ko'
89
+ if (hasLatin) return 'en'
90
+ return 'other'
91
+ }
92
+
93
+ export function buildEvaluation(caseFile, kase) {
94
+ const query = kase?.args?.query
95
+ const targets = Array.isArray(kase?.expect?.topNContains)
96
+ ? kase.expect.topNContains.map((value) => String(value || '')).filter(Boolean)
97
+ : []
98
+ if (typeof query !== 'string' || !query.trim() || targets.length === 0) return null
99
+ const temporal = parsePeriod(String(kase?.args?.period || ''), true)
100
+ return {
101
+ id: String(kase.id),
102
+ caseFile,
103
+ label: String(kase.label || kase.id),
104
+ query: query.trim(),
105
+ language: classifyQueryLanguage(query),
106
+ targets,
107
+ contractTopN: Number.isInteger(kase?.expect?.topN) ? kase.expect.topN : 5,
108
+ filter: {
109
+ projectScope: String(kase?.args?.projectScope || 'mixdog'),
110
+ categories: (Array.isArray(kase?.args?.category) ? kase.args.category : [kase?.args?.category])
111
+ .map((value) => String(value || '').trim().toLowerCase())
112
+ .filter(Boolean),
113
+ startMs: Number.isFinite(Number(temporal?.startMs)) ? Number(temporal.startMs) : null,
114
+ endMs: Number.isFinite(Number(temporal?.endMs)) ? Number(temporal.endMs) : null,
115
+ },
116
+ }
117
+ }
118
+
119
+ export function documentMatchesFilter(document, filter = {}) {
120
+ const scope = String(filter.projectScope || 'mixdog')
121
+ if (scope === 'common' && document.projectId != null) return false
122
+ if (scope !== 'all' && scope !== 'common' && document.projectId != null && document.projectId !== scope) return false
123
+ if (Array.isArray(filter.categories) && filter.categories.length > 0) {
124
+ if (!filter.categories.includes(String(document.category || '').toLowerCase())) return false
125
+ }
126
+ const ts = Number(document.ts)
127
+ if (filter.startMs != null && (!Number.isFinite(ts) || ts < filter.startMs)) return false
128
+ if (filter.endMs != null && (!Number.isFinite(ts) || ts > filter.endMs)) return false
129
+ return true
130
+ }
131
+
132
+ export function attachPositiveIds(evaluation, documents, targetMatches = new Map()) {
133
+ const eligible = documents.filter((document) => documentMatchesFilter(document, evaluation.filter))
134
+ const eligibleIds = new Set(eligible.map((document) => document.id))
135
+ const documentIds = new Set(documents.map((document) => document.id))
136
+ const positiveIdsByTarget = evaluation.targets.map((target) => {
137
+ const needle = target.toLowerCase()
138
+ const positiveIds = new Set(
139
+ eligible
140
+ .filter((document) => document.textLower.includes(needle))
141
+ .map((document) => document.id),
142
+ )
143
+ for (const match of targetMatches.get(needle) || []) {
144
+ if (!documentIds.has(match.rootId)) continue
145
+ if (!documentMatchesFilter(match, evaluation.filter)) continue
146
+ positiveIds.add(match.rootId)
147
+ }
148
+ return [...positiveIds]
149
+ })
150
+ const extraCandidateIds = [...new Set(positiveIdsByTarget.flat())]
151
+ .filter((id) => !eligibleIds.has(id))
152
+ return {
153
+ ...evaluation,
154
+ candidateCount: eligible.length + extraCandidateIds.length,
155
+ extraCandidateIds,
156
+ positiveIdsByTarget,
157
+ }
158
+ }
159
+
160
+ function deterministicIds(ids) {
161
+ return [...new Set(ids)]
162
+ .map((id) => ({
163
+ id,
164
+ digest: createHash('sha256').update(String(id)).digest('hex'),
165
+ }))
166
+ .sort((left, right) => left.digest.localeCompare(right.digest) || left.id - right.id)
167
+ .map((row) => row.id)
168
+ }
169
+
170
+ export function selectDeterministicCorpus(documents, evaluations, limit, positiveCap = 0) {
171
+ const requested = Number(limit)
172
+ const cap = Math.max(0, Math.floor(Number(positiveCap) || 0))
173
+ const targetPositiveIds = evaluations.flatMap((evaluation) => evaluation.positiveIdsByTarget)
174
+ const selectedPositiveIds = cap > 0
175
+ ? targetPositiveIds.flatMap((ids) => deterministicIds(ids).slice(0, cap))
176
+ : targetPositiveIds.flat()
177
+ if (!Number.isInteger(requested) || requested <= 0 || requested >= documents.length) {
178
+ return {
179
+ documents,
180
+ sourceDocuments: documents.length,
181
+ selectedDocuments: documents.length,
182
+ positiveDocuments: new Set(selectedPositiveIds).size,
183
+ method: 'full-corpus',
184
+ }
185
+ }
186
+ const positiveIds = new Set(selectedPositiveIds)
187
+ const remaining = documents
188
+ .filter((document) => !positiveIds.has(document.id))
189
+ .map((document) => ({
190
+ id: document.id,
191
+ digest: createHash('sha256').update(String(document.id)).digest('hex'),
192
+ }))
193
+ .sort((left, right) => left.digest.localeCompare(right.digest) || left.id - right.id)
194
+ const selectedIds = new Set(positiveIds)
195
+ const targetSize = Math.max(requested, selectedIds.size)
196
+ for (const row of remaining) {
197
+ if (selectedIds.size >= targetSize) break
198
+ selectedIds.add(row.id)
199
+ }
200
+ return {
201
+ documents: documents.filter((document) => selectedIds.has(document.id)),
202
+ sourceDocuments: documents.length,
203
+ selectedDocuments: selectedIds.size,
204
+ positiveDocuments: positiveIds.size,
205
+ method: `${cap > 0 ? `up-to-${cap}-positive-roots-per-target` : 'all-positive-roots'} + sha256(document-id) negatives`,
206
+ }
207
+ }
208
+
209
+ export function tokenizeBm25(text) {
210
+ return String(text || '').toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || []
211
+ }
212
+
213
+ export function prepareBm25Documents(documents) {
214
+ return documents.map((document) => {
215
+ const tokens = tokenizeBm25(document.text)
216
+ const termFrequency = new Map()
217
+ for (const token of tokens) termFrequency.set(token, (termFrequency.get(token) || 0) + 1)
218
+ return { length: tokens.length, termFrequency }
219
+ })
220
+ }
221
+
222
+ export function rankBm25(query, candidateIndices, preparedDocuments, k1 = 1.2, b = 0.75) {
223
+ const terms = [...new Set(tokenizeBm25(query))]
224
+ if (terms.length === 0 || candidateIndices.length === 0) return []
225
+ const averageLength = candidateIndices.reduce(
226
+ (sum, index) => sum + preparedDocuments[index].length,
227
+ 0,
228
+ ) / candidateIndices.length
229
+ const documentFrequency = new Map(terms.map((term) => [term, 0]))
230
+ for (const index of candidateIndices) {
231
+ const tf = preparedDocuments[index].termFrequency
232
+ for (const term of terms) if (tf.has(term)) documentFrequency.set(term, documentFrequency.get(term) + 1)
233
+ }
234
+ const scored = []
235
+ for (const index of candidateIndices) {
236
+ const document = preparedDocuments[index]
237
+ let score = 0
238
+ for (const term of terms) {
239
+ const frequency = document.termFrequency.get(term) || 0
240
+ if (frequency === 0) continue
241
+ const df = documentFrequency.get(term) || 0
242
+ const idf = Math.log(1 + ((candidateIndices.length - df + 0.5) / (df + 0.5)))
243
+ const denominator = frequency + k1 * (1 - b + b * (document.length / Math.max(1, averageLength)))
244
+ score += idf * ((frequency * (k1 + 1)) / denominator)
245
+ }
246
+ if (score > 0) scored.push({ index, score })
247
+ }
248
+ scored.sort((left, right) => right.score - left.score || left.index - right.index)
249
+ return scored
250
+ }
251
+
252
+ export function rankDense(queryVector, documentVectors, dims, candidateIndices) {
253
+ const scored = new Array(candidateIndices.length)
254
+ for (let position = 0; position < candidateIndices.length; position += 1) {
255
+ const index = candidateIndices[position]
256
+ const offset = index * dims
257
+ let score = 0
258
+ for (let dimension = 0; dimension < dims; dimension += 1) {
259
+ score += queryVector[dimension] * documentVectors[offset + dimension]
260
+ }
261
+ scored[position] = { index, score }
262
+ }
263
+ scored.sort((left, right) => right.score - left.score || left.index - right.index)
264
+ return scored
265
+ }
266
+
267
+ export function rankEqualRrf(denseRanked, lexicalRanked, k = 60) {
268
+ const scores = new Map()
269
+ for (let index = 0; index < denseRanked.length; index += 1) {
270
+ scores.set(denseRanked[index].index, 1 / (k + index + 1))
271
+ }
272
+ for (let index = 0; index < lexicalRanked.length; index += 1) {
273
+ const documentIndex = lexicalRanked[index].index
274
+ scores.set(documentIndex, (scores.get(documentIndex) || 0) + 1 / (k + index + 1))
275
+ }
276
+ return [...scores.entries()]
277
+ .map(([index, score]) => ({ index, score }))
278
+ .sort((left, right) => right.score - left.score || left.index - right.index)
279
+ }
280
+
281
+ function discountedGain(rank) {
282
+ return 1 / Math.log2(rank + 1)
283
+ }
284
+
285
+ export function scoreRanking(evaluation, ranked, documents, cutoff = 10) {
286
+ const rankById = new Map()
287
+ for (let index = 0; index < ranked.length; index += 1) {
288
+ rankById.set(documents[ranked[index].index].id, index + 1)
289
+ }
290
+ const targetRows = evaluation.targets.map((target, index) => {
291
+ const positiveIds = evaluation.positiveIdsByTarget[index] || []
292
+ let rank = null
293
+ for (const id of positiveIds) {
294
+ const value = rankById.get(id)
295
+ if (value != null && (rank == null || value < rank)) rank = value
296
+ }
297
+ return {
298
+ target,
299
+ positiveCount: positiveIds.length,
300
+ rank,
301
+ rrAt10: rank != null && rank <= cutoff ? 1 / rank : 0,
302
+ recallAt5: rank != null && rank <= 5 ? 1 : 0,
303
+ recallAt10: rank != null && rank <= cutoff ? 1 : 0,
304
+ }
305
+ })
306
+ const relevantIds = new Set(evaluation.positiveIdsByTarget.flat())
307
+ let dcg = 0
308
+ for (let index = 0; index < Math.min(cutoff, ranked.length); index += 1) {
309
+ if (relevantIds.has(documents[ranked[index].index].id)) dcg += discountedGain(index + 1)
310
+ }
311
+ let idealDcg = 0
312
+ for (let rank = 1; rank <= Math.min(cutoff, relevantIds.size); rank += 1) idealDcg += discountedGain(rank)
313
+ const divisor = targetRows.length || 1
314
+ return {
315
+ mrrAt10: targetRows.reduce((sum, row) => sum + row.rrAt10, 0) / divisor,
316
+ recallAt5: targetRows.reduce((sum, row) => sum + row.recallAt5, 0) / divisor,
317
+ recallAt10: targetRows.reduce((sum, row) => sum + row.recallAt10, 0) / divisor,
318
+ ndcgAt10: idealDcg > 0 ? dcg / idealDcg : 0,
319
+ targets: targetRows,
320
+ top3: ranked.slice(0, 3).map((row) => ({
321
+ id: documents[row.index].id,
322
+ score: row.score,
323
+ text: documents[row.index].text.slice(0, 240),
324
+ })),
325
+ }
326
+ }
327
+
328
+ function mean(rows, key) {
329
+ if (rows.length === 0) return 0
330
+ return rows.reduce((sum, row) => sum + Number(row[key] || 0), 0) / rows.length
331
+ }
332
+
333
+ export function aggregateScoredRows(rows) {
334
+ const aggregate = (subset) => ({
335
+ cases: subset.length,
336
+ mrrAt10: mean(subset, 'mrrAt10'),
337
+ recallAt5: mean(subset, 'recallAt5'),
338
+ recallAt10: mean(subset, 'recallAt10'),
339
+ ndcgAt10: mean(subset, 'ndcgAt10'),
340
+ })
341
+ const byLanguage = {}
342
+ for (const language of ['ko', 'en', 'mixed', 'other']) {
343
+ byLanguage[language] = aggregate(rows.filter((row) => row.language === language))
344
+ }
345
+ return { overall: aggregate(rows), byLanguage }
346
+ }
347
+
348
+ export function percentile(values, ratio) {
349
+ if (!Array.isArray(values) || values.length === 0) return 0
350
+ const sorted = [...values].sort((left, right) => left - right)
351
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1))
352
+ return sorted[index]
353
+ }