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
30 changes: 30 additions & 0 deletions .github/workflows/build-helpers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ on:
- "packages/runtime-local/script/watcher/**"
- "packages/runtime-local/test/file/fixtures/watcher-*"
- "test/script/watcher-native.test.ts"
- "packages/harness/script/build-sqlite.ts"
- "packages/harness/src/storage/sqlite-engine.ts"
- "script/pack-workspace.ts"
- "script/build-workspace.ts"
- "test/script/release/workspace-sqlite.test.ts"
push:
branches: [dev, main]
paths:
Expand All @@ -25,6 +30,11 @@ on:
- "packages/runtime-local/script/watcher/**"
- "packages/runtime-local/test/file/fixtures/watcher-*"
- "test/script/watcher-native.test.ts"
- "packages/harness/script/build-sqlite.ts"
- "packages/harness/src/storage/sqlite-engine.ts"
- "script/pack-workspace.ts"
- "script/build-workspace.ts"
- "test/script/release/workspace-sqlite.test.ts"

permissions:
contents: read
Expand Down Expand Up @@ -133,3 +143,23 @@ jobs:
name: sandbox-assets-windows-x64
path: packages/runtime-local/sandbox-assets
if-no-files-found: error

sqlite:
name: Patched macOS SQLite engine
runs-on: macos-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.3.14"
- run: bun packages/harness/script/build-sqlite.ts
- run: bun install --frozen-lockfile
- name: Verify packaged SQLite without host libraries
run: bun test --config /dev/null test/script/release/workspace-sqlite.test.ts
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: sqlite-assets-darwin
path: packages/harness/.artifacts/sqlite/
if-no-files-found: error
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,40 @@ jobs:
bun script/pack-workspace.ts packages/product-runtime "${{ runner.temp }}/runtime-packages"
bun script/runtime-composition-check.ts "${{ runner.temp }}/runtime-packages"

