| 1 | // Pure helper for shaping the Claude message list, split out of runtime.mjs so |
| 2 | // it's exercisable in Node tests (test/context.test.mjs) without the SDK/ws deps. |
| 3 | |
| 4 | // trimContext drops assistant turns from BOTH ends of the message list. The API |
| 5 | // requires the first turn to be `user`; and with extended thinking on (the |
| 6 | // runtime always sends `thinking:{adaptive}`) the conversation also can't END on |
| 7 | // an assistant turn — there's no prefill under thinking, so a trailing assistant |
| 8 | // turn is a hard "assistant message prefill" 400. A trailing assistant turn shows |
| 9 | // up whenever the newest message we can see is our own reply (e.g. a redelivery |
| 10 | // race where a message got processed after we'd already answered); trimming it |
| 11 | // lets the request through instead of erroring. Returns a new array. |
| 12 | export function trimContext(messages) { |
| 13 | const out = [...(messages || [])]; |
| 14 | while (out.length && out[0].role === "assistant") out.shift(); |
| 15 | while (out.length && out[out.length - 1].role === "assistant") out.pop(); |
| 16 | return out; |
| 17 | } |
| 18 | |
| 19 | // scrubSelfNotes removes the "[note to self: …]" history annotation from a |
| 20 | // reply about to POST. The note is appended to past assistant turns (see |
| 21 | // buildContext in runtime.mjs) so the model knows which of its old replies |
| 22 | // carried buttons — but the model reads those turns as its own words and, |
| 23 | // despite the system-prompt rule, eventually imitates the pattern into a live |
| 24 | // reply. The note is for the model, never for members. |
| 25 | export function scrubSelfNotes(text) { |
| 26 | return (text || "").replace(/\s*\[note to self:[^\]]*\]/g, "").trim(); |
| 27 | } |
| 28 | |