Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
9b803cb
Sign the latency-gate baseline agent per-turn commits
alexanderguy Jul 1, 2026
4e2c4b4
Add a flag-gated delta-scoped claim-check validation path
alexanderguy Jul 1, 2026
7452c3e
Lock the delta claim-check watermark prune boundary
alexanderguy Jul 1, 2026
1188a7a
Propagate the delta-scope claim-check flag to the spawned sidecar
alexanderguy Jul 1, 2026
5ea0f82
Thread a per-repo cache through the repo-store git calls
alexanderguy Jul 1, 2026
73ce417
Assemble repo-store commit trees without the on-disk index
alexanderguy Jul 1, 2026
79430ea
Narrow claim-check writes to a targeted tree delta
alexanderguy Jul 1, 2026
570b061
Harden the delta write against ambiguous and out-of-scope input
alexanderguy Jul 1, 2026
ba12271
Surface prospective consumed OIDs from the tree listing
alexanderguy Jul 1, 2026
f72a13b
Close the delta-write review follow-ups
alexanderguy Jul 1, 2026
1f65cf1
Reject delete shapes that mismatch the base entry type
alexanderguy Jul 1, 2026
30726fa
Thread the repo-store git cache through the claim-check delta reads
alexanderguy Jul 1, 2026
2688f3a
Make the delta-scoped claim-check validation the default
alexanderguy Jul 1, 2026
def142a
Document the workflow-run latency benches
alexanderguy Jul 1, 2026
b139223
Slice the substrate-mirror turns from memory instead of reloading
alexanderguy Jul 1, 2026
2cd9944
Pin that inbound mail delivery does not dedup by Message-ID
alexanderguy Jul 1, 2026
7ca4144
Extract the shared workflow-deploy orchestrator construction
alexanderguy Jul 1, 2026
b060e6b
Deploy a single-step workflow once at the head
alexanderguy Jul 2, 2026
056fc6c
Reject a workflow deploy that names an unbuildable provider
alexanderguy Jul 2, 2026
a758d7d
Extract the workflow-deployment spawn sequence into a core
alexanderguy Jul 2, 2026
bc2f88f
Persist a per-deployment record for restore
alexanderguy Jul 2, 2026
4f3545a
Restore persisted workflow deployments on sidecar restart
alexanderguy Jul 2, 2026
b78efb0
Promote child-termination helpers out of the recycle path
alexanderguy Jul 2, 2026
e991c56
Bound the child spawn ready handshake with a timeout
alexanderguy Jul 2, 2026
bcf40ab
Re-register workflow deployment routes on sidecar reconnect
alexanderguy Jul 3, 2026
75f16d6
Ack the agent key for a single-step deployment
alexanderguy Jul 3, 2026
a448ebb
Run single-agent instances as single-step workflows
alexanderguy Jul 3, 2026
5624a6f
Fail over inference sources per step in workflow children
alexanderguy Jul 3, 2026
84602e3
Prove inference-source failover in a real workflow child
alexanderguy Jul 3, 2026
b51eb72
Provision launched agents without the trivial workflow wrap
alexanderguy Jul 3, 2026
82778e4
Delete the orchestrator's dead trivial deploy branch
alexanderguy Jul 3, 2026
cf1ffbd
Register the deployment signing identity for every workflow deploy
alexanderguy Jul 3, 2026
50a0cfa
Separate warm-harness start from deploy staging
alexanderguy Jul 3, 2026
b5572f1
Add transient per-step route binding to the sidecar router
alexanderguy Jul 3, 2026
2ec6222
Provision multi-step workflow steps without spawning
alexanderguy Jul 3, 2026
8417bc0
Exercise the reconnect path in the per-step route test
alexanderguy Jul 3, 2026
ad16a80
Stage multi-step workflow steps without a warm harness
alexanderguy Jul 3, 2026
9827958
Cover shared deploy staging through the staged entry point
alexanderguy Jul 3, 2026
6bbca87
Remove the warm-harness launch entry point
alexanderguy Jul 4, 2026
0ecc8be
Retire the session-start wire path
alexanderguy Jul 4, 2026
5e782e8
Remove the in-process trivial deploy path
alexanderguy Jul 4, 2026
a9b0c83
Retire the in-process session runtime
alexanderguy Jul 4, 2026
add1b91
Rename the survivors the deletions left misnamed
alexanderguy Jul 4, 2026
c4c1900
Remove the dead agent-metadata persistence layer
alexanderguy Jul 4, 2026
d1fe915
Rewrite the deploy narrative for the substrate-only flow
alexanderguy Jul 4, 2026
59ab2fa
Stop pushing grants and sources to the sidecar on reconnect
alexanderguy Jul 4, 2026
6298f18
Remove the unused abort instance route
alexanderguy Jul 4, 2026
dfdb533
Advance the mirror turn count by the persisted delta
alexanderguy Jul 4, 2026
7aa4da6
Drop the vestigial deploy-result discriminant
alexanderguy Jul 4, 2026
1e2d591
Register the deployment address after resolving the ack key
alexanderguy Jul 4, 2026
376bd5e
Cover the unwired single-step deploy handoff error
alexanderguy Jul 4, 2026
df1d973
Refresh comments the renames and deletions left stale
alexanderguy Jul 4, 2026
f928d20
Reject a same-address deploy that is still in flight
alexanderguy Jul 4, 2026
b768b3d
Reverse the recorded hub key when a single-step spawn fails
alexanderguy Jul 4, 2026
23b76fa
Widen the sources-updated control frame to carry the source list
alexanderguy Jul 4, 2026
b2014a2
Read single-step build sources through a mutable reference
alexanderguy Jul 5, 2026
d1ad3ed
Rotate a warm single-step agent's sources on a sources-updated frame
alexanderguy Jul 5, 2026
4ad92b6
Push an inference-source rotation to the child from the supervisor
alexanderguy Jul 5, 2026
242d0ad
Route inbound source rotations to single-step deployments
alexanderguy Jul 5, 2026
379af79
Answer inbound source rotations over the sidecar hub link
alexanderguy Jul 5, 2026
a47aef3
Answer a malformed request frame instead of dropping it silently
alexanderguy Jul 5, 2026
3b2b10f
Reject a malformed pack frame instead of hanging its transfer
alexanderguy Jul 5, 2026
b912e44
Retire the orphaned session.abort and grants.update wire surface
alexanderguy Jul 5, 2026
b81441f
Serialize the substrate-mirror entry points against overlap
alexanderguy Jul 5, 2026
7643606
Drop a stale tracker reference from the recovery-sweep comment
alexanderguy Jul 5, 2026
1af0ded
Build the child spawn env through one shared producer
alexanderguy Jul 6, 2026
1001773
Release the mail subscription when a spawn fails
alexanderguy Jul 6, 2026
862e314
Carry a live source rotation across a recycle respawn
alexanderguy Jul 6, 2026
6e2d683
Keep a failing deploy cleanup from masking the real error
alexanderguy Jul 6, 2026
b5f69b3
Persist a source rotation so it survives a sidecar restart
alexanderguy Jul 6, 2026
41019c0
Re-apply the live sources after a warm agent's first build
alexanderguy Jul 6, 2026
9bbd960
Route every startup-phase throw through the teardown owner
alexanderguy Jul 6, 2026
3748ac2
Refresh comments the trivial-path removal left stale
alexanderguy Jul 6, 2026
ec685ba
Rewrite the harness design doc for the deployment-record model
alexanderguy Jul 6, 2026
a9ffa6d
Write the deployment record atomically and durably
alexanderguy Jul 6, 2026
bb97c38
Bound the recycle respawn ready handshake with a timeout
alexanderguy Jul 6, 2026
26644b1
Swap the rotation sources before persisting to survive a recycle
alexanderguy Jul 6, 2026
c487da4
Round-trip run events faithfully in the child run tests
alexanderguy Jul 6, 2026
4cedbe8
Throw on a missing or mismatched terminal event in the child
alexanderguy Jul 6, 2026
898ab23
Store the deployment address and public key on its row
alexanderguy Jul 6, 2026
42dae61
Persist and resolve the deployment public key hub-side
alexanderguy Jul 6, 2026
9539468
Challenge workflow-deployment addresses on reconnect
alexanderguy Jul 7, 2026
ba061bf
Gate register-frame routing on key existence
alexanderguy Jul 7, 2026
a041cc6
Fail closed on a key-lookup error during reconnect
alexanderguy Jul 7, 2026
6e89b98
Make register additive instead of replacing the address set
alexanderguy Jul 7, 2026
b00e9f7
Serialize per-connection frame dispatch to close the register race
alexanderguy Jul 7, 2026
f461c9f
Close the sidecar socket when a reconnect deploy-ref read fails
alexanderguy Jul 7, 2026
25ef8f7
Reap the respawned child when its credentials read fails
alexanderguy Jul 7, 2026
5266d81
Preserve the spawn failure cause when teardown also throws
alexanderguy Jul 7, 2026
d491345
Attribute source-rotation rollback safety to frame serialization
alexanderguy Jul 7, 2026
ee90e33
Reconcile the deployment key comment with the not-null address add
alexanderguy Jul 7, 2026
352f1ef
Rewrite the claim-check API comment for the delta write path
alexanderguy Jul 7, 2026
f2b3485
Correct two stale symbol fates in the execution-host design doc
alexanderguy Jul 7, 2026
d1183f2
Correct stale in-process-harness references after its removal
alexanderguy Jul 7, 2026
e381eba
Reject an empty sources list at the sources-update boundary
alexanderguy Jul 7, 2026
4541fa7
Assert the deploy public key shape in orchestrator tests
alexanderguy Jul 7, 2026
b6f7908
Remove the dead deploy-apply error frame and emitter path
alexanderguy Jul 7, 2026
610b8d6
Guard shutdown teardown so it always kills the child and stops
alexanderguy Jul 7, 2026
330a4d8
Reflow three mid-sentence comment wraps
alexanderguy Jul 7, 2026
7699e47
Make supervisor shutdown structurally total
alexanderguy Jul 7, 2026
5179d03
Reflow an awkward comment wrap in tool materialization
alexanderguy Jul 7, 2026
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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ lint: FORCE

