Files
AgentCompany/src/main/database/agent-permissions.ts
T
ZacharyZhang-NY dbdec86cdd refactor(database): split 5 god modules into domain-scoped files
Five files under `src/main/database/` exceeded 400 LOC by combining
multiple responsibilities. Split along natural fault lines. Zero
behavior change — each original `build*Methods` factory still returns
the same method surface, now via spread of smaller builders.

| Original                          | Split into                          | LOC each       |
|-----------------------------------|-------------------------------------|----------------|
| tasks-crud.ts (640)               | tasks-crud / task-sessions-crud /   | 250 / 252 /    |
|                                   | task-queries                        | 177            |
| agents-hierarchy.ts (487)         | agents-hierarchy / agent-permissions| 350 / 151      |
| company-settings.ts (476)         | company-settings / company-metrics  | 122 / 370      |
| documents.ts (426)                | documents / config-revisions        | 318 / 126      |
| runs.ts (413)                     | runs / run-logs / run-events /      | 245 / 96 /     |
|                                   | cost-entries                        | 82 / 52        |

AppDatabase call sites unchanged:
- db.tasks.saveTask, db.tasks.ensureAgentSession (task-sessions-crud),
  db.tasks.listAgentTasks (task-queries)
- db.agents.getChainOfCommand, db.agents.hasAgentPermission
- db.company.getCompanySettings, db.company.getStandupReport
  (company-metrics)
- db.documentsMod.saveDocument, db.documentsMod.recordConfigRevision
- db.runs.createRun, db.runs.appendRunLog (run-logs),
  db.runs.appendRunEvent (run-events), db.runs.recordCostEntry
  (cost-entries)

`database-base.ts` (449) kept as-is — thin facade where every
pass-through is one line; splitting adds mechanical files without
behavior benefit. Will revisit if it grows further.

Aggregator files (`tasks.ts`, `agents.ts`, `company.ts`) each import
and spread the new sub-builders. No renamed exports.
2026-04-17 16:35:19 -04:00

152 lines
5.4 KiB
TypeScript

import type { DatabaseSync } from "node:sqlite";
import { type ReadAll, type Transaction, nowIso, parseJson, toBoolean } from "./helpers";
/**
* Agent permission, runtime-state, and heartbeat-policy helpers extracted
* from agents-hierarchy.ts. Every method is spread into the final
* `buildAgentMethods` result so AppDatabase call-sites remain unchanged.
*/
export function buildAgentPermissionMethods(
getDb: () => DatabaseSync,
_readAll: ReadAll,
_transaction: Transaction,
) {
return {
getAgentPermissions(agentId: string): Record<string, boolean> {
const row = getDb()
.prepare("SELECT permissions_json FROM agents WHERE id = ?")
.get(agentId) as { permissions_json: string } | undefined;
return parseJson<Record<string, boolean>>(row?.permissions_json, {});
},
setAgentPermission(
agentId: string,
companyId: string,
permissionKey: string,
enabled: boolean,
): void {
const row = getDb()
.prepare("SELECT permissions_json FROM agents WHERE id = ?")
.get(agentId) as { permissions_json: string } | undefined;
const current = parseJson<Record<string, boolean>>(row?.permissions_json, {});
if (enabled) {
current[permissionKey] = true;
} else {
delete current[permissionKey];
}
const ts = nowIso();
getDb()
.prepare(
"UPDATE agents SET permissions_json = ?, updated_at = ? WHERE id = ? AND company_id = ?",
)
.run(JSON.stringify(current), ts, agentId, companyId);
},
hasAgentPermission(agentId: string, permissionKey: string): boolean {
const row = getDb()
.prepare("SELECT permissions_json FROM agents WHERE id = ?")
.get(agentId) as { permissions_json: string } | undefined;
const perms = parseJson<Record<string, boolean>>(row?.permissions_json, {});
return perms[permissionKey] === true;
},
getRuntimeState(agentId: string): Record<string, unknown> {
const row = getDb()
.prepare("SELECT runtime_state_json FROM agents WHERE id = ?")
.get(agentId) as { runtime_state_json: string | null } | undefined;
return parseJson<Record<string, unknown>>(row?.runtime_state_json, {});
},
setRuntimeState(
agentId: string,
companyId: string,
state: Record<string, unknown>,
): void {
const ts = nowIso();
getDb()
.prepare(
"UPDATE agents SET runtime_state_json = ?, updated_at = ? WHERE id = ? AND company_id = ?",
)
.run(JSON.stringify(state), ts, agentId, companyId);
},
getHeartbeatPolicy(agentId: string): {
enabled: boolean;
intervalSec: number;
cooldownSec: number;
maxConcurrentRuns: number;
wakeOnDemand: boolean;
} {
const row = getDb()
.prepare(
"SELECT heartbeat_enabled, heartbeat_interval_sec, heartbeat_policy_json FROM agents WHERE id = ?",
)
.get(agentId) as
| {
heartbeat_enabled: number;
heartbeat_interval_sec: number;
heartbeat_policy_json: string | null;
}
| undefined;
if (!row) throw new Error(`Agent ${agentId} not found.`);
const policy = parseJson<Record<string, unknown>>(row.heartbeat_policy_json, {});
return {
enabled: toBoolean(row.heartbeat_enabled),
intervalSec: row.heartbeat_interval_sec ?? 120,
cooldownSec: typeof policy.cooldownSec === "number" ? policy.cooldownSec : 10,
maxConcurrentRuns:
typeof policy.maxConcurrentRuns === "number" ? policy.maxConcurrentRuns : 1,
wakeOnDemand: policy.wakeOnDemand !== false,
};
},
setHeartbeatPolicy(
agentId: string,
companyId: string,
policy: {
enabled?: boolean;
intervalSec?: number;
cooldownSec?: number;
maxConcurrentRuns?: number;
wakeOnDemand?: boolean;
},
): void {
const row = getDb()
.prepare(
"SELECT heartbeat_enabled, heartbeat_interval_sec, heartbeat_policy_json FROM agents WHERE id = ?",
)
.get(agentId) as
| {
heartbeat_enabled: number;
heartbeat_interval_sec: number;
heartbeat_policy_json: string | null;
}
| undefined;
if (!row) throw new Error(`Agent ${agentId} not found.`);
const current = parseJson<Record<string, unknown>>(row.heartbeat_policy_json, {});
const merged = {
enabled: policy.enabled ?? toBoolean(row.heartbeat_enabled),
intervalSec: policy.intervalSec ?? row.heartbeat_interval_sec ?? 120,
cooldownSec:
policy.cooldownSec ??
(typeof current.cooldownSec === "number" ? current.cooldownSec : 10),
maxConcurrentRuns:
policy.maxConcurrentRuns ??
(typeof current.maxConcurrentRuns === "number" ? current.maxConcurrentRuns : 1),
wakeOnDemand: policy.wakeOnDemand ?? (current.wakeOnDemand !== false),
};
const policyJson = JSON.stringify({
cooldownSec: merged.cooldownSec,
maxConcurrentRuns: merged.maxConcurrentRuns,
wakeOnDemand: merged.wakeOnDemand,
});
const ts = nowIso();
getDb()
.prepare(
"UPDATE agents SET heartbeat_enabled = ?, heartbeat_interval_sec = ?, heartbeat_policy_json = ?, updated_at = ? WHERE id = ? AND company_id = ?",
)
.run(merged.enabled ? 1 : 0, merged.intervalSec, policyJson, ts, agentId, companyId);
},
};
}