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
7 changes: 4 additions & 3 deletions packages/gittensory-engine/src/miner/attempt-log.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Driver-level structured attempt log — pure event shapes (#4294). Mirrors `governor-ledger.ts`: fixed vocabulary,
// fail-closed normalization, JSON-round-trip-verified payloads. Persistence (SQLite/JSONL export) is a miner-package
// follow-up; this module is the engine-side contract the invoke layer writes into.
// fail-closed normalization, JSON-round-trip-verified payloads. SQLite persistence + JSONL export live in
// `packages/gittensory-miner/lib/attempt-log.js`, which imports this normalizer from the engine package.

import type { CodingAgentExecutionMode } from "./coding-agent-mode.js";

export const ATTEMPT_LOG_EVENT_TYPES = Object.freeze([
"attempt_started",
"attempt_tool_edit",
"attempt_shadow",
"attempt_succeeded",
"attempt_failed",
Expand Down Expand Up @@ -104,7 +105,7 @@ export function formatAttemptLogJsonl(events: readonly NormalizedAttemptLogEvent
return events.map((event) => JSON.stringify(event)).join("\n");
}

/** In-memory appender for tests and local tooling — production persistence imports the normalizer only. */
/** In-memory appender for tests and local tooling — production persistence uses `gittensory-miner/lib/attempt-log.js`. */
export function createAttemptLogBuffer(): {
append: (event: AttemptLogEvent) => NormalizedAttemptLogEvent;
events: () => readonly NormalizedAttemptLogEvent[];
Expand Down
1 change: 1 addition & 0 deletions packages/gittensory-engine/test/attempt-log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
test("ATTEMPT_LOG_EVENT_TYPES is a fixed vocabulary", () => {
assert.deepEqual([...ATTEMPT_LOG_EVENT_TYPES], [
"attempt_started",
"attempt_tool_edit",
"attempt_shadow",
"attempt_succeeded",
"attempt_failed",
Expand Down
2 changes: 1 addition & 1 deletion packages/gittensory-miner/docs/coding-agent-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ A driver never runs in isolation. The neighborhood it plugs into:
| Execution mode | `coding-agent-mode.ts` | `paused` / `dry_run` / `live` with deny-toward-safety precedence; `codingAgentModeExecutes(mode)` is the single "should this attempt actually spawn?" boolean. A `dry_run` is a pure no-op at the driver boundary. |
| Invocation | `coding-agent-invoke.ts` | `invokeCodingAgentDriver(driver, task, mode?, log?)` — gates on the mode, calls `driver.run`, and streams lifecycle events to an `AttemptLogSink`. |
| Factory | `driver-factory.ts` | `createCodingAgentDriver(options)` resolves a driver by configured name; `resolveConfiguredCodingAgentDriverNames` / `isConfiguredCodingAgentDriver` deny unknown names by default; `runCodingAgentAttempt(options)` is the top-level "resolve + invoke" convenience. |
| Attempt log | `attempt-log.ts` (#4294) | `ATTEMPT_LOG_EVENT_TYPES` + `normalizeAttemptLogEvent` + `createAttemptLogBuffer` + `formatAttemptLogJsonl` — an append-only, JSONL-exportable event trace per attempt, independent of any driver's own transcript. |
| Attempt log | `attempt-log.ts` (#4294) | `ATTEMPT_LOG_EVENT_TYPES` + `normalizeAttemptLogEvent` + `createAttemptLogBuffer` + `formatAttemptLogJsonl` — an append-only, JSONL-exportable event trace per attempt, independent of any driver's own transcript. Durable persistence: `packages/gittensory-miner/lib/attempt-log.js` (sibling SQLite store; imports the engine normalizer). |
| Metering | `attempt-metering.ts` (#4311) | `accumulateAttemptUsage` / `meterAttemptUsage` / `evaluateAttemptBudget` over `AttemptBudgetAxis` (`tokens` / `turns` / `wallClockMs` / `costUsd`). |
| Acceptance criteria | (#4271) | The immutable criteria file at `task.acceptanceCriteriaPath`, written before the driver starts. |
| Worktree isolation | (#4269) | Each attempt's `task.workingDirectory` is a dedicated git worktree; a driver must never edit outside it. |
Expand Down
37 changes: 37 additions & 0 deletions packages/gittensory-miner/lib/attempt-log.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { AttemptLogEvent } from "@jsonbored/gittensory-engine";

export type AttemptLogEntry = {
id: number;
seq: number;
eventType: string;
attemptId: string;
actionClass: string;
mode: string;
reason: string;
payload: Record<string, unknown>;
createdAt: string;
};

export type ReadAttemptLogEventsFilter = {
attemptId?: string | null;
};

export type AttemptLog = {
dbPath: string;
appendAttemptLogEvent(event: AttemptLogEvent): AttemptLogEntry;
readAttemptLogEvents(filter?: ReadAttemptLogEventsFilter): AttemptLogEntry[];
exportAttemptLogJsonl(attemptId: string): string;
close(): void;
};

export function resolveAttemptLogDbPath(env?: Record<string, string | undefined>): string;

export function initAttemptLog(dbPath?: string): AttemptLog;

export function appendAttemptLogEvent(event: AttemptLogEvent): AttemptLogEntry;

export function readAttemptLogEvents(filter?: ReadAttemptLogEventsFilter): AttemptLogEntry[];

export function exportAttemptLogJsonl(attemptId: string): string;

export function closeDefaultAttemptLog(): void;
177 changes: 177 additions & 0 deletions packages/gittensory-miner/lib/attempt-log.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { formatAttemptLogJsonl, normalizeAttemptLogEvent } from "@jsonbored/gittensory-engine";
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";

// Append-only driver attempt log (#4294): a structured, attempt-scoped event trace for every CodingAgentDriver run
// (started, tool/edit, succeeded/failed/aborted). IMMUTABILITY INVARIANT: INSERT + SELECT only — rows are never
// rewritten or removed after append.
//
// Why a sibling store instead of extending event-ledger.js: event-ledger is the general miner-loop audit trail
// (discovered_issue, plan_built, pr_prepared, …) keyed by repo scope with a growing free-form type vocabulary.
// Attempt events are keyed by attempt_id, validated against the engine's fixed ATTEMPT_LOG_EVENT_TYPES, and are
// exported per attempt as JSONL — mixing both into one table would couple unrelated lifecycles and complicate the
// per-attempt dump path. This module mirrors governor-ledger.js: engine holds pure normalization, miner holds SQLite.

const defaultDbFileName = "attempt-log.sqlite3";
let defaultAttemptLog = null;

export function resolveAttemptLogDbPath(env = process.env) {
return resolveLocalStoreDbPath(defaultDbFileName, "GITTENSORY_MINER_ATTEMPT_LOG_DB", env);
}

function normalizeDbPath(dbPath) {
return normalizeLocalStoreDbPath(dbPath, resolveAttemptLogDbPath(), "invalid_attempt_log_db_path");
}

/** Read-filter attempt scope: omitted/nullish → unscoped (all events); otherwise a non-empty attempt id. */
function normalizeReadAttemptIdFilter(attemptId) {
if (attemptId === undefined || attemptId === null) return undefined;
if (typeof attemptId !== "string") throw new Error("invalid_attempt_id");
const trimmed = attemptId.trim();
if (!trimmed) throw new Error("invalid_attempt_id");
return trimmed;
}

/** Export requires an explicit attempt id — JSONL dumps are always per attempt. */
function normalizeRequiredAttemptId(attemptId) {
const normalized = normalizeReadAttemptIdFilter(attemptId);
if (normalized === undefined) throw new Error("invalid_attempt_id");
return normalized;
}

function rowToEntry(row) {
let payload;
try {
payload = JSON.parse(row.payload_json);
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
throw new Error("corrupted_attempt_log_row");
}
} catch {
throw new Error("corrupted_attempt_log_row");
}
return {
id: row.id,
seq: row.seq,
eventType: row.event_type,
attemptId: row.attempt_id,
actionClass: row.action_class,
mode: row.mode,
reason: row.reason,
payload,
createdAt: row.created_at,
};
}

function rowToNormalized(row) {
return {
eventType: row.event_type,
attemptId: row.attempt_id,
actionClass: row.action_class,
mode: row.mode,
reason: row.reason,
payloadJson: row.payload_json,
};
}

/**
* Opens the append-only attempt log, creating the table on first use. `seq` is a monotonically increasing counter
* maintained by this module (next = current MAX(seq) + 1) with a UNIQUE(seq) constraint. Rows read back in seq ASC
* order. (#4294)
*/
export function initAttemptLog(dbPath = resolveAttemptLogDbPath()) {
const resolvedPath = normalizeDbPath(dbPath);
const db = openLocalStoreDb(resolvedPath);
db.exec(`
CREATE TABLE IF NOT EXISTS attempt_log_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
seq INTEGER NOT NULL UNIQUE,
attempt_id TEXT NOT NULL,
event_type TEXT NOT NULL,
action_class TEXT NOT NULL,
mode TEXT NOT NULL,
reason TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL
)
`);
db.exec(
"CREATE INDEX IF NOT EXISTS idx_attempt_log_attempt ON attempt_log_events (attempt_id, seq)",
);

const nextSeqStatement = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS nextSeq FROM attempt_log_events");
const appendStatement = db.prepare(`
INSERT INTO attempt_log_events (
seq, attempt_id, event_type, action_class, mode, reason, payload_json, created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
const getByIdStatement = db.prepare("SELECT * FROM attempt_log_events WHERE id = ?");
const readAllStatement = db.prepare("SELECT * FROM attempt_log_events ORDER BY seq ASC");
const readByAttemptStatement = db.prepare(
"SELECT * FROM attempt_log_events WHERE attempt_id = ? ORDER BY seq ASC",
);

return {
dbPath: resolvedPath,
appendAttemptLogEvent(event) {
const normalized = normalizeAttemptLogEvent(event);
const createdAt = new Date().toISOString();
db.exec("BEGIN IMMEDIATE");
try {
const { nextSeq } = nextSeqStatement.get();
const result = appendStatement.run(
nextSeq,
normalized.attemptId,
normalized.eventType,
normalized.actionClass,
normalized.mode,
normalized.reason,
normalized.payloadJson,
createdAt,
);
const entry = rowToEntry(getByIdStatement.get(Number(result.lastInsertRowid)));
db.exec("COMMIT");
return entry;
} catch (error) {
db.exec("ROLLBACK");
throw error;
}
},
readAttemptLogEvents(filter = {}) {
const attemptId = normalizeReadAttemptIdFilter(filter.attemptId);
const rows =
attemptId === undefined ? readAllStatement.all() : readByAttemptStatement.all(attemptId);
return rows.map(rowToEntry);
},
exportAttemptLogJsonl(attemptId) {
const scopedAttemptId = normalizeRequiredAttemptId(attemptId);
const rows = readByAttemptStatement.all(scopedAttemptId);
return formatAttemptLogJsonl(rows.map(rowToNormalized));
},
close() {
db.close();
},
};
}

function getDefaultAttemptLog() {
defaultAttemptLog ??= initAttemptLog();
return defaultAttemptLog;
}

export function appendAttemptLogEvent(event) {
return getDefaultAttemptLog().appendAttemptLogEvent(event);
}

export function readAttemptLogEvents(filter) {
return getDefaultAttemptLog().readAttemptLogEvents(filter);
}

export function exportAttemptLogJsonl(attemptId) {
return getDefaultAttemptLog().exportAttemptLogJsonl(attemptId);
}

export function closeDefaultAttemptLog() {
if (!defaultAttemptLog) return;
defaultAttemptLog.close();
defaultAttemptLog = null;
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"expected-engine.version"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check lib/version.js && node --check lib/local-store.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/worktree-allocator.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-adjudication.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/calibration-types.js && node --check lib/calibration.js"
"build": "node --check bin/gittensory-miner.js && node --check lib/version.js && node --check lib/local-store.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/worktree-allocator.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-adjudication.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/attempt-log.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/calibration-types.js && node --check lib/calibration.js"
},
"dependencies": {
"@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0"
Expand Down
1 change: 1 addition & 0 deletions test/unit/coding-agent-miner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ describe("attempt log normalization (#4294)", () => {
it("exposes a frozen event vocabulary", () => {
expect([...ATTEMPT_LOG_EVENT_TYPES]).toEqual([
"attempt_started",
"attempt_tool_edit",
"attempt_shadow",
"attempt_succeeded",
"attempt_failed",
Expand Down
Loading