feat(modules): add HistoryModule for full uncompressed message history - #151
Conversation
Exposes three read-only agent tools backed by context-manager's new
native chronicle secondary-index queries (queryMessagesByTime /
queryMessagesByChannel / queryMessagesByTimeAndChannel /
getChannelMessageCounts / getChannelTokenStats, @animalabs/context-manager
>= 0.6.0):
- `stats` — per-channel message counts (all-time) + token totals
(range-scoped), for orienting before pulling raw content.
- `extract` — paginated raw messages for a time range and/or channel,
rendered as readable text by default or raw content
blocks on request.
- `search` — substring/regex match over a narrowed candidate window,
with an explicit truncation signal when the caller's
filter was too broad for maxScan rather than silently
missing matches past the cap.
These queries are O(log n + k) against chronicle's native field
indexes, not a full-store scan, so the module stays fast against
multi-million-message production stores. Kept deliberately read-only
(no side effects), same posture as HealthModule — bind(contextManager)
after AgentFramework.create(), mirroring HealthModule.bind().
Requires the companion chronicle (native secondary field index) and
context-manager (query wrappers) changes to actually exercise the
native path; degrades to a clear tool error (not a crash) against an
older chronicle build lacking the capability.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI status note"Changelog entry present": fixed (fragment added in 9ba878c). "Build & Test" ( Verified locally against real builds of both companion branches (chronicle#17's native module + context-manager's Suggested merge order: chronicle#17 → publish → context-manager 🤖 Generated with Claude Code |
…ronicle to ^0.4.0 context-manager 0.9.0 published (anima-research/context-manager#99) — ships the queryMessagesByTime/queryMessagesByChannel/etc. this module's HistoryModule depends on. Also bumps this package's own direct chronicle dependency to ^0.4.0: leaving it at ^0.3.0 while context-manager pulled in 0.4.0 produced two separate installed chronicle copies (npm can't dedupe across an unsatisfied range), which made TypeScript see two structurally-identical-but-distinct JsStore types and fail to compile anywhere this package's own code touches a JsStore. Bumping both to ^0.4.0 lets npm dedupe to one copy. Unblocks CI: tsc --noEmit was failing outright against the old published context-manager (missing exports); now clean, and the full suite passes (677/681, 4 pre-existing unrelated skips) against the real published dependency chain. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Bumped both `@animalabs/context-manager` (^0.8.0 → ^0.9.0, now published: anima-research/context-manager#99) and this package's own direct `@animalabs/chronicle` (^0.3.0 → ^0.4.0) in 9485f16. The chronicle bump was necessary too, not just cosmetic: leaving this package's own direct chronicle dependency at ^0.3.0 while context-manager pulled in 0.4.0 left npm with two separate installed chronicle copies (an unsatisfied range can't be deduped), and TypeScript saw two structurally-identical-but-distinct `JsStore` types — failing to compile anywhere this package's own code (`framework.ts`, `recovery/offline-branch.ts`, tests) touches a `JsStore`. Bumping both to `^0.4.0` lets npm dedupe to one shared copy. CI is now fully green: all 4 platform/Node-version jobs pass, changelog check passes. 🤖 Generated with Claude Code |
|
Recommendation: hold merge until the three findings below are fixed and regression-tested. The published dependency chain builds and the existing tests pass, but real-store probes expose a channel-schema mismatch, a pagination cap bypass, and an unbounded synchronous regex path. Reviewed head:
Validation:
— Reviewed with OpenAI Codex. |
Two review findings on #151's HistoryModule (head 9485f16): - [P1] search's regex:true path ran the caller's pattern via a synchronous RegExp.exec() on the main thread — a catastrophically-backtracking pattern (e.g. (a+)+$) blocks the framework's single-threaded event loop for every agent, not just the calling search. A Promise.race/setTimeout can't interrupt this (nothing on the same thread can preempt synchronous JS), so regex matching now runs on a one-shot node:worker_threads worker (src/modules/history/search-regex-worker.ts) with a hard wall-clock deadline; a stuck worker is forcibly terminate()'d and the call surfaces as a clean tool error, never a silently-empty result. Follows the existing gate-script.ts/gate-script-worker.ts precedent for sandboxing script-shaped execution in this repo, simplified to a one-shot request/response (handleSearch was already async, so no need for gate-script's Atomics.wait synchronous-emulation machinery). Plain substring search is unaffected (linear, in-process, no ReDoS risk) — only regex mode pays the worker-spawn cost. - [P2] extract/search capped limit/offset/maxScan with bare Math.min(value, max), which only enforces an upper bound — Math.min(-1, 200) is -1. A negative limit/offset/maxScan reached context-manager's native pagination calls unbounded (repro: extract({limit:-1}) against a 250-message store returned all 250). Replaced with clampCount(), which rejects any non-finite/non-integer/ negative value as a clean tool error before any native call is made. Adds regression coverage: the reviewer's ReDoS repro (asserts the call returns in bounded time with a clear failure, not a hung test or an empty success), a control regex to prove the worker path still matches correctly, and 18 negative/non-integer/NaN/Infinity cases across extract's limit/offset and search's limit/maxScan, including the reviewer's exact 250-message/248-candidate repro shapes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ation Two review findings on the HistoryModule PR: - ReDoS: an arbitrary caller-supplied regex was matched synchronously on the framework's event loop. A catastrophically-backtracking pattern (e.g. (a+)+$ against 32 'a's) blocks not just this tool call but every agent's turns, health checks, and timers, since Node is single-threaded and a Promise timeout cannot interrupt an in-flight synchronous RegExp.exec. Fixed by moving regex-mode matching (only regex mode — substring search is inherently linear and stays in-process) to a one-shot worker thread (search-regex-worker.ts) with a hard wall-clock deadline; the worker is always terminate()'d in a finally block whether it finished, errored, or had to be killed mid-backtrack. Follows this repo's existing gate-script.ts/gate-script-worker.ts precedent for the same class of problem (bounding caller-influenced execution) rather than inventing a new mechanism. - Negative pagination: Math.min(value, max) only enforces an upper bound, so limit:-1/maxScan:-2 reached native chronicle calls unbounded, defeating the advertised hard caps (verified: a 250-message store returned all 250 on limit:-1). Fixed with clampCount(), which rejects any limit/offset/maxScan that isn't a finite non-negative integer before any context-manager call. Also: getChannelId() now checks metadata.channelId before metadata.external.channelId, matching context-manager 0.9.1's dual-schema channel index (bumped below) — a channel-filtered result found via the newer schema no longer displays channelId: null. Bumps @animalabs/context-manager to ^0.9.1 (anima-research/context-manager now indexes and merges both channel-id metadata shapes — 0.9.0 only indexed metadata.external.channelId, which real agent-framework MCPL ingestion never writes, so HistoryModule silently found nothing for real framework history; see that repo's 0.9.1 changelog). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
All three findings fixed. 1. [P1] Channel schema mismatch — fixed upstream, not in this repo. Confirmed: this was a context-manager bug (its native channel index only covered `metadata.external.channelId`, but agent-framework's real `handleMcplChannelIncoming` ingestion writes `metadata.channelId` directly). Fixed and published as `@animalabs/context-manager@0.9.1` (indexes and merges BOTH schemas now — see that repo's 0.9.1 changelog), which this PR now depends on (`^0.9.1`, bumped in 465c80b). Also fixed `getChannelId()` here to check the same two schemas in the same order, so a channel-filtered result found via the newer schema doesn't display `channelId: null`. 2. [P1] ReDoS via unbounded synchronous regex — fixed in a55b788. Regex-mode search (only regex mode — substring search is inherently linear, stays in-process, common case pays no overhead) now runs in a one-shot worker thread (`search-regex-worker.ts`) with a hard wall-clock deadline; the worker is always `terminate()`'d in a `finally` block whether it finished, errored, or had to be killed mid-backtrack. Confirmed your Promise-timeout observation was correct and didn't attempt that approach — went straight to real OS-level concurrency. Followed this repo's own existing precedent for the same class of problem (`src/gate/gate-script.ts`/`gate-script-worker.ts`, used for agent-authored `gate.js`) rather than inventing a new mechanism or adding a new dependency (checked — nothing in this monorepo already pulls in re2/an equivalent). New test reproduces your exact repro (32×'a' + '!', `(a+)+$`, `maxScan:1`) and asserts a bounded return time with a clean `isError` failure, not a hang or silent empty success, plus a control case proving the worker path still matches correctly. 3. [P2] Negative pagination bypass — fixed in a55b788. New `clampCount()` rejects any `limit`/`offset`/`maxScan` that isn't a finite, non-negative integer, checked before any context-manager call — verified against your exact repro shapes (250-message store, 248-candidate pool) with an assertion that zero native calls are made before rejection. Verification: `npx tsc --noEmit` clean, full `npm test` 788/792 pass, 0 failures, 4 pre-existing unrelated skips. 🤖 Generated with Claude Code |
|
Recommendation: hold merge for the P2 channel-counting regression in the newly selected context-manager dependency. All three original review probes now pass: normal MCPL channel queries work, negative pagination is rejected, and the pathological regex returns a clean timeout from the worker. Two smaller pagination edge cases are also listed below. Re-reviewed head:
Verification:
— Re-reviewed with OpenAI Codex. |
Two more small review findings on #151's HistoryModule (head 465c80b): - [P3] search's limit:0 wasn't honored in either matching loop (the in-process substring loop in index.ts, and search-regex-worker.ts's own independent copy of the same loop): both pushed a match onto the results array BEFORE checking matches.length >= limit, so with limit:0 (a value clampCount already accepts — it's >= 0) and at least one matching candidate, both paths still returned exactly 1 match instead of 0. Moved the limit check to before any per-candidate work in both loops. - [P3] extract's offset validation ceiling was Number.MAX_SAFE_INTEGER, but the native chronicle call it reaches takes a u32 offset (max 4294967295). A value between the u32 max and MAX_SAFE_INTEGER passed clampCount unchanged, then wrapped/truncated crossing the N-API boundary — repro: extract({offset: 4294967296, limit: 1}) against a real 1-message store "succeeded" and returned page 0 instead of an empty page or a validation error. Added NATIVE_OFFSET_MAX (0xFFFFFFFF) as the offset ceiling, clamped the same way limit already is (consistent clamp-not-reject behavior). Adds regression coverage: limit:0 in both substring and regex mode (asserts zero matches, not one), and offset:4294967296 asserted clamped to 4294967295 before it ever reaches context-manager. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
All three findings fixed. 1. [P2] getChannelCounts double-counting — fixed upstream. Context-manager's dual-schema fix in 0.9.1 correctly unioned ordinals for extraction/token-stats, but `getChannelCounts` was summing the two native indexes' counts, double-counting a message that legitimately carries both `metadata.channelId` AND `metadata.external.channelId` (a real shape — MCPL ingestion preserves incoming `metadata.external` while also adding its own top-level `metadata.channelId`). Fixed and published as `@animalabs/context-manager@0.9.2`: `getChannelCounts` now reuses the already-correct `queryChannelOrdinals` union (same logic `queryByChannel` relies on) per distinct channel value instead of summing native counts. This PR now depends on `^0.9.2` (bumped in 57ec676). 2. [P3] `search`'s `limit:0` not honored — fixed in b0058ef. Both matching loops (in-process substring, and the worker's independent copy) were appending a match before checking the limit. Moved the check to before any per-candidate work in both places. New tests: `limit:0` against a real matching candidate returns zero matches, for both substring and regex/worker paths. 3. [P3] Offset beyond native u32 range wrapped instead of erroring — fixed in b0058ef. Added `NATIVE_OFFSET_MAX = 0xffffffff`, `extract`'s offset `clampCount` call now bounds to that instead of `Number.MAX_SAFE_INTEGER`. New test reproduces your exact repro (`offset: 4294967296`) and asserts the value actually reaching context-manager is clamped to `4294967295`, not the raw wrap-prone input. Verification: `npx tsc --noEmit` clean, full `npm test` 823/827 pass, 0 failures, 4 pre-existing unrelated skips. 🤖 Generated with Claude Code |
|
Recommendation: merge. All remaining findings are resolved at I reviewed the pagination changes and the dependency's updated channel-count implementation, then replayed all six review probes. They all pass:
No new actionable findings in these fixes. Validation on the reviewed head:
— Re-reviewed with OpenAI Codex. |
…at/operator-surgery-log Conflicts in src/framework.ts were option-bag and return-shape unions: updateAgentRuntimeSettings opts gain both allowInfeasible and requester/note; nudge/unstick keep the quiesced hint AND the operator-log record; the scheduler busy test keeps the surgery hold with main's quiesce comment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Adds
HistoryModule, exposing three read-only agent tools backed by context-manager's new native chronicle secondary-index queries (queryMessagesByTime/queryMessagesByChannel/queryMessagesByTimeAndChannel/getChannelMessageCounts/getChannelTokenStats, requires@animalabs/context-manager>= 0.6.0 with the companion history-index PR):stats— per-channel message counts (all-time) + token totals (range-scoped), for orienting before pulling raw content.extract— paginated raw messages for a time range and/or channel, rendered as readable text by default (format:"raw"for unmodified content blocks).search— substring/regex match over a narrowed candidate window, with an explicittruncatedsignal when the caller's filter was too broad formaxScan— never silently drops matches past the cap.These queries are O(log n + k) against chronicle's native field indexes, not a full-store scan, so the module stays responsive against multi-million-message production stores (Mythos/Sol scale). Kept deliberately read-only (no side effects, no message mutation), same posture as
HealthModule—bind(contextManager)afterAgentFramework.create(), mirroringHealthModule.bind()sinceModuleContextitself exposes no store/context-manager reference.Degrades gracefully (a clean
ToolResulterror, not a crash) against an older chronicle build that lacks the native index-query capability.Dependencies
Needs both companion PRs to exercise the native path end-to-end:
feat/chronicle-history-indexbranchNot wired into any resident's recipe — module enablement is host-app-level config (see
forking-knowledge-miner's per-module recipe gate pattern), left for a separate follow-up.Test plan
npm run typecheckclean (verified against the two companion branches' local builds)test/history-module.test.ts: 17 tests covering all three tools (stats shape, extract date/channel filtering + format + limit capping, search substring/regex/invalid-regex/maxScan truncation, unbound-module and capability-absent error paths)origin/main(no file overlap with this repo's other in-flight local work)🤖 Generated with Claude Code