mixdog 0.9.97 → 0.9.100
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 +9 -2
- package/scripts/dependency-lock-cache-key.mjs +30 -0
- package/src/rules/agent/30-explorer.md +43 -32
- package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -84
- package/src/runtime/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +1 -191
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -4
- package/src/runtime/channels/lib/worker-ipc.mjs +0 -66
- package/src/runtime/channels/lib/worker-main.mjs +1 -20
- package/src/runtime/memory/lib/http-wire.mjs +39 -4
- package/src/runtime/shared/json-metrics.mjs +94 -0
- package/src/runtime/shared/turn-snapshot.mjs +13 -0
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- package/src/session-runtime/provider-models.mjs +11 -1
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/runtime-core.mjs +9 -4
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- package/src/session-runtime/session-title.mjs +27 -3
- package/src/standalone/channel-transport.mjs +28 -12
- package/src/standalone/daemon.mjs +3 -0
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-service.mjs +23 -1
- package/src/standalone/session-transport.mjs +3 -2
- package/src/tui/app/prompt-submit.mjs +7 -9
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- package/src/tui/dist/index.mjs +249 -43
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/session/agent-job-feed.mjs +5 -1
- package/src/tui/session/live-share.mjs +21 -7
- package/src/tui/session/queue-helpers.mjs +6 -6
- package/src/tui/session/session-api-ext.mjs +12 -4
- package/src/tui/session/session-api.mjs +10 -1
- package/src/tui/session/session-flow.mjs +4 -2
- package/src/tui/session-local.mjs +15 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.100",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -88,13 +88,18 @@
|
|
|
88
88
|
"test:embedding-runtime": "node --test scripts/embedding-runtime-prune-test.mjs scripts/memory-pg-recovery-test.mjs && node scripts/verify-embedding-runtime.mjs",
|
|
89
89
|
"test:embedding-runtime:core": "node --test scripts/embedding-runtime-prune-test.mjs scripts/memory-pg-recovery-test.mjs && node scripts/verify-embedding-runtime.mjs --core",
|
|
90
90
|
"test:embedding-runtime:warmup": "node scripts/verify-embedding-runtime.mjs --warmup",
|
|
91
|
+
"prepare:code-graph-test": "node scripts/prepare-code-graph-test.mjs",
|
|
91
92
|
"test:code-graph-dispatch": "node --test scripts/code-graph-dispatch-test.mjs",
|
|
92
93
|
"test:tui-queue": "node --test scripts/submit-commandbusy-race-test.mjs scripts/steering-drain-buckets-test.mjs scripts/abort-recovery-test.mjs scripts/message-rewind-test.mjs scripts/execution-pending-resume-kick-test.mjs scripts/execution-resume-esc-integration-test.mjs scripts/pending-stale-injection-test.mjs",
|
|
93
94
|
"test:tui-input-render": "node --test scripts/prompt-immediate-render-test.mjs scripts/prompt-key-parity-test.mjs",
|
|
94
95
|
"test:tui-streaming-window": "node --test scripts/streaming-tail-window-test.mjs scripts/tui-store-frame-batch-test.mjs && node scripts/tui-transcript-jitter-harness.mjs",
|
|
95
96
|
"test:tui-ambiguous-width": "node --test scripts/tui-ambiguous-width-test.mjs",
|
|
96
97
|
"test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/verify-release-assets-test.mjs && node --check scripts/deploy-workflow-test.mjs && node --check scripts/release-version-discipline-test.mjs && node --test scripts/verify-release-assets-test.mjs scripts/deploy-workflow-test.mjs scripts/release-version-discipline-test.mjs",
|
|
97
|
-
"test:release-focused": "npm run test:release-
|
|
98
|
+
"test:release-focused": "npm run test:release-focused:contracts && npm run test:release-focused:providers && npm run test:release-focused:compact && npm run test:release-focused:session",
|
|
99
|
+
"test:release-focused:contracts": "npm run test:release-assets && npm run test:tool-contracts && npm run test:tool-batching && npm run test:placeholder && npm run smoke:patch && npm run test:patch-binary-cache",
|
|
100
|
+
"test:release-focused:providers": "npm run test:providers && npm run test:deferred-tools && npm run test:compact && npm run test:context && npm run prepare:code-graph-test && node --test scripts/code-graph-root-federation-test.mjs scripts/code-graph-aggregate-cwd-test.mjs && npm run test:code-graph-dispatch && node --test scripts/code-graph-disk-hit-test.mjs",
|
|
101
|
+
"test:release-focused:compact": "npm run smoke:compact && npm run test:shellhardening",
|
|
102
|
+
"test:release-focused:session": "npm run test:project-registry && npm run test:session && npm run test:workflow-editor && npm run test:embedding-runtime && node --test scripts/tui-transcript-perf-test.mjs",
|
|
98
103
|
"test:native-edit-wire": "node --test scripts/native-edit-wire-test.mjs",
|
|
99
104
|
"test:patch-binary-cache": "node --test scripts/patch-binary-cache-test.mjs",
|
|
100
105
|
"test:patch-parity": "node --test scripts/v4a-parity-test.mjs",
|
|
@@ -155,9 +160,11 @@
|
|
|
155
160
|
"pg": "^8.22.0",
|
|
156
161
|
"puppeteer-core": "^25.2.0",
|
|
157
162
|
"react": "^19.2.7",
|
|
163
|
+
"sharp": "^0.35.3",
|
|
158
164
|
"string-width": "^8.2.1",
|
|
159
165
|
"strip-ansi": "^7.2.0",
|
|
160
166
|
"tiktoken": "^1.0.22",
|
|
167
|
+
"unpdf": "^1.8.0",
|
|
161
168
|
"undici": "^8.5.0",
|
|
162
169
|
"wrap-ansi": "^10.0.0",
|
|
163
170
|
"ws": "^8.21.0",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { resolve } from 'node:path'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import { normalizeRuntimeLockfile } from './runtime-dependency-cache-key.mjs'
|
|
9
|
+
|
|
10
|
+
export const DEPENDENCY_LOCK_CACHE_SCHEMA = 1
|
|
11
|
+
|
|
12
|
+
export function dependencyLockCacheKey(lockfile) {
|
|
13
|
+
const normalized = normalizeRuntimeLockfile(lockfile)
|
|
14
|
+
const fingerprint = createHash('sha256')
|
|
15
|
+
.update(JSON.stringify(normalized))
|
|
16
|
+
.digest('hex')
|
|
17
|
+
return `dependency-lock-v${DEPENDENCY_LOCK_CACHE_SCHEMA}-${fingerprint}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''
|
|
21
|
+
if (invokedPath === import.meta.url) {
|
|
22
|
+
const lockfilePath = process.argv[2]
|
|
23
|
+
if (!lockfilePath) throw new Error('Usage: dependency-lock-cache-key.mjs <package-lock.json>')
|
|
24
|
+
readFile(resolve(lockfilePath))
|
|
25
|
+
.then((lockfile) => process.stdout.write(`${dependencyLockCacheKey(lockfile)}\n`))
|
|
26
|
+
.catch((error) => {
|
|
27
|
+
process.stderr.write(`Dependency lock cache key failed: ${error?.message || error}\n`)
|
|
28
|
+
process.exitCode = 1
|
|
29
|
+
})
|
|
30
|
+
}
|
|
@@ -9,19 +9,19 @@ kind: retrieval
|
|
|
9
9
|
Return only WHERE (`path:line`), never WHY. You ARE `explore`; never call it.
|
|
10
10
|
Use only grep/find/glob/code_graph; `read` and `list` are forbidden.
|
|
11
11
|
|
|
12
|
-
Turn 1 (`turn 1/3`) is the whole search and should already mint anchors.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
a file/dir name or
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
Turn 1 (`turn 1/3`) is the whole search and should already mint anchors.
|
|
13
|
+
Extract every concrete locator facet implied by the query, deduplicate
|
|
14
|
+
overlap, and send the maximum independent fan-out in one batch under the
|
|
15
|
+
shared one-route contract; never cap query or facet count. Route each facet to
|
|
16
|
+
exactly one cheapest anchor source: `code_graph` `symbol_search` when it names
|
|
17
|
+
a plausible symbol/identifier; grep `content_with_context` with `pattern[]` of
|
|
18
|
+
4–8 code-token variants for a concept or quoted error; `find` `query[]` only
|
|
19
|
+
when the target itself is a file/dir name or unverified path fragment. For a
|
|
20
|
+
symptom/behavior query, include the reported surface and its immediate
|
|
21
|
+
producer as sibling facets in the same batch when both are needed to locate
|
|
22
|
+
the behavior. Never add generic repository metadata, tests, docs, or adjacent
|
|
23
|
+
subsystems unless the query asks for them, and never send the same facet to
|
|
24
|
+
multiple tools merely for confidence.
|
|
25
25
|
|
|
26
26
|
Grep defaults to `output_mode:"content_with_context"` with `context:0`
|
|
27
27
|
(matches only — the match line already carries its citable `path:line`) and a
|
|
@@ -35,33 +35,44 @@ queries to English identifiers.
|
|
|
35
35
|
Scope is every `<roots><root>…</root></roots>` entry when supplied, otherwise
|
|
36
36
|
session cwd. Search every supplied root in the turn-1 batch: grep/glob batch
|
|
37
37
|
`path[]`, while find uses one sibling call per root. Never silently fall back to
|
|
38
|
-
cwd or omit a supplied root. A
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
cwd or omit a supplied root. A tool-returned path is immutable evidence: copy
|
|
39
|
+
it exactly and never join, prefix, normalize, repair, or reconstruct it from a
|
|
40
|
+
root. A relative find result is a pre-anchor for code-location queries and may
|
|
41
|
+
only scope a later grep; for file/dir-location queries it may be returned
|
|
42
|
+
verbatim. For unverified `src` paths, use `find` first; never guess or invent
|
|
43
|
+
directories or pair `path:"."` with guessed `src/**`. Scoped grep/glob may use
|
|
44
|
+
only a supplied root or an exact find-returned path. After zero hits, change
|
|
45
|
+
tokens or scope, never wording or guessed paths.
|
|
43
46
|
|
|
44
47
|
An anchor is a `path:line` containing a query token or synonym, including a
|
|
45
48
|
code_graph hit. Generic terms without query specificity are zero. Never
|
|
46
|
-
re-locate, reconfirm, or
|
|
47
|
-
pre-anchor and counts as zero. After every result, stop and
|
|
48
|
-
|
|
49
|
+
re-locate, reconfirm, upgrade, or weaken an anchor. A path without `:line` is a
|
|
50
|
+
pre-anchor and counts as zero for code locations. After every result, stop and
|
|
51
|
+
answer as soon as the query's locator objective has specific-token anchors;
|
|
52
|
+
never return a weak or merely plausible anchor.
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
Turn 2 is allowed only when turn 1 has zero valid anchors for the locator
|
|
55
|
+
objective. Batch every unresolved facet at once with changed concrete tokens
|
|
56
|
+
or scope; never repeat the same tokens and scope. Exact pre-anchors may use
|
|
57
|
+
one batched scoped `content_with_context` grep with `head_limit` to mint
|
|
58
|
+
coordinates.
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
Turn 3 is allowed only when turn 2 produced new exact pre-anchors, or an
|
|
61
|
+
explicit flow/default-resolution query has entry anchors but still needs one
|
|
62
|
+
dependent hop. Batch every final scoped grep or resolving hop at once. Never
|
|
63
|
+
start a broad search, add a facet, or merely reword the query on turn 3; if
|
|
64
|
+
these conditions are absent, return `EXPLORATION_FAILED` after turn 2.
|
|
65
|
+
|
|
66
|
+
Use at most 3 tool turns and label every tool message `turn N/3`; normally use
|
|
67
|
+
one batch and one answer. An allowed turn is not a target: fail as soon as the
|
|
68
|
+
next turn lacks a concrete evidence-producing move. The first matching
|
|
69
|
+
entry/definition anchors a concept, value, or default; never trace its chain
|
|
70
|
+
unless the query explicitly asks for flow/default resolution.
|
|
60
71
|
|
|
61
72
|
Answer in at most 3 lines, most specific first:
|
|
62
73
|
`path:line — symbol — short reason`. Copy every cited `path:line` verbatim from
|
|
63
74
|
a tool result in this session; never estimate, adjust, or recall it. Every
|
|
64
75
|
code-location line requires `:line`; never return a bare filename or vague
|
|
65
76
|
prose. A file/dir-location query may return an exact verified path without
|
|
66
|
-
`:line`. Return `EXPLORATION_FAILED`
|
|
67
|
-
|
|
77
|
+
`:line`. Return `EXPLORATION_FAILED` when the bounded recovery rules above
|
|
78
|
+
cannot produce a verified anchor; never fabricate, estimate, or soften one.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
1
|
import { isInclusiveProvider } from '../../shared/llm/cost.mjs';
|
|
2
|
+
import { estimateJsonBytes, hashStructuredValue } from '../../shared/json-metrics.mjs';
|
|
3
3
|
import {
|
|
4
4
|
appendAgentTrace,
|
|
5
5
|
drainAgentTrace,
|
|
@@ -17,7 +17,23 @@ import {
|
|
|
17
17
|
|
|
18
18
|
function estimateProviderPayloadBytes(messages, model, tools) {
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
let referencedBytes = 0;
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const walk = (value) => {
|
|
23
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return;
|
|
24
|
+
seen.add(value);
|
|
25
|
+
if (typeof value.attachmentRef === 'string') {
|
|
26
|
+
const bytes = Number(value.sizeBytes) || 0;
|
|
27
|
+
referencedBytes += value.type === 'image' || value.type === 'file'
|
|
28
|
+
? Math.ceil(bytes / 3) * 4
|
|
29
|
+
: bytes;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value)) value.forEach(walk);
|
|
33
|
+
else Object.values(value).forEach(walk);
|
|
34
|
+
};
|
|
35
|
+
walk(messages);
|
|
36
|
+
return estimateJsonBytes({ model, messages, tools: tools || [] }) + referencedBytes;
|
|
21
37
|
}
|
|
22
38
|
catch {
|
|
23
39
|
return null;
|
|
@@ -60,9 +76,7 @@ function extractCacheWriteTokens(usage) {
|
|
|
60
76
|
// hashing megabytes per turn. Truncated SHA1 keeps the trace row compact.
|
|
61
77
|
function messagePrefixHash(messages) {
|
|
62
78
|
try {
|
|
63
|
-
|
|
64
|
-
const slice = json.length > 4096 ? json.slice(0, 4096) : json;
|
|
65
|
-
return createHash('sha1').update(slice).digest('hex').slice(0, 12);
|
|
79
|
+
return hashStructuredValue(messages || [], { maxStringChars: 4096 }).slice(0, 12);
|
|
66
80
|
} catch {
|
|
67
81
|
return null;
|
|
68
82
|
}
|
|
@@ -112,6 +112,13 @@ function _sanitizeGrokModels(models) {
|
|
|
112
112
|
const display = _displayGrokModel(m);
|
|
113
113
|
const family = _normalizeGrokFamily(m?.id);
|
|
114
114
|
const reasoningLevels = _grokModelSupportsEffort(m?.id) ? ['none', 'low', 'medium', 'high'] : [];
|
|
115
|
+
// api.x.ai /models does not expose a max-output field. Historical
|
|
116
|
+
// enrichment could persist contextWindow as outputTokens (for example
|
|
117
|
+
// 500k/500k); clear it rather than advertise an invented request cap.
|
|
118
|
+
if (out?.outputTokens != null) {
|
|
119
|
+
changed = true;
|
|
120
|
+
out = { ...out, outputTokens: null };
|
|
121
|
+
}
|
|
115
122
|
if (display && display !== out?.display) {
|
|
116
123
|
changed = true;
|
|
117
124
|
out = { ...out, display, name: out?.name || out?.id };
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
attachmentTextForPart,
|
|
4
|
+
isAttachmentReference,
|
|
5
|
+
readAttachmentBase64,
|
|
6
|
+
} from '../../../attachments/store.mjs';
|
|
2
7
|
|
|
3
8
|
const DEFAULT_IMAGE_MIME = 'image/png';
|
|
4
9
|
|
|
@@ -9,6 +14,9 @@ function cleanMimeType(value) {
|
|
|
9
14
|
|
|
10
15
|
function imageInfo(block) {
|
|
11
16
|
if (!block || typeof block !== 'object' || block.type !== 'image') return null;
|
|
17
|
+
if (isAttachmentReference(block)) {
|
|
18
|
+
return { data: readAttachmentBase64(block), mimeType: cleanMimeType(block.mimeType || block.mediaType) };
|
|
19
|
+
}
|
|
12
20
|
if (typeof block.data === 'string' && block.data) {
|
|
13
21
|
return { data: block.data, mimeType: cleanMimeType(block.mimeType || block.mediaType) };
|
|
14
22
|
}
|
|
@@ -32,10 +40,13 @@ function geminiInlineInfo(block) {
|
|
|
32
40
|
// as their native document shape; estimators must never serialize its base64.
|
|
33
41
|
function fileInfo(block) {
|
|
34
42
|
if (!block || typeof block !== 'object' || block.type !== 'file') return null;
|
|
35
|
-
|
|
43
|
+
const data = isAttachmentReference(block)
|
|
44
|
+
? readAttachmentBase64(block)
|
|
45
|
+
: (typeof block.data === 'string' ? block.data : '');
|
|
46
|
+
if (!data) return null;
|
|
36
47
|
const mimeType = String(block.mimeType || block.mediaType || 'application/pdf').trim().toLowerCase() || 'application/pdf';
|
|
37
48
|
const filename = typeof block.filename === 'string' && block.filename ? block.filename : '';
|
|
38
|
-
return { data
|
|
49
|
+
return { data, mimeType, filename };
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
function imageUrlFromPart(block) {
|
|
@@ -95,6 +106,7 @@ function imageMimeFromDataUrl(url) {
|
|
|
95
106
|
function textFromPart(block) {
|
|
96
107
|
if (typeof block === 'string') return block;
|
|
97
108
|
if (!block || typeof block !== 'object') return '';
|
|
109
|
+
if (block.type === 'text' && isAttachmentReference(block)) return attachmentTextForPart(block);
|
|
98
110
|
if (typeof block.text === 'string') return block.text;
|
|
99
111
|
if (typeof block.content === 'string') return block.content;
|
|
100
112
|
return '';
|
|
@@ -112,7 +124,7 @@ function contentParts(content) {
|
|
|
112
124
|
if (content && typeof content === 'object'
|
|
113
125
|
&& (imageUrlFromPart(content) || imageFileIdFromPart(content)
|
|
114
126
|
|| imageFileUriFromPart(content) || geminiInlineInfo(content)
|
|
115
|
-
|| fileInfo(content))) return [content];
|
|
127
|
+
|| fileInfo(content) || isAttachmentReference(content))) return [content];
|
|
116
128
|
return null;
|
|
117
129
|
}
|
|
118
130
|
|
|
@@ -149,6 +161,14 @@ function imageIdentity(kind, value, mimeType = '') {
|
|
|
149
161
|
|
|
150
162
|
function imageDescriptor(part) {
|
|
151
163
|
if (!part || typeof part !== 'object') return null;
|
|
164
|
+
if (part.type === 'image' && isAttachmentReference(part)) {
|
|
165
|
+
return {
|
|
166
|
+
identity: imageIdentity('attachment-ref', part.attachmentRef, part.mimeType || part.mediaType),
|
|
167
|
+
width: positiveDimension(part.width, part.dimensions?.width),
|
|
168
|
+
height: positiveDimension(part.height, part.dimensions?.height),
|
|
169
|
+
detail: String(part.detail || 'auto').toLowerCase(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
152
172
|
const info = imageInfo(part);
|
|
153
173
|
const inline = geminiInlineInfo(part);
|
|
154
174
|
const url = imageUrlFromPart(part);
|
|
@@ -187,6 +207,9 @@ export function contentFileDescriptors(content) {
|
|
|
187
207
|
const parts = contentParts(content);
|
|
188
208
|
if (!parts) return [];
|
|
189
209
|
return parts.flatMap((part) => {
|
|
210
|
+
if (part?.type === 'file' && isAttachmentReference(part)) {
|
|
211
|
+
return [{ mimeType: String(part.mimeType || part.mediaType || 'application/pdf'), sizeBytes: Number(part.sizeBytes) || 0 }];
|
|
212
|
+
}
|
|
190
213
|
const file = fileInfo(part);
|
|
191
214
|
if (!file) return [];
|
|
192
215
|
return [{ mimeType: file.mimeType, sizeBytes: Math.floor((file.data.length * 3) / 4) }];
|
|
@@ -215,6 +238,7 @@ function storedHistoryImagePlaceholder(part) {
|
|
|
215
238
|
function sanitizePartForStoredHistory(part) {
|
|
216
239
|
if (typeof part === 'string') return part;
|
|
217
240
|
if (!part || typeof part !== 'object') return part;
|
|
241
|
+
if (isAttachmentReference(part)) return part;
|
|
218
242
|
if (part.type === 'image' || part.type === 'image_url' || part.type === 'input_image' || imageUrlFromPart(part) || imageFileIdFromPart(part) || imageFileUriFromPart(part) || geminiInlineInfo(part)) {
|
|
219
243
|
return { type: 'text', text: storedHistoryImagePlaceholder(part) };
|
|
220
244
|
}
|
|
@@ -307,6 +331,9 @@ export function normalizeContentForAnthropic(content) {
|
|
|
307
331
|
if (part?.type === 'input_text' || part?.type === 'output_text') {
|
|
308
332
|
return { type: 'text', text: part.text || '' };
|
|
309
333
|
}
|
|
334
|
+
if (part?.type === 'text' && isAttachmentReference(part)) {
|
|
335
|
+
return { type: 'text', text: attachmentTextForPart(part) };
|
|
336
|
+
}
|
|
310
337
|
return part;
|
|
311
338
|
});
|
|
312
339
|
}
|
|
@@ -360,7 +360,7 @@ function _modelsDevMetadataSync(id, provider) {
|
|
|
360
360
|
if (!pid) return null;
|
|
361
361
|
if (!_mdCache) {
|
|
362
362
|
warmModelsDevFromDiskSync();
|
|
363
|
-
if (!_mdCache)
|
|
363
|
+
if (!_mdCache) return null;
|
|
364
364
|
}
|
|
365
365
|
const row = _mdCache?.[pid]?.models?.[id];
|
|
366
366
|
if (!row || !row.cost) return null;
|
|
@@ -372,10 +372,12 @@ function _modelsDevMetadataSync(id, provider) {
|
|
|
372
372
|
// require a `cost` field (staleness only needs family/release_date) and
|
|
373
373
|
// returns the raw row untouched. Warms from disk one-shot if memory is cold;
|
|
374
374
|
// returns null when the catalog is unavailable so callers can skip filtering.
|
|
375
|
+
// Sync reads never start network work: session warmup owns remote catalog I/O,
|
|
376
|
+
// and injected/provider-local transports must remain request-hermetic.
|
|
375
377
|
// `_test` (tests only) injects a fake catalog map without touching disk.
|
|
376
378
|
export function getModelsDevRowSync(id, provider, _test) {
|
|
377
379
|
const cat = _test || _mdCache || (warmModelsDevFromDiskSync(), _mdCache);
|
|
378
|
-
if (!cat)
|
|
380
|
+
if (!cat) return null;
|
|
379
381
|
const pid = _modelsDevProviderId(provider);
|
|
380
382
|
if (!pid) return null;
|
|
381
383
|
const row = cat?.[pid]?.models?.[id];
|
|
@@ -387,7 +389,7 @@ export function getModelsDevRowSync(id, provider, _test) {
|
|
|
387
389
|
// provider's whole catalog. Returns null when the catalog is cold/unavailable.
|
|
388
390
|
export function getModelsDevProviderModelsSync(provider, _test) {
|
|
389
391
|
const cat = _test || _mdCache || (warmModelsDevFromDiskSync(), _mdCache);
|
|
390
|
-
if (!cat)
|
|
392
|
+
if (!cat) return null;
|
|
391
393
|
const pid = _modelsDevProviderId(provider);
|
|
392
394
|
if (!pid) return null;
|
|
393
395
|
const models = cat?.[pid]?.models;
|
|
@@ -31,6 +31,22 @@ function readModelsFromCacheFile(fileName) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function providerCachedModelsSync(provider) {
|
|
35
|
+
const p = String(provider || '').toLowerCase();
|
|
36
|
+
const files = PROVIDER_CACHE_FILES[p] || [];
|
|
37
|
+
const rows = [];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
for (const fileName of files) {
|
|
40
|
+
for (const row of readModelsFromCacheFile(fileName)) {
|
|
41
|
+
const id = rowId(row);
|
|
42
|
+
if (!id || seen.has(id)) continue;
|
|
43
|
+
seen.add(id);
|
|
44
|
+
rows.push({ ...row, id });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
function modelAliases(id) {
|
|
35
51
|
const text = String(id || '').trim();
|
|
36
52
|
if (!text) return [];
|
|
@@ -60,11 +76,17 @@ function getProviderCachedModelSync(provider, model) {
|
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
export function providerCachedModelMetadataSync(provider, model) {
|
|
79
|
+
const p = String(provider || '').toLowerCase();
|
|
63
80
|
const row = getProviderCachedModelSync(provider, model);
|
|
64
81
|
if (!row) return null;
|
|
65
82
|
return {
|
|
66
83
|
contextWindow: num(row.contextWindow ?? row.context_window ?? row.maxContextWindow ?? row.max_context_window ?? row.max_input_tokens),
|
|
67
|
-
|
|
84
|
+
// Grok's /models response currently publishes a context window but no
|
|
85
|
+
// output ceiling. Older enriched cache rows copied contextWindow into
|
|
86
|
+
// outputTokens; never feed that derived value back as provider-native.
|
|
87
|
+
outputTokens: p === 'grok-oauth'
|
|
88
|
+
? null
|
|
89
|
+
: num(row.outputTokens ?? row.maxOutputTokens ?? row.max_output_tokens ?? row.output_token_limit),
|
|
68
90
|
supportsVision: row.supportsVision === true,
|
|
69
91
|
supportsFunctionCalling: row.supportsFunctionCalling === true,
|
|
70
92
|
supportsWebSearch: row.supportsWebSearch === true,
|
|
@@ -9,6 +9,7 @@ import { readStreamOutcome } from '../../providers/lib/stream-outcome.mjs';
|
|
|
9
9
|
import { normalizeCompactType, DEFAULT_COMPACT_TYPE } from '../compact.mjs';
|
|
10
10
|
import { loadSession, saveSession, saveSessionAsync, saveSessionAsyncDeferred, readSessionLifecycleFromDisk } from '../store.mjs';
|
|
11
11
|
import { createAbortController } from '../../../../shared/abort-controller.mjs';
|
|
12
|
+
import { estimateJsonBytes } from '../../../../shared/json-metrics.mjs';
|
|
12
13
|
import { logLlmCall } from '../../../../shared/llm/usage-log.mjs';
|
|
13
14
|
import { appendAgentTrace } from '../../agent-trace.mjs';
|
|
14
15
|
import { recordStandaloneStatusTelemetry } from './status-telemetry.mjs';
|
|
@@ -568,7 +569,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
568
569
|
const _prefetchBytes = Buffer.byteLength(explicitPrefetchResult || '', 'utf8');
|
|
569
570
|
const _promptBytes = promptContentBytes(prompt);
|
|
570
571
|
const _userTurnBytes = promptContentBytes(_userTurnContent);
|
|
571
|
-
const _messagesBytes =
|
|
572
|
+
const _messagesBytes = estimateJsonBytes(historyMessages || []);
|
|
572
573
|
const _totalBytes = _userTurnBytes + _messagesBytes;
|
|
573
574
|
appendAgentTrace({
|
|
574
575
|
kind: 'context',
|
|
@@ -355,6 +355,9 @@ function normalizePendingMessageEntry(entry) {
|
|
|
355
355
|
id: pendingMessageId(entry),
|
|
356
356
|
enqueuedAt: Number(entry.enqueuedAt) || Date.now(),
|
|
357
357
|
};
|
|
358
|
+
const entryOptions = entry.options && typeof entry.options === 'object'
|
|
359
|
+
? { options: entry.options }
|
|
360
|
+
: {};
|
|
358
361
|
const marker = entry.notificationKind === COMPLETION_NOTIFICATION_KIND
|
|
359
362
|
? { notificationKind: COMPLETION_NOTIFICATION_KIND, enqueuedAt: Number(entry.enqueuedAt) || Date.now() }
|
|
360
363
|
: null;
|
|
@@ -366,13 +369,13 @@ function normalizePendingMessageEntry(entry) {
|
|
|
366
369
|
if (content == null) return null;
|
|
367
370
|
const text = typeof entry.text === 'string' ? entry.text.trim() : promptContentText(content).trim();
|
|
368
371
|
let out = null;
|
|
369
|
-
if (Array.isArray(content)) out = content.length > 0 ? { content, text } : null;
|
|
372
|
+
if (Array.isArray(content)) out = content.length > 0 ? { content, text, ...entryOptions } : null;
|
|
370
373
|
else if (typeof content === 'string') {
|
|
371
374
|
const value = content.trim();
|
|
372
|
-
out = value ? { content: value, text: text || value } : null;
|
|
375
|
+
out = value ? { content: value, text: text || value, ...entryOptions } : null;
|
|
373
376
|
} else {
|
|
374
377
|
const fallback = promptContentText(content).trim();
|
|
375
|
-
out = fallback ? { content: fallback, text: text || fallback } : null;
|
|
378
|
+
out = fallback ? { content: fallback, text: text || fallback, ...entryOptions } : null;
|
|
376
379
|
}
|
|
377
380
|
if (!out) return null;
|
|
378
381
|
// The lifecycle token rides along every derived copy (see carryLifecycleToken).
|
|
@@ -394,7 +397,12 @@ function pendingMessageQueueEntry(entry) {
|
|
|
394
397
|
const marker = isCompletionNotificationEntry(normalized)
|
|
395
398
|
? { notificationKind: COMPLETION_NOTIFICATION_KIND, enqueuedAt: normalized.enqueuedAt }
|
|
396
399
|
: null;
|
|
397
|
-
const base = {
|
|
400
|
+
const base = {
|
|
401
|
+
...identity,
|
|
402
|
+
content: normalized.content,
|
|
403
|
+
text: normalized.text || promptContentText(normalized.content).trim(),
|
|
404
|
+
...(normalized.options ? { options: normalized.options } : {}),
|
|
405
|
+
};
|
|
398
406
|
return carryLifecycleToken(marker ? { ...base, ...marker } : base, entry);
|
|
399
407
|
}
|
|
400
408
|
|
|
@@ -427,6 +435,20 @@ function normalizePersistedEntry(entry, options = {}) {
|
|
|
427
435
|
const message = entry.message.trim();
|
|
428
436
|
return message ? { id, message, enqueuedAt } : null;
|
|
429
437
|
}
|
|
438
|
+
if (Object.prototype.hasOwnProperty.call(entry, 'content')) {
|
|
439
|
+
const normalized = normalizePendingMessageEntry(entry);
|
|
440
|
+
if (!normalized) return null;
|
|
441
|
+
if (typeof normalized.content === 'string' && !normalized.options) {
|
|
442
|
+
return { id, message: normalized.text || normalized.content, enqueuedAt };
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
id,
|
|
446
|
+
content: normalized.content,
|
|
447
|
+
text: normalized.text,
|
|
448
|
+
...(normalized.options ? { options: normalized.options } : {}),
|
|
449
|
+
enqueuedAt,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
430
452
|
const t = pendingMessageText(entry);
|
|
431
453
|
return t ? { id, message: t, enqueuedAt } : null;
|
|
432
454
|
}
|
|
@@ -1195,10 +1217,26 @@ async function _flushForeignDrainBatch() {
|
|
|
1195
1217
|
&& !isCompletionNotificationEntry(entry)
|
|
1196
1218
|
&& !isLegacyUnmarkedCompletionNotification(text)
|
|
1197
1219
|
&& text && !isInternalRuntimeNotificationText(text);
|
|
1220
|
+
const normalized = normalizePendingMessageEntry(entry);
|
|
1221
|
+
const structured = Array.isArray(normalized?.content) || Boolean(normalized?.options);
|
|
1198
1222
|
if (foreignUser && isStaleUserInjection(entry)) {
|
|
1199
|
-
|
|
1223
|
+
const lateText = lateDeliveryText(text, entry);
|
|
1224
|
+
const content = Array.isArray(normalized?.content)
|
|
1225
|
+
? [{ type: 'text', text: `${lateText.slice(0, lateText.length - text.length)}` }, ...normalized.content]
|
|
1226
|
+
: lateText;
|
|
1227
|
+
taken.push({
|
|
1228
|
+
...(structured ? { content } : {}),
|
|
1229
|
+
text: lateText,
|
|
1230
|
+
id,
|
|
1231
|
+
...(normalized?.options ? { options: normalized.options } : {}),
|
|
1232
|
+
});
|
|
1200
1233
|
} else if (foreignUser) {
|
|
1201
|
-
taken.push({
|
|
1234
|
+
taken.push({
|
|
1235
|
+
...(structured ? { content: normalized?.content ?? text } : {}),
|
|
1236
|
+
text,
|
|
1237
|
+
id,
|
|
1238
|
+
...(normalized?.options ? { options: normalized.options } : {}),
|
|
1239
|
+
});
|
|
1202
1240
|
} else {
|
|
1203
1241
|
kept.push(entry);
|
|
1204
1242
|
}
|
|
@@ -4,13 +4,14 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { isInternalRuntimeNotificationText as contractIsInternalRuntimeNotificationText } from '../../../../shared/tool-execution-contract.mjs';
|
|
6
6
|
import { SUMMARY_PREFIX } from '../compact.mjs';
|
|
7
|
+
import { attachmentTextForPart, isAttachmentReference } from '../../../../attachments/store.mjs';
|
|
7
8
|
|
|
8
9
|
export function promptContentText(content) {
|
|
9
10
|
if (typeof content === 'string') return content;
|
|
10
11
|
if (Array.isArray(content)) {
|
|
11
12
|
return content.map((part) => {
|
|
12
13
|
if (typeof part === 'string') return part;
|
|
13
|
-
if (part?.type === 'text') return part.text || '';
|
|
14
|
+
if (part?.type === 'text') return isAttachmentReference(part) ? attachmentTextForPart(part) : (part.text || '');
|
|
14
15
|
if (part?.type === 'image') return '[Image]';
|
|
15
16
|
return part?.text || '';
|
|
16
17
|
}).filter(Boolean).join('\n');
|
|
@@ -4,12 +4,9 @@ import { renameWithRetrySync } from '../../../../shared/atomic-file.mjs';
|
|
|
4
4
|
import { sanitizeContentForStoredHistory } from '../../providers/media-normalization.mjs';
|
|
5
5
|
import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from '../lifecycle-scan.mjs';
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// text placeholder at serialization time — keeping session files small without
|
|
11
|
-
// starving the model of the image mid-conversation. Returns the same object
|
|
12
|
-
// reference when nothing changed (no-image sessions pay only a shallow scan).
|
|
7
|
+
// Inline legacy media is replaced with placeholders on disk. New prompt media
|
|
8
|
+
// already arrives as durable content-addressed refs, so session JSON stays small
|
|
9
|
+
// while provider lowering can still resolve it across turns.
|
|
13
10
|
// Per-message disk projection shared by _sessionForDisk and the save-worker
|
|
14
11
|
// delta path (tail-only projection). Returns the SAME array reference when no
|
|
15
12
|
// message changed. Idempotent: already-sanitized content passes through
|