Wire OpenCode into the session-log collector + backfill (CROW-1096) - #1104
Conversation
b05485e to
553b08f
Compare
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
-
Red — collector reads a JSON object store OpenCode 1.18.x no longer writes.
OpenCodeHome.sessionDir()(Packages/CrowOpenCode/Sources/CrowOpenCode/OpenCodeHome.swift:39-42) andOpenCodeAgent.logSources(Packages/CrowOpenCode/Sources/CrowOpenCode/OpenCodeAgent.swift:205-208) point at~/.local/share/opencode/storage/session/**/ses_*.json.OpenCodeStore.sessionFiles(Packages/CrowCore/Sources/CrowCore/LogSync/OpenCodeStore.swift:123-135) enumerates that tree; if it is missing, the enumerator returns[]and the live collector silently skips (Packages/CrowDaemon/Sources/CrowDaemon/LogSyncCollector.swift:117-121). Docs (docs/session-log-collector.md) present this layout as the wired location.Verified (reproduced): OpenCode 1.18.15 (in Crow's documented 1.17.10+/1.18.x window) on this machine has no
~/.local/share/opencode/storage/directory. Sessions live in~/.local/share/opencode/opencode.db(SQLite): 34sessionrows, 392messagerows, 1829partrows, withsession.directoryandsession.parent_idas the attribution columns this PR expected on JSON anchors. Current upstream session persistence is SQLite (Database), notStorage.write(["session", …]). A sweep against this install uploads nothing.The new tests pass because they stage a synthetic
storage/{project,session,message,part}tree that the live harness does not create. PointlogSources/ backfill atopencode.db(and keep a JSON fallback only if 1.17 still writes it).
Architecture / Existing Patterns
- Existing pathway:
CodingAgent.logSources→LogSyncCollector.resolveFiles(cwdFilter) →TranscriptNormalizer.normalize, as extended for Codex (CROW-1089). Extending that pipeline with a format-aware cwd probe is the right shape. - Red (same defect as above): the on-disk source is the wrong current OpenCode format.
AgentLogFormat.sqlitealready exists as the Cursor placeholder (Packages/CrowCore/Sources/CrowCore/Agent/AgentLogSource.swift:12-18); 1.18.x OpenCode is a SQLite store (opencode.db, relationalsession/message/part), not a scattered JSON object store..openCodeStore+ the JSON reassembler invent a parallel reader for a layout that is gone. Schema differs from Cursor's blobstore.db, so this still needs an OpenCode-specific SQLite normalizer — but it should sit on the existing.sqlite(or a sibling) path overOpenCodeHome.dataDir()/opencode.db, notstorage/session/.
Security Review
Strengths:
- Attribution stays exact-cwd, drop-on-miss — no guessing a worktree from a slug (
OpenCodeStore.cwd(ofSessionFile:)returns nil for children and for missing directory). - Child/subagent sessions (
parentID/isChild) are excluded, matching Claude'ssubagents/rule. - Upload still goes only through the workspace's local-only gateway;
.openCodeStoreis stamped.logDirso an unknown format never hits the server enum (AgentLogFormat.artifactStamp). - Opt-in remains default-off.
Concerns:
- None beyond the Red: a silent no-op is not an exfil risk, but it also means the security invariant "we upload what this worktree actually ran" is vacuously true (nothing is uploaded).
Code Quality
- Yellow — truncated session 1 can be followed by session 2.
TranscriptNormalizer.reassembleOpenCodeStore(Packages/CrowCore/Sources/CrowCore/LogSync/TranscriptNormalizer.swift:117-136) ORswasTruncatedand continues the loop.OpenCodeStore.reassembleleaves leftover budget when a large line will not fit, so the next cwd-matched session's header can be appended after a cut transcript. CodexconcatenateNDJSONstops at the cap (break). If the JSON reassembler is kept as a legacy fallback, break afterwasTruncated(and add a two-session fixture under a tight cap). - Tests never open a real
opencode.db, so they cannot catch the layout miss. A fixture copied from 1.18.x schema (or a "storage dir absent → zero files" case against the live path) should be part of the fix. - Green:
XDG_DATA_HOMEempty-string handling matches the existingXDG_CONFIG_HOMEguard. Message/part filename sort is correct for OpenCode's ascendingmsg_/prt_ids (sessions themselves are descending ULIDs, but those are not used as the concat order — mtime is).
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by 1 Red, 1 Yellow, 1 Green findings.
f28b6ae to
ed30051
Compare
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
-
Red — live collector and backfill read a JSON object store current OpenCode no longer writes.
OpenCodeHome.sessionDir()(Packages/CrowOpenCode/Sources/CrowOpenCode/OpenCodeHome.swift:39-42) andOpenCodeAgent.logSources(Packages/CrowOpenCode/Sources/CrowOpenCode/OpenCodeAgent.swift:205-208) point at~/.local/share/opencode/storage/session/**/ses_*.json.OpenCodeStore.sessionFiles(Packages/CrowCore/Sources/CrowCore/LogSync/OpenCodeStore.swift:123-135) enumerates that tree; a missing directory makes the enumerator return[], andLogSyncCollectorsilently skips (Packages/CrowDaemon/Sources/CrowDaemon/LogSyncCollector.swift:120-121). Docs present this layout as the wired location (docs/session-log-collector.md:46).Verified (reproduced): OpenCode 1.18.15 (inside Crow's documented
1.17.10+/ 1.18.x window indocs/agent-harness-matrix.md) on this machine has no~/.local/share/opencode/storage/directory. Sessions live in~/.local/share/opencode/opencode.db(SQLite): 34sessionrows, 392messagerows, 1829partrows. The attribution columns this PR expected on JSON anchors aresession.directoryandsession.parent_id. A sweep against this install resolves zero files and uploads nothing.New tests pass because they stage a synthetic
storage/{project,session,message,part}tree the live harness does not create. PointlogSources/ backfill atopencode.db(keep a JSON fallback only if a still-supported 1.17 build actually writes it — 1.17.3 already used SQLite).
Architecture / Existing Patterns
- Existing pathway:
CodingAgent.logSources→LogSyncCollector.resolveFiles(cwdFilter) →TranscriptNormalizer.normalize, as extended for Codex (CROW-1089). Extending that pipeline with a format-aware cwd probe is the right shape; this is not a parallel collector. - Red (same defect as above): the on-disk source is the wrong current OpenCode format.
AgentLogFormat.sqlitealready exists as the Cursor placeholder (Packages/CrowCore/Sources/CrowCore/Agent/AgentLogSource.swift:12-18). 1.18.x OpenCode is a SQLite store (opencode.db, relationalsession/message/part), not a scattered JSON object store..openCodeStoreplus the JSON reassembler invent a reader for a layout that is gone. Schema differs from Cursor's blobstore.db, so this still needs an OpenCode-specific SQLite normalizer — but it should sit on the existing.sqlite(or a sibling) path overOpenCodeHome.dataDir()/opencode.db, notstorage/session/.
Security Review
Strengths:
- Attribution stays exact-cwd, drop-on-miss — no guessing a worktree from a slug (
OpenCodeStore.cwd(ofSessionFile:)returns nil for children and for a missingdirectory). - Child/subagent sessions (
parentID/isChild) are excluded, matching Claude'ssubagents/rule. - Upload still goes only through the workspace's local-only gateway;
.openCodeStoreis stamped.logDirso an unknown format never hits the server enum (AgentLogFormat.artifactStamp). - Opt-in remains default-off.
Concerns:
- None beyond the Red: a silent no-op is not an exfil risk, but the security invariant "we upload what this worktree actually ran" is vacuously true (nothing is uploaded).
Code Quality
- Yellow — after a truncated session, leftover byte budget can start the next session.
TranscriptNormalizer.reassembleOpenCodeStore(Packages/CrowCore/Sources/CrowCore/LogSync/TranscriptNormalizer.swift:117-136) ORswasTruncatedand continues the loop.OpenCodeStore.reassembleleaves unused budget when the next line will not fit, so the following cwd-matched session's header can be appended after a cut transcript. CodexconcatenateNDJSONstops at the cap (break/break outer, same file ~81-91). If the JSON reassembler is kept as a legacy fallback, break afterwasTruncatedand add a two-session fixture under a tight cap. - Tests never open a real
opencode.db, so they cannot catch the layout miss. A fixture copied from 1.18.x schema (or a "storage dir absent → zero files" case against the live path) should be part of the fix. - Green: empty
XDG_DATA_HOME=is treated as unset, matching the existingXDG_CONFIG_HOMEguard (OpenCodeHome.swift:20-23). Message/part filename sort is correct for OpenCode's ascendingmsg_/prt_ids. Child-session exclusion andartifactStamp → .logDirare well-tested on the synthetic tree.
swift test on this PR's new suites is green: CrowCore OpenCodeStore / artifactStamp (8), CrowOpenCode (65), CrowDaemon LogSync/Backfill OpenCode cases (21). That green is against staged JSON, not against current OpenCode.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by 1 Red, 1 Yellow, 3 Green findings.
ed30051 to
4f08504
Compare
|
Thanks — the Red finding was correct and I've reworked the PR around it (pushed in 3fd2eba). Red — wrong on-disk format. I independently reproduced it: this machine's OpenCode was an ancient 1.1.29 (JSON Rewrote
Platform. The reader needs the Yellow — truncation. The SQLite reassembler now Tests now build a real |
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None. The previous Red (JSON storage/ tree vs the live opencode.db SQLite store) is fixed: OpenCodeHome.databasePath() (Packages/CrowOpenCode/Sources/CrowOpenCode/OpenCodeHome.swift:32-36) points at <dataDir>/opencode.db, and OpenCodeStore (Packages/CrowCore/Sources/CrowCore/LogSync/OpenCodeStore.swift:234-270) reads session.directory / parent_id / time_created and message/part.data with bound parameters. Verified against a live OpenCode 1.17+ opencode.db on this machine: WAL sidecars present, 34 session / 392 message / 1829 part rows; PRAGMA table_info(session) matches the reader (directory, parent_id, time_created, time_updated). Child rows (parent_id set) are dropped; a missing cwd is dropped, never guessed.
The previous Yellow (truncated session 1 followed by session 2's header) is also fixed: emitSessions (OpenCodeStore.swift:165-174) breaks the session loop, covered by truncationStopsAndDoesNotStartNextSession.
Architecture / Existing Patterns
-
Existing pathway:
CodingAgent.logSources→LogSyncCollector.resolveFiles→ format-aware normalize. Claude/Codex go throughTranscriptNormalizerfile concatenation; OpenCode correctly bypasses that for a cwd/session-id selectornormalize(files:)cannot express (LogSyncCollector.swift:141-143). This is an extension of the existing pipeline, not a parallel collector. -
.openCodeStoreas an internal discriminator that stamps.logDiron upload (AgentLogFormat.artifactStamp) is the right sibling to Cursor's unimplemented.sqlite— different schema, different selector. Do not collapse them. -
Yellow — file mtime is the wrong quiescence signal for a shared WAL database.
OpenCodeAgent.logSources(Packages/CrowOpenCode/Sources/CrowOpenCode/OpenCodeAgent.swift:205-208) returns a single.filesource atopencode.db.resolveFiles(LogSyncCollector.swift:271-274) returns only that path. The quiet-period gate (LogSyncCollector.swift:123-128) then usesnewestModification(files)— the main file'scontentModificationDate— to decide whether a still-running Crow session is idle enough to upload (write-once 409).Verified (reproduced): this install's OpenCode store is WAL (
opencode.db-wal/opencode.db-shm). A throwaway WAL SQLite insert did not change the main.dbmtime; only the-walmtime moved. The collector never inspects the WAL sidecar. An in-progress OpenCode session can therefore look idle for longer thanquietPeriodMinuteswhile new messages sit in the WAL; the collector uploads a snapshot, and the server's write-once 409 makes that snapshot permanent. The comments already note the DB is shared across every worktree when omittingagentSessionID(LogSyncCollector.swift:150-152); that same fact makes global file mtime the wrong grain.Key quiescence on
MAX(time_updated)of the cwd-matched top-levelsessionrows (the analogue of Codex's per-rollout file mtime). Including-walin the mtime set without a cwd filter would still confuse this worktree with every other OpenCode session on the machine.
Security Review
Strengths:
- Exact-cwd attribution; missing
directoryandparent_idchildren dropped — no guessing. - Read-only
sqlite3_open_v2+SQLITE_TRANSIENTbinds; session id is a bound?, not string-concatenated into SQL. - Upload still only via the workspace's local-only gateway;
.openCodeStorenever hits the server enum. - Opt-in remains default-off.
Concerns:
- None beyond the Yellow: a premature write-once upload is an integrity issue (incomplete transcript locked in), not an exfil issue.
Code Quality
- Tests now build a real SQLite fixture (
OpenCodeDBFixture) and cover enumeration, cwd probe, child exclusion, ordering, multi-session concat, truncation,OpenCodeHomeXDG (including emptyXDG_DATA_HOME=),logSourcesshape, and backfill ledger keying / upload.swift test --filter OpenCodeis green on CrowCore (10), CrowOpenCode, and CrowDaemon (2). Rootswift buildlinkscrowd. - Linux
#elseno-op is documented and matches the macOS-only daemon. - Green:
OpenCodeDBFixtureis duplicated in CrowCore and CrowDaemon — a maintainability nit, not a defect.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by 0 Red, 1 Yellow, 1 Green findings.
3fd2eba to
9e852a4
Compare
|
Thanks — addressed the Yellow (pushed in d16d92c). Yellow — WAL / shared-DB quiescence. You're right that the Fixed by keying the OpenCode quiescence gate on New tests (all gated to Green — duplicated |
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None. Prior Red (JSON storage/ tree vs live opencode.db) and Yellows (truncation continuing to the next session; opencode.db file mtime as the WAL quiescence signal) are fixed.
Architecture / Existing Patterns
- Existing pathway:
CodingAgent.logSources→LogSyncCollector.resolveFiles→ format-aware normalize. Claude/Codex/Grok go throughTranscriptNormalizerfile concatenation; OpenCode correctly bypasses that for a cwd/session-id selectornormalize(files:)cannot express (Packages/CrowDaemon/Sources/CrowDaemon/LogSyncCollector.swift). This extends the existing pipeline rather than inventing a parallel collector. .openCodeStoreas an internal discriminator that stamps.logDiron upload (AgentLogFormat.artifactStampinPackages/CrowCore/Sources/CrowCore/Agent/AgentLogSource.swift) remains the right sibling to Cursor's unimplemented.sqlite.- Prior Yellow on shared-WAL quiescence is addressed:
OpenCodeStore.newestActivity(Packages/CrowCore/Sources/CrowCore/LogSync/OpenCodeStore.swift) keys idle-ness on cwd-matched session/message/part timestamps instead of the main-file mtime, and the collector actually calls it. Tests cover a stale session row plus a newer message, cwd scoping, and child exclusion.
Security Review
Strengths:
- Exact-cwd attribution; missing
directoryandparent_idchildren dropped — no guessing (Packages/CrowCore/Sources/CrowCore/LogSync/OpenCodeStore.swift,Packages/CrowCore/Sources/CrowCore/LogSync/BackfillScanner.swift). - Read-only
sqlite3_open_v2+SQLITE_TRANSIENTbinds; session id is a bound?. Table-name interpolation inmaxTimeCreatedis a compile-time"message"/"part"constant, documented as such. - Upload still only via the workspace's local-only gateway;
.openCodeStorenever hits the server enum. - Opt-in remains default-off.
Concerns:
- None blocking. Upstream also has a V2
session_messagetable; this reader usesmessage/part, which a prior review verified are populated on a live 1.17+opencode.db. If OpenCode ever writes onlysession_message, both reassembly and quiescence would go empty/stale — a version-pin target, not a defect in this change.
Code Quality
- Green — leftover duplicated comments in
Packages/CrowCore/Sources/CrowCore/LogSync/BackfillScanner.swift("Three harnesses" then "Four harnesses") andPackages/CrowDaemon/Sources/CrowDaemon/LogSyncCollector.swift/Packages/CrowDaemon/Sources/CrowDaemon/BackfillService.swift. Harmless merge residue. - Green —
OpenCodeDBFixtureduplicated across CrowCore and CrowDaemon test targets. Author declined sharing (separate packages; a test-support module is disproportionate). Restating as Green only. - Green —
readMessages/readPartsload the whole session beforemaxBytescan stop fetching (Packages/CrowCore/Sources/CrowCore/LogSync/OpenCodeStore.swift).concatenateNDJSONreads incrementally against the same cap. Output is still bounded; consider a cursor if realparttables get huge. - Green —
sizeOverride: 0on OpenCode backfill rows (Packages/CrowCore/Sources/CrowCore/LogSync/BackfillScanner.swift) so the UI does not show the whole-DB size on every session. Conscious tradeoff. - Green — the GitHub PR description still describes the JSON object store (
storage/session/**/ses_*.json, recursivelogSources). The code and in-repo docs are SQLite; worth updating the GitHub body so the next reader is not sent to a layout that is gone.
Static analysis: swift test was not re-run in this pass (environment flake); the author reports CrowCore / CrowOpenCode / CrowDaemon green after the quiescence fix, matching the prior review's swift test --filter OpenCode result.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Approve — driven by 0 Red, 0 Yellow, 5 Green findings.
Closes #1096 Wires OpenCode end-to-end into the session-log collector (CROW-1056) and historical backfill (CROW-1075) — live upload AND backfill — as a further harness alongside Claude, Codex, and Grok. OpenCode 1.17.10+ (Crow's documented window) keeps every session in a single SQLite database, `~/.local/share/opencode/opencode.db`, with relational session/message/part tables (verified against upstream sst/opencode packages/core/src/session/sql.ts). The pre-1.17 JSON object store under `<dataDir>/storage/` is legacy that upstream migrates into the DB on upgrade, so only the database is read. - OpenCodeStore (CrowCore) reads opencode.db: session(id, parent_id, directory, title, time_created, time_updated), message(id, session_id, time_created, data) ordered by (time_created, id), part(id, message_id, data) ordered by id. Attribution is the `session.directory` column (absolute cwd); child (parent_id) and cwd-less rows are dropped. Each row's JSON `data` is re-serialized compact into NDJSON. - The source is a single `.file` at opencode.db (`.openCodeStore`, stamped `.logDir` on upload). The live collector selects cwd-matched top-level sessions and reassembles their rows; backfill reassembles one session by id. - Quiescence: the `opencode.db` file mtime is the wrong idle signal (OpenCode runs the store in WAL mode — commits land in the -wal sidecar without bumping the main file; and one DB is shared by every worktree). The collector instead keys the OpenCode quiet-period gate on OpenCodeStore.newestActivity, the newest write time of this worktree's cwd-matched top-level sessions (max of session time_updated/created and its newest message/part time_created — append-only, so they move on every turn). Claude/Codex/Grok keep file-mtime quiescence. Platform: the reader needs the SQLite3 module. The daemon that runs the collector is macOS-only (SQLite3 is an SDK module — no new dependency, no CI change). On the Linux CI lane (compiles crowd/crow, runs no daemon) OpenCodeStore is a compiled no-op and its behavioral tests are gated to canImport(SQLite3). Tests build a real opencode.db fixture (OpenCodeDBFixture): enumeration, cwd probe (child/missing-dir dropped), ordering, multi-session concat, truncation break, newestActivity (incl. the WAL/stale-session-row case and cwd scoping), and end-to-end backfill upload. swift test green on macOS (CrowCore, CrowOpenCode, CrowDaemon LogSync/Backfill); crowd/crow link; CLIDocs generated-doc check passes. Docs updated (session-log-collector, session-backfill, cli-reference, cli, harness-transcript-locations, CLAUDE.md). 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: 155D929E-E113-4B30-B981-F5FE9D878C51
d16d92c to
49804c4
Compare
Closes #1096
Wires OpenCode end-to-end into the session-log collector (CROW-1056) and historical backfill (CROW-1075) — live upload and backfill — the third harness after Claude and Codex (CROW-1089). Follows the pattern of PR #1092 (Codex).
The shape problem — why OpenCode needed a reassembler
Claude and Codex each write one NDJSON transcript per session with the cwd on a head line, so the shared pipeline fits them:
AgentLogCwdReaderreads the cwd from the head,TranscriptNormalizerconcatenates the files.OpenCode is a multi-file object store. A single session is scattered across:
— all pretty-printed single-object JSON, with the cwd on the session anchor's
directoryfield (fallback the project'sworktree), not a head line. The head-cwd reader and file-concatenation normalizer don't fit, so a reassembler was needed.How
OpenCodeStore(CrowCore) — a pure reader: parses a session anchor (cwd + parentID), enumerates sessions, and reassembles a session'smessage+partrecords into ordered NDJSON — session header, then each message in id order, each followed by its parts in id order. OpenCode'smsg_/prt_ids are monotonic ascending by creation time (verified against a live store: a filename sort equals atime.createdsort).AgentLogFormat.openCodeStore— an internal normalization discriminator. The reassembled artifact is NDJSON, so it uploads stamped as.logDirvia the newartifactStamp; the server never seesopenCodeStore.TranscriptNormalizerreassembles for.openCodeStore;LogSyncCollector's cwd filter is now format-aware — OpenCode parses the whole session anchor fordirectoryand drops child/subagent sessions.OpenCodeAgent.logSourcesreturns a recursive, cwd-filtered.openCodeStoresource over thesession/tree (path viaOpenCodeHome,$XDG_DATA_HOME-aware).BackfillScannerreconstructs OpenCode sessions (harness.opencode, cwd from the anchor, no git branch → ticket from the worktree name);BackfillServicekeys ledger / upload / format /agentKindoff the harness.Attribution invariant
Exact cwd match; a session with no readable cwd is dropped, never guessed. Child/subagent sessions (
parentID != nil) are excluded — they belong to a parent, mirroring Claude'ssubagents/exclusion.Acceptance
swift testgreen; new tests cover the object-store reassembler and OpenCode backfill reconstruction.Testing
swift testgreen: CrowCore (775), CrowOpenCode (65), and the CrowDaemon LogSync/Backfill suites. New tests cover the object-store reassembler (ordering, byte cap, child exclusion, project-worktree fallback),OpenCodeHomeXDG resolution, the.openCodeStorelogSourcesshape, OpenCode backfill reconstruction + harness-scoped ledger keying, and the collector's OpenCode cwd filter. Rootswift build(crow+crowd) links clean.Note
Two CrowDaemon tests fail on the base branch already, unrelated to this change and outside its diff:
RPCLanePolicyTests(lane drift for the pre-existingterminal-set/corveil-verify/corveil-reinstall-skillverbs, CROW-1085/CROW-1011) andWebNotificationCenterTests.bootCatchResetsTheHistory(anapp.jsanchor string). This PR touches no RPC handlers, lanes, or web assets.🤖 Generated with Claude Code