From fb221baa21250ae618000e79c9b4824d9ed1d12d Mon Sep 17 00:00:00 2001 From: pr0ln Date: Sat, 4 Apr 2026 10:24:14 +0900 Subject: [PATCH] Limit auto-mode classifier transcript growth (#277) * Limit auto-mode classifier transcript growth * Release persisted tool results from transcript state --------- Co-authored-by: pr0ln --- .../UserToolSuccessMessage.tsx | 51 +++- src/utils/permissions/yoloClassifier.test.ts | 79 +++++ src/utils/permissions/yoloClassifier.ts | 281 ++++++++++++------ src/utils/toolResultStorage.test.ts | 5 + src/utils/toolResultStorage.ts | 5 + 5 files changed, 332 insertions(+), 89 deletions(-) create mode 100644 src/utils/permissions/yoloClassifier.test.ts diff --git a/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx b/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx index 593061fc..06f42d6d 100644 --- a/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +++ b/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx @@ -7,7 +7,7 @@ import { useAppState } from '../../../state/AppState.js'; import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js'; import type { NormalizedUserMessage, ProgressMessage } from '../../../types/message.js'; import { deleteClassifierApproval, getClassifierApproval, getYoloClassifierApproval } from '../../../utils/classifierApprovals.js'; -import type { buildMessageLookups } from '../../../utils/messages.js'; +import { extractTag, type buildMessageLookups } from '../../../utils/messages.js'; import { MessageResponse } from '../../MessageResponse.js'; import { HookProgressMessage } from '../HookProgressMessage.js'; type Props = { @@ -49,8 +49,34 @@ export function UserToolSuccessMessage({ React.useEffect(() => { deleteClassifierApproval(toolUseID); }, [toolUseID]); + + const fallbackContent = React.useMemo(() => { + if (!Array.isArray(message.message.content)) return null; + const toolResultBlock = message.message.content.find(block => block.type === 'tool_result'); + if (!toolResultBlock || typeof toolResultBlock.content !== 'string') { + return null; + } + return extractTag(toolResultBlock.content, 'persisted-output') ?? toolResultBlock.content; + }, [message.message.content]); if (!message.toolUseResult || !tool) { - return null; + return fallbackContent ? + + {fallbackContent} + {feature('BASH_CLASSIFIER') ? classifierRule && + + {figures.tick} + {' Auto-approved · matched '} + {`"${classifierRule}"`} + + : null} + {feature('TRANSCRIPT_CLASSIFIER') ? yoloReason && + Allowed by auto mode classifier + : null} + + + + + : null; } // Resumed transcripts deserialize toolUseResult via raw JSON.parse with no @@ -59,7 +85,24 @@ export function UserToolSuccessMessage({ // Validate against outputSchema before rendering — mirrors CollapsedReadSearchContent. const parsedOutput = tool.outputSchema?.safeParse(message.toolUseResult); if (parsedOutput && !parsedOutput.success) { - return null; + return fallbackContent ? + + {fallbackContent} + {feature('BASH_CLASSIFIER') ? classifierRule && + + {figures.tick} + {' Auto-approved · matched '} + {`"${classifierRule}"`} + + : null} + {feature('TRANSCRIPT_CLASSIFIER') ? yoloReason && + Allowed by auto mode classifier + : null} + + + + + : null; } const toolResult = parsedOutput?.data ?? message.toolUseResult; const renderedMessage = tool.renderToolResultMessage?.(toolResult as never, filterToolProgressMessages(progressMessagesForMessage), { @@ -101,4 +144,4 @@ export function UserToolSuccessMessage({ ; } -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiZmlndXJlcyIsIlJlYWN0IiwiU2VudHJ5RXJyb3JCb3VuZGFyeSIsIkJveCIsIlRleHQiLCJ1c2VUaGVtZSIsInVzZUFwcFN0YXRlIiwiZmlsdGVyVG9vbFByb2dyZXNzTWVzc2FnZXMiLCJUb29sIiwiVG9vbHMiLCJOb3JtYWxpemVkVXNlck1lc3NhZ2UiLCJQcm9ncmVzc01lc3NhZ2UiLCJkZWxldGVDbGFzc2lmaWVyQXBwcm92YWwiLCJnZXRDbGFzc2lmaWVyQXBwcm92YWwiLCJnZXRZb2xvQ2xhc3NpZmllckFwcHJvdmFsIiwiYnVpbGRNZXNzYWdlTG9va3VwcyIsIk1lc3NhZ2VSZXNwb25zZSIsIkhvb2tQcm9ncmVzc01lc3NhZ2UiLCJQcm9wcyIsIm1lc3NhZ2UiLCJsb29rdXBzIiwiUmV0dXJuVHlwZSIsInRvb2xVc2VJRCIsInByb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlIiwic3R5bGUiLCJ0b29sIiwidG9vbHMiLCJ2ZXJib3NlIiwid2lkdGgiLCJpc1RyYW5zY3JpcHRNb2RlIiwiVXNlclRvb2xTdWNjZXNzTWVzc2FnZSIsIlJlYWN0Tm9kZSIsInRoZW1lIiwiaXNCcmllZk9ubHkiLCJzIiwiY2xhc3NpZmllclJ1bGUiLCJ1c2VTdGF0ZSIsInlvbG9SZWFzb24iLCJ1c2VFZmZlY3QiLCJ0b29sVXNlUmVzdWx0IiwicGFyc2VkT3V0cHV0Iiwib3V0cHV0U2NoZW1hIiwic2FmZVBhcnNlIiwic3VjY2VzcyIsInRvb2xSZXN1bHQiLCJkYXRhIiwicmVuZGVyZWRNZXNzYWdlIiwicmVuZGVyVG9vbFJlc3VsdE1lc3NhZ2UiLCJpbnB1dCIsInRvb2xVc2VCeVRvb2xVc2VJRCIsImdldCIsInJlbmRlcnNBc0Fzc2lzdGFudFRleHQiLCJ1c2VyRmFjaW5nTmFtZSIsInVuZGVmaW5lZCIsInRpY2siXSwic291cmNlcyI6WyJVc2VyVG9vbFN1Y2Nlc3NNZXNzYWdlLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBmZWF0dXJlIH0gZnJvbSAnYnVuOmJ1bmRsZSdcbmltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IFNlbnRyeUVycm9yQm91bmRhcnkgfSBmcm9tICdzcmMvY29tcG9uZW50cy9TZW50cnlFcnJvckJvdW5kYXJ5LmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0LCB1c2VUaGVtZSB9IGZyb20gJy4uLy4uLy4uL2luay5qcydcbmltcG9ydCB7IHVzZUFwcFN0YXRlIH0gZnJvbSAnLi4vLi4vLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQge1xuICBmaWx0ZXJUb29sUHJvZ3Jlc3NNZXNzYWdlcyxcbiAgdHlwZSBUb29sLFxuICB0eXBlIFRvb2xzLFxufSBmcm9tICcuLi8uLi8uLi9Ub29sLmpzJ1xuaW1wb3J0IHR5cGUge1xuICBOb3JtYWxpemVkVXNlck1lc3NhZ2UsXG4gIFByb2dyZXNzTWVzc2FnZSxcbn0gZnJvbSAnLi4vLi4vLi4vdHlwZXMvbWVzc2FnZS5qcydcbmltcG9ydCB7XG4gIGRlbGV0ZUNsYXNzaWZpZXJBcHByb3ZhbCxcbiAgZ2V0Q2xhc3NpZmllckFwcHJvdmFsLFxuICBnZXRZb2xvQ2xhc3NpZmllckFwcHJvdmFsLFxufSBmcm9tICcuLi8uLi8uLi91dGlscy9jbGFzc2lmaWVyQXBwcm92YWxzLmpzJ1xuaW1wb3J0IHR5cGUgeyBidWlsZE1lc3NhZ2VMb29rdXBzIH0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvbWVzc2FnZXMuanMnXG5pbXBvcnQgeyBNZXNzYWdlUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9NZXNzYWdlUmVzcG9uc2UuanMnXG5pbXBvcnQgeyBIb29rUHJvZ3Jlc3NNZXNzYWdlIH0gZnJvbSAnLi4vSG9va1Byb2dyZXNzTWVzc2FnZS5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgbWVzc2FnZTogTm9ybWFsaXplZFVzZXJNZXNzYWdlXG4gIGxvb2t1cHM6IFJldHVyblR5cGU8dHlwZW9mIGJ1aWxkTWVzc2FnZUxvb2t1cHM+XG4gIHRvb2xVc2VJRDogc3RyaW5nXG4gIHByb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlOiBQcm9ncmVzc01lc3NhZ2VbXVxuICBzdHlsZT86ICdjb25kZW5zZWQnXG4gIHRvb2w/OiBUb29sXG4gIHRvb2xzOiBUb29sc1xuICB2ZXJib3NlOiBib29sZWFuXG4gIHdpZHRoOiBudW1iZXIgfCBzdHJpbmdcbiAgaXNUcmFuc2NyaXB0TW9kZT86IGJvb2xlYW5cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFVzZXJUb29sU3VjY2Vzc01lc3NhZ2Uoe1xuICBtZXNzYWdlLFxuICBsb29rdXBzLFxuICB0b29sVXNlSUQsXG4gIHByb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlLFxuICBzdHlsZSxcbiAgdG9vbCxcbiAgdG9vbHMsXG4gIHZlcmJvc2UsXG4gIHdpZHRoLFxuICBpc1RyYW5zY3JpcHRNb2RlLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbdGhlbWVdID0gdXNlVGhlbWUoKVxuICAvLyBIb29rIHN0YXlzIGluc2lkZSBmZWF0dXJlKCkgdGVybmFyeSBzbyBleHRlcm5hbCBidWlsZHMgZG9uJ3QgcGF5IGFcbiAgLy8gcGVyLXNjcm9sbGJhY2stbWVzc2FnZSBzdG9yZSBzdWJzY3JpcHRpb24g4oCUIHNhbWUgcGF0dGVybiBhc1xuICAvLyBVc2VyUHJvbXB0TWVzc2FnZS50c3guXG4gIGNvbnN0IGlzQnJpZWZPbmx5ID1cbiAgICBmZWF0dXJlKCdLQUlST1MnKSB8fCBmZWF0dXJlKCdLQUlST1NfQlJJRUYnKVxuICAgICAgPyAvLyBiaW9tZS1pZ25vcmUgbGludC9jb3JyZWN0bmVzcy91c2VIb29rQXRUb3BMZXZlbDogZmVhdHVyZSgpIGlzIGEgY29tcGlsZS10aW1lIGNvbnN0YW50XG4gICAgICAgIHVzZUFwcFN0YXRlKHMgPT4gcy5pc0JyaWVmT25seSlcbiAgICAgIDogZmFsc2VcblxuICAvLyBDYXB0dXJlIGNsYXNzaWZpZXIgYXBwcm92YWwgb25jZSBvbiBtb3VudCwgdGhlbiBkZWxldGUgZnJvbSBNYXAgdG8gcHJldmVudCBsaW5lYXIgZ3Jvd3RoLlxuICAvLyB1c2VTdGF0ZSBsYXp5IGluaXRpYWxpemVyIGVuc3VyZXMgdGhlIHZhbHVlIHBlcnNpc3RzIGFjcm9zcyByZS1yZW5kZXJzLlxuICBjb25zdCBbY2xhc3NpZmllclJ1bGVdID0gUmVhY3QudXNlU3RhdGUoKCkgPT5cbiAgICBnZXRDbGFzc2lmaWVyQXBwcm92YWwodG9vbFVzZUlEKSxcbiAgKVxuICBjb25zdCBbeW9sb1JlYXNvbl0gPSBSZWFjdC51c2VTdGF0ZSgoKSA9PlxuICAgIGdldFlvbG9DbGFzc2lmaWVyQXBwcm92YWwodG9vbFVzZUlEKSxcbiAgKVxuICBSZWFjdC51c2VFZmZlY3QoKCkgPT4ge1xuICAgIGRlbGV0ZUNsYXNzaWZpZXJBcHByb3ZhbCh0b29sVXNlSUQpXG4gIH0sIFt0b29sVXNlSURdKVxuXG4gIGlmICghbWVzc2FnZS50b29sVXNlUmVzdWx0IHx8ICF0b29sKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIC8vIFJlc3VtZWQgdHJhbnNjcmlwdHMgZGVzZXJpYWxpemUgdG9vbFVzZVJlc3VsdCB2aWEgcmF3IEpTT04ucGFyc2Ugd2l0aCBub1xuICAvLyB2YWxpZGF0aW9uIChwYXJzZUpTT05MKS4gQSBwYXJ0aWFsL2NvcnJ1cHQvb2xkLWZvcm1hdCByZXN1bHQgY3Jhc2hlc1xuICAvLyByZW5kZXJUb29sUmVzdWx0TWVzc2FnZSBvbiBmaXJzdCBmaWVsZCBhY2Nlc3MgKGFudGhyb3BpY3MvY2xhdWRlLWNvZGUjMzk4MTcpLlxuICAvLyBWYWxpZGF0ZSBhZ2FpbnN0IG91dHB1dFNjaGVtYSBiZWZvcmUgcmVuZGVyaW5nIOKAlCBtaXJyb3JzIENvbGxhcHNlZFJlYWRTZWFyY2hDb250ZW50LlxuICBjb25zdCBwYXJzZWRPdXRwdXQgPSB0b29sLm91dHB1dFNjaGVtYT8uc2FmZVBhcnNlKG1lc3NhZ2UudG9vbFVzZVJlc3VsdClcbiAgaWYgKHBhcnNlZE91dHB1dCAmJiAhcGFyc2VkT3V0cHV0LnN1Y2Nlc3MpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG4gIGNvbnN0IHRvb2xSZXN1bHQgPSBwYXJzZWRPdXRwdXQ/LmRhdGEgPz8gbWVzc2FnZS50b29sVXNlUmVzdWx0XG5cbiAgY29uc3QgcmVuZGVyZWRNZXNzYWdlID1cbiAgICB0b29sLnJlbmRlclRvb2xSZXN1bHRNZXNzYWdlPy4oXG4gICAgICB0b29sUmVzdWx0IGFzIG5ldmVyLFxuICAgICAgZmlsdGVyVG9vbFByb2dyZXNzTWVzc2FnZXMocHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2UpLFxuICAgICAge1xuICAgICAgICBzdHlsZSxcbiAgICAgICAgdGhlbWUsXG4gICAgICAgIHRvb2xzLFxuICAgICAgICB2ZXJib3NlLFxuICAgICAgICBpc1RyYW5zY3JpcHRNb2RlLFxuICAgICAgICBpc0JyaWVmT25seSxcbiAgICAgICAgaW5wdXQ6IGxvb2t1cHMudG9vbFVzZUJ5VG9vbFVzZUlELmdldCh0b29sVXNlSUQpPy5pbnB1dCxcbiAgICAgIH0sXG4gICAgKSA/PyBudWxsXG5cbiAgLy8gRG9uJ3QgcmVuZGVyIGFueXRoaW5nIGlmIHRoZSB0b29sIHJlc3VsdCBtZXNzYWdlIGlzIG51bGxcbiAgaWYgKHJlbmRlcmVkTWVzc2FnZSA9PT0gbnVsbCkge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICAvLyBUb29scyB0aGF0IHJldHVybiAnJyBmcm9tIHVzZXJGYWNpbmdOYW1lIG9wdCBvdXQgb2YgdG9vbCBjaHJvbWUgYW5kXG4gIC8vIHJlbmRlciBsaWtlIHBsYWluIGFzc2lzdGFudCB0ZXh0LiBTa2lwIHRoZSB0b29sLXJlc3VsdCB3aWR0aCBjb25zdHJhaW50XG4gIC8vIHNvIE1hcmtkb3duVGFibGUncyBTQUZFVFlfTUFSR0lOPTQgKHR1bmVkIGZvciB0aGUgYXNzaXN0YW50LXRleHQgMi1jb2xcbiAgLy8gZG90IGd1dHRlcikgaG9sZHMg4oCUIG90aGVyd2lzZSB0YWJsZXMgd3JhcCB0aGVpciBib3gtZHJhd2luZyBjaGFycy5cbiAgY29uc3QgcmVuZGVyc0FzQXNzaXN0YW50VGV4dCA9IHRvb2wudXNlckZhY2luZ05hbWUodW5kZWZpbmVkKSA9PT0gJydcblxuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgPEJveFxuICAgICAgICBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCJcbiAgICAgICAgd2lkdGg9e3JlbmRlcnNBc0Fzc2lzdGFudFRleHQgPyB1bmRlZmluZWQgOiB3aWR0aH1cbiAgICAgID5cbiAgICAgICAge3JlbmRlcmVkTWVzc2FnZX1cbiAgICAgICAge2ZlYXR1cmUoJ0JBU0hfQ0xBU1NJRklFUicpXG4gICAgICAgICAgPyBjbGFzc2lmaWVyUnVsZSAmJiAoXG4gICAgICAgICAgICAgIDxNZXNzYWdlUmVzcG9uc2UgaGVpZ2h0PXsxfT5cbiAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgICAgIDxUZXh0IGNvbG9yPVwic3VjY2Vzc1wiPntmaWd1cmVzLnRpY2t9PC9UZXh0PlxuICAgICAgICAgICAgICAgICAgeycgQXV0by1hcHByb3ZlZCBcXHUwMGI3IG1hdGNoZWQgJ31cbiAgICAgICAgICAgICAgICAgIHtgXCIke2NsYXNzaWZpZXJSdWxlfVwiYH1cbiAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICAgICAgKVxuICAgICAgICAgIDogbnVsbH1cbiAgICAgICAge2ZlYXR1cmUoJ1RSQU5TQ1JJUFRfQ0xBU1NJRklFUicpXG4gICAgICAgICAgPyB5b2xvUmVhc29uICYmIChcbiAgICAgICAgICAgICAgPE1lc3NhZ2VSZXNwb25zZSBoZWlnaHQ9ezF9PlxuICAgICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPkFsbG93ZWQgYnkgYXV0byBtb2RlIGNsYXNzaWZpZXI8L1RleHQ+XG4gICAgICAgICAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICAgICAgKVxuICAgICAgICAgIDogbnVsbH1cbiAgICAgIDwvQm94PlxuICAgICAgPFNlbnRyeUVycm9yQm91bmRhcnk+XG4gICAgICAgIDxIb29rUHJvZ3Jlc3NNZXNzYWdlXG4gICAgICAgICAgaG9va0V2ZW50PVwiUG9zdFRvb2xVc2VcIlxuICAgICAgICAgIGxvb2t1cHM9e2xvb2t1cHN9XG4gICAgICAgICAgdG9vbFVzZUlEPXt0b29sVXNlSUR9XG4gICAgICAgICAgdmVyYm9zZT17dmVyYm9zZX1cbiAgICAgICAgICBpc1RyYW5zY3JpcHRNb2RlPXtpc1RyYW5zY3JpcHRNb2RlfVxuICAgICAgICAvPlxuICAgICAgPC9TZW50cnlFcnJvckJvdW5kYXJ5PlxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLFNBQVNBLE9BQU8sUUFBUSxZQUFZO0FBQ3BDLE9BQU9DLE9BQU8sTUFBTSxTQUFTO0FBQzdCLE9BQU8sS0FBS0MsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsbUJBQW1CLFFBQVEsdUNBQXVDO0FBQzNFLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxFQUFFQyxRQUFRLFFBQVEsaUJBQWlCO0FBQ3JELFNBQVNDLFdBQVcsUUFBUSw0QkFBNEI7QUFDeEQsU0FDRUMsMEJBQTBCLEVBQzFCLEtBQUtDLElBQUksRUFDVCxLQUFLQyxLQUFLLFFBQ0wsa0JBQWtCO0FBQ3pCLGNBQ0VDLHFCQUFxQixFQUNyQkMsZUFBZSxRQUNWLDJCQUEyQjtBQUNsQyxTQUNFQyx3QkFBd0IsRUFDeEJDLHFCQUFxQixFQUNyQkMseUJBQXlCLFFBQ3BCLHVDQUF1QztBQUM5QyxjQUFjQyxtQkFBbUIsUUFBUSw0QkFBNEI7QUFDckUsU0FBU0MsZUFBZSxRQUFRLDBCQUEwQjtBQUMxRCxTQUFTQyxtQkFBbUIsUUFBUSwyQkFBMkI7QUFFL0QsS0FBS0MsS0FBSyxHQUFHO0VBQ1hDLE9BQU8sRUFBRVQscUJBQXFCO0VBQzlCVSxPQUFPLEVBQUVDLFVBQVUsQ0FBQyxPQUFPTixt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 \ No newline at end of file +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiZmlndXJlcyIsIlJlYWN0IiwiU2VudHJ5RXJyb3JCb3VuZGFyeSIsIkJveCIsIlRleHQiLCJ1c2VUaGVtZSIsInVzZUFwcFN0YXRlIiwiZmlsdGVyVG9vbFByb2dyZXNzTWVzc2FnZXMiLCJUb29sIiwiVG9vbHMiLCJOb3JtYWxpemVkVXNlck1lc3NhZ2UiLCJQcm9ncmVzc01lc3NhZ2UiLCJkZWxldGVDbGFzc2lmaWVyQXBwcm92YWwiLCJnZXRDbGFzc2lmaWVyQXBwcm92YWwiLCJnZXRZb2xvQ2xhc3NpZmllckFwcHJvdmFsIiwiYnVpbGRNZXNzYWdlTG9va3VwcyIsIk1lc3NhZ2VSZXNwb25zZSIsIkhvb2tQcm9ncmVzc01lc3NhZ2UiLCJQcm9wcyIsIm1lc3NhZ2UiLCJsb29rdXBzIiwiUmV0dXJuVHlwZSIsInRvb2xVc2VJRCIsInByb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlIiwic3R5bGUiLCJ0b29sIiwidG9vbHMiLCJ2ZXJib3NlIiwid2lkdGgiLCJpc1RyYW5zY3JpcHRNb2RlIiwiVXNlclRvb2xTdWNjZXNzTWVzc2FnZSIsIlJlYWN0Tm9kZSIsInRoZW1lIiwiaXNCcmllZk9ubHkiLCJzIiwiY2xhc3NpZmllclJ1bGUiLCJ1c2VTdGF0ZSIsInlvbG9SZWFzb24iLCJ1c2VFZmZlY3QiLCJ0b29sVXNlUmVzdWx0IiwicGFyc2VkT3V0cHV0Iiwib3V0cHV0U2NoZW1hIiwic2FmZVBhcnNlIiwic3VjY2VzcyIsInRvb2xSZXN1bHQiLCJkYXRhIiwicmVuZGVyZWRNZXNzYWdlIiwicmVuZGVyVG9vbFJlc3VsdE1lc3NhZ2UiLCJpbnB1dCIsInRvb2xVc2VCeVRvb2xVc2VJRCIsImdldCIsInJlbmRlcnNBc0Fzc2lzdGFudFRleHQiLCJ1c2VyRmFjaW5nTmFtZSIsInVuZGVmaW5lZCIsInRpY2siXSwic291cmNlcyI6WyJVc2VyVG9vbFN1Y2Nlc3NNZXNzYWdlLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBmZWF0dXJlIH0gZnJvbSAnYnVuOmJ1bmRsZSdcbmltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IFNlbnRyeUVycm9yQm91bmRhcnkgfSBmcm9tICdzcmMvY29tcG9uZW50cy9TZW50cnlFcnJvckJvdW5kYXJ5LmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0LCB1c2VUaGVtZSB9IGZyb20gJy4uLy4uLy4uL2luay5qcydcbmltcG9ydCB7IHVzZUFwcFN0YXRlIH0gZnJvbSAnLi4vLi4vLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQge1xuICBmaWx0ZXJUb29sUHJvZ3Jlc3NNZXNzYWdlcyxcbiAgdHlwZSBUb29sLFxuICB0eXBlIFRvb2xzLFxufSBmcm9tICcuLi8uLi8uLi9Ub29sLmpzJ1xuaW1wb3J0IHR5cGUge1xuICBOb3JtYWxpemVkVXNlck1lc3NhZ2UsXG4gIFByb2dyZXNzTWVzc2FnZSxcbn0gZnJvbSAnLi4vLi4vLi4vdHlwZXMvbWVzc2FnZS5qcydcbmltcG9ydCB7XG4gIGRlbGV0ZUNsYXNzaWZpZXJBcHByb3ZhbCxcbiAgZ2V0Q2xhc3NpZmllckFwcHJvdmFsLFxuICBnZXRZb2xvQ2xhc3NpZmllckFwcHJvdmFsLFxufSBmcm9tICcuLi8uLi8uLi91dGlscy9jbGFzc2lmaWVyQXBwcm92YWxzLmpzJ1xuaW1wb3J0IHR5cGUgeyBidWlsZE1lc3NhZ2VMb29rdXBzIH0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvbWVzc2FnZXMuanMnXG5pbXBvcnQgeyBNZXNzYWdlUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9NZXNzYWdlUmVzcG9uc2UuanMnXG5pbXBvcnQgeyBIb29rUHJvZ3Jlc3NNZXNzYWdlIH0gZnJvbSAnLi4vSG9va1Byb2dyZXNzTWVzc2FnZS5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgbWVzc2FnZTogTm9ybWFsaXplZFVzZXJNZXNzYWdlXG4gIGxvb2t1cHM6IFJldHVyblR5cGU8dHlwZW9mIGJ1aWxkTWVzc2FnZUxvb2t1cHM+XG4gIHRvb2xVc2VJRDogc3RyaW5nXG4gIHByb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlOiBQcm9ncmVzc01lc3NhZ2VbXVxuICBzdHlsZT86ICdjb25kZW5zZWQnXG4gIHRvb2w/OiBUb29sXG4gIHRvb2xzOiBUb29sc1xuICB2ZXJib3NlOiBib29sZWFuXG4gIHdpZHRoOiBudW1iZXIgfCBzdHJpbmdcbiAgaXNUcmFuc2NyaXB0TW9kZT86IGJvb2xlYW5cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFVzZXJUb29sU3VjY2Vzc01lc3NhZ2Uoe1xuICBtZXNzYWdlLFxuICBsb29rdXBzLFxuICB0b29sVXNlSUQsXG4gIHByb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlLFxuICBzdHlsZSxcbiAgdG9vbCxcbiAgdG9vbHMsXG4gIHZlcmJvc2UsXG4gIHdpZHRoLFxuICBpc1RyYW5zY3JpcHRNb2RlLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbdGhlbWVdID0gdXNlVGhlbWUoKVxuICAvLyBIb29rIHN0YXlzIGluc2lkZSBmZWF0dXJlKCkgdGVybmFyeSBzbyBleHRlcm5hbCBidWlsZHMgZG9uJ3QgcGF5IGFcbiAgLy8gcGVyLXNjcm9sbGJhY2stbWVzc2FnZSBzdG9yZSBzdWJzY3JpcHRpb24g4oCUIHNhbWUgcGF0dGVybiBhc1xuICAvLyBVc2VyUHJvbXB0TWVzc2FnZS50c3guXG4gIGNvbnN0IGlzQnJpZWZPbmx5ID1cbiAgICBmZWF0dXJlKCdLQUlST1MnKSB8fCBmZWF0dXJlKCdLQUlST1NfQlJJRUYnKVxuICAgICAgPyAvLyBiaW9tZS1pZ25vcmUgbGludC9jb3JyZWN0bmVzcy91c2VIb29rQXRUb3BMZXZlbDogZmVhdHVyZSgpIGlzIGEgY29tcGlsZS10aW1lIGNvbnN0YW50XG4gICAgICAgIHVzZUFwcFN0YXRlKHMgPT4gcy5pc0JyaWVmT25seSlcbiAgICAgIDogZmFsc2VcblxuICAvLyBDYXB0dXJlIGNsYXNzaWZpZXIgYXBwcm92YWwgb25jZSBvbiBtb3VudCwgdGhlbiBkZWxldGUgZnJvbSBNYXAgdG8gcHJldmVudCBsaW5lYXIgZ3Jvd3RoLlxuICAvLyB1c2VTdGF0ZSBsYXp5IGluaXRpYWxpemVyIGVuc3VyZXMgdGhlIHZhbHVlIHBlcnNpc3RzIGFjcm9zcyByZS1yZW5kZXJzLlxuICBjb25zdCBbY2xhc3NpZmllclJ1bGVdID0gUmVhY3QudXNlU3RhdGUoKCkgPT5cbiAgICBnZXRDbGFzc2lmaWVyQXBwcm92YWwodG9vbFVzZUlEKSxcbiAgKVxuICBjb25zdCBbeW9sb1JlYXNvbl0gPSBSZWFjdC51c2VTdGF0ZSgoKSA9PlxuICAgIGdldFlvbG9DbGFzc2lmaWVyQXBwcm92YWwodG9vbFVzZUlEKSxcbiAgKVxuICBSZWFjdC51c2VFZmZlY3QoKCkgPT4ge1xuICAgIGRlbGV0ZUNsYXNzaWZpZXJBcHByb3ZhbCh0b29sVXNlSUQpXG4gIH0sIFt0b29sVXNlSURdKVxuXG4gIGlmICghbWVzc2FnZS50b29sVXNlUmVzdWx0IHx8ICF0b29sKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIC8vIFJlc3VtZWQgdHJhbnNjcmlwdHMgZGVzZXJpYWxpemUgdG9vbFVzZVJlc3VsdCB2aWEgcmF3IEpTT04ucGFyc2Ugd2l0aCBub1xuICAvLyB2YWxpZGF0aW9uIChwYXJzZUpTT05MKS4gQSBwYXJ0aWFsL2NvcnJ1cHQvb2xkLWZvcm1hdCByZXN1bHQgY3Jhc2hlc1xuICAvLyByZW5kZXJUb29sUmVzdWx0TWVzc2FnZSBvbiBmaXJzdCBmaWVsZCBhY2Nlc3MgKGFudGhyb3BpY3MvY2xhdWRlLWNvZGUjMzk4MTcpLlxuICAvLyBWYWxpZGF0ZSBhZ2FpbnN0IG91dHB1dFNjaGVtYSBiZWZvcmUgcmVuZGVyaW5nIOKAlCBtaXJyb3JzIENvbGxhcHNlZFJlYWRTZWFyY2hDb250ZW50LlxuICBjb25zdCBwYXJzZWRPdXRwdXQgPSB0b29sLm91dHB1dFNjaGVtYT8uc2FmZVBhcnNlKG1lc3NhZ2UudG9vbFVzZVJlc3VsdClcbiAgaWYgKHBhcnNlZE91dHB1dCAmJiAhcGFyc2VkT3V0cHV0LnN1Y2Nlc3MpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG4gIGNvbnN0IHRvb2xSZXN1bHQgPSBwYXJzZWRPdXRwdXQ/LmRhdGEgPz8gbWVzc2FnZS50b29sVXNlUmVzdWx0XG5cbiAgY29uc3QgcmVuZGVyZWRNZXNzYWdlID1cbiAgICB0b29sLnJlbmRlclRvb2xSZXN1bHRNZXNzYWdlPy4oXG4gICAgICB0b29sUmVzdWx0IGFzIG5ldmVyLFxuICAgICAgZmlsdGVyVG9vbFByb2dyZXNzTWVzc2FnZXMocHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2UpLFxuICAgICAge1xuICAgICAgICBzdHlsZSxcbiAgICAgICAgdGhlbWUsXG4gICAgICAgIHRvb2xzLFxuICAgICAgICB2ZXJib3NlLFxuICAgICAgICBpc1RyYW5zY3JpcHRNb2RlLFxuICAgICAgICBpc0JyaWVmT25seSxcbiAgICAgICAgaW5wdXQ6IGxvb2t1cHMudG9vbFVzZUJ5VG9vbFVzZUlELmdldCh0b29sVXNlSUQpPy5pbnB1dCxcbiAgICAgIH0sXG4gICAgKSA/PyBudWxsXG5cbiAgLy8gRG9uJ3QgcmVuZGVyIGFueXRoaW5nIGlmIHRoZSB0b29sIHJlc3VsdCBtZXNzYWdlIGlzIG51bGxcbiAgaWYgKHJlbmRlcmVkTWVzc2FnZSA9PT0gbnVsbCkge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICAvLyBUb29scyB0aGF0IHJldHVybiAnJyBmcm9tIHVzZXJGYWNpbmdOYW1lIG9wdCBvdXQgb2YgdG9vbCBjaHJvbWUgYW5kXG4gIC8vIHJlbmRlciBsaWtlIHBsYWluIGFzc2lzdGFudCB0ZXh0LiBTa2lwIHRoZSB0b29sLXJlc3VsdCB3aWR0aCBjb25zdHJhaW50XG4gIC8vIHNvIE1hcmtkb3duVGFibGUncyBTQUZFVFlfTUFSR0lOPTQgKHR1bmVkIGZvciB0aGUgYXNzaXN0YW50LXRleHQgMi1jb2xcbiAgLy8gZG90IGd1dHRlcikgaG9sZHMg4oCUIG90aGVyd2lzZSB0YWJsZXMgd3JhcCB0aGVpciBib3gtZHJhd2luZyBjaGFycy5cbiAgY29uc3QgcmVuZGVyc0FzQXNzaXN0YW50VGV4dCA9IHRvb2wudXNlckZhY2luZ05hbWUodW5kZWZpbmVkKSA9PT0gJydcblxuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgPEJveFxuICAgICAgICBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCJcbiAgICAgICAgd2lkdGg9e3JlbmRlcnNBc0Fzc2lzdGFudFRleHQgPyB1bmRlZmluZWQgOiB3aWR0aH1cbiAgICAgID5cbiAgICAgICAge3JlbmRlcmVkTWVzc2FnZX1cbiAgICAgICAge2ZlYXR1cmUoJ0JBU0hfQ0xBU1NJRklFUicpXG4gICAgICAgICAgPyBjbGFzc2lmaWVyUnVsZSAmJiAoXG4gICAgICAgICAgICAgIDxNZXNzYWdlUmVzcG9uc2UgaGVpZ2h0PXsxfT5cbiAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgICAgIDxUZXh0IGNvbG9yPVwic3VjY2Vzc1wiPntmaWd1cmVzLnRpY2t9PC9UZXh0PlxuICAgICAgICAgICAgICAgICAgeycgQXV0by1hcHByb3ZlZCBcXHUwMGI3IG1hdGNoZWQgJ31cbiAgICAgICAgICAgICAgICAgIHtgXCIke2NsYXNzaWZpZXJSdWxlfVwiYH1cbiAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICAgICAgKVxuICAgICAgICAgIDogbnVsbH1cbiAgICAgICAge2ZlYXR1cmUoJ1RSQU5TQ1JJUFRfQ0xBU1NJRklFUicpXG4gICAgICAgICAgPyB5b2xvUmVhc29uICYmIChcbiAgICAgICAgICAgICAgPE1lc3NhZ2VSZXNwb25zZSBoZWlnaHQ9ezF9PlxuICAgICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPkFsbG93ZWQgYnkgYXV0byBtb2RlIGNsYXNzaWZpZXI8L1RleHQ+XG4gICAgICAgICAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICAgICAgKVxuICAgICAgICAgIDogbnVsbH1cbiAgICAgIDwvQm94PlxuICAgICAgPFNlbnRyeUVycm9yQm91bmRhcnk+XG4gICAgICAgIDxIb29rUHJvZ3Jlc3NNZXNzYWdlXG4gICAgICAgICAgaG9va0V2ZW50PVwiUG9zdFRvb2xVc2VcIlxuICAgICAgICAgIGxvb2t1cHM9e2xvb2t1cHN9XG4gICAgICAgICAgdG9vbFVzZUlEPXt0b29sVXNlSUR9XG4gICAgICAgICAgdmVyYm9zZT17dmVyYm9zZX1cbiAgICAgICAgICBpc1RyYW5zY3JpcHRNb2RlPXtpc1RyYW5zY3JpcHRNb2RlfVxuICAgICAgICAvPlxuICAgICAgPC9TZW50cnlFcnJvckJvdW5kYXJ5PlxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLFNBQVNBLE9BQU8sUUFBUSxZQUFZO0FBQ3BDLE9BQU9DLE9BQU8sTUFBTSxTQUFTO0FBQzdCLE9BQU8sS0FBS0MsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsbUJBQW1CLFFBQVEsdUNBQXVDO0FBQzNFLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxFQUFFQyxRQUFRLFFBQVEsaUJBQWlCO0FBQ3JELFNBQVNDLFdBQVcsUUFBUSw0QkFBNEI7QUFDeEQsU0FDRUMsMEJBQTBCLEVBQzFCLEtBQUtDLElBQUksRUFDVCxLQUFLQyxLQUFLLFFBQ0wsa0JBQWtCO0FBQ3pCLGNBQ0VDLHFCQUFxQixFQUNyQkMsZUFBZSxRQUNWLDJCQUEyQjtBQUNsQyxTQUNFQyx3QkFBd0IsRUFDeEJDLHFCQUFxQixFQUNyQkMseUJBQXlCLFFBQ3BCLHVDQUF1QztBQUM5QyxjQUFjQyxtQkFBbUIsUUFBUSw0QkFBNEI7QUFDckUsU0FBU0MsZUFBZSxRQUFRLDBCQUEwQjtBQUMxRCxTQUFTQyxtQkFBbUIsUUFBUSwyQkFBMkI7QUFFL0QsS0FBS0MsS0FBSyxHQUFHO0VBQ1hDLE9BQU8sRUFBRVQscUJBQXFCO0VBQzlCVSxPQUFPLEVBQUVDLFVBQVUsQ0FBQyxPQUFPTixt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 diff --git a/src/utils/permissions/yoloClassifier.test.ts b/src/utils/permissions/yoloClassifier.test.ts new file mode 100644 index 00000000..cf97c5ad --- /dev/null +++ b/src/utils/permissions/yoloClassifier.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test } from 'bun:test' + +import { buildTranscriptForClassifier } from './yoloClassifier.js' + +const tools = [ + { + name: 'Bash', + aliases: [], + toAutoClassifierInput(input: Record) { + return String(input.command ?? '') + }, + }, +] as any + +describe('buildTranscriptForClassifier', () => { + test('keeps the most recent transcript entries within budget', () => { + const messages = [ + { + type: 'user', + message: { + content: 'old-user', + }, + }, + { + type: 'assistant', + message: { + content: [ + { + type: 'tool_use', + name: 'Bash', + input: { command: 'old-tool' }, + }, + ], + }, + }, + { + type: 'user', + message: { + content: 'new-user', + }, + }, + { + type: 'assistant', + message: { + content: [ + { + type: 'tool_use', + name: 'Bash', + input: { command: 'new-tool' }, + }, + ], + }, + }, + ] as any + + const transcript = buildTranscriptForClassifier(messages, tools, 32) + + expect(transcript).toContain('new-user') + expect(transcript).toContain('new-tool') + expect(transcript).not.toContain('old-user') + expect(transcript).not.toContain('old-tool') + }) + + test('truncates oversized user blocks before serialization', () => { + const messages = [ + { + type: 'user', + message: { + content: 'x'.repeat(40_000), + }, + }, + ] as any + + const transcript = buildTranscriptForClassifier(messages, tools) + + expect(transcript.length).toBeLessThan(33_000) + expect(transcript).toContain('[truncated ') + }) +}) diff --git a/src/utils/permissions/yoloClassifier.ts b/src/utils/permissions/yoloClassifier.ts index 77ee5187..ea7b9e40 100644 --- a/src/utils/permissions/yoloClassifier.ts +++ b/src/utils/permissions/yoloClassifier.ts @@ -68,6 +68,9 @@ const ANTHROPIC_PERMISSIONS_TEMPLATE: string = : '' /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */ +const MAX_CLASSIFIER_TRANSCRIPT_CHARS = 200_000 +const MAX_CLASSIFIER_BLOCK_VALUE_CHARS = 32_000 + function isUsingExternalPermissions(): boolean { if (process.env.USER_TYPE !== 'ant') return true const config = getFeatureValue_CACHED_MAY_BE_STALE( @@ -293,6 +296,64 @@ export type TranscriptEntry = { content: TranscriptBlock[] } +function messageToTranscriptEntry(msg: Message): TranscriptEntry | null { + if (msg.type === 'attachment' && msg.attachment.type === 'queued_command') { + const prompt = msg.attachment.prompt + let text: string | null = null + if (typeof prompt === 'string') { + text = prompt + } else if (Array.isArray(prompt)) { + text = + prompt + .filter( + (block): block is { type: 'text'; text: string } => + block.type === 'text', + ) + .map(block => block.text) + .join('\n') || null + } + return text === null + ? null + : { + role: 'user', + content: [{ type: 'text', text }], + } + } + + if (msg.type === 'user') { + const content = msg.message.content + const textBlocks: TranscriptBlock[] = [] + if (typeof content === 'string') { + textBlocks.push({ type: 'text', text: content }) + } else if (Array.isArray(content)) { + for (const block of content) { + if (block.type === 'text') { + textBlocks.push({ type: 'text', text: block.text }) + } + } + } + return textBlocks.length > 0 ? { role: 'user', content: textBlocks } : null + } + + if (msg.type === 'assistant') { + const blocks: TranscriptBlock[] = [] + for (const block of msg.message.content) { + // Only include tool_use blocks — assistant text is model-authored + // and could be crafted to influence the classifier's decision. + if (block.type === 'tool_use') { + blocks.push({ + type: 'tool_use', + name: block.name, + input: block.input, + }) + } + } + return blocks.length > 0 ? { role: 'assistant', content: blocks } : null + } + + return null +} + /** * Build transcript entries from messages. * Includes user text messages and assistant tool_use blocks (excluding assistant text). @@ -302,58 +363,9 @@ export type TranscriptEntry = { export function buildTranscriptEntries(messages: Message[]): TranscriptEntry[] { const transcript: TranscriptEntry[] = [] for (const msg of messages) { - if (msg.type === 'attachment' && msg.attachment.type === 'queued_command') { - const prompt = msg.attachment.prompt - let text: string | null = null - if (typeof prompt === 'string') { - text = prompt - } else if (Array.isArray(prompt)) { - text = - prompt - .filter( - (block): block is { type: 'text'; text: string } => - block.type === 'text', - ) - .map(block => block.text) - .join('\n') || null - } - if (text !== null) { - transcript.push({ - role: 'user', - content: [{ type: 'text', text }], - }) - } - } else if (msg.type === 'user') { - const content = msg.message.content - const textBlocks: TranscriptBlock[] = [] - if (typeof content === 'string') { - textBlocks.push({ type: 'text', text: content }) - } else if (Array.isArray(content)) { - for (const block of content) { - if (block.type === 'text') { - textBlocks.push({ type: 'text', text: block.text }) - } - } - } - if (textBlocks.length > 0) { - transcript.push({ role: 'user', content: textBlocks }) - } - } else if (msg.type === 'assistant') { - const blocks: TranscriptBlock[] = [] - for (const block of msg.message.content) { - // Only include tool_use blocks — assistant text is model-authored - // and could be crafted to influence the classifier's decision. - if (block.type === 'tool_use') { - blocks.push({ - type: 'tool_use', - name: block.name, - input: block.input, - }) - } - } - if (blocks.length > 0) { - transcript.push({ role: 'assistant', content: blocks }) - } + const entry = messageToTranscriptEntry(msg) + if (entry) { + transcript.push(entry) } } return transcript @@ -372,6 +384,17 @@ function buildToolLookup(tools: Tools): ToolLookup { return map } +function truncateClassifierValue(value: string): string { + if (value.length <= MAX_CLASSIFIER_BLOCK_VALUE_CHARS) { + return value + } + const omitted = value.length - MAX_CLASSIFIER_BLOCK_VALUE_CHARS + return ( + value.slice(0, MAX_CLASSIFIER_BLOCK_VALUE_CHARS) + + `… [truncated ${omitted} chars]` + ) +} + /** * Serialize a single transcript block as a JSONL dict line: `{"Bash":"ls"}` * for tool calls, `{"user":"text"}` for user text. The tool value is the @@ -410,15 +433,22 @@ function toCompactBlock( } if (encoded === '') return '' if (isJsonlTranscriptEnabled()) { - return jsonStringify({ [block.name]: encoded }) + '\n' + const jsonlValue = + typeof encoded === 'string' + ? truncateClassifierValue(encoded) + : encoded + return jsonStringify({ [block.name]: jsonlValue }) + '\n' } - const s = typeof encoded === 'string' ? encoded : jsonStringify(encoded) + const s = + typeof encoded === 'string' + ? truncateClassifierValue(encoded) + : jsonStringify(encoded) return `${block.name} ${s}\n` } if (block.type === 'text' && role === 'user') { return isJsonlTranscriptEnabled() - ? jsonStringify({ user: block.text }) + '\n' - : `User: ${block.text}\n` + ? jsonStringify({ user: truncateClassifierValue(block.text) }) + '\n' + : `User: ${truncateClassifierValue(block.text)}\n` } return '' } @@ -427,6 +457,96 @@ function toCompact(entry: TranscriptEntry, lookup: ToolLookup): string { return entry.content.map(b => toCompactBlock(b, entry.role, lookup)).join('') } +function serializeTranscriptForClassifier( + messages: Message[], + tools: Tools, + maxChars: number, +): { + userContentBlocks: Anthropic.TextBlockParam[] + promptLengths: { + toolCalls: number + userPrompts: number + } + transcriptEntries: number + truncated: boolean +} { + const lookup = buildToolLookup(tools) + const keptEntries: Array> = + [] + let totalChars = 0 + let truncated = false + + for (let i = messages.length - 1; i >= 0; i--) { + const entry = messageToTranscriptEntry(messages[i]!) + if (!entry) continue + + const serializedBlocks: Array<{ + role: TranscriptEntry['role'] + text: string + }> = [] + let entryChars = 0 + + for (const block of entry.content) { + const serialized = toCompactBlock(block, entry.role, lookup) + if (serialized === '') continue + serializedBlocks.push({ role: entry.role, text: serialized }) + entryChars += serialized.length + } + if (serializedBlocks.length === 0) continue + + if (totalChars + entryChars > maxChars) { + if (totalChars === 0) { + const partialEntry: typeof serializedBlocks = [] + let partialChars = 0 + for (let j = serializedBlocks.length - 1; j >= 0; j--) { + const serialized = serializedBlocks[j]! + if (partialChars + serialized.text.length > maxChars) continue + partialEntry.unshift(serialized) + partialChars += serialized.text.length + } + if (partialEntry.length > 0) { + keptEntries.push(partialEntry) + totalChars += partialChars + } + } + truncated = true + break + } + + keptEntries.push(serializedBlocks) + totalChars += entryChars + if (totalChars >= maxChars) { + truncated = i > 0 + break + } + } + + const userContentBlocks: Anthropic.TextBlockParam[] = [] + let userPromptsLength = 0 + let toolCallsLength = 0 + + for (let i = keptEntries.length - 1; i >= 0; i--) { + for (const block of keptEntries[i]!) { + userContentBlocks.push({ type: 'text' as const, text: block.text }) + if (block.role === 'user') { + userPromptsLength += block.text.length + } else { + toolCallsLength += block.text.length + } + } + } + + return { + userContentBlocks, + promptLengths: { + toolCalls: toolCallsLength, + userPrompts: userPromptsLength, + }, + transcriptEntries: keptEntries.length, + truncated, + } +} + /** * Build a compact transcript string including user messages and assistant tool_use blocks. * Used by AgentTool for handoff classification. @@ -434,10 +554,10 @@ function toCompact(entry: TranscriptEntry, lookup: ToolLookup): string { export function buildTranscriptForClassifier( messages: Message[], tools: Tools, + maxChars: number = MAX_CLASSIFIER_TRANSCRIPT_CHARS, ): string { - const lookup = buildToolLookup(tools) - return buildTranscriptEntries(messages) - .map(e => toCompact(e, lookup)) + return serializeTranscriptForClassifier(messages, tools, maxChars) + .userContentBlocks.map(block => block.text) .join('') } @@ -1027,34 +1147,24 @@ export async function classifyYoloAction( } const systemPrompt = await buildYoloSystemPrompt(context) - const transcriptEntries = buildTranscriptEntries(messages) + const transcriptBudget = Math.max( + 0, + MAX_CLASSIFIER_TRANSCRIPT_CHARS - actionCompact.length, + ) + const serializedTranscript = serializeTranscriptForClassifier( + messages, + tools, + transcriptBudget, + ) const claudeMdMessage = buildClaudeMdMessage() const prefixMessages: Anthropic.MessageParam[] = claudeMdMessage ? [claudeMdMessage] : [] - let toolCallsLength = actionCompact.length - let userPromptsLength = 0 - const userContentBlocks: Anthropic.TextBlockParam[] = [] - for (const entry of transcriptEntries) { - for (const block of entry.content) { - const serialized = toCompactBlock(block, entry.role, lookup) - if (serialized === '') continue - switch (entry.role) { - case 'user': - userPromptsLength += serialized.length - break - case 'assistant': - toolCallsLength += serialized.length - break - default: { - const _exhaustive: never = entry.role - void _exhaustive - } - } - userContentBlocks.push({ type: 'text' as const, text: serialized }) - } - } + const toolCallsLength = + actionCompact.length + serializedTranscript.promptLengths.toolCalls + const userPromptsLength = serializedTranscript.promptLengths.userPrompts + const userContentBlocks = [...serializedTranscript.userContentBlocks] const userPrompt = userContentBlocks.map(b => b.text).join('') + actionCompact const promptLengths = { @@ -1080,7 +1190,8 @@ export async function classifyYoloAction( `(sys=${promptLengths.systemPrompt} ` + `tools=${promptLengths.toolCalls} ` + `user=${promptLengths.userPrompts}) ` + - `transcriptEntries=${transcriptEntries.length} ` + + `transcriptEntries=${serializedTranscript.transcriptEntries} ` + + `truncated=${serializedTranscript.truncated} ` + `messages=${messages.length}`, ) logForDebugging( @@ -1119,7 +1230,7 @@ export async function classifyYoloAction( mainLoopTokens: mainLoopTokens ?? tokenCountWithEstimation(messages), classifierChars, classifierTokensEst, - transcriptEntries: transcriptEntries.length, + transcriptEntries: serializedTranscript.transcriptEntries, messages: messages.length, action: actionCompact, }, @@ -1274,7 +1385,7 @@ export async function classifyYoloAction( mainLoopTokens, classifierChars, classifierTokensEst, - transcriptEntries: transcriptEntries.length, + transcriptEntries: serializedTranscript.transcriptEntries, messages: messages.length, action: actionCompact, model, diff --git a/src/utils/toolResultStorage.test.ts b/src/utils/toolResultStorage.test.ts index 8040f1ff..8e5dca1b 100644 --- a/src/utils/toolResultStorage.test.ts +++ b/src/utils/toolResultStorage.test.ts @@ -14,6 +14,10 @@ test('applyToolResultReplacementsToMessages replaces matching tool results and p is_error: false, }, ], + toolUseResult: { + stdout: 'very large tool output', + stderr: '', + }, }) const messages = [unrelated, oversizedResult] const replacement = @@ -30,6 +34,7 @@ test('applyToolResultReplacementsToMessages replaces matching tool results and p expect((next[1]!.message.content as Array<{ content: string }>)[0]!.content).toBe( replacement, ) + expect(next[1]!.toolUseResult).toBeUndefined() }) test('applyToolResultReplacementsToMessages is idempotent when messages are already hydrated', () => { diff --git a/src/utils/toolResultStorage.ts b/src/utils/toolResultStorage.ts index e5f16c1a..9efc8070 100644 --- a/src/utils/toolResultStorage.ts +++ b/src/utils/toolResultStorage.ts @@ -726,6 +726,11 @@ function replaceToolResultContents( : { ...block, content: replacement } }), }, + // Drop the original tool payload once the model-facing content has been + // replaced with a persisted preview. Keeping both defeats the memory + // savings for long sessions because the live transcript still retains + // the oversized structured result. + toolUseResult: undefined, } }) return changed ? nextMessages : messages