agent-storage:
name: Agent Storage (PostgreSQL ${{ matrix.postgres }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
postgres: [16, 17, 18]
services:
postgres:
image: postgres:${{ matrix.postgres }}
env:
POSTGRES_PASSWORD: storage-ci-only
POSTGRES_DB: synergy_storage_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s --health-timeout 5s --health-retries 12
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.3.14"
- run: bun install --frozen-lockfile
- name: Verify both database implementations against the same contract
working-directory: packages/harness
run: bun test --timeout 30000 test/storage
env:
SYNERGY_REQUIRE_POSTGRES_TESTS: "1"
SYNERGY_TEST_POSTGRES_URL: postgres://postgres:storage-ci-only@127.0.0.1:5432/synergy_storage_test

oryn-validation:
name: Oryn Configuration
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ jobs:
pattern: sandbox-assets-*
merge-multiple: true

- name: Download patched SQLite engine
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: sqlite-assets-darwin
path: packages/harness/.artifacts/sqlite

- name: Validate product release environment
run: bun run ./script/release/validate-product-environment.ts
env:
Expand Down
17 changes: 10 additions & 7 deletions .synergy/skill/change-persistence/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ description: Add or modify Synergy durable state, JSON storage keys, SQLite tabl

## Implement the Current Model

### File-backed JSON

1. Build logical keys through `StoragePath`; use `Storage` for locks, atomic writes, reads, scans, and removal.
2. Keep independently updated or streamed records independently addressable. Do not rewrite a whole session or collection for one leaf update.
3. Update derived indexes and events in the same owner transaction/lifecycle as the canonical write.
4. Preserve the atomic-write transient-retry contract: `Storage` write+rename retries `EPERM`/`EACCES`/`EBUSY` (classified by `isRetryableIOError`) so Windows sharing violations do not fail persistence, permanent errors fail fast, and temp files are removed (with the same transient retry) on the failure path. Do not bypass `Storage` with a bare rename; extend `test/storage/storage-retry.test.ts` when changing write-path failure behavior.
5. Authoritative rollout evidence uses private, durable Storage writes and the bounded `RolloutArtifact` stream store. Keep progress independently committed, verify content hashes, and preserve partial observations. Do not replace its persistence failures with diagnostic warnings, empty data, or successful completion; propagate `RolloutRecordingError` so execution admission can stop.
### Authoritative Agent records

1. Build logical keys through `StoragePath`; use an explicit `Storage.Handle`. Normal Agent record code must never read or write legacy JSON files.
2. Keep independently updated or streamed records independently addressable. Wrap the complete business mutation, indexes, receipts and outbox notifications in `Storage.transaction()`.
3. Nested writes join the caller's transaction. Defer cache and event effects until commit. Never run tools, network calls, plugin reloads, filesystem writes or buffer drains inside a retryable SQL transaction. Keep transaction-owned part writes out of streaming retry buffers; verify rollback followed by a delivery retry cannot resurrect the abandoned parts.
4. Treat commit uncertainty as an unresolved result; reconcile the operation receipt before retrying. Preserve storage, ownership and integrity errors instead of treating them as missing records.
5. Flush artifact bytes before publishing references. Stage unpublished large imports and register resumable post-deletion cleanup. Rollout evidence retains its separate allocation/evidence and projection/head transactions.
6. Physical writes retain the atomic-file transient-retry contract for Windows sharing violations. Extend the real-file retry tests when changing that helper.
7. Keep the SQLite subprocess alive through owner process-group cancellation so terminal evidence can drain. Verify real `SIGINT`/`SIGTERM` delivery to an isolated owner group and forced owner loss; explicit shutdown, request deadlines and parent-disconnection cleanup must still terminate the worker.
8. Run the shared SQLite/PostgreSQL contract tests for engine changes; CI requires real PostgreSQL 16–18. macOS source development needs the verified SQLite engine from `bun packages/harness/script/build-sqlite.ts`. For engine packaging changes, run `bun test --config /dev/null test/script/release/workspace-sqlite.test.ts` on macOS to open the actual unpacked archive with host libraries masked.

### SQLite and other domain stores

Expand Down
1 change: 1 addition & 0 deletions .synergy/skill/testing-guide/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Coverage has a floor. `bun run coverage:check` enforces per-package line/functio
- Every exemption entry carries a `reason`; entries that match nothing, overlap, or cover more than 25% of a package fail validation.
- Bun 1.3.14 supports no ignore comments (`istanbul ignore`, `v8 ignore`, and `c8 ignore` are all inert), so whole-file exemption is the only exclusion mechanism. Do not add ignore comments expecting them to work.
- A source file never loaded by any test counts as 0% and fails the package — add a real test that loads it rather than exempting blindly.
- Runtime-owning CLI tests must start with only the shared isolation preload, because the harness preload installs a Handle that maintenance must reject. Register these suites in the shared batch planner; preserve the original package's coverage report directory when selecting the fresh composition.
- For Solid wrappers exercised through a Vite-compiled DOM fixture, verify whether Bun attributes coverage to the emitted bundle instead of the TSX source. An exact-file exemption must identify the behavioral suite and this instrumentation boundary; keep directly testable logic measured separately.

Use [Development reference](../../../docs/reference/development.md) and [Open-source quality](../../../docs/operations/open-source-quality.md) for current command ownership. Do not invent a root `bun test`; the root script intentionally rejects that ambiguous command.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,5 @@ Coding agents and LLM tools should begin with [llms.txt](llms.txt). Read [AGENTS
Contributions, bug reports, and feature ideas are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md), follow the [Code of Conduct](CODE_OF_CONDUCT.md), and use the repository's [security reporting process](.github/SECURITY.md) for vulnerabilities rather than opening a public issue.

Synergy is open source under the [MIT License](LICENSE).

Agent records use transactional SQLite by default, with an explicit PostgreSQL option. Existing Home data upgrades through a resumable, backed-up migration. See [Agent storage](docs/architecture/agent-storage.md) and [storage operations](docs/reference/storage-and-paths.md).
4 changes: 3 additions & 1 deletion benchmark/runtime/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async function inspect() {
Config.schema().strict().parse(config)
const experiment = process.argv[4] ? Experiment.File.parse(await Bun.file(process.argv[4]).json()) : undefined
let measured: unknown
const runtime = process.argv[5] ? await composition.open({ mode: "oneshot" }) : undefined
try {
if (process.argv[5])
measured = await ScopeContext.provide({
Expand Down Expand Up @@ -60,7 +61,8 @@ async function inspect() {
}),
)
} finally {
await ScopeRuntime.disposeAll()
if (runtime) await runtime.close()
else await ScopeRuntime.disposeAll()
}
}

Expand Down
39 changes: 35 additions & 4 deletions benchmark/test/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ import { mkdtemp, rm } from "node:fs/promises"
import os from "node:os"
import path from "node:path"

async function inspect(runtime: string, config: Record<string, unknown> = {}) {
async function inspect(runtime: string, config: Record<string, unknown> = {}, model?: string) {
const home = await mkdtemp(path.join(os.tmpdir(), "synergy-bench-contract-"))
try {
const file = path.join(home, "config.json")
await Bun.write(file, JSON.stringify(config))
const child = Bun.spawn([process.execPath, "runtime/inspect.ts", runtime, file], {
const child = Bun.spawn([process.execPath, "runtime/inspect.ts", runtime, file, "", model ?? "", "synergy"], {
cwd: path.resolve(import.meta.dir, ".."),
env: { PATH: process.env.PATH, SYNERGY_HOME: home, SYNERGY_CONFIG_CONTENT: "{}" },
env: {
PATH: process.env.PATH,
SYNERGY_HOME: home,
SYNERGY_CONFIG: file,
SYNERGY_CONFIG_CONTENT: "{}",
SYNERGY_DISABLE_MODELS_FETCH: "1",
SYNERGY_DISABLE_DEFAULT_PLUGINS: "1",
MODELS_DEV_API_JSON: path.resolve(import.meta.dir, "../../packages/testing/fixtures/models-api.json"),
},
stdout: "pipe",
stderr: "pipe",
})
Expand Down Expand Up @@ -82,4 +90,27 @@ test("offline preflight rejects an unavailable measured model before inference",
} finally {
await rm(home, { recursive: true, force: true })
}
})
}, 30000)

test("full preflight measures a configured model with an owned transactional runtime", async () => {
const result = await inspect(
"full",
{
execution: { agentWorkerMinIdle: 0 },
pluginMarketplace: { enabled: false },
provider: {
fixture: {
name: "Fixture",
npm: "@ai-sdk/openai-compatible",
env: [],
options: { baseURL: "http://127.0.0.1:1/v1", apiKey: "fixture-only" },
models: { fixture: { name: "Fixture", tool_call: true, limit: { context: 128000, output: 4096 } } },
},
},
},
"fixture/fixture",
)
expect(result.code, result.stderr).toBe(0)
const report = JSON.parse(result.stdout)
expect(report.measured.model.id).toBe("fixture")
}, 30000)
35 changes: 22 additions & 13 deletions benchmark/test/test_docker.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import asyncio
import json
import os
import shutil
import zipfile
from pathlib import Path

import pytest
Expand Down Expand Up @@ -157,8 +159,6 @@ def test_real_synergy_paired_rollout(prepared_fixture) -> None:


def assert_retained_credentials_absent(root: Path) -> None:
import zipfile

sentinel = b"deterministic-local-fixture"
for evidence_file in root.glob("trials/*/attempt-*/evidence.json"):
evidence = read_json(evidence_file)
Expand All @@ -173,10 +173,13 @@ def assert_retained_credentials_absent(root: Path) -> None:


def primary_attempts(root: Path):
for call in root.glob("trials/*/attempt-*/*/agent/home/.synergy/data/sessions/*/*/rollout/runs/*/calls/*.json"):
if read_json(call)["purpose"] == "synergy":
for attempt in (call.parent.parent / "attempts" / call.stem).glob("*.json"):
yield read_json(attempt)
for file in root.glob("trials/*/attempt-*/*/agent/rollout.zip"):
with zipfile.ZipFile(file) as archive:
manifest = json.loads(archive.read("manifest.json"))
assert manifest["format"] == "synergy-rollout" and manifest["version"] == 1
for snapshot in manifest["snapshots"]:
calls = {call["id"] for call in snapshot["calls"] if call["purpose"] == "synergy"}
yield from (attempt for attempt in snapshot["attempts"] if attempt["callID"] in calls)


@pytest.mark.parametrize("mode", ["long", "disconnect", "timeout", "cancel", "docker-stop"])
Expand Down Expand Up @@ -219,14 +222,20 @@ async def run() -> None:
container = containers.strip()
probe = """
import json
import sqlite3
from contextlib import closing
from pathlib import Path
observed = False
for call in Path('/logs/agent/home').glob('.synergy/data/sessions/*/*/rollout/runs/*/calls/*.json'):
if json.loads(call.read_text())['purpose'] != 'synergy':
continue
for file in (call.parent.parent / 'attempts' / call.stem).glob('*.json'):
observed |= (json.loads(file.read_text()).get('response') or {}).get('bytes', 0) > 0
print(observed)
root = Path('/logs/agent/home/.synergy/data/storage')
namespace = json.loads((root / 'manifest.json').read_text())['namespace']
with closing(sqlite3.connect((root / 'agent.sqlite').as_uri() + '?mode=ro', uri=True)) as db:
rows = [(json.loads(key), json.loads(body)) for key, body in db.execute(
"SELECT key_text, body FROM storage_records WHERE namespace=? AND kind='rollout' AND body IS NOT NULL",
(namespace,),
)]
calls = {(tuple(key[:6]), key[7]) for key, value in rows
if len(key) == 8 and key[4] == 'runs' and key[6] == 'calls' and value['purpose'] == 'synergy'}
print(any(len(key) == 9 and key[6] == 'attempts' and (tuple(key[:6]), key[7]) in calls
and (value.get('response') or {}).get('bytes', 0) > 0 for key, value in rows))
"""
while (
await asyncio.to_thread(
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Architecture documents define current invariants, ownership boundaries, and the

- [Architecture overview](architecture/README.md)
- [Runtime and Scope](architecture/runtime-and-scope.md)
- [Agent storage](architecture/agent-storage.md)
- [Workspace and files](architecture/workspace-and-files.md)
- [Sessions and messages](architecture/session-and-messages.md)
- [LLM loop and compaction](architecture/llm-loop.md)
Expand Down
Loading
Loading