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
4 changes: 4 additions & 0 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ Session runtime state lives under the global projects tree (not in the repo):

`createOptimizedContextStore` (`src/session/optimized-context-store.ts`) wraps the
Interchange git store to keep per-checkpoint cost independent of session length.
Checkpoint commits go through system git and use the operator's global
`user.name` / `user.email` when both are set, so commit-author hooks see a real
identity; otherwise they fall back to Interchange's harness author
(`interchange-harness`, `harness@interchange.local`).
The append-only snapshots (`turns.jsonl`, `prompt.jsonl`) are written as rolling
segments (`turns-0001.jsonl`, ...) that seal at 256KB, so `git add` re-hashes only
the small active segment instead of the whole growing file. Segment zero keeps the
Expand Down
133 changes: 133 additions & 0 deletions src/session/optimized-context-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,71 @@ function tempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "opt-store-"));
}

function isolatedGitEnv(gitconfig: string): NodeJS.ProcessEnv {
const dir = tempDir();
const config = path.join(dir, "gitconfig");
fs.writeFileSync(config, gitconfig);
return {
...process.env,
GIT_CONFIG_GLOBAL: config,
GIT_CONFIG_SYSTEM: "/dev/null",
GIT_CONFIG_NOSYSTEM: "1",
HOME: dir,
XDG_CONFIG_HOME: dir,
};
}

async function headIdent(dir: string): Promise<{
authorName: string;
authorEmail: string;
committerName: string;
committerEmail: string;
}> {
const proc = Bun.spawn(["git", "-C", dir, "log", "-1", "--format=%an%n%ae%n%cn%n%ce"], {
stdout: "pipe",
stderr: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
if (exitCode !== 0) {
throw new Error(`git log failed: ${stderr.trim() || stdout.trim()}`);
}
const [authorName, authorEmail, committerName, committerEmail] = stdout.trimEnd().split("\n");
if (
authorName === undefined ||
authorEmail === undefined ||
committerName === undefined ||
committerEmail === undefined
) {
throw new Error(`unexpected git log identity output: ${JSON.stringify(stdout)}`);
}
return { authorName, authorEmail, committerName, committerEmail };
}

const EMPTY_CHECKPOINT_METADATA = {
pendingOperations: [],
tokenUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thinking: 0 },
};

async function commitEmptyCheckpoint(
dir: string,
opts?: Parameters<typeof createOptimizedContextStore>[1],
): Promise<void> {
const store = await createOptimizedContextStore(dir, opts);
await store.writeMetadata(EMPTY_CHECKPOINT_METADATA);
await store.commit({ message: "checkpoint: tool-execution" });
}

const HARNESS_IDENT = {
authorName: "interchange-harness",
authorEmail: "harness@interchange.local",
committerName: "interchange-harness",
committerEmail: "harness@interchange.local",
};

function turn(text: string): ConversationTurn {
return { role: "user", content: [{ type: "text", text }], timestamp: 1 };
}
Expand Down Expand Up @@ -472,4 +537,72 @@ describe("createOptimizedContextStore checkpoint", () => {
const atHead = await store.readAt(head.hash);
expect(atHead).toHaveLength(total);
}, 20_000);

test("records the operator identity as author and committer from global git config", async () => {
const dir = tempDir();
await commitEmptyCheckpoint(dir, {
env: isolatedGitEnv(`[user]\n\tname = Sawyer\n\temail = sawyer@dirtroad.dev\n`),
});

expect(await headIdent(dir)).toEqual({
authorName: "Sawyer",
authorEmail: "sawyer@dirtroad.dev",
committerName: "Sawyer",
committerEmail: "sawyer@dirtroad.dev",
});
});

test("records an injected author as both author and committer", async () => {
const dir = tempDir();
await commitEmptyCheckpoint(dir, {
author: { name: "Sawyer", email: "sawyer@dirtroad.dev" },
});

expect(await headIdent(dir)).toEqual({
authorName: "Sawyer",
authorEmail: "sawyer@dirtroad.dev",
committerName: "Sawyer",
committerEmail: "sawyer@dirtroad.dev",
});
});

test("falls back to the harness identity when global config is missing", async () => {
const dir = tempDir();
await commitEmptyCheckpoint(dir, { env: isolatedGitEnv("") });
expect(await headIdent(dir)).toEqual(HARNESS_IDENT);
});

test("falls back when only one of name or email is set", async () => {
const nameOnly = tempDir();
await commitEmptyCheckpoint(nameOnly, {
env: isolatedGitEnv(`[user]\n\tname = Sawyer\n`),
});
expect(await headIdent(nameOnly)).toEqual(HARNESS_IDENT);

const emailOnly = tempDir();
await commitEmptyCheckpoint(emailOnly, {
env: isolatedGitEnv(`[user]\n\temail = sawyer@dirtroad.dev\n`),
});
expect(await headIdent(emailOnly)).toEqual(HARNESS_IDENT);
});

test("falls back when global name and email are empty or whitespace", async () => {
const bothEmpty = tempDir();
await commitEmptyCheckpoint(bothEmpty, {
env: isolatedGitEnv(`[user]\n\tname =\n\temail =\n`),
});
expect(await headIdent(bothEmpty)).toEqual(HARNESS_IDENT);

const bothWhitespace = tempDir();
await commitEmptyCheckpoint(bothWhitespace, {
env: isolatedGitEnv(`[user]\n\tname = \n\temail = \n`),
});
expect(await headIdent(bothWhitespace)).toEqual(HARNESS_IDENT);

const nameOnlyWhitespaceEmail = tempDir();
await commitEmptyCheckpoint(nameOnlyWhitespaceEmail, {
env: isolatedGitEnv(`[user]\n\tname = Sawyer\n\temail = \n`),
});
expect(await headIdent(nameOnlyWhitespaceEmail)).toEqual(HARNESS_IDENT);
});
});
74 changes: 60 additions & 14 deletions src/session/optimized-context-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ const TOOL_OUTPUT_DIR = "tool-output";

