Skip to content
Closed
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: 3 additions & 3 deletions docs/perf-profiling-corpus.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Optimization **status vocabulary** for a hotspot:

A v1–v3 win is **never** called "confirmed" from current-only coverage. `validatePerfCorpusReport()` enforces this: a `CPU-self-time confirmed` classification is rejected unless the report carries profiler self-time evidence.

## Schema (gjc.perf-corpus/2)
## Schema (gjc.perf-corpus/3)

`PerfCorpusReport` keeps the evidence classes as **separate named fields** per fixture:

Expand All @@ -44,7 +44,7 @@ A v1–v3 win is **never** called "confirmed" from current-only coverage. `valid
- `byteParity: { renderedGolden?, persistedJsonlGolden?, providerPayloadGolden?, materializedSessionGolden? }`
- `memoryBaseline?: { surface, profile, iterations, operations, operationsPerSecond, samples, postTeardown, rssSlopeBytesPerSecond, heapSlopeBytesPerSecond, processTreeBaselineRssBytes, processTreePostTeardownRssBytes, processTreeSampler }`
- `runner: { command, argv, environment, platform, arch, bunVersion?, ci?, profile, durationTargetMs?, memoryIsolation, iterationsTarget, gcExposed, memoryChildGcExposed, memoryChildExecArgv }` pins the actual parent argv, normalized workload controls, isolation, parent GC availability, and the fixed isolated-child runtime flags separately.
- `gitSha` is the full checked-out `HEAD` when Git is available, with `GITHUB_SHA` used only as a fallback; `gitDirty` explicitly marks tracked or untracked worktree changes so local evidence cannot silently masquerade as a clean commit. The runner captures SHA and the complete porcelain worktree fingerprint before and after the workloads and rejects any in-flight source-state change.
- `gitSha` is the full checked-out `HEAD`; unavailable Git provenance fails closed rather than trusting a workflow environment variable. `gitDirty` explicitly marks tracked or untracked worktree changes so local evidence cannot silently masquerade as a clean commit. The runner captures SHA and the complete porcelain worktree fingerprint before and after the workloads and rejects any in-flight source-state change.
- Every detailed sample separates `rssBytes`, `heapUsedBytes`, `heapTotalBytes`, `externalBytes`, `arrayBuffersBytes`, and `activeResourceCount`.

`hotspotClassifications: HotspotClassification[]` carry `{ hotspotId, status, evidenceClass, artifactRefs, notes }`. The current v1–v3 reclassification lives in `V1_V3_RECLASSIFICATION`; no entry is `CPU-self-time confirmed` because no profiler artifacts have been captured yet.
Expand Down Expand Up @@ -98,7 +98,7 @@ Held thresholds (`HELD_PERF_THRESHOLDS`) name candidates that need variance char
## Memory baseline protocol

Detailed memory fixtures cover seven explicit surfaces: CLI startup/configuration, AgentSession-style message/context lifecycle, blob/external buffers, worker generations, Telegram reconnect/queue settlement, TUI render/dispose churn, and shared/native transfer boundaries. The fixtures are synthetic lifecycle proxies: they establish a reproducible allocation and teardown envelope but do not by themselves prove a production leak. A production optimization claim still requires a workload adapter that exercises the implicated owner and a same-host before/after artifact.
The command-line runner executes each memory surface in a fresh Bun subprocess and records `runner.memoryIsolation: "process-per-surface"` so allocator high-water state from one fixture cannot contaminate the next surface's baseline. Programmatic `runPerfCorpusBenchmark()` defaults to in-process fixtures and records `"in-process"` for focused contract tests; pass `{ isolatedMemory: true }` for acceptance-equivalent evidence. Process-tree RSS snapshots exclude the `ps` sampler process and degrade both endpoints to `"unavailable"` when either snapshot fails. The process-tree baseline is captured after GC, followed by another GC that clears sampler allocations before the local baseline and workload begin. Soak workloads use single-iteration batches so approximately 50 ms sampling cannot be hidden behind a large synchronous chunk. Post-teardown return fields remain `null` when GC is unavailable.
The authenticated command-line runner is the only supported execution surface. It executes each memory surface in a fresh Bun subprocess and records `runner.memoryIsolation: "process-per-surface"` so allocator high-water state from one fixture cannot contaminate the next surface's baseline. The benchmark module intentionally exposes no programmatic runner because imported execution cannot satisfy the frozen process-argv contract. Process-tree RSS snapshots exclude the `ps` sampler process and degrade both endpoints to `"unavailable"` when either snapshot fails. The process-tree baseline is captured after GC, followed by another GC that clears sampler allocations before the local baseline and workload begin. Soak workloads use single-iteration batches so approximately 50 ms sampling cannot be hidden behind a large synchronous chunk. Post-teardown return fields remain `null` when GC is unavailable.

Use the `short` profile for deterministic contract and shape checks; its bounded iteration window intentionally reports `null` slopes when less than 250 ms is observed. Use `soak` for repeated sampling and slope characterization. For decision evidence:
The soak default runs each surface for at least one second and samples at approximately 50 ms intervals. `GJC_MEMORY_DURATION_MS` accepts 250–60000 ms and `GJC_MEMORY_ITERATIONS` accepts 1–10000000; record overrides with the artifact.
Expand Down
2 changes: 2 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

- Anthropic cache-control resolution now falls back to `model.cacheRetention` at the provider boundary, preserving configured retention and request-over-model precedence through special dispatch wrappers such as GitLab Duo. A configured `cacheRetention: "none"` can no longer be dropped and replaced by the new automatic Claude-family cache marker.
- Anthropic explicit prompt caching now advances its conversation breakpoint during tool-use loops by marking the latest completed assistant tool-use turn while leaving the newest tool result uncached. Previously it kept refreshing only the original human message until another human turn arrived, pinning proxy cache reads to the static tools/system prefix throughout long agentic runs.
- SQLite-backed authentication storage now finalizes temporary and cached statements and closes its owned database connection, allowing settings directories and WAL files to be removed immediately on Windows.

## [0.12.12] - 2026-08-05

### Fixed
Expand Down
85 changes: 61 additions & 24 deletions packages/ai/src/auth-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4541,25 +4541,44 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
}

#authCredentialsTableExists(): boolean {
const row = this.#db
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'auth_credentials'")
.get() as { present?: number } | undefined;
return row?.present === 1;
const stmt = this.#db.prepare(
"SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'auth_credentials'",
);
try {
const row = stmt.get() as { present?: number } | undefined;
return row?.present === 1;
} finally {
stmt.finalize();
}
}

#readAuthSchemaVersion(): number | null {
const row = this.#db.prepare("SELECT version FROM auth_schema_version WHERE id = 1").get() as
| { version?: number }
| undefined;
return typeof row?.version === "number" ? row.version : null;
const stmt = this.#db.prepare("SELECT version FROM auth_schema_version WHERE id = 1");
try {
const row = stmt.get() as { version?: number } | undefined;
return typeof row?.version === "number" ? row.version : null;
} finally {
stmt.finalize();
}
}

#writeAuthSchemaVersion(version: number): void {
this.#db.prepare("INSERT OR REPLACE INTO auth_schema_version(id, version) VALUES (1, ?)").run(version);
const stmt = this.#db.prepare("INSERT OR REPLACE INTO auth_schema_version(id, version) VALUES (1, ?)");
try {
stmt.run(version);
} finally {
stmt.finalize();
}
}

#inferAuthSchemaVersion(): number {
const cols = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
const stmt = this.#db.prepare("PRAGMA table_info(auth_credentials)");
let cols: Array<{ name?: string }>;
try {
cols = stmt.all() as Array<{ name?: string }>;
} finally {
stmt.finalize();
}
const hasDisabledCause = cols.some(column => column.name === "disabled_cause");
const hasIdentityKey = cols.some(column => column.name === "identity_key");
const hasAccountId = cols.some(column => column.name === "account_id");
Expand Down Expand Up @@ -4607,7 +4626,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {

#migrateAuthSchemaV0ToV1(): void {
const migrate = this.#db.transaction(() => {
const v0Cols = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
const v0ColsStmt = this.#db.prepare("PRAGMA table_info(auth_credentials)");
let v0Cols: Array<{ name?: string }>;
try {
v0Cols = v0ColsStmt.all() as Array<{ name?: string }>;
} finally {
v0ColsStmt.finalize();
}
const hasDisabled = v0Cols.some(col => col.name === "disabled");

this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_v0");
Expand Down Expand Up @@ -4684,17 +4709,25 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
}

#backfillCredentialIdentityKeys(): void {
const rows = this.#db
.prepare(
"SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE identity_key IS NULL ORDER BY id ASC",
)
.all() as AuthRow[];
const selectStmt = this.#db.prepare(
"SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE identity_key IS NULL ORDER BY id ASC",
);
let rows: AuthRow[];
try {
rows = selectStmt.all() as AuthRow[];
} finally {
selectStmt.finalize();
}
if (rows.length === 0) return;

const updateIdentity = this.#db.prepare("UPDATE auth_credentials SET identity_key = ? WHERE id = ?");
for (const row of rows) {
const identityKey = resolveRowCredentialIdentityKey(row.provider, row);
updateIdentity.run(identityKey, row.id);
const updateIdentityStmt = this.#db.prepare("UPDATE auth_credentials SET identity_key = ? WHERE id = ?");
try {
for (const row of rows) {
const identityKey = resolveRowCredentialIdentityKey(row.provider, row);
updateIdentityStmt.run(identityKey, row.id);
}
} finally {
updateIdentityStmt.finalize();
}
}

Expand Down Expand Up @@ -4927,9 +4960,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {

updateAuthCredential(id: number, credential: AuthCredential): void {
try {
const providerRow = this.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?").get(id) as
| { provider?: string }
| undefined;
const providerStmt = this.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?");
let providerRow: { provider?: string } | undefined;
try {
providerRow = providerStmt.get(id) as { provider?: string } | undefined;
} finally {
providerStmt.finalize();
}
const provider = providerRow?.provider ?? "";
const serialized = serializeCredential(provider, credential);
if (!serialized) return;
Expand Down Expand Up @@ -5092,6 +5129,6 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
this.#upsertCacheStmt.finalize();
this.#deleteCachePrefixStmt.finalize();
this.#deleteExpiredCacheStmt.finalize();
this.#db.close();
this.#db.close(true);
}
}
22 changes: 22 additions & 0 deletions packages/ai/test/auth-storage-if-absent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,28 @@ describe("if-absent auth credential writes", () => {
store.close();
}
});
test("SqliteAuthCredentialStore close releases WAL files for immediate directory removal", async () => {
const dbDir = path.join(tempDir, "close-releases-wal");
await fs.mkdir(dbDir);
const store = await SqliteAuthCredentialStore.open(path.join(dbDir, "agent.db"));

try {
const inserted = store.upsertAuthCredentialForProviderIfAbsent("anthropic", oauth("teardown"));
expect(inserted.inserted).toBe(true);
store.setCache("teardown-cache", "value", Math.floor(Date.now() / 1000) + 60);
expect(store.getCache("teardown-cache")).toBe("value");
} finally {
store.close();
}

await fs.rm(dbDir, { recursive: true });
expect(
await fs
.access(dbDir)
.then(() => true)
.catch(() => false),
).toBe(false);
});

test("SqliteAuthCredentialStore returns skipped-invalid without inserting", async () => {
const store = await SqliteAuthCredentialStore.open(path.join(tempDir, "invalid.db"));
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- Slash commands now expand in non-interactive runs. `gjc -p "/init"` previously reached the model as the literal text `/init`, so no command body was injected, no file was written, and the model still answered as if the command had run. Print mode now loads the same bundled and file-based command list interactive mode uses before it prompts.
- `gjc plugin install <name>` now names the marketplaces that offer `<name>` when the npm resolution it falls back to fails, so a plugin name copied out of `gjc plugin discover` no longer dead-ends on a bare `install_failed`.
- A remote multi-select ask now shows what is already selected. The ask tool re-issues one remote request per toggle, but the request carried no selection state, so Telegram kept posting an identical prompt with no sign that option 1 had been picked — the checkbox rendering existed only for durable workflow gates. `AskAnswerRequest` now carries `multi` and the selected option labels, the notification bus publishes them as `selectedOptionIndices` with the `(N selected)` question prefix while keeping the ask tool's own Next/Done control, and pre-numbered options (deep interview) are renumbered once instead of rendering as `1. ☑ 1. …`.
- Repaired Windows memory-release regressions in the v0.12.12 profiling and runtime paths: authenticated perf-corpus execution now recovers the real Windows command line, avoids reused child PIDs, revalidates sealed input bytes across Windows metadata APIs, and documents only the supported canonical runner; SQLite-backed settings and memory-guard claims finalize statements and close owned databases; checkpoint durability tolerates only Windows directory-`fsync` `EPERM`; recovery promotion is failure-atomic; and post-ACK team cutover failures consume the durable retry budget instead of looping ambiguously.

## [0.12.12] - 2026-08-05

Expand Down
47 changes: 27 additions & 20 deletions packages/coding-agent/bench/perf-corpus-rlm-analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ def _read_file_bytes(path: Path, maximum_bytes: int) -> tuple[bytes, os.stat_res
return bytes(raw), before


def _rescan_file_identity(info: os.stat_result) -> tuple[int, ...]:
identity = (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns)
return identity if os.name == "nt" else (*identity, info.st_ctime_ns)


def _canonical_digest(value: Any) -> str:
return _sha256_bytes(
json.dumps(value, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
Expand Down Expand Up @@ -2070,6 +2075,11 @@ def run_analysis(
authenticated_file_stats,
) = _validate_sealed_inputs(input_real, prereg, expected_bindings)
hashes.update(sealed_hashes)
authenticated_file_hashes = {
ATTEMPT_LEDGER_FILENAME: sealed_hashes["attemptLedgerSha256"],
RAW_MANIFEST_FILENAME: sealed_hashes["rawManifestSha256"],
**{filename: binding["sha256"] for filename, binding in raw_bindings.items()},
}
except (EvidenceError, FileNotFoundError) as error:
finding = _finding(
"SEALED_INPUT_INVALID",
Expand Down Expand Up @@ -2113,7 +2123,7 @@ def run_analysis(
entry_info: dict[str, os.stat_result] = {}
for entry in sorted(scanned_entries, key=lambda item: item.name):
try:
info = entry.stat(follow_symlinks=False)
info = (input_real / entry.name).lstat()
scanned_total_size += info.st_size
entry_info[entry.name] = info
except OSError as error:
Expand All @@ -2127,26 +2137,23 @@ def run_analysis(
continue
present_names.add(entry.name)
authenticated_info = authenticated_file_stats.get(entry.name)
if authenticated_info is not None and (
info.st_dev,
info.st_ino,
info.st_size,
info.st_mtime_ns,
info.st_ctime_ns,
) != (
authenticated_info.st_dev,
authenticated_info.st_ino,
authenticated_info.st_size,
authenticated_info.st_mtime_ns,
authenticated_info.st_ctime_ns,
):
global_findings.append(
_finding(
"AUTHENTICATED_INPUT_METADATA_DRIFT",
"PROTOCOL",
f"authenticated input changed after byte capture: {entry.name}",
if authenticated_info is not None:
authenticated_input_changed = _rescan_file_identity(info) != _rescan_file_identity(authenticated_info)
try:
current_raw, _ = _read_file_bytes(input_real / entry.name, bounds["maximumBytesPerFile"])
authenticated_input_changed = authenticated_input_changed or (
_sha256_bytes(current_raw) != authenticated_file_hashes[entry.name]
)
except (EvidenceError, OSError):
authenticated_input_changed = True
if authenticated_input_changed:
global_findings.append(
_finding(
"AUTHENTICATED_INPUT_METADATA_DRIFT",
"PROTOCOL",
f"authenticated input changed after byte capture: {entry.name}",
)
)
)
if entry.name not in expected_names or not entry.name.endswith(".json"):
global_findings.append(
_finding(
Expand Down
Loading
Loading