diff --git a/AGENTS.md b/AGENTS.md index bf927ba..c6f7a5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ opencode-acp/ │ ├── logger.ts # Structured logging (logs/acp/) │ ├── auth.ts # Plugin authentication │ ├── token-utils.ts # Token counting utilities -│ ├── message-ids.ts # Message ID mapping (raw ↔ mNNNN refs) +│ ├── message-ids.ts # Message ID mapping (raw ↔ mNNNNNN refs) │ ├── compress-permission.ts # Permission management for compress tool │ ├── protected-patterns.ts # File pattern protection logic │ ├── host-permissions.ts # Host-based permission system @@ -160,14 +160,14 @@ index.ts (Plugin Entry — registers hooks + tools) ├─► Message Transform Hook (experimental.chat.messages.transform) ← runs EVERY LLM call │ │ │ ├─► checkSession() → state init, load persisted state - │ ├─► stripHallucinations() → remove stale mNNNN refs from model output - │ ├─► assignMessageRefs() → bidirectional map: raw message IDs ↔ mNNNN refs + │ ├─► stripHallucinations() → remove stale mNNNNN refs from model output + │ ├─► assignMessageRefs() → bidirectional map: raw message IDs ↔ mNNNNN refs │ ├─► syncCompressionBlocks() → deactivate orphaned blocks (messages deleted externally) │ ├─► runMajorGC() → age-based block deactivation + truncate oversized summaries │ ├─► prune() → replace compressed ranges with summary blocks in messages │ ├─► injectCompressNudges() → add context-limit / turn / iteration nudges │ │ └─► includes block aging guidance (only when context usage > 50%) - │ ├─► injectMessageIds() → tag every message with mNNNN ref (or BLOCKED) + │ ├─► injectMessageIds() → tag every message with mNNNNN ref (or BLOCKED) │ ├─► applyAnchoredNudges() → render nudge text into actual messages │ └─► stripStaleMetadata() → clean up removed messages' metadata │ @@ -179,7 +179,7 @@ index.ts (Plugin Entry — registers hooks + tools) │ └─► Track compress tool start/complete → attach duration to blocks │ ├─► Text Complete Hook (experimental.text.complete) - │ └─► Strip hallucinated mNNNN/bN refs from completions + │ └─► Strip hallucinated mNNNNN/bN refs from completions │ └─► Compress Tool (registered as "compress") │ @@ -469,6 +469,19 @@ For reference when modifying code — these bugs were real and the fixes are loa 3. Understand the module dependency graph (Section 4.1) 4. Check if the change affects backward compatibility (Section 2.6) +### 5.1.1 Development Workflow + +All changes MUST follow this workflow: + +1. Create a feature branch from `master` +2. Implement changes +3. Ensure `npm run build` and `npm run typecheck` pass +4. Ensure all tests pass: `npm run test` +5. Commit with descriptive messages +6. Push branch and create a GitHub PR +7. Obtain **dual-agent review** (Sections 5.3 + 5.4) on the PR +8. Merge PR after both reviews pass + ### 5.2 After Making Changes 1. `npm run build` must pass @@ -477,7 +490,25 @@ For reference when modifying code — these bugs were real and the fixes are loa 4. Deploy locally and test in opencode 5. Update version in `package.json` before publishing -### 5.3 Commit Convention +### 5.3 Code Review (MANDATORY) + +All source code changes (files under `lib/`) MUST undergo independent review by **at least 2 separate agents** before merge. This applies to: + +- New modules added to `lib/` +- Modified source files +- Changes to shared types, interfaces, or exports + +**Review checklist:** + +| Category | What to Check | +|----------|---------------| +| **Correctness** | Logic matches intent, no off-by-one errors, edge cases handled | +| **Backward compatibility** | No breaking changes to persisted state format, exported APIs, or internal tags (Section 2.6) | +| **Performance** | No unnecessary CPU/memory overhead, no O(n²) where O(n) suffices | +| **Type safety** | No `as any`, no `@ts-ignore`, no type assertion hacks | +| **State integrity** | State mutations are safe, no lost data on save/load cycle | + +### 5.4 Commit Convention Use descriptive commit messages. Historical examples: - `fix: aging warning only shows when context usage > 50%` @@ -485,9 +516,9 @@ Use descriptive commit messages. Historical examples: - `chore: bump version to 1.0.1` - `fix: config migration moved to getConfig() entry point` -### 5.4 Test Review (MANDATORY) +### 5.5 Test Review (MANDATORY) -All new and modified test files MUST undergo independent review by **at least 2 separate agents** before being committed. This requirement applies to: +All new and modified test files MUST undergo independent review by **at least 2 separate agents** before merge (same requirement as Section 5.3 code review). This requirement applies to: - New test files added to `tests/` - Modified test files (changed test logic, not just test names) diff --git a/lib/compress/message-utils.ts b/lib/compress/message-utils.ts index 97f05d9..fa97b76 100644 --- a/lib/compress/message-utils.ts +++ b/lib/compress/message-utils.ts @@ -88,16 +88,16 @@ const ISSUE_TEMPLATES: Record = { "refer to protected messages and cannot be compressed.", ], "invalid-format": [ - "is invalid. Use an injected raw message ID of the form mNNNN.", - "are invalid. Use injected raw message IDs of the form mNNNN.", + "is invalid. Use an injected raw message ID of the form mNNNNN.", + "are invalid. Use injected raw message IDs of the form mNNNNN.", ], "block-id": [ - "is invalid here. Block IDs like bN are not allowed; use an mNNNN message ID instead.", - "are invalid here. Block IDs like bN are not allowed; use mNNNN message IDs instead.", + "is invalid here. Block IDs like bN are not allowed; use an mNNNNN message ID instead.", + "are invalid here. Block IDs like bN are not allowed; use mNNNNN message IDs instead.", ], "not-in-context": [ - "is not available in the current conversation context. Choose an injected mNNNN ID visible in context.", - "are not available in the current conversation context. Choose injected mNNNN IDs visible in context.", + "is not available in the current conversation context. Choose an injected mNNNNN ID visible in context.", + "are not available in the current conversation context. Choose injected mNNNNN IDs visible in context.", ], protected: [ "refers to a protected message and cannot be compressed.", diff --git a/lib/compress/message.ts b/lib/compress/message.ts index 53dc028..e7b8bae 100644 --- a/lib/compress/message.ts +++ b/lib/compress/message.ts @@ -25,7 +25,7 @@ function buildSchema() { tool.schema.object({ messageId: tool.schema .string() - .describe("Raw message ID to compress (e.g. m0001)"), + .describe("Raw message ID to compress (e.g. m00001)"), topic: tool.schema .string() .describe("Short label (3-5 words) for this one message summary"), diff --git a/lib/compress/range.ts b/lib/compress/range.ts index 8bbf081..c86ebc7 100644 --- a/lib/compress/range.ts +++ b/lib/compress/range.ts @@ -37,11 +37,11 @@ function buildSchema() { startId: tool.schema .string() .describe( - "Message or block ID marking the beginning of range (e.g. m0001, b2)", + "Message or block ID marking the beginning of range (e.g. m00001, b2)", ), endId: tool.schema .string() - .describe("Message or block ID marking the end of range (e.g. m0012, b5)"), + .describe("Message or block ID marking the end of range (e.g. m00012, b5)"), summary: tool.schema .string() .describe("Complete technical summary replacing all content in range"), diff --git a/lib/compress/search.ts b/lib/compress/search.ts index 980df38..640f00a 100644 --- a/lib/compress/search.ts +++ b/lib/compress/search.ts @@ -55,11 +55,11 @@ export function resolveBoundaryIds( const parsedEndId = parseBoundaryId(endId) if (parsedStartId === null) { - issues.push("startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).") + issues.push("startId is invalid. Use an injected message ID (mNNNNN) or block ID (bN).") } if (parsedEndId === null) { - issues.push("endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).") + issues.push("endId is invalid. Use an injected message ID (mNNNNN) or block ID (bN).") } if (issues.length > 0) { diff --git a/lib/message-ids.ts b/lib/message-ids.ts index e3efdaa..b53d9ed 100644 --- a/lib/message-ids.ts +++ b/lib/message-ids.ts @@ -1,13 +1,13 @@ import type { SessionState, WithParts } from "./state" import { isIgnoredUserMessage } from "./messages/query" -const MESSAGE_REF_REGEX = /^m(\d{4})$/ +const MESSAGE_REF_REGEX = /^m(\d{4,5})$/ const BLOCK_REF_REGEX = /^b([1-9]\d*)$/ const MESSAGE_ID_TAG_NAME = "dcp-message-id" -const MESSAGE_REF_WIDTH = 4 +const MESSAGE_REF_WIDTH = 5 const MESSAGE_REF_MIN_INDEX = 1 -export const MESSAGE_REF_MAX_INDEX = 9999 +export const MESSAGE_REF_MAX_INDEX = 99999 export type ParsedBoundaryId = | { @@ -28,7 +28,7 @@ export function formatMessageRef(index: number): string { index > MESSAGE_REF_MAX_INDEX ) { throw new Error( - `Message ID index out of bounds: ${index}. Supported range is 0-${MESSAGE_REF_MAX_INDEX}.`, + `Message ID index out of bounds: ${index}. Supported range is ${MESSAGE_REF_MIN_INDEX}-${MESSAGE_REF_MAX_INDEX}.`, ) } return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}` diff --git a/lib/prompts/compress-message.ts b/lib/prompts/compress-message.ts index d0964d3..59a9a45 100644 --- a/lib/prompts/compress-message.ts +++ b/lib/prompts/compress-message.ts @@ -13,11 +13,11 @@ If a message contains no significant technical decisions, code changes, or user MESSAGE IDS You specify individual raw messages by ID using the injected IDs visible in the conversation: -- \`mNNNN\` IDs identify raw messages +- \`mNNNNN\` IDs identify raw messages Each message has an ID inside XML metadata tags like \`m0007\`. The same ID tag appears in every tool output of the message it belongs to — each unique ID identifies one complete message. -Treat these tags as message metadata only, not as content to summarize. Use only the inner \`mNNNN\` value as the \`messageId\`. +Treat these tags as message metadata only, not as content to summarize. Use only the inner \`mNNNNN\` value as the \`messageId\`. The \`priority\` attribute indicates relative context cost. You MUST compress high-priority messages when their full text is no longer necessary for the active task. If prior compress-tool results are present, always compress and summarize them minimally only as part of a broader compression pass. Do not invoke the compress tool solely to re-compress an earlier compression result. Messages marked as \`BLOCKED\` cannot be compressed. @@ -25,8 +25,8 @@ Messages marked as \`BLOCKED\` cannot be compre Rules: - Pick each \`messageId\` directly from injected IDs visible in context. -- Only use raw message IDs of the form \`mNNNN\`. -- Ignore XML attributes such as \`priority\` when copying the ID; use only the inner \`mNNNN\` value. +- Only use raw message IDs of the form \`mNNNNN\`. +- Ignore XML attributes such as \`priority\` when copying the ID; use only the inner \`mNNNNN\` value. - Do not invent IDs. Use only IDs that are present in context. BATCHING diff --git a/lib/prompts/compress-range.ts b/lib/prompts/compress-range.ts index 8e2cbbc..2dedb3a 100644 --- a/lib/prompts/compress-range.ts +++ b/lib/prompts/compress-range.ts @@ -18,7 +18,7 @@ Compressed block sections in context are clearly marked with a header: - \`[Compressed conversation section]\` -Compressed block IDs always use the \`bN\` form (never \`mNNNN\`) and are represented in the same XML metadata tag format. +Compressed block IDs always use the \`bN\` form (never \`mNNNNN\`) and are represented in the same XML metadata tag format. Rules: @@ -41,7 +41,7 @@ When you use compressed block placeholders, write the surrounding summary text s BOUNDARY IDS You specify boundaries by ID using the injected IDs visible in the conversation: -- \`mNNNN\` IDs identify raw messages +- \`mNNNNN\` IDs identify raw messages - \`bN\` IDs identify previously compressed blocks Each message has an ID inside XML metadata tags like \`...\`. diff --git a/lib/prompts/extensions/tool.ts b/lib/prompts/extensions/tool.ts index ff852ac..217914b 100644 --- a/lib/prompts/extensions/tool.ts +++ b/lib/prompts/extensions/tool.ts @@ -10,8 +10,8 @@ THE FORMAT OF COMPRESS topic: string, // Short label (3-5 words) - e.g., "Auth System Exploration" content: [ // One or more ranges to compress { - startId: string, // Boundary ID at range start: mNNNN or bN - endId: string, // Boundary ID at range end: mNNNN or bN + startId: string, // Boundary ID at range start: mNNNNN or bN + endId: string, // Boundary ID at range end: mNNNNN or bN summary: string // Complete technical summary replacing all content in range } ] @@ -26,7 +26,7 @@ THE FORMAT OF COMPRESS topic: string, // Short label (3-5 words) for the overall batch content: [ // One or more messages to compress independently { - messageId: string, // Raw message ID only: mNNNN (ignore metadata attributes like priority) + messageId: string, // Raw message ID only: mNNNNN (ignore metadata attributes like priority) topic: string, // Short label (3-5 words) for this one message summary summary: string // Complete technical summary replacing that one message } diff --git a/lib/state/state.ts b/lib/state/state.ts index ffc44f1..4651601 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -13,6 +13,7 @@ import { collectTurnNudgeAnchors, } from "./utils" import { getLastUserMessage } from "../messages/query" +import { parseMessageRef, formatMessageRef } from "../message-ids" export const checkSession = async ( client: any, @@ -195,6 +196,18 @@ export async function ensureSessionInitialized( state.messageIds.byRef.delete(ref) } } + // Migrate 4-digit refs (m0001) to 5-digit (m00001) for msgid expansion + for (const [rawId, oldRef] of state.messageIds.byRawId) { + const parsed = parseMessageRef(oldRef) + if (parsed !== null) { + const newRef = formatMessageRef(parsed) + if (newRef !== oldRef) { + state.messageIds.byRawId.set(rawId, newRef) + state.messageIds.byRef.delete(oldRef) + state.messageIds.byRef.set(newRef, rawId) + } + } + } } if (persistedAny._persistedLastCompaction !== undefined) { state.lastCompaction = Math.max(state.lastCompaction, persistedAny._persistedLastCompaction) diff --git a/tests/compress-message.test.ts b/tests/compress-message.test.ts index 2528fc7..33031ba 100644 --- a/tests/compress-message.test.ts +++ b/tests/compress-message.test.ts @@ -195,12 +195,12 @@ test("compress message mode batches individual message summaries", async () => { topic: "Batch stale notes", content: [ { - messageId: "m0002", + messageId: "m00002", topic: "Code path note", summary: "Captured the assistant's code-path findings.", }, { - messageId: "m0003", + messageId: "m00003", topic: "Task output note", summary: "Captured the assistant's task-backed follow-up.", }, @@ -222,11 +222,11 @@ test("compress message mode batches individual message summaries", async () => { (a, b) => a.blockId - b.blockId, ) - assert.equal(blocks[0]?.startId, "m0002") - assert.equal(blocks[0]?.endId, "m0002") + assert.equal(blocks[0]?.startId, "m00002") + assert.equal(blocks[0]?.endId, "m00002") assert.equal(blocks[0]?.topic, "Code path note") - assert.equal(blocks[1]?.startId, "m0003") - assert.equal(blocks[1]?.endId, "m0003") + assert.equal(blocks[1]?.startId, "m00003") + assert.equal(blocks[1]?.endId, "m00003") assert.match( blocks[1]?.summary || "", /The following protected tools were used in this conversation as well:/, @@ -271,7 +271,7 @@ test("compress message mode appends protected prompt info", async () => { topic: "Protected note", content: [ { - messageId: "m0001", + messageId: "m00001", topic: "User request note", summary: "Captured the user's investigation request.", }, @@ -330,7 +330,7 @@ test("compress message mode ignores protect tags on ignored user messages", asyn topic: "Ignored protected note", content: [ { - messageId: "m0001", + messageId: "m00001", topic: "Ignored note", summary: "Captured the ignored user message.", }, @@ -381,7 +381,7 @@ test("compress message mode stores call id for later duration attachment", async topic: "Batch stale notes", content: [ { - messageId: "m0002", + messageId: "m00002", topic: "Code path note", summary: "Captured the assistant's code-path findings.", }, @@ -437,12 +437,12 @@ test("compress message mode does not partially apply when preparation fails", as topic: "Batch stale notes", content: [ { - messageId: "m0002", + messageId: "m00002", topic: "Code path note", summary: "Captured the assistant's code-path findings.", }, { - messageId: "m0003", + messageId: "m00003", topic: "Task output note", summary: "Captured the assistant's task-backed follow-up.", }, @@ -543,12 +543,12 @@ test("compress message mode skips protected user message references", async () = summary: "Should be skipped.", }, { - messageId: "m0001", + messageId: "m00001", topic: "Hidden protected ref", summary: "Should also be skipped.", }, { - messageId: "m0002", + messageId: "m00002", topic: "Valid note", summary: "Captured the assistant's code-path findings.", }, @@ -566,7 +566,7 @@ test("compress message mode skips protected user message references", async () = assert.match(result, /^Compressed 1 message into \[Compressed conversation section\]\./) assert.match(result, /Skipped 2 issues:/) assert.match(result, /messageId BLOCKED refers to a protected message/) - assert.match(result, /messageId m0001 refers to a protected message/) + assert.match(result, /messageId m00001 refers to a protected message/) }) test("compress message mode allows messages containing compress tool parts", async () => { @@ -622,7 +622,7 @@ test("compress message mode allows messages containing compress tool parts", asy topic: "Compress compress call", content: [ { - messageId: "m0004", + messageId: "m00004", topic: "Compress tool message", summary: "Captured the earlier compress tool call.", }, @@ -640,7 +640,7 @@ test("compress message mode allows messages containing compress tool parts", asy assert.equal(result, "Compressed 1 message into [Compressed conversation section].\nIMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.") assert.equal(state.prune.messages.blocksById.size, 1) const block = Array.from(state.prune.messages.blocksById.values())[0] - assert.equal(block?.startId, "m0004") + assert.equal(block?.startId, "m00004") }) test("compress message mode sends one aggregated notification for batched messages", async () => { @@ -681,12 +681,12 @@ test("compress message mode sends one aggregated notification for batched messag topic: "Batch stale notes", content: [ { - messageId: "m0002", + messageId: "m00002", topic: "Code path note", summary: "Captured the assistant's code-path findings.", }, { - messageId: "m0003", + messageId: "m00003", topic: "Task output note", summary: "Captured the assistant's task-backed follow-up.", }, @@ -737,7 +737,7 @@ test("compress message mode skips messages that are already actively compressed" topic: "First pass", content: [ { - messageId: "m0002", + messageId: "m00002", topic: "Code path note", summary: "Captured the assistant's code-path findings.", }, @@ -756,12 +756,12 @@ test("compress message mode skips messages that are already actively compressed" topic: "Second pass", content: [ { - messageId: "m0002", + messageId: "m00002", topic: "Already compressed note", summary: "Should be skipped because it is already compressed.", }, { - messageId: "m0003", + messageId: "m00003", topic: "Task output note", summary: "Captured the assistant's task-backed follow-up.", }, @@ -778,7 +778,7 @@ test("compress message mode skips messages that are already actively compressed" assert.equal(state.prune.messages.blocksById.size, 2) assert.match(result, /^Compressed 1 message into \[Compressed conversation section\]\./) assert.match(result, /Skipped 1 issue:/) - assert.match(result, /messageId m0002 is already part of an active compression\./) + assert.match(result, /messageId m00002 is already part of an active compression\./) }) test("compress message mode skips invalid batch entries and reports issues", async () => { @@ -814,17 +814,17 @@ test("compress message mode skips invalid batch entries and reports issues", asy summary: "Should be skipped.", }, { - messageId: "m0002", + messageId: "m00002", topic: "Valid note", summary: "Captured the assistant's code-path findings.", }, { - messageId: "m9999", + messageId: "m09999", topic: "Missing message", summary: "Should also be skipped.", }, { - messageId: "m0002", + messageId: "m00002", topic: "Duplicate valid note", summary: "Duplicate entry should be skipped.", }, @@ -842,8 +842,8 @@ test("compress message mode skips invalid batch entries and reports issues", asy assert.match(result, /^Compressed 1 message into \[Compressed conversation section\]\./) assert.match(result, /Skipped 3 issues:/) assert.match(result, /Block IDs like bN are not allowed/) - assert.match(result, /messageId m9999 is not available in the current conversation context/) - assert.match(result, /messageId m0002 was selected more than once in this batch\./) + assert.match(result, /messageId m09999 is not available in the current conversation context/) + assert.match(result, /messageId m00002 was selected more than once in this batch\./) }) test("compress message mode reports issues when every batch entry is skipped", async () => { @@ -880,7 +880,7 @@ test("compress message mode reports issues when every batch entry is skipped", a summary: "Should be skipped.", }, { - messageId: "m9999", + messageId: "m09999", topic: "Missing message", summary: "Should also be skipped.", }, diff --git a/tests/compress-range-placeholders.test.ts b/tests/compress-range-placeholders.test.ts index 69fb9d0..9de6274 100644 --- a/tests/compress-range-placeholders.test.ts +++ b/tests/compress-range-placeholders.test.ts @@ -19,8 +19,8 @@ function createBlock(blockId: number, body: string): CompressionBlock { compressedTokens: 0, summaryTokens: 0, topic: `Block ${blockId}`, - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", anchorMessageId: `msg-${blockId}`, compressMessageId: `compress-${blockId}`, includedBlockIds: [], diff --git a/tests/compress-range.test.ts b/tests/compress-range.test.ts index b9610b4..5baf1ec 100644 --- a/tests/compress-range.test.ts +++ b/tests/compress-range.test.ts @@ -136,8 +136,8 @@ test("compress range rebuilds subagent message refs after session state was rese const rawMessages = buildMessages(sessionID) const state = createSessionState() state.sessionId = "ses_other" - state.messageIds.byRawId.set("other-message", "m0001") - state.messageIds.byRef.set("m0001", "other-message") + state.messageIds.byRawId.set("other-message", "m00001") + state.messageIds.byRef.set("m00001", "other-message") state.messageIds.nextRef = 2 const logger = new Logger(false) @@ -164,8 +164,8 @@ test("compress range rebuilds subagent message refs after session state was rese topic: "Subagent race fix", content: [ { - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", summary: "Captured the initial investigation and follow-up request.", }, ], @@ -182,8 +182,8 @@ test("compress range rebuilds subagent message refs after session state was rese assert.equal(result, "Compressed 2 messages into [Compressed conversation section].\nIMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.") assert.equal(state.sessionId, sessionID) assert.equal(state.isSubAgent, true) - assert.equal(state.messageIds.byRef.get("m0001"), "msg-assistant-1") - assert.equal(state.messageIds.byRef.get("m0002"), "msg-user-2") + assert.equal(state.messageIds.byRef.get("m00001"), "msg-assistant-1") + assert.equal(state.messageIds.byRef.get("m00002"), "msg-user-2") assert.equal(state.prune.messages.blocksById.size, 1) }) @@ -250,8 +250,8 @@ test("compress range mode appends protected prompt info", async () => { topic: "Protected range", content: [ { - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", summary: "Captured release investigation.", }, ], @@ -310,13 +310,13 @@ test("compress range mode batches multiple ranges into one notification", async topic: "Batch stale notes", content: [ { - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", summary: "Captured the initial assistant investigation.", }, { - startId: "m0002", - endId: "m0002", + startId: "m00002", + endId: "m00002", summary: "Captured the follow-up user request.", }, ], @@ -370,13 +370,13 @@ test("compress range mode rejects overlapping batched ranges", async () => { topic: "Overlapping ranges", content: [ { - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", summary: "Captured the initial investigation and follow-up request.", }, { - startId: "m0002", - endId: "m0002", + startId: "m00002", + endId: "m00002", summary: "Captured the follow-up request again.", }, ], diff --git a/tests/compress-search.test.ts b/tests/compress-search.test.ts index d73076f..08aac13 100644 --- a/tests/compress-search.test.ts +++ b/tests/compress-search.test.ts @@ -86,8 +86,8 @@ function makeBlock(overrides: Partial = {}): CompressionBlock mode: "range", topic: "test", batchTopic: "test", - startId: "m0001", - endId: "m0003", + startId: "m00001", + endId: "m00003", anchorMessageId: "raw-1", compressMessageId: "comp-1", compressCallId: undefined, @@ -227,12 +227,12 @@ test("resolveBoundaryIds resolves message IDs correctly", () => { const ctx = makeContext([msg1, msg2]) const state = makeState() - state.messageIds.byRef.set("m0001", "raw-a") - state.messageIds.byRef.set("m0002", "raw-b") - state.messageIds.byRawId.set("raw-a", "m0001") - state.messageIds.byRawId.set("raw-b", "m0002") + state.messageIds.byRef.set("m00001", "raw-a") + state.messageIds.byRef.set("m00002", "raw-b") + state.messageIds.byRawId.set("raw-a", "m00001") + state.messageIds.byRawId.set("raw-b", "m00002") - const { startReference, endReference } = resolveBoundaryIds(ctx, state, "m0001", "m0002") + const { startReference, endReference } = resolveBoundaryIds(ctx, state, "m00001", "m00002") assert.equal(startReference.kind, "message") assert.equal(startReference.messageId, "raw-a") assert.equal(startReference.rawIndex, 0) @@ -261,7 +261,7 @@ test("resolveBoundaryIds throws on invalid startId format", () => { const ctx = makeContext([]) const state = makeState() assert.throws( - () => resolveBoundaryIds(ctx, state, "invalid", "m0001"), + () => resolveBoundaryIds(ctx, state, "invalid", "m00001"), /startId is invalid/, ) }) @@ -270,7 +270,7 @@ test("resolveBoundaryIds throws on unknown message ref", () => { const ctx = makeContext([]) const state = makeState() assert.throws( - () => resolveBoundaryIds(ctx, state, "m0099", "m0100"), + () => resolveBoundaryIds(ctx, state, "m00099", "m00100"), /not available/, ) }) @@ -281,13 +281,13 @@ test("resolveBoundaryIds auto-swaps reversed boundaries (Bug 34)", () => { const ctx = makeContext([msg1, msg2]) const state = makeState() - state.messageIds.byRef.set("m0001", "raw-a") - state.messageIds.byRef.set("m0002", "raw-b") - state.messageIds.byRawId.set("raw-a", "m0001") - state.messageIds.byRawId.set("raw-b", "m0002") + state.messageIds.byRef.set("m00001", "raw-a") + state.messageIds.byRef.set("m00002", "raw-b") + state.messageIds.byRawId.set("raw-a", "m00001") + state.messageIds.byRawId.set("raw-b", "m00002") // Pass in reversed order: end before start - const { startReference, endReference } = resolveBoundaryIds(ctx, state, "m0002", "m0001") + const { startReference, endReference } = resolveBoundaryIds(ctx, state, "m00002", "m00001") assert.equal(startReference.messageId, "raw-a") assert.equal(startReference.rawIndex, 0) assert.equal(endReference.messageId, "raw-b") diff --git a/tests/compress-state.test.ts b/tests/compress-state.test.ts index de127f2..f838cbe 100644 --- a/tests/compress-state.test.ts +++ b/tests/compress-state.test.ts @@ -28,8 +28,8 @@ function makeBlock(overrides: Partial = {}): CompressionBlock mode: "range", topic: "test", batchTopic: "test", - startId: "m0001", - endId: "m0003", + startId: "m00001", + endId: "m00003", anchorMessageId: "anchor-1", compressMessageId: "comp-1", compressCallId: undefined, @@ -108,8 +108,8 @@ function makeCompressionInput(overrides: Partial = {}): C return { topic: "test topic", batchTopic: "batch topic", - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", mode: "range", runId: 1, compressMessageId: "comp-1", diff --git a/tests/compression-groups.test.ts b/tests/compression-groups.test.ts index c5bac05..2664d50 100644 --- a/tests/compression-groups.test.ts +++ b/tests/compression-groups.test.ts @@ -200,8 +200,8 @@ test("compression notifications increment by tool call across range and message topic: "Range batch", content: [ { - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", summary: "Captured the opening user request.", }, ], @@ -234,12 +234,12 @@ test("compression notifications increment by tool call across range and message topic: "Message batch", content: [ { - messageId: "m0002", + messageId: "m00002", topic: "Code path note", summary: "Captured the assistant code-path findings.", }, { - messageId: "m0003", + messageId: "m00003", topic: "Task output note", summary: "Captured the assistant task-backed follow-up.", }, @@ -292,12 +292,12 @@ test("decompress groups batched message compressions by tool call", async () => topic: "Batch stale notes", content: [ { - messageId: "m0002", + messageId: "m00002", topic: "Code path note", summary: "Captured the assistant code-path findings.", }, { - messageId: "m0003", + messageId: "m00003", topic: "Task output note", summary: "Captured the assistant task-backed follow-up.", }, @@ -392,13 +392,13 @@ test("decompress keeps batched ranges individually restorable", async () => { topic: "Batch stale notes", content: [ { - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", summary: "Captured the opening user request.", }, { - startId: "m0002", - endId: "m0002", + startId: "m00002", + endId: "m00002", summary: "Captured the assistant code-path findings.", }, ], diff --git a/tests/e2e-blocks-nudges.test.ts b/tests/e2e-blocks-nudges.test.ts index 00fe621..4602f38 100644 --- a/tests/e2e-blocks-nudges.test.ts +++ b/tests/e2e-blocks-nudges.test.ts @@ -260,8 +260,8 @@ test("block aging: old blocks are deactivated by major GC", async () => { mode: "message", topic: "test", batchTopic: "test", - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", anchorMessageId: "u2", compressMessageId: "msg-comp", compressCallId: "call-comp", @@ -349,8 +349,8 @@ test("session switch: state is reinitialized when session changes", async () => await handler({}, output1) assert.equal(state.sessionId, SID_A) - assert.equal(state.messageIds.byRawId.get("u1a"), "m0001") - assert.equal(state.messageIds.byRawId.get("a1a"), "m0002") + assert.equal(state.messageIds.byRawId.get("u1a"), "m00001") + assert.equal(state.messageIds.byRawId.get("a1a"), "m00002") // Second call with a DIFFERENT session (session B) // checkSession detects the change and reinitializes @@ -369,8 +369,8 @@ test("session switch: state is reinitialized when session changes", async () => assert.equal(state.messageIds.byRawId.has("u1a"), false, "old session IDs should be cleared") // New session gets fresh IDs - assert.equal(state.messageIds.byRawId.get("u1b"), "m0001") - assert.equal(state.messageIds.byRawId.get("a1b"), "m0002") + assert.equal(state.messageIds.byRawId.get("u1b"), "m00001") + assert.equal(state.messageIds.byRawId.get("a1b"), "m00002") }) // ─── Test: Message IDs injected into tool parts ───────────────────────────── @@ -403,8 +403,8 @@ test("message ID injection: IDs are appended to tool parts", async () => { "tool output should contain message ID tag", ) assert.ok( - toolOutput.includes("m0002"), - "tool output should contain the m0002 ref", + toolOutput.includes("m00002"), + "tool output should contain the m00002 ref", ) }) @@ -459,8 +459,8 @@ test("block consumption: newer block deactivates consumed blocks", async () => { mode: "message", topic: "old", batchTopic: "old", - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", anchorMessageId: "u1", compressMessageId: "msg-comp1", compressCallId: "call-comp1", @@ -495,8 +495,8 @@ test("block consumption: newer block deactivates consumed blocks", async () => { mode: "message", topic: "new", batchTopic: "new", - startId: "m0003", - endId: "m0004", + startId: "m00003", + endId: "m00004", anchorMessageId: "u3", compressMessageId: "msg-comp2", compressCallId: "call-comp2", @@ -564,8 +564,8 @@ test("ID accumulation: sequential runs never produce duplicate refs", async () = assert.equal(new Set(allRefs).size, 10, "all refs should be unique") assert.equal(state.messageIds.nextRef, 11) - assert.equal(state.messageIds.byRawId.get("r4_u1"), "m0009") - assert.equal(state.messageIds.byRawId.get("r4_a1"), "m0010") + assert.equal(state.messageIds.byRawId.get("r4_u1"), "m00009") + assert.equal(state.messageIds.byRawId.get("r4_a1"), "m00010") }) // ─── Test: Mixed valid and invalid messages ───────────────────────────────── @@ -589,7 +589,7 @@ test("mixed messages: only valid messages survive, IDs assigned to survivors", a const ids = output.messages.map((m: WithParts) => m.info.id) assert.deepEqual(ids, ["u1", "a1", "u2"]) - assert.equal(state.messageIds.byRawId.get("u1"), "m0001") - assert.equal(state.messageIds.byRawId.get("a1"), "m0002") - assert.equal(state.messageIds.byRawId.get("u2"), "m0003") + assert.equal(state.messageIds.byRawId.get("u1"), "m00001") + assert.equal(state.messageIds.byRawId.get("a1"), "m00002") + assert.equal(state.messageIds.byRawId.get("u2"), "m00003") }) diff --git a/tests/e2e-message-transform.test.ts b/tests/e2e-message-transform.test.ts index 2964149..31c6dd6 100644 --- a/tests/e2e-message-transform.test.ts +++ b/tests/e2e-message-transform.test.ts @@ -217,15 +217,15 @@ test("basic pipeline: assigns message IDs and preserves all messages", async () assert.equal(output.messages.length, 5) // Message IDs should be assigned - assert.equal(state.messageIds.byRawId.get("u1"), "m0001") - assert.equal(state.messageIds.byRawId.get("a1"), "m0002") - assert.equal(state.messageIds.byRawId.get("u2"), "m0003") - assert.equal(state.messageIds.byRawId.get("a2"), "m0004") - assert.equal(state.messageIds.byRawId.get("u3"), "m0005") + assert.equal(state.messageIds.byRawId.get("u1"), "m00001") + assert.equal(state.messageIds.byRawId.get("a1"), "m00002") + assert.equal(state.messageIds.byRawId.get("u2"), "m00003") + assert.equal(state.messageIds.byRawId.get("a2"), "m00004") + assert.equal(state.messageIds.byRawId.get("u3"), "m00005") // Reverse mapping should exist - assert.equal(state.messageIds.byRef.get("m0001"), "u1") - assert.equal(state.messageIds.byRef.get("m0005"), "u3") + assert.equal(state.messageIds.byRef.get("m00001"), "u1") + assert.equal(state.messageIds.byRef.get("m00005"), "u3") }) // ─── Test: Message IDs are stable across multiple pipeline runs ────────────── @@ -242,8 +242,8 @@ test("message IDs remain stable across sequential pipeline calls", async () => { } await handler({}, output1) - assert.equal(state.messageIds.byRawId.get("u1"), "m0001") - assert.equal(state.messageIds.byRawId.get("a1"), "m0002") + assert.equal(state.messageIds.byRawId.get("u1"), "m00001") + assert.equal(state.messageIds.byRawId.get("a1"), "m00002") assert.equal(state.messageIds.nextRef, 3) // Second call adds new messages; existing IDs should remain stable @@ -258,11 +258,11 @@ test("message IDs remain stable across sequential pipeline calls", async () => { await handler({}, output2) // Old IDs stable - assert.equal(state.messageIds.byRawId.get("u1"), "m0001") - assert.equal(state.messageIds.byRawId.get("a1"), "m0002") + assert.equal(state.messageIds.byRawId.get("u1"), "m00001") + assert.equal(state.messageIds.byRawId.get("a1"), "m00002") // New IDs assigned - assert.equal(state.messageIds.byRawId.get("u2"), "m0003") - assert.equal(state.messageIds.byRawId.get("a2"), "m0004") + assert.equal(state.messageIds.byRawId.get("u2"), "m00003") + assert.equal(state.messageIds.byRawId.get("a2"), "m00004") assert.equal(state.messageIds.nextRef, 5) }) @@ -325,8 +325,8 @@ test("compression blocks: compressed messages are replaced with summaries", asyn mode: "message", topic: "test topic", batchTopic: "test topic", - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", anchorMessageId: "u2", // summary injected at this anchor compressMessageId: "msg-compress", compressCallId: "call-compress", @@ -441,8 +441,8 @@ test("message IDs remain consistent after compression and pruning", async () => mode: "message", topic: "early chat", batchTopic: "early chat", - startId: "m0001", - endId: "m0002", + startId: "m00001", + endId: "m00002", anchorMessageId: "u3", compressMessageId: "msg-comp", compressCallId: "call-comp", @@ -593,10 +593,10 @@ test("deny permission: still filters messages and strips hallucinations", async test("state persistence: session state survives save/load round-trip", async () => { const { state, tempDir } = setupPipeline() - state.messageIds.byRawId.set("u1", "m0001") - state.messageIds.byRawId.set("a1", "m0002") - state.messageIds.byRef.set("m0001", "u1") - state.messageIds.byRef.set("m0002", "a1") + state.messageIds.byRawId.set("u1", "m00001") + state.messageIds.byRawId.set("a1", "m00002") + state.messageIds.byRef.set("m00001", "u1") + state.messageIds.byRef.set("m00002", "a1") state.messageIds.nextRef = 3 state.stats.totalPruneTokens = 5000 @@ -607,8 +607,8 @@ test("state persistence: session state survives save/load round-trip", async () const loaded = await loadSessionState(SID, logger) assert.ok(loaded, "state file should be loadable") - assert.equal(loaded!.messageIds?.byRawId?.["u1"], "m0001") - assert.equal(loaded!.messageIds?.byRawId?.["a1"], "m0002") + assert.equal(loaded!.messageIds?.byRawId?.["u1"], "m00001") + assert.equal(loaded!.messageIds?.byRawId?.["a1"], "m00002") assert.equal(loaded!.messageIds?.nextRef, 3) assert.equal(loaded!.stats.totalPruneTokens, 5000) diff --git a/tests/hooks-permission.test.ts b/tests/hooks-permission.test.ts index cfa5b47..502e681 100644 --- a/tests/hooks-permission.test.ts +++ b/tests/hooks-permission.test.ts @@ -290,8 +290,8 @@ test("event hook attaches durations to matching blocks by message and call id", mode: "message", topic: "one", batchTopic: "one", - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", anchorMessageId: "msg-a", compressMessageId: "message-1", compressCallId: "call-1", @@ -316,8 +316,8 @@ test("event hook attaches durations to matching blocks by message and call id", mode: "message", topic: "two", batchTopic: "two", - startId: "m0002", - endId: "m0002", + startId: "m00002", + endId: "m00002", anchorMessageId: "msg-b", compressMessageId: "message-1", compressCallId: "call-2", @@ -401,8 +401,8 @@ test("event hook falls back to completed runtime when running duration missing", mode: "message", topic: "one", batchTopic: "one", - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", anchorMessageId: "msg-a", compressMessageId: "message-1", compressCallId: "call-3", @@ -460,8 +460,8 @@ test("event hook queues duration updates until the matching session is loaded", mode: "message", topic: "one", batchTopic: "one", - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", anchorMessageId: "msg-a", compressMessageId: "message-1", compressCallId: "call-remote", @@ -572,8 +572,8 @@ test("event hook keeps same call id distinct across message ids", async () => { mode: "message", topic: "one", batchTopic: "one", - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", anchorMessageId: "msg-a", compressMessageId: "message-1", compressCallId: "shared-call", @@ -598,8 +598,8 @@ test("event hook keeps same call id distinct across message ids", async () => { mode: "message", topic: "two", batchTopic: "two", - startId: "m0002", - endId: "m0002", + startId: "m00002", + endId: "m00002", anchorMessageId: "msg-b", compressMessageId: "message-2", compressCallId: "shared-call", diff --git a/tests/message-ids.test.ts b/tests/message-ids.test.ts index d9f5a00..ac263bc 100644 --- a/tests/message-ids.test.ts +++ b/tests/message-ids.test.ts @@ -94,16 +94,20 @@ function makeState(overrides: Partial = {}): SessionState { // --- Tests for formatMessageRef --- -test("formatMessageRef formats index 1 as m0001", () => { - assert.equal(formatMessageRef(1), "m0001") +test("formatMessageRef formats index 1 as m00001", () => { + assert.equal(formatMessageRef(1), "m00001") }) -test("formatMessageRef formats index 9999 as m9999", () => { - assert.equal(formatMessageRef(9999), "m9999") +test("formatMessageRef formats index 9999 as m09999", () => { + assert.equal(formatMessageRef(9999), "m09999") }) -test("formatMessageRef formats index 123 as m0123", () => { - assert.equal(formatMessageRef(123), "m0123") +test("formatMessageRef formats index 123 as m00123", () => { + assert.equal(formatMessageRef(123), "m00123") +}) + +test("formatMessageRef formats index 99999 as m99999", () => { + assert.equal(formatMessageRef(99999), "m99999") }) test("formatMessageRef throws for index 0", () => { @@ -114,8 +118,8 @@ test("formatMessageRef throws for negative index", () => { assert.throws(() => formatMessageRef(-1), /out of bounds/) }) -test("formatMessageRef throws for index exceeding 9999", () => { - assert.throws(() => formatMessageRef(10000), /out of bounds/) +test("formatMessageRef throws for index exceeding 99999", () => { + assert.throws(() => formatMessageRef(100000), /out of bounds/) }) test("formatMessageRef throws for non-integer", () => { @@ -142,32 +146,40 @@ test("formatBlockRef throws for negative", () => { // --- Tests for parseMessageRef --- -test("parseMessageRef parses m0001 to index 1", () => { +test("parseMessageRef parses m0001 (4-digit) to index 1", () => { assert.equal(parseMessageRef("m0001"), 1) }) -test("parseMessageRef parses m9999 to index 9999", () => { +test("parseMessageRef parses m00001 (5-digit) to index 1", () => { + assert.equal(parseMessageRef("m00001"), 1) +}) + +test("parseMessageRef parses m9999 (4-digit) to index 9999", () => { assert.equal(parseMessageRef("m9999"), 9999) }) +test("parseMessageRef parses m99999 to index 99999", () => { + assert.equal(parseMessageRef("m99999"), 99999) +}) + test("parseMessageRef is case-insensitive", () => { - assert.equal(parseMessageRef("M0001"), 1) + assert.equal(parseMessageRef("M00001"), 1) }) test("parseMessageRef trims whitespace", () => { - assert.equal(parseMessageRef(" m0001 "), 1) + assert.equal(parseMessageRef(" m00001 "), 1) }) test("parseMessageRef returns null for invalid format", () => { assert.equal(parseMessageRef("m0"), null) - assert.equal(parseMessageRef("m10000"), null) + assert.equal(parseMessageRef("m100000"), null) assert.equal(parseMessageRef("abc"), null) assert.equal(parseMessageRef(""), null) }) test("parseMessageRef returns null for wrong digit count", () => { assert.equal(parseMessageRef("m001"), null) - assert.equal(parseMessageRef("m00001"), null) + assert.equal(parseMessageRef("m0000001"), null) }) // --- Tests for parseBlockRef --- @@ -201,12 +213,12 @@ test("parseBlockRef returns null for invalid format", () => { // --- Tests for parseBoundaryId --- test("parseBoundaryId parses message ref to message kind", () => { - const result = parseBoundaryId("m0005") + const result = parseBoundaryId("m00005") assert.ok(result) assert.equal(result!.kind, "message") if (result!.kind === "message") { assert.equal(result!.index, 5) - assert.equal(result!.ref, "m0005") + assert.equal(result!.ref, "m00005") } }) @@ -228,7 +240,7 @@ test("parseBoundaryId returns null for unrecognized input", () => { // --- Tests for assignMessageRefs --- -test("assignMessageRefs assigns sequential mNNNN IDs to new messages", () => { +test("assignMessageRefs assigns sequential mNNNNN IDs to new messages", () => { const state = makeState() const msg1 = makeMessage({ id: "raw-a" }) const msg2 = makeMessage({ id: "raw-b" }) @@ -237,12 +249,12 @@ test("assignMessageRefs assigns sequential mNNNN IDs to new messages", () => { const count = assignMessageRefs(state, [msg1, msg2, msg3]) assert.equal(count, 3) - assert.equal(state.messageIds.byRawId.get("raw-a"), "m0001") - assert.equal(state.messageIds.byRawId.get("raw-b"), "m0002") - assert.equal(state.messageIds.byRawId.get("raw-c"), "m0003") - assert.equal(state.messageIds.byRef.get("m0001"), "raw-a") - assert.equal(state.messageIds.byRef.get("m0002"), "raw-b") - assert.equal(state.messageIds.byRef.get("m0003"), "raw-c") + assert.equal(state.messageIds.byRawId.get("raw-a"), "m00001") + assert.equal(state.messageIds.byRawId.get("raw-b"), "m00002") + assert.equal(state.messageIds.byRawId.get("raw-c"), "m00003") + assert.equal(state.messageIds.byRef.get("m00001"), "raw-a") + assert.equal(state.messageIds.byRef.get("m00002"), "raw-b") + assert.equal(state.messageIds.byRef.get("m00003"), "raw-c") }) test("assignMessageRefs preserves existing IDs on second call", () => { @@ -250,14 +262,14 @@ test("assignMessageRefs preserves existing IDs on second call", () => { const msg1 = makeMessage({ id: "raw-a" }) assignMessageRefs(state, [msg1]) - assert.equal(state.messageIds.byRawId.get("raw-a"), "m0001") + assert.equal(state.messageIds.byRawId.get("raw-a"), "m00001") const msg2 = makeMessage({ id: "raw-b" }) const count = assignMessageRefs(state, [msg1, msg2]) assert.equal(count, 1) - assert.equal(state.messageIds.byRawId.get("raw-a"), "m0001") - assert.equal(state.messageIds.byRawId.get("raw-b"), "m0002") + assert.equal(state.messageIds.byRawId.get("raw-a"), "m00001") + assert.equal(state.messageIds.byRawId.get("raw-b"), "m00002") }) test("assignMessageRefs skips messages with empty or missing IDs", () => { @@ -269,7 +281,7 @@ test("assignMessageRefs skips messages with empty or missing IDs", () => { const count = assignMessageRefs(state, [msg1Original, msg2]) assert.equal(count, 1) - assert.equal(state.messageIds.byRawId.get("raw-good"), "m0001") + assert.equal(state.messageIds.byRawId.get("raw-good"), "m00001") }) test("assignMessageRefs skips DCP synthetic message IDs", () => { @@ -281,7 +293,7 @@ test("assignMessageRefs skips DCP synthetic message IDs", () => { const count = assignMessageRefs(state, [msg1, msg2, msg3]) assert.equal(count, 1) - assert.equal(state.messageIds.byRawId.get("raw-normal"), "m0001") + assert.equal(state.messageIds.byRawId.get("raw-normal"), "m00001") assert.ok(!state.messageIds.byRawId.has("msg_dcp_summary_123")) assert.ok(!state.messageIds.byRawId.has("msg_dcp_text_456")) }) @@ -299,20 +311,20 @@ test("assignMessageRefs skips ignored user messages", () => { assert.equal(count, 1) assert.ok(!state.messageIds.byRawId.has("ignored-user")) - assert.equal(state.messageIds.byRawId.get("raw-normal"), "m0001") + assert.equal(state.messageIds.byRawId.get("raw-normal"), "m00001") }) test("assignMessageRefs handles gap in nextRef by scanning for free slot", () => { const state = makeState() state.messageIds.nextRef = 1 - state.messageIds.byRef.set("m0001", "already-used") - state.messageIds.byRawId.set("already-used", "m0001") + state.messageIds.byRef.set("m00001", "already-used") + state.messageIds.byRawId.set("already-used", "m00001") const msg = makeMessage({ id: "raw-new" }) const count = assignMessageRefs(state, [msg]) assert.equal(count, 1) - assert.equal(state.messageIds.byRawId.get("raw-new"), "m0002") + assert.equal(state.messageIds.byRawId.get("raw-new"), "m00002") }) test("assignMessageRefs skips first user message in sub-agent mode", () => { @@ -325,11 +337,46 @@ test("assignMessageRefs skips first user message in sub-agent mode", () => { assert.equal(count, 2) assert.ok(!state.messageIds.byRawId.has("sub-prompt")) - assert.equal(state.messageIds.byRawId.get("raw-asst"), "m0001") - assert.equal(state.messageIds.byRawId.get("raw-user2"), "m0002") + assert.equal(state.messageIds.byRawId.get("raw-asst"), "m00001") + assert.equal(state.messageIds.byRawId.get("raw-user2"), "m00002") }) test("assignMessageRefs returns 0 for empty message array", () => { const state = makeState() assert.equal(assignMessageRefs(state, []), 0) }) + +// --- Backward compatibility: 4-digit → 5-digit ref migration --- + +test("parseBoundaryId normalizes 4-digit ref to 5-digit", () => { + const result = parseBoundaryId("m0001") + assert.ok(result !== null) + assert.equal(result.kind, "message") + assert.equal(result.ref, "m00001") + assert.equal(result.index, 1) +}) + +test("parseBoundaryId normalizes 4-digit ref m9999 to 5-digit m09999", () => { + const result = parseBoundaryId("m9999") + assert.ok(result !== null) + assert.equal(result.kind, "message") + assert.equal(result.ref, "m09999") + assert.equal(result.index, 9999) +}) + +test("4-digit to 5-digit migration roundtrip: parseMessageRef then formatMessageRef", () => { + // This is the exact pattern used in state.ts ensureSessionInitialized + const oldRef = "m0001" + const parsed = parseMessageRef(oldRef) + assert.equal(parsed, 1) + const newRef = formatMessageRef(parsed!) + assert.equal(newRef, "m00001") + assert.notEqual(newRef, oldRef) // Confirms migration needed +}) + +test("5-digit refs are unchanged by roundtrip", () => { + const ref = "m00001" + const parsed = parseMessageRef(ref) + const normalized = formatMessageRef(parsed!) + assert.equal(normalized, ref) +}) diff --git a/tests/message-priority.test.ts b/tests/message-priority.test.ts index 64fff9e..7d69242 100644 --- a/tests/message-priority.test.ts +++ b/tests/message-priority.test.ts @@ -215,17 +215,17 @@ test("injectMessageIds injects ID into every tool output for assistant messages" // User messages: still injected into all text parts assert.match( (userTextOne as any).text, - /\n\nm0001<\/dcp-message-id>/, + /\n\nm00001<\/dcp-message-id>/, ) assert.match( (userTextTwo as any).text, - /\n\nm0001<\/dcp-message-id>/, + /\n\nm00001<\/dcp-message-id>/, ) // Assistant messages: ID injected into every tool output assert.doesNotMatch((assistantTextOne as any).text, /dcp-message-id/) - assert.match((assistantToolOne as any).state.output, /m0002<\/dcp-message-id>/) + assert.match((assistantToolOne as any).state.output, /m00002<\/dcp-message-id>/) assert.doesNotMatch((assistantTextTwo as any).text, /dcp-message-id/) - assert.match((assistantToolTwo as any).state.output, /m0002<\/dcp-message-id>/) + assert.match((assistantToolTwo as any).state.output, /m00002<\/dcp-message-id>/) }) test("injectMessageIds marks every protected user text part as BLOCKED in message mode", () => { @@ -277,7 +277,7 @@ test("injectMessageIds marks every protected user text part as BLOCKED in messag assert.doesNotMatch((userTextTwo as any).text, /priority=/) assert.match( (assistantText as any).text, - /\n\nm0002<\/dcp-message-id>/, + /\n\nm00002<\/dcp-message-id>/, ) }) @@ -320,9 +320,9 @@ test("injectMessageIds injects ID into every tool output in range mode", () => { // Every tool output gets the ID assert.doesNotMatch((assistantTextOne as any).text, /dcp-message-id/) - assert.match((assistantToolOne as any).state.output, /m0002<\/dcp-message-id>/) + assert.match((assistantToolOne as any).state.output, /m00002<\/dcp-message-id>/) assert.doesNotMatch((assistantTextTwo as any).text, /dcp-message-id/) - assert.match((assistantToolTwo as any).state.output, /m0002<\/dcp-message-id>/) + assert.match((assistantToolTwo as any).state.output, /m00002<\/dcp-message-id>/) }) test("message mode marks compress tool messages as high priority even when short", () => { @@ -364,10 +364,10 @@ test("message mode marks compress tool messages as high priority even when short // ID injected into tool output, not the text part assert.doesNotMatch((assistantText as any).text, /dcp-message-id/) - assert.match((assistantTool as any).state.output, /m0002<\/dcp-message-id>/) + assert.match((assistantTool as any).state.output, /m00002<\/dcp-message-id>/) assert.match( (assistantTool as any).state.output, - /m0002<\/dcp-message-id>/, + /m00002<\/dcp-message-id>/, ) }) @@ -413,10 +413,10 @@ test("message-mode nudges append to existing text parts and list only earlier vi assert.equal(injectedNudge?.type, "text") assert.match((injectedNudge as any).text, /\n\nBase context nudge/) assert.match((injectedNudge as any).text, /Message priority context:/) - // m0001 (user, 6000 tokens) and m0002 (assistant, 6000 tokens) are both high priority - assert.match((injectedNudge as any).text, /High-priority message IDs before this point: m0001, m0002/) - assert.doesNotMatch((injectedNudge as any).text, /m0003/) - assert.doesNotMatch((injectedNudge as any).text, /m0004/) + // m00001 (user, 6000 tokens) and m00002 (assistant, 6000 tokens) are both high priority + assert.match((injectedNudge as any).text, /High-priority message IDs before this point: m00001, m00002/) + assert.doesNotMatch((injectedNudge as any).text, /m00003/) + assert.doesNotMatch((injectedNudge as any).text, /m00004/) }) test("message-mode nudges exclude protected user messages from priority guidance", () => { @@ -452,8 +452,8 @@ test("message-mode nudges exclude protected user messages from priority guidance const injectedNudge = messages[2]?.parts[0] assert.equal(injectedNudge?.type, "text") - assert.match((injectedNudge as any).text, /High-priority message IDs before this point: m0002/) - assert.doesNotMatch((injectedNudge as any).text, /m0001/) + assert.match((injectedNudge as any).text, /High-priority message IDs before this point: m00002/) + assert.doesNotMatch((injectedNudge as any).text, /m00001/) }) test("range-mode nudges append to existing text parts before tool outputs", () => { @@ -680,8 +680,8 @@ test("message-mode rendered compressed summaries mark block IDs as BLOCKED", () mode: "range", topic: "Earlier notes", batchTopic: "Earlier notes", - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", anchorMessageId: "msg-user-1", compressMessageId: "msg-origin", includedBlockIds: [], @@ -730,8 +730,8 @@ test("range-mode rendered compressed summaries keep block IDs", () => { mode: "range", topic: "Earlier notes", batchTopic: "Earlier notes", - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", anchorMessageId: "msg-user-1", compressMessageId: "msg-origin", includedBlockIds: [], @@ -758,8 +758,8 @@ test("range-mode rendered compressed summaries keep block IDs", () => { test("hallucination stripping removes all dcp-prefixed XML tags including variants", async () => { const text = "alpha" + - 'm0008' + - 'm0008' + + 'm00008' + + 'm00008' + "strip this" + "strip this too" + "omega" @@ -803,7 +803,7 @@ test("hallucination stripping handles nested dcp tags", async () => { test("hallucination stripping handles mixed paired and orphan tags", async () => { assert.equal( stripHallucinationsFromString( - 'text\nm0045\n\n', + 'text\nm00045\n\n', ), "text\n\n\n", ) diff --git a/tests/prompts.test.ts b/tests/prompts.test.ts index 8e1a19b..25249df 100644 --- a/tests/prompts.test.ts +++ b/tests/prompts.test.ts @@ -111,7 +111,7 @@ test("prompt store exposes bundled message-mode compress prompt", () => { assert.match(runtimePrompts.compressMessage, /selected individual messages/i) assert.match( runtimePrompts.compressMessage, - /Only use raw message IDs of the form `mNNNN`\./, + /Only use raw message IDs of the form `mNNNNN`\./, ) assert.match(runtimePrompts.compressMessage, /priority="high"/) assert.match(runtimePrompts.compressMessage, /high-priority messages/i) @@ -128,7 +128,7 @@ test("compress-message overrides preserve plain-text metadata mentions", () => { [ "Override body.", "", - 'Each message has an ID inside XML metadata tags like `m0007`.', + 'Each message has an ID inside XML metadata tags like `m00007`.', "Messages marked as `BLOCKED` cannot be compressed.", ].join("\n"), "compress-message.md", @@ -140,7 +140,7 @@ test("compress-message overrides preserve plain-text metadata mentions", () => { assert.match(runtimePrompts.compressMessage, /Override body\./) assert.match( runtimePrompts.compressMessage, - /m0007<\/dcp-message-id>/, + /m00007<\/dcp-message-id>/, ) assert.match(runtimePrompts.compressMessage, /BLOCKED<\/dcp-message-id>/) } finally { diff --git a/tests/token-counting.test.ts b/tests/token-counting.test.ts index 03e9439..dd93167 100644 --- a/tests/token-counting.test.ts +++ b/tests/token-counting.test.ts @@ -51,7 +51,7 @@ test("counting includes input for large built-in tool calls", () => { input: { topic: "Compression topic", content: [ - { messageId: "m0001", topic: "Prior work", summary: "Compressed summary" }, + { messageId: "m00001", topic: "Prior work", summary: "Compressed summary" }, ], }, output: "compressed", diff --git a/tests/token-usage.test.ts b/tests/token-usage.test.ts index 549edea..688e9ad 100644 --- a/tests/token-usage.test.ts +++ b/tests/token-usage.test.ts @@ -186,8 +186,8 @@ function createActiveBlock( mode: "message", topic: `Summary ${blockId}`, batchTopic: `Summary ${blockId}`, - startId: "m0001", - endId: "m0001", + startId: "m00001", + endId: "m00001", anchorMessageId: `msg-${blockId}`, compressMessageId: `compress-${blockId}`, includedBlockIds: [],