const log = getLogger([LOG_NAMESPACE_ROOT, "session", "context-store"]);

const AUTHOR = {
export interface CheckpointAuthor {
name: string;
email: string;
}

const HARNESS_AUTHOR: CheckpointAuthor = {
name: "interchange-harness",
email: "harness@interchange.local",
};
Expand Down Expand Up @@ -299,16 +304,25 @@ export async function loadRecentTurns(dir: string, minTurns: number): Promise<Co
return turns;
}

async function runGit(dir: string, args: string[]): Promise<string> {
async function runGit(
dir: string,
args: string[],
author?: CheckpointAuthor,
env: NodeJS.ProcessEnv = process.env,
): Promise<string> {
const proc = Bun.spawn(["git", "-C", dir, ...args], {
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
GIT_AUTHOR_NAME: AUTHOR.name,
GIT_AUTHOR_EMAIL: AUTHOR.email,
GIT_COMMITTER_NAME: AUTHOR.name,
GIT_COMMITTER_EMAIL: AUTHOR.email,
...env,
...(author === undefined
? {}
: {
GIT_AUTHOR_NAME: author.name,
GIT_AUTHOR_EMAIL: author.email,
GIT_COMMITTER_NAME: author.name,
GIT_COMMITTER_EMAIL: author.email,
}),
},
});
const [exitCode, stdout, stderr] = await Promise.all([
Expand All @@ -322,6 +336,33 @@ async function runGit(dir: string, args: string[]): Promise<string> {
return stdout.trimEnd();
}

async function gitConfigGlobal(key: string, env: NodeJS.ProcessEnv): Promise<string | null> {
const proc = Bun.spawn(["git", "config", "--global", "--get", key], {
stdout: "pipe",
stderr: "pipe",
env,
});
const [exitCode, stdout] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
if (exitCode !== 0) return null;
const value = stdout.trim();
return value.length > 0 ? value : null;
}

// Operator commit-author hooks see a real identity; machines without both
// global user.name and user.email still checkpoint via the harness fallback.
async function resolveCheckpointAuthor(env: NodeJS.ProcessEnv): Promise<CheckpointAuthor> {
const [name, email] = await Promise.all([
gitConfigGlobal("user.name", env),
gitConfigGlobal("user.email", env),
]);
if (name === null || email === null) return HARNESS_AUTHOR;
return { name, email };
}

/**
* Names of the tail turn segments (`turns-0001.jsonl`, ...) present in a commit
* tree, in segment order. The base store reads the zeroth segment itself; these
Expand Down Expand Up @@ -395,7 +436,12 @@ async function reconcileSegmentStaging(
* segment files so `git add` re-hashes only the small active segment, and only
* spilled tool-output blobs that are new since the last commit are staged.
*/
export async function createOptimizedContextStore(dir: string): Promise<ContextStore> {
export async function createOptimizedContextStore(
dir: string,
opts?: { author?: CheckpointAuthor; env?: NodeJS.ProcessEnv },
): Promise<ContextStore> {
const gitEnv = opts?.env ?? process.env;
const author = opts?.author ?? (await resolveCheckpointAuthor(gitEnv));
const base = await createIsogitStore(dir);
const pendingBlobFilepaths = new Set<string>();
const pendingSegmentPaths = new Set<string>();
Expand Down Expand Up @@ -550,12 +596,12 @@ export async function createOptimizedContextStore(dir: string): Promise<ContextS
if (remove.length > 0) {
await runGit(dir, ["rm", "--cached", "--ignore-unmatch", "--", ...remove]);
}
await runGit(dir, [
"commit",
"-m",
options.message,
`--author=${AUTHOR.name} <${AUTHOR.email}>`,
]);
await runGit(
dir,
["commit", "-m", options.message, `--author=${author.name} <${author.email}>`],
author,
gitEnv,
);
pendingBlobFilepaths.clear();
pendingSegmentPaths.clear();
return describeHead(dir, options.message);
Expand Down
Loading