test: FORCE
bun test packages/ apps/ bin/ tests/agent/ tests/agent-audit-log/ tests/agent-blob-spill/ tests/agent-common/ tests/agent-multi-provider/ tests/agent-quickstart/ tests/agent-resume/ tests/agent-rewind/ tests/agent-rich-tool/ tests/agent-structured-payload/ tests/coding-agent/ tests/hub-agent/lib/ tests/inference-testing/ tests/tool-packaging/ tests/workflow/
bun test --timeout 120000 tests/hub-agent/deploy-flow.test.ts tests/workflow-deploy/trivial-roundtrip.test.ts tests/workflow-deploy/multistep-signal.test.ts tests/workflow-deploy/single-step-real-agent.test.ts tests/workflow-deploy/single-step-message-input.test.ts tests/workflow-deploy/single-step-posix-tool.test.ts tests/workflow-deploy/single-step-grants-bridge.test.ts tests/workflow-deploy/single-step-event-threading.test.ts tests/workflow-deploy/single-step-conversation-durability.test.ts tests/workflow-deploy/single-step-full-lifecycle.test.ts tests/workflow-deploy/cross-process-custom-adapter.test.ts tests/workflow-deploy/conversation-state-wal.test.ts tests/workflow-deploy/drain-roundtrip.test.ts tests/workflow-deploy/child-workflow-roundtrip.test.ts tests/workflow-deploy/unresolvable-director.test.ts tests/workflow-deploy/fifo-mail.test.ts tests/workflow-deploy/mail-edge-cases.test.ts tests/inference/ tests/skill-attachment-flow.test.ts tests/hub-api/ tests/db/
bun test --timeout 120000 tests/workflow-deploy/multistep-signal.test.ts tests/workflow-deploy/single-step-real-agent.test.ts tests/workflow-deploy/instance-reroute-real-agent.test.ts tests/workflow-deploy/instance-failover-real-agent.test.ts tests/workflow-deploy/multistep-signed-send.test.ts tests/workflow-deploy/single-step-message-input.test.ts tests/workflow-deploy/single-step-posix-tool.test.ts tests/workflow-deploy/single-step-grants-bridge.test.ts tests/workflow-deploy/single-step-event-threading.test.ts tests/workflow-deploy/single-step-conversation-durability.test.ts tests/workflow-deploy/single-step-full-lifecycle.test.ts tests/workflow-deploy/cross-process-custom-adapter.test.ts tests/workflow-deploy/conversation-state-wal.test.ts tests/workflow-deploy/drain-roundtrip.test.ts tests/workflow-deploy/child-workflow-roundtrip.test.ts tests/workflow-deploy/unresolvable-director.test.ts tests/workflow-deploy/fifo-mail.test.ts tests/workflow-deploy/mail-edge-cases.test.ts tests/inference/ tests/skill-attachment-flow.test.ts tests/hub-api/ tests/db/

