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
6 changes: 4 additions & 2 deletions .github/workflows/ci-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ on:
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- "crates/**"
- "implementations/rust/**"
- "contracts/**"
- ".github/workflows/ci-rust.yml"
pull_request:
branches: [main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- "crates/**"
- "implementations/rust/**"
- "contracts/**"
- ".github/workflows/ci-rust.yml"

jobs:
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml → .github/workflows/ci-swift.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
name: CI
name: CI (Swift)

on:
push:
branches: [main]
paths:
- "Package.swift"
- "implementations/swift/**"
- "contracts/**"
- ".github/workflows/ci-swift.yml"
pull_request:
paths:
- "Package.swift"
- "implementations/swift/**"
- "contracts/**"
- ".github/workflows/ci-swift.yml"
workflow_dispatch:

concurrency:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "Tag: ${TAG}"

INFO="Sources/AgentSessionKit/AgentSessionKitInfo.swift"
INFO="implementations/swift/Sources/AgentSessionKit/AgentSessionKitInfo.swift"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the Rust crate version before publishing

When a later shared release updates the Swift constant and changelog but accidentally leaves the Rust manifest behind, this job still publishes because it validates only AgentSessionKitInfo.version and runs only Swift commands. Rust consumers fetching that tag then receive a crate whose package version disagrees with the release, so the release check should also compare implementations/rust/crates/agent-session-core/Cargo.toml with TAG before publishing.

AGENTS.md reference: AGENTS.md:L74-L79

Useful? React with 👍 / 👎.

if [ ! -f "$INFO" ]; then
echo "::error::${INFO} is missing; the version constant is the release contract."
exit 1
Expand Down
28 changes: 23 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ own mapping as an extension in its own module.
```text
.
├── Package.swift # swift-tools-version 6.2, macOS 26
├── Cargo.toml # Cargo workspace root
├── RELEASING.md # semver policy + how a tag is cut
├── Sources/
├── contracts/ # Facts both lanes are tested against
│ └── storage/
│ └── session-index-v5.sql # Canonical index DDL + user_version
├── implementations/rust/
│ └── crates/
│ └── agent-session-core/ # macOS / Linux / Windows, read-only
└── implementations/swift/
├── Sources/
│ ├── AgentSessionKit/ # Swift 5 language mode (migration pending)
│ │ ├── AgentSessionKitInfo.swift # version, repository URL, release-notes URL
│ │ ├── Harness/
Expand Down Expand Up @@ -57,12 +65,22 @@ own mapping as an extension in its own module.
│ │ ├── MCPArguments.swift # Typed tools/call argument decoding
│ │ ├── MCPSocketServer.swift # AF_UNIX listener, 0600
│ │ └── MCPStdioBridge.swift # Byte pump for spawn-a-command clients
│ └── AgentSessionLive/ # Swift 6 language mode. Placeholder.
└── Tests/
├── AgentSessionKitTests/
└── AgentSessionLiveTests/
│ └── AgentSessionLive/ # Swift 6 language mode. Placeholder.
└── Tests/
├── AgentSessionKitTests/
└── AgentSessionLiveTests/
```

The two implementation lanes are peers. Neither is generated from the other,
so anything that must hold in both languages goes in `contracts/` with a test
on each side — `SessionIndexContractTests` in Swift and `index::contract_tests`
in Rust both read `contracts/storage/session-index-v5.sql`. Changing a shared
fact means changing the contract, both lanes, and the kit minor version in one
pull request.

Both manifests stay at the repository root and reach into `implementations/`
with explicit paths, so consumers' git URLs, pins, and imports never move.

Boundary rule: `AgentSessionKit` answers "what is on disk right now" — one
pass, one snapshot, no observers. Anything that watches, debounces, or tails
belongs in `AgentSessionLive`.
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,38 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.7.0] - 2026-08-31

Swift and Rust become peer implementation lanes. The repository now says out
loud what it had already become when the Rust crate landed: one set of
session-reading semantics with two implementations, neither generated from
the other, and a `contracts/` directory holding the facts both must honour.

