mixdog 1.3.2 → 1.3.4
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 +8 -8
- package/scripts/verify-runtime-dependency-versions.mjs +158 -0
- package/src/agents/reviewer/AGENT.md +5 -7
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -3
- package/src/defaults/skills/local-provider/SKILL.md +33 -6
- package/src/rules/lead/LEAD.md +5 -8
- package/src/rules/shared/45-destructive.md +5 -1
- package/src/rules/shared/60-verification.md +6 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
- package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
- package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
- package/src/runtime/attachments/legacy-office.mjs +110 -0
- package/src/runtime/attachments/prompt-restore.mjs +70 -0
- package/src/runtime/attachments/store.mjs +60 -3
- package/src/runtime/effort-judge/judge-client.mjs +61 -14
- package/src/runtime/effort-judge/model-install.mjs +34 -2
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
- package/src/runtime/office/portable/portable-soffice.mjs +132 -30
- package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
- package/src/runtime/shared/agent-avatar.mjs +3 -2
- package/src/runtime/shared/agent-room-host.d.mts +31 -0
- package/src/runtime/shared/agent-room-host.mjs +58 -0
- package/src/runtime/shared/bot-hops.mjs +16 -11
- package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
- package/src/runtime/shared/redact-directories.mjs +18 -0
- package/src/runtime/shared/remove-directory.mjs +28 -0
- package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
- package/src/runtime/shared/session-sidebar-host.mjs +16 -3
- package/src/runtime/shared/tool-execution-contract.mjs +4 -0
- package/src/runtime/shared/tool-work-units.mjs +12 -9
- package/src/runtime/shared/turn-snapshot.mjs +233 -122
- package/src/runtime/shared/user-data-guard.mjs +51 -19
- package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
- package/src/runtime/shared/worktrees/contract.mjs +11 -3
- package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
- package/src/runtime/shared/worktrees/manager.mjs +419 -52
- package/src/runtime/shared/worktrees/registry.mjs +3 -1
- package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
- package/src/runtime/tidy/process.mjs +41 -8
- package/src/runtime/tidy/resolve.mjs +10 -7
- package/src/session-runtime/agent-home-delivery.mjs +73 -91
- package/src/session-runtime/agent-profile-changes.mjs +16 -0
- package/src/session-runtime/agent-room-delivery.mjs +2 -63
- package/src/session-runtime/agent-task-sessions.mjs +194 -64
- package/src/session-runtime/agent-tool-routing.mjs +49 -4
- package/src/session-runtime/boot/apis.mjs +3 -0
- package/src/session-runtime/boot/facade.mjs +5 -6
- package/src/session-runtime/boot/tools.mjs +23 -1
- package/src/session-runtime/held-requests.mjs +20 -25
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
- package/src/session-runtime/model-recency.mjs +1 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
- package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
- package/src/session-runtime/runtime-review-api.mjs +4 -1
- package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
- package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +44 -20
- package/src/session-runtime/services/agent-tool/render.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +12 -2
- package/src/session-runtime/services/worktree-access.mjs +27 -0
- package/src/session-runtime/session-hooks.mjs +9 -17
- package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
- package/src/session-runtime/setup-tool/executor.mjs +4 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
- package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
- package/src/session-runtime/tool-policy-refresh.mjs +1 -0
- package/src/session-runtime/tool-surface.mjs +9 -5
- package/src/session-runtime/turn/session-ops.mjs +8 -0
- package/src/session-runtime/turn/turn-run.mjs +5 -1
- package/src/session-runtime/usage-stats-api.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
- package/src/session-runtime/workspace/transition-steps.mjs +1 -18
- package/src/session-runtime/workspace/transitions.mjs +279 -67
- package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
- package/src/standalone/daemon-lag-profiler.mjs +3 -1
- package/src/standalone/daemon-session-control.mjs +140 -23
- package/src/standalone/daemon-telemetry.mjs +9 -0
- package/src/standalone/daemon.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +11 -2
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
- package/src/standalone/session-service/agent-route-follow.mjs +69 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
- package/src/standalone/session-service.mjs +10 -0
- package/src/standalone/session-tool-output.mjs +1 -0
- package/src/standalone/staged-import.mjs +47 -0
- package/src/tui/App.jsx +2 -0
- package/src/tui/app/message-selector.mjs +15 -2
- package/src/tui/app/model-options.mjs +16 -2
- package/src/tui/app/model-picker.mjs +2 -1
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/dist/index.mjs +403 -210
- package/src/tui/paste-attachments.mjs +0 -1
- package/src/tui/paste-text-policy.mjs +4 -5
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
- package/src/tui/session/notice-surface.mjs +3 -0
- package/src/tui/session/session-api/intake/abort.mjs +33 -17
- package/src/tui/session/session-api/intake/rewind.mjs +39 -4
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +11 -48
- package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
- package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
- package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
- package/src/session-runtime/room-member-gate.mjs +0 -61
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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 =
|
|
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(),
|