Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cargento/skills/cargento/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Data sources (read-only, no external calls; all parsing is defensive — a broke
- `~/.local/share/opencode/opencode*.db` (SQLite, read-only) — OpenCode: `session` table (`parent_id` links subagent/child sessions, `directory`, `title`, `time_updated` in epoch ms; archived sessions filtered), `session_message` for turns/prompts (message kind = the `type` column; prompt text in `data.text`). Busy/idle is not persisted — inferred from `time_updated` freshness
- `~/.cursor/chats/*/<agent-id>/store.db` (SQLite, read-only) + the sibling `meta.json` — Cursor CLI: the `meta` table holds hex-encoded JSON (session name, `latestRootBlobId`, and `subagentInfo` on a child), and the sibling file holds `cwd`. Minimal support: discovery + working/idle via db/WAL mtime + title + project + the model of the newest message + subagents + a standing permission gate; no turn ETA (content is hex blobs). The gate is `pendingToolExecutionContracts` on the newest blob by `rowid` — non-empty means somebody is being asked, and `pendingToolCallStartedAtMs` beside it is when they started waiting. Newest by `rowid` and not simply anywhere, because the store is append-only and content-addressed: the blob that stood at the gate is still there long after the answer, and `rowid` is the only order the schema keeps. The key is matched on bytes, since the carrier is a length-prefixed binary frame rather than JSON, and the wait is published only while the row is fresh — an abandoned session stays pending in the file forever, so without that the board would carry a red row and a notification for a session nobody is in. A reading is also dropped when its stamp is too old to have been written by the store's own last write, since a real gate is what froze that file: that is what keeps a stamp belonging to some neighbouring record, or a message that merely quotes the field, from turning a row red with a wait nobody believes. The model is `providerOptions.cursor.modelName`, found by following the chat's root blob to the message blobs it lists and reading a bounded slice of the newest few. Cursor writes its own codename there and it is published verbatim, since mapping it to a marketing name is a guess that would silently mislabel the next codename. Blobs are plaintext on every store measured, but the store carries an encryption key it is never asked for: where the bytes do not read, the session reports no model. The workspace is `cwd` in the `meta.json` beside the store — no spelling of it appears in the `meta` payload at all — so the key spellings that payload was read for stay on as a fallback beneath it. Either source is accepted, bare path or `file://`, only when it resolves to a directory that exists, since a wrong key would label the row confidently and wrongly; when neither does, the row reads `cursor`, and a store with no `meta.json` (a subagent's has none) is a session without a workspace rather than a broken store. A subagent keeps its own store under the same workspace hash and names its agent in `subagentInfo.rootParentAgentId` — another store's directory name, which is exactly the session id another row carries — so children fold onto that row as pills labelled by `typeName` (their own name is the generic `New Agent`), each with the model read from its own store; a child whose named agent is not among the rows stays a row of its own rather than disappearing
- `~/.local/share/goose/sessions/sessions.db` (SQLite, read-only) — Goose v1.10.0+: `sessions` (`working_dir`, `updated_at` UTC, `session_type='subagent'` + `parent_session_id`; archived + infrastructure types `hidden/terminal/gateway/acp` filtered), `messages` (role, `created_timestamp`) for turns/prompts, `usage_ledger` for the token rate (`messages.tokens` is never written by goose). Per-session activity = `updated_at` column, not file mtime (shared DB) — a long single-message generation can briefly read Idle since `updated_at` only bumps on message insert. Legacy per-session `.jsonl` and the `GOOSE_PATH_ROOT` override are not supported
- `~/.factory/projects/<project>/<session-id>.jsonl` — Factory Droid: line 1 `session_start` (`id`, `sessionTitle`, `cwd`); messages are Anthropic-style blocks (`role`, `content[]`, `timestamp`)
- `~/.factory/sessions/<slugified-cwd>/<session-id>.jsonl` — Factory Droid 0.202.0 (the older `~/.factory/projects/<project>/<session-id>.jsonl` root is still searched): line 1 `session_start` (`id`, `sessionTitle`, `cwd`); messages are Anthropic-style blocks (`role`, `content[]`, `timestamp`). The sibling `<session-id>.settings.json` is not read

Pi relocation: `PI_CODING_AGENT_SESSION_DIR` is an authoritative direct session-store override and takes precedence over `PI_CODING_AGENT_DIR`, Pi's global `sessionDir` setting, and the default. `PI_CODING_AGENT_DIR` relocates the configuration root; a relative global `sessionDir` resolves below it. A one-off Pi `--session-dir` and a project-local `.pi/settings.json` are not discoverable from this separate process. Set `PI_CODING_AGENT_SESSION_DIR` to the effective directory in either case.

Expand Down
8 changes: 7 additions & 1 deletion cargento/skills/cargento/cargento_runtime/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,13 @@ def app_data(root: str | None, *parts: str) -> str | None:
app_data(roaming_app_data, "Block", "goose", "data", "sessions", "sessions.db"),
app_data(local_app_data, "Block", "goose", "data", "sessions", "sessions.db"),
),
"droid.projects": ordered(under_home(".factory", "projects")),
# Droid 0.202.0 writes <home>/.factory/sessions/<slugified-cwd>/<id>.jsonl.
# Measured 2026-09-02: with `projects` as the only root, three real
# transcripts discovered nothing (DRC-4331). `projects` stays as the
# fallback because no measurement says which versions wrote there.
"droid.projects": ordered(
under_home(".factory", "sessions"), under_home(".factory", "projects")
),
}