### Changed
- **Layout: `implementations/swift/` and `implementations/rust/`.** The Swift
sources and tests moved out of the repository root, and the Rust crates
moved out of `crates/`. Both manifests stay at the root and reach in with
explicit paths, so **no consumer changes anything**: the git URL, the
`exact:` pin, `import AgentSessionKit`, and both product names are
unaffected. `crates/` at the root became `implementations/rust/crates/`,
which does move the Cargo path for anyone depending on the crate by path
rather than by git.
- `ci.yml` is now `ci-swift.yml`, and both lane workflows are scoped to their
own paths plus `contracts/`.

### Added
- **`contracts/storage/session-index-v5.sql`** — the canonical index DDL and
`user_version`, extracted from the Swift writer that had been its only
definition. Both lanes are now tested against it: `SessionIndexContractTests`
checks that Swift creates exactly the objects the contract names, and
`index::contract_tests` builds a database from the file and opens it with the
Rust reader. A schema change in either lane alone now fails CI.

### Fixed
- `AgentSessionKitInfoTests` resolved the repository root by walking three
directories up from `#filePath`, which the new layout broke. It is five now,
and the failure was real: the test could not find `CHANGELOG.md`.

### Fixed
- **Rust discovery/search/transcript parity.** The first Rust session-core
slice now mirrors Swift's default title/user/assistant search scopes,
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[workspace]
resolver = "2"
members = ["crates/agent-session-core"]
members = ["implementations/rust/crates/agent-session-core"]

