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
12 changes: 4 additions & 8 deletions apps/server/src/harness/atomic/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ export type AtomicSettings = {
extensions?: readonly string[];
/**
* The SQLite file the workflows of Planner sessions are journaled in, in place
* of Atomic's default beside its agent directory. Atomic reads the journal
* location only from `ATOMIC_WORKFLOW_JOURNAL_PATH` and the on-disk settings,
* so the adapter sets that variable until it shuts down.
* of Atomic's default beside its agent directory. Each Planner session is given
* it through `createAgentSession({ journal })`, which takes precedence over
* `ATOMIC_WORKFLOW_JOURNAL_PATH`; the process environment is left unchanged.
*/
journal?: string;
};
Expand Down Expand Up @@ -472,8 +472,6 @@ export function createAtomicAdapter(
}
let runtime: Promise<ModelRuntime> | undefined;
let open = new Set<HarnessV1Session>();
let journal = process.env.ATOMIC_WORKFLOW_JOURNAL_PATH;
if (settings.journal) process.env.ATOMIC_WORKFLOW_JOURNAL_PATH = settings.journal;

return {
specificationVersion: "harness-v1",
Expand Down Expand Up @@ -660,6 +658,7 @@ export function createAtomicAdapter(
builtins: full ? undefined : BUILTINS_OFF,
tools: full ? undefined : [...hostNames, ATOMIC_RESULT_TOOL_NAME],
extensionBindings: full ? { humanInput: full.humanInput } : undefined,
journal: full && settings.journal ? { path: settings.journal } : undefined,
customTools: turn.tools.map(hostTool),
});
let session = created.session;
Expand Down Expand Up @@ -838,9 +837,6 @@ export function createAtomicAdapter(

async shutdown() {
await Promise.allSettled([...open].map(session => session.doDestroy()));
if (!settings.journal) return;
if (journal === undefined) delete process.env.ATOMIC_WORKFLOW_JOURNAL_PATH;
else process.env.ATOMIC_WORKFLOW_JOURNAL_PATH = journal;
},
};
}
5 changes: 4 additions & 1 deletion apps/server/src/harness/atomic/full.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export default workflow({
operator: await Bun.file(join(agentDir, "settings.json")).exists(),
journal: await Bun.file(journal).exists(),
defaultJournal: await Bun.file(join(root, "workflows", "journal.sqlite")).exists(),
environment: process.env.ATOMIC_WORKFLOW_JOURNAL_PATH,
project: projectPackage
? await Bun.file(join(cwd, ".atomic", "settings.json")).text()
: undefined,
Expand Down Expand Up @@ -531,8 +532,10 @@ test(
expect(stage.toolNames).toContain("write");
expect(stage.toolNames).not.toContain(ATOMIC_RESULT_TOOL_NAME);
expect(stage.system).not.toContain("CHOPIN-INSTRUCTIONS-MARKER");
// The run is journaled where Chopin chose, not in Atomic's default beside its agent directory.
// The run is journaled where Chopin chose, not in Atomic's default beside its agent directory,
// and through the session rather than the process-wide environment variable.
expect(result.files).toMatchObject({ journal: true, defaultJournal: false });
expect(result.files?.environment).toBeUndefined();
} finally {
if (environment !== undefined) process.env.NODE_ENV = environment;
await room.close();
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/harness/atomic/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ export function stateDirectory(): string {
/**
* Where Planner sessions journal their workflow runs. A run resumes only with
* the journal and working directory it started with, so the journal lives
* beside the channel directories rather than in Atomic's own home.
* beside the channel directories rather than in Atomic's own home. An
* operator's non-empty `ATOMIC_WORKFLOW_JOURNAL_PATH` still names the file.
*/
export function workflowJournal(): string {
return join(stateDirectory(), "atomic", "workflows.sqlite");
return process.env.ATOMIC_WORKFLOW_JOURNAL_PATH
|| join(stateDirectory(), "atomic", "workflows.sqlite");
}

/**
Expand Down
13 changes: 8 additions & 5 deletions apps/server/src/harness/harnesses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, expect, it } from "bun:test";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { workflowJournal } from "./atomic/workspace";
import { harnesses, harnessFor, registerCredential, shutdownHarnesses } from "./harnesses";

afterEach(shutdownHarnesses);
Expand Down Expand Up @@ -60,20 +61,22 @@ it("journals Atomic workflow runs in Chopin's state directory unless the operato
let state = join(tmpdir(), `chopin-state-${crypto.randomUUID()}`);
let restore = (name: string, value: string | undefined) =>
value === undefined ? delete process.env[name] : process.env[name] = value;
let journal = (): string | undefined => process.env.ATOMIC_WORKFLOW_JOURNAL_PATH;
try {
process.env.XDG_STATE_HOME = state;
delete process.env.ATOMIC_WORKFLOW_JOURNAL_PATH;
harnessFor({ harness: "atomic", harnessAuth: "ai-gateway", host: "0.0.0.0" });
if (process.platform === "linux") {
expect(journal()).toBe(join(state, "chopin", "atomic", "workflows.sqlite"));
expect(workflowJournal()).toBe(join(state, "chopin", "atomic", "workflows.sqlite"));
}
// Each Planner session is given its journal; the process environment is left alone.
harnessFor({ harness: "atomic", harnessAuth: "ai-gateway", host: "0.0.0.0" });
expect(process.env.ATOMIC_WORKFLOW_JOURNAL_PATH).toBeUndefined();
await shutdownHarnesses();
expect(journal()).toBeUndefined();

process.env.ATOMIC_WORKFLOW_JOURNAL_PATH = "/srv/atomic/runs.sqlite";
expect(workflowJournal()).toBe("/srv/atomic/runs.sqlite");
harnessFor({ harness: "atomic", harnessAuth: "ai-gateway", host: "0.0.0.0" });
expect(journal()).toBe("/srv/atomic/runs.sqlite");
await shutdownHarnesses();
expect(process.env.ATOMIC_WORKFLOW_JOURNAL_PATH).toBe("/srv/atomic/runs.sqlite");
} finally {
await shutdownHarnesses();
restore("XDG_STATE_HOME", previous.state);
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/harness/harnesses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function createAtomicHarness(
return createAtomicAdapter({
auth: settings.auth as AtomicAuthMode,
extensions: settings.extensions,
journal: process.env.ATOMIC_WORKFLOW_JOURNAL_PATH || workflowJournal(),
journal: workflowJournal(),
});
}

Expand Down
24 changes: 12 additions & 12 deletions bun.lock

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

4 changes: 3 additions & 1 deletion docs/hosted-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,9 @@ workflow journal, so runs interrupted that way can be resumed from a later
session. Chopin keeps that journal at
`$XDG_STATE_HOME/chopin/atomic/workflows.sqlite` under the same per-user state
directory as the documents' own directories, because a run resumes only with
the journal and working directory it started in. A non-empty
the journal and working directory it started in. Each Planner session is
given that file through Atomic's `createAgentSession({ journal })`, so the
server's process environment is left unchanged. An operator's non-empty
`ATOMIC_WORKFLOW_JOURNAL_PATH` names another file instead; Atomic's
`workflows.journal` setting is not used for Planner sessions. A Build is not a workflow run and has no card in Chat: its tasks, their
state and pull requests show under **Build**, whichever machine runs it. The
Expand Down
5 changes: 3 additions & 2 deletions docs/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ each owner's GitHub App user token and does not consume `HARNESS_AUTH`;
setting `auto` on loopback passes the startup guard but does not enable host
login in that adapter.

`HARNESS=atomic` runs Atomic 0.9.38-alpha.9 in the Chopin server process through its
`HARNESS=atomic` runs Atomic 0.9.39 in the Chopin server process through its
headless SDK (`createAgentSession()`). It does not spawn the `atomic` CLI or use
RPC mode.

Expand Down Expand Up @@ -152,7 +152,8 @@ Atomic caveats:
x64, including glibc and musl native modules. Expect a larger image.
- Atomic workflows need no database server. Their runs are journaled in a
SQLite file, `$XDG_STATE_HOME/chopin/atomic/workflows.sqlite`, beside the
documents' Planner directories (see
documents' Planner directories, which Chopin passes to each Planner session
as its `journal` rather than through the process environment (see
[Full Atomic Planner](hosted-agent.md#full-atomic-planner)). The Docker image
sets `XDG_STATE_HOME=/var/lib/chopin`; mount a volume there to keep runs
and those directories when the container is replaced.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@ai-sdk/harness": "1.0.146",
"@ai-sdk/mcp": "2.0.72",
"@ai-sdk/sandbox-just-bash": "1.0.146",
"@bastani/atomic": "0.9.38-alpha.9",
"@bastani/atomic": "0.9.39",
"ai": "7.0.135",
"typebox": "1.3.7",
"zod": "4.4.3"
Expand Down
Loading