feat: session replay tab + JSONL transcript import#155
Conversation
Adds a Replay tab to the viewer that plays any stored session as a scrubbable timeline (prompts, tool calls, tool results, responses) with play/pause, speed control, and keyboard navigation. Adds `agentmemory import-jsonl [path]` CLI subcommand and a matching POST /agentmemory/replay/import-jsonl endpoint to backfill sessions from Claude Code JSONL transcripts (default ~/.claude/projects). All new logic routes through iii-sdk primitives: registerFunction for mem::replay::load / mem::replay::sessions / mem::replay::import-jsonl, and the existing HTTP trigger in api.ts for REST exposure. Security: JSONL import rejects symlinks, paths containing sensitive terms, and tolerates malformed lines without aborting the batch. All imports are recorded in the audit log.
📝 WalkthroughWalkthroughAdds session replay: viewer "Replay" tab with scrubbable timelines and keyboard controls, JSONL import via CLI and REST, backend replay SDK functions/KV storage, JSONL parsing and timeline projection, package version bumped to 0.8.13. Changes
Sequence Diagram(s)sequenceDiagram
participant UserCLI as User (CLI)
participant CLI as CLI Handler
participant API as Service API
participant KV as KV Store
participant Audit as Audit Log
UserCLI->>CLI: agentmemory import-jsonl [path]
CLI->>API: GET /agentmemory/livez (2s probe)
API-->>CLI: 200 OK
CLI->>API: POST /agentmemory/replay/import-jsonl { path?, maxFiles? }
API->>API: validate path (sensitive/symlink)
API->>API: discover .jsonl files
loop for each discovered file
API->>API: parse JSONL → observations
API->>KV: write session metadata
API->>KV: write observations (by id)
end
API->>Audit: safeAudit(import metadata)
API-->>CLI: 202 { imported, sessionIds, observations }
CLI->>UserCLI: print success + viewer URL
sequenceDiagram
participant User as Browser User
participant Viewer as Replay Tab UI
participant API as Service API
participant KV as KV Store
User->>Viewer: switch to Replay tab
Viewer->>API: GET /agentmemory/replay/sessions
API->>KV: read sessions
KV-->>API: sessions[]
API-->>Viewer: sessions[]
User->>Viewer: select session
Viewer->>API: GET /agentmemory/replay/load?sessionId=...
API->>KV: fetch session + observations
KV-->>API: session + observations[]
API->>API: projectTimeline(observations)
API-->>Viewer: timeline { events, startedAt, totalDurationMs }
User->>Viewer: press Play
Viewer->>Viewer: start timer loop (100ms)
loop until end
Viewer->>Viewer: advance offset, update cursor, render
end
Viewer->>Viewer: stop playback
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (2)
src/functions/export-import.ts (1)
179-180: Centralize the export version allowlist.This list now has to stay in sync with
ExportData.versioninsrc/types.ts, which makes every release bump a multi-file footgun. Hoist the versions into one sharedconstand derive both the runtimeSetand the type from it.Based on learnings: When bumping version, update
src/types.ts(ExportData version union) andsrc/functions/export-import.ts(supportedVersions set).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/functions/export-import.ts` around lines 179 - 180, Extract the hard-coded version list into a single exported constant array (e.g., EXPORT_VERSIONS) and import it where needed; replace the local supportedVersions Set in export-import.ts with new Set(EXPORT_VERSIONS) (so the runtime check uses that shared array) and change the ExportData.version union in src/types.ts to derive its type from the same array (e.g., export type ExportVersion = typeof EXPORT_VERSIONS[number]; then use ExportVersion in ExportData). Update references to supportedVersions and ExportData.version to use the shared symbols (EXPORT_VERSIONS and ExportVersion) so a single update bumps both runtime and type checks.src/replay/jsonl-parser.ts (1)
172-179: Capture the fallbacknowtimestamp once.
new Date().toISOString()is evaluated twice here as independent fallbacks. As per coding guidelines — "Capture timestamps once withnew Date().toISOString()and reuse instead of calling Date multiple times" — hoist a single value sostartedAtandendedAtuse the same fallback.♻️ Proposed fix
+ const nowIso = new Date().toISOString(); return { sessionId: effectiveSessionId, project: deriveProject(cwd), cwd: cwd || process.cwd(), - startedAt: firstTs || new Date().toISOString(), - endedAt: lastTs || new Date().toISOString(), + startedAt: firstTs || nowIso, + endedAt: lastTs || nowIso, observations, };As per coding guidelines: "Capture timestamps once with
new Date().toISOString()and reuse instead of calling Date multiple times".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/replay/jsonl-parser.ts` around lines 172 - 179, The two fallbacks call new Date().toISOString() independently; capture one timestamp once and reuse it so startedAt and endedAt share the same fallback value. In the function that returns the object (referencing keys sessionId, project via deriveProject(cwd), cwd, startedAt, endedAt, observations and variables effectiveSessionId/firstTs/lastTs), introduce a single variable (e.g., nowIso) assigned to new Date().toISOString() before the return and use nowIso as the fallback for both startedAt and endedAt (i.e., startedAt: firstTs || nowIso, endedAt: lastTs || nowIso).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli.ts`:
- Around line 829-833: The import POST fetch call (const res = await
fetch(`${base}/agentmemory/replay/import-jsonl`, { ... })) can hang
indefinitely; wrap it with an AbortSignal.timeout and pass the signal into fetch
so it aborts after a generous timeout (matching the /livez check). Create a
signal via AbortSignal.timeout(<ms>) and add it to the fetch options (signal),
and ensure any spinner/error handling treats an AbortError as a timeout; update
the fetch invocation that uses base, headers, and body to include this signal.
In `@src/functions/replay.ts`:
- Around line 17-29: The current isSensitive() uses raw substring matches in
SENSITIVE_PATH_TERMS (including "token"), causing false positives for paths like
"jsonwebtoken-demo"; update SENSITIVE_PATH_TERMS and isSensitive() to use
scoped, case-insensitive pattern matching instead of plain includes — e.g.,
replace the raw "token" entry with a boundary-aware regex or explicit filename
patterns and implement matching in isSensitive() using regex tests that check
path/word boundaries and path separators (for example, a regex like
/(^|[\\/_.-])token([\\/_.-]|$)/i or explicit checks for filenames like ".env",
"id_rsa", "*_secret*", etc.), ensuring other entries still match correctly.
- Around line 208-213: The loop currently awaits each kv.set sequentially for
parsed.observations (using kv.set and KV.observations(parsed.sessionId)),
causing N serial KV round-trips; replace the per-iteration await with collecting
promises (e.g., map parsed.observations to kv.set(...) promises), await
Promise.all on that array, and then increment observationCount by the number of
observations (or sum results) after the Promise.all completes; keep the existing
sessionIds.push(parsed.sessionId) behavior and ensure any error handling around
the batched writes is preserved.
- Around line 161-178: The directory-walked files returned by findJsonlFiles()
are not re-checked for sensitive paths; add a per-file sensitive check inside
the import loop: after skipping symlinks (the isSymlink(file) check) call
isSensitive(file) and skip/return an error when true (mirroring the existing
behavior used for the root abs), ensuring you reference the same variables
(files, file, abs) and keep the check before reading/processing each file so
descendants with sensitive names are not imported.
- Around line 193-206: The current import only creates a Session when none
exists, so importing into an existing session leaves Session.observationCount
stale; modify the logic after fetching existing via kv.get<Session>(KV.sessions,
parsed.sessionId) to handle the existing case by updating
existing.observationCount (e.g., existing.observationCount +=
parsed.observations.length or recomputing from KV.observations if you prefer
accuracy), update any changed metadata (endedAt/status if needed), then persist
the updated session with await kv.set(KV.sessions, existing.id, existing) so the
viewer reflects the new count; reference the existing variable, KV.sessions, and
parsed.observations in the fix.
- Line 146: The ternary assigning abs is redundant and doesn't expand a leading
'~', causing paths like '~/file.jsonl' to resolve incorrectly; replace the
isAbsolute(...) ? resolve(...) : resolve(...) expression with a single resolve
call that first expands a leading '~' (e.g., if rawPath startsWith('~'), replace
the leading '~' with os.homedir()), then call resolve(expandedPath) and assign
to abs; update references to rawPath/abs accordingly and remove the unnecessary
isAbsolute check.
In `@src/triggers/api.ts`:
- Around line 365-372: Validation trims whitespace but payload currently assigns
the raw string; change the assignment so payload.path receives the trimmed value
(use body.path.trim()) instead of body.path to ensure downstream lookups don't
fail due to leading/trailing whitespace; update the assignment to payload.path =
body.path.trim() in the same block that validates body.path.
- Around line 322-394: You added three HTTP routes in triggers/api.ts (functions
"api::replay::load", "api::replay::sessions", "api::replay::import") which push
the public REST count past the hardcoded value; update the startup banner in
src/index.ts and the API/docs count in README.md to reflect the new total
(increment the previous REST endpoint count by 3) so the displayed endpoint
totals are accurate.
In `@src/version.ts`:
- Line 1: The VERSION union in export const VERSION is missing the "0.7.9" and
"0.8.0" literals that are present on ExportData.version in src/types.ts; update
the VERSION type union to include "0.7.9" and "0.8.0" so both enumerations stay
in sync (keep the assigned value "0.8.13" as-is).
In `@src/viewer/index.html`:
- Around line 3186-3211: The replay interval started by startReplayTimer keeps
running after switching away from the replay tab; update switchTab to call
stopReplayTimer (clearing state.replay.timer via stopReplayTimer) when leaving
the 'replay' tab, or modify the tick in startReplayTimer to short-circuit
immediately if state.activeTab !== 'replay' (and avoid calling renderReplay or
updating state.replay.offsetAt), referencing the functions startReplayTimer,
stopReplayTimer, switchTab, state.replay.timer, state.activeTab, and
renderReplay to locate the changes.
- Line 3129: The placeholder HTML uses class="empty" which has no CSS, so update
the markup to reuse the existing .empty-state styles (and its expected inner
structure) or add a matching .empty rule; specifically replace occurrences of
<div class="empty"> (and its closing tag) with the .empty-state structure that
includes an inner element for .empty-icon and a <p> for the hint text, or
alternatively add a CSS rule for .empty mirroring .empty-state styling so the
Replay tab hint renders styled; ensure you update both occurrences where
class="empty" appears.
---
Nitpick comments:
In `@src/functions/export-import.ts`:
- Around line 179-180: Extract the hard-coded version list into a single
exported constant array (e.g., EXPORT_VERSIONS) and import it where needed;
replace the local supportedVersions Set in export-import.ts with new
Set(EXPORT_VERSIONS) (so the runtime check uses that shared array) and change
the ExportData.version union in src/types.ts to derive its type from the same
array (e.g., export type ExportVersion = typeof EXPORT_VERSIONS[number]; then
use ExportVersion in ExportData). Update references to supportedVersions and
ExportData.version to use the shared symbols (EXPORT_VERSIONS and ExportVersion)
so a single update bumps both runtime and type checks.
In `@src/replay/jsonl-parser.ts`:
- Around line 172-179: The two fallbacks call new Date().toISOString()
independently; capture one timestamp once and reuse it so startedAt and endedAt
share the same fallback value. In the function that returns the object
(referencing keys sessionId, project via deriveProject(cwd), cwd, startedAt,
endedAt, observations and variables effectiveSessionId/firstTs/lastTs),
introduce a single variable (e.g., nowIso) assigned to new Date().toISOString()
before the return and use nowIso as the fallback for both startedAt and endedAt
(i.e., startedAt: firstTs || nowIso, endedAt: lastTs || nowIso).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c33773b8-4e58-4379-9d79-386aed1806e0
📒 Files selected for processing (20)
CHANGELOG.mdREADME.mdpackage.jsonpackages/mcp/package.jsonplugin/.claude-plugin/plugin.jsonsrc/cli.tssrc/functions/export-import.tssrc/functions/replay.tssrc/index.tssrc/replay/jsonl-parser.tssrc/replay/timeline.tssrc/triggers/api.tssrc/types.tssrc/version.tssrc/viewer/index.htmltest/export-import.test.tstest/fixtures/jsonl/basic.jsonltest/fixtures/jsonl/errors.jsonltest/fixtures/jsonl/tool-use.jsonltest/replay.test.ts
| sdk.registerFunction("api::replay::load", | ||
| async (req: ApiRequest): Promise<Response> => { | ||
| const authErr = checkAuth(req, secret); | ||
| if (authErr) return authErr; | ||
| const sessionId = asNonEmptyString(req.query_params?.["sessionId"]); | ||
| if (!sessionId) { | ||
| return { status_code: 400, body: { error: "sessionId is required" } }; | ||
| } | ||
| const result = await sdk.trigger({ | ||
| function_id: "mem::replay::load", | ||
| payload: { sessionId }, | ||
| }); | ||
| return { status_code: 200, body: result }; | ||
| }, | ||
| ); | ||
| sdk.registerTrigger({ | ||
| type: "http", | ||
| function_id: "api::replay::load", | ||
| config: { api_path: "/agentmemory/replay/load", http_method: "GET" }, | ||
| }); | ||
|
|
||
| sdk.registerFunction("api::replay::sessions", | ||
| async (req: ApiRequest): Promise<Response> => { | ||
| const authErr = checkAuth(req, secret); | ||
| if (authErr) return authErr; | ||
| const result = await sdk.trigger({ function_id: "mem::replay::sessions" }); | ||
| return { status_code: 200, body: result }; | ||
| }, | ||
| ); | ||
| sdk.registerTrigger({ | ||
| type: "http", | ||
| function_id: "api::replay::sessions", | ||
| config: { api_path: "/agentmemory/replay/sessions", http_method: "GET" }, | ||
| }); | ||
|
|
||
| sdk.registerFunction("api::replay::import", | ||
| async ( | ||
| req: ApiRequest<{ path?: string; maxFiles?: number }>, | ||
| ): Promise<Response> => { | ||
| const authErr = checkAuth(req, secret); | ||
| if (authErr) return authErr; | ||
| const body = (req.body ?? {}) as Record<string, unknown>; | ||
| const payload: { path?: string; maxFiles?: number } = {}; | ||
| if (body.path !== undefined) { | ||
| if (typeof body.path !== "string" || body.path.trim().length === 0) { | ||
| return { | ||
| status_code: 400, | ||
| body: { error: "path must be a non-empty string" }, | ||
| }; | ||
| } | ||
| payload.path = body.path; | ||
| } | ||
| if (body.maxFiles !== undefined) { | ||
| if (!Number.isInteger(body.maxFiles) || (body.maxFiles as number) < 1) { | ||
| return { | ||
| status_code: 400, | ||
| body: { error: "maxFiles must be a positive integer" }, | ||
| }; | ||
| } | ||
| payload.maxFiles = body.maxFiles as number; | ||
| } | ||
| const result = await sdk.trigger({ | ||
| function_id: "mem::replay::import-jsonl", | ||
| payload, | ||
| }); | ||
| return { status_code: 202, body: result }; | ||
| }, | ||
| ); | ||
| sdk.registerTrigger({ | ||
| type: "http", | ||
| function_id: "api::replay::import", | ||
| config: { api_path: "/agentmemory/replay/import-jsonl", http_method: "POST" }, | ||
| }); |
There was a problem hiding this comment.
Update the hardcoded REST endpoint counts.
These three new routes push the public REST total past 104, so the startup banner in src/index.ts and the API/docs copy in README.md are stale again.
As per coding guidelines: When adding REST endpoints, update: triggers/api.ts, index.ts, and README.md with endpoint counts.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/triggers/api.ts` around lines 322 - 394, You added three HTTP routes in
triggers/api.ts (functions "api::replay::load", "api::replay::sessions",
"api::replay::import") which push the public REST count past the hardcoded
value; update the startup banner in src/index.ts and the API/docs count in
README.md to reflect the new total (increment the previous REST endpoint count
by 3) so the displayed endpoint totals are accurate.
| '</div>' + | ||
| '<div class="replay-detail">' + renderReplayDetail(cursorEvent) + '</div>' + | ||
| '</div>' | ||
| : '<div class="empty">Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</div>'); |
There was a problem hiding this comment.
Placeholder element uses an undefined CSS class.
class="empty" has no matching style in this file (only .empty-state is defined earlier). The empty-state hint for the Replay tab will render unstyled. Either reuse .empty-state (with the inner .empty-icon/<p> structure) or add an .empty rule.
💅 Proposed fix
- : '<div class="empty">Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</div>');
+ : '<div class="empty-state"><p>Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</p></div>');Same applies to class="empty" at Line 3136.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| : '<div class="empty">Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</div>'); | |
| : '<div class="empty-state"><p>Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</p></div>'); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/viewer/index.html` at line 3129, The placeholder HTML uses class="empty"
which has no CSS, so update the markup to reuse the existing .empty-state styles
(and its expected inner structure) or add a matching .empty rule; specifically
replace occurrences of <div class="empty"> (and its closing tag) with the
.empty-state structure that includes an inner element for .empty-icon and a <p>
for the hint text, or alternatively add a CSS rule for .empty mirroring
.empty-state styling so the Replay tab hint renders styled; ensure you update
both occurrences where class="empty" appears.
Security: - Tighten sensitive-path guard to boundary-aware regexes (no more false positive on project names containing "token", "secret", etc.) - Re-check every walked JSONL file against the guard, not just the root argument - Expand leading "~" in user-supplied paths before resolve() Correctness: - Update existing Session.observationCount on re-import instead of silently leaving it stale - Parallelize per-session observation writes via Promise.all - Capture a single fallback timestamp in the parser so startedAt and endedAt agree when metadata is missing - Trim incoming path in the import REST handler Robustness: - Add AbortSignal.timeout(120s) to the CLI import fetch and surface TimeoutError cleanly - Stop the replay interval when switching away from the replay tab - Style the Replay tab empty state with a new .empty rule Sync: - Bump REST endpoint count (104 -> 107) in startup banner + README - Add 0.7.9 and 0.8.0 to VERSION union so it matches ExportData.version Tests: - Add replay-sensitive.test.ts covering false-positive avoidance - Add parser test for shared fallback timestamp
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/functions/replay.ts (1)
148-150:⚠️ Potential issue | 🟡 Minor
~userstyle paths are mishandled.
rawPath.startsWith("~")matches~alice/fooand thenslice(1)yieldsalice/foo, which gets joined under the current user's home — silently redirecting a different-user lookup rather than rejecting it. Guard against this by restricting expansion to exactly~or a~/prefix.🛡️ Proposed fix
- const expanded = rawPath.startsWith("~") - ? join(homedir(), rawPath.slice(1)) - : rawPath; + const expanded = + rawPath === "~" || rawPath.startsWith("~/") + ? join(homedir(), rawPath.slice(1).replace(/^\/+/, "")) + : rawPath;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/functions/replay.ts` around lines 148 - 150, The path expansion incorrectly treats "~alice/foo" as the current user's home; update the expansion logic in replay.ts where rawPath is converted to expanded so it only expands for exactly "~" or paths starting with "~/". Replace the condition rawPath.startsWith("~") with a strict check (rawPath === "~" || rawPath.startsWith("~/")) and continue to use join(homedir(), rawPath.slice(1)) only in that allowed case so "~user" style paths are left unchanged (or handled/rejected elsewhere).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/functions/replay.ts`:
- Around line 17-27: The existing SENSITIVE_PATH_PATTERNS entry for "secret"
doesn't match directories named "secrets" because the current alternation only
permits an optional "s" at end-of-string; update the pattern in
SENSITIVE_PATH_PATTERNS so it accepts either "secret" or "secrets" when followed
by a path separator or end-of-string (e.g., change the "secret" pattern to allow
the plural before a separator or $), ensuring the regex still respects the
existing anchors and separators; modify the entry inside the
SENSITIVE_PATH_PATTERNS array so functions referencing that constant will
correctly catch "secrets/" directories.
In `@src/replay/jsonl-parser.ts`:
- Line 103: The code computes fallback timestamps per-iteration with const ts =
entry.timestamp || new Date().toISOString();; instead capture a single fallback
value before the loop (e.g., const fallbackTs = new Date().toISOString();) and
then inside the loop use const ts = entry.timestamp || fallbackTs; to ensure all
entries in the batch share the same ISO timestamp and follow the project's
timestamp guideline.
---
Duplicate comments:
In `@src/functions/replay.ts`:
- Around line 148-150: The path expansion incorrectly treats "~alice/foo" as the
current user's home; update the expansion logic in replay.ts where rawPath is
converted to expanded so it only expands for exactly "~" or paths starting with
"~/". Replace the condition rawPath.startsWith("~") with a strict check (rawPath
=== "~" || rawPath.startsWith("~/")) and continue to use join(homedir(),
rawPath.slice(1)) only in that allowed case so "~user" style paths are left
unchanged (or handled/rejected elsewhere).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9560e91a-190e-4ed0-a8e7-0b2fde0cb361
📒 Files selected for processing (10)
README.mdsrc/cli.tssrc/functions/replay.tssrc/index.tssrc/replay/jsonl-parser.tssrc/triggers/api.tssrc/version.tssrc/viewer/index.htmltest/replay-sensitive.test.tstest/replay.test.ts
✅ Files skipped from review due to trivial changes (2)
- test/replay.test.ts
- src/triggers/api.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- README.md
- src/version.ts
- src/viewer/index.html
- src/cli.ts
| const SENSITIVE_PATH_PATTERNS: RegExp[] = [ | ||
| /(^|[\\/_.-])secret([\\/_.-]|s?$)/i, | ||
| /(^|[\\/_.-])credentials?([\\/_.-]|$)/i, | ||
| /(^|[\\/_.-])private[_-]?key([\\/_.-]|$)/i, | ||
| /(^|[\\/])\.env(\.[\w-]+)?$/i, | ||
| /(^|[\\/_.-])id_rsa([\\/_.-]|$)/i, | ||
| /(^|[\\/])auth[_-]?token([\\/_.-]|$)/i, | ||
| /(^|[\\/])bearer[_-]?token([\\/_.-]|$)/i, | ||
| /(^|[\\/])access[_-]?token([\\/_.-]|$)/i, | ||
| /(^|[\\/])api[_-]?token([\\/_.-]|$)/i, | ||
| ]; |
There was a problem hiding this comment.
secrets/ directory not caught by the secret pattern.
/(^|[\\/_.-])secret([\\/_.-]|s?$)/i accepts s? only when anchored at end-of-string. A path like /repo/secrets/foo.jsonl fails both branches (s is not a separator, and s isn't followed by $), so a transcript tucked under a secrets/ directory will sail through the guard. Consider allowing the plural before a separator as well.
🛡️ Proposed fix
- /(^|[\\/_.-])secret([\\/_.-]|s?$)/i,
+ /(^|[\\/_.-])secrets?([\\/_.-]|$)/i,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/functions/replay.ts` around lines 17 - 27, The existing
SENSITIVE_PATH_PATTERNS entry for "secret" doesn't match directories named
"secrets" because the current alternation only permits an optional "s" at
end-of-string; update the pattern in SENSITIVE_PATH_PATTERNS so it accepts
either "secret" or "secrets" when followed by a path separator or end-of-string
(e.g., change the "secret" pattern to allow the plural before a separator or $),
ensuring the regex still respects the existing anchors and separators; modify
the entry inside the SENSITIVE_PATH_PATTERNS array so functions referencing that
constant will correctly catch "secrets/" directories.
| for (const entry of entries) { | ||
| if (entry.sessionId && !sessionId) sessionId = entry.sessionId; | ||
| if (entry.cwd && !cwd) cwd = entry.cwd; | ||
| const ts = entry.timestamp || new Date().toISOString(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Capture the fallback timestamp once before the loop.
new Date().toISOString() is evaluated on every iteration when entry.timestamp is missing, producing slightly different ISO strings for entries in the same batch and violating the project's timestamp guideline.
♻️ Proposed fix
+ const nowIso = new Date().toISOString();
const observations: RawObservation[] = [];
for (const entry of entries) {
if (entry.sessionId && !sessionId) sessionId = entry.sessionId;
if (entry.cwd && !cwd) cwd = entry.cwd;
- const ts = entry.timestamp || new Date().toISOString();
+ const ts = entry.timestamp || nowIso;
@@
- const nowIso = new Date().toISOString();
return {As per coding guidelines: "Capture timestamps once with new Date().toISOString() and reuse instead of calling Date multiple times".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/replay/jsonl-parser.ts` at line 103, The code computes fallback
timestamps per-iteration with const ts = entry.timestamp || new
Date().toISOString();; instead capture a single fallback value before the loop
(e.g., const fallbackTs = new Date().toISOString();) and then inside the loop
use const ts = entry.timestamp || fallbackTs; to ensure all entries in the batch
share the same ISO timestamp and follow the project's timestamp guideline.
Summary
agentmemory import-jsonl [path]CLI andPOST /agentmemory/replay/import-jsonlendpoint backfill sessions from Claude Code JSONL transcripts. Default scans~/.claude/projects; pass an explicit file or directory to scope it.mem::replay::load,mem::replay::sessions,mem::replay::import-jsonl— exposed through the existing API trigger. No side-channel servers; everything flows through iii-sdk v0.11.Why
agentmemory already captures every observation (hookType, tool_name, tool_input, tool_output) per session. Replay is the missing visualization for data the engine already has — one extra tab, zero new storage, with an import path for transcripts that predate installation.
Security
secret,credential,.env,id_rsa,token).safeAudit.test/viewer-security.test.tsstill passes.Test plan
npx vitest run test/replay.test.ts— 9/9 (parser + timeline projection + malformed-line tolerance + pacing)npx vitest run test/viewer-security.test.ts— 2/2 (CSP regression)npx vitest run test/compress-file.test.ts test/export-import.test.ts— 13/13pnpm testnpx tsc --noEmit— no new errors in touched fileshttp://localhost:3113, Replay tab, pick session, verify playback/step/speed/keyboardagentmemory import-jsonl ~/.claude/projects/<something>/<sess>.jsonlthen replay itFiles
src/functions/replay.ts,src/replay/jsonl-parser.ts,src/replay/timeline.ts,test/replay.test.ts,test/fixtures/jsonl/*.jsonlsrc/viewer/index.html(tab + CSS + replay runtime),src/triggers/api.ts(3 REST routes),src/cli.ts(import-jsonlsubcommand + help),src/index.ts(wireregisterReplayFunctions), version bumps (0.8.13), CHANGELOG, READMESummary by CodeRabbit
New Features
Security