Skip to content

Commit 8ae2f92

Browse files
authored
feat(parser): add Omnigent support (#1284)
Supersedes #1167 — same feature, rebuilt as a lean series on the capability work that landed in #1282/#1283. The comment on #1167 records what was cut and why. ## What this does Adds Omnigent to the supported agents. Omnigent stores every conversation in one SQLite file (`chat.db`), so this follows the same container model as Zed: sync watches one file, and the archive holds one session per conversation (`omnigent:0:<hex>`). - Scheduled syncs reparse the container when its fingerprint (mtime + hash of the db and WAL) changes. - Watcher events do a bounded, indexed scan of only the members whose `updated_at` moved, and retire disappeared members as tombstones. WAL `-shm` checkpoint noise is ignored. - Deleting a conversation (or the whole db) tombstones its sessions; nothing is destroyed. ## Notes - Supports the two schema generations that ship in released Omnigent builds, including the current binary-uuid one. The oldest shape is reported as unsupported without failing sync; the interim `session_overrides` shape is not supported. - `chat.db` co-locates transcripts with authentication secrets, so Omnigent is excluded from remote sync (capability from #1282, nested-root enforcement from #1283). - Validated against a chat.db produced by Omnigent's own store code at upstream head, not just fixtures (evidence in `docs/internal/session-format-sources.md`). - Known trade-offs (Zed parity): the scheduled reparse scales with container size, and edits that don't bump `updated_at` wait for the scheduled pass instead of the watcher. ## Where to look - `internal/parser/omnigent.go` — schema detection and member queries - `internal/parser/omnigent_provider.go` — discovery, fingerprinting, watcher scan, tombstones - `internal/sync/engine.go` — container scheduling and the persistent-archive audit fallback - `internal/sync/omnigent_integration_test.go` — end-to-end sync coverage Co-authored-by: Wes McKinney <wesm@users.noreply.github.com>
1 parent fa96090 commit 8ae2f92

39 files changed

Lines changed: 8194 additions & 363 deletions

README.md

Lines changed: 57 additions & 55 deletions
Large diffs are not rendered by default.

cmd/agentsview/periodic_sync_test.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,26 @@ func TestScheduledReconcileTargetsSelectsOnlyOptedInProviders(t *testing.T) {
2222
aiderDir := filepath.Join(home, "aider")
2323
coworkDir := filepath.Join(home, "cowork")
2424
claudeDir := filepath.Join(home, "claude")
25+
omnigentDir := filepath.Join(home, "omnigent")
2526
require.NoError(t, os.MkdirAll(aiderDir, 0o755))
2627
require.NoError(t, os.MkdirAll(coworkDir, 0o755))
2728
require.NoError(t, os.MkdirAll(claudeDir, 0o755))
29+
require.NoError(t, os.MkdirAll(omnigentDir, 0o755))
2830

2931
cfg := config.Config{
3032
AgentDirs: map[parser.AgentType][]string{
31-
parser.AgentAider: {aiderDir},
32-
parser.AgentCowork: {coworkDir},
33-
parser.AgentClaude: {claudeDir},
33+
parser.AgentAider: {aiderDir},
34+
parser.AgentCowork: {coworkDir},
35+
parser.AgentClaude: {claudeDir},
36+
parser.AgentOmnigent: {omnigentDir},
3437
},
3538
}
3639
targets := scheduledReconcileTargets(cfg)
37-
require.Len(t, targets, 1, "only the opted-in provider is scheduled")
40+
require.Len(t, targets, 2, "only opted-in providers are scheduled")
3841
assert.Equal(t, parser.AgentAider, targets[0].Agent)
3942
assert.Equal(t, []string{aiderDir}, targets[0].Roots)
43+
assert.Equal(t, parser.AgentOmnigent, targets[1].Agent)
44+
assert.Equal(t, []string{omnigentDir}, targets[1].Roots)
4045
}
4146

4247
func TestScheduledReconcileDefersUnavailableOptedInRoots(t *testing.T) {

docs/configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ can still be parsed.
263263
| OpenClaw | `~/.openclaw/assets/static/agents/` and `~/.kimi_openclaw/assets/static/agents/` | JSONL per session |
264264
| OpenCode | `~/.local/share/opencode/` | SQLite DB or `storage/` JSON files |
265265
| OpenHands CLI | `~/.openhands/conversations/` | Per-conversation `base_state.json` + `events/*.json` |
266+
| Omnigent | `~/.omnigent/` | SQLite `chat.db`, one session per conversation |
266267
| Pi | `~/.pi/agent/sessions/` | JSONL per session |
267268
| Poolside | `~/Library/Application Support/poolside/trajectories/` (macOS), `~/.local/state/poolside/trajectories/` (Linux), `%APPDATA%\\poolside\\trajectories\\` (Windows) | NDJSON trajectory files |
268269
| Piebald | `~/.local/share/piebald/` | SQLite database (`app.db`) |
@@ -292,6 +293,16 @@ and tool calls). If `chat_history.jsonl` is missing, AgentsView falls back
292293
to summary-only mode. Set `GROK_DIR` or `grok_dirs` to override the default
293294
directory.
294295

296+
Omnigent sessions are read from `~/.omnigent/chat.db`. Set `OMNIGENT_DIR` or
297+
`omnigent_dirs` to override the default directory. AgentsView creates one
298+
session per conversation and supports the split text-ID and current
299+
binary-UUID schema generations; the older single-table schema is detected and
300+
reported as unsupported without losing sessions already synced from it.
301+
Remote HTTP and SSH sync stay disabled for Omnigent because `chat.db`
302+
co-locates transcripts with authentication secrets. A metadata-only edit made
303+
directly in `chat.db` can be deferred by the immediate filesystem-event sync;
304+
the next scheduled reconciliation pass or an explicit resync picks it up.
305+
295306
**VS Code Copilot default directories** vary by platform:
296307

297308
- **macOS:** `~/Library/Application Support/Code/User/`

docs/internal/session-format-sources.md

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ Grok section and remove the explicit registry exception in the coverage test.
7575
`attachment.type=queued_command` are written mid-stream, in file order
7676
between consecutive `assistant` records that share one `message.id`, so a
7777
queued command can fall inside a streaming run that straddles an incremental
78-
sync boundary. Reverified 2026-07-23 against the transcript shape reported in
79-
[#1238](https://github.com/kenn-io/agentsview/issues/1238): Claude Code for
80-
VS Code writes standalone `user` records wrapped in `ide_opened_file` or
78+
sync boundary. Reverified 2026-07-23 against the transcript shape reported
79+
in [#1238](https://github.com/kenn-io/agentsview/issues/1238): Claude Code
80+
for VS Code writes standalone `user` records wrapped in `ide_opened_file` or
8181
`ide_selection` tags for editor context rather than operator prompts.
8282

8383
## OpenClaude (`openclaude`)
@@ -228,11 +228,11 @@ Grok section and remove the explicit registry exception in the coverage test.
228228
from those tokens rather than consuming a persisted USD total.
229229
- **Working directory:** SQLite sessions store a per-session `directory` and a
230230
`project_id`. The synthetic `global` project uses `worktree=/`. Agentsview
231-
prefers a concrete `session.directory` over `project.worktree` when resolving
232-
cwd/project (verified against live `opencode.db` rows under `project_id=global`
233-
on 2026-07-23; see #1236).
234-
- **Invalid tool calls:** Model calls to unknown or malformed tools are
235-
recorded as a synthetic `invalid` tool part whose `execute` succeeds
231+
prefers a concrete `session.directory` over `project.worktree` when
232+
resolving cwd/project (verified against live `opencode.db` rows under
233+
`project_id=global` on 2026-07-23; see #1236).
234+
- **Invalid tool calls:** Model calls to unknown or malformed tools are recorded
235+
as a synthetic `invalid` tool part whose `execute` succeeds
236236
(`packages/opencode/src/tool/invalid.ts`, registered in
237237
`packages/opencode/src/tool/registry.ts` at the pinned commit), so
238238
`state.status` is `completed` with the error text in the output. Agentsview
@@ -247,10 +247,10 @@ Grok section and remove the explicit registry exception in the coverage test.
247247
failure signal. The tool's own output text carries no `exit status N`
248248
marker, and the shell is `COMSPEC`/`cmd.exe` on Windows, so text-pattern
249249
matching alone misses these failures on every platform. Agentsview treats a
250-
non-zero `state.metadata.exit` on a `bash` tool part as a failure and attaches
251-
an errored result event. Only `bash` parts record `exit`; other tools omit the
252-
key. Verified 2026-07-24 against a live `opencode.db` where all 24 bash
253-
parts with `exit` in `{1, 127, 128}` had output text without an
250+
non-zero `state.metadata.exit` on a `bash` tool part as a failure and
251+
attaches an errored result event. Only `bash` parts record `exit`; other
252+
tools omit the key. Verified 2026-07-24 against a live `opencode.db` where
253+
all 24 bash parts with `exit` in `{1, 127, 128}` had output text without an
254254
`exit status` marker, and the 81 successful parts recorded `exit=0`. Known
255255
gaps: a command that legitimately exits non-zero (`grep` with no match)
256256
counts as a failure, matching the existing `exit status N` heuristic, and a
@@ -294,8 +294,8 @@ Grok section and remove the explicit registry exception in the coverage test.
294294
OpenCode-based rebuild (public beta 2026-03-10, GA 2026-04-02); new sessions
295295
stopped appearing around 2026-03-21.
296296
- **Usage and cost:** `ui_messages.json` carries per-request `api_req_started`
297-
metadata with input, output, cache-read, and cache-write tokens, explicit USD
298-
cost, and `usageMissing` flag. `task_metadata.json` does not carry the
297+
metadata with input, output, cache-read, and cache-write tokens, explicit
298+
USD cost, and `usageMissing` flag. `task_metadata.json` does not carry the
299299
RooCode-style ID/token/cost wiring; token and cost totals are derived from
300300
the transcript itself.
301301
- **Agentsview:** `internal/parser/kilo_legacy.go` and
@@ -1124,20 +1124,20 @@ Grok section and remove the explicit registry exception in the coverage test.
11241124
- **Upstream:** The public
11251125
[pool release repository](https://github.com/poolsideai/pool) (README,
11261126
changelog, and third-party notices only; no source code) and the
1127-
[Poolside Agent CLI documentation](https://docs.poolside.ai/cli/pool)
1128-
were checked 2026-07-23. Upstream confirms sessions are saved
1129-
automatically and that per-session trajectory files exist (`pool config`
1130-
prints the trajectory directory; `pool history trajectories` browses
1131-
them), but publishes neither the on-disk paths nor the NDJSON event
1132-
schema. The event format was characterized from real trajectory files.
1127+
[Poolside Agent CLI documentation](https://docs.poolside.ai/cli/pool) were
1128+
checked 2026-07-23. Upstream confirms sessions are saved automatically and
1129+
that per-session trajectory files exist (`pool config` prints the trajectory
1130+
directory; `pool history trajectories` browses them), but publishes neither
1131+
the on-disk paths nor the NDJSON event schema. The event format was
1132+
characterized from real trajectory files.
11331133
- **Usage and cost:** Per-inference token counts (`input_tokens`,
1134-
`output_tokens`, `cache_read_input_tokens`, `cache_write_input_tokens`)
1135-
are persisted in `tool_call.inference.end` events. The model is recorded
1136-
in `tool_call.inference.start` and paired by `step_id`. No authoritative
1137-
USD cost is persisted; Agentsview computes cost from its pricing catalog.
1134+
`output_tokens`, `cache_read_input_tokens`, `cache_write_input_tokens`) are
1135+
persisted in `tool_call.inference.end` events. The model is recorded in
1136+
`tool_call.inference.start` and paired by `step_id`. No authoritative USD
1137+
cost is persisted; Agentsview computes cost from its pricing catalog.
11381138
- **Agentsview:** `internal/parser/poolside.go` and
1139-
`internal/parser/poolside_provider.go`; single-file provider with
1140-
NDJSON line-by-line parsing.
1139+
`internal/parser/poolside_provider.go`; single-file provider with NDJSON
1140+
line-by-line parsing.
11411141

11421142
## Reasonix (`reasonix`)
11431143

@@ -1155,3 +1155,53 @@ Grok section and remove the explicit registry exception in the coverage test.
11551155
- **Agentsview:** `internal/parser/reasonix.go` and
11561156
`internal/parser/reasonix_provider.go`; discovery spans multiple roots and
11571157
uses metadata sidecars for identity.
1158+
1159+
## Omnigent (`omnigent`)
1160+
1161+
- **Format:** A shared SQLite `chat.db` containing conversations and ordered
1162+
conversation items, with session metadata and usage stored alongside each
1163+
conversation.
1164+
- **Evidence:** `source`.
1165+
- **Upstream:** The first-party
1166+
[database documentation](https://omnigent.ai/docs/deploy/database)
1167+
identifies SQLite `chat.db` as the local persistence store and was checked
1168+
2026-07-27. Clone `https://github.com/omnigent-ai/omnigent.git` at
1169+
`61fd72350ea4c4aba776fbc01c40774079d352e8`. The pinned
1170+
[conversation schema](https://github.com/omnigent-ai/omnigent/blob/61fd72350ea4c4aba776fbc01c40774079d352e8/omnigent/db/db_models.py),
1171+
and
1172+
[store decoding](https://github.com/omnigent-ai/omnigent/blob/61fd72350ea4c4aba776fbc01c40774079d352e8/omnigent/stores/conversation_store/sqlalchemy_store.py)
1173+
describe persistence. The current schema indexes conversation changes by
1174+
`(workspace_id, archived, updated_at, id)` rather than a bare `updated_at`
1175+
index. `session_usage` lives on `omnigent_conversation_metadata`, and both
1176+
`set_session_usage` and `increment_session_usage` update that metadata row
1177+
without changing `conversations.updated_at`. The metadata table has runner
1178+
and project lookup indexes but no modification timestamp or change index.
1179+
Consequently, the immediate filesystem-event sync can defer a metadata-only
1180+
edit. The next scheduled reconciliation pass, an explicit resync, or an
1181+
archive audit reparses the whole changed container and is not limited to a
1182+
bounded candidate set, so it picks up the edit regardless of how long ago it
1183+
was made. The pinned
1184+
[message entity](https://github.com/omnigent-ai/omnigent/blob/61fd72350ea4c4aba776fbc01c40774079d352e8/omnigent/entities/conversation.py)
1185+
and
1186+
[deterministic benchmark seeder](https://github.com/omnigent-ai/omnigent/blob/61fd72350ea4c4aba776fbc01c40774079d352e8/dev/benchmarks/omnigent/seed.py)
1187+
were inspected. The seeder runs the Alembic lineage to head before
1188+
inserting model-backed rows. Agentsview supports two schema generations
1189+
observed at that lineage head: the split text-ID generation (session
1190+
metadata in `omnigent_conversation_metadata`, model overrides in a separate
1191+
`agent_configuration` table) and the current split binary-UUID generation
1192+
(16-byte `BLOB` ids, `session_overrides` JSON on `conversations`). The
1193+
earlier single-table generation, where session metadata columns (including
1194+
`kind`) lived directly on `conversations` with no separate metadata table,
1195+
predates that split and is detected-unsupported: Agentsview fails closed
1196+
with a nonfatal `ErrOmnigentUnsupportedSchema`, skips the container, and
1197+
preserves any archive rows already synced from it.
1198+
- **Regeneration:** From that checkout, run
1199+
`uv run dev/benchmarks/omnigent/seed.py --database-uri sqlite:////absolute/temp/path/chat.db --sessions 3 --items-per-session 4 --projects 1 --filed-fraction 1`,
1200+
then set `OMNIGENT_SOURCE_DB` to the generated file for the opt-in parser
1201+
test. Never use a live Omnigent data directory.
1202+
- **Usage and cost:** Session usage can contain input and output tokens,
1203+
per-model breakdowns, and an optional authoritative USD total. An absent
1204+
cost remains unset so Agentsview can use catalog pricing.
1205+
- **Agentsview:** `internal/parser/omnigent.go` and
1206+
`internal/parser/omnigent_provider.go`; fixtures under
1207+
`internal/parser/testdata/omnigent/` provide observed event-shape evidence.

frontend/src/lib/utils/agents.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe("KNOWN_AGENTS", () => {
4949
"posit-assistant",
5050
"roocode",
5151
"poolside",
52+
"omnigent",
5253
]);
5354
});
5455

@@ -124,6 +125,9 @@ describe("agentColor", () => {
124125
expect(agentColor("roocode")).toBe(
125126
"var(--accent-rose)",
126127
);
128+
expect(agentColor("omnigent")).toBe(
129+
"var(--accent-teal)",
130+
);
127131
});
128132

129133
it("falls back to blue for unknown agents", () => {
@@ -200,6 +204,7 @@ describe("agentLabel", () => {
200204
expect(agentLabel("deepseek-tui")).toBe("DeepSeek TUI");
201205
expect(agentLabel("qoder")).toBe("Qoder");
202206
expect(agentLabel("roocode")).toBe("RooCode");
207+
expect(agentLabel("omnigent")).toBe("Omnigent");
203208
});
204209

205210
it("capitalizes simple agent names", () => {

frontend/src/lib/utils/agents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export const KNOWN_AGENTS: readonly AgentMeta[] = [
7575
},
7676
{ name: "roocode", color: "var(--accent-rose)", label: "RooCode" },
7777
{ name: "poolside", color: "var(--accent-cyan)", label: "Poolside" },
78+
{ name: "omnigent", color: "var(--accent-teal)", label: "Omnigent" },
7879
];
7980

8081
const agentColorMap = new Map(

internal/db/session_batch.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type SessionBatchResult struct {
3232
ExcludedSessions int
3333
ExcludedIDs []string
3434
FailedSessions int
35+
FailedIDs []string
3536
Errors []error
3637
}
3738

@@ -109,6 +110,7 @@ func (db *DB) WriteSessionBatch(
109110
return result, rerr
110111
}
111112
result.FailedSessions++
113+
result.FailedIDs = append(result.FailedIDs, write.Session.ID)
112114
result.Errors = append(result.Errors, err)
113115
}
114116
}

internal/db/sessions.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,6 +2270,85 @@ func (db *DB) ListSessionIDsByFilePath(path, agent string) ([]string, error) {
22702270
return ids, nil
22712271
}
22722272

2273+
const descendantSessionRootBatchSize = 100
2274+
2275+
// ListActiveDescendantSessionSourcePaths returns the source paths of active
2276+
// descendants already linked beneath parentIDs. Both the seed and recursive
2277+
// steps use idx_sessions_parent, so work scales with the affected subagent
2278+
// trees rather than every archived session.
2279+
func (db *DB) ListActiveDescendantSessionSourcePaths(
2280+
ctx context.Context,
2281+
machine, agent string,
2282+
parentIDs []string,
2283+
) ([]string, error) {
2284+
if err := ctx.Err(); err != nil {
2285+
return nil, err
2286+
}
2287+
seen := make(map[string]struct{})
2288+
var paths []string
2289+
for start := 0; start < len(parentIDs); start += descendantSessionRootBatchSize {
2290+
end := min(start+descendantSessionRootBatchSize, len(parentIDs))
2291+
batch := parentIDs[start:end]
2292+
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(batch)), ",")
2293+
query := `
2294+
WITH RECURSIVE descendants(id, file_path) AS (
2295+
SELECT id, file_path
2296+
FROM sessions INDEXED BY idx_sessions_parent
2297+
WHERE parent_session_id IS NOT NULL
2298+
AND parent_session_id IN (` + placeholders + `)
2299+
AND machine = ? AND agent = ? AND deleted_at IS NULL
2300+
UNION
2301+
SELECT s.id, s.file_path
2302+
FROM sessions AS s INDEXED BY idx_sessions_parent
2303+
JOIN descendants AS d ON s.parent_session_id = d.id
2304+
WHERE s.parent_session_id IS NOT NULL
2305+
AND s.machine = ? AND s.agent = ? AND s.deleted_at IS NULL
2306+
)
2307+
SELECT file_path
2308+
FROM descendants
2309+
WHERE file_path IS NOT NULL AND file_path <> ''
2310+
ORDER BY file_path`
2311+
args := make([]any, 0, len(batch)+4)
2312+
for _, id := range batch {
2313+
args = append(args, id)
2314+
}
2315+
args = append(args, machine, agent, machine, agent)
2316+
rows, err := db.getReader().QueryContext(ctx, query, args...)
2317+
if err != nil {
2318+
return nil, fmt.Errorf(
2319+
"listing active descendant session sources: %w", err,
2320+
)
2321+
}
2322+
for rows.Next() {
2323+
var path string
2324+
if err := rows.Scan(&path); err != nil {
2325+
_ = rows.Close()
2326+
return nil, fmt.Errorf(
2327+
"scanning active descendant session source: %w", err,
2328+
)
2329+
}
2330+
if _, exists := seen[path]; exists {
2331+
continue
2332+
}
2333+
seen[path] = struct{}{}
2334+
paths = append(paths, path)
2335+
}
2336+
if err := rows.Err(); err != nil {
2337+
_ = rows.Close()
2338+
return nil, fmt.Errorf(
2339+
"iterating active descendant session sources: %w", err,
2340+
)
2341+
}
2342+
if err := rows.Close(); err != nil {
2343+
return nil, fmt.Errorf(
2344+
"closing active descendant session sources: %w", err,
2345+
)
2346+
}
2347+
}
2348+
sort.Strings(paths)
2349+
return paths, nil
2350+
}
2351+
22732352
const storedSourcePathHintRootBatchSize = 100
22742353

22752354
// StoredSourcePathHintScope identifies one affected stored-source prefix.

internal/parser/capabilities_sync_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ func TestProviderSyncSemanticsDeclarations(t *testing.T) {
6767
AgentIcodemate: {
6868
UnchangedResults: UnchangedResultMTimeAndHash,
6969
},
70+
AgentOmnigent: {
71+
FingerprintHashInCacheKey: true,
72+
FingerprintHashRequiredForFreshness: true,
73+
UnchangedResults: UnchangedResultMTimeAndHash,
74+
},
7075
}
7176

7277
for _, factory := range ProviderFactories() {

internal/parser/kiro_provider.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,9 @@ func (s kiroSourceSet) Fingerprint(
611611
MTimeNS: info.ModTime().UnixNano(),
612612
}
613613
if src.Kind == kiroSourceSQLiteDB {
614-
if compositeMtime, err := sqliteDBCompositeMtime(src.DBPath); err == nil {
614+
if compositeMtime, err := sqliteDBCompositeMtime(
615+
src.DBPath, sqliteDBJournalSuffixes,
616+
); err == nil {
615617
fingerprint.MTimeNS = compositeMtime
616618
}
617619
return fingerprint, nil

0 commit comments

Comments
 (0)