📏 fix: Let activityCharLimit Reach Tool Inputs

Round-fifteen review: `activityCharLimit` is documented as the
per-entry truncation for tool input AND output, but `buildPrompt`
hard-coded inputs at 200 characters — so raising the setting could
never surface a distinguishing path, query, or operation that appears
past the first 200 characters of a long argument. Inputs now truncate
at the configured limit alongside outputs; the 200-char constant
remains only for the intent line (renamed INTENT_CHAR_LIMIT to match).
Config fidelity pinned in runtime.spec: a 400-char argument survives a
450 limit and truncates under a 50 limit.

The round's other finding is the fifth restatement of the documented
edited+reconnect index-space limitation, answered on-thread with the
prior four cross-references.
This commit is contained in:
Danny Avila 2026-07-28 15:04:42 -04:00
parent b28d8b127f
commit 1fbc1d405b
2 changed files with 43 additions and 3 deletions

View file

@ -139,6 +139,42 @@ describe('buildPrompt', () => {
expect(line).toContain('x'.repeat(100));
});
/** `activityCharLimit` is documented as the per-entry limit for tool input
* AND output a hard-coded input cap would make the setting unable to
* reach a distinguishing path or query past the first 200 characters. */
it('applies the configured charLimit to tool inputs, not a hard-coded cap', () => {
const longQuery = 'q'.repeat(400);
const prompt = buildPrompt(
[
{
toolName: 'search',
toolInput: { query: longQuery },
toolUseId: 'a',
status: 'success',
toolOutput: 'ok',
},
],
450,
);
/** 400-char argument survives intact under a 450 limit (the old 200-char
* cap would have cut it), while a tighter limit still truncates. */
expect(prompt).toContain(longQuery);
const tight = buildPrompt(
[
{
toolName: 'search',
toolInput: { query: longQuery },
toolUseId: 'a',
status: 'success',
toolOutput: 'ok',
},
],
50,
);
expect(tight).not.toContain('q'.repeat(60));
expect(tight).toContain('…');
});
it('serializes small structured values exactly like JSON.stringify', () => {
const toolInput = { q: 'docs', filters: { lang: 'en', page: 2 }, ids: [1, 2] };
const prompt = buildPrompt(

View file

@ -167,7 +167,11 @@ export interface ActivityLabelHookOptions {
const DEFAULT_MAX_PER_RUN = 20;
const DEFAULT_CHAR_LIMIT = 600;
const INPUT_CHAR_LIMIT = 200;
/** Intent-line truncation only. Tool inputs and outputs both truncate at the
* configured `activityCharLimit` the schema documents it as the per-entry
* limit for BOTH, so a hard-coded input cap would make the setting unable to
* reach a distinguishing path or query past the first 200 characters. */
const INTENT_CHAR_LIMIT = 200;
const SUMMARY_TIMEOUT_MS = 12_000;
/** Hard bound on the PERSISTED label. The instruction asks for 49 words, but
* a model that ignores it or is steered by injection through untrusted
@ -331,7 +335,7 @@ export function buildPrompt(
const sections: string[] = [instruction ?? ACTIVITY_INSTRUCTION];
if (context?.lastAssistantText) {
sections.push(
`Intent (assistant's last message): ${truncate(context.lastAssistantText, INPUT_CHAR_LIMIT)}`,
`Intent (assistant's last message): ${truncate(context.lastAssistantText, INTENT_CHAR_LIMIT)}`,
);
}
if (context?.thinkingExcerpts?.length) {
@ -344,7 +348,7 @@ export function buildPrompt(
);
}
const lines = entries.map((entry) => {
const input = truncate(stringifyUnknown(entry.toolInput, INPUT_CHAR_LIMIT), INPUT_CHAR_LIMIT);
const input = truncate(stringifyUnknown(entry.toolInput, charLimit), charLimit);
const outcome =
entry.status === 'error'
? `ERROR: ${truncate(entry.error ?? 'unknown error', charLimit)}`