mixdog 1.3.2 → 1.3.3

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 +7 -7
  2. package/scripts/verify-runtime-dependency-versions.mjs +158 -0
  3. package/src/agents/reviewer/AGENT.md +5 -7
  4. package/src/agents/reviewer/agent.json +1 -1
  5. package/src/defaults/shipped-history.json +4 -0
  6. package/src/defaults/skills/browser-use/SKILL.md +6 -3
  7. package/src/defaults/skills/local-provider/SKILL.md +33 -6
  8. package/src/rules/lead/LEAD.md +5 -8
  9. package/src/rules/shared/45-destructive.md +5 -1
  10. package/src/rules/shared/60-verification.md +6 -4
  11. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
  12. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
  13. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
  16. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  17. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  18. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
  19. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
  21. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
  22. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
  24. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
  25. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
  26. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
  27. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
  28. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
  29. package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
  30. package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
  31. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
  32. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
  33. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
  34. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
  37. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
  38. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
  39. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
  41. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
  42. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
  43. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
  45. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
  46. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
  47. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
  50. package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
  51. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
  52. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
  53. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
  54. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
  56. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
  57. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
  58. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
  59. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
  60. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
  61. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
  63. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
  64. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
  65. package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
  66. package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
  68. package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
  69. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
  70. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
  71. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
  72. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
  73. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
  74. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
  75. package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
  76. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
  77. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
  79. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
  81. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
  82. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
  84. package/src/runtime/attachments/legacy-office.mjs +110 -0
  85. package/src/runtime/attachments/prompt-restore.mjs +70 -0
  86. package/src/runtime/attachments/store.mjs +60 -3
  87. package/src/runtime/effort-judge/judge-client.mjs +61 -14
  88. package/src/runtime/effort-judge/model-install.mjs +34 -2
  89. package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
  90. package/src/runtime/office/portable/portable-soffice.mjs +132 -30
  91. package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
  92. package/src/runtime/shared/agent-avatar.mjs +3 -2
  93. package/src/runtime/shared/agent-room-host.d.mts +31 -0
  94. package/src/runtime/shared/agent-room-host.mjs +58 -0
  95. package/src/runtime/shared/bot-hops.mjs +16 -11
  96. package/src/runtime/shared/redact-directories.mjs +18 -0
  97. package/src/runtime/shared/remove-directory.mjs +28 -0
  98. package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
  99. package/src/runtime/shared/session-sidebar-host.mjs +16 -3
  100. package/src/runtime/shared/tool-execution-contract.mjs +4 -0
  101. package/src/runtime/shared/tool-work-units.mjs +12 -9
  102. package/src/runtime/shared/turn-snapshot.mjs +233 -122
  103. package/src/runtime/shared/user-data-guard.mjs +51 -19
  104. package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
  105. package/src/runtime/shared/worktrees/contract.mjs +11 -3
  106. package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
  107. package/src/runtime/shared/worktrees/manager.mjs +419 -52
  108. package/src/runtime/shared/worktrees/registry.mjs +3 -1
  109. package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
  110. package/src/runtime/tidy/process.mjs +41 -8
  111. package/src/runtime/tidy/resolve.mjs +10 -7
  112. package/src/session-runtime/agent-home-delivery.mjs +73 -91
  113. package/src/session-runtime/agent-profile-changes.mjs +16 -0
  114. package/src/session-runtime/agent-room-delivery.mjs +2 -63
  115. package/src/session-runtime/agent-task-sessions.mjs +194 -64
  116. package/src/session-runtime/boot/apis.mjs +3 -0
  117. package/src/session-runtime/boot/facade.mjs +5 -6
  118. package/src/session-runtime/boot/tools.mjs +21 -1
  119. package/src/session-runtime/held-requests.mjs +20 -25
  120. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
  121. package/src/session-runtime/model-recency.mjs +1 -0
  122. package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
  123. package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
  124. package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
  125. package/src/session-runtime/runtime-review-api.mjs +4 -1
  126. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +34 -19
  127. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
  128. package/src/session-runtime/services/provider-admin.mjs +12 -2
  129. package/src/session-runtime/services/worktree-access.mjs +27 -0
  130. package/src/session-runtime/session-hooks.mjs +9 -17
  131. package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
  132. package/src/session-runtime/setup-tool/executor.mjs +4 -0
  133. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
  134. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
  135. package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
  136. package/src/session-runtime/tool-policy-refresh.mjs +1 -0
  137. package/src/session-runtime/tool-surface.mjs +9 -5
  138. package/src/session-runtime/turn/session-ops.mjs +8 -0
  139. package/src/session-runtime/turn/turn-run.mjs +5 -1
  140. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
  141. package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
  142. package/src/session-runtime/workspace/transition-steps.mjs +1 -18
  143. package/src/session-runtime/workspace/transitions.mjs +279 -67
  144. package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
  145. package/src/standalone/daemon-lag-profiler.mjs +3 -1
  146. package/src/standalone/daemon-session-control.mjs +140 -23
  147. package/src/standalone/daemon-telemetry.mjs +9 -0
  148. package/src/standalone/daemon.mjs +5 -0
  149. package/src/standalone/local-session-runtime.mjs +11 -2
  150. package/src/standalone/session-protocol.mjs +1 -0
  151. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
  152. package/src/standalone/session-service/agent-route-follow.mjs +69 -0
  153. package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
  154. package/src/standalone/session-service.mjs +10 -0
  155. package/src/standalone/session-tool-output.mjs +1 -0
  156. package/src/standalone/staged-import.mjs +47 -0
  157. package/src/tui/App.jsx +2 -0
  158. package/src/tui/app/message-selector.mjs +15 -2
  159. package/src/tui/app/model-options.mjs +16 -2
  160. package/src/tui/app/model-picker.mjs +2 -1
  161. package/src/tui/app/route-pickers.mjs +3 -1
  162. package/src/tui/dist/index.mjs +92 -40
  163. package/src/tui/paste-attachments.mjs +0 -1
  164. package/src/tui/paste-text-policy.mjs +4 -5
  165. package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
  166. package/src/tui/session/notice-surface.mjs +3 -0
  167. package/src/tui/session/session-api/intake/abort.mjs +33 -17
  168. package/src/tui/session/session-api/intake/rewind.mjs +39 -4
  169. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  170. package/src/tui/session/tool-approval.mjs +11 -48
  171. package/src/session-runtime/room-member-gate.mjs +0 -61
