mixdog 0.9.128 → 0.9.129
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 +1 -1
- package/src/app.mjs +2 -0
- package/src/headless-command.mjs +1 -1
- package/src/headless-exec.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +278 -25
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -9
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +27 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
package/package.json
CHANGED
package/src/app.mjs
CHANGED
package/src/headless-command.mjs
CHANGED
|
@@ -4,7 +4,7 @@ const FLAG_OPTIONS = new Set([
|
|
|
4
4
|
'--web-search', '--memory', '--json',
|
|
5
5
|
]);
|
|
6
6
|
const EXEC_UNSUPPORTED_FLAGS = new Set([
|
|
7
|
-
'--readonly', '--remote', '--onboarding',
|
|
7
|
+
'--readonly', '--remote', '--onboarding',
|
|
8
8
|
]);
|
|
9
9
|
const HEADLESS_WORKFLOW_ERROR = 'option --workflow is not supported for mixdog exec';
|
|
10
10
|
|
package/src/headless-exec.mjs
CHANGED
|
@@ -90,6 +90,8 @@ function createJsonLifecycle({
|
|
|
90
90
|
effort,
|
|
91
91
|
fast,
|
|
92
92
|
cwd,
|
|
93
|
+
webSearch = false,
|
|
94
|
+
memory = false,
|
|
93
95
|
}) {
|
|
94
96
|
let threadId = `exec_${randomUUID().replace(/-/g, '')}`;
|
|
95
97
|
const turnId = 'turn_1';
|
|
@@ -143,6 +145,8 @@ function createJsonLifecycle({
|
|
|
143
145
|
tool_mode: 'full',
|
|
144
146
|
approval_mode: 'implicit',
|
|
145
147
|
delegation: false,
|
|
148
|
+
web_search: webSearch === true,
|
|
149
|
+
memory: memory === true,
|
|
146
150
|
},
|
|
147
151
|
}, turnStartedAt);
|
|
148
152
|
emit({
|
|
@@ -540,6 +544,8 @@ export async function runHeadlessExec({
|
|
|
540
544
|
model,
|
|
541
545
|
effort,
|
|
542
546
|
fast,
|
|
547
|
+
webSearch = false,
|
|
548
|
+
memory = false,
|
|
543
549
|
json = false,
|
|
544
550
|
cwd = process.cwd(),
|
|
545
551
|
write = (text) => stdout.write(text),
|
|
@@ -571,6 +577,8 @@ export async function runHeadlessExec({
|
|
|
571
577
|
effort,
|
|
572
578
|
fast,
|
|
573
579
|
cwd,
|
|
580
|
+
webSearch,
|
|
581
|
+
memory,
|
|
574
582
|
}) : null;
|
|
575
583
|
let boundary = null;
|
|
576
584
|
let runtime = null;
|
|
@@ -602,6 +610,12 @@ export async function runHeadlessExec({
|
|
|
602
610
|
const createRuntime = runtimeFactory || (
|
|
603
611
|
await import('./mixdog-session-runtime.mjs')
|
|
604
612
|
).createMixdogSessionRuntime;
|
|
613
|
+
// Headless defaults: web research and memory tools stay OFF unless the
|
|
614
|
+
// caller opts in via --web-search / --memory. Delegation is already
|
|
615
|
+
// disallowed below, completing the solo surface. The per-process
|
|
616
|
+
// MIXDOG_FEATURE_* overrides are the runtime's canonical switches.
|
|
617
|
+
process.env.MIXDOG_FEATURE_WEB_SEARCH = webSearch === true ? '1' : '0';
|
|
618
|
+
process.env.MIXDOG_FEATURE_MEMORY = memory === true ? '1' : '0';
|
|
605
619
|
runtime = await createRuntime({
|
|
606
620
|
provider,
|
|
607
621
|
model,
|
|
@@ -1009,6 +1009,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1009
1009
|
// server-side cache prefix stable.
|
|
1010
1010
|
const _assistantTurnMsg = attachAssistantTranscriptMetadata({
|
|
1011
1011
|
role: 'assistant',
|
|
1012
|
+
createdAt: Date.now(),
|
|
1012
1013
|
// Sub-agent tool-call turns carry only mid-turn preamble in
|
|
1013
1014
|
// response.content (the real result rides the later final-answer
|
|
1014
1015
|
// turn). Blank it so it never accumulates as input tokens.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { estimateTokens } from '../context-utils.mjs';
|
|
2
2
|
|
|
3
3
|
const CONVERSATION_LINE_CHARS = 800;
|
|
4
|
-
const WORKING_FILE_CAP = 20;
|
|
5
4
|
const TOOL_OUTCOME_CHARS = 80;
|
|
6
5
|
|
|
7
6
|
function textOf(m) {
|
|
@@ -42,13 +41,15 @@ function normalizeWorkingPath(value, cwd) {
|
|
|
42
41
|
|
|
43
42
|
function pathsFromTool(name, args) {
|
|
44
43
|
const out = [];
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
44
|
+
const rawPaths = [args.path, name === 'code_graph' ? args.files : null];
|
|
45
|
+
for (const rawPath of rawPaths) {
|
|
46
|
+
const candidates = Array.isArray(rawPath)
|
|
47
|
+
? rawPath
|
|
48
|
+
: String(rawPath || '').split(',');
|
|
49
|
+
for (const item of candidates) {
|
|
50
|
+
const value = typeof item === 'string' ? item.trim() : '';
|
|
51
|
+
if (isFilePath(value)) out.push(value);
|
|
52
|
+
}
|
|
52
53
|
}
|
|
53
54
|
if (name === 'apply_patch' && typeof args.patch === 'string') {
|
|
54
55
|
for (const line of args.patch.split('\n')) {
|
|
@@ -59,28 +60,206 @@ function pathsFromTool(name, args) {
|
|
|
59
60
|
return out;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
function normalizeEventTime(value) {
|
|
64
|
+
if (value == null || value === '') return null;
|
|
65
|
+
const millis = typeof value === 'number' ? value : Date.parse(String(value));
|
|
66
|
+
if (!Number.isFinite(millis) || millis <= 0) return null;
|
|
67
|
+
return new Date(millis).toISOString();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function newerEventTime(left, right) {
|
|
71
|
+
const leftMs = left ? Date.parse(left) : 0;
|
|
72
|
+
const rightMs = right ? Date.parse(right) : 0;
|
|
73
|
+
return rightMs > leftMs ? right : left;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toolEventTime(message, toolCall, resultMessage, fallback) {
|
|
77
|
+
const candidates = [
|
|
78
|
+
resultMessage?.createdAt,
|
|
79
|
+
resultMessage?.timestamp,
|
|
80
|
+
toolCall?.createdAt,
|
|
81
|
+
toolCall?.timestamp,
|
|
82
|
+
message?.createdAt,
|
|
83
|
+
message?.timestamp,
|
|
84
|
+
message?.meta?.createdAt,
|
|
85
|
+
fallback,
|
|
86
|
+
];
|
|
87
|
+
for (const candidate of candidates) {
|
|
88
|
+
const normalized = normalizeEventTime(candidate);
|
|
89
|
+
if (normalized) return normalized;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseWorkingEntry(value) {
|
|
95
|
+
const raw = String(value || '').trim();
|
|
96
|
+
const metadata = /\s+\[([^\]]+)\]\s*$/.exec(raw);
|
|
97
|
+
const path = metadata ? raw.slice(0, metadata.index).trim() : raw;
|
|
98
|
+
const fields = {};
|
|
99
|
+
for (const part of String(metadata?.[1] || '').split(';')) {
|
|
100
|
+
const hit = /^\s*(editedAt|seenAt)=(.+?)\s*$/.exec(part);
|
|
101
|
+
if (hit) fields[hit[1]] = normalizeEventTime(hit[2]);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
path,
|
|
105
|
+
editedAt: fields.editedAt || null,
|
|
106
|
+
seenAt: fields.seenAt || null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function priorWorkingFileGroups(text, cwd) {
|
|
111
|
+
const modified = [];
|
|
112
|
+
const referenced = [];
|
|
113
|
+
let section = null;
|
|
114
|
+
for (const raw of String(text || '').split('\n')) {
|
|
115
|
+
const line = raw.trim();
|
|
116
|
+
if (line === '## Working files') {
|
|
117
|
+
section = 'referenced';
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!section) continue;
|
|
121
|
+
if (line === '### Modified') {
|
|
122
|
+
section = 'modified';
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (line === '### Referenced') {
|
|
126
|
+
section = 'referenced';
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (/^##\s+/.test(line) || /^<\/?prior-compacted-context>$/.test(line)) {
|
|
130
|
+
section = null;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const hit = /^-\s+(.+)$/.exec(line);
|
|
134
|
+
if (!hit || hit[1] === '(none)' || /^\+\d+\s+omitted$/.test(hit[1])) continue;
|
|
135
|
+
const entry = parseWorkingEntry(hit[1]);
|
|
136
|
+
entry.path = normalizeWorkingPath(entry.path, cwd);
|
|
137
|
+
if (!isFilePath(entry.path)) continue;
|
|
138
|
+
if (section === 'modified') {
|
|
139
|
+
modified.push(entry);
|
|
140
|
+
} else {
|
|
141
|
+
referenced.push(entry);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { modified, referenced };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mergeWorkingEntries(current, prior, limit, cwd) {
|
|
148
|
+
const entries = new Map();
|
|
149
|
+
let order = 0;
|
|
150
|
+
const touch = (raw, kind) => {
|
|
151
|
+
const path = normalizeWorkingPath(raw?.path, cwd);
|
|
152
|
+
if (!isFilePath(path)) return;
|
|
153
|
+
const key = path.toLowerCase();
|
|
154
|
+
let entry = entries.get(key);
|
|
155
|
+
if (!entry) {
|
|
156
|
+
entry = {
|
|
157
|
+
path,
|
|
158
|
+
editedAt: null,
|
|
159
|
+
seenAt: null,
|
|
160
|
+
modified: false,
|
|
161
|
+
order: order++,
|
|
162
|
+
};
|
|
163
|
+
entries.set(key, entry);
|
|
164
|
+
}
|
|
165
|
+
entry.seenAt = newerEventTime(entry.seenAt, normalizeEventTime(raw?.seenAt));
|
|
166
|
+
if (kind === 'modified') {
|
|
167
|
+
entry.modified = true;
|
|
168
|
+
entry.editedAt = newerEventTime(entry.editedAt, normalizeEventTime(raw?.editedAt));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
for (const entry of current.modified) touch(entry, 'modified');
|
|
172
|
+
for (const entry of current.referenced) touch(entry, 'referenced');
|
|
173
|
+
for (const entry of prior.modified) touch(entry, 'modified');
|
|
174
|
+
for (const entry of prior.referenced) touch(entry, 'referenced');
|
|
175
|
+
const sorted = [...entries.values()].sort((left, right) => {
|
|
176
|
+
const leftTime = Date.parse(left.editedAt || left.seenAt || '') || 0;
|
|
177
|
+
const rightTime = Date.parse(right.editedAt || right.seenAt || '') || 0;
|
|
178
|
+
return rightTime - leftTime || left.order - right.order;
|
|
179
|
+
});
|
|
180
|
+
const modified = sorted.filter((entry) => entry.modified);
|
|
181
|
+
const referenced = sorted.filter((entry) => !entry.modified);
|
|
182
|
+
if (!Number.isFinite(limit)) return { modified, referenced };
|
|
183
|
+
const keptModified = modified.slice(0, limit);
|
|
184
|
+
return {
|
|
185
|
+
modified: keptModified,
|
|
186
|
+
referenced: referenced.slice(0, Math.max(0, limit - keptModified.length)),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function collectWorkingFileGroups(messages, cap = Number.POSITIVE_INFINITY, {
|
|
191
|
+
cwd,
|
|
192
|
+
previousSummary,
|
|
193
|
+
now = Date.now(),
|
|
194
|
+
} = {}) {
|
|
195
|
+
const numericCap = Number(cap);
|
|
196
|
+
const limit = Number.isFinite(numericCap) && numericCap > 0
|
|
197
|
+
? Math.floor(numericCap)
|
|
198
|
+
: Number.POSITIVE_INFINITY;
|
|
199
|
+
const results = indexToolResults(messages);
|
|
200
|
+
const resultMessages = indexToolResultMessages(messages);
|
|
201
|
+
const currentModified = [];
|
|
202
|
+
const currentReferenced = [];
|
|
203
|
+
const supported = new Set(['read', 'apply_patch', 'grep', 'glob', 'find', 'code_graph', 'list']);
|
|
204
|
+
for (let i = (messages || []).length - 1; i >= 0; i -= 1) {
|
|
67
205
|
const m = messages[i];
|
|
68
206
|
if (m?.role !== 'assistant' || !Array.isArray(m.toolCalls)) continue;
|
|
69
|
-
for (let j = m.toolCalls.length - 1; j >= 0
|
|
207
|
+
for (let j = m.toolCalls.length - 1; j >= 0; j -= 1) {
|
|
70
208
|
const tc = m.toolCalls[j];
|
|
71
209
|
const name = toolName(tc);
|
|
72
|
-
if (!
|
|
210
|
+
if (!supported.has(name)) continue;
|
|
211
|
+
const resultMessage = resultMessages.get(String(tc.id || tc.toolCallId || ''));
|
|
212
|
+
if (name === 'apply_patch') {
|
|
213
|
+
const output = results.get(String(tc.id || tc.toolCallId || ''));
|
|
214
|
+
if (/Error:|failed|rejected/i.test(String(output || ''))) continue;
|
|
215
|
+
}
|
|
216
|
+
const eventTime = toolEventTime(m, tc, resultMessage, now);
|
|
73
217
|
for (const p of pathsFromTool(name, parseArgs(tc))) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
218
|
+
const path = normalizeWorkingPath(p, cwd);
|
|
219
|
+
if (!path) continue;
|
|
220
|
+
if (name === 'apply_patch') {
|
|
221
|
+
currentModified.push({
|
|
222
|
+
path,
|
|
223
|
+
editedAt: eventTime,
|
|
224
|
+
seenAt: eventTime,
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
currentReferenced.push({
|
|
228
|
+
path,
|
|
229
|
+
editedAt: null,
|
|
230
|
+
seenAt: eventTime,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
80
233
|
}
|
|
81
234
|
}
|
|
82
235
|
}
|
|
83
|
-
|
|
236
|
+
const prior = priorWorkingFileGroups(previousSummary, cwd);
|
|
237
|
+
return mergeWorkingEntries({
|
|
238
|
+
modified: currentModified,
|
|
239
|
+
referenced: currentReferenced,
|
|
240
|
+
}, prior, limit, cwd);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function collectWorkingFiles(messages, cap = Number.POSITIVE_INFINITY, options = {}) {
|
|
244
|
+
const groups = collectWorkingFileGroups(messages, cap, options);
|
|
245
|
+
return [...groups.modified, ...groups.referenced].map((entry) => entry.path);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function stripWorkingFileSections(text) {
|
|
249
|
+
const out = [];
|
|
250
|
+
let skipping = false;
|
|
251
|
+
for (const raw of String(text || '').split('\n')) {
|
|
252
|
+
const line = raw.trim();
|
|
253
|
+
if (line === '## Working files') {
|
|
254
|
+
skipping = true;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (skipping && (/^##\s+/.test(line) || /^<\/?prior-compacted-context>$/.test(line))) {
|
|
258
|
+
skipping = false;
|
|
259
|
+
}
|
|
260
|
+
if (!skipping) out.push(raw);
|
|
261
|
+
}
|
|
262
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
84
263
|
}
|
|
85
264
|
|
|
86
265
|
function indexToolResults(messages) {
|
|
@@ -93,6 +272,16 @@ function indexToolResults(messages) {
|
|
|
93
272
|
return map;
|
|
94
273
|
}
|
|
95
274
|
|
|
275
|
+
function indexToolResultMessages(messages) {
|
|
276
|
+
const map = new Map();
|
|
277
|
+
for (const message of messages || []) {
|
|
278
|
+
if (message?.role !== 'tool') continue;
|
|
279
|
+
const id = String(message.toolCallId || message.tool_call_id || '');
|
|
280
|
+
if (id) map.set(id, message);
|
|
281
|
+
}
|
|
282
|
+
return map;
|
|
283
|
+
}
|
|
284
|
+
|
|
96
285
|
function patchTarget(args) {
|
|
97
286
|
if (isFilePath(args.path)) return String(args.path).trim();
|
|
98
287
|
if (typeof args.patch === 'string') {
|
|
@@ -215,7 +404,31 @@ export function composeRecallHandoff({
|
|
|
215
404
|
parts.push('', '## Tool results', ...toolLines);
|
|
216
405
|
}
|
|
217
406
|
parts.push('', '## Working files');
|
|
218
|
-
|
|
407
|
+
const groups = Array.isArray(workingFiles)
|
|
408
|
+
? {
|
|
409
|
+
modified: [],
|
|
410
|
+
referenced: workingFiles.map((entry) => (
|
|
411
|
+
typeof entry === 'string' ? { path: entry, seenAt: null } : entry
|
|
412
|
+
)),
|
|
413
|
+
}
|
|
414
|
+
: {
|
|
415
|
+
modified: Array.isArray(workingFiles?.modified) ? workingFiles.modified : [],
|
|
416
|
+
referenced: Array.isArray(workingFiles?.referenced) ? workingFiles.referenced : [],
|
|
417
|
+
};
|
|
418
|
+
const formatEntry = (entry, modified) => {
|
|
419
|
+
const normalized = typeof entry === 'string' ? { path: entry } : entry;
|
|
420
|
+
const fields = [];
|
|
421
|
+
if (modified) fields.push(`editedAt=${normalized?.editedAt || 'unknown'}`);
|
|
422
|
+
fields.push(`seenAt=${normalized?.seenAt || normalized?.editedAt || 'unknown'}`);
|
|
423
|
+
return `- ${normalized?.path || ''} [${fields.join('; ')}]`;
|
|
424
|
+
};
|
|
425
|
+
if (groups.modified.length) {
|
|
426
|
+
parts.push('### Modified', ...groups.modified.map((entry) => formatEntry(entry, true)));
|
|
427
|
+
}
|
|
428
|
+
if (groups.referenced.length) {
|
|
429
|
+
parts.push('### Referenced', ...groups.referenced.map((entry) => formatEntry(entry, false)));
|
|
430
|
+
}
|
|
431
|
+
if (!groups.modified.length && !groups.referenced.length) parts.push('- (none)');
|
|
219
432
|
return parts.join('\n');
|
|
220
433
|
}
|
|
221
434
|
|
|
@@ -240,8 +453,48 @@ export function fitRecallHandoffText(text, maxTokens) {
|
|
|
240
453
|
candidate = `${prefix}${kept.join('\n')}${suffix}`;
|
|
241
454
|
}
|
|
242
455
|
if (estimateTokens(candidate) > cap && start >= lines.length) {
|
|
243
|
-
|
|
244
|
-
|
|
456
|
+
candidate = candidate.replace(/\n## Tool results\n[\s\S]*?(?=\n## Working files)/, '\n');
|
|
457
|
+
}
|
|
458
|
+
if (estimateTokens(candidate) <= cap) return candidate;
|
|
459
|
+
const rows = candidate.split('\n');
|
|
460
|
+
const referencedAt = rows.findIndex((line) => line.trim() === '### Referenced');
|
|
461
|
+
if (referencedAt < 0) return candidate;
|
|
462
|
+
let referencedEnd = referencedAt + 1;
|
|
463
|
+
while (referencedEnd < rows.length && !/^#{2,3}\s+/.test(rows[referencedEnd].trim())) {
|
|
464
|
+
referencedEnd += 1;
|
|
465
|
+
}
|
|
466
|
+
const references = rows
|
|
467
|
+
.slice(referencedAt + 1, referencedEnd)
|
|
468
|
+
.filter((line) => /^-\s+/.test(line) && !/^\-\s+\+\d+\s+omitted$/.test(line));
|
|
469
|
+
const prefixRows = rows.slice(0, referencedAt + 1);
|
|
470
|
+
const suffixRows = rows.slice(referencedEnd);
|
|
471
|
+
let lo = 0;
|
|
472
|
+
let hi = references.length;
|
|
473
|
+
let best = -1;
|
|
474
|
+
while (lo <= hi) {
|
|
475
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
476
|
+
const omitted = references.length - mid;
|
|
477
|
+
const next = [
|
|
478
|
+
...prefixRows,
|
|
479
|
+
...references.slice(0, mid),
|
|
480
|
+
...(omitted > 0 ? [`- +${omitted} omitted`] : []),
|
|
481
|
+
...suffixRows,
|
|
482
|
+
].join('\n');
|
|
483
|
+
if (estimateTokens(next) <= cap) {
|
|
484
|
+
best = mid;
|
|
485
|
+
lo = mid + 1;
|
|
486
|
+
} else {
|
|
487
|
+
hi = mid - 1;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (best >= 0) {
|
|
491
|
+
const omitted = references.length - best;
|
|
492
|
+
return [
|
|
493
|
+
...prefixRows,
|
|
494
|
+
...references.slice(0, best),
|
|
495
|
+
...(omitted > 0 ? [`- +${omitted} omitted`] : []),
|
|
496
|
+
...suffixRows,
|
|
497
|
+
].join('\n');
|
|
245
498
|
}
|
|
246
499
|
return candidate;
|
|
247
500
|
}
|
|
@@ -51,11 +51,12 @@ import {
|
|
|
51
51
|
import { buildPostCompactFileAttachment } from './file-reattach.mjs';
|
|
52
52
|
import {
|
|
53
53
|
collectToolOutcomeLines,
|
|
54
|
-
|
|
54
|
+
collectWorkingFileGroups,
|
|
55
55
|
composeRecallHandoff,
|
|
56
56
|
fitRecallHandoffText,
|
|
57
57
|
conversationLinesFromMemoryText,
|
|
58
58
|
excludeTailFromConversation,
|
|
59
|
+
stripWorkingFileSections,
|
|
59
60
|
} from './handoff.mjs';
|
|
60
61
|
|
|
61
62
|
// Post-compact file re-attachment (claude-code parity): re-inject fresh reads
|
|
@@ -839,22 +840,38 @@ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
|
|
|
839
840
|
const recallRoom = (Number.isFinite(recallTokenCap) && recallTokenCap > 0)
|
|
840
841
|
? Math.min(recallRoomUncapped, Math.max(512, recallTokenCap - tailTokens))
|
|
841
842
|
: recallRoomUncapped;
|
|
842
|
-
const
|
|
843
|
-
const
|
|
843
|
+
const priorWithoutWorkingFiles = stripWorkingFileSections(recallFit.prior);
|
|
844
|
+
const priorLines = new Set(
|
|
845
|
+
priorWithoutWorkingFiles
|
|
846
|
+
.split('\n')
|
|
847
|
+
.map((line) => line.trim())
|
|
848
|
+
.filter(Boolean),
|
|
849
|
+
);
|
|
850
|
+
const toolLines = collectToolOutcomeLines(live)
|
|
851
|
+
.filter((line) => !priorLines.has(String(line || '').trim()));
|
|
852
|
+
const workingFiles = collectWorkingFileGroups(live, undefined, {
|
|
853
|
+
cwd: opts.cwd,
|
|
854
|
+
previousSummary: recallFit.prior,
|
|
855
|
+
now: Date.now(),
|
|
856
|
+
});
|
|
844
857
|
const conversationLines = excludeTailFromConversation(
|
|
845
858
|
conversationLinesFromMemoryText(recallFit.recall),
|
|
846
859
|
recallTail,
|
|
847
|
-
);
|
|
860
|
+
).filter((line) => !priorLines.has(String(line || '').trim()));
|
|
848
861
|
const composedRecall = composeRecallHandoff({
|
|
849
862
|
sessionId: opts.sessionId || '',
|
|
850
863
|
conversationLines,
|
|
851
864
|
toolLines,
|
|
852
865
|
workingFiles,
|
|
853
866
|
});
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
867
|
+
const priorRoom = priorWithoutWorkingFiles
|
|
868
|
+
? Math.max(256, Math.floor((recallRoom - 200) * 0.55))
|
|
869
|
+
: 0;
|
|
870
|
+
const currentRoom = Math.max(256, recallRoom - priorRoom - 200);
|
|
871
|
+
const fittedRecall = fitRecallHandoffText(composedRecall, currentRoom);
|
|
872
|
+
const priorPart = priorWithoutWorkingFiles
|
|
873
|
+
? fitRecallHandoffText(priorWithoutWorkingFiles, priorRoom)
|
|
874
|
+
: '';
|
|
858
875
|
const summaryMessage = fitRecallFastTrackSummaryMessage(
|
|
859
876
|
oldHistory,
|
|
860
877
|
fittedRecall,
|
|
@@ -911,7 +928,7 @@ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
|
|
|
911
928
|
recallEmpty: !recallFit.recall,
|
|
912
929
|
priorEmpty: !recallFit.prior,
|
|
913
930
|
recallTruncatedInSummary: !!recallFit.recall && !summaryContent.includes(recallFit.recall),
|
|
914
|
-
priorTruncatedInSummary: !!
|
|
931
|
+
priorTruncatedInSummary: !!priorPart && !summaryContent.includes(priorPart),
|
|
915
932
|
tailTruncated: recallTail.some((m) => messageContentHasMarker(m, RECALL_TAIL_TRUNCATION_MARKER) || messageContentHasMarker(m, RECALL_TAIL_SHORT_TRUNCATION_MARKER)),
|
|
916
933
|
fileReattached: reattach.reattached,
|
|
917
934
|
tailOptions: recallTailOpts,
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
import { normalizeOutputPath } from './path-utils.mjs';
|
|
62
62
|
import { normalizeErrorMessage } from './path-diagnostics.mjs';
|
|
63
63
|
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
64
|
-
import { scrubLoaderVars, scrubProviderSecrets, scrubRuntimeRootVars } from '../env-scrub.mjs';
|
|
64
|
+
import { applyShellEgressPolicy, scrubLoaderVars, scrubProviderSecrets, scrubRuntimeRootVars } from '../env-scrub.mjs';
|
|
65
65
|
import { resourceAdmission } from '../../../../shared/resource-admission.mjs';
|
|
66
66
|
import {
|
|
67
67
|
findPathExecutable,
|
|
@@ -549,6 +549,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
549
549
|
scrubProviderSecrets(spawnEnv);
|
|
550
550
|
scrubLoaderVars(spawnEnv);
|
|
551
551
|
scrubRuntimeRootVars(spawnEnv);
|
|
552
|
+
applyShellEgressPolicy(spawnEnv);
|
|
552
553
|
let wrappedCommand;
|
|
553
554
|
let _teePlan = null;
|
|
554
555
|
let execShell = shell;
|
|
@@ -108,7 +108,14 @@ export function describeShellStartupPolicy({
|
|
|
108
108
|
} = {}) {
|
|
109
109
|
const { available, unavailable } = detectPathCapabilities(pathOptions);
|
|
110
110
|
const resolvedShell = shell || basename(String(resolveShellFor('default')?.shell || 'unknown'));
|
|
111
|
-
|
|
111
|
+
// Session-variable capability line: when the process-level egress block
|
|
112
|
+
// is active (mixdog exec default pins MIXDOG_FEATURE_WEB_SEARCH=0 and
|
|
113
|
+
// shell children get a dead proxy), say so here — the model must not
|
|
114
|
+
// spend calls attempting web access that is guaranteed to fail.
|
|
115
|
+
const network = String(process.env.MIXDOG_FEATURE_WEB_SEARCH || '') === '0'
|
|
116
|
+
? ' network=offline (external egress blocked for every command — curl/wget/pip/apt/git included; only loopback services are reachable; never attempt web access).'
|
|
117
|
+
: '';
|
|
118
|
+
return `- Shell startup environment: OS=${os}; shell=${resolvedShell}; available=${available.join(', ') || 'none'}; unavailable=${unavailable.join(', ') || 'none'}.${network} For shell commands, treat every unavailable entry as absent. Invoke one only if the same command first installs it or exposes it on PATH.`;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
export function appendShellStartupPolicy(rules, tools, options = {}) {
|
|
@@ -75,6 +75,33 @@ const SECRET_EXACT = new Set([
|
|
|
75
75
|
export function scrubLoaderVars(env) {
|
|
76
76
|
if (!env || typeof env !== 'object') return env;
|
|
77
77
|
for (const k of LOADER_VARS) delete env[k];
|
|
78
|
+
return _continueScrubLoaderVars(env);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Headless "web off" reaches shell children too: `mixdog exec` pins
|
|
82
|
+
// MIXDOG_FEATURE_WEB_SEARCH=0 (default; --web-search opts back in) and every
|
|
83
|
+
// model-spawned subprocess then receives the standard proxy variables pointed
|
|
84
|
+
// at a dead loopback endpoint. curl/wget/git-http/pip/apt honor these and
|
|
85
|
+
// fail fast on public hosts, while NO_PROXY keeps loopback traffic
|
|
86
|
+
// (task-local servers) working. Raw-socket clients bypass proxy variables —
|
|
87
|
+
// this is a uniform egress policy for the common tooling path, not a sandbox.
|
|
88
|
+
// The runtime's own provider calls are unaffected: this mutates only the
|
|
89
|
+
// child spawn env, never process.env.
|
|
90
|
+
const SHELL_EGRESS_DEAD_PROXY = 'http://127.0.0.1:1';
|
|
91
|
+
const SHELL_EGRESS_PROXY_VARS = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'FTP_PROXY', 'RSYNC_PROXY'];
|
|
92
|
+
export function applyShellEgressPolicy(env) {
|
|
93
|
+
if (!env || typeof env !== 'object') return env;
|
|
94
|
+
if (String(process.env.MIXDOG_FEATURE_WEB_SEARCH ?? '') !== '0') return env;
|
|
95
|
+
for (const name of SHELL_EGRESS_PROXY_VARS) {
|
|
96
|
+
env[name] = SHELL_EGRESS_DEAD_PROXY;
|
|
97
|
+
env[name.toLowerCase()] = SHELL_EGRESS_DEAD_PROXY;
|
|
98
|
+
}
|
|
99
|
+
env.NO_PROXY = 'localhost,127.0.0.1,::1';
|
|
100
|
+
env.no_proxy = env.NO_PROXY;
|
|
101
|
+
return env;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _continueScrubLoaderVars(env) {
|
|
78
105
|
// Wildcard sweep: the exact-name list covers the common loader vars but
|
|
79
106
|
// the DYLD_/LD_ families have many siblings (DYLD_FRAMEWORK_PATH,
|
|
80
107
|
// DYLD_FALLBACK_LIBRARY_PATH, LD_AUDIT, LD_BIND_NOW, …). Delete every
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
2
|
+
"version": "0.1.11",
|
|
3
3
|
"_comment": "Synced from immutable graph-v release assets.",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
7
|
-
"sha256": "
|
|
6
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.11/mixdog-graph-darwin-arm64",
|
|
7
|
+
"sha256": "b606149747aa4106f3648d07d16b014f4c7c4b47d5b526eb29832aa3dba01180"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
11
|
-
"sha256": "
|
|
10
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.11/mixdog-graph-darwin-x64",
|
|
11
|
+
"sha256": "196f10b3d4216995efc41a96ab38c09c0e27676b14f459e34e30990bb35f2c68"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
15
|
-
"sha256": "
|
|
14
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.11/mixdog-graph-linux-arm64",
|
|
15
|
+
"sha256": "70a1e0c591db4a3a96070cf490c412186f84e9897690307935679a55ae8f69b8"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
19
|
-
"sha256": "
|
|
18
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.11/mixdog-graph-linux-x64",
|
|
19
|
+
"sha256": "0936a3dc6d9ee4a1e97fe57f1ac5f1f62db89435cc199c6303c9fce3e1f29f05"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.11/mixdog-graph-win32-x64.exe",
|
|
23
|
+
"sha256": "8d63891c2e15c13f894023f59994a45e54f64cca72c7098cc09069dce700bd43"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|