test-load: FORCE
bun test --timeout 300000 tests/workflow-deploy/fifo-mail-load.test.ts
Expand Down
4 changes: 1 addition & 3 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createDB, createGrantStore } from "@intx/db";
import { createDB } from "@intx/db";
import { createApp, createAuth } from "@intx/hub-api";
import {
createAgentRepoStore,
Expand Down Expand Up @@ -35,7 +35,6 @@ const { db } = createDB({
});

const auth = createAuth(db);
const grantStore = createGrantStore(db);

const hubDataDir = process.env["HUB_DATA_DIR"];
if (!hubDataDir) {
Expand Down Expand Up @@ -141,7 +140,6 @@ createHubSessionOrchestrator({
router: sidecarRouter,
db,
eventCollectors,
grantStore,
agentRepoStore,
});

Expand Down
64 changes: 50 additions & 14 deletions apps/sidecar/src/agent-key-registration-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ import {
} from "@intx/workflow-host";
import type { AgentDeployFrame } from "@intx/types/sidecar";

import { createSidecarDeployRouter } from "./workflow-host-wiring";
import { type } from "arktype";

import {
createSidecarDeployRouter,
deriveDeploymentId,
} from "./workflow-host-wiring";
import { WorkflowDeploymentRecord } from "./workflow-deployment-record";
import {
createMultistepDrainRouter,
createMultistepMailRouter,
Expand Down Expand Up @@ -219,34 +225,33 @@ describe("agent signing-key registration lifecycle on the host transport", () =>
const repoStore = createSpawnTestRepoStore(tempBase);

const router = createSidecarDeployRouter({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- multi-step branch never invokes sessions
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- the single-step branch invokes only initRepo (head deploy-tree repo); provisionAgent/persistHubPublicKey stay unused (the supervised child mints its own key and persists no hub-agent config)
sessions: {
provisionAgent: async () => {
throw new Error("must not invoke provisionAgent");
},
persistHubPublicKey: async () => {
throw new Error("must not invoke persistHubPublicKey");
},
initRepo: async () => undefined,
} as unknown as Parameters<
typeof createSidecarDeployRouter
>[0]["sessions"],
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- only loadOrGenerateKey is exercised (the single-step branch registers the agent's signing key on the transport before spawn)
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- the single-step branch registers the agent's signing key (loadOrGenerateKey) and records the hub key (recordHubKey) at the head before spawn
keyStore: {
recordHubKey: () => {
throw new Error("must not invoke recordHubKey");
},
recordHubKey: () => undefined,
loadOrGenerateKey: async () => ({
keyPair: await generateKeyPair(),
isNew: false,
}),
} as unknown as Parameters<
typeof createSidecarDeployRouter
>[0]["keyStore"],
onAgentEvent: () => () => undefined,
transport,
repoStore,
signingKeySeed: keyPair.privateKey,
createAgentCrypto: createEd25519Crypto,
assertSourceBuildable: () => undefined,
registerDeployment: () => undefined,
unregisterDeployment: () => undefined,
multistepSubprocessSpawner: spawner,
Expand All @@ -271,13 +276,15 @@ describe("agent signing-key registration lifecycle on the host transport", () =>
steps: { "step-1": { kind: "step" } },
},
sources: {
"step-1": {
id: "step-1",
provider: "anthropic",
baseURL: "https://api.anthropic.com",
apiKey: "sk-step-1",
model: "claude-3-5",
},
"step-1": [
{
id: "step-1",
provider: "anthropic",
baseURL: "https://api.anthropic.com",
apiKey: "sk-step-1",
model: "claude-3-5",
},
],
},
},
};
Expand Down Expand Up @@ -316,6 +323,26 @@ describe("agent signing-key registration lifecycle on the host transport", () =>
// (b) Registered after deploy -- the supervisor can now sign agent mail.
expect(isRegistered(transport)).toBe(true);

// (b') The deploy persisted a schema-valid restore record for the
// deployment, carrying the head address so a boot-time restore can
// re-establish it.
const deploymentId = deriveDeploymentId(AGENT_ADDRESS);
const recordFile = path.join(
dataDir,
"workflow-runs",
deploymentId,
"deployment.json",
);
const parsedRecord = WorkflowDeploymentRecord(
JSON.parse(await fs.readFile(recordFile, "utf8")),
);
if (parsedRecord instanceof type.errors) {
throw new Error(
`deployment record failed validation: ${parsedRecord.summary}`,
);
}
expect(parsedRecord.agentAddress).toBe(AGENT_ADDRESS);

const undeploy = router.undeploy;
if (undeploy === undefined) throw new Error("router.undeploy undefined");
await undeploy({
Expand All @@ -327,6 +354,15 @@ describe("agent signing-key registration lifecycle on the host transport", () =>
// (c) Unregistered after undeploy -- no leaked key for a torn-down agent.
expect(isRegistered(transport)).toBe(false);

// (c') Undeploy dropped the restore record so a boot-time restore will
// not re-spawn the torn-down deployment.
expect(
await fs.access(recordFile).then(
() => true,
() => false,
),
).toBe(false);

await fs.rm(tempBase, { recursive: true, force: true });
await fs.rm(dataDir, { recursive: true, force: true });
});
Expand Down
102 changes: 102 additions & 0 deletions apps/sidecar/src/atomic-write.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, test, expect } from "bun:test";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";

import { writeFileAtomicDurable } from "./atomic-write";

async function makeDir(): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), "atomic-write-"));
}

async function listNames(dir: string): Promise<string[]> {
return (await fs.readdir(dir)).sort();
}

describe("writeFileAtomicDurable", () => {
test("round-trips contents to the target path", async () => {
const dir = await makeDir();
const file = path.join(dir, "record.json");

await writeFileAtomicDurable(file, '{"a":1}', { mode: 0o600 });
expect(await fs.readFile(file, "utf8")).toBe('{"a":1}');

await fs.rm(dir, { recursive: true, force: true });
});

test("applies mode on the created file", async () => {
const dir = await makeDir();
const file = path.join(dir, "record.json");

await writeFileAtomicDurable(file, "x", { mode: 0o600 });
const stat = await fs.stat(file);
expect(stat.mode & 0o777).toBe(0o600);

await fs.rm(dir, { recursive: true, force: true });
});

test("re-applies mode when overwriting an existing file", async () => {
const dir = await makeDir();
const file = path.join(dir, "record.json");

// Seed a pre-existing file with a laxer mode. A plain in-place
// overwrite keeps the original mode; the temp+rename path creates a
// fresh file every write, so the requested mode actually lands.
await fs.writeFile(file, "old", { mode: 0o644 });
await writeFileAtomicDurable(file, "new", { mode: 0o600 });

expect(await fs.readFile(file, "utf8")).toBe("new");
const stat = await fs.stat(file);
expect(stat.mode & 0o777).toBe(0o600);

await fs.rm(dir, { recursive: true, force: true });
});

test("leaves no temp orphan after a successful write", async () => {
const dir = await makeDir();
const file = path.join(dir, "record.json");

await writeFileAtomicDurable(file, "x", { mode: 0o600 });
expect(await listNames(dir)).toEqual(["record.json"]);

await fs.rm(dir, { recursive: true, force: true });
});

test("a write that fails before staging leaves the prior file intact", async () => {
const dir = await makeDir();
const file = path.join(dir, "record.json");
await fs.writeFile(file, "old", { mode: 0o600 });

// Deny writes to the directory so the temp-file creation fails
// outright. The prior complete record must survive untouched -- an
// interrupted rotation must never corrupt the sole restore source.
await fs.chmod(dir, 0o500);
try {
await expect(
writeFileAtomicDurable(file, "new", { mode: 0o600 }),
).rejects.toThrow();
expect(await fs.readFile(file, "utf8")).toBe("old");
expect(await listNames(dir)).toEqual(["record.json"]);
} finally {
await fs.chmod(dir, 0o700);
await fs.rm(dir, { recursive: true, force: true });
}
});

test("a write that fails at the rename unlinks its staged temp", async () => {
const dir = await makeDir();
const file = path.join(dir, "record.json");

// Occupy the target path with a directory so the rename fails after
// the temp file has already been staged and fsynced. This exercises
// the error-path unlink: the staged temp must be removed rather than
// stranded as an orphan.
await fs.mkdir(file);
await expect(
writeFileAtomicDurable(file, "new", { mode: 0o600 }),
).rejects.toThrow();
expect(await listNames(dir)).toEqual(["record.json"]);

await fs.rm(dir, { recursive: true, force: true });
});
});
79 changes: 79 additions & 0 deletions apps/sidecar/src/atomic-write.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Atomic, durable file replacement for the sidecar's non-rebuildable
// on-disk records. Distinct from the cache's rebuildable temp+rename
// (no fsync, a lost write just forces a re-fetch) and from
// `fsyncWriteFile`'s in-place fsync write (no atomicity, a torn write
// leaves a half-file): this is the tier for a sole restore source that
// must survive both a process kill and a power loss without ever
// exposing a torn record.

import { open, rename, unlink } from "node:fs/promises";
import { dirname } from "node:path";

import { getLogger } from "@intx/log";
import { hexEncode } from "@intx/types";

const logger = getLogger(["interchange", "sidecar", "atomic-write"]);

export interface AtomicWriteOptions {
/** Permission mode applied when the temp file is created. */
mode: number;
}

/**
* Replace `path` with `contents` atomically and durably. The bytes land
* in a fresh per-write temp file that is fsynced and then `rename`d over
* `path`; because rename is atomic within a directory, a reader only
* ever observes the prior complete file or the new complete file, never
* a torn one. The fsync before the rename is what extends that
* guarantee past process death to OS crash / power loss: without it, the
* ext4 delayed-allocation window can surface the renamed path as a
* zero-length file after a power loss.
*
* The parent directory is fsynced after the rename so the new link is
* itself durable, but a filesystem that rejects directory fsync
* (FAT/exFAT, some network mounts) only degrades durability -- the file
* is already renamed and fsynced -- so that failure is logged, not
* thrown.
*
* `mode` is applied on the temp file's creation, so it takes effect on
* every write. A plain in-place overwrite of an existing file would
* silently keep the original file's mode instead.
*
* The temp file follows `createTarballCache`'s `.tmp.<pid>.<rand>`
* naming convention for consistency across the sidecar's staged writes.
*/
export async function writeFileAtomicDurable(
path: string,
contents: string,
options: AtomicWriteOptions,
): Promise<void> {
const tmp = `${path}.tmp.${String(process.pid)}.${hexEncode(crypto.getRandomValues(new Uint8Array(8)))}`;
try {
const handle = await open(tmp, "w", options.mode);
try {
await handle.writeFile(contents);
await handle.sync();
} finally {
await handle.close();
}
await rename(tmp, path);
} catch (cause) {
// The write failed and is about to rethrow; unlink the temp so a
// failed write leaves no orphan. Best-effort: the temp may never
// have been created, and a second failure here must not mask the
// original cause.
await unlink(tmp).catch(() => undefined);
throw cause;
}

try {
const dirHandle = await open(dirname(path), "r");
try {
await dirHandle.sync();
} finally {
await dirHandle.close();
}
} catch (err) {
logger.warn`parent-dir fsync failed for ${path}; durability is degraded but the file is renamed and fsynced — ${err instanceof Error ? err.message : String(err)}`;
}
}
53 changes: 0 additions & 53 deletions apps/sidecar/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
// re-reading env, so the boundary stays at the boot edge.

import { AdapterManifest } from "@intx/inference";
import type { GCPolicy, RetentionPolicy } from "@intx/storage-isogit";

const DEFAULT_CACHE_MAX_BYTES = 10 * 1024 * 1024 * 1024;

Expand Down Expand Up @@ -69,55 +68,3 @@ export function readAdapterManifest(): AdapterManifest {
}
return AdapterManifest.assert(parsed);
}

// Write-path GC for the sidecar's deployed-agent repos. The reactor
// commits loose objects every cycle, so these repos accumulate loose
// objects far faster than packs; the loose threshold is the dominant
// trigger here. Retention defaults to tip-only: the sidecar treats the
// repo as the agent's current state, not a long-term archive, so dropping
// commit history keeps the repo small for reactor read/commit latency. An
// operator that needs the history preserved sets
// SIDECAR_AGENT_GC_RETENTION=keep-history.
const DEFAULT_SIDECAR_AGENT_GC_PACK_THRESHOLD = 16;
const DEFAULT_SIDECAR_AGENT_GC_LOOSE_THRESHOLD = 512;
const DEFAULT_SIDECAR_AGENT_GC_WARN_BYTES = 128 * 1024 * 1024;

function readPositiveIntEnv(name: string, fallback: number): number {
const raw = process.env[name];
if (raw === undefined || raw.trim() === "") return fallback;
const n = Number(raw);
if (!Number.isInteger(n) || n <= 0) {
throw new Error(`${name} must be a positive integer; got ${raw}`);
}
return n;
}

function readRetentionEnv(
name: string,
fallback: RetentionPolicy,
): RetentionPolicy {
const raw = process.env[name];
if (raw === undefined || raw.trim() === "") return fallback;
if (raw === "tip-only" || raw === "keep-history") return raw;
throw new Error(
`${name} must be "tip-only" or "keep-history"; got ${JSON.stringify(raw)}`,
);
}

export function readAgentGCPolicy(): GCPolicy {
return {
packThreshold: readPositiveIntEnv(
"SIDECAR_AGENT_GC_PACK_THRESHOLD",
DEFAULT_SIDECAR_AGENT_GC_PACK_THRESHOLD,
),
looseThreshold: readPositiveIntEnv(
"SIDECAR_AGENT_GC_LOOSE_THRESHOLD",
DEFAULT_SIDECAR_AGENT_GC_LOOSE_THRESHOLD,
),
warnBytes: readPositiveIntEnv(
"SIDECAR_AGENT_GC_WARN_BYTES",
DEFAULT_SIDECAR_AGENT_GC_WARN_BYTES,
),
retention: readRetentionEnv("SIDECAR_AGENT_GC_RETENTION", "tip-only"),
};
}
Loading