chore: push all design docs, V2 plan specs, and current working state

Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2,
AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code
changes across packages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
AirCoding
2026-06-12 17:12:29 +08:00
parent 8f55c962bb
commit ae44be31d5
364 changed files with 46779 additions and 2812 deletions

View File

@@ -8,12 +8,22 @@ import { Database } from 'bun:sqlite'
* 3. Mark agent.lost
* 4. Preserve workspaces
* 5. Orphan artifact scan → register or quarantine
* 6. FK-off scan (8 invariants, DD §18.3)
* 6. FK-off scan (8 invariants, DD §18.3) — performs REAL reparent/archive
* 7. Workspace GC
* 8. Rebuild queue
*
* INV-5: rebuild from SQLite, not EventBus replay.
*
* Round5 Wf-A rework A.6/A.7:
* - fkChecks now matches DD §18.3 exactly: cross-table constraints across
* tasks / task_attempts / agents / tool_runs / command_runs / workspaces /
* diagnostics / evidence_refs — not the pre-rework blanket sessions parent.
* - Violations trigger actual UPDATE/DELETE SQL: reparent by pointing the
* orphan FK at the most recent valid row of the parent table (DD §18.3
* "re-parents or archives"); if no valid parent exists the row is archived
* (UPDATE status to the appropriate terminal value, falling back to DELETE
* for tables that lack a status column).
*
* @module packages/runtime/src/storage/Recovery
*/
@@ -28,6 +38,8 @@ export interface RecoveryOptions {
artifactRoot: string
dbPath: string
projectRoot: string
/** Optional existing Database instance to reuse (avoids Bun SQLite file locking issues) */
db?: Database
}
export interface OrphanArtifactReport {
@@ -58,6 +70,35 @@ export interface RecoveryReport {
completedAt: ISOTimeString
}
/**
* Single FK-off invariant per DD §18.3.
*
* `nullable` means the FK column permits NULL: when non-null the value must
* reference a real row in `parent_table`; when null the row is never orphan.
*
* `action: 'reparent'` — point the orphan FK at the most recent valid row
* of the parent table. Used when a same-table
* replacement keeps the row meaningful (e.g. an
* orphan agent still belongs to a real task in
* the same session).
* `action: 'archive'` — flag the orphan row as terminal (UPDATE status)
* or DELETE if the table has no status column.
*
* `archive_status` is the terminal status value written for archive actions;
* it must match db-schema-v1.md for the given table. `null` triggers DELETE.
*/
interface FkInvariant {
table: string
fk_column: string
parent_table: string
nullable: boolean
action: 'reparent' | 'archive'
archive_status?: string | null
// SQL fragment to SELECT a fallback parent id; defaults to any row id
// from `parent_table`. Subclasses / future tables can override.
fallback_select?: string
}
export class Recovery {
private artifactRoot: string
private _dbPath: string
@@ -70,15 +111,20 @@ export class Recovery {
this._dbPath = options.dbPath
this.projectRoot = options.projectRoot
this.quarantineDir = join(this.artifactRoot, 'tmp', 'orphans')
this.open_db()
// Use provided DB instance if given, otherwise try to open
if (options.db) {
this.db = options.db
} else {
this.open_db()
}
}
/**
* Open the session database for FK-off scan.
* Open the session database for FK-off scan (read-write so we can repair).
*/
private open_db(): void {
try {
this.db = new Database(this._dbPath, { readonly: true })
this.db = new Database(this._dbPath, { readonly: false })
} catch {
this.db = null
}
@@ -151,7 +197,29 @@ export class Recovery {
/**
* FK-off scan — checks 8 invariants per DD §18.3.
* Returns an OrphanReferenceReport with reparented/archived references.
* Performs real reparent/archive (UPDATE / DELETE), not just report.
*
* The 8 invariants per DD §18.3 l.1325-1332:
* 1. tasks.session_id → sessions.id
* 2. task_attempts.task_id → tasks.id
* 3. agents.task_id → tasks.id (nullable)
* 4. tool_runs.task_id → tasks.id (nullable)
* tool_runs.agent_id → agents.id (nullable)
* 5. command_runs.task_id → tasks.id (nullable)
* command_runs.agent_id → agents.id (nullable)
* command_runs.tool_run_id → tool_runs.id (nullable)
* 6. workspaces.task_id → tasks.id (nullable)
* workspaces.agent_id → agents.id (nullable)
* 7. diagnostics.command_run_id → command_runs.id (nullable)
* diagnostics.artifact_id → artifacts.id (nullable)
* 8. evidence_refs foreign columns (nullable):
* task_id, agent_id, tool_run_id, command_run_id,
* artifact_id, diagnostic_id, message_id
* → corresponding parent tables
*
* Each invariant is modeled as an FkInvariant. Nullable columns are
* skipped entirely when null (they cannot orphan). Non-null orphan values
* are either reparented to the most-recent valid parent row, or archived.
*/
private async scanOrphanReferences(): Promise<OrphanReferenceReport> {
const report: OrphanReferenceReport = {
@@ -161,46 +229,215 @@ export class Recovery {
errors: [],
}
// 8 FK-off invariant checks (DD §18.3):
const fkChecks = [
{ table: 'tasks', fk_column: 'session_id', parent_table: 'sessions' },
{ table: 'messages', fk_column: 'session_id', parent_table: 'sessions' },
{ table: 'task_attempts', fk_column: 'task_id', parent_table: 'tasks' },
{ table: 'agents', fk_column: 'session_id', parent_table: 'sessions' },
{ table: 'tool_runs', fk_column: 'session_id', parent_table: 'sessions' },
{ table: 'command_runs', fk_column: 'session_id', parent_table: 'sessions' },
{ table: 'artifacts', fk_column: 'session_id', parent_table: 'sessions' },
{ table: 'evidence_refs', fk_column: 'session_id', parent_table: 'sessions' },
const fkChecks: FkInvariant[] = [
// 1. tasks.session_id → sessions.id (NOT NULL per db-schema §7)
{
table: 'tasks', fk_column: 'session_id', parent_table: 'sessions',
nullable: false, action: 'archive', archive_status: 'cancelled',
},
// 2. task_attempts.task_id → tasks.id (NOT NULL per db-schema §9)
{
table: 'task_attempts', fk_column: 'task_id', parent_table: 'tasks',
nullable: false, action: 'archive', archive_status: 'cancelled',
},
// 3. agents.task_id → tasks.id (nullable)
{
table: 'agents', fk_column: 'task_id', parent_table: 'tasks',
nullable: true, action: 'reparent',
},
// 4. tool_runs.task_id / agent_id
{
table: 'tool_runs', fk_column: 'task_id', parent_table: 'tasks',
nullable: true, action: 'reparent',
},
{
table: 'tool_runs', fk_column: 'agent_id', parent_table: 'agents',
nullable: true, action: 'reparent',
},
// 5. command_runs.task_id / agent_id / tool_run_id
{
table: 'command_runs', fk_column: 'task_id', parent_table: 'tasks',
nullable: true, action: 'reparent',
},
{
table: 'command_runs', fk_column: 'agent_id', parent_table: 'agents',
nullable: true, action: 'reparent',
},
{
table: 'command_runs', fk_column: 'tool_run_id', parent_table: 'tool_runs',
nullable: true, action: 'reparent',
},
// 6. workspaces.task_id / agent_id
{
table: 'workspaces', fk_column: 'task_id', parent_table: 'tasks',
nullable: true, action: 'reparent',
},
{
table: 'workspaces', fk_column: 'agent_id', parent_table: 'agents',
nullable: true, action: 'reparent',
},
// 7. diagnostics.command_run_id / artifact_id
{
table: 'diagnostics', fk_column: 'command_run_id', parent_table: 'command_runs',
nullable: true, action: 'reparent',
},
{
table: 'diagnostics', fk_column: 'artifact_id', parent_table: 'artifacts',
nullable: true, action: 'reparent',
},
// 8. evidence_refs foreign columns (all nullable)
{
table: 'evidence_refs', fk_column: 'task_id', parent_table: 'tasks',
nullable: true, action: 'reparent',
},
{
table: 'evidence_refs', fk_column: 'agent_id', parent_table: 'agents',
nullable: true, action: 'reparent',
},
{
table: 'evidence_refs', fk_column: 'tool_run_id', parent_table: 'tool_runs',
nullable: true, action: 'reparent',
},
{
table: 'evidence_refs', fk_column: 'command_run_id', parent_table: 'command_runs',
nullable: true, action: 'reparent',
},
{
table: 'evidence_refs', fk_column: 'artifact_id', parent_table: 'artifacts',
nullable: true, action: 'reparent',
},
{
table: 'evidence_refs', fk_column: 'diagnostic_id', parent_table: 'diagnostics',
nullable: true, action: 'reparent',
},
{
table: 'evidence_refs', fk_column: 'message_id', parent_table: 'messages',
nullable: true, action: 'reparent',
},
]
if (!this.db) return report
for (const check of fkChecks) {
try {
if (!this.db) return report;
const stmt = this.db.prepare(
`SELECT t.${check.fk_column} AS orphan_ref, COUNT(*) AS count
FROM ${check.table} t
LEFT JOIN ${check.parent_table} o ON t.${check.fk_column} = o.id
WHERE t.${check.fk_column} IS NOT NULL AND o.id IS NULL
GROUP BY t.${check.fk_column}`
)
const orphans = stmt.all() as Array<{ orphan_ref: string; count: number }>
for (const o of orphans) {
report.totalFound++
// Archive orphans: flag metadata for review
report.archived.push({
table: check.table,
id: o.orphan_ref,
reason: `FK-off: ${check.fk_column}${check.parent_table} (${o.count} rows)`
})
}
await this.runFkCheck(check, report)
} catch (error) {
report.errors.push(`FK check failed for ${check.table}.${check.fk_column}: ${error}`)
report.errors.push(
`FK check failed for ${check.table}.${check.fk_column}: ${error}`,
)
}
}
return report
}
/**
* Execute a single FK-off invariant against the live DB.
* Performs real UPDATE/DELETE; never just records.
*/
private async runFkCheck(
check: FkInvariant,
report: OrphanReferenceReport,
): Promise<void> {
const db = this.db
if (!db) return
// Skip invariants whose table does not exist in this session DB
// (test schemas / fresh sessions may be missing some tables).
if (!this.tableExists(db, check.table)) return
if (!this.tableExists(db, check.parent_table)) return
// Collect orphan rows. For nullable FKs we skip nulls; for non-nullable
// we still allow the scan but rely on the DB schema to enforce NOT NULL.
const whereNull = check.nullable ? `AND t.${check.fk_column} IS NOT NULL` : ''
const orphanStmt = db.prepare(
`SELECT t.id AS row_id, t.${check.fk_column} AS orphan_ref
FROM ${check.table} t
LEFT JOIN ${check.parent_table} o ON t.${check.fk_column} = o.id
WHERE o.id IS NULL ${whereNull}`,
)
const orphans = orphanStmt.all() as Array<{ row_id: string; orphan_ref: string }>
if (orphans.length === 0) return
report.totalFound += orphans.length
// Find a fallback parent id once per check (most-recent valid row).
let fallbackParentId: string | null = null
const needFallback = check.action === 'reparent'
if (needFallback) {
fallbackParentId = this.findRecentValidParent(db, check.parent_table)
}
const updateStmt = db.prepare(
`UPDATE ${check.table} SET ${check.fk_column} = ? WHERE id = ?`,
)
const archiveStatusStmt = db.prepare(
`UPDATE ${check.table} SET status = ? WHERE id = ?`,
)
const deleteStmt = db.prepare(`DELETE FROM ${check.table} WHERE id = ?`)
for (const o of orphans) {
if (check.action === 'reparent' && fallbackParentId) {
updateStmt.run(fallbackParentId, o.row_id)
report.reparented.push({
table: check.table,
id: o.row_id,
new_parent_id: fallbackParentId,
})
continue
}
// No reparent possible, or action is archive. Apply real DB write.
if (check.archive_status) {
archiveStatusStmt.run(check.archive_status, o.row_id)
report.archived.push({
table: check.table,
id: o.row_id,
reason: `FK-off: ${check.fk_column}${check.parent_table}`,
})
} else {
deleteStmt.run(o.row_id)
report.archived.push({
table: check.table,
id: o.row_id,
reason: `FK-off: ${check.fk_column}${check.parent_table} (deleted)`,
})
}
}
}
/**
* Return the most-recent row id in `parent_table`, or null if the table
* is empty. Used as the fallback parent during reparent actions.
*/
private findRecentValidParent(db: Database, parent_table: string): string | null {
if (!this.tableExists(db, parent_table)) return null
try {
const row = db.prepare(
`SELECT id FROM ${parent_table} ORDER BY id DESC LIMIT 1`,
).get() as { id?: string } | null
return row?.id ?? null
} catch {
return null
}
}
/**
* Cheap existence check against sqlite_schema. Avoids throwing on test
* schemas that omit some of the production tables.
*/
private tableExists(db: Database, table: string): boolean {
try {
const row = db.prepare(
`SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?`,
).get(table) as { 1?: number } | null
return row !== null
} catch {
return false
}
}
/**
* PID liveness check for running agents.
* Uses Signal 0 (kill -0) to check process existence.