# Rust implementation lane of agent-session-kit. The Swift package rooted at
# Package.swift and this Cargo workspace are peer implementations of the same
# session-reading semantics; contracts and fixtures are shared. The Swift
# sources are untouched by this workspace.
# Rust implementation lane of agent-session-kit. This workspace and the
# SwiftPM package rooted at Package.swift are peer implementations of the
# same session-reading semantics, living side by side under
# implementations/. Both manifests stay at the repository root so neither
# language's tooling needs a non-default working directory.
10 changes: 9 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,32 @@ let package = Package(
.library(name: "AgentSessionKit", targets: ["AgentSessionKit"]),
.library(name: "AgentSessionLive", targets: ["AgentSessionLive"])
],
// Swift and Rust are peer implementation lanes under implementations/.
// Explicit paths keep this manifest at the repository root so every
// consumer's git URL, exact pin, and `import AgentSessionKit` are
// unchanged by the layout.
targets: [
// Discovery, parsing, indexing, and the MCP transport. Extracted
// from Vibe Bar, where this code grew up inside the app target;
// it is still on the Swift 5 language mode because the adapters
// were written against it. Migrating is its own change.
.target(
name: "AgentSessionKit",
path: "implementations/swift/Sources/AgentSessionKit",
Comment thread
AstroQore marked this conversation as resolved.
swiftSettings: [.swiftLanguageMode(.v5)]
),
// Live views over the same stores — file-system watching and
// incremental tailing. Swift 6 language mode from the start,
// because nothing here predates strict concurrency.
.target(
name: "AgentSessionLive",
dependencies: ["AgentSessionKit"]
dependencies: ["AgentSessionKit"],
path: "implementations/swift/Sources/AgentSessionLive"
),
.testTarget(
name: "AgentSessionKitTests",
dependencies: ["AgentSessionKit"],
path: "implementations/swift/Tests/AgentSessionKitTests",
swiftSettings: [.swiftLanguageMode(.v5)]
),
// `Fixtures/` holds sample source records, one directory per
Expand All @@ -38,6 +45,7 @@ let package = Package(
.testTarget(
name: "AgentSessionLiveTests",
dependencies: ["AgentSessionLive"],
path: "implementations/swift/Tests/AgentSessionLiveTests",
resources: [.copy("Fixtures")]
)
]
Expand Down
60 changes: 50 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,72 @@
# agent-session-kit

A Swift package that reads the session stores AI coding agents leave on your
Mac, and serves them over MCP.
Reads the session stores AI coding agents leave on your machine, and serves
them over MCP. **Two implementations, one set of semantics:** a Swift package
for macOS hosts and a Rust crate for cross-platform ones.

Every coding agent keeps a local record of what you asked it — Codex writes
rollout JSONL, Claude Code writes transcript JSONL, Cursor and AntiGravity
write SQLite databases another process holds open. Each one has its own
layout, its own idea of a "session id", and its own way of spelling a
timestamp. This package hides all of that behind one adapter protocol, a
timestamp. This repository hides all of that behind one adapter protocol, a
searchable index, and a local MCP transport.

No third-party dependencies. Foundation, Darwin, and the system SQLite.
No third-party dependencies in either lane beyond the system SQLite.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the dependency-free claim

This claim is false for the newly documented Rust lane: its manifest depends on rusqlite, serde, serde_json, and thiserror, while rusqlite enables bundled SQLite rather than using only the system library. This can mislead consumers assessing binary size, licensing, or supply-chain exposure; scope the statement to Swift or document the Rust dependencies accurately.

Useful? React with 👍 / 👎.


```swift
.package(url: "https://github.com/AstroQore/agent-session-kit", exact: "0.3.0")
// Swift — macOS 26, Swift 6.2
.package(url: "https://github.com/AstroQore/agent-session-kit", exact: "0.7.0")
```

```toml
# Rust — macOS, Linux, Windows
agent-session-core = { git = "https://github.com/AstroQore/agent-session-kit", tag = "0.7.0" }
```

`exact:` while the package is `0.x` — see
[Versioning and releases](#versioning-and-releases).

## Targets
## Layout

```text
Package.swift SwiftPM entry point
Cargo.toml Cargo workspace entry point
contracts/ Semantics both lanes are checked against
implementations/swift/ Sources/ + Tests/
implementations/rust/ crates/
```

Both manifests stay at the repository root, so a consumer's git URL, pin, and
`import AgentSessionKit` are unaffected by the layout — and neither language's
tooling needs a non-default working directory.

The two lanes are peers, not a primary and a port. Neither is generated from
the other, which is why anything they must agree on lives in `contracts/` with
a test on each side. Today that is the session index schema
(`contracts/storage/session-index-v5.sql`): the Swift writer is checked to
create exactly those objects, and the Rust reader is checked to open a database
built from that file. Add to `contracts/` whenever a new fact has to hold in
both languages; a change there is a coordinated change plus a kit minor bump.

## What each lane provides

**Swift** (`implementations/swift`, macOS 26, Swift 6.2)

| Target | What it is |
| ------ | ---------- |
| `AgentSessionKit` | Discovery, parsing, deletion planning, the FTS5 session index, and the MCP Unix-socket / stdio transport. |
| `AgentSessionLive` | Live views over the same stores — the unified event model, the state reducer, and the tailing protocols. |

Requires macOS 26 and Swift 6.2.
**Rust** (`implementations/rust`, macOS / Linux / Windows)

| Crate | What it is |
| ----- | ---------- |
| `agent-session-core` | Read-only session index access, lightweight Codex and Claude Code discovery, tolerant JSONL transcript paging, and the resume command builder. |

The Rust lane is deliberately the smaller of the two: it covers what a
cross-platform host needs to render sessions without the Swift runtime. It
never writes the index — schema mismatch is refused, never rebuilt — because
the writer is the host that owns the file.

## Architecture

Expand Down Expand Up @@ -544,16 +584,16 @@ package is `0.x`, a **minor** bump carries anything a caller could trip over
storage key) and a **patch** carries fixes and internals only. Pin `exact:`.

```swift
.package(url: "https://github.com/AstroQore/agent-session-kit", exact: "0.3.0")
.package(url: "https://github.com/AstroQore/agent-session-kit", exact: "0.7.0")
```

Every build knows which version it is:

```swift
import AgentSessionKit

AgentSessionKitInfo.version // "0.3.0"
AgentSessionKitInfo.bundledReleaseNotesURL // .../releases/tag/0.3.0
AgentSessionKitInfo.version // "0.7.0"
AgentSessionKitInfo.bundledReleaseNotesURL // .../releases/tag/0.7.0
AgentSessionKitInfo.repositoryURL
```

Expand Down
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Nothing here is `1.0` yet, so `exact:` is the pin a host should use. See

## The version constant

`Sources/AgentSessionKit/AgentSessionKitInfo.swift` holds
`implementations/swift/Sources/AgentSessionKit/AgentSessionKitInfo.swift` holds
`AgentSessionKitInfo.version`. This package is linked *statically* by its
hosts: once it is compiled in there is no bundle, no `Info.plist`, and no
dylib for anyone to read a version off. The constant is the only answer to
Expand Down
76 changes: 76 additions & 0 deletions contracts/storage/session-index-v5.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
-- Session index, schema version 5.
--
-- Canonical DDL for `session_index.sqlite3`. Both implementation lanes are
-- checked against this file: the Swift writer creates exactly these objects,
-- and the Rust reader refuses any database whose `PRAGMA user_version` is not
-- 5 rather than rebuilding it.
--
-- The index is derived data. A host may delete the file and pay a re-scan, so
-- a version bump is a re-index, never a migration. Changing anything here is
-- therefore a coordinated change across both lanes plus a kit minor version.

PRAGMA user_version = 5;

CREATE TABLE IF NOT EXISTS session_index_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY,
provider TEXT NOT NULL,
session_id TEXT NOT NULL,
provider_variant TEXT,
harness TEXT,
model TEXT,
title TEXT,
summary TEXT,
project_dir TEXT,
created_at INTEGER,
last_active_at INTEGER,
source_path TEXT NOT NULL,
size_bytes INTEGER NOT NULL DEFAULT 0,
message_count INTEGER NOT NULL DEFAULT -1,
UNIQUE(provider, session_id, source_path)
);
CREATE INDEX IF NOT EXISTS sessions_last_active_idx
ON sessions(last_active_at DESC);
CREATE INDEX IF NOT EXISTS sessions_effective_active_idx
ON sessions(COALESCE(last_active_at, created_at) DESC, id DESC);
CREATE INDEX IF NOT EXISTS sessions_provider_effective_active_idx
ON sessions(provider, COALESCE(last_active_at, created_at) DESC, id DESC);
CREATE INDEX IF NOT EXISTS sessions_provider_project_idx
ON sessions(provider, project_dir);
CREATE INDEX IF NOT EXISTS sessions_harness_effective_active_idx
ON sessions(harness, COALESCE(last_active_at, created_at) DESC, id DESC);
CREATE TABLE IF NOT EXISTS session_files (
path_hash TEXT PRIMARY KEY,
path TEXT NOT NULL,
provider TEXT NOT NULL,
mtime_ns INTEGER NOT NULL,
size INTEGER NOT NULL,
session_row INTEGER REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS session_messages (
id INTEGER PRIMARY KEY,
session_row INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
seq INTEGER NOT NULL,
role TEXT NOT NULL,
excerpt TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS session_messages_row_idx
ON session_messages(session_row, seq);
CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
excerpt,
content='session_messages',
content_rowid='id',
tokenize='trigram'
);
CREATE TRIGGER IF NOT EXISTS session_messages_ai
AFTER INSERT ON session_messages BEGIN
INSERT INTO session_fts(rowid, excerpt) VALUES (new.id, new.excerpt);
END;
CREATE TRIGGER IF NOT EXISTS session_messages_ad
AFTER DELETE ON session_messages BEGIN
INSERT INTO session_fts(session_fts, rowid, excerpt)
VALUES('delete', old.id, old.excerpt);
END;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "agent-session-core"
version = "0.1.0"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-only"
description = "Cross-platform Rust implementation of agent-session-kit's session reading: index queries, lightweight discovery, transcripts, and resume commands."
Expand Down
Loading
Loading