diff options
Diffstat (limited to 'modules/core/module-aiassist.js')
| -rw-r--r-- | modules/core/module-aiassist.js | 44 |
1 files changed, 39 insertions, 5 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 09551c2..0a0ea71 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -162,7 +162,7 @@ buildSystemPrompt() { parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, offset: { type: 'integer', description: 'Start character offset (default 0)' }, - limit: { type: 'integer', description: 'Max characters per page (default 8000). The effective page size follows the current model\'s budget and is capped again before the result enters context — the response carries returned_chars/total_chars/truncated/next_offset, page on those' } + limit: { type: 'integer', description: 'Max characters per page (defaults to the active model page budget, ~12k for a large model). A page is never larger than the result clip, so next_offset is always the next unseen character and is capped again before the result enters context — the response carries returned_chars/total_chars/truncated/next_offset, page on those' } }, required: ['path'] } } }, { @@ -1449,12 +1449,28 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re try { const obj = JSON.parse(text); if (obj && typeof obj.content === 'string' && obj.content.length > 0) { - const keep = Math.max(500, max - 200); // headroom for the other keys + // headroom for the other keys, and a whole character (never half a + // surrogate pair, which would make JSON.stringify emit a bad body) + let keep = Math.max(500, max - 200); + if (keep < obj.content.length) { + const c = obj.content.charCodeAt(keep - 1); + if (c >= 0xD800 && c <= 0xDBFF) keep -= 1; + } + const hadMore = (obj.next_offset !== null && obj.next_offset !== undefined); + const full = obj.content.length; obj.returned_chars = keep; + obj.returned = keep; obj.content = obj.content.slice(0, keep) + - '\n…[clipped: ' + (obj.content.length - keep) + ' of ' + obj.content.length + + '\n…[clipped: ' + (full - keep) + ' of ' + full + ' chars withheld — re-read with offset/limit if you need the rest]'; obj.clipped = true; + // A page that was already the last one has no next_offset to keep. If + // the tail is removed anyway, the caller must still be told there is + // more, or it concludes it has the whole file (measured 2026-11-02). + if (!hadMore) { + obj.next_offset = (Number(obj.offset) || 0) + keep; + obj.truncated = true; + } return JSON.stringify(obj); } } catch (e) { /* not JSON → fall through to the string clip */ } @@ -2581,7 +2597,13 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re const total = content.length; let offset = Math.max(0, Math.min(parseInt(a.offset, 10) || 0, total)); const pageCap = Math.max(500, Math.min(this._toolLimit('readPageChars'), 200000)); - const limit = Math.max(500, Math.min(parseInt(a.limit, 10) || 8000, pageCap)); + // A page bigger than the result budget gets clipped on the way out, + // and a clipped page's tail is invisible to the model. Cap the page + // at what the clip will keep (200 = headroom for the envelope keys) + // so a page is always shown whole and next_offset stays contiguous. + const resultRoom = Math.max(500, this._toolLimit('toolResultChars') - 200); + const defaultPage = Math.min(pageCap, resultRoom); + const limit = Math.max(500, Math.min(parseInt(a.limit, 10) || defaultPage, pageCap, resultRoom)); // Never split a surrogate pair (emoji/CJK-ext) across pages: // a lone surrogate makes JSON.stringify emit an invalid body. if (offset > 0) { @@ -2928,7 +2950,19 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re if (!orch) return JSON.stringify({ error: 'Orchestrator not available' }); try { const result = await orch.delegateTask(task); - return JSON.stringify({ success: true, task: task.substring(0, 80), result: typeof result === 'string' ? result.substring(0, 500) : 'completed' }); + // undefined/null/{} must all read as "no answer" — JSON.stringify(undefined) + // is undefined, and JSON.stringify({}) is a truthy '{}'. + const answer = typeof result === 'string' + ? result + : ((result && typeof result.content === 'string') ? result.content : ''); + // Never invent "completed": an empty answer is reported as a failure + // so the caller (model or human) knows nothing came back. + return JSON.stringify({ + success: !!answer, + task: task.substring(0, 80), + result: answer ? answer.substring(0, 500) : null, + error: answer ? undefined : 'worker returned no answer' + }); } catch (e) { return JSON.stringify({ error: 'Delegation failed: ' + e.message }); } |