@@ -0,0 +1,110 @@
1
+ // Legacy Office (.doc / .xls / .ppt, OLE compound) attachments. Text
2
+ // extraction reads OOXML packages only, so a legacy file is saved as its
3
+ // package by LibreOffice (convertLegacyOffice, the conversion `office open`
4
+ // uses) in a private scratch folder, never beside the user's file.
5
+ import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { basename, extname, join } from 'node:path';
8
+ import { redactDirectories } from '../shared/redact-directories.mjs';
9
+ import { removeDirectoryWithRetry } from '../shared/remove-directory.mjs';
10
+
11
+ // The first eight bytes of every legacy Office (OLE compound) file.
12
+ const OLE_COMPOUND_MAGIC = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]);
13
+
14
+ const PACKAGE_BY_LEGACY_EXTENSION = { doc: 'docx', xls: 'xlsx', ppt: 'pptx' };
15
+ const PACKAGE_BY_LEGACY_MIME = {
16
+ 'application/msword': 'docx',
17
+ 'application/vnd.ms-excel': 'xlsx',
18
+ 'application/vnd.ms-powerpoint': 'pptx',
19
+ };
20
+ const PACKAGE_BY_OOXML_EXTENSION = { docx: 'docx', xlsx: 'xlsx', xlsm: 'xlsx', pptx: 'pptx' };
21
+
22
+ export const OOXML_MIME_BY_PACKAGE = {
23
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
24
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
25
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
26
+ };
27
+ const PACKAGE_BY_OOXML_MIME = {
28
+ ...Object.fromEntries(Object.entries(OOXML_MIME_BY_PACKAGE).map(([kind, mime]) => [mime, kind])),
29
+ 'application/vnd.ms-excel.sheet.macroenabled.12': 'xlsx',
30
+ };
31
+
32
+ export function isLegacyOfficeBuffer(buffer) {
33
+ return buffer.length >= 8 && buffer.subarray(0, 8).equals(OLE_COMPOUND_MAGIC);
34
+ }
35
+
36
+ /** The package extension (`docx`/`xlsx`/`pptx`) an attachment labelled legacy Office
37
+ * (by MIME type or file extension) converts to, else ''. */
38
+ export function legacyOfficeLabelPackage(mimeType, filename) {
39
+ const mime = String(mimeType || '')
40
+ .split(';', 1)[0]
41
+ .trim()
42
+ .toLowerCase();
43
+ const extension = extname(String(filename || ''))
44
+ .slice(1)
45
+ .toLowerCase();
46
+ return PACKAGE_BY_LEGACY_MIME[mime] || PACKAGE_BY_LEGACY_EXTENSION[extension] || '';
47
+ }
48
+
49
+ /** The package an OLE file labelled as an OOXML Office file (renamed, or sent
50
+ * with the OOXML MIME type and no extension) converts to, else ''. */
51
+ export function ooxmlLabelPackage(mimeType, filename) {
52
+ const mime = String(mimeType || '')
53
+ .split(';', 1)[0]
54
+ .trim()
55
+ .toLowerCase();
56
+ const extension = extname(String(filename || ''))
57
+ .slice(1)
58
+ .toLowerCase();
59
+ return PACKAGE_BY_OOXML_MIME[mime] || PACKAGE_BY_OOXML_EXTENSION[extension] || '';
60
+ }
61
+
62
+ async function defaultConvert(path, output, options) {
63
+ const { convertLegacyOffice } = await import('../office/portable/portable-soffice.mjs');
64
+ return convertLegacyOffice(path, output, options);
65
+ }
66
+
67
+ /**
68
+ * Saves the legacy Office `buffer` as the OOXML package `kind` and returns the
69
+ * package bytes and its file name. The scratch folder is always removed, once
70
+ * the converter has returned (it returns only after its process has closed;
71
+ * a removal that still meets a held file is retried, and a final failure is
72
+ * logged). A failure (LibreOffice missing, damaged or protected file, bytes
73
+ * that are not a legacy Office file) throws a message with the scratch path
74
+ * taken out; a cancellation rethrows the abort itself.
75
+ */
76
+ export async function convertLegacyOfficeBuffer(
77
+ buffer,
78
+ filename,
79
+ kind,
80
+ { signal = null, convert = defaultConvert, remove = undefined, log = undefined, retryMs = undefined } = {}
81
+ ) {
82
+ signal?.throwIfAborted();
83
+ if (!isLegacyOfficeBuffer(buffer)) {
84
+ throw new Error(
85
+ `${filename || 'This attachment'} is labelled as a legacy Office file but its content is not one, so it cannot be converted. Open it in Microsoft Office and save it as .${kind}, then attach that copy.`
86
+ );
87
+ }
88
+ const stem = basename(String(filename || 'attachment'), extname(String(filename || ''))) || 'attachment';
89
+ const safeStem = stem.replace(/[^\w.-]+/g, '_').slice(0, 80) || 'attachment';
90
+ const legacyExtension = Object.keys(PACKAGE_BY_LEGACY_EXTENSION).find(
91
+ (ext) => PACKAGE_BY_LEGACY_EXTENSION[ext] === kind
92
+ );
93
+ const dir = await mkdtemp(join(tmpdir(), 'mixdog-attachment-'));
94
+ let converted;
95
+ try {
96
+ const source = join(dir, `${safeStem}.${legacyExtension}`);
97
+ const output = join(dir, `${safeStem}.${kind}`);
98
+ await writeFile(source, buffer);
99
+ await convert(source, output, { signal });
100
+ converted = { buffer: await readFile(output), filename: `${stem}.${kind}` };
101
+ } catch (error) {
102
+ signal?.throwIfAborted();
103
+ throw new Error(redactDirectories(error instanceof Error ? error.message : String(error), [dir]));
104
+ } finally {
105
+ await removeDirectoryWithRetry(dir, { remove, log, retryMs });
106
+ }
107
+ // A cancel that arrived while the folder was being removed still cancels.
108
+ signal?.throwIfAborted();
109
+ return converted;
110
+ }
@@ -0,0 +1,70 @@
1
+ // Rebuilds the composer's restore payload (`pastedTexts`, `pastedImages`) from a
2
+ // stored, materialized user prompt. The model history holds the EXPANDED prompt:
3
+ // each `[Pasted text #n …]` / `[File #n …]` token became a `textKind` reference
4
+ // part, in token order, and each image an image reference part. The transcript
5
+ // row's own text still spells the tokens, so the n-th reference part belongs to
6
+ // the n-th token; the entries carry references only and are hydrated by the
7
+ // caller (`hydratePastedAttachments`) exactly as the abort/queue restores are.
8
+ import { isAttachmentReference } from './store.mjs';
9
+
10
+ const TEXT_TOKEN_RE = /\[(Pasted text|File) #(\d+)(?:: ([^\]\r\n]+))?[^\]\r\n]*\]/g;
11
+ const IMAGE_TOKEN_RE = /\[Image #(\d+)[^\]\r\n]*\]/g;
12
+
13
+ export function restorableAttachmentsFromPrompt(content, rowText) {
14
+ const parts = Array.isArray(content) ? content : [];
15
+ const text = String(rowText ?? '');
16
+ const textRefs = parts.filter(
17
+ (part) =>
18
+ part?.type === 'text' && isAttachmentReference(part) && (part.textKind === 'paste' || part.textKind === 'file')
19
+ );
20
+ const imageRefs = parts.filter((part) => part?.type === 'image' && isAttachmentReference(part));
21
+
22
+ // A `[File #n: name]` token is also spelled for binary files (PDF/Office),
23
+ // which never became a text reference: a token takes the next text reference
24
+ // only when it is the same kind (and, for files, the same name).
25
+ const pastedTexts = {};
26
+ let next = 0;
27
+ for (const match of text.matchAll(TEXT_TOKEN_RE)) {
28
+ const part = textRefs[next];
29
+ if (!part) break;
30
+ const isFile = match[1] === 'File';
31
+ if (isFile ? part.textKind !== 'file' || match[3] !== part.filename : part.textKind !== 'paste') continue;
32
+ next += 1;
33
+ const id = Number(match[2]);
34
+ if (!id || pastedTexts[id]) continue;
35
+ pastedTexts[id] = {
36
+ id,
37
+ source: part.textKind === 'file' ? 'file' : 'paste',
38
+ attachmentRef: part.attachmentRef,
39
+ sizeBytes: part.sizeBytes,
40
+ ...(part.filename ? { filename: part.filename } : {}),
41
+ ...(part.mimeType ? { mimeType: part.mimeType } : {}),
42
+ };
43
+ }
44
+
45
+ const imageIds = [...text.matchAll(IMAGE_TOKEN_RE)].map((match) => Number(match[1]));
46
+ const usedIds = new Set([...Object.keys(pastedTexts).map(Number), ...imageIds]);
47
+ let nextId = 1;
48
+ const pastedImages = {};
49
+ imageRefs.forEach((part, index) => {
50
+ let id = Number(part.pasteId) || imageIds[index] || 0;
51
+ if (!id || pastedImages[id]) {
52
+ while (usedIds.has(nextId)) nextId += 1;
53
+ id = nextId;
54
+ usedIds.add(id);
55
+ }
56
+ pastedImages[id] = {
57
+ id,
58
+ type: 'image',
59
+ mediaType: String(part.mimeType || part.mediaType || 'image/png'),
60
+ attachmentRef: part.attachmentRef,
61
+ sizeBytes: part.sizeBytes,
62
+ ...(part.filename ? { filename: part.filename } : {}),
63
+ };
64
+ });
65
+
66
+ return {
67
+ pastedTexts: Object.keys(pastedTexts).length ? pastedTexts : null,
68
+ pastedImages: Object.keys(pastedImages).length ? pastedImages : null,
69
+ };
70
+ }
@@ -25,6 +25,13 @@ import { dirname, join } from 'node:path';
25
25
 
26
26
  import { writeFileAtomicSync, writeJsonAtomicAsync } from '../shared/atomic-file.mjs';
27
27
  import { resolvePluginData } from '../shared/plugin-paths.mjs';
28
+ import {
29
+ convertLegacyOfficeBuffer,
30
+ isLegacyOfficeBuffer,
31
+ legacyOfficeLabelPackage,
32
+ OOXML_MIME_BY_PACKAGE,
33
+ ooxmlLabelPackage,
34
+ } from './legacy-office.mjs';
28
35
  import { MAX_ATTACHMENT_BLOB_BYTES, MAX_PDF_BYTES, MAX_PDF_PAGES, isPdfBuffer } from './limits.mjs';
29
36
  import { inspectPdfBuffer, PDF_INTAKE_TIMEOUT_MS } from './pdf-extract.mjs';
30
37
 
@@ -389,7 +396,7 @@ function materializePastedTexts(pastedTexts) {
389
396
  type: 'text',
390
397
  attachmentRef: raw.attachmentRef,
391
398
  sizeBytes: attachmentBlobSize(raw, raw.sizeBytes),
392
- ...(raw.source === 'file' ? { textKind: 'file' } : {}),
399
+ textKind: raw.source === 'file' ? 'file' : 'paste',
393
400
  ...(raw.filename ? { filename: raw.filename } : {}),
394
401
  ...(raw.mimeType ? { mimeType: raw.mimeType } : {}),
395
402
  };
@@ -630,6 +637,50 @@ export function providerTakesNativePdf(provider) {
630
637
  return provider?.nativePdf !== false;
631
638
  }
632
639
 
640
+ /**
641
+ * A legacy Office attachment (.doc/.xls/.ppt: by label, or an OLE file renamed
642
+ * to an OOXML name) as the OOXML package LibreOffice saves it as, stored under
643
+ * its own reference, so text extraction and every later lowering see an
644
+ * ordinary package. A conversion that cannot run (LibreOffice missing, damaged
645
+ * file, or bytes that are not a legacy file under a legacy label) becomes a text
646
+ * part carrying the converter's message, so the model and the user are told
647
+ * instead of the file vanishing. A cancellation (`signal`) is rethrown, never
648
+ * turned into text. Any other part is returned as is.
649
+ */
650
+ async function packageLegacyOfficePart(part, { convertLegacy, signal }) {
651
+ if (part?.type !== 'file' || !isAttachmentReference(part)) return part;
652
+ const mimeType = part.mimeType || part.mediaType;
653
+ const legacyKind = legacyOfficeLabelPackage(mimeType, part.filename);
654
+ const kind = legacyKind || ooxmlLabelPackage(mimeType, part.filename);
655
+ if (!kind) return part;
656
+ const buffer = readAttachmentBuffer(part);
657
+ // An OOXML-labelled part converts only when its bytes are legacy; a legacy
658
+ // label is always converted (or explained).
659
+ if (!legacyKind && !isLegacyOfficeBuffer(buffer)) return part;
660
+ const name = String(part.filename || '')
661
+ .split(/[\\/]/)
662
+ .pop();
663
+ try {
664
+ const converted = await convertLegacyOfficeBuffer(buffer, part.filename, kind, {
665
+ signal,
666
+ ...(convertLegacy ? { convert: convertLegacy } : {}),
667
+ });
668
+ const { mediaType: _mediaType, ...rest } = part;
669
+ return {
670
+ ...rest,
671
+ ...saveBuffer(converted.buffer),
672
+ mimeType: OOXML_MIME_BY_PACKAGE[kind],
673
+ filename: converted.filename,
674
+ };
675
+ } catch (error) {
676
+ if (signal?.aborted) throw error;
677
+ return {
678
+ type: 'text',
679
+ text: `--- ${name || 'attachment'} ---\n${error instanceof Error ? error.message : String(error)}`,
680
+ };
681
+ }
682
+ }
683
+
633
684
  /**
634
685
  * PDF intake validation. Every PDF keeps its content-addressed reference — the
635
686
  * one conversion to text happens at provider lowering — so this only checks what
@@ -638,13 +689,19 @@ export function providerTakesNativePdf(provider) {
638
689
  * PDF as text reads its first MAX_PDF_PAGES pages, so a longer PDF is accepted.
639
690
  * The page count is recorded on the part for estimates.
640
691
  */
641
- export async function preparePromptSubmissionForProvider(intake, provider) {
692
+ export async function preparePromptSubmissionForProvider(
693
+ intake,
694
+ provider,
695
+ { convertLegacy = undefined, signal = null } = {}
696
+ ) {
642
697
  const prompt = intake?.prompt;
643
698
  if (!Array.isArray(prompt)) return intake;
644
699
  const nativePdf = providerTakesNativePdf(provider);
645
700
  let changed = false;
646
701
  const prepared = [];
647
- for (const part of prompt) {
702
+ for (const candidate of prompt) {
703
+ const part = await packageLegacyOfficePart(candidate, { convertLegacy, signal });
704
+ if (part !== candidate) changed = true;
648
705
  const mimeType = String(part?.mimeType || part?.mediaType || '').toLowerCase();
649
706
  if (part?.type !== 'file' || mimeType !== 'application/pdf' || !isAttachmentReference(part)) {
650
707
  prepared.push(part);
@@ -14,7 +14,12 @@ import { fileURLToPath } from 'node:url';
14
14
  import { resolvePluginData } from '../shared/plugin-paths.mjs';
15
15
  import { onnxRuntimeSupported } from '../shared/onnx-runtime-support.mjs';
16
16
  import { fileHasData } from '../shared/file-has-data.mjs';
17
- import { effortJudgeInstallCurrent, effortJudgeInstallStamped, installEffortJudgeModel } from './model-install.mjs';
17
+ import {
18
+ effortJudgeInstallCurrent,
19
+ effortJudgeInstallStamped,
20
+ installEffortJudgeModel,
21
+ invalidateDamagedEffortJudgeInstall,
22
+ } from './model-install.mjs';
18
23
 
19
24
  export const EFFORT_JUDGE_UNSUPPORTED = 'Auto effort is unsupported on darwin-x64 (no onnxruntime-node binding)';
20
25
  const WORKER_PATH = fileURLToPath(new URL('./judge-worker.mjs', import.meta.url));
@@ -139,6 +144,7 @@ function ensureWorker(dir) {
139
144
  lastLoadError = msg.message;
140
145
  lastLoadErrorAt = Date.now();
141
146
  notifyReady(false);
147
+ void repairAfterLoadError(dir);
142
148
  } else if (msg.type === 'result') {
143
149
  settle(msg.id, (entry) => entry.resolve(msg.probs));
144
150
  } else if (msg.type === 'error') {
@@ -204,6 +210,49 @@ function request(target, { request: text, prev, prevRequest, step }) {
204
210
  // One install at a time (the settings install and the boot refresh can overlap).
205
211
  let installing = null;
206
212
  let refreshChecked = false;
213
+ let verifying = false;
214
+
215
+ /** Why the judge last failed to load; '' when it loaded or never failed. */
216
+ export function effortJudgeLastError() {
217
+ return lastLoadError;
218
+ }
219
+
220
+ // Runs `task` as the one install: the slot stays taken until it settles, so
221
+ // installs, refreshes and repairs never overlap.
222
+ function occupyInstall(task) {
223
+ const run = task().finally(() => {
224
+ if (installing === run) installing = null;
225
+ });
226
+ installing = run;
227
+ return run;
228
+ }
229
+
230
+ // A model that fails to load may be damaged on disk (a crash can leave a
231
+ // stamped file with a zeroed tail). Re-verify the hashes once per load error,
232
+ // off the turn's path: a mismatch drops the stamp and re-downloads only the bad
233
+ // files; matching hashes keep the plain LOAD_RETRY_MS back-off. Verifying and
234
+ // invalidating hold the install slot, after any install in flight has settled,
235
+ // so the stamp they remove is never one an installer just wrote.
236
+ async function repairAfterLoadError(dir) {
237
+ if (verifying || process.env.MIXDOG_EFFORT_JUDGE_DIR || !onnxRuntimeSupported()) return;
238
+ verifying = true;
239
+ try {
240
+ while (installing) await installing.catch(() => {});
241
+ await occupyInstall(async () => {
242
+ const bad = await invalidateDamagedEffortJudgeInstall(dir);
243
+ if (!bad.length) return;
244
+ process.stderr.write(`[effort-judge] reinstalling damaged model files: ${bad.join(', ')}\n`);
245
+ await runInstall();
246
+ // The files are replaced: the next turn loads them without the back-off.
247
+ lastLoadErrorAt = 0;
248
+ lastLoadError = '';
249
+ });
250
+ } catch (error) {
251
+ process.stderr.write(`[effort-judge] model repair failed: ${error?.message || error}\n`);
252
+ } finally {
253
+ verifying = false;
254
+ }
255
+ }
207
256
 
208
257
  /**
209
258
  * Installs or updates the judge model from the release the bundled manifest
@@ -213,19 +262,17 @@ let refreshChecked = false;
213
262
  */
214
263
  export function installEffortJudge() {
215
264
  if (!onnxRuntimeSupported()) return Promise.reject(new Error(EFFORT_JUDGE_UNSUPPORTED));
216
- installing ??= (async () => {
217
- const dir = effortJudgeModelDir();
218
- if (process.env.MIXDOG_EFFORT_JUDGE_DIR || (effortJudgeAvailable(dir) && effortJudgeInstallCurrent(dir))) {
219
- if (!effortJudgeAvailable(dir))
220
- throw new Error(`The Auto reasoning model is not installed (expected in ${dir}).`);
221
- return;
222
- }
223
- await installEffortJudgeModel(dir);
224
- if (worker) await shutdownEffortJudge();
225
- })().finally(() => {
226
- installing = null;
227
- });
228
- return installing;
265
+ return installing ?? occupyInstall(runInstall);
266
+ }
267
+
268
+ async function runInstall() {
269
+ const dir = effortJudgeModelDir();
270
+ if (process.env.MIXDOG_EFFORT_JUDGE_DIR || (effortJudgeAvailable(dir) && effortJudgeInstallCurrent(dir))) {
271
+ if (!effortJudgeAvailable(dir)) throw new Error(`The Auto reasoning model is not installed (expected in ${dir}).`);
272
+ return;
273
+ }
274
+ await installEffortJudgeModel(dir);
275
+ if (worker) await shutdownEffortJudge();
229
276
  }
230
277
 
231
278
  /**
@@ -5,9 +5,10 @@
5
5
  // install.json records the installed release; a newer bundled manifest makes
6
6
  // the install stale and the next refresh downloads only what changed.
7
7
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
8
+ import { open as openAsync } from 'node:fs/promises';
8
9
  import { join } from 'node:path';
9
10
  import { fileURLToPath } from 'node:url';
10
- import { parseJsonText, writeJsonAtomicSync } from '../shared/atomic-file.mjs';
11
+ import { isFileFsyncUnsupported, parseJsonText, writeJsonAtomicSync } from '../shared/atomic-file.mjs';
11
12
  import { downloadToFileWithRetry } from '../shared/bounded-download.mjs';
12
13
  import { sha256File, validSha256 } from '../shared/native-asset.mjs';
13
14
 
@@ -52,10 +53,40 @@ export function effortJudgeInstallCurrent(dir, manifest = effortJudgeManifest())
52
53
  return stampedRelease(dir) === manifest.release;
53
54
  }
54
55
 
56
+ /**
57
+ * Files in `dir` that are missing or differ from the manifest sha256. When any
58
+ * does, the stamp is removed so the folder reads as not installed and the next
59
+ * install re-downloads only those files (a crash can leave a stamped model with
60
+ * its tail zeroed: same size, wrong hash).
61
+ */
62
+ export async function invalidateDamagedEffortJudgeInstall(dir, manifest = effortJudgeManifest()) {
63
+ const bad = [];
64
+ for (const [name, file] of Object.entries(manifest.files)) {
65
+ const target = join(dir, name);
66
+ if (!existsSync(target) || (await sha256File(target)) !== file.sha256) bad.push(name);
67
+ }
68
+ if (bad.length) rmSync(join(dir, STAMP), { force: true });
69
+ return bad;
70
+ }
71
+
72
+ // Flushes the downloaded file through a write handle (Windows refuses fsync on
73
+ // a read-only one) so a crash right after the rename cannot publish a file
74
+ // whose size is committed but whose tail never reached the disk.
75
+ export async function fsyncFile(path, open = openAsync) {
76
+ const handle = await open(path, 'r+');
77
+ try {
78
+ await handle.sync();
79
+ } catch (err) {
80
+ if (!isFileFsyncUnsupported(err)) throw err;
81
+ } finally {
82
+ await handle.close();
83
+ }
84
+ }
85
+
55
86
  /** Download what is missing or different, then stamp the release. */
56
87
  export async function installEffortJudgeModel(
57
88
  dir,
58
- { fetchFn = globalThis.fetch, manifest = effortJudgeManifest() } = {}
89
+ { fetchFn = globalThis.fetch, manifest = effortJudgeManifest(), syncFile = fsyncFile } = {}
59
90
  ) {
60
91
  mkdirSync(dir, { recursive: true });
61
92
  let replaced = false;
@@ -73,6 +104,7 @@ export async function installEffortJudgeModel(
73
104
  const actual = await sha256File(partial);
74
105
  if (actual !== file.sha256)
75
106
  throw new Error(`effort judge ${name}: sha256 mismatch (expected ${file.sha256}, got ${actual})`);
107
+ await syncFile(partial);
76
108
  // From the first replaced file until the new stamp, the folder is a mix of
77
109
  // releases: without a stamp it reads as not installed, so an interrupted
78
110
  // install is never loaded.
@@ -86,11 +86,23 @@ export function parseCycle1LineFormat(raw) {
86
86
  const chunks = [];
87
87
  for (const line of lines) {
88
88
  const parts = line.split('|');
89
- if (parts.length < 4 || !/^\d+(?:\s*,\s*\d+)*$/.test(parts[0].trim())) {
89
+ // Models often write a run as `1-8` (sessions 2026-10-22: 32 of 71 lines);
90
+ // an ascending range expands to its indexes, everything else stays invalid.
91
+ if (parts.length < 4 || !/^\d+(?:\s*-\s*\d+)?(?:\s*,\s*\d+(?:\s*-\s*\d+)?)*$/.test(parts[0].trim())) {
90
92
  chunks.push({ _idxList: [], _parseError: 'invalid_line_format' });
91
93
  continue;
92
94
  }
93
- const indexes = parts[0].split(',').map((value) => Number(value.trim()));
95
+ const indexes = [];
96
+ for (const token of parts[0].split(',')) {
97
+ const [from, to = from] = token.split('-').map((value) => Number(value.trim()));
98
+ // Past the safe-integer range `n + 1 === n`, so the expansion would never end.
99
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || to < from || to - from >= 1000) {
100
+ indexes.length = 0;
101
+ indexes.push(0);
102
+ break;
103
+ }
104
+ for (let n = from; n <= to; n += 1) indexes.push(n);
105
+ }
94
106
  chunks.push({
95
107
  _idxList: indexes,
96
108
  element: parts[1].trim(),