Expand Down
8 changes: 7 additions & 1 deletion cargento/skills/cargento/tests/test_config_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,13 @@ def test_posix_defaults_are_unchanged(self) -> None:
self.assertEqual(["/home/u/.gemini/tmp"], roots["gemini.tmp"])
self.assertEqual(["/home/u/.copilot"], roots["copilot.root"])
self.assertEqual(["/home/u/.cursor/chats"], roots["cursor.chats"])
self.assertEqual(["/home/u/.factory/projects"], roots["droid.projects"])
# Sessions first: droid 0.202.0 writes <home>/.factory/sessions/<slugified-cwd>/
# <id>.jsonl, and with `projects` as the only root three real transcripts
# discovered nothing (DRC-4331). `projects` stays as the fallback because
# no measurement says which versions wrote there.
self.assertEqual(
["/home/u/.factory/sessions", "/home/u/.factory/projects"], roots["droid.projects"]
)
self.assertEqual(["/home/u/.local/share/opencode"], roots["opencode.data"])
self.assertEqual(["/home/u/.local/share/goose/sessions/sessions.db"], roots["goose.db"])

Expand Down
21 changes: 21 additions & 0 deletions cargento/skills/cargento/tests/test_droid.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import os
import shutil
import tempfile
import time
import unittest
Expand All @@ -16,6 +17,7 @@

from .support import (
STORE_OVERRIDES,
USER_HOME,
RuntimeTestCase,
make_runtime,
runtime,
Expand Down Expand Up @@ -107,6 +109,25 @@ def test_droid_sessions_from_project_transcripts(self) -> None:
self.assertEqual("ship it", s["last_prompt"])
self.assertEqual(runtime_records.parse_ts(iso), s["started_at"])

def test_the_0_202_0_layout_is_discovered_under_the_default_home(self) -> None:
# droid 0.202.0 writes <home>/.factory/sessions/<slugified-cwd>/<id>.jsonl
# beside an <id>.settings.json the *.jsonl glob must ignore. With the
# root at `projects` alone, three real transcripts discovered nothing
# (DRC-4331), so this goes through the resolved default roots rather
# than a store override, which would pass against any root at all.
factory = Path(USER_HOME) / ".factory"
self.addCleanup(shutil.rmtree, factory)
fp = self._transcript(
factory / "sessions", "-w-droidproj", "s0202", {"id": "s0202", "cwd": "/w/droidproj"}
)
(fp.parent / "s0202.settings.json").write_text("{}")
config, state = runtime()

self.assertTrue(droid_collector.discover(config, state))
rows = droid_collector.collect(config, state, self.NOW, 24, True)
self.assertEqual(["s0202"], [row["sid"] for row in rows])
self.assertEqual("w/droidproj", rows[0]["project"])


class DroidReviewFixTest(unittest.TestCase):
NOW = 1_700_000_000.0
Expand Down
Loading