diff --git a/.changeset/fix-approx-count-serialize.md b/.changeset/fix-approx-count-serialize.md new file mode 100644 index 0000000..b2c0f0d --- /dev/null +++ b/.changeset/fix-approx-count-serialize.md @@ -0,0 +1,5 @@ +--- +"flint": patch +--- + +Harden `approxCount` (and the `count` fallback) against non-serializable tool-call arguments. `JSON.stringify` could throw on circular references or `BigInt` values, or return `undefined` for a bare `undefined`, crashing a pure token estimate. Such arguments now contribute 0 tokens instead of throwing. diff --git a/.changeset/fix-memory-append-type.md b/.changeset/fix-memory-append-type.md new file mode 100644 index 0000000..68d7610 --- /dev/null +++ b/.changeset/fix-memory-append-type.md @@ -0,0 +1,5 @@ +--- +"flint": patch +--- + +Fix `ConversationMemory.append` type signature: it was declared as returning `void` but the implementation is `async` and performs summarization. The interface now correctly returns `Promise` so callers don't silently drop the promise. diff --git a/.changeset/fix-redact-private-keys.md b/.changeset/fix-redact-private-keys.md new file mode 100644 index 0000000..5697ca7 --- /dev/null +++ b/.changeset/fix-redact-private-keys.md @@ -0,0 +1,5 @@ +--- +"flint": patch +--- + +Broaden the private-key pattern in the `secretPatterns` redaction preset. The previous regex only matched key types made of uppercase letters and spaces, so it missed the most common modern formats — generic PKCS#8 `-----BEGIN PRIVATE KEY-----` (no type word), `ENCRYPTED PRIVATE KEY`, and types containing digits or hyphens. These are now redacted. diff --git a/packages/flint/src/memory.ts b/packages/flint/src/memory.ts index d10be24..72e64fe 100644 --- a/packages/flint/src/memory.ts +++ b/packages/flint/src/memory.ts @@ -58,7 +58,7 @@ export type ConversationMemoryOpts = { }; export type ConversationMemory = { - append(m: Message): void; + append(m: Message): Promise; messages(): Message[]; summary(): string | undefined; clear(): void; diff --git a/packages/flint/src/primitives/approx-count.ts b/packages/flint/src/primitives/approx-count.ts index 5bb61f1..98ebb67 100644 --- a/packages/flint/src/primitives/approx-count.ts +++ b/packages/flint/src/primitives/approx-count.ts @@ -29,7 +29,16 @@ export function approxCount(messages: Message[]): number { if (msg.role === 'assistant' && msg.toolCalls) { for (const tc of msg.toolCalls) { total += ROLE_OVERHEAD; - total += textTokens(JSON.stringify(tc.arguments)); + // tc.arguments is `unknown` — JSON.stringify can throw (circular refs, + // BigInt) or return undefined (a bare undefined value). Either case must + // not crash a pure token estimate, so fall back to 0 for the arguments. + let serialized: string | undefined; + try { + serialized = JSON.stringify(tc.arguments); + } catch { + serialized = undefined; + } + total += serialized === undefined ? 0 : textTokens(serialized); } } } diff --git a/packages/flint/src/safety/redact.ts b/packages/flint/src/safety/redact.ts index c057368..f81c722 100644 --- a/packages/flint/src/safety/redact.ts +++ b/packages/flint/src/safety/redact.ts @@ -39,7 +39,7 @@ export const secretPatterns: RegExp[] = [ /gho_[a-zA-Z0-9]{36}/g, /xox[baprs]-[a-zA-Z0-9-]{10,}/g, /sk_(?:live|test)_[a-zA-Z0-9]{24,}/g, - /-----BEGIN [A-Z ]+ PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+ PRIVATE KEY-----/g, + /-----BEGIN (?:[A-Z0-9 -]+ )?PRIVATE KEY-----[\s\S]+?-----END (?:[A-Z0-9 -]+ )?PRIVATE KEY-----/g, /\b\d{3}-\d{2}-\d{4}\b/g, /\b(?:\d{4}[\s-]?){3}\d{4}\b/g, ]; diff --git a/packages/flint/test/count.test.ts b/packages/flint/test/count.test.ts index 03964fc..6c69a04 100644 --- a/packages/flint/test/count.test.ts +++ b/packages/flint/test/count.test.ts @@ -55,6 +55,25 @@ describe('approxCount', () => { const more: Message[] = [...base, { role: 'assistant', content: 'hi back' }]; expect(approxCount(more)).toBeGreaterThanOrEqual(approxCount(base)); }); + + it('does not throw on non-serializable tool arguments', () => { + const circular: Record = {}; + circular.self = circular; + const msgs: Message[] = [ + { + role: 'assistant', + content: '', + toolCalls: [ + { id: 'c1', name: 'circular', arguments: circular }, + { id: 'c2', name: 'bigint', arguments: { n: 1n } }, + { id: 'c3', name: 'undef', arguments: undefined }, + ], + }, + ]; + // role 4 + 3 tool-call overheads (4 each); unserializable args contribute 0. + expect(() => approxCount(msgs)).not.toThrow(); + expect(approxCount(msgs)).toBe(4 + 4 * 3); + }); }); describe('count', () => { diff --git a/packages/flint/test/safety/redact.test.ts b/packages/flint/test/safety/redact.test.ts index 426c7d1..5f8b2d5 100644 --- a/packages/flint/test/safety/redact.test.ts +++ b/packages/flint/test/safety/redact.test.ts @@ -91,6 +91,18 @@ describe('secretPatterns preset', () => { ['Stripe live key', 'sk_live_abcdefghijklmnopqrstuvwx'], ['SSN', 'SSN: 123-45-6789'], ['Credit card', 'card 4111-1111-1111-1111'], + [ + 'RSA private key', + '-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKj\n-----END RSA PRIVATE KEY-----', + ], + [ + 'generic PKCS#8 private key', + '-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGBy\n-----END PRIVATE KEY-----', + ], + [ + 'encrypted private key', + '-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFLTBXBgkq\n-----END ENCRYPTED PRIVATE KEY-----', + ], ]; for (const [label, text] of cases) {