aboutsummaryrefslogtreecommitdiff
path: root/modules/core/core-bundle.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/core-bundle.js')
-rw-r--r--modules/core/core-bundle.js93
1 files changed, 70 insertions, 23 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 4a2416c..c4bf64e 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -8218,10 +8218,10 @@ buildSystemPrompt() {
}
} catch (e) {}
base += '\n\n[File reads, tools, verification]\n' +
- '- read_file returns at most 8000 chars. If the reply says truncated=true, call read_file again with offset=<next_offset> before concluding anything about the file.\n' +
+ '- read_file returns 8000 chars by default (limit up to 16000). If the reply says truncated=true, call read_file again with offset=<next_offset> before concluding anything about the file.\n' +
'- Prefer targeted reads over whole files: run_command with grep -n "<pattern>" <file>, sed -n \'10,40p\' <file>, head -40 <file>, tail -40 <file>, wc -l <file>.\n' +
'- sed without -i is read-only and allowed; sed -i, rm, mv and writes need user confirmation.\n' +
- '- After ANY write or edit: re-read the changed lines, then run a check (test, node scripts/lint-no-undef.py, or a curl) and say in one line what you verified.\n' +
+ '- After ANY write or edit: re-read the changed lines (cat or sed -n) and run a check that exists in THIS terminal — `test` for the suite, `grep -n` to confirm the new line, `wc -l` for size. There is no shell, no node and no curl here.\n' +
'- If a tool fails twice, change approach (different tool, smaller scope) instead of retrying the same call.';
return base;
}
@@ -9006,17 +9006,20 @@ buildSystemPrompt() {
}
} catch (e) { /* world model not available */ }
- loopResult = await this.agenticLoop(messages, opts);
+ const _traceTag = opts && opts.agentTag ? opts.agentTag : 'main';
+ try {
+ loopResult = await this.agenticLoop(messages, opts);
+ } catch (_loopErr) {
+ this._flushToolTrace(sess, _traceTag); // never leave stale notes behind
+ throw _loopErr;
+ }
sess.context.push({ role: 'user', content: userMessage });
if (loopResult.finalAnswer) {
sess.context.push({ role: 'assistant', content: loopResult.finalAnswer });
}
// Durable trace of what was read/run this turn (2026-10-21) — without this,
// every later turn started blind and the model re-read the same files.
- if (this._turnObservations && this._turnObservations.length) {
- sess.context.push({ role: 'assistant', content: '⚙️ tool trace: ' + this._turnObservations.join(' | ') });
- }
- this._turnObservations = [];
+ this._flushToolTrace(sess, _traceTag);
if (sess.context.length > this.maxContext * 2) {
sess.context.splice(0, sess.context.length - this.maxContext * 2);
}
@@ -9381,15 +9384,43 @@ buildSystemPrompt() {
}
// 2026-10-21: tool results lived only inside the current turn, so the next turn
// had no idea what the model had already read or run. Keep a compact per-turn
- // trace and persist it into the session context (see the turn-end push).
- _noteObservation(name, args, result) {
+ // trace (per agent tag) and persist it into the session context.
+ // Review-hardened: no file bodies, no untrusted web output, no secrets.
+ _noteObservation(name, args, result, tag) {
try {
- if (!this._turnObservations) this._turnObservations = [];
- if (this._turnObservations.length >= 12) return;
- const argStr = (typeof args === 'object' && args !== null) ? JSON.stringify(args) : String(args || '');
- const note = (name + '(' + argStr + ') → ' + String(result === undefined || result === null ? '' : result))
- .replace(/\s+/g, ' ').trim();
- this._turnObservations.push(note.slice(0, 240));
+ if (/^(web_search|web_fetch|scrape|fetch)$/.test(name)) return; // injection surface
+ const key = tag || 'main';
+ if (!this._turnObservations) this._turnObservations = {};
+ const buf = this._turnObservations[key] || (this._turnObservations[key] = []);
+ const clean = s => String(s === undefined || s === null ? '' : s)
+ .replace(/\s+/g, ' ')
+ .replace(/(token|key|secret|password|bearer)[^,}\s]*/ig, '$1:[redacted]');
+ let argStr = (typeof args === 'object' && args !== null) ? JSON.stringify(args) : String(args || '');
+ argStr = clean(argStr).slice(0, 80);
+ let note;
+ if (name === 'read_file') {
+ let p = {};
+ try { p = JSON.parse(String(result)) || {}; } catch (e) {}
+ note = 'read_file ' + (p.path || argStr) + ' [' + (p.offset || 0) + '+' + (p.returned || 0) +
+ '/' + (p.total_chars === undefined ? '?' : p.total_chars) +
+ (p.truncated ? ', next=' + p.next_offset : '') + ']';
+ } else {
+ note = name + '(' + argStr + ') → ' + clean(String(result)).slice(0, 120);
+ }
+ buf.push(note.slice(0, 240));
+ // Ring buffer: keep the first 2 (the goal) and the last 10 (verification).
+ if (buf.length > 12) buf.splice(2, 1);
+ } catch (_e) {}
+ }
+ // Push this turn's trace into the session context and clear the buffer.
+ _flushToolTrace(sess, tag) {
+ try {
+ const key = tag || 'main';
+ const buf = (this._turnObservations && this._turnObservations[key]) || [];
+ if (buf.length && sess && sess.context) {
+ sess.context.push({ role: 'system', content: '⚙️ tool trace: ' + buf.join(' | ') });
+ }
+ if (this._turnObservations) delete this._turnObservations[key];
} catch (_e) {}
}
_escapeHtml(str) {
@@ -9608,9 +9639,14 @@ buildSystemPrompt() {
for (const tc of res.tool_calls) {
const sig = tc.function.name + ':' + (tc.function.arguments || '');
let entry = _seenCalls.get(sig);
- if (!entry || (now - entry.ts) > _loopWindow || entry.epoch !== this._toolEpoch) {
- // A mutation since the last attempt invalidates the block: re-run instead of replaying.
+ if (!entry || (now - entry.ts) > _loopWindow) {
entry = { count: 0, ts: now, resultHash: null, blocked: false, lastResult: null, epoch: this._toolEpoch };
+ } else if (entry.epoch !== this._toolEpoch) {
+ // A mutation since the last attempt unblocks the replay, but the
+ // repeat counter must survive: resetting it let an identical
+ // mutating call loop forever (review 2026-10-21).
+ entry.epoch = this._toolEpoch;
+ entry.blocked = false;
}
entry.count++;
entry.ts = now;
@@ -9739,7 +9775,7 @@ buildSystemPrompt() {
}
} catch (_e) {}
messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
- this._noteObservation(fn.name, fn.arguments, toolResult);
+ this._noteObservation(fn.name, fn.arguments, toolResult, _agentTag);
}
// Only block on destructive tools
if (destructiveTools.length > 0) {
@@ -9814,11 +9850,12 @@ buildSystemPrompt() {
}
} catch (_e) {}
messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
- this._noteObservation(fn.name, fn.arguments, toolResult);
+ this._noteObservation(fn.name, fn.arguments, toolResult, _agentTag);
}
} else {
messages.push({
role: 'tool',
+ tool_call_id: (destructiveTools[0] && destructiveTools[0].id) || undefined,
content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied by user. Non-destructive actions already executed.`
});
}
@@ -10436,9 +10473,19 @@ buildSystemPrompt() {
// bigger file unreadable — the model could neither finish it nor
// resume. Now the caller pages with offset/limit and gets next_offset.
const total = content.length;
- const offset = Math.max(0, Math.min(parseInt(a.offset, 10) || 0, total));
- const limit = Math.max(1000, Math.min(parseInt(a.limit, 10) || 8000, 32000));
- const slice = content.slice(offset, offset + limit);
+ let offset = Math.max(0, Math.min(parseInt(a.offset, 10) || 0, total));
+ const limit = Math.max(500, Math.min(parseInt(a.limit, 10) || 8000, 16000));
+ // Never split a surrogate pair (emoji/CJK-ext) across pages:
+ // a lone surrogate makes JSON.stringify emit an invalid body.
+ if (offset > 0) {
+ const prev = content.charCodeAt(offset - 1);
+ if (prev >= 0xD800 && prev <= 0xDBFF) offset += 1;
+ }
+ let slice = content.slice(offset, offset + limit);
+ if (slice.length && slice.charCodeAt(slice.length - 1) >= 0xD800 &&
+ slice.charCodeAt(slice.length - 1) <= 0xDBFF) {
+ slice = slice.slice(0, -1);
+ }
const nextOffset = offset + slice.length < total ? offset + slice.length : null;
return JSON.stringify({
path: path, offset: offset, returned: slice.length,
@@ -12010,7 +12057,7 @@ buildSystemPrompt() {
(m.role === 'user' ? '👤' : m.role === 'assistant' ? '🤖' : '⚙️') + ': ' +
(m.content || '').substring(0, 400)
).join(' | ');
- let compressed = `[Earlier context (${oldMsgs.length} messages): ${summaryText}]`;
+ let compressed = `[Earlier context (${oldMsgs.length} messages): ${summaryText.slice(0, 4000)}]`;
// Tool traces must survive compression, so the AI one-sentence summary is only
// used when there are none to lose (2026-10-21).
const _traces = oldMsgs.filter(m => /^⚙️ tool trace:/.test(m.content || ''));