mixdog 0.9.154 → 0.9.155
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.
- package/package.json +2 -2
- package/scripts/build-voice-runtime-unix.sh +24 -0
- package/scripts/voice-runtime-config.json +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +47 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +4 -1
- package/src/runtime/channels/data/voice-runtime-manifest.json +17 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +59 -0
- package/src/runtime/office/assurance-benchmark.mjs +67 -1
- package/src/runtime/office/assurance.mjs +24 -17
- package/src/runtime/office/capabilities.mjs +17 -17
- package/src/runtime/office/composition-system.mjs +451 -0
- package/src/runtime/office/design-library.mjs +102 -0
- package/src/runtime/office/design-system.mjs +357 -168
- package/src/runtime/office/index.mjs +149 -20
- package/src/runtime/office/office-com-host.ps1 +132 -46
- package/src/runtime/office/pdf-render.mjs +203 -35
- package/src/runtime/office/portable-ooxml.mjs +1090 -47
- package/src/runtime/office/portable-package.mjs +390 -0
- package/src/runtime/office/portable-sheet-styles.mjs +241 -0
- package/src/runtime/office/portable-slide-shapes.mjs +283 -0
- package/src/runtime/office/quality-live-benchmark.mjs +82 -36
- package/src/runtime/office/quality-pipeline.mjs +3 -0
- package/src/runtime/office/tool-defs.mjs +2 -2
- package/src/session-runtime/goal-reminder.mjs +79 -0
- package/src/session-runtime/goal-runtime.mjs +105 -45
- package/src/session-runtime/goal-text.mjs +58 -0
- package/src/session-runtime/runtime-core.mjs +14 -1
- package/src/session-runtime/session-lifecycle.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +5 -0
- package/src/standalone/session-service.mjs +30 -3
- package/src/tui/dist/index.mjs +12 -3
- package/src/tui/lib/voice-setup.mjs +19 -3
- package/src/tui/session/goal-continuation.mjs +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.155",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -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 && npm run test:office-approval --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",
|
|
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",
|
|
@@ -41,17 +41,41 @@ curl --fail --location --retry 3 \
|
|
|
41
41
|
--output "${WORK_ROOT}/source.tar.gz"
|
|
42
42
|
tar -xzf "${WORK_ROOT}/source.tar.gz" -C "${WORK_ROOT}"
|
|
43
43
|
|
|
44
|
+
# Portability baseline (GGML_NATIVE=OFF + explicit ISA on x86-64).
|
|
45
|
+
#
|
|
46
|
+
# ggml defaults GGML_NATIVE=ON, which tunes the binary to the BUILD machine.
|
|
47
|
+
# CI runners are wider than consumer hardware, so a native build ships
|
|
48
|
+
# instructions the target CPU may not have and aborts with SIGILL /
|
|
49
|
+
# STATUS_ILLEGAL_INSTRUCTION right after backend init — the `--help` smoke
|
|
50
|
+
# below cannot catch it, because on the build machine it passes.
|
|
51
|
+
#
|
|
52
|
+
# x86-64 pins an AVX2/FMA/F16C baseline (Haswell 2013+); AVX-512, AVX-VNNI and
|
|
53
|
+
# BMI2 stay OFF because they are not universal on current consumer CPUs.
|
|
54
|
+
# arm64 needs no ISA pins — NEON is baseline — but must still disable NATIVE.
|
|
44
55
|
CMAKE_ARGS=(
|
|
45
56
|
-S "${SOURCE_ROOT}"
|
|
46
57
|
-B "${BUILD_ROOT}"
|
|
47
58
|
-DCMAKE_BUILD_TYPE=Release
|
|
48
59
|
-DBUILD_SHARED_LIBS=OFF
|
|
60
|
+
-DGGML_NATIVE=OFF
|
|
49
61
|
-DWHISPER_BUILD_EXAMPLES=ON
|
|
50
62
|
-DWHISPER_BUILD_SERVER=ON
|
|
51
63
|
-DWHISPER_BUILD_TESTS=OFF
|
|
52
64
|
-DWHISPER_SDL2=OFF
|
|
53
65
|
)
|
|
54
66
|
|
|
67
|
+
if [[ "${TARGET_ARCH}" == "x64" ]]; then
|
|
68
|
+
CMAKE_ARGS+=(
|
|
69
|
+
-DGGML_AVX=ON
|
|
70
|
+
-DGGML_AVX2=ON
|
|
71
|
+
-DGGML_FMA=ON
|
|
72
|
+
-DGGML_F16C=ON
|
|
73
|
+
-DGGML_AVX512=OFF
|
|
74
|
+
-DGGML_AVX_VNNI=OFF
|
|
75
|
+
-DGGML_BMI2=OFF
|
|
76
|
+
)
|
|
77
|
+
fi
|
|
78
|
+
|
|
55
79
|
case "${BACKEND}" in
|
|
56
80
|
vulkan)
|
|
57
81
|
CMAKE_ARGS+=(-DGGML_VULKAN=ON)
|
|
@@ -240,8 +240,8 @@ export function requestAnthropicTools(tools, messages, opts) {
|
|
|
240
240
|
// Test-only: expose the lowering so the steering-provenance test can assert
|
|
241
241
|
// the API-key provider keeps steering-tagged user turns distinct (mirrors
|
|
242
242
|
// anthropic-oauth._buildRequestBodyForCacheSmoke coverage).
|
|
243
|
-
export function _toAnthropicMessagesForTest(messages) {
|
|
244
|
-
return toAnthropicMessages(messages);
|
|
243
|
+
export function _toAnthropicMessagesForTest(messages, availableTools) {
|
|
244
|
+
return toAnthropicMessages(messages, availableTools);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
// Applies cache_control markers to the FINAL, already-sanitized Anthropic
|
|
@@ -296,12 +296,16 @@ function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
296
296
|
// Message-tail cache budget (4-BP layout + ANTHROPIC_MSG_SLOTS) is shared
|
|
297
297
|
// with anthropic.mjs — see resolveAnthropicMessageCacheSlots.
|
|
298
298
|
const messageCacheSlots = resolveAnthropicMessageCacheSlots(systemBlocks, ttls);
|
|
299
|
+
// Tools are resolved BEFORE the messages: the lowering needs the final
|
|
300
|
+
// tool list to drop tool_reference blocks whose tool no longer ships in
|
|
301
|
+
// this request (otherwise the API rejects the whole turn).
|
|
302
|
+
const requestTools = requestAnthropicTools(tools, chatMsgs, opts);
|
|
299
303
|
// Build → sanitize (once, inside toAnthropicMessages) → mark. Markers are
|
|
300
304
|
// applied to the FINAL sanitized array by invariant, so block drops /
|
|
301
305
|
// inserts / reorders performed by the sanitizer can never move or delete a
|
|
302
306
|
// marked block. NEVER sanitize again after this (see send path).
|
|
303
307
|
const anthropicMessages = applyAnthropicCacheMarkers(
|
|
304
|
-
toAnthropicMessages(chatMsgs),
|
|
308
|
+
toAnthropicMessages(chatMsgs, requestTools),
|
|
305
309
|
messageCacheSlots,
|
|
306
310
|
);
|
|
307
311
|
|
|
@@ -317,7 +321,6 @@ function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
317
321
|
|
|
318
322
|
if (systemBlocks.length) body.system = systemBlocks;
|
|
319
323
|
|
|
320
|
-
const requestTools = requestAnthropicTools(tools, chatMsgs, opts);
|
|
321
324
|
if (requestTools.length) {
|
|
322
325
|
// No cache_control on tools — the systemBase BP already covers the
|
|
323
326
|
// tools prefix via Anthropic's prompt cache prefix semantics (order:
|
|
@@ -145,12 +145,16 @@ export class AnthropicProvider {
|
|
|
145
145
|
// Message-tail cache budget (4-BP layout + ANTHROPIC_MSG_SLOTS) is
|
|
146
146
|
// shared with anthropic-oauth — see resolveAnthropicMessageCacheSlots.
|
|
147
147
|
const messageCacheSlots = resolveAnthropicMessageCacheSlots(systemBlocks, ttls);
|
|
148
|
+
// Tools are resolved BEFORE the messages: the lowering needs the final
|
|
149
|
+
// tool list to drop tool_reference blocks whose tool no longer ships
|
|
150
|
+
// in this request (otherwise the API rejects the whole turn).
|
|
151
|
+
const requestTools = requestAnthropicTools(tools, chatMsgs, opts);
|
|
148
152
|
// Build → sanitize (once, inside toAnthropicMessages) → mark. Markers
|
|
149
153
|
// are applied to the FINAL sanitized array by invariant, so block
|
|
150
154
|
// drops / inserts / reorders performed by the sanitizer can never move
|
|
151
155
|
// or delete a marked block. NEVER sanitize again after this.
|
|
152
156
|
const anthropicMessages = applyAnthropicCacheMarkers(
|
|
153
|
-
toAnthropicMessages(chatMsgs),
|
|
157
|
+
toAnthropicMessages(chatMsgs, requestTools),
|
|
154
158
|
messageCacheSlots,
|
|
155
159
|
);
|
|
156
160
|
|
|
@@ -163,7 +167,6 @@ export class AnthropicProvider {
|
|
|
163
167
|
applyAnthropicServerFallback(params, useModel, {
|
|
164
168
|
enabled: this.config?.disableBetaHeaders !== true && opts.serverFallback !== false,
|
|
165
169
|
});
|
|
166
|
-
const requestTools = requestAnthropicTools(tools, chatMsgs, opts);
|
|
167
170
|
if (requestTools.length) {
|
|
168
171
|
// No cache_control on tools — the system BP covers tools via
|
|
169
172
|
// Anthropic prefix semantics (order: tools → system → messages).
|
|
@@ -89,7 +89,39 @@ export function resolveAnthropicMessageCacheSlots(systemBlocks, ttls) {
|
|
|
89
89
|
// root cause of the sporadic COLD-turn cache miss: pre-sanitize markers landed
|
|
90
90
|
// on blocks the sanitizer then rewrote, so the provider-visible breakpoint
|
|
91
91
|
// diverged from the cached one).
|
|
92
|
-
|
|
92
|
+
// Optional `availableTools`: the exact tool list this request will ship. A
|
|
93
|
+
// tool_reference whose tool is absent from that list makes the API reject the
|
|
94
|
+
// WHOLE request ("Tool reference '<name>' not found in available tools"), and
|
|
95
|
+
// the dangling block lives in the transcript — so once a loaded tool leaves
|
|
96
|
+
// the deferred catalog (feature toggle, provider/model rebuild, disconnected
|
|
97
|
+
// MCP server) every later turn of that session fails identically. Passing the
|
|
98
|
+
// list lets the lowering drop unresolvable references instead. Omitted =>
|
|
99
|
+
// every reference is kept verbatim.
|
|
100
|
+
function anthropicToolNameSet(availableTools) {
|
|
101
|
+
if (!Array.isArray(availableTools)) return null;
|
|
102
|
+
const names = new Set();
|
|
103
|
+
for (const tool of availableTools) {
|
|
104
|
+
const name = typeof tool?.name === 'string' ? tool.name.trim() : '';
|
|
105
|
+
if (name) names.add(name);
|
|
106
|
+
}
|
|
107
|
+
return names;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Replacement content for a tool_result whose references were all dropped.
|
|
111
|
+
// The stored text result (the deferred-load summary) is the natural fallback;
|
|
112
|
+
// an EMPTY tool_result is itself a 400, so a blank result gets a placeholder.
|
|
113
|
+
function toolResultContentWithoutReferences(message, droppedNames) {
|
|
114
|
+
const normalized = normalizeContentForAnthropic(message.content);
|
|
115
|
+
if (typeof normalized === 'string' && normalized.trim()) return normalized;
|
|
116
|
+
if (Array.isArray(normalized) && normalized.length > 0) return normalized;
|
|
117
|
+
return [{
|
|
118
|
+
type: 'text',
|
|
119
|
+
text: `[tool references dropped - no longer available: ${droppedNames.join(', ')}]`,
|
|
120
|
+
}];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function toAnthropicMessages(messages, availableTools) {
|
|
124
|
+
const availableNames = anthropicToolNameSet(availableTools);
|
|
93
125
|
const result = [];
|
|
94
126
|
for (let idx = 0; idx < messages.length; idx++) {
|
|
95
127
|
const m = messages[idx];
|
|
@@ -136,12 +168,23 @@ export function toAnthropicMessages(messages) {
|
|
|
136
168
|
&& Array.isArray(native?.toolReferences)
|
|
137
169
|
? native.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
|
|
138
170
|
: [];
|
|
171
|
+
// Keep only references this request can actually back with a
|
|
172
|
+
// definition; a reference without one is a hard 400 on every turn
|
|
173
|
+
// that replays it (see anthropicToolNameSet).
|
|
174
|
+
const usableReferences = availableNames
|
|
175
|
+
? references.filter((name) => availableNames.has(name))
|
|
176
|
+
: references;
|
|
177
|
+
const droppedReferences = usableReferences.length === references.length
|
|
178
|
+
? []
|
|
179
|
+
: references.filter((name) => !usableReferences.includes(name));
|
|
139
180
|
const block = {
|
|
140
181
|
type: 'tool_result',
|
|
141
182
|
tool_use_id: m.toolCallId || '',
|
|
142
|
-
content:
|
|
143
|
-
?
|
|
144
|
-
:
|
|
183
|
+
content: usableReferences.length
|
|
184
|
+
? usableReferences.map((tool_name) => ({ type: 'tool_reference', tool_name }))
|
|
185
|
+
: (droppedReferences.length
|
|
186
|
+
? toolResultContentWithoutReferences(m, droppedReferences)
|
|
187
|
+
: normalizeContentForAnthropic(m.content)),
|
|
145
188
|
...((m.toolKind === 'error' || m.isError === true) ? { is_error: true } : {}),
|
|
146
189
|
};
|
|
147
190
|
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
acknowledgePendingDeferredToolDelta,
|
|
19
19
|
snapshotPendingDeferredToolDelta,
|
|
20
20
|
} from '../../../../../session-runtime/deferred-tool-delta.mjs';
|
|
21
|
+
import {
|
|
22
|
+
acknowledgePendingGoalReminder,
|
|
23
|
+
snapshotPendingGoalReminder,
|
|
24
|
+
} from '../../../../../session-runtime/goal-reminder.mjs';
|
|
21
25
|
import { recordStandaloneStatusTelemetry } from './status-telemetry.mjs';
|
|
22
26
|
import { normalizeStaleCompactingStage } from './compaction-runner.mjs';
|
|
23
27
|
import { resolveSessionContextMeta, positiveContextWindow } from './context-meta.mjs';
|
|
@@ -276,6 +280,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
276
280
|
let _pwstTurnDrained = null;
|
|
277
281
|
let _turnPendingEntries = [];
|
|
278
282
|
let _turnDeferredToolDelta = null;
|
|
283
|
+
let _turnGoalReminder = null;
|
|
279
284
|
// After the first turn, the next prompt comes from the drained queue.
|
|
280
285
|
// (On the first iteration _pendingTail is empty and `prompt` is the
|
|
281
286
|
// caller's original message.)
|
|
@@ -589,11 +594,15 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
589
594
|
}
|
|
590
595
|
const _currentTimeBlock = buildCurrentTimeBlock(prompt);
|
|
591
596
|
_turnDeferredToolDelta = snapshotPendingDeferredToolDelta(session);
|
|
597
|
+
// Event-driven Goal state: present only after compaction dropped the
|
|
598
|
+
// Goal's own tool results, never on an ordinary turn.
|
|
599
|
+
_turnGoalReminder = snapshotPendingGoalReminder(session);
|
|
592
600
|
const _turnReminderBlock = [
|
|
593
601
|
_currentTimeBlock
|
|
594
602
|
? `<system-reminder>\n# Current Time\n${_currentTimeBlock}\n</system-reminder>`
|
|
595
603
|
: '',
|
|
596
604
|
_turnDeferredToolDelta?.content || '',
|
|
605
|
+
_turnGoalReminder?.content || '',
|
|
597
606
|
].filter(Boolean).join('\n\n');
|
|
598
607
|
const _baseUserTurnContent = prefixUserTurnContent(prompt, _contextBlock);
|
|
599
608
|
const _userTurnContent = prefixSessionStartContent(_baseUserTurnContent, _turnReminderBlock);
|
|
@@ -1067,6 +1076,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1067
1076
|
if (_turnDeferredToolDelta) {
|
|
1068
1077
|
acknowledgePendingDeferredToolDelta(session, _turnDeferredToolDelta.revision);
|
|
1069
1078
|
}
|
|
1079
|
+
if (_turnGoalReminder) {
|
|
1080
|
+
acknowledgePendingGoalReminder(session, _turnGoalReminder.revision);
|
|
1081
|
+
}
|
|
1070
1082
|
const terminalResultPreview = {
|
|
1071
1083
|
...result,
|
|
1072
1084
|
trimmed: messagesDropped > 0,
|
|
@@ -36,6 +36,7 @@ import { traceAgentCompact, messagePrefixHash } from '../agent-trace.mjs';
|
|
|
36
36
|
import { invalidateProviderRequestToolsScope } from '../../../../session-runtime/provider-request-tools.mjs';
|
|
37
37
|
import { bumpUsageMetricsEpoch } from './manager.mjs';
|
|
38
38
|
import { resetReadStateAfterCompaction } from './read-dedup.mjs';
|
|
39
|
+
import { markPendingGoalReminder } from '../../../../session-runtime/goal-reminder.mjs';
|
|
39
40
|
|
|
40
41
|
const RECOVERED_ERROR_MESSAGE_MAX_CHARS = 300;
|
|
41
42
|
const ANSI_ESCAPE_RE = /[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*)?\u0007|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
@@ -590,6 +591,12 @@ export async function runPreSendCompactPass(state) {
|
|
|
590
591
|
catch { /* best-effort: PostCompact hook must never break the loop */ }
|
|
591
592
|
}
|
|
592
593
|
}
|
|
594
|
+
// Compaction drops the Goal's own tool results, so the durable task
|
|
595
|
+
// snapshot vanishes mid-objective and the model keeps working
|
|
596
|
+
// without its checklist. Mark one tail-injected state reminder for
|
|
597
|
+
// the next turn; it clears when that turn is accepted.
|
|
598
|
+
try { markPendingGoalReminder(sessionRef, 'compaction'); }
|
|
599
|
+
catch { /* best-effort: a Goal reminder must never break compaction */ }
|
|
593
600
|
}
|
|
594
601
|
return {
|
|
595
602
|
iterations,
|
|
@@ -157,7 +157,10 @@ export function _clearLiveSession(id) {
|
|
|
157
157
|
// in the persisted JSON) stay resident for this long after their last use so
|
|
158
158
|
// multi-turn image recognition keeps working across an idle gap. Beyond the
|
|
159
159
|
// TTL the memory cost wins and the snapshot is reclaimed like any other.
|
|
160
|
-
export const LIVE_MEDIA_RETENTION_MS =
|
|
160
|
+
export const LIVE_MEDIA_RETENTION_MS = Math.max(
|
|
161
|
+
60_000,
|
|
162
|
+
Number(process.env.MIXDOG_LIVE_MEDIA_RETENTION_MS) || 10 * 60 * 1000,
|
|
163
|
+
); // 10m default — raw image bytes are the most expensive thing we retain
|
|
161
164
|
|
|
162
165
|
export function _messagesCarryLiveMedia(messages) {
|
|
163
166
|
if (!Array.isArray(messages)) return false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.9.
|
|
2
|
+
"version": "1.9.3",
|
|
3
3
|
"source": "ggml-org/whisper.cpp@306c88f4d1286aec1bf96e544632897886af5501",
|
|
4
4
|
"model": {
|
|
5
5
|
"id": "large-v3-turbo-q8_0",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
{
|
|
65
65
|
"id": "vulkan",
|
|
66
66
|
"requires": null,
|
|
67
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.
|
|
68
|
-
"sha256": "
|
|
69
|
-
"size":
|
|
67
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.3/whisper-server-win32-x64-vulkan.zip",
|
|
68
|
+
"sha256": "a80997f6841decf08421ab261084fa365ba879932221ba53de98bc3710c9dced",
|
|
69
|
+
"size": 16733128,
|
|
70
70
|
"format": "zip",
|
|
71
71
|
"executable": "whisper-server.exe"
|
|
72
72
|
}
|
|
@@ -77,9 +77,9 @@
|
|
|
77
77
|
{
|
|
78
78
|
"id": "vulkan",
|
|
79
79
|
"requires": null,
|
|
80
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.
|
|
81
|
-
"sha256": "
|
|
82
|
-
"size":
|
|
80
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.3/whisper-server-linux-x64-vulkan.zip",
|
|
81
|
+
"sha256": "b4614d11e13f383be33a278003d66401e6a5a94024e9124ea9ada615177d4897",
|
|
82
|
+
"size": 14333249,
|
|
83
83
|
"format": "zip",
|
|
84
84
|
"executable": "whisper-server"
|
|
85
85
|
}
|
|
@@ -90,9 +90,9 @@
|
|
|
90
90
|
{
|
|
91
91
|
"id": "vulkan",
|
|
92
92
|
"requires": null,
|
|
93
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.
|
|
94
|
-
"sha256": "
|
|
95
|
-
"size":
|
|
93
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.3/whisper-server-linux-arm64-vulkan.zip",
|
|
94
|
+
"sha256": "b267e322e66df8d46b2fccd3812d4dcba57de9781baf40ebecb9fe43c477b10b",
|
|
95
|
+
"size": 14229402,
|
|
96
96
|
"format": "zip",
|
|
97
97
|
"executable": "whisper-server"
|
|
98
98
|
}
|
|
@@ -103,8 +103,8 @@
|
|
|
103
103
|
{
|
|
104
104
|
"id": "metal",
|
|
105
105
|
"requires": null,
|
|
106
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.
|
|
107
|
-
"sha256": "
|
|
106
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.3/whisper-server-darwin-arm64-metal.zip",
|
|
107
|
+
"sha256": "6523f7acf18924e87ab80e7c712d76e8558cc09215df98da703b9102c0f5650b",
|
|
108
108
|
"size": 1350283,
|
|
109
109
|
"format": "zip",
|
|
110
110
|
"executable": "whisper-server"
|
|
@@ -116,15 +116,15 @@
|
|
|
116
116
|
{
|
|
117
117
|
"id": "cpu",
|
|
118
118
|
"requires": null,
|
|
119
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.
|
|
120
|
-
"sha256": "
|
|
121
|
-
"size":
|
|
119
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/voice-runtime-v1.9.3/whisper-server-darwin-x64-cpu.zip",
|
|
120
|
+
"sha256": "3d76426a0c7ec718b33ff54521a1d29ed09e4ed00ac9e8075f81d6bbe77a5d14",
|
|
121
|
+
"size": 1333240,
|
|
122
122
|
"format": "zip",
|
|
123
123
|
"executable": "whisper-server"
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
|
-
"release_tag": "voice-runtime-v1.9.
|
|
129
|
-
"build_hash": "
|
|
128
|
+
"release_tag": "voice-runtime-v1.9.3",
|
|
129
|
+
"build_hash": "d9a066477feb39afd5bc51cf50453d3ce1a194be5de907357ed7f9c1e9677044"
|
|
130
130
|
}
|
|
@@ -551,6 +551,33 @@ export async function ensureWhisperRuntime(dataDir, onProgress = null) {
|
|
|
551
551
|
})
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
// Version-drift guard. `active-version` holds `whisper-<version>-<variantId>`,
|
|
555
|
+
// so an installed runtime falls BEHIND whenever the bundled manifest moves on
|
|
556
|
+
// (observed: a 754 MB cuBLAS-11 bundle superseded by a 17 MB Vulkan build that
|
|
557
|
+
// keeps GPU acceleration). The legacy executable fallback in
|
|
558
|
+
// resolveManagedWhisperCmd still resolves the old layout, which is exactly
|
|
559
|
+
// what made the drift permanent — every caller saw `installed: true`, so the
|
|
560
|
+
// upgrade path never ran and the superseded bytes stayed on disk forever.
|
|
561
|
+
//
|
|
562
|
+
// Reported separately from `installed` on purpose: the old runtime still WORKS,
|
|
563
|
+
// so the transcribe hot path must keep using it (offline, mid-install), while
|
|
564
|
+
// the install path treats this as "re-fetch".
|
|
565
|
+
export function isManagedWhisperStale(dataDir) {
|
|
566
|
+
const activeFile = join(dataDir, 'voice-runtime', 'active-version')
|
|
567
|
+
if (!existsSync(activeFile)) return false
|
|
568
|
+
const activeName = readFileSync(activeFile, 'utf8').trim()
|
|
569
|
+
if (!activeName) return false
|
|
570
|
+
if (!existsSync(BUNDLED_MANIFEST_PATH)) return false
|
|
571
|
+
try {
|
|
572
|
+
const manifest = JSON.parse(readFileSync(BUNDLED_MANIFEST_PATH, 'utf8'))
|
|
573
|
+
if (!manifest?.version) return false
|
|
574
|
+
return !activeName.startsWith(`whisper-${manifest.version}-`)
|
|
575
|
+
} catch {
|
|
576
|
+
// An unreadable manifest is not evidence of staleness.
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
554
581
|
// Read-only resolver: returns the cached binary path when the managed runtime
|
|
555
582
|
// is fully installed, null otherwise. Used by the transcribe hot path and the
|
|
556
583
|
// /cli-check endpoint to test installation state without triggering a fetch.
|
|
@@ -747,6 +774,9 @@ export function resolveVoiceRuntime(dataDir, { modelId = 'standard' } = {}) {
|
|
|
747
774
|
kind: 'managed-whisper.cpp',
|
|
748
775
|
label: 'whisper.cpp',
|
|
749
776
|
installed: !!(managedWhisperCmd && serverCmd && managedModelPath && managedFfmpegPath),
|
|
777
|
+
// Installed, but from an older manifest version — usable now, upgradeable
|
|
778
|
+
// by the install path. See isManagedWhisperStale.
|
|
779
|
+
stale: isManagedWhisperStale(dataDir),
|
|
750
780
|
binary: !!managedWhisperCmd,
|
|
751
781
|
model: !!managedModelPath,
|
|
752
782
|
ffmpeg: !!managedFfmpegPath,
|
|
@@ -759,6 +789,35 @@ export function resolveVoiceRuntime(dataDir, { modelId = 'standard' } = {}) {
|
|
|
759
789
|
}
|
|
760
790
|
}
|
|
761
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Uninstall counterpart to ensureWhisperRuntime + ensureWhisperModel.
|
|
794
|
+
*
|
|
795
|
+
* Voice used to be install-only: turning it OFF flipped a config flag and left
|
|
796
|
+
* the whisper runtime and model weights (1.5 GB+ on a machine that picked a
|
|
797
|
+
* GPU variant) resident forever, so "off" was never actually cold. Removes
|
|
798
|
+
* both; re-enabling re-fetches them through the normal ensure* path.
|
|
799
|
+
*
|
|
800
|
+
* The managed ffmpeg runtime is deliberately KEPT — it is shared with non-voice
|
|
801
|
+
* media paths and is two orders of magnitude smaller.
|
|
802
|
+
*
|
|
803
|
+
* @returns {string[]} removed directory labels (empty when nothing was present)
|
|
804
|
+
*/
|
|
805
|
+
export function removeManagedVoiceRuntime(dataDir) {
|
|
806
|
+
const removed = []
|
|
807
|
+
for (const parts of [['voice-runtime'], ['voice', 'models']]) {
|
|
808
|
+
const target = join(dataDir, ...parts)
|
|
809
|
+
if (!existsSync(target)) continue
|
|
810
|
+
try {
|
|
811
|
+
rmSync(target, { recursive: true, force: true })
|
|
812
|
+
removed.push(parts.join('/'))
|
|
813
|
+
} catch (err) {
|
|
814
|
+
// A locked file (transcribe in flight) must not fail the toggle itself.
|
|
815
|
+
process.stderr.write(`[voice-runtime] uninstall skipped ${parts.join('/')}: ${err?.message || err}\n`)
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return removed
|
|
819
|
+
}
|
|
820
|
+
|
|
762
821
|
// Single managed location for the whisper model weight file. Idempotent: if
|
|
763
822
|
// the resolved file exists and matches the manifest sha256, return without
|
|
764
823
|
// re-downloading. Atomic install via stage-then-rename so a partial download
|
|
@@ -326,6 +326,71 @@ async function postSaveReleaseGate() {
|
|
|
326
326
|
};
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
async function contentAwareCompositionGate() {
|
|
330
|
+
const content = {
|
|
331
|
+
packageId: 'composition-gate',
|
|
332
|
+
audience: 'executive committee',
|
|
333
|
+
objective: 'approve the operating plan',
|
|
334
|
+
decision: 'approve the plan',
|
|
335
|
+
facts: [{ id: 'revenue', label: 'Revenue', value: 120 }],
|
|
336
|
+
claims: [{ id: 'decision', text: 'Approve the plan', factIds: ['revenue'] }],
|
|
337
|
+
};
|
|
338
|
+
const operation = {
|
|
339
|
+
op: 'compose_document',
|
|
340
|
+
title: 'Decision brief',
|
|
341
|
+
summary: 'Approve the plan.',
|
|
342
|
+
sections: [{
|
|
343
|
+
heading: 'Evidence',
|
|
344
|
+
paragraphs: ['Revenue supports the decision.'],
|
|
345
|
+
table: [['Metric', 'Value'], ['Revenue', 120]],
|
|
346
|
+
}],
|
|
347
|
+
};
|
|
348
|
+
const first = expandOfficeDesignOperations({
|
|
349
|
+
format: 'docx',
|
|
350
|
+
backend: 'microsoft-office-com',
|
|
351
|
+
created: true,
|
|
352
|
+
design: { purpose: 'decide', expressionMode: 'strong-fit', content },
|
|
353
|
+
operations: [operation],
|
|
354
|
+
});
|
|
355
|
+
const firstId = first.semantic[0].composition.id;
|
|
356
|
+
const second = expandOfficeDesignOperations({
|
|
357
|
+
format: 'docx',
|
|
358
|
+
backend: 'microsoft-office-com',
|
|
359
|
+
created: true,
|
|
360
|
+
design: { purpose: 'decide', expressionMode: 'strong-fit', content },
|
|
361
|
+
library: {
|
|
362
|
+
source: 'mixdog-starter',
|
|
363
|
+
recentCompositions: [{
|
|
364
|
+
fingerprint: first.composition.fingerprint,
|
|
365
|
+
format: 'docx',
|
|
366
|
+
compositionIds: [firstId],
|
|
367
|
+
}],
|
|
368
|
+
},
|
|
369
|
+
operations: [operation],
|
|
370
|
+
});
|
|
371
|
+
const gate = evaluateOfficeSubmissionGate({
|
|
372
|
+
persisted: true,
|
|
373
|
+
issues: [{
|
|
374
|
+
severity: 'warning',
|
|
375
|
+
code: 'recent_composition_repeat',
|
|
376
|
+
path: '/',
|
|
377
|
+
message: 'same sequence',
|
|
378
|
+
}],
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
category: 'content-aware-composition-gate',
|
|
382
|
+
passed: firstId !== second.semantic[0].composition.id
|
|
383
|
+
&& second.semantic[0].composition.historyPenalty === 0
|
|
384
|
+
&& !gate.ok
|
|
385
|
+
&& gate.blocking[0].severity === 'error',
|
|
386
|
+
evidence: {
|
|
387
|
+
first: firstId,
|
|
388
|
+
second: second.semantic[0].composition.id,
|
|
389
|
+
gate,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
329
394
|
export async function runOfficeAssuranceBenchmark() {
|
|
330
395
|
const cases = await Promise.all([
|
|
331
396
|
spreadsheetBenchMini(),
|
|
@@ -337,10 +402,11 @@ export async function runOfficeAssuranceBenchmark() {
|
|
|
337
402
|
contentModelCrossApp(),
|
|
338
403
|
semanticDeliverableQuality(),
|
|
339
404
|
postSaveReleaseGate(),
|
|
405
|
+
contentAwareCompositionGate(),
|
|
340
406
|
]);
|
|
341
407
|
const passed = cases.filter((entry) => entry.passed).length;
|
|
342
408
|
return {
|
|
343
|
-
version:
|
|
409
|
+
version: 3,
|
|
344
410
|
createdAt: new Date().toISOString(),
|
|
345
411
|
measurementKind: 'deterministic-office-assurance',
|
|
346
412
|
categories: cases.length,
|
|
@@ -354,21 +354,22 @@ function headingLevel(paragraph) {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
function wordBlockOrder(document) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
357
|
+
const blocks = Array.isArray(document?.blockOrder) && document.blockOrder.length
|
|
358
|
+
? document.blockOrder.map((entry) => ({ ...entry, start: Number(entry.start) }))
|
|
359
|
+
: [
|
|
360
|
+
...(document?.paragraphs || []).map((entry) => ({
|
|
361
|
+
type: 'paragraph',
|
|
362
|
+
index: entry.index,
|
|
363
|
+
path: entry.path,
|
|
364
|
+
start: Number(entry.start),
|
|
365
|
+
})),
|
|
366
|
+
...(document?.tables || []).map((entry) => ({
|
|
367
|
+
type: 'table',
|
|
368
|
+
index: entry.index,
|
|
369
|
+
path: entry.path,
|
|
370
|
+
start: Number(entry.start),
|
|
371
|
+
})),
|
|
372
|
+
];
|
|
372
373
|
if (blocks.every((entry) => Number.isFinite(entry.start))) {
|
|
373
374
|
blocks.sort((left, right) => left.start - right.start);
|
|
374
375
|
}
|
|
@@ -379,7 +380,9 @@ function reviewDocxStructure(document) {
|
|
|
379
380
|
const issues = [];
|
|
380
381
|
const paragraphs = Array.isArray(document?.paragraphs) ? document.paragraphs : [];
|
|
381
382
|
const tables = Array.isArray(document?.tables) ? document.tables : [];
|
|
382
|
-
const content = paragraphs.filter((paragraph) =>
|
|
383
|
+
const content = paragraphs.filter((paragraph) => (
|
|
384
|
+
paragraph?.inTable !== true && String(paragraph.text || '').trim()
|
|
385
|
+
));
|
|
383
386
|
const headings = content
|
|
384
387
|
.map((paragraph) => ({ paragraph, level: headingLevel(paragraph) }))
|
|
385
388
|
.filter((entry) => entry.level !== null);
|
|
@@ -408,7 +411,11 @@ function reviewDocxStructure(document) {
|
|
|
408
411
|
const block = order[index];
|
|
409
412
|
if (block.type !== 'paragraph') continue;
|
|
410
413
|
const paragraph = paragraphsByIndex.get(Number(block.index));
|
|
411
|
-
if (
|
|
414
|
+
if (
|
|
415
|
+
paragraph?.inTable === true
|
|
416
|
+
|| headingLevel(paragraph) === null
|
|
417
|
+
|| !String(paragraph?.text || '').trim()
|
|
418
|
+
) continue;
|
|
412
419
|
const nextBlock = order.slice(index + 1).find((entry) => {
|
|
413
420
|
if (entry.type === 'table') return true;
|
|
414
421
|
return String(paragraphsByIndex.get(Number(entry.index))?.text || '').trim();
|