holo-codex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/README.zh-CN.md +215 -0
- package/SECURITY.md +39 -0
- package/assets/brand/README.md +35 -0
- package/assets/brand/holo-codex-icon.svg +28 -0
- package/assets/brand/holo-codex-lockup.svg +49 -0
- package/assets/brand/holo-codex-mark.svg +33 -0
- package/assets/brand/holo-codex-plugin-card.png +0 -0
- package/assets/brand/holo-codex-plugin-card.svg +81 -0
- package/assets/brand/holo-codex-readme-hero.png +0 -0
- package/assets/brand/holo-codex-readme-hero.svg +140 -0
- package/assets/brand/holo-codex-social-preview.png +0 -0
- package/assets/brand/holo-codex-social-preview.svg +130 -0
- package/assets/brand/holo-codex-wordmark-options.svg +52 -0
- package/docs/checklists/agent-loop-first-delivery-audit.md +129 -0
- package/docs/examples/generic-loop-repo-hygiene.md +168 -0
- package/docs/install.md +190 -0
- package/docs/local-release-readiness.md +206 -0
- package/docs/release-checklist.md +144 -0
- package/docs/self-bootstrap.md +150 -0
- package/docs/trust-and-safety.md +45 -0
- package/package.json +83 -0
- package/plugins/autonomous-pr-loop/.codex-plugin/plugin.json +17 -0
- package/plugins/autonomous-pr-loop/.mcp.json +13 -0
- package/plugins/autonomous-pr-loop/bin/agent-loop.mjs +31 -0
- package/plugins/autonomous-pr-loop/core/artifacts.ts +164 -0
- package/plugins/autonomous-pr-loop/core/autonomy-policy.ts +206 -0
- package/plugins/autonomous-pr-loop/core/ci.ts +131 -0
- package/plugins/autonomous-pr-loop/core/cli-i18n.ts +123 -0
- package/plugins/autonomous-pr-loop/core/cli.ts +1413 -0
- package/plugins/autonomous-pr-loop/core/command-runner.ts +446 -0
- package/plugins/autonomous-pr-loop/core/command.ts +47 -0
- package/plugins/autonomous-pr-loop/core/config-editor.ts +140 -0
- package/plugins/autonomous-pr-loop/core/config.ts +293 -0
- package/plugins/autonomous-pr-loop/core/controller-host.ts +19 -0
- package/plugins/autonomous-pr-loop/core/dashboard-server.ts +536 -0
- package/plugins/autonomous-pr-loop/core/delivery-work-item.ts +217 -0
- package/plugins/autonomous-pr-loop/core/doctor.ts +335 -0
- package/plugins/autonomous-pr-loop/core/errors.ts +82 -0
- package/plugins/autonomous-pr-loop/core/gate-recovery.ts +176 -0
- package/plugins/autonomous-pr-loop/core/gates.ts +26 -0
- package/plugins/autonomous-pr-loop/core/generic-lifecycle.ts +399 -0
- package/plugins/autonomous-pr-loop/core/git.ts +213 -0
- package/plugins/autonomous-pr-loop/core/github.ts +269 -0
- package/plugins/autonomous-pr-loop/core/gitnexus.ts +90 -0
- package/plugins/autonomous-pr-loop/core/happy.ts +42 -0
- package/plugins/autonomous-pr-loop/core/hook-capture.ts +115 -0
- package/plugins/autonomous-pr-loop/core/hook-events.ts +22 -0
- package/plugins/autonomous-pr-loop/core/hook-installation.ts +85 -0
- package/plugins/autonomous-pr-loop/core/hook-observer.ts +84 -0
- package/plugins/autonomous-pr-loop/core/hook-policy.ts +423 -0
- package/plugins/autonomous-pr-loop/core/hook-router.ts +452 -0
- package/plugins/autonomous-pr-loop/core/index.ts +32 -0
- package/plugins/autonomous-pr-loop/core/local-install.ts +778 -0
- package/plugins/autonomous-pr-loop/core/locale.ts +60 -0
- package/plugins/autonomous-pr-loop/core/loop-shapes.ts +190 -0
- package/plugins/autonomous-pr-loop/core/mcp-controller.ts +1479 -0
- package/plugins/autonomous-pr-loop/core/notification-feed.ts +263 -0
- package/plugins/autonomous-pr-loop/core/plan-parser.ts +206 -0
- package/plugins/autonomous-pr-loop/core/plugin-paths.ts +32 -0
- package/plugins/autonomous-pr-loop/core/policy.ts +65 -0
- package/plugins/autonomous-pr-loop/core/pr-lifecycle.ts +464 -0
- package/plugins/autonomous-pr-loop/core/pr-selector.ts +284 -0
- package/plugins/autonomous-pr-loop/core/profiles.ts +439 -0
- package/plugins/autonomous-pr-loop/core/redaction.ts +17 -0
- package/plugins/autonomous-pr-loop/core/repo-root.ts +22 -0
- package/plugins/autonomous-pr-loop/core/review-comments.ts +77 -0
- package/plugins/autonomous-pr-loop/core/scope-guard.ts +179 -0
- package/plugins/autonomous-pr-loop/core/state-machine.ts +828 -0
- package/plugins/autonomous-pr-loop/core/state-types.ts +130 -0
- package/plugins/autonomous-pr-loop/core/storage.ts +2527 -0
- package/plugins/autonomous-pr-loop/core/types.ts +567 -0
- package/plugins/autonomous-pr-loop/core/worker-events.ts +412 -0
- package/plugins/autonomous-pr-loop/core/worker-policy.ts +72 -0
- package/plugins/autonomous-pr-loop/core/worker-prompts.ts +182 -0
- package/plugins/autonomous-pr-loop/core/worker.ts +809 -0
- package/plugins/autonomous-pr-loop/core/workflow-board.ts +1515 -0
- package/plugins/autonomous-pr-loop/hooks/dist/permission-request.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/post-compact.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/post-tool-use.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/pre-compact.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/pre-tool-use.js +3460 -0
- package/plugins/autonomous-pr-loop/hooks/dist/session-start.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/stop.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/user-prompt-submit.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/hooks.json +106 -0
- package/plugins/autonomous-pr-loop/hooks/observe-runner.ts +25 -0
- package/plugins/autonomous-pr-loop/hooks/permission-request.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/post-compact.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/post-tool-use.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/pre-compact.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/pre-tool-use.ts +44 -0
- package/plugins/autonomous-pr-loop/hooks/session-start.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/stop.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/user-prompt-submit.ts +4 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/index.ts +87 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/tools.ts +205 -0
- package/plugins/autonomous-pr-loop/package.json +9 -0
- package/plugins/autonomous-pr-loop/schemas/config.schema.json +74 -0
- package/plugins/autonomous-pr-loop/schemas/marketplace.schema.json +46 -0
- package/plugins/autonomous-pr-loop/schemas/plugin.schema.json +32 -0
- package/plugins/autonomous-pr-loop/schemas/state.schema.json +19 -0
- package/plugins/autonomous-pr-loop/schemas/worker-event.schema.json +19 -0
- package/plugins/autonomous-pr-loop/schemas/worker-result.schema.json +58 -0
- package/plugins/autonomous-pr-loop/scripts/agent-loop.ts +44 -0
- package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/SKILL.md +26 -0
- package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/agents/openai.yaml +6 -0
- package/plugins/autonomous-pr-loop/ui/index.html +26 -0
- package/plugins/autonomous-pr-loop/ui/public/favicon.svg +7 -0
- package/plugins/autonomous-pr-loop/ui/src/api.ts +639 -0
- package/plugins/autonomous-pr-loop/ui/src/app.tsx +238 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ActivityBadge.tsx +31 -0
- package/plugins/autonomous-pr-loop/ui/src/components/BrandMark.tsx +36 -0
- package/plugins/autonomous-pr-loop/ui/src/components/Collapsible.tsx +6 -0
- package/plugins/autonomous-pr-loop/ui/src/components/CommandPreview.tsx +15 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ConfigEditor.tsx +389 -0
- package/plugins/autonomous-pr-loop/ui/src/components/EmptyState.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ErrorState.tsx +12 -0
- package/plugins/autonomous-pr-loop/ui/src/components/List.tsx +7 -0
- package/plugins/autonomous-pr-loop/ui/src/components/MetricRow.tsx +6 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ResponsiveTable.tsx +65 -0
- package/plugins/autonomous-pr-loop/ui/src/components/RiskBadge.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/components/StatusBadge.tsx +29 -0
- package/plugins/autonomous-pr-loop/ui/src/components/TopMetric.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/fixtures.ts +1152 -0
- package/plugins/autonomous-pr-loop/ui/src/i18n.ts +1105 -0
- package/plugins/autonomous-pr-loop/ui/src/main.tsx +14 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenter.tsx +470 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenterParts.tsx +276 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/agent-timeline/AgentTimelineView.tsx +73 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/artifact-viewer/ArtifactViewer.tsx +44 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/dry-run-preview/DryRunPreview.tsx +66 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/event-ledger/EventLedger.tsx +17 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/gate-center/GateCenter.tsx +34 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/MissionControl.tsx +104 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/WorkflowBoard.tsx +577 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/notifications/NotificationsView.tsx +30 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/plan-navigator/PlanNavigator.tsx +19 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/policy-config/PolicyConfig.tsx +22 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/pr-inbox/PrInbox.tsx +26 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/recovery-center/RecoveryCenter.tsx +125 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/scope-guard/ScopeGuard.tsx +16 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/worker-runs/WorkerRuns.tsx +39 -0
- package/plugins/autonomous-pr-loop/ui/src/styles.css +2673 -0
- package/plugins/autonomous-pr-loop/ui/src/theme.ts +57 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,2462 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
// plugins/autonomous-pr-loop/hooks/observe-runner.ts
|
|
4
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
5
|
+
|
|
6
|
+
// plugins/autonomous-pr-loop/core/hook-observer.ts
|
|
7
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
// plugins/autonomous-pr-loop/core/config.ts
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
// plugins/autonomous-pr-loop/core/errors.ts
|
|
13
|
+
var AgentLoopError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
details;
|
|
16
|
+
exitCode;
|
|
17
|
+
constructor(code, message, options = {}) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "AgentLoopError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.details = options.details;
|
|
22
|
+
this.exitCode = options.exitCode ?? (isGateCode(code) ? 2 : 1);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function isGateCode(code) {
|
|
26
|
+
return code === "needs_repo_init" || code === "unsupported_remote" || code === "needs_secret_or_login" || code === "policy_violation" || code === "ambiguous_next_pr" || code === "dirty_unowned_worktree" || code === "required_tool_unavailable" || code === "ci_required_checks_missing" || code === "ci_pending_timeout" || code === "merge_requires_confirmation" || code === "github_transient_failure" || code === "gitnexus_check_failed" || code === "github_resource_not_found" || code === "worker_failed" || code === "worker_output_invalid" || code === "review_out_of_scope" || code === "worker_timeout" || code === "worker_already_running" || code === "generic_goal_needs_confirmation" || code === "generic_human_gate" || code === "generic_scope_change_requested";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// plugins/autonomous-pr-loop/core/locale.ts
|
|
30
|
+
var DEFAULT_LOCALE = "zh-CN";
|
|
31
|
+
|
|
32
|
+
// plugins/autonomous-pr-loop/core/profiles.ts
|
|
33
|
+
var DEFAULT_LOOP_SHAPE_ID = "pr-loop";
|
|
34
|
+
var DEFAULT_WORKFLOW_PROFILE_ID = "default_pr_loop";
|
|
35
|
+
var DEFAULT_ROLE_PROFILE_ID = "default_pr_roles";
|
|
36
|
+
|
|
37
|
+
// plugins/autonomous-pr-loop/core/config.ts
|
|
38
|
+
var CONFIG_DIR = ".agent-loop";
|
|
39
|
+
var DEFAULT_PROTECTED_PATHS = [
|
|
40
|
+
".git/**",
|
|
41
|
+
".agent-loop/**",
|
|
42
|
+
".claude/**",
|
|
43
|
+
"AGENTS.md",
|
|
44
|
+
"CLAUDE.md",
|
|
45
|
+
".env*",
|
|
46
|
+
"**/*secret*"
|
|
47
|
+
];
|
|
48
|
+
function statePath(repoRoot) {
|
|
49
|
+
return join(repoRoot, CONFIG_DIR, "state.sqlite");
|
|
50
|
+
}
|
|
51
|
+
function withConfigDefaults(input) {
|
|
52
|
+
const mergeMode = input.mergeMode ?? (input.allowAutoMerge ? "conditional" : "manual");
|
|
53
|
+
return {
|
|
54
|
+
repoId: input.repoId,
|
|
55
|
+
locale: input.locale ?? DEFAULT_LOCALE,
|
|
56
|
+
loopShape: input.loopShape ?? DEFAULT_LOOP_SHAPE_ID,
|
|
57
|
+
workflowProfile: input.workflowProfile ?? DEFAULT_WORKFLOW_PROFILE_ID,
|
|
58
|
+
roleProfile: input.roleProfile ?? DEFAULT_ROLE_PROFILE_ID,
|
|
59
|
+
baseBranch: input.baseBranch ?? "main",
|
|
60
|
+
branchPrefix: input.branchPrefix ?? "codex/",
|
|
61
|
+
plansDir: input.plansDir ?? "docs/plans",
|
|
62
|
+
...input.lintCommand ? { lintCommand: input.lintCommand } : {},
|
|
63
|
+
...input.testCommand ? { testCommand: input.testCommand } : {},
|
|
64
|
+
...input.gitnexusRepo ? { gitnexusRepo: input.gitnexusRepo } : {},
|
|
65
|
+
gitnexusRequired: input.gitnexusRequired ?? true,
|
|
66
|
+
requiredChecks: input.requiredChecks ?? [],
|
|
67
|
+
requireReviewApproval: input.requireReviewApproval ?? true,
|
|
68
|
+
autonomyMode: input.autonomyMode ?? "autonomous_until_gate",
|
|
69
|
+
mergeMode,
|
|
70
|
+
notifyMode: input.notifyMode ?? "important_only",
|
|
71
|
+
reviewHandling: input.reviewHandling ?? "fix_scoped_and_carry_forward",
|
|
72
|
+
...input.carryoverTarget ? { carryoverTarget: input.carryoverTarget } : {},
|
|
73
|
+
allowAutoMerge: mergeMode === "conditional",
|
|
74
|
+
maxReviewFixRounds: input.maxReviewFixRounds ?? 3,
|
|
75
|
+
maxTestFixRounds: input.maxTestFixRounds ?? 2,
|
|
76
|
+
maxCiReruns: input.maxCiReruns ?? 1,
|
|
77
|
+
commandTimeoutMs: input.commandTimeoutMs ?? 6e5,
|
|
78
|
+
commandOutputLimitBytes: input.commandOutputLimitBytes ?? 65536,
|
|
79
|
+
githubRetryMaxAttempts: input.githubRetryMaxAttempts ?? 3,
|
|
80
|
+
githubRetryBaseDelayMs: input.githubRetryBaseDelayMs ?? 1e3,
|
|
81
|
+
reviewCiPollIntervalMs: input.reviewCiPollIntervalMs ?? 3e4,
|
|
82
|
+
reviewCiMaxWaitMs: input.reviewCiMaxWaitMs ?? 18e5,
|
|
83
|
+
workerBackend: input.workerBackend ?? "codex-exec",
|
|
84
|
+
workerTimeoutMs: input.workerTimeoutMs ?? 18e5,
|
|
85
|
+
workerMaxRetries: input.workerMaxRetries ?? 1,
|
|
86
|
+
workerEphemeral: input.workerEphemeral ?? false,
|
|
87
|
+
protectedPaths: input.protectedPaths ?? DEFAULT_PROTECTED_PATHS,
|
|
88
|
+
...input.dashboard ? { dashboard: input.dashboard } : {}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function isRecord(value) {
|
|
92
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// plugins/autonomous-pr-loop/core/hook-events.ts
|
|
96
|
+
var CODEX_HOOK_EVENTS = [
|
|
97
|
+
"PreToolUse",
|
|
98
|
+
"PostToolUse",
|
|
99
|
+
"UserPromptSubmit",
|
|
100
|
+
"Stop",
|
|
101
|
+
"SessionStart",
|
|
102
|
+
"PreCompact",
|
|
103
|
+
"PostCompact",
|
|
104
|
+
"PermissionRequest"
|
|
105
|
+
];
|
|
106
|
+
var OBSERVE_ONLY_HOOK_EVENTS = CODEX_HOOK_EVENTS.filter((event) => event !== "PreToolUse");
|
|
107
|
+
function hookEventKind(event) {
|
|
108
|
+
return `hook_${event.replaceAll(/([a-z])([A-Z])/g, "$1_$2").toLowerCase()}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// plugins/autonomous-pr-loop/core/hook-router.ts
|
|
112
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
113
|
+
import { execFileSync } from "node:child_process";
|
|
114
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
115
|
+
import { homedir } from "node:os";
|
|
116
|
+
import { dirname, isAbsolute, join as join2, resolve } from "node:path";
|
|
117
|
+
function hookRegistryPath(codexHome = codexHomePath()) {
|
|
118
|
+
return join2(codexHome, "agent-loop", "hook-bindings.json");
|
|
119
|
+
}
|
|
120
|
+
function hookRegistryLockPath(codexHome = codexHomePath()) {
|
|
121
|
+
return `${hookRegistryPath(codexHome)}.lock`;
|
|
122
|
+
}
|
|
123
|
+
function codexHomePath() {
|
|
124
|
+
return process.env.CODEX_HOME ?? join2(homedir(), ".codex");
|
|
125
|
+
}
|
|
126
|
+
function resolveHookRoute(payload, options = {}) {
|
|
127
|
+
const context = hookContextFromPayload(payload, options.legacyRepoRoot);
|
|
128
|
+
let registry;
|
|
129
|
+
try {
|
|
130
|
+
registry = readRegistry(options.codexHome ?? codexHomePath());
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return { status: "route_error", context, reason: error instanceof Error ? error.message : String(error) };
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const active = registry.bindings.filter((binding) => binding.status === "active");
|
|
136
|
+
const worktreeMatches = active.filter((binding) => bindingMatchesContext(binding, context));
|
|
137
|
+
const contextSessionHash = context.sessionId ? sha256(context.sessionId) : void 0;
|
|
138
|
+
const sessionMatches = context.sessionId ? worktreeMatches.filter((binding) => binding.sessionIdHash === contextSessionHash) : [];
|
|
139
|
+
const candidates = sessionMatches.length > 0 ? sessionMatches : worktreeMatches.filter((binding) => binding.sessionIdHash === void 0);
|
|
140
|
+
if (candidates.length === 1) {
|
|
141
|
+
const binding = touchBinding(candidates[0], context, options.codexHome);
|
|
142
|
+
if (contextSessionHash && binding.sessionIdHash !== void 0 && binding.sessionIdHash !== contextSessionHash) {
|
|
143
|
+
return { status: "no_match", context, reason: "Hook binding was claimed by another Codex session.", worktreeBinding: true };
|
|
144
|
+
}
|
|
145
|
+
return { status: "matched", binding, context, legacy: false };
|
|
146
|
+
}
|
|
147
|
+
if (candidates.length > 1) {
|
|
148
|
+
return { status: "ambiguous", context, bindings: candidates, reason: "Multiple hook bindings match this Codex session context." };
|
|
149
|
+
}
|
|
150
|
+
if (worktreeMatches.length > 0) {
|
|
151
|
+
return { status: "no_match", context, reason: "Active hook bindings exist for this worktree, but none match this Codex session.", worktreeBinding: true };
|
|
152
|
+
}
|
|
153
|
+
const legacy = legacyRoute(options.legacyRepoRoot, context);
|
|
154
|
+
if (legacy) {
|
|
155
|
+
return { status: "matched", binding: legacy, context, legacy: true };
|
|
156
|
+
}
|
|
157
|
+
return { status: "no_match", context, reason: "No active agent-loop hook binding matches this Codex session context." };
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { status: "route_error", context, reason: error instanceof Error ? error.message : String(error) };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function hookContextFromPayload(payload, fallbackCwd = process.cwd()) {
|
|
163
|
+
const record = isRecord(payload) ? payload : {};
|
|
164
|
+
return resolveHookContext({
|
|
165
|
+
cwd: stringValue(record.cwd) ?? fallbackCwd,
|
|
166
|
+
sessionId: stringValue(record.session_id) ?? stringValue(record.sessionId),
|
|
167
|
+
turnId: stringValue(record.turn_id) ?? stringValue(record.turnId),
|
|
168
|
+
transcriptPath: stringValue(record.transcript_path) ?? stringValue(record.transcriptPath)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function resolveHookContext(input) {
|
|
172
|
+
const cwd = canonicalPath(input.cwd);
|
|
173
|
+
const worktreeRoot = gitOutput(["rev-parse", "--show-toplevel"], cwd);
|
|
174
|
+
const commonDir = gitOutput(["rev-parse", "--git-common-dir"], cwd);
|
|
175
|
+
const branch = gitOutput(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
176
|
+
const commonPath = commonDir ? canonicalPath(isAbsolute(commonDir) ? commonDir : join2(cwd, commonDir)) : void 0;
|
|
177
|
+
return {
|
|
178
|
+
cwd,
|
|
179
|
+
worktreeRoot: worktreeRoot ? canonicalPath(worktreeRoot) : cwd,
|
|
180
|
+
...commonPath ? { gitCommonDir: commonPath } : {},
|
|
181
|
+
...branch && branch !== "HEAD" ? { branch } : {},
|
|
182
|
+
...input.sessionId ? { sessionId: input.sessionId } : {},
|
|
183
|
+
...input.turnId ? { turnId: input.turnId } : {},
|
|
184
|
+
...input.transcriptPath ? { transcriptPathSha256: sha256(input.transcriptPath) } : {}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function readRegistry(codexHome) {
|
|
188
|
+
const path = hookRegistryPath(codexHome);
|
|
189
|
+
if (!existsSync(path)) {
|
|
190
|
+
return { version: 1, bindings: [] };
|
|
191
|
+
}
|
|
192
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
193
|
+
if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.bindings)) {
|
|
194
|
+
throw new Error(`Invalid hook binding registry: expected { version: 1, bindings: [...] } in ${path}`);
|
|
195
|
+
}
|
|
196
|
+
const bindings = parsed.bindings.map(parseBinding);
|
|
197
|
+
const invalid = bindings.findIndex((binding) => binding === void 0);
|
|
198
|
+
if (invalid >= 0) {
|
|
199
|
+
throw new Error(`Invalid hook binding registry: invalid binding at index ${invalid} in ${path}`);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
version: 1,
|
|
203
|
+
bindings: bindings.filter((binding) => binding !== void 0)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function writeRegistry(registry, codexHome) {
|
|
207
|
+
const path = hookRegistryPath(codexHome);
|
|
208
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
209
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
210
|
+
writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}
|
|
211
|
+
`, { mode: 384 });
|
|
212
|
+
renameSync(tmp, path);
|
|
213
|
+
}
|
|
214
|
+
function parseBinding(value) {
|
|
215
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.repoRoot !== "string" || typeof value.worktreeRoot !== "string") {
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
const status = value.status === "stale" || value.status === "disabled" ? value.status : "active";
|
|
219
|
+
if (typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") {
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
id: value.id,
|
|
224
|
+
repoRoot: value.repoRoot,
|
|
225
|
+
worktreeRoot: value.worktreeRoot,
|
|
226
|
+
...typeof value.gitCommonDir === "string" ? { gitCommonDir: value.gitCommonDir } : {},
|
|
227
|
+
...typeof value.branch === "string" ? { branch: value.branch } : {},
|
|
228
|
+
...typeof value.runId === "string" ? { runId: value.runId } : {},
|
|
229
|
+
...typeof value.sessionIdHash === "string" ? { sessionIdHash: value.sessionIdHash } : typeof value.sessionId === "string" ? { sessionIdHash: sha256(value.sessionId) } : {},
|
|
230
|
+
...typeof value.transcriptPathSha256 === "string" ? { transcriptPathSha256: value.transcriptPathSha256 } : {},
|
|
231
|
+
status,
|
|
232
|
+
createdAt: value.createdAt,
|
|
233
|
+
updatedAt: value.updatedAt,
|
|
234
|
+
...typeof value.lastSeenAt === "string" ? { lastSeenAt: value.lastSeenAt } : {}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function touchBinding(binding, context, codexHome = codexHomePath()) {
|
|
238
|
+
return withRegistryLock(codexHome, () => {
|
|
239
|
+
const registry = readRegistry(codexHome);
|
|
240
|
+
const current = registry.bindings.find((item) => item.id === binding.id) ?? binding;
|
|
241
|
+
const contextSessionHash = context.sessionId ? sha256(context.sessionId) : void 0;
|
|
242
|
+
if (current.sessionIdHash !== void 0 && contextSessionHash !== void 0 && current.sessionIdHash !== contextSessionHash) {
|
|
243
|
+
return current;
|
|
244
|
+
}
|
|
245
|
+
const nowMs = Date.now();
|
|
246
|
+
const shouldClaimSession = current.sessionIdHash === void 0 && contextSessionHash !== void 0;
|
|
247
|
+
const shouldClaimTranscript = current.transcriptPathSha256 === void 0 && context.transcriptPathSha256 !== void 0;
|
|
248
|
+
const lastSeenAtMs = current.lastSeenAt ? Date.parse(current.lastSeenAt) : 0;
|
|
249
|
+
const shouldRefreshLastSeen = !Number.isFinite(lastSeenAtMs) || nowMs - lastSeenAtMs > TOUCH_REFRESH_MS;
|
|
250
|
+
if (!shouldClaimSession && !shouldClaimTranscript && !shouldRefreshLastSeen) {
|
|
251
|
+
return current;
|
|
252
|
+
}
|
|
253
|
+
const now2 = new Date(nowMs).toISOString();
|
|
254
|
+
const updated = {
|
|
255
|
+
...current,
|
|
256
|
+
...shouldClaimSession ? { sessionIdHash: contextSessionHash } : {},
|
|
257
|
+
...shouldClaimTranscript ? { transcriptPathSha256: context.transcriptPathSha256 } : {},
|
|
258
|
+
lastSeenAt: now2,
|
|
259
|
+
updatedAt: now2
|
|
260
|
+
};
|
|
261
|
+
registry.bindings = registry.bindings.map((item) => item.id === current.id ? updated : item);
|
|
262
|
+
writeRegistry(registry, codexHome);
|
|
263
|
+
return updated;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function legacyRoute(legacyRepoRoot, context) {
|
|
267
|
+
if (!legacyRepoRoot) return void 0;
|
|
268
|
+
const legacyContext = resolveHookContext({ cwd: legacyRepoRoot });
|
|
269
|
+
if (legacyContext.worktreeRoot !== context.worktreeRoot) {
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
273
|
+
return {
|
|
274
|
+
id: `legacy:${sha256(legacyContext.worktreeRoot).slice(0, 16)}`,
|
|
275
|
+
repoRoot: canonicalPath(legacyRepoRoot),
|
|
276
|
+
worktreeRoot: legacyContext.worktreeRoot,
|
|
277
|
+
...legacyContext.gitCommonDir ? { gitCommonDir: legacyContext.gitCommonDir } : {},
|
|
278
|
+
...legacyContext.branch ? { branch: legacyContext.branch } : {},
|
|
279
|
+
status: "active",
|
|
280
|
+
createdAt: now2,
|
|
281
|
+
updatedAt: now2
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function bindingMatchesContext(binding, context) {
|
|
285
|
+
if (binding.worktreeRoot === context.worktreeRoot) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return binding.gitCommonDir !== void 0 && context.gitCommonDir !== void 0 && binding.gitCommonDir === context.gitCommonDir && context.cwd.startsWith(`${binding.worktreeRoot}/`);
|
|
289
|
+
}
|
|
290
|
+
function canonicalPath(path) {
|
|
291
|
+
const resolved = resolve(path);
|
|
292
|
+
return existsSync(resolved) ? realpathSync(resolved) : resolved;
|
|
293
|
+
}
|
|
294
|
+
function gitOutput(args, cwd) {
|
|
295
|
+
try {
|
|
296
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || void 0;
|
|
297
|
+
} catch {
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function sha256(value) {
|
|
302
|
+
return createHash("sha256").update(value).digest("hex");
|
|
303
|
+
}
|
|
304
|
+
function withRegistryLock(codexHome, fn) {
|
|
305
|
+
const path = hookRegistryPath(codexHome);
|
|
306
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
307
|
+
const lockPath = hookRegistryLockPath(codexHome);
|
|
308
|
+
let fd;
|
|
309
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
310
|
+
try {
|
|
311
|
+
fd = openSync(lockPath, "wx", 384);
|
|
312
|
+
writeFileSync(fd, `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
313
|
+
`);
|
|
314
|
+
break;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
|
|
317
|
+
if (recoverStaleLock(lockPath)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
sleepSync(20);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (fd === void 0) {
|
|
327
|
+
throw new Error(`Timed out waiting for hook registry lock: ${lockPath}`);
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
return fn();
|
|
331
|
+
} finally {
|
|
332
|
+
closeSync(fd);
|
|
333
|
+
rmSync(lockPath, { force: true });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
var LOCK_STALE_MS = 3e4;
|
|
337
|
+
var TOUCH_REFRESH_MS = 1e4;
|
|
338
|
+
function recoverStaleLock(lockPath) {
|
|
339
|
+
const report = inspectLockPath(lockPath);
|
|
340
|
+
if (!report.stale) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
rmSync(lockPath, { force: true });
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
function inspectLockPath(path) {
|
|
347
|
+
if (!existsSync(path)) {
|
|
348
|
+
return { path, exists: false, stale: false };
|
|
349
|
+
}
|
|
350
|
+
const metadata = readLockMetadata(path);
|
|
351
|
+
const stat = statSync(path);
|
|
352
|
+
const ageMs = Date.now() - (metadata.createdAtMs ?? stat.mtimeMs);
|
|
353
|
+
const alive = metadata.pid ? processAlive(metadata.pid) : void 0;
|
|
354
|
+
return {
|
|
355
|
+
path,
|
|
356
|
+
exists: true,
|
|
357
|
+
stale: ageMs > LOCK_STALE_MS && alive !== true,
|
|
358
|
+
ageMs,
|
|
359
|
+
...metadata.pid ? { pid: metadata.pid } : {},
|
|
360
|
+
...alive === void 0 ? {} : { processAlive: alive }
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function readLockMetadata(path) {
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
366
|
+
if (!isRecord(parsed)) return {};
|
|
367
|
+
const pid = typeof parsed.pid === "number" ? parsed.pid : void 0;
|
|
368
|
+
const createdAtMs = typeof parsed.createdAt === "string" ? Date.parse(parsed.createdAt) : void 0;
|
|
369
|
+
return {
|
|
370
|
+
...pid && Number.isInteger(pid) && pid > 0 ? { pid } : {},
|
|
371
|
+
...createdAtMs && Number.isFinite(createdAtMs) ? { createdAtMs } : {}
|
|
372
|
+
};
|
|
373
|
+
} catch {
|
|
374
|
+
return {};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function processAlive(pid) {
|
|
378
|
+
try {
|
|
379
|
+
process.kill(pid, 0);
|
|
380
|
+
return true;
|
|
381
|
+
} catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function sleepSync(ms) {
|
|
386
|
+
try {
|
|
387
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
388
|
+
} catch {
|
|
389
|
+
const end = Date.now() + ms;
|
|
390
|
+
while (Date.now() < end) {
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function stringValue(value) {
|
|
395
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// plugins/autonomous-pr-loop/core/redaction.ts
|
|
399
|
+
function redactSecrets(value) {
|
|
400
|
+
return value.replace(/\bBearer\s+\S+/gi, "Bearer [redacted]").replace(/\b[A-Za-z0-9._%+-]+:[^@\s]+@/g, "[redacted]@").replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[redacted]").replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "[redacted]").replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]").replace(/((?:token|api_key|authorization|password|secret)\s*[:=]\s*)(["'])(?:(?!\2).)*\2/gi, "$1$2[redacted]$2").replace(/((?:token|api_key|authorization|password|secret)\s*[:=]\s*)[^\n\r,;}]+/gi, "$1[redacted]");
|
|
401
|
+
}
|
|
402
|
+
function isSecretKey(key) {
|
|
403
|
+
return /token|api_key|authorization|password|secret/i.test(key);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// plugins/autonomous-pr-loop/core/storage.ts
|
|
407
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
408
|
+
import { dirname as dirname2 } from "node:path";
|
|
409
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
410
|
+
import { DatabaseSync } from "node:sqlite";
|
|
411
|
+
var STORAGE_SCHEMA_VERSION = 8;
|
|
412
|
+
var SUPPORTED_SCHEMA_VERSIONS = [1, 2, 3, 4, 5, 6, 7, STORAGE_SCHEMA_VERSION];
|
|
413
|
+
var TIMELINE_SOURCES = ["event", "worker_event", "worker", "state", "gate", "artifact", "decision"];
|
|
414
|
+
var TIMELINE_TRIGGER_NAMES = [
|
|
415
|
+
"timeline_events_insert",
|
|
416
|
+
"timeline_worker_events_insert",
|
|
417
|
+
"timeline_workers_insert",
|
|
418
|
+
"timeline_workers_status_update",
|
|
419
|
+
"timeline_states_insert",
|
|
420
|
+
"timeline_gates_insert",
|
|
421
|
+
"timeline_artifacts_insert",
|
|
422
|
+
"timeline_decisions_insert"
|
|
423
|
+
];
|
|
424
|
+
var PR_C_TABLES_SQL = `
|
|
425
|
+
create table if not exists pr_links (
|
|
426
|
+
id text primary key,
|
|
427
|
+
run_id text not null,
|
|
428
|
+
branch text not null,
|
|
429
|
+
pr_number integer not null,
|
|
430
|
+
url text not null,
|
|
431
|
+
head_ref text not null,
|
|
432
|
+
base_ref text not null,
|
|
433
|
+
state text not null,
|
|
434
|
+
draft integer not null,
|
|
435
|
+
created_at text not null,
|
|
436
|
+
updated_at text not null,
|
|
437
|
+
unique(run_id, pr_number),
|
|
438
|
+
foreign key(run_id) references runs(id)
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
create table if not exists ci_checks (
|
|
442
|
+
id text primary key,
|
|
443
|
+
run_id text not null,
|
|
444
|
+
pr_number integer not null,
|
|
445
|
+
name text not null,
|
|
446
|
+
status text not null,
|
|
447
|
+
conclusion text,
|
|
448
|
+
url text,
|
|
449
|
+
started_at text,
|
|
450
|
+
completed_at text,
|
|
451
|
+
observed_at text not null,
|
|
452
|
+
foreign key(run_id) references runs(id)
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
create table if not exists review_comments (
|
|
456
|
+
id text primary key,
|
|
457
|
+
run_id text not null,
|
|
458
|
+
pr_number integer not null,
|
|
459
|
+
comment_id text not null,
|
|
460
|
+
url text not null,
|
|
461
|
+
author text not null,
|
|
462
|
+
body text not null,
|
|
463
|
+
path text not null,
|
|
464
|
+
line integer,
|
|
465
|
+
diff_hunk text not null,
|
|
466
|
+
is_resolved integer not null,
|
|
467
|
+
is_outdated integer not null,
|
|
468
|
+
actionable integer not null,
|
|
469
|
+
status text not null,
|
|
470
|
+
observed_at text not null,
|
|
471
|
+
unique(run_id, comment_id),
|
|
472
|
+
foreign key(run_id) references runs(id)
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
create table if not exists decisions (
|
|
476
|
+
id text primary key,
|
|
477
|
+
run_id text not null,
|
|
478
|
+
kind text not null,
|
|
479
|
+
message text not null,
|
|
480
|
+
details_json text,
|
|
481
|
+
created_at text not null,
|
|
482
|
+
foreign key(run_id) references runs(id)
|
|
483
|
+
);
|
|
484
|
+
`;
|
|
485
|
+
var PR_D_TABLES_SQL = `
|
|
486
|
+
create table if not exists workers (
|
|
487
|
+
id text primary key,
|
|
488
|
+
run_id text not null,
|
|
489
|
+
type text not null,
|
|
490
|
+
backend text not null,
|
|
491
|
+
status text not null,
|
|
492
|
+
thread_id text,
|
|
493
|
+
attempt integer not null,
|
|
494
|
+
resume_used integer not null,
|
|
495
|
+
started_at text not null,
|
|
496
|
+
completed_at text,
|
|
497
|
+
exit_code integer,
|
|
498
|
+
result_artifact_id text,
|
|
499
|
+
raw_jsonl_artifact_id text,
|
|
500
|
+
error text,
|
|
501
|
+
foreign key(run_id) references runs(id)
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
create table if not exists worker_events (
|
|
505
|
+
seq integer primary key autoincrement,
|
|
506
|
+
id text not null unique,
|
|
507
|
+
worker_id text not null,
|
|
508
|
+
run_id text not null,
|
|
509
|
+
event_type text not null,
|
|
510
|
+
item_type text,
|
|
511
|
+
item_id text,
|
|
512
|
+
item_status text,
|
|
513
|
+
thread_id text,
|
|
514
|
+
backend text,
|
|
515
|
+
summary_json text,
|
|
516
|
+
usage_json text,
|
|
517
|
+
artifact_ids_json text,
|
|
518
|
+
created_at text not null,
|
|
519
|
+
foreign key(worker_id) references workers(id),
|
|
520
|
+
foreign key(run_id) references runs(id)
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
create unique index if not exists workers_single_running
|
|
524
|
+
on workers(status)
|
|
525
|
+
where status = 'running';
|
|
526
|
+
`;
|
|
527
|
+
var PR_E_INDEXES_SQL = `
|
|
528
|
+
create unique index if not exists runs_single_running
|
|
529
|
+
on runs(status)
|
|
530
|
+
where status = 'RUNNING';
|
|
531
|
+
`;
|
|
532
|
+
var PR_E_TABLES_SQL = `
|
|
533
|
+
create table if not exists run_checks (
|
|
534
|
+
run_id text not null,
|
|
535
|
+
kind text not null,
|
|
536
|
+
status text not null,
|
|
537
|
+
details_json text,
|
|
538
|
+
created_at text not null,
|
|
539
|
+
primary key(run_id, kind),
|
|
540
|
+
foreign key(run_id) references runs(id)
|
|
541
|
+
);
|
|
542
|
+
`;
|
|
543
|
+
var TIMELINE_INDEX_SQL = `
|
|
544
|
+
create table if not exists timeline_index (
|
|
545
|
+
timeline_seq integer primary key autoincrement,
|
|
546
|
+
source text not null,
|
|
547
|
+
source_id text not null,
|
|
548
|
+
source_seq integer,
|
|
549
|
+
run_id text,
|
|
550
|
+
worker_id text,
|
|
551
|
+
created_at text not null,
|
|
552
|
+
unique(source, source_id)
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
create index if not exists timeline_index_created
|
|
556
|
+
on timeline_index(created_at desc, timeline_seq desc);
|
|
557
|
+
create index if not exists timeline_index_run
|
|
558
|
+
on timeline_index(run_id, timeline_seq desc);
|
|
559
|
+
create index if not exists timeline_index_worker
|
|
560
|
+
on timeline_index(worker_id, timeline_seq desc);
|
|
561
|
+
`;
|
|
562
|
+
var TIMELINE_TRIGGERS_SQL = `
|
|
563
|
+
create trigger if not exists timeline_events_insert
|
|
564
|
+
after insert on events
|
|
565
|
+
begin
|
|
566
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
567
|
+
values ('event', new.id, new.seq, new.run_id, null, new.created_at);
|
|
568
|
+
end;
|
|
569
|
+
|
|
570
|
+
create trigger if not exists timeline_worker_events_insert
|
|
571
|
+
after insert on worker_events
|
|
572
|
+
begin
|
|
573
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
574
|
+
values ('worker_event', new.id, new.seq, new.run_id, new.worker_id, new.created_at);
|
|
575
|
+
end;
|
|
576
|
+
|
|
577
|
+
create trigger if not exists timeline_workers_insert
|
|
578
|
+
after insert on workers
|
|
579
|
+
begin
|
|
580
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
581
|
+
values ('worker', new.id || ':' || new.status, null, new.run_id, new.id, new.started_at);
|
|
582
|
+
end;
|
|
583
|
+
|
|
584
|
+
create trigger if not exists timeline_workers_status_update
|
|
585
|
+
after update of status on workers
|
|
586
|
+
when old.status is not new.status
|
|
587
|
+
begin
|
|
588
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
589
|
+
values (
|
|
590
|
+
'worker',
|
|
591
|
+
new.id || ':' || new.status,
|
|
592
|
+
null,
|
|
593
|
+
new.run_id,
|
|
594
|
+
new.id,
|
|
595
|
+
coalesce(new.completed_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
596
|
+
);
|
|
597
|
+
end;
|
|
598
|
+
|
|
599
|
+
create trigger if not exists timeline_states_insert
|
|
600
|
+
after insert on states
|
|
601
|
+
begin
|
|
602
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
603
|
+
values ('state', cast(new.id as text), new.id, new.run_id, null, new.created_at);
|
|
604
|
+
end;
|
|
605
|
+
|
|
606
|
+
create trigger if not exists timeline_gates_insert
|
|
607
|
+
after insert on gates
|
|
608
|
+
begin
|
|
609
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
610
|
+
values ('gate', new.id, null, new.run_id, null, new.created_at);
|
|
611
|
+
end;
|
|
612
|
+
|
|
613
|
+
create trigger if not exists timeline_artifacts_insert
|
|
614
|
+
after insert on artifacts
|
|
615
|
+
begin
|
|
616
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
617
|
+
values ('artifact', new.id, null, new.run_id, null, new.created_at);
|
|
618
|
+
end;
|
|
619
|
+
|
|
620
|
+
create trigger if not exists timeline_decisions_insert
|
|
621
|
+
after insert on decisions
|
|
622
|
+
begin
|
|
623
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
624
|
+
values ('decision', new.id, null, new.run_id, null, new.created_at);
|
|
625
|
+
end;
|
|
626
|
+
`;
|
|
627
|
+
var SCHEMA_SQL = `
|
|
628
|
+
create table if not exists runs (
|
|
629
|
+
id text primary key,
|
|
630
|
+
status text not null,
|
|
631
|
+
current_state text,
|
|
632
|
+
version integer not null default 0,
|
|
633
|
+
branch text,
|
|
634
|
+
worktree_clean integer,
|
|
635
|
+
started_at text,
|
|
636
|
+
stopped_at text,
|
|
637
|
+
created_at text not null,
|
|
638
|
+
updated_at text not null
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
create table if not exists states (
|
|
642
|
+
id integer primary key autoincrement,
|
|
643
|
+
run_id text,
|
|
644
|
+
status text not null,
|
|
645
|
+
state text,
|
|
646
|
+
version integer not null,
|
|
647
|
+
payload_json text,
|
|
648
|
+
created_at text not null,
|
|
649
|
+
foreign key(run_id) references runs(id)
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
create table if not exists events (
|
|
653
|
+
seq integer primary key autoincrement,
|
|
654
|
+
id text not null unique,
|
|
655
|
+
run_id text,
|
|
656
|
+
kind text not null,
|
|
657
|
+
message text not null,
|
|
658
|
+
state_before text,
|
|
659
|
+
state_after text,
|
|
660
|
+
payload_json text,
|
|
661
|
+
artifact_ids_json text,
|
|
662
|
+
created_at text not null,
|
|
663
|
+
foreign key(run_id) references runs(id)
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
create table if not exists gates (
|
|
667
|
+
id text primary key,
|
|
668
|
+
run_id text,
|
|
669
|
+
kind text not null,
|
|
670
|
+
status text not null,
|
|
671
|
+
message text not null,
|
|
672
|
+
details_json text,
|
|
673
|
+
created_at text not null,
|
|
674
|
+
resolved_at text,
|
|
675
|
+
decision_note text,
|
|
676
|
+
decided_at text,
|
|
677
|
+
foreign key(run_id) references runs(id)
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
create table if not exists artifacts (
|
|
681
|
+
id text primary key,
|
|
682
|
+
run_id text,
|
|
683
|
+
kind text not null,
|
|
684
|
+
name text,
|
|
685
|
+
path text not null,
|
|
686
|
+
sha256 text,
|
|
687
|
+
metadata_json text,
|
|
688
|
+
created_at text not null,
|
|
689
|
+
foreign key(run_id) references runs(id)
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
create table if not exists repo_config (
|
|
693
|
+
id integer primary key check (id = 1),
|
|
694
|
+
schema_version integer not null,
|
|
695
|
+
config_json text not null,
|
|
696
|
+
updated_at text not null
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
${PR_C_TABLES_SQL}
|
|
700
|
+
${PR_D_TABLES_SQL}
|
|
701
|
+
${PR_E_TABLES_SQL}
|
|
702
|
+
${PR_E_INDEXES_SQL}
|
|
703
|
+
`;
|
|
704
|
+
var SqliteAgentLoopStorage = class {
|
|
705
|
+
constructor(path, options = {}) {
|
|
706
|
+
this.path = path;
|
|
707
|
+
this.mode = options.mode ?? "rw";
|
|
708
|
+
if (this.mode === "rw") {
|
|
709
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
710
|
+
} else if (!existsSync2(path)) {
|
|
711
|
+
throw new AgentLoopError("storage_error", "Read-only storage file does not exist.", {
|
|
712
|
+
details: { path }
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
this.db = new DatabaseSync(path, {
|
|
716
|
+
readOnly: this.mode === "ro",
|
|
717
|
+
enableForeignKeyConstraints: true,
|
|
718
|
+
timeout: 5e3
|
|
719
|
+
});
|
|
720
|
+
try {
|
|
721
|
+
this.db.exec("PRAGMA foreign_keys=ON");
|
|
722
|
+
this.db.exec("PRAGMA busy_timeout=5000");
|
|
723
|
+
if (this.mode === "rw") {
|
|
724
|
+
this.db.exec("PRAGMA journal_mode=WAL");
|
|
725
|
+
}
|
|
726
|
+
this.ensureSchema();
|
|
727
|
+
if (this.mode === "rw") {
|
|
728
|
+
this.ensureRepoConfigVersion();
|
|
729
|
+
} else {
|
|
730
|
+
this.validateRepoConfigVersion();
|
|
731
|
+
}
|
|
732
|
+
const workersSql = `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
|
|
733
|
+
started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
|
|
734
|
+
from workers`;
|
|
735
|
+
this.listWorkersByRunStatement = this.db.prepare(`${workersSql} where run_id = ? order by started_at desc limit ?`);
|
|
736
|
+
this.listWorkersStatement = this.db.prepare(`${workersSql} order by started_at desc limit ?`);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
this.db.close();
|
|
739
|
+
throw toStorageError(error, "Failed to open agent-loop storage.");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
path;
|
|
743
|
+
db;
|
|
744
|
+
mode;
|
|
745
|
+
listWorkersByRunStatement;
|
|
746
|
+
listWorkersStatement;
|
|
747
|
+
close() {
|
|
748
|
+
this.db.close();
|
|
749
|
+
}
|
|
750
|
+
writeRepoConfig(config) {
|
|
751
|
+
const snapshot = JSON.stringify({ schemaVersion: STORAGE_SCHEMA_VERSION, ...config });
|
|
752
|
+
this.transaction(() => {
|
|
753
|
+
this.db.prepare(
|
|
754
|
+
`insert into repo_config (id, schema_version, config_json, updated_at)
|
|
755
|
+
values (1, ?, ?, ?)
|
|
756
|
+
on conflict(id) do update set
|
|
757
|
+
schema_version = excluded.schema_version,
|
|
758
|
+
config_json = excluded.config_json,
|
|
759
|
+
updated_at = excluded.updated_at`
|
|
760
|
+
).run(STORAGE_SCHEMA_VERSION, snapshot, now());
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
readRepoConfig() {
|
|
764
|
+
const row = this.db.prepare("select schema_version, config_json from repo_config where id = 1").get();
|
|
765
|
+
if (!row) {
|
|
766
|
+
return void 0;
|
|
767
|
+
}
|
|
768
|
+
if (!isSupportedSchemaVersion(row.schema_version)) {
|
|
769
|
+
throw new AgentLoopError(
|
|
770
|
+
"storage_schema_mismatch",
|
|
771
|
+
`Stored repo config schema version ${row.schema_version} is not supported.`,
|
|
772
|
+
{ details: { expected: STORAGE_SCHEMA_VERSION, actual: row.schema_version } }
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const parsed = parseJson(row.config_json, "Stored repo config JSON is invalid.");
|
|
776
|
+
const { schemaVersion: _schemaVersion, ...config } = parsed;
|
|
777
|
+
return config;
|
|
778
|
+
}
|
|
779
|
+
createRun(status, options = {}) {
|
|
780
|
+
const createdAt = now();
|
|
781
|
+
const run = {
|
|
782
|
+
id: randomUUID2(),
|
|
783
|
+
status,
|
|
784
|
+
...options.currentState ? { currentState: options.currentState } : {},
|
|
785
|
+
version: 0,
|
|
786
|
+
...options.branch ? { branch: options.branch } : {},
|
|
787
|
+
...options.worktreeClean !== void 0 ? { worktreeClean: options.worktreeClean } : {},
|
|
788
|
+
createdAt,
|
|
789
|
+
updatedAt: createdAt,
|
|
790
|
+
startedAt: createdAt
|
|
791
|
+
};
|
|
792
|
+
try {
|
|
793
|
+
this.transaction(() => {
|
|
794
|
+
this.db.prepare(
|
|
795
|
+
`insert into runs (
|
|
796
|
+
id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
|
|
797
|
+
)
|
|
798
|
+
values (?, ?, ?, ?, ?, ?, ?, null, ?, ?)`
|
|
799
|
+
).run(
|
|
800
|
+
run.id,
|
|
801
|
+
run.status,
|
|
802
|
+
run.currentState ?? null,
|
|
803
|
+
run.version,
|
|
804
|
+
run.branch ?? null,
|
|
805
|
+
boolToDb(run.worktreeClean),
|
|
806
|
+
run.startedAt ?? null,
|
|
807
|
+
run.createdAt,
|
|
808
|
+
run.updatedAt
|
|
809
|
+
);
|
|
810
|
+
this.db.prepare(
|
|
811
|
+
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
812
|
+
values (?, ?, ?, ?, null, ?)`
|
|
813
|
+
).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
|
|
814
|
+
});
|
|
815
|
+
} catch (error) {
|
|
816
|
+
if (isUniqueConstraintError(error)) {
|
|
817
|
+
throw new AgentLoopError("version_conflict", "Another active run already exists.", {
|
|
818
|
+
details: { status },
|
|
819
|
+
exitCode: 2
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
throw error;
|
|
823
|
+
}
|
|
824
|
+
return run;
|
|
825
|
+
}
|
|
826
|
+
getOrCreateActiveRun(options = {}) {
|
|
827
|
+
return this.transaction(() => {
|
|
828
|
+
const active = this.getActiveRun();
|
|
829
|
+
if (active) {
|
|
830
|
+
return { run: active, created: false };
|
|
831
|
+
}
|
|
832
|
+
const createdAt = now();
|
|
833
|
+
const run = {
|
|
834
|
+
id: randomUUID2(),
|
|
835
|
+
status: "RUNNING",
|
|
836
|
+
...options.currentState ? { currentState: options.currentState } : {},
|
|
837
|
+
version: 0,
|
|
838
|
+
...options.branch ? { branch: options.branch } : {},
|
|
839
|
+
...options.worktreeClean !== void 0 ? { worktreeClean: options.worktreeClean } : {},
|
|
840
|
+
createdAt,
|
|
841
|
+
updatedAt: createdAt,
|
|
842
|
+
startedAt: createdAt
|
|
843
|
+
};
|
|
844
|
+
this.db.prepare(
|
|
845
|
+
`insert into runs (
|
|
846
|
+
id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
|
|
847
|
+
)
|
|
848
|
+
values (?, ?, ?, ?, ?, ?, ?, null, ?, ?)`
|
|
849
|
+
).run(
|
|
850
|
+
run.id,
|
|
851
|
+
run.status,
|
|
852
|
+
run.currentState ?? null,
|
|
853
|
+
run.version,
|
|
854
|
+
run.branch ?? null,
|
|
855
|
+
boolToDb(run.worktreeClean),
|
|
856
|
+
run.startedAt ?? null,
|
|
857
|
+
run.createdAt,
|
|
858
|
+
run.updatedAt
|
|
859
|
+
);
|
|
860
|
+
this.db.prepare(
|
|
861
|
+
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
862
|
+
values (?, ?, ?, ?, null, ?)`
|
|
863
|
+
).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
|
|
864
|
+
return { run, created: true };
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
recordRunCheck(check) {
|
|
868
|
+
const stored = { ...check, createdAt: now() };
|
|
869
|
+
this.transaction(() => {
|
|
870
|
+
this.db.prepare(
|
|
871
|
+
`insert into run_checks (run_id, kind, status, details_json, created_at)
|
|
872
|
+
values (?, ?, ?, ?, ?)
|
|
873
|
+
on conflict(run_id, kind) do update set
|
|
874
|
+
status = excluded.status,
|
|
875
|
+
details_json = excluded.details_json,
|
|
876
|
+
created_at = excluded.created_at`
|
|
877
|
+
).run(
|
|
878
|
+
stored.runId,
|
|
879
|
+
stored.kind,
|
|
880
|
+
stored.status,
|
|
881
|
+
stored.details === void 0 ? null : JSON.stringify(stored.details),
|
|
882
|
+
stored.createdAt
|
|
883
|
+
);
|
|
884
|
+
});
|
|
885
|
+
return stored;
|
|
886
|
+
}
|
|
887
|
+
hasRunCheck(runId, kind) {
|
|
888
|
+
const row = this.db.prepare("select 1 from run_checks where run_id = ? and kind = ? and status in ('passed', 'skipped') limit 1").get(runId, kind);
|
|
889
|
+
return row !== void 0;
|
|
890
|
+
}
|
|
891
|
+
listRunChecks(runId) {
|
|
892
|
+
const rows = this.db.prepare("select run_id, kind, status, details_json, created_at from run_checks where run_id = ? order by created_at desc").all(runId);
|
|
893
|
+
return rows.map(fromRunCheckRow);
|
|
894
|
+
}
|
|
895
|
+
updateRunStatus(runId, expectedVersion, status, options = {}) {
|
|
896
|
+
const updatedAt = now();
|
|
897
|
+
return this.transaction(() => {
|
|
898
|
+
const result = this.db.prepare(
|
|
899
|
+
`update runs
|
|
900
|
+
set status = ?,
|
|
901
|
+
current_state = coalesce(?, current_state),
|
|
902
|
+
branch = coalesce(?, branch),
|
|
903
|
+
worktree_clean = coalesce(?, worktree_clean),
|
|
904
|
+
stopped_at = coalesce(?, stopped_at),
|
|
905
|
+
version = version + 1,
|
|
906
|
+
updated_at = ?
|
|
907
|
+
where id = ? and version = ?`
|
|
908
|
+
).run(
|
|
909
|
+
status,
|
|
910
|
+
options.currentState ?? null,
|
|
911
|
+
options.branch ?? null,
|
|
912
|
+
boolToDb(options.worktreeClean),
|
|
913
|
+
options.stoppedAt ?? null,
|
|
914
|
+
updatedAt,
|
|
915
|
+
runId,
|
|
916
|
+
expectedVersion
|
|
917
|
+
);
|
|
918
|
+
if (result.changes !== 1) {
|
|
919
|
+
throw new AgentLoopError(
|
|
920
|
+
"version_conflict",
|
|
921
|
+
`Run ${runId} was updated by another writer.`,
|
|
922
|
+
{ details: { runId, expectedVersion } }
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
const run = this.getRun(runId);
|
|
926
|
+
this.db.prepare(
|
|
927
|
+
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
928
|
+
values (?, ?, ?, ?, null, ?)`
|
|
929
|
+
).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
|
|
930
|
+
return run;
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
appendEvent(event) {
|
|
934
|
+
const stored = {
|
|
935
|
+
id: randomUUID2(),
|
|
936
|
+
...event,
|
|
937
|
+
createdAt: now()
|
|
938
|
+
};
|
|
939
|
+
let seq = 0;
|
|
940
|
+
this.transaction(() => {
|
|
941
|
+
this.db.prepare(
|
|
942
|
+
`insert into events (
|
|
943
|
+
id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
|
|
944
|
+
)
|
|
945
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
946
|
+
).run(
|
|
947
|
+
stored.id,
|
|
948
|
+
stored.runId ?? null,
|
|
949
|
+
stored.kind,
|
|
950
|
+
stored.message,
|
|
951
|
+
stored.stateBefore ?? null,
|
|
952
|
+
stored.stateAfter ?? null,
|
|
953
|
+
stored.payload === void 0 ? null : JSON.stringify(stored.payload),
|
|
954
|
+
stored.artifactIds === void 0 ? null : JSON.stringify(stored.artifactIds),
|
|
955
|
+
stored.createdAt
|
|
956
|
+
);
|
|
957
|
+
seq = Number(this.db.prepare("select last_insert_rowid() as seq").get().seq);
|
|
958
|
+
});
|
|
959
|
+
return { seq, ...stored };
|
|
960
|
+
}
|
|
961
|
+
writeGate(gate) {
|
|
962
|
+
this.transaction(() => {
|
|
963
|
+
this.db.prepare(
|
|
964
|
+
`insert into gates (id, run_id, kind, status, message, details_json, created_at, resolved_at)
|
|
965
|
+
values (?, ?, ?, 'open', ?, ?, ?, null)`
|
|
966
|
+
).run(
|
|
967
|
+
randomUUID2(),
|
|
968
|
+
gate.runId ?? null,
|
|
969
|
+
gate.kind,
|
|
970
|
+
gate.message,
|
|
971
|
+
gate.details === void 0 ? null : JSON.stringify(gate.details),
|
|
972
|
+
now()
|
|
973
|
+
);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
resolveOpenGates(runId) {
|
|
977
|
+
this.transaction(() => {
|
|
978
|
+
this.db.prepare(
|
|
979
|
+
`update gates
|
|
980
|
+
set status = 'resolved', resolved_at = ?
|
|
981
|
+
where run_id = ? and status = 'open'`
|
|
982
|
+
).run(now(), runId);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
resolveOpenGatesByKind(kind, options = {}) {
|
|
986
|
+
const scope = options.scope ?? (options.runId ? "run" : "repo");
|
|
987
|
+
this.transaction(() => {
|
|
988
|
+
if (scope === "run") {
|
|
989
|
+
if (!options.runId) {
|
|
990
|
+
throw new AgentLoopError("storage_error", "runId is required for run-scoped gate recovery.");
|
|
991
|
+
}
|
|
992
|
+
this.db.prepare(
|
|
993
|
+
`update gates
|
|
994
|
+
set status = 'resolved', resolved_at = ?
|
|
995
|
+
where kind = ? and run_id = ? and status = 'open'`
|
|
996
|
+
).run(now(), kind, options.runId);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (scope === "repo") {
|
|
1000
|
+
this.db.prepare(
|
|
1001
|
+
`update gates
|
|
1002
|
+
set status = 'resolved', resolved_at = ?
|
|
1003
|
+
where kind = ? and run_id is null and status = 'open'`
|
|
1004
|
+
).run(now(), kind);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
this.db.prepare(
|
|
1008
|
+
`update gates
|
|
1009
|
+
set status = 'resolved', resolved_at = ?
|
|
1010
|
+
where kind = ? and status = 'open'`
|
|
1011
|
+
).run(now(), kind);
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
listGates(runId) {
|
|
1015
|
+
const sql = `select id, run_id, kind, status, message, details_json, created_at,
|
|
1016
|
+
resolved_at, decision_note, decided_at
|
|
1017
|
+
from gates
|
|
1018
|
+
${runId ? "where run_id = ?" : ""}
|
|
1019
|
+
order by created_at desc
|
|
1020
|
+
limit 100`;
|
|
1021
|
+
const rows = runId ? this.db.prepare(sql).all(runId) : this.db.prepare(sql).all();
|
|
1022
|
+
return rows.map(fromGateRow);
|
|
1023
|
+
}
|
|
1024
|
+
getGate(gateId) {
|
|
1025
|
+
const row = this.db.prepare(
|
|
1026
|
+
`select id, run_id, kind, status, message, details_json, created_at,
|
|
1027
|
+
resolved_at, decision_note, decided_at
|
|
1028
|
+
from gates
|
|
1029
|
+
where id = ?`
|
|
1030
|
+
).get(gateId);
|
|
1031
|
+
return row ? fromGateRow(row) : void 0;
|
|
1032
|
+
}
|
|
1033
|
+
decideGate(gateId, decision, note) {
|
|
1034
|
+
if (note.trim().length === 0) {
|
|
1035
|
+
throw new AgentLoopError("invalid_config", "Gate decision note is required.");
|
|
1036
|
+
}
|
|
1037
|
+
const decidedAt = now();
|
|
1038
|
+
this.transaction(() => {
|
|
1039
|
+
const result = this.db.prepare(
|
|
1040
|
+
`update gates
|
|
1041
|
+
set status = ?, decision_note = ?, decided_at = ?, resolved_at = coalesce(resolved_at, ?)
|
|
1042
|
+
where id = ? and status = 'open'`
|
|
1043
|
+
).run(decision, note, decidedAt, decidedAt, gateId);
|
|
1044
|
+
if (result.changes !== 1) {
|
|
1045
|
+
const gate2 = this.getGate(gateId);
|
|
1046
|
+
if (!gate2) {
|
|
1047
|
+
throw new AgentLoopError("storage_error", `Gate not found: ${gateId}`);
|
|
1048
|
+
}
|
|
1049
|
+
throw new AgentLoopError("storage_error", `Gate ${gateId} is not open.`, {
|
|
1050
|
+
details: { gateId, status: gate2.status }
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
const gate = this.getGate(gateId);
|
|
1055
|
+
if (!gate) {
|
|
1056
|
+
throw new AgentLoopError("storage_error", `Gate not found after decision: ${gateId}`);
|
|
1057
|
+
}
|
|
1058
|
+
return gate;
|
|
1059
|
+
}
|
|
1060
|
+
getCurrentStatus() {
|
|
1061
|
+
const repoGate = this.db.prepare(
|
|
1062
|
+
`select kind, message, details_json
|
|
1063
|
+
from gates
|
|
1064
|
+
where status = 'open' and run_id is null
|
|
1065
|
+
order by created_at desc
|
|
1066
|
+
limit 1`
|
|
1067
|
+
).get();
|
|
1068
|
+
if (repoGate) {
|
|
1069
|
+
return {
|
|
1070
|
+
status: "BLOCKED",
|
|
1071
|
+
gate: statusGateFromRow(repoGate)
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
const row = this.db.prepare(
|
|
1075
|
+
`select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
|
|
1076
|
+
from runs
|
|
1077
|
+
order by updated_at desc, rowid desc
|
|
1078
|
+
limit 1`
|
|
1079
|
+
).get();
|
|
1080
|
+
if (!row) {
|
|
1081
|
+
return { status: "IDLE" };
|
|
1082
|
+
}
|
|
1083
|
+
const run = fromRunRow(row);
|
|
1084
|
+
const runGate = this.db.prepare(
|
|
1085
|
+
`select kind, message, details_json
|
|
1086
|
+
from gates
|
|
1087
|
+
where status = 'open' and run_id = ?
|
|
1088
|
+
order by created_at desc
|
|
1089
|
+
limit 1`
|
|
1090
|
+
).get(run.id);
|
|
1091
|
+
if (runGate) {
|
|
1092
|
+
return {
|
|
1093
|
+
status: "BLOCKED",
|
|
1094
|
+
run,
|
|
1095
|
+
gate: statusGateFromRow(runGate)
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
if (run.status === "BLOCKED" && latestGateSatisfied(this.db, run.id)) {
|
|
1099
|
+
return { status: "READY", run: { ...run, status: "READY" } };
|
|
1100
|
+
}
|
|
1101
|
+
return { status: run.status, run };
|
|
1102
|
+
}
|
|
1103
|
+
listEvents(options = 50) {
|
|
1104
|
+
const limit = typeof options === "number" ? options : options.limit ?? 50;
|
|
1105
|
+
const sinceSeq = typeof options === "number" ? void 0 : options.sinceSeq;
|
|
1106
|
+
const rows = sinceSeq === void 0 ? this.db.prepare(
|
|
1107
|
+
`select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
|
|
1108
|
+
from events
|
|
1109
|
+
order by seq desc
|
|
1110
|
+
limit ?`
|
|
1111
|
+
).all(limit) : this.db.prepare(
|
|
1112
|
+
`select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
|
|
1113
|
+
from events
|
|
1114
|
+
where seq > ?
|
|
1115
|
+
order by seq asc
|
|
1116
|
+
limit ?`
|
|
1117
|
+
).all(sinceSeq, limit);
|
|
1118
|
+
return rows.map(fromEventRow);
|
|
1119
|
+
}
|
|
1120
|
+
findLatestEvent(runId, kind) {
|
|
1121
|
+
const row = this.db.prepare(
|
|
1122
|
+
`select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
|
|
1123
|
+
from events
|
|
1124
|
+
where run_id = ? and kind = ?
|
|
1125
|
+
order by seq desc
|
|
1126
|
+
limit 1`
|
|
1127
|
+
).get(runId, kind);
|
|
1128
|
+
return row ? fromEventRow(row) : void 0;
|
|
1129
|
+
}
|
|
1130
|
+
listAgentTimeline(query = {}) {
|
|
1131
|
+
const limit = clampLimit(query.limit ?? 50);
|
|
1132
|
+
const cursor = query.cursor ? decodeTimelineCursor(query.cursor) : void 0;
|
|
1133
|
+
const params = [];
|
|
1134
|
+
const where = [];
|
|
1135
|
+
if (cursor) {
|
|
1136
|
+
where.push("(created_at < ? or (created_at = ? and timeline_seq < ?))");
|
|
1137
|
+
params.push(cursor.occurredAt, cursor.occurredAt, cursor.timelineSeq);
|
|
1138
|
+
}
|
|
1139
|
+
if (query.sources?.length) {
|
|
1140
|
+
const sources = normalizeTimelineSources(query.sources);
|
|
1141
|
+
where.push(`source in (${sources.map(() => "?").join(", ")})`);
|
|
1142
|
+
params.push(...sources);
|
|
1143
|
+
}
|
|
1144
|
+
if (query.runId) {
|
|
1145
|
+
where.push("run_id = ?");
|
|
1146
|
+
params.push(query.runId);
|
|
1147
|
+
}
|
|
1148
|
+
if (query.workerId) {
|
|
1149
|
+
where.push("worker_id = ?");
|
|
1150
|
+
params.push(query.workerId);
|
|
1151
|
+
}
|
|
1152
|
+
params.push(limit + 1);
|
|
1153
|
+
const rows = this.db.prepare(
|
|
1154
|
+
`select timeline_seq, source, source_id, source_seq, run_id, worker_id, created_at
|
|
1155
|
+
from timeline_index
|
|
1156
|
+
${where.length ? `where ${where.join(" and ")}` : ""}
|
|
1157
|
+
order by created_at desc, timeline_seq desc
|
|
1158
|
+
limit ?`
|
|
1159
|
+
).all(...params);
|
|
1160
|
+
const pageRows = rows.slice(0, limit);
|
|
1161
|
+
const entries = pageRows.map((row) => this.timelineEntry(row)).filter((entry) => entry !== void 0);
|
|
1162
|
+
const last = pageRows[pageRows.length - 1];
|
|
1163
|
+
return {
|
|
1164
|
+
entries,
|
|
1165
|
+
...rows.length > limit && last ? { nextCursor: encodeTimelineCursor(last.timeline_seq, last.created_at) } : {}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
checkTimelineIntegrity() {
|
|
1169
|
+
const missingTable = !hasTable(this.db, "timeline_index");
|
|
1170
|
+
const triggers = new Set(this.db.prepare("select name from sqlite_master where type = 'trigger' and name like 'timeline_%'").all().map((row) => row.name));
|
|
1171
|
+
const missingTriggers = TIMELINE_TRIGGER_NAMES.filter((name) => !triggers.has(name));
|
|
1172
|
+
const sourceCounts = Object.fromEntries(TIMELINE_SOURCES.map((source) => [source, 0]));
|
|
1173
|
+
const missingSourceRows = [];
|
|
1174
|
+
if (!missingTable) {
|
|
1175
|
+
const rows = this.db.prepare("select source, count(*) as count from timeline_index group by source").all();
|
|
1176
|
+
for (const row of rows) {
|
|
1177
|
+
if (TIMELINE_SOURCES.includes(row.source)) {
|
|
1178
|
+
sourceCounts[row.source] = row.count;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
missingSourceRows.push(...timelineMissingSourceRows(this.db));
|
|
1182
|
+
}
|
|
1183
|
+
const ok = !missingTable && missingTriggers.length === 0 && missingSourceRows.length === 0;
|
|
1184
|
+
return {
|
|
1185
|
+
ok,
|
|
1186
|
+
missingTable,
|
|
1187
|
+
missingTriggers,
|
|
1188
|
+
missingSourceRows,
|
|
1189
|
+
sourceCounts,
|
|
1190
|
+
repair: "Run storage migration or rebuild timeline_index by dropping timeline_index/triggers and reopening storage in read-write mode."
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
upsertPrLink(link) {
|
|
1194
|
+
const createdAt = now();
|
|
1195
|
+
const id = randomUUID2();
|
|
1196
|
+
this.transaction(() => {
|
|
1197
|
+
this.db.prepare(
|
|
1198
|
+
`insert into pr_links (
|
|
1199
|
+
id, run_id, branch, pr_number, url, head_ref, base_ref, state, draft, created_at, updated_at
|
|
1200
|
+
)
|
|
1201
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1202
|
+
on conflict(run_id, pr_number) do update set
|
|
1203
|
+
branch = excluded.branch,
|
|
1204
|
+
url = excluded.url,
|
|
1205
|
+
head_ref = excluded.head_ref,
|
|
1206
|
+
base_ref = excluded.base_ref,
|
|
1207
|
+
state = excluded.state,
|
|
1208
|
+
draft = excluded.draft,
|
|
1209
|
+
updated_at = excluded.updated_at`
|
|
1210
|
+
).run(
|
|
1211
|
+
id,
|
|
1212
|
+
link.runId,
|
|
1213
|
+
link.branch,
|
|
1214
|
+
link.prNumber,
|
|
1215
|
+
link.url,
|
|
1216
|
+
link.headRef,
|
|
1217
|
+
link.baseRef,
|
|
1218
|
+
link.state,
|
|
1219
|
+
boolToDb(link.draft),
|
|
1220
|
+
createdAt,
|
|
1221
|
+
createdAt
|
|
1222
|
+
);
|
|
1223
|
+
});
|
|
1224
|
+
const stored = this.getPrLink(link.runId);
|
|
1225
|
+
if (!stored) {
|
|
1226
|
+
throw new AgentLoopError("storage_error", "PR link was not stored.");
|
|
1227
|
+
}
|
|
1228
|
+
return stored;
|
|
1229
|
+
}
|
|
1230
|
+
getPrLink(runId) {
|
|
1231
|
+
const row = this.db.prepare(
|
|
1232
|
+
`select id, run_id, branch, pr_number, url, head_ref, base_ref, state, draft, created_at, updated_at
|
|
1233
|
+
from pr_links
|
|
1234
|
+
where run_id = ?
|
|
1235
|
+
order by updated_at desc
|
|
1236
|
+
limit 1`
|
|
1237
|
+
).get(runId);
|
|
1238
|
+
return row ? fromPrLinkRow(row) : void 0;
|
|
1239
|
+
}
|
|
1240
|
+
replaceCiChecks(runId, prNumber, checks) {
|
|
1241
|
+
const observedAt = now();
|
|
1242
|
+
this.transaction(() => {
|
|
1243
|
+
this.db.prepare("delete from ci_checks where run_id = ? and pr_number = ?").run(runId, prNumber);
|
|
1244
|
+
for (const check of checks) {
|
|
1245
|
+
this.db.prepare(
|
|
1246
|
+
`insert into ci_checks (
|
|
1247
|
+
id, run_id, pr_number, name, status, conclusion, url, started_at, completed_at, observed_at
|
|
1248
|
+
)
|
|
1249
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1250
|
+
).run(
|
|
1251
|
+
randomUUID2(),
|
|
1252
|
+
runId,
|
|
1253
|
+
prNumber,
|
|
1254
|
+
check.name,
|
|
1255
|
+
check.status,
|
|
1256
|
+
check.conclusion ?? null,
|
|
1257
|
+
check.url ?? null,
|
|
1258
|
+
check.startedAt ?? null,
|
|
1259
|
+
check.completedAt ?? null,
|
|
1260
|
+
observedAt
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
return this.listCiChecks(runId);
|
|
1265
|
+
}
|
|
1266
|
+
listCiChecks(runId) {
|
|
1267
|
+
const rows = this.db.prepare(
|
|
1268
|
+
`select id, run_id, pr_number, name, status, conclusion, url, started_at, completed_at, observed_at
|
|
1269
|
+
from ci_checks
|
|
1270
|
+
where run_id = ?
|
|
1271
|
+
order by observed_at desc, name asc`
|
|
1272
|
+
).all(runId);
|
|
1273
|
+
return rows.map(fromCiCheckRow);
|
|
1274
|
+
}
|
|
1275
|
+
replaceReviewComments(runId, prNumber, comments) {
|
|
1276
|
+
const observedAt = now();
|
|
1277
|
+
this.transaction(() => {
|
|
1278
|
+
this.db.prepare("delete from review_comments where run_id = ? and pr_number = ?").run(runId, prNumber);
|
|
1279
|
+
for (const comment of comments) {
|
|
1280
|
+
this.db.prepare(
|
|
1281
|
+
`insert into review_comments (
|
|
1282
|
+
id, run_id, pr_number, comment_id, url, author, body, path, line, diff_hunk,
|
|
1283
|
+
is_resolved, is_outdated, actionable, status, observed_at
|
|
1284
|
+
)
|
|
1285
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1286
|
+
).run(
|
|
1287
|
+
randomUUID2(),
|
|
1288
|
+
runId,
|
|
1289
|
+
prNumber,
|
|
1290
|
+
comment.commentId,
|
|
1291
|
+
comment.url,
|
|
1292
|
+
comment.author,
|
|
1293
|
+
comment.body,
|
|
1294
|
+
comment.path,
|
|
1295
|
+
comment.line ?? null,
|
|
1296
|
+
comment.diffHunk,
|
|
1297
|
+
boolToDb(comment.isResolved),
|
|
1298
|
+
boolToDb(comment.isOutdated),
|
|
1299
|
+
boolToDb(comment.actionable),
|
|
1300
|
+
comment.status,
|
|
1301
|
+
observedAt
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
return this.listReviewComments(runId);
|
|
1306
|
+
}
|
|
1307
|
+
listReviewComments(runId) {
|
|
1308
|
+
const rows = this.db.prepare(
|
|
1309
|
+
`select id, run_id, pr_number, comment_id, url, author, body, path, line, diff_hunk,
|
|
1310
|
+
is_resolved, is_outdated, actionable, status, observed_at
|
|
1311
|
+
from review_comments
|
|
1312
|
+
where run_id = ?
|
|
1313
|
+
order by observed_at desc, path asc`
|
|
1314
|
+
).all(runId);
|
|
1315
|
+
return rows.map(fromReviewCommentRow);
|
|
1316
|
+
}
|
|
1317
|
+
appendDecision(decision) {
|
|
1318
|
+
const stored = { id: randomUUID2(), ...decision, createdAt: now() };
|
|
1319
|
+
this.transaction(() => {
|
|
1320
|
+
this.db.prepare(
|
|
1321
|
+
`insert into decisions (id, run_id, kind, message, details_json, created_at)
|
|
1322
|
+
values (?, ?, ?, ?, ?, ?)`
|
|
1323
|
+
).run(
|
|
1324
|
+
stored.id,
|
|
1325
|
+
stored.runId,
|
|
1326
|
+
stored.kind,
|
|
1327
|
+
stored.message,
|
|
1328
|
+
stored.details === void 0 ? null : JSON.stringify(stored.details),
|
|
1329
|
+
stored.createdAt
|
|
1330
|
+
);
|
|
1331
|
+
});
|
|
1332
|
+
return stored;
|
|
1333
|
+
}
|
|
1334
|
+
listDecisions(runId) {
|
|
1335
|
+
const rows = this.db.prepare(
|
|
1336
|
+
`select id, run_id, kind, message, details_json, created_at
|
|
1337
|
+
from decisions
|
|
1338
|
+
where run_id = ?
|
|
1339
|
+
order by created_at desc`
|
|
1340
|
+
).all(runId);
|
|
1341
|
+
return rows.map(fromDecisionRow);
|
|
1342
|
+
}
|
|
1343
|
+
createWorker(worker) {
|
|
1344
|
+
const id = randomUUID2();
|
|
1345
|
+
const startedAt = now();
|
|
1346
|
+
try {
|
|
1347
|
+
this.transaction(() => {
|
|
1348
|
+
this.db.prepare(
|
|
1349
|
+
`insert into workers (
|
|
1350
|
+
id, run_id, type, backend, status, thread_id, attempt, resume_used,
|
|
1351
|
+
started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
|
|
1352
|
+
)
|
|
1353
|
+
values (?, ?, ?, ?, 'running', null, ?, ?, ?, null, null, null, null, null)`
|
|
1354
|
+
).run(id, worker.runId, worker.type, worker.backend, worker.attempt, boolToDb(worker.resumeUsed), startedAt);
|
|
1355
|
+
});
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
if (isUniqueConstraintError(error)) {
|
|
1358
|
+
throw new AgentLoopError("worker_already_running", "Another worker is already running.", {
|
|
1359
|
+
details: { runId: worker.runId },
|
|
1360
|
+
exitCode: 2
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
throw error;
|
|
1364
|
+
}
|
|
1365
|
+
return this.getWorker(id);
|
|
1366
|
+
}
|
|
1367
|
+
updateWorker(workerId, patch) {
|
|
1368
|
+
this.transaction(() => {
|
|
1369
|
+
this.db.prepare(
|
|
1370
|
+
`update workers
|
|
1371
|
+
set status = coalesce(?, status),
|
|
1372
|
+
thread_id = coalesce(?, thread_id),
|
|
1373
|
+
completed_at = coalesce(?, completed_at),
|
|
1374
|
+
exit_code = coalesce(?, exit_code),
|
|
1375
|
+
result_artifact_id = coalesce(?, result_artifact_id),
|
|
1376
|
+
raw_jsonl_artifact_id = coalesce(?, raw_jsonl_artifact_id),
|
|
1377
|
+
error = coalesce(?, error)
|
|
1378
|
+
where id = ?`
|
|
1379
|
+
).run(
|
|
1380
|
+
patch.status ?? null,
|
|
1381
|
+
patch.threadId ?? null,
|
|
1382
|
+
patch.completedAt ?? null,
|
|
1383
|
+
patch.exitCode ?? null,
|
|
1384
|
+
patch.resultArtifactId ?? null,
|
|
1385
|
+
patch.rawJsonlArtifactId ?? null,
|
|
1386
|
+
patch.error ?? null,
|
|
1387
|
+
workerId
|
|
1388
|
+
);
|
|
1389
|
+
});
|
|
1390
|
+
return this.getWorker(workerId);
|
|
1391
|
+
}
|
|
1392
|
+
getRunningWorker() {
|
|
1393
|
+
const row = this.db.prepare(
|
|
1394
|
+
`select id, run_id, type, backend, status, thread_id, attempt, resume_used,
|
|
1395
|
+
started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
|
|
1396
|
+
from workers
|
|
1397
|
+
where status = 'running'
|
|
1398
|
+
order by started_at desc
|
|
1399
|
+
limit 1`
|
|
1400
|
+
).get();
|
|
1401
|
+
return row ? fromWorkerRow(row) : void 0;
|
|
1402
|
+
}
|
|
1403
|
+
listWorkers(runId, limit = 50) {
|
|
1404
|
+
const rows = runId ? this.listWorkersByRunStatement.all(runId, limit) : this.listWorkersStatement.all(limit);
|
|
1405
|
+
return rows.map(fromWorkerRow);
|
|
1406
|
+
}
|
|
1407
|
+
appendWorkerEvent(event) {
|
|
1408
|
+
const existing = this.findDuplicateWorkerEvent(event);
|
|
1409
|
+
if (existing) {
|
|
1410
|
+
return existing;
|
|
1411
|
+
}
|
|
1412
|
+
const stored = { id: randomUUID2(), ...event, createdAt: now() };
|
|
1413
|
+
let seq = 0;
|
|
1414
|
+
this.transaction(() => {
|
|
1415
|
+
this.db.prepare(
|
|
1416
|
+
`insert into worker_events (
|
|
1417
|
+
id, worker_id, run_id, event_type, item_type, item_id, item_status,
|
|
1418
|
+
thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
|
|
1419
|
+
)
|
|
1420
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1421
|
+
).run(
|
|
1422
|
+
stored.id,
|
|
1423
|
+
stored.workerId,
|
|
1424
|
+
stored.runId,
|
|
1425
|
+
stored.eventType,
|
|
1426
|
+
stored.itemType ?? null,
|
|
1427
|
+
stored.itemId ?? null,
|
|
1428
|
+
stored.itemStatus ?? null,
|
|
1429
|
+
stored.threadId ?? null,
|
|
1430
|
+
stored.backend ?? null,
|
|
1431
|
+
stored.summary === void 0 ? null : JSON.stringify(stored.summary),
|
|
1432
|
+
stored.usage === void 0 ? null : JSON.stringify(stored.usage),
|
|
1433
|
+
stored.artifactIds === void 0 ? null : JSON.stringify(stored.artifactIds),
|
|
1434
|
+
stored.createdAt
|
|
1435
|
+
);
|
|
1436
|
+
seq = Number(this.db.prepare("select last_insert_rowid() as seq").get().seq);
|
|
1437
|
+
});
|
|
1438
|
+
return { seq, ...stored };
|
|
1439
|
+
}
|
|
1440
|
+
listWorkerEvents(workerId) {
|
|
1441
|
+
const rows = this.db.prepare(
|
|
1442
|
+
`select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
|
|
1443
|
+
thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
|
|
1444
|
+
from worker_events
|
|
1445
|
+
where worker_id = ?
|
|
1446
|
+
order by seq asc`
|
|
1447
|
+
).all(workerId);
|
|
1448
|
+
return rows.map(fromWorkerEventRow);
|
|
1449
|
+
}
|
|
1450
|
+
findDuplicateWorkerEvent(event) {
|
|
1451
|
+
if (!event.threadId) {
|
|
1452
|
+
return void 0;
|
|
1453
|
+
}
|
|
1454
|
+
const row = event.itemId ? this.db.prepare(
|
|
1455
|
+
`select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
|
|
1456
|
+
thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
|
|
1457
|
+
from worker_events
|
|
1458
|
+
where thread_id = ? and item_id = ? and coalesce(item_status, '') = ?
|
|
1459
|
+
limit 1`
|
|
1460
|
+
).get(event.threadId, event.itemId, event.itemStatus ?? "") : this.db.prepare(
|
|
1461
|
+
`select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
|
|
1462
|
+
thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
|
|
1463
|
+
from worker_events
|
|
1464
|
+
where thread_id = ? and event_type = ? and item_id is null
|
|
1465
|
+
limit 1`
|
|
1466
|
+
).get(event.threadId, event.eventType);
|
|
1467
|
+
return row ? fromWorkerEventRow(row) : void 0;
|
|
1468
|
+
}
|
|
1469
|
+
insertArtifact(record) {
|
|
1470
|
+
this.transaction(() => {
|
|
1471
|
+
this.db.prepare(
|
|
1472
|
+
`insert into artifacts (id, run_id, kind, name, path, sha256, metadata_json, created_at)
|
|
1473
|
+
values (?, ?, ?, ?, ?, ?, null, ?)`
|
|
1474
|
+
).run(
|
|
1475
|
+
record.id,
|
|
1476
|
+
record.runId,
|
|
1477
|
+
record.kind,
|
|
1478
|
+
record.name,
|
|
1479
|
+
record.path,
|
|
1480
|
+
record.sha256,
|
|
1481
|
+
record.createdAt
|
|
1482
|
+
);
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
getArtifact(artifactId) {
|
|
1486
|
+
const row = this.db.prepare(
|
|
1487
|
+
`select id, run_id, kind, name, path, sha256, created_at
|
|
1488
|
+
from artifacts
|
|
1489
|
+
where id = ?`
|
|
1490
|
+
).get(artifactId);
|
|
1491
|
+
if (!row) {
|
|
1492
|
+
throw new AgentLoopError("storage_error", `Artifact not found: ${artifactId}`);
|
|
1493
|
+
}
|
|
1494
|
+
return fromArtifactRow(row);
|
|
1495
|
+
}
|
|
1496
|
+
listArtifacts(runId) {
|
|
1497
|
+
const rows = this.db.prepare(
|
|
1498
|
+
`select id, run_id, kind, name, path, sha256, created_at
|
|
1499
|
+
from artifacts
|
|
1500
|
+
where run_id = ?
|
|
1501
|
+
order by created_at asc`
|
|
1502
|
+
).all(runId);
|
|
1503
|
+
return rows.map(fromArtifactRow);
|
|
1504
|
+
}
|
|
1505
|
+
linkArtifactToEvent(eventId, artifactId) {
|
|
1506
|
+
this.transaction(() => {
|
|
1507
|
+
const row = this.db.prepare("select artifact_ids_json from events where id = ?").get(eventId);
|
|
1508
|
+
if (!row) {
|
|
1509
|
+
throw new AgentLoopError("storage_error", `Event not found: ${eventId}`);
|
|
1510
|
+
}
|
|
1511
|
+
const ids = row.artifact_ids_json ? parseJson(row.artifact_ids_json, "Stored artifact id list is invalid.") : [];
|
|
1512
|
+
if (!ids.includes(artifactId)) {
|
|
1513
|
+
ids.push(artifactId);
|
|
1514
|
+
}
|
|
1515
|
+
this.db.prepare("update events set artifact_ids_json = ? where id = ?").run(JSON.stringify(ids), eventId);
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
getCurrentRun() {
|
|
1519
|
+
const row = this.db.prepare(
|
|
1520
|
+
`select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
|
|
1521
|
+
from runs
|
|
1522
|
+
order by updated_at desc
|
|
1523
|
+
limit 1`
|
|
1524
|
+
).get();
|
|
1525
|
+
return row ? fromRunRow(row) : void 0;
|
|
1526
|
+
}
|
|
1527
|
+
listRuns(limit = 50) {
|
|
1528
|
+
const rows = this.db.prepare(
|
|
1529
|
+
`select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
|
|
1530
|
+
from runs
|
|
1531
|
+
order by updated_at desc
|
|
1532
|
+
limit ?`
|
|
1533
|
+
).all(limit);
|
|
1534
|
+
return rows.map(fromRunRow);
|
|
1535
|
+
}
|
|
1536
|
+
/** Run a group of read queries against one SQLite snapshot. */
|
|
1537
|
+
readTransaction(fn) {
|
|
1538
|
+
this.db.exec("BEGIN");
|
|
1539
|
+
try {
|
|
1540
|
+
const result = fn();
|
|
1541
|
+
this.db.exec("COMMIT");
|
|
1542
|
+
return result;
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
try {
|
|
1545
|
+
this.db.exec("ROLLBACK");
|
|
1546
|
+
} catch (rollbackError) {
|
|
1547
|
+
throw new AgentLoopError("storage_error", "Read transaction rollback failed.", {
|
|
1548
|
+
details: {
|
|
1549
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
1550
|
+
rollback: rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
ensureSchema() {
|
|
1558
|
+
const currentVersion = this.getUserVersion();
|
|
1559
|
+
if (currentVersion !== 0 && !isSupportedSchemaVersion(currentVersion)) {
|
|
1560
|
+
throw new AgentLoopError(
|
|
1561
|
+
"storage_schema_mismatch",
|
|
1562
|
+
`SQLite schema version ${currentVersion} is not supported.`,
|
|
1563
|
+
{ details: { expected: STORAGE_SCHEMA_VERSION, actual: currentVersion } }
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
if (currentVersion === STORAGE_SCHEMA_VERSION) {
|
|
1567
|
+
if (this.mode !== "ro") {
|
|
1568
|
+
this.transaction(() => this.reconcileHighFidelityWorkerEventsV8());
|
|
1569
|
+
}
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (this.mode === "ro") {
|
|
1573
|
+
throw new AgentLoopError(
|
|
1574
|
+
"storage_schema_mismatch",
|
|
1575
|
+
`SQLite schema version ${currentVersion} requires migration before read-only use.`,
|
|
1576
|
+
{ details: { expected: STORAGE_SCHEMA_VERSION, actual: currentVersion } }
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
this.transaction(() => {
|
|
1580
|
+
const lockedVersion = this.getUserVersion();
|
|
1581
|
+
if (lockedVersion === STORAGE_SCHEMA_VERSION) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (lockedVersion !== 0 && !isSupportedSchemaVersion(lockedVersion)) {
|
|
1585
|
+
throw new AgentLoopError(
|
|
1586
|
+
"storage_schema_mismatch",
|
|
1587
|
+
`SQLite schema version ${lockedVersion} is not supported.`,
|
|
1588
|
+
{ details: { expected: STORAGE_SCHEMA_VERSION, actual: lockedVersion } }
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
this.db.exec(SCHEMA_SQL);
|
|
1592
|
+
this.migratePrC();
|
|
1593
|
+
this.migratePrD();
|
|
1594
|
+
this.migratePrE();
|
|
1595
|
+
this.migrateF0();
|
|
1596
|
+
this.migrateTimelineV7();
|
|
1597
|
+
this.migrateHighFidelityWorkerEventsV8();
|
|
1598
|
+
this.markSchemaVersion();
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
migratePrC() {
|
|
1602
|
+
addColumnIfMissing(this.db, "runs", "current_state", "text");
|
|
1603
|
+
addColumnIfMissing(this.db, "runs", "branch", "text");
|
|
1604
|
+
addColumnIfMissing(this.db, "runs", "worktree_clean", "integer");
|
|
1605
|
+
addColumnIfMissing(this.db, "runs", "started_at", "text");
|
|
1606
|
+
addColumnIfMissing(this.db, "runs", "stopped_at", "text");
|
|
1607
|
+
addColumnIfMissing(this.db, "states", "state", "text");
|
|
1608
|
+
addColumnIfMissing(this.db, "states", "payload_json", "text");
|
|
1609
|
+
addColumnIfMissing(this.db, "events", "state_before", "text");
|
|
1610
|
+
addColumnIfMissing(this.db, "events", "state_after", "text");
|
|
1611
|
+
addColumnIfMissing(this.db, "events", "artifact_ids_json", "text");
|
|
1612
|
+
addColumnIfMissing(this.db, "artifacts", "name", "text");
|
|
1613
|
+
addColumnIfMissing(this.db, "artifacts", "sha256", "text");
|
|
1614
|
+
this.db.exec(PR_C_TABLES_SQL);
|
|
1615
|
+
}
|
|
1616
|
+
migratePrD() {
|
|
1617
|
+
this.db.exec(PR_D_TABLES_SQL);
|
|
1618
|
+
}
|
|
1619
|
+
migratePrE() {
|
|
1620
|
+
addColumnIfMissing(this.db, "gates", "decision_note", "text");
|
|
1621
|
+
addColumnIfMissing(this.db, "gates", "decided_at", "text");
|
|
1622
|
+
this.db.exec(PR_E_TABLES_SQL);
|
|
1623
|
+
this.db.exec(PR_E_INDEXES_SQL);
|
|
1624
|
+
}
|
|
1625
|
+
migrateF0() {
|
|
1626
|
+
rebuildEventsWithSeq(this.db);
|
|
1627
|
+
rebuildWorkerEventsWithSeq(this.db);
|
|
1628
|
+
}
|
|
1629
|
+
migrateTimelineV7() {
|
|
1630
|
+
this.db.exec(TIMELINE_INDEX_SQL);
|
|
1631
|
+
this.db.exec(TIMELINE_TRIGGERS_SQL);
|
|
1632
|
+
backfillTimelineIndex(this.db);
|
|
1633
|
+
}
|
|
1634
|
+
migrateHighFidelityWorkerEventsV8() {
|
|
1635
|
+
addColumnIfMissing(this.db, "worker_events", "item_id", "text");
|
|
1636
|
+
addColumnIfMissing(this.db, "worker_events", "item_status", "text");
|
|
1637
|
+
addColumnIfMissing(this.db, "worker_events", "thread_id", "text");
|
|
1638
|
+
addColumnIfMissing(this.db, "worker_events", "backend", "text");
|
|
1639
|
+
addColumnIfMissing(this.db, "worker_events", "artifact_ids_json", "text");
|
|
1640
|
+
this.reconcileHighFidelityWorkerEventsV8();
|
|
1641
|
+
}
|
|
1642
|
+
reconcileHighFidelityWorkerEventsV8() {
|
|
1643
|
+
dedupeHighFidelityWorkerEventsV8(this.db);
|
|
1644
|
+
this.db.exec(`
|
|
1645
|
+
drop index if exists worker_events_thread_item_unique;
|
|
1646
|
+
create unique index if not exists worker_events_thread_item_status_unique
|
|
1647
|
+
on worker_events(thread_id, item_id, coalesce(item_status, ''))
|
|
1648
|
+
where item_id is not null;
|
|
1649
|
+
create unique index if not exists worker_events_thread_event_unique
|
|
1650
|
+
on worker_events(thread_id, event_type)
|
|
1651
|
+
where item_id is null;
|
|
1652
|
+
`);
|
|
1653
|
+
}
|
|
1654
|
+
markSchemaVersion() {
|
|
1655
|
+
this.db.exec(`PRAGMA user_version = ${STORAGE_SCHEMA_VERSION}`);
|
|
1656
|
+
}
|
|
1657
|
+
ensureRepoConfigVersion() {
|
|
1658
|
+
this.validateRepoConfigVersion(true);
|
|
1659
|
+
}
|
|
1660
|
+
validateRepoConfigVersion(rewrite = false) {
|
|
1661
|
+
let row;
|
|
1662
|
+
try {
|
|
1663
|
+
row = this.db.prepare("select schema_version, config_json from repo_config where id = 1").get();
|
|
1664
|
+
} catch (error) {
|
|
1665
|
+
throw toStorageError(error, "Could not read stored repo config metadata.");
|
|
1666
|
+
}
|
|
1667
|
+
if (!row) {
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
if (!isSupportedSchemaVersion(row.schema_version)) {
|
|
1671
|
+
throw new AgentLoopError(
|
|
1672
|
+
"storage_schema_mismatch",
|
|
1673
|
+
`Stored repo config schema version ${row.schema_version} is not supported.`,
|
|
1674
|
+
{ details: { expected: STORAGE_SCHEMA_VERSION, actual: row.schema_version } }
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
const parsed = parseJson(row.config_json, "Stored repo config snapshot JSON is invalid.");
|
|
1678
|
+
if (parsed.schemaVersion === STORAGE_SCHEMA_VERSION) {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (rewrite && isSupportedSchemaVersion(parsed.schemaVersion ?? 0) && typeof parsed.repoId === "string") {
|
|
1682
|
+
this.writeRepoConfig(withConfigDefaults(parsed));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
throw new AgentLoopError("storage_error", "Stored repo config snapshot schemaVersion is invalid.", {
|
|
1686
|
+
details: { expected: STORAGE_SCHEMA_VERSION, actual: parsed.schemaVersion }
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
getUserVersion() {
|
|
1690
|
+
const row = this.db.prepare("PRAGMA user_version").get();
|
|
1691
|
+
return row.user_version;
|
|
1692
|
+
}
|
|
1693
|
+
getRun(runId) {
|
|
1694
|
+
const row = this.db.prepare(
|
|
1695
|
+
`select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
|
|
1696
|
+
from runs
|
|
1697
|
+
where id = ?`
|
|
1698
|
+
).get(runId);
|
|
1699
|
+
if (!row) {
|
|
1700
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1701
|
+
}
|
|
1702
|
+
return fromRunRow(row);
|
|
1703
|
+
}
|
|
1704
|
+
getActiveRun() {
|
|
1705
|
+
const row = this.db.prepare(
|
|
1706
|
+
`select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
|
|
1707
|
+
from runs
|
|
1708
|
+
where status = 'RUNNING'
|
|
1709
|
+
order by updated_at desc
|
|
1710
|
+
limit 1`
|
|
1711
|
+
).get();
|
|
1712
|
+
return row ? fromRunRow(row) : void 0;
|
|
1713
|
+
}
|
|
1714
|
+
getWorker(workerId) {
|
|
1715
|
+
const row = this.db.prepare(
|
|
1716
|
+
`select id, run_id, type, backend, status, thread_id, attempt, resume_used,
|
|
1717
|
+
started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
|
|
1718
|
+
from workers
|
|
1719
|
+
where id = ?`
|
|
1720
|
+
).get(workerId);
|
|
1721
|
+
if (!row) {
|
|
1722
|
+
throw new AgentLoopError("storage_error", `Worker not found: ${workerId}`);
|
|
1723
|
+
}
|
|
1724
|
+
return fromWorkerRow(row);
|
|
1725
|
+
}
|
|
1726
|
+
timelineEntry(row) {
|
|
1727
|
+
if (!isTimelineSource(row.source)) {
|
|
1728
|
+
return void 0;
|
|
1729
|
+
}
|
|
1730
|
+
if (row.source === "event") {
|
|
1731
|
+
const sourceRow2 = this.db.prepare(
|
|
1732
|
+
`select seq, id, run_id, kind, message, artifact_ids_json, created_at
|
|
1733
|
+
from events where id = ?`
|
|
1734
|
+
).get(row.source_id);
|
|
1735
|
+
if (!sourceRow2) return void 0;
|
|
1736
|
+
const artifactIds = sourceRow2.artifact_ids_json ? parseJson(sourceRow2.artifact_ids_json, "Stored event artifact list JSON is invalid.") : void 0;
|
|
1737
|
+
return timelineEntry(row, {
|
|
1738
|
+
kind: sourceRow2.kind,
|
|
1739
|
+
title: sourceRow2.kind,
|
|
1740
|
+
summary: sourceRow2.message,
|
|
1741
|
+
...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
|
|
1742
|
+
...artifactIds ? { artifactIds } : {},
|
|
1743
|
+
rawRef: { table: "events", id: sourceRow2.id, seq: sourceRow2.seq }
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
if (row.source === "worker_event") {
|
|
1747
|
+
const sourceRow2 = this.db.prepare(
|
|
1748
|
+
`select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
|
|
1749
|
+
thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
|
|
1750
|
+
from worker_events where id = ?`
|
|
1751
|
+
).get(row.source_id);
|
|
1752
|
+
if (!sourceRow2) return void 0;
|
|
1753
|
+
const worker = this.db.prepare("select thread_id from workers where id = ?").get(sourceRow2.worker_id);
|
|
1754
|
+
const summary = sourceRow2.summary_json ? summarizeTimelinePayload(parseJson(sourceRow2.summary_json, "Stored worker event summary JSON is invalid.")) : sourceRow2.event_type;
|
|
1755
|
+
const artifactIds = sourceRow2.artifact_ids_json ? parseJson(sourceRow2.artifact_ids_json, "Stored worker event artifact list JSON is invalid.") : void 0;
|
|
1756
|
+
return timelineEntry(row, {
|
|
1757
|
+
kind: sourceRow2.item_type ?? sourceRow2.event_type,
|
|
1758
|
+
title: workerEventTimelineTitle(sourceRow2),
|
|
1759
|
+
summary,
|
|
1760
|
+
runId: sourceRow2.run_id,
|
|
1761
|
+
workerId: sourceRow2.worker_id,
|
|
1762
|
+
...sourceRow2.thread_id ? { threadId: sourceRow2.thread_id } : worker?.thread_id ? { threadId: worker.thread_id } : {},
|
|
1763
|
+
...sourceRow2.item_status ? { status: sourceRow2.item_status } : {},
|
|
1764
|
+
...artifactIds?.length ? { artifactIds } : {},
|
|
1765
|
+
rawRef: { table: "worker_events", id: sourceRow2.id, seq: sourceRow2.seq }
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
if (row.source === "worker") {
|
|
1769
|
+
const sourceRow2 = this.db.prepare(
|
|
1770
|
+
`select id, run_id, type, backend, status, thread_id, attempt, resume_used,
|
|
1771
|
+
started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
|
|
1772
|
+
from workers where id = ?`
|
|
1773
|
+
).get(row.worker_id ?? workerIdFromSourceId(row.source_id));
|
|
1774
|
+
if (!sourceRow2) return void 0;
|
|
1775
|
+
const status = statusFromWorkerSourceId(row.source_id) ?? sourceRow2.status;
|
|
1776
|
+
return timelineEntry(row, {
|
|
1777
|
+
kind: sourceRow2.type,
|
|
1778
|
+
title: `${sourceRow2.type} worker ${status}`,
|
|
1779
|
+
summary: summarizeTimelinePayload({
|
|
1780
|
+
status,
|
|
1781
|
+
attempt: sourceRow2.attempt,
|
|
1782
|
+
backend: sourceRow2.backend,
|
|
1783
|
+
exitCode: sourceRow2.exit_code,
|
|
1784
|
+
error: sourceRow2.error
|
|
1785
|
+
}),
|
|
1786
|
+
runId: sourceRow2.run_id,
|
|
1787
|
+
workerId: sourceRow2.id,
|
|
1788
|
+
...sourceRow2.thread_id ? { threadId: sourceRow2.thread_id } : {},
|
|
1789
|
+
status,
|
|
1790
|
+
artifactIds: [sourceRow2.result_artifact_id, sourceRow2.raw_jsonl_artifact_id].filter((id) => Boolean(id)),
|
|
1791
|
+
rawRef: { table: "workers", id: row.source_id }
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
if (row.source === "state") {
|
|
1795
|
+
const sourceRow2 = this.db.prepare("select id, run_id, status, state, version, created_at from states where id = ?").get(Number(row.source_id));
|
|
1796
|
+
if (!sourceRow2) return void 0;
|
|
1797
|
+
return timelineEntry(row, {
|
|
1798
|
+
kind: sourceRow2.state ?? sourceRow2.status,
|
|
1799
|
+
title: "State changed",
|
|
1800
|
+
summary: summarizeTimelinePayload({ status: sourceRow2.status, state: sourceRow2.state, version: sourceRow2.version }),
|
|
1801
|
+
...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
|
|
1802
|
+
status: sourceRow2.status,
|
|
1803
|
+
rawRef: { table: "states", id: String(sourceRow2.id), seq: sourceRow2.id }
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
if (row.source === "gate") {
|
|
1807
|
+
const sourceRow2 = this.db.prepare(
|
|
1808
|
+
`select id, run_id, kind, status, message, details_json, created_at,
|
|
1809
|
+
resolved_at, decision_note, decided_at
|
|
1810
|
+
from gates where id = ?`
|
|
1811
|
+
).get(row.source_id);
|
|
1812
|
+
if (!sourceRow2) return void 0;
|
|
1813
|
+
return timelineEntry(row, {
|
|
1814
|
+
kind: sourceRow2.kind,
|
|
1815
|
+
title: `Gate opened: ${sourceRow2.kind}`,
|
|
1816
|
+
summary: sourceRow2.message,
|
|
1817
|
+
...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
|
|
1818
|
+
status: sourceRow2.status,
|
|
1819
|
+
rawRef: { table: "gates", id: sourceRow2.id }
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
if (row.source === "artifact") {
|
|
1823
|
+
const sourceRow2 = this.db.prepare("select id, run_id, kind, name, path, sha256, created_at from artifacts where id = ?").get(row.source_id);
|
|
1824
|
+
if (!sourceRow2) return void 0;
|
|
1825
|
+
return timelineEntry(row, {
|
|
1826
|
+
kind: sourceRow2.kind,
|
|
1827
|
+
title: `Artifact: ${sourceRow2.name ?? sourceRow2.id}`,
|
|
1828
|
+
summary: summarizeTimelinePayload({ name: sourceRow2.name ?? sourceRow2.id, kind: sourceRow2.kind, sha256: sourceRow2.sha256 }),
|
|
1829
|
+
runId: sourceRow2.run_id,
|
|
1830
|
+
artifactIds: [sourceRow2.id],
|
|
1831
|
+
rawRef: { table: "artifacts", id: sourceRow2.id }
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
const sourceRow = this.db.prepare("select id, run_id, kind, message, created_at from decisions where id = ?").get(row.source_id);
|
|
1835
|
+
if (!sourceRow) return void 0;
|
|
1836
|
+
return timelineEntry(row, {
|
|
1837
|
+
kind: sourceRow.kind,
|
|
1838
|
+
title: sourceRow.kind,
|
|
1839
|
+
summary: sourceRow.message,
|
|
1840
|
+
runId: sourceRow.run_id,
|
|
1841
|
+
rawRef: { table: "decisions", id: sourceRow.id }
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
transaction(fn) {
|
|
1845
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1846
|
+
try {
|
|
1847
|
+
const result = fn();
|
|
1848
|
+
this.db.exec("COMMIT");
|
|
1849
|
+
return result;
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
try {
|
|
1852
|
+
this.db.exec("ROLLBACK");
|
|
1853
|
+
} catch (rollbackError) {
|
|
1854
|
+
throw new AgentLoopError("storage_error", "Transaction rollback failed.", {
|
|
1855
|
+
details: {
|
|
1856
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
1857
|
+
rollback: rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
throw error;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
function timelineEntry(row, entry) {
|
|
1866
|
+
return {
|
|
1867
|
+
timelineSeq: row.timeline_seq,
|
|
1868
|
+
occurredAt: row.created_at,
|
|
1869
|
+
cursor: encodeTimelineCursor(row.timeline_seq, row.created_at),
|
|
1870
|
+
source: row.source,
|
|
1871
|
+
kind: entry.kind,
|
|
1872
|
+
...entry.runId ? { runId: entry.runId } : {},
|
|
1873
|
+
...entry.workerId ? { workerId: entry.workerId } : {},
|
|
1874
|
+
...entry.threadId ? { threadId: entry.threadId } : {},
|
|
1875
|
+
title: truncateTimelineText(redactTimelineText(entry.title), 160),
|
|
1876
|
+
summary: truncateTimelineText(redactTimelineText(entry.summary), 1e3),
|
|
1877
|
+
...entry.status ? { status: entry.status } : {},
|
|
1878
|
+
...entry.artifactIds?.length ? { artifactIds: entry.artifactIds } : {},
|
|
1879
|
+
createdAt: row.created_at,
|
|
1880
|
+
rawRef: entry.rawRef
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function backfillTimelineIndex(db) {
|
|
1884
|
+
db.exec(`
|
|
1885
|
+
insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
|
|
1886
|
+
select source, source_id, source_seq, run_id, worker_id, created_at
|
|
1887
|
+
from (
|
|
1888
|
+
select 'event' as source, id as source_id, seq as source_seq, run_id, null as worker_id, created_at
|
|
1889
|
+
from events
|
|
1890
|
+
union all
|
|
1891
|
+
select 'worker_event' as source, id as source_id, seq as source_seq, run_id, worker_id, created_at
|
|
1892
|
+
from worker_events
|
|
1893
|
+
union all
|
|
1894
|
+
select 'worker' as source, id || ':' || status as source_id, null as source_seq, run_id, id as worker_id, started_at as created_at
|
|
1895
|
+
from workers
|
|
1896
|
+
union all
|
|
1897
|
+
select 'state' as source, cast(id as text) as source_id, id as source_seq, run_id, null as worker_id, created_at
|
|
1898
|
+
from states
|
|
1899
|
+
union all
|
|
1900
|
+
select 'gate' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
|
|
1901
|
+
from gates
|
|
1902
|
+
union all
|
|
1903
|
+
select 'artifact' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
|
|
1904
|
+
from artifacts
|
|
1905
|
+
union all
|
|
1906
|
+
select 'decision' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
|
|
1907
|
+
from decisions
|
|
1908
|
+
)
|
|
1909
|
+
order by created_at asc, source asc, source_id asc;
|
|
1910
|
+
`);
|
|
1911
|
+
}
|
|
1912
|
+
function normalizeTimelineSources(sources) {
|
|
1913
|
+
const unique = [...new Set(sources)];
|
|
1914
|
+
if (unique.some((source) => !isTimelineSource(source))) {
|
|
1915
|
+
throw new AgentLoopError("invalid_config", "Unsupported timeline source.", { details: { sources } });
|
|
1916
|
+
}
|
|
1917
|
+
return unique;
|
|
1918
|
+
}
|
|
1919
|
+
function isTimelineSource(value) {
|
|
1920
|
+
return TIMELINE_SOURCES.includes(value);
|
|
1921
|
+
}
|
|
1922
|
+
function clampLimit(value) {
|
|
1923
|
+
if (!Number.isFinite(value)) {
|
|
1924
|
+
return 50;
|
|
1925
|
+
}
|
|
1926
|
+
return Math.min(Math.max(Math.trunc(value), 1), 200);
|
|
1927
|
+
}
|
|
1928
|
+
function encodeTimelineCursor(timelineSeq, occurredAt) {
|
|
1929
|
+
return Buffer.from(JSON.stringify({ timelineSeq, ...occurredAt ? { occurredAt } : {} }), "utf8").toString("base64url");
|
|
1930
|
+
}
|
|
1931
|
+
function decodeTimelineCursor(cursor) {
|
|
1932
|
+
try {
|
|
1933
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
1934
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1935
|
+
const timelineSeq = parsed.timelineSeq;
|
|
1936
|
+
const occurredAt = parsed.occurredAt;
|
|
1937
|
+
if (typeof timelineSeq === "number" && Number.isInteger(timelineSeq) && timelineSeq > 0 && typeof occurredAt === "string" && occurredAt.length > 0) {
|
|
1938
|
+
return { timelineSeq, occurredAt };
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
} catch {
|
|
1942
|
+
}
|
|
1943
|
+
throw new AgentLoopError("invalid_config", "Timeline cursor is invalid.");
|
|
1944
|
+
}
|
|
1945
|
+
function timelineMissingSourceRows(db) {
|
|
1946
|
+
const checks = [
|
|
1947
|
+
{
|
|
1948
|
+
source: "event",
|
|
1949
|
+
sql: `select count(*) as count
|
|
1950
|
+
from events source
|
|
1951
|
+
left join timeline_index ti on ti.source = 'event' and ti.source_id = source.id
|
|
1952
|
+
where ti.timeline_seq is null`
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
source: "worker_event",
|
|
1956
|
+
sql: `select count(*) as count
|
|
1957
|
+
from worker_events source
|
|
1958
|
+
left join timeline_index ti on ti.source = 'worker_event' and ti.source_id = source.id
|
|
1959
|
+
where ti.timeline_seq is null`
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
source: "worker",
|
|
1963
|
+
sql: `select count(*) as count
|
|
1964
|
+
from workers source
|
|
1965
|
+
left join timeline_index ti on ti.source = 'worker' and ti.source_id = source.id || ':' || source.status
|
|
1966
|
+
where ti.timeline_seq is null`
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
source: "state",
|
|
1970
|
+
sql: `select count(*) as count
|
|
1971
|
+
from states source
|
|
1972
|
+
left join timeline_index ti on ti.source = 'state' and ti.source_id = cast(source.id as text)
|
|
1973
|
+
where ti.timeline_seq is null`
|
|
1974
|
+
},
|
|
1975
|
+
{
|
|
1976
|
+
source: "gate",
|
|
1977
|
+
sql: `select count(*) as count
|
|
1978
|
+
from gates source
|
|
1979
|
+
left join timeline_index ti on ti.source = 'gate' and ti.source_id = source.id
|
|
1980
|
+
where ti.timeline_seq is null`
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
source: "artifact",
|
|
1984
|
+
sql: `select count(*) as count
|
|
1985
|
+
from artifacts source
|
|
1986
|
+
left join timeline_index ti on ti.source = 'artifact' and ti.source_id = source.id
|
|
1987
|
+
where ti.timeline_seq is null`
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
source: "decision",
|
|
1991
|
+
sql: `select count(*) as count
|
|
1992
|
+
from decisions source
|
|
1993
|
+
left join timeline_index ti on ti.source = 'decision' and ti.source_id = source.id
|
|
1994
|
+
where ti.timeline_seq is null`
|
|
1995
|
+
}
|
|
1996
|
+
];
|
|
1997
|
+
return checks.flatMap((check) => {
|
|
1998
|
+
const row = db.prepare(check.sql).get();
|
|
1999
|
+
const missing = row?.count ?? 0;
|
|
2000
|
+
return missing > 0 ? [{ source: check.source, missing }] : [];
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
function summarizeTimelinePayload(value) {
|
|
2004
|
+
if (typeof value === "string") {
|
|
2005
|
+
return value;
|
|
2006
|
+
}
|
|
2007
|
+
if (value === void 0 || value === null) {
|
|
2008
|
+
return "";
|
|
2009
|
+
}
|
|
2010
|
+
return JSON.stringify(redactTimelineValue(value));
|
|
2011
|
+
}
|
|
2012
|
+
function redactTimelineValue(value) {
|
|
2013
|
+
if (Array.isArray(value)) {
|
|
2014
|
+
return value.slice(0, 20).map(redactTimelineValue);
|
|
2015
|
+
}
|
|
2016
|
+
if (typeof value !== "object" || value === null) {
|
|
2017
|
+
return value;
|
|
2018
|
+
}
|
|
2019
|
+
const redacted = {};
|
|
2020
|
+
for (const [key, nested] of Object.entries(value).slice(0, 40)) {
|
|
2021
|
+
redacted[key] = isSecretKey(key) ? "[redacted]" : redactTimelineValue(nested);
|
|
2022
|
+
}
|
|
2023
|
+
return redacted;
|
|
2024
|
+
}
|
|
2025
|
+
function redactTimelineText(value) {
|
|
2026
|
+
return redactSecrets(value);
|
|
2027
|
+
}
|
|
2028
|
+
function truncateTimelineText(value, maxLength) {
|
|
2029
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
|
|
2030
|
+
}
|
|
2031
|
+
function statusFromWorkerSourceId(sourceId) {
|
|
2032
|
+
const status = sourceId.split(":").at(-1);
|
|
2033
|
+
return status && ["running", "succeeded", "failed", "timed_out", "invalid_output"].includes(status) ? status : void 0;
|
|
2034
|
+
}
|
|
2035
|
+
function workerIdFromSourceId(sourceId) {
|
|
2036
|
+
return sourceId.split(":")[0] ?? sourceId;
|
|
2037
|
+
}
|
|
2038
|
+
function fromRunRow(row) {
|
|
2039
|
+
return {
|
|
2040
|
+
id: row.id,
|
|
2041
|
+
status: row.status,
|
|
2042
|
+
...row.current_state ? { currentState: row.current_state } : {},
|
|
2043
|
+
version: row.version,
|
|
2044
|
+
...row.branch ? { branch: row.branch } : {},
|
|
2045
|
+
...row.worktree_clean !== null ? { worktreeClean: row.worktree_clean === 1 } : {},
|
|
2046
|
+
createdAt: row.created_at,
|
|
2047
|
+
updatedAt: row.updated_at,
|
|
2048
|
+
...row.started_at ? { startedAt: row.started_at } : {},
|
|
2049
|
+
...row.stopped_at ? { stoppedAt: row.stopped_at } : {}
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function fromEventRow(row) {
|
|
2053
|
+
return {
|
|
2054
|
+
id: row.id,
|
|
2055
|
+
seq: row.seq,
|
|
2056
|
+
...row.run_id ? { runId: row.run_id } : {},
|
|
2057
|
+
kind: row.kind,
|
|
2058
|
+
message: row.message,
|
|
2059
|
+
...row.state_before ? { stateBefore: row.state_before } : {},
|
|
2060
|
+
...row.state_after ? { stateAfter: row.state_after } : {},
|
|
2061
|
+
...row.payload_json ? { payload: parseJson(row.payload_json, "Stored event payload JSON is invalid.") } : {},
|
|
2062
|
+
...row.artifact_ids_json ? { artifactIds: parseJson(row.artifact_ids_json, "Stored event artifact list JSON is invalid.") } : {},
|
|
2063
|
+
createdAt: row.created_at
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
function statusGateFromRow(row) {
|
|
2067
|
+
return {
|
|
2068
|
+
kind: row.kind,
|
|
2069
|
+
message: row.message,
|
|
2070
|
+
...row.details_json ? { details: parseJson(row.details_json, "Stored gate details JSON is invalid.") } : {}
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
function latestGateSatisfied(db, runId) {
|
|
2074
|
+
const row = db.prepare(
|
|
2075
|
+
`select status
|
|
2076
|
+
from gates
|
|
2077
|
+
where run_id = ?
|
|
2078
|
+
order by created_at desc
|
|
2079
|
+
limit 1`
|
|
2080
|
+
).get(runId);
|
|
2081
|
+
return row?.status === "approved" || row?.status === "resolved";
|
|
2082
|
+
}
|
|
2083
|
+
function fromGateRow(row) {
|
|
2084
|
+
return {
|
|
2085
|
+
id: row.id,
|
|
2086
|
+
...row.run_id ? { runId: row.run_id } : {},
|
|
2087
|
+
kind: row.kind,
|
|
2088
|
+
status: row.status,
|
|
2089
|
+
message: row.message,
|
|
2090
|
+
...row.details_json ? { details: parseJson(row.details_json, "Stored gate details JSON is invalid.") } : {},
|
|
2091
|
+
createdAt: row.created_at,
|
|
2092
|
+
...row.resolved_at ? { resolvedAt: row.resolved_at } : {},
|
|
2093
|
+
...row.decision_note ? { decisionNote: row.decision_note } : {},
|
|
2094
|
+
...row.decided_at ? { decidedAt: row.decided_at } : {}
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
function fromArtifactRow(row) {
|
|
2098
|
+
return {
|
|
2099
|
+
id: row.id,
|
|
2100
|
+
runId: row.run_id,
|
|
2101
|
+
kind: row.kind,
|
|
2102
|
+
name: row.name ?? row.id,
|
|
2103
|
+
path: row.path,
|
|
2104
|
+
sha256: row.sha256 ?? "",
|
|
2105
|
+
createdAt: row.created_at
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function fromPrLinkRow(row) {
|
|
2109
|
+
return {
|
|
2110
|
+
id: row.id,
|
|
2111
|
+
runId: row.run_id,
|
|
2112
|
+
branch: row.branch,
|
|
2113
|
+
prNumber: row.pr_number,
|
|
2114
|
+
url: row.url,
|
|
2115
|
+
headRef: row.head_ref,
|
|
2116
|
+
baseRef: row.base_ref,
|
|
2117
|
+
state: row.state,
|
|
2118
|
+
draft: row.draft === 1,
|
|
2119
|
+
createdAt: row.created_at,
|
|
2120
|
+
updatedAt: row.updated_at
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
function fromCiCheckRow(row) {
|
|
2124
|
+
return {
|
|
2125
|
+
id: row.id,
|
|
2126
|
+
runId: row.run_id,
|
|
2127
|
+
prNumber: row.pr_number,
|
|
2128
|
+
name: row.name,
|
|
2129
|
+
status: row.status,
|
|
2130
|
+
...row.conclusion ? { conclusion: row.conclusion } : {},
|
|
2131
|
+
...row.url ? { url: row.url } : {},
|
|
2132
|
+
...row.started_at ? { startedAt: row.started_at } : {},
|
|
2133
|
+
...row.completed_at ? { completedAt: row.completed_at } : {},
|
|
2134
|
+
observedAt: row.observed_at
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function fromReviewCommentRow(row) {
|
|
2138
|
+
return {
|
|
2139
|
+
id: row.id,
|
|
2140
|
+
runId: row.run_id,
|
|
2141
|
+
prNumber: row.pr_number,
|
|
2142
|
+
commentId: row.comment_id,
|
|
2143
|
+
url: row.url,
|
|
2144
|
+
author: row.author,
|
|
2145
|
+
body: row.body,
|
|
2146
|
+
path: row.path,
|
|
2147
|
+
...row.line === null ? {} : { line: row.line },
|
|
2148
|
+
diffHunk: row.diff_hunk,
|
|
2149
|
+
isResolved: row.is_resolved === 1,
|
|
2150
|
+
isOutdated: row.is_outdated === 1,
|
|
2151
|
+
actionable: row.actionable === 1,
|
|
2152
|
+
status: row.status,
|
|
2153
|
+
observedAt: row.observed_at
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
function fromDecisionRow(row) {
|
|
2157
|
+
return {
|
|
2158
|
+
id: row.id,
|
|
2159
|
+
runId: row.run_id,
|
|
2160
|
+
kind: row.kind,
|
|
2161
|
+
message: row.message,
|
|
2162
|
+
...row.details_json ? { details: parseJson(row.details_json, "Stored decision details JSON is invalid.") } : {},
|
|
2163
|
+
createdAt: row.created_at
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function fromRunCheckRow(row) {
|
|
2167
|
+
return {
|
|
2168
|
+
runId: row.run_id,
|
|
2169
|
+
kind: row.kind,
|
|
2170
|
+
status: row.status,
|
|
2171
|
+
...row.details_json ? { details: JSON.parse(row.details_json) } : {},
|
|
2172
|
+
createdAt: row.created_at
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
function fromWorkerRow(row) {
|
|
2176
|
+
return {
|
|
2177
|
+
id: row.id,
|
|
2178
|
+
runId: row.run_id,
|
|
2179
|
+
type: row.type,
|
|
2180
|
+
backend: row.backend,
|
|
2181
|
+
status: row.status,
|
|
2182
|
+
...row.thread_id ? { threadId: row.thread_id } : {},
|
|
2183
|
+
attempt: row.attempt,
|
|
2184
|
+
resumeUsed: row.resume_used === 1,
|
|
2185
|
+
startedAt: row.started_at,
|
|
2186
|
+
...row.completed_at ? { completedAt: row.completed_at } : {},
|
|
2187
|
+
...row.exit_code === null ? {} : { exitCode: row.exit_code },
|
|
2188
|
+
...row.result_artifact_id ? { resultArtifactId: row.result_artifact_id } : {},
|
|
2189
|
+
...row.raw_jsonl_artifact_id ? { rawJsonlArtifactId: row.raw_jsonl_artifact_id } : {},
|
|
2190
|
+
...row.error ? { error: row.error } : {}
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
function fromWorkerEventRow(row) {
|
|
2194
|
+
return {
|
|
2195
|
+
id: row.id,
|
|
2196
|
+
seq: row.seq,
|
|
2197
|
+
workerId: row.worker_id,
|
|
2198
|
+
runId: row.run_id,
|
|
2199
|
+
eventType: row.event_type,
|
|
2200
|
+
...row.item_type ? { itemType: row.item_type } : {},
|
|
2201
|
+
...row.item_id ? { itemId: row.item_id } : {},
|
|
2202
|
+
...row.item_status ? { itemStatus: row.item_status } : {},
|
|
2203
|
+
...row.thread_id ? { threadId: row.thread_id } : {},
|
|
2204
|
+
...row.backend ? { backend: row.backend } : {},
|
|
2205
|
+
...row.summary_json ? { summary: parseJson(row.summary_json, "Stored worker event summary JSON is invalid.") } : {},
|
|
2206
|
+
...row.usage_json ? { usage: parseJson(row.usage_json, "Stored worker event usage JSON is invalid.") } : {},
|
|
2207
|
+
...row.artifact_ids_json ? { artifactIds: parseJson(row.artifact_ids_json, "Stored worker event artifact list JSON is invalid.") } : {},
|
|
2208
|
+
createdAt: row.created_at
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
function workerEventTimelineTitle(row) {
|
|
2212
|
+
const item = row.item_type ?? row.event_type;
|
|
2213
|
+
return row.item_status ? `${row.item_status} ${item}` : item;
|
|
2214
|
+
}
|
|
2215
|
+
function isSupportedSchemaVersion(value) {
|
|
2216
|
+
return SUPPORTED_SCHEMA_VERSIONS.includes(value);
|
|
2217
|
+
}
|
|
2218
|
+
function rebuildEventsWithSeq(db) {
|
|
2219
|
+
if (hasColumn(db, "events", "seq")) {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
db.exec(`
|
|
2223
|
+
alter table events rename to events_legacy_v6;
|
|
2224
|
+
create table events (
|
|
2225
|
+
seq integer primary key autoincrement,
|
|
2226
|
+
id text not null unique,
|
|
2227
|
+
run_id text,
|
|
2228
|
+
kind text not null,
|
|
2229
|
+
message text not null,
|
|
2230
|
+
state_before text,
|
|
2231
|
+
state_after text,
|
|
2232
|
+
payload_json text,
|
|
2233
|
+
artifact_ids_json text,
|
|
2234
|
+
created_at text not null,
|
|
2235
|
+
foreign key(run_id) references runs(id)
|
|
2236
|
+
);
|
|
2237
|
+
insert into events (
|
|
2238
|
+
id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
|
|
2239
|
+
)
|
|
2240
|
+
select id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
|
|
2241
|
+
from events_legacy_v6
|
|
2242
|
+
order by created_at asc, id asc;
|
|
2243
|
+
drop table events_legacy_v6;
|
|
2244
|
+
`);
|
|
2245
|
+
}
|
|
2246
|
+
function rebuildWorkerEventsWithSeq(db) {
|
|
2247
|
+
if (hasColumn(db, "worker_events", "seq")) {
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
db.exec(`
|
|
2251
|
+
alter table worker_events rename to worker_events_legacy_v6;
|
|
2252
|
+
create table worker_events (
|
|
2253
|
+
seq integer primary key autoincrement,
|
|
2254
|
+
id text not null unique,
|
|
2255
|
+
worker_id text not null,
|
|
2256
|
+
run_id text not null,
|
|
2257
|
+
event_type text not null,
|
|
2258
|
+
item_type text,
|
|
2259
|
+
summary_json text,
|
|
2260
|
+
usage_json text,
|
|
2261
|
+
created_at text not null,
|
|
2262
|
+
foreign key(worker_id) references workers(id),
|
|
2263
|
+
foreign key(run_id) references runs(id)
|
|
2264
|
+
);
|
|
2265
|
+
insert into worker_events (
|
|
2266
|
+
id, worker_id, run_id, event_type, item_type, summary_json, usage_json, created_at
|
|
2267
|
+
)
|
|
2268
|
+
select id, worker_id, run_id, event_type, item_type, summary_json, usage_json, created_at
|
|
2269
|
+
from worker_events_legacy_v6
|
|
2270
|
+
order by created_at asc, id asc;
|
|
2271
|
+
drop table worker_events_legacy_v6;
|
|
2272
|
+
`);
|
|
2273
|
+
}
|
|
2274
|
+
function dedupeHighFidelityWorkerEventsV8(db) {
|
|
2275
|
+
db.exec(`
|
|
2276
|
+
create temp table if not exists worker_event_dedupe_ids (
|
|
2277
|
+
id text primary key
|
|
2278
|
+
);
|
|
2279
|
+
delete from worker_event_dedupe_ids;
|
|
2280
|
+
insert or ignore into worker_event_dedupe_ids (id)
|
|
2281
|
+
select id from (
|
|
2282
|
+
select id from (
|
|
2283
|
+
select id,
|
|
2284
|
+
seq,
|
|
2285
|
+
row_number() over (
|
|
2286
|
+
partition by thread_id, item_id, coalesce(item_status, '')
|
|
2287
|
+
order by seq asc
|
|
2288
|
+
) as duplicate_rank
|
|
2289
|
+
from worker_events
|
|
2290
|
+
where thread_id is not null and item_id is not null
|
|
2291
|
+
)
|
|
2292
|
+
where duplicate_rank > 1
|
|
2293
|
+
);
|
|
2294
|
+
insert or ignore into worker_event_dedupe_ids (id)
|
|
2295
|
+
select id from (
|
|
2296
|
+
select id from (
|
|
2297
|
+
select id,
|
|
2298
|
+
seq,
|
|
2299
|
+
row_number() over (
|
|
2300
|
+
partition by thread_id, event_type
|
|
2301
|
+
order by seq asc
|
|
2302
|
+
) as duplicate_rank
|
|
2303
|
+
from worker_events
|
|
2304
|
+
where thread_id is not null and item_id is null
|
|
2305
|
+
)
|
|
2306
|
+
where duplicate_rank > 1
|
|
2307
|
+
);
|
|
2308
|
+
delete from timeline_index
|
|
2309
|
+
where source = 'worker_event'
|
|
2310
|
+
and source_id in (select id from worker_event_dedupe_ids);
|
|
2311
|
+
delete from worker_events
|
|
2312
|
+
where id in (select id from worker_event_dedupe_ids);
|
|
2313
|
+
delete from worker_event_dedupe_ids;
|
|
2314
|
+
`);
|
|
2315
|
+
}
|
|
2316
|
+
function hasColumn(db, tableName, columnName) {
|
|
2317
|
+
validateSqlIdentifier(tableName);
|
|
2318
|
+
validateSqlIdentifier(columnName);
|
|
2319
|
+
const columns = db.prepare(`pragma table_info(${tableName})`).all();
|
|
2320
|
+
return columns.some((column) => column.name === columnName);
|
|
2321
|
+
}
|
|
2322
|
+
function hasTable(db, tableName) {
|
|
2323
|
+
validateSqlIdentifier(tableName);
|
|
2324
|
+
const row = db.prepare("select 1 from sqlite_master where type = 'table' and name = ? limit 1").get(tableName);
|
|
2325
|
+
return row !== void 0;
|
|
2326
|
+
}
|
|
2327
|
+
function boolToDb(value) {
|
|
2328
|
+
if (value === void 0) {
|
|
2329
|
+
return null;
|
|
2330
|
+
}
|
|
2331
|
+
return value ? 1 : 0;
|
|
2332
|
+
}
|
|
2333
|
+
function addColumnIfMissing(db, tableName, columnName, definition) {
|
|
2334
|
+
validateSqlIdentifier(tableName);
|
|
2335
|
+
validateSqlIdentifier(columnName);
|
|
2336
|
+
if (!hasColumn(db, tableName, columnName)) {
|
|
2337
|
+
db.exec(`alter table ${tableName} add column ${columnName} ${definition}`);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
function validateSqlIdentifier(value) {
|
|
2341
|
+
if (!/^[a-z0-9_]+$/.test(value)) {
|
|
2342
|
+
throw new AgentLoopError("storage_error", `Unsafe SQLite identifier: ${value}`);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
function now() {
|
|
2346
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2347
|
+
}
|
|
2348
|
+
function parseJson(value, message) {
|
|
2349
|
+
try {
|
|
2350
|
+
return JSON.parse(value);
|
|
2351
|
+
} catch (error) {
|
|
2352
|
+
throw new AgentLoopError("storage_error", message, {
|
|
2353
|
+
details: { cause: error instanceof Error ? error.message : String(error) }
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
function isUniqueConstraintError(error) {
|
|
2358
|
+
return error instanceof Error && /unique constraint/i.test(error.message);
|
|
2359
|
+
}
|
|
2360
|
+
function toStorageError(error, message) {
|
|
2361
|
+
if (error instanceof AgentLoopError) {
|
|
2362
|
+
return error;
|
|
2363
|
+
}
|
|
2364
|
+
return new AgentLoopError("storage_error", message, {
|
|
2365
|
+
details: { cause: error instanceof Error ? error.message : String(error) }
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// plugins/autonomous-pr-loop/core/hook-observer.ts
|
|
2370
|
+
function observeCodexHook(event, payload, repoRoot) {
|
|
2371
|
+
try {
|
|
2372
|
+
const route = resolveHookRoute(payload, { legacyRepoRoot: repoRoot });
|
|
2373
|
+
if (route.status === "no_match") {
|
|
2374
|
+
return { continue: true, observed: false };
|
|
2375
|
+
}
|
|
2376
|
+
if (route.status === "ambiguous") {
|
|
2377
|
+
return { continue: true, observed: false, error: route.reason };
|
|
2378
|
+
}
|
|
2379
|
+
if (route.status === "route_error") {
|
|
2380
|
+
return { continue: true, observed: false, error: route.reason };
|
|
2381
|
+
}
|
|
2382
|
+
const storage = new SqliteAgentLoopStorage(statePath(route.binding.repoRoot));
|
|
2383
|
+
try {
|
|
2384
|
+
const run = route.binding.runId ? storage.listRuns(200).find((item) => item.id === route.binding.runId) : storage.getCurrentRun();
|
|
2385
|
+
storage.appendEvent({
|
|
2386
|
+
...run ? { runId: run.id } : {},
|
|
2387
|
+
kind: hookEventKind(event),
|
|
2388
|
+
message: `Codex ${event} hook observed.`,
|
|
2389
|
+
payload: {
|
|
2390
|
+
...normalizeHookPayload(event, payload),
|
|
2391
|
+
hookRouting: route.legacy ? "legacy" : "binding",
|
|
2392
|
+
worktreeRoot: route.context.worktreeRoot
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
} finally {
|
|
2396
|
+
storage.close();
|
|
2397
|
+
}
|
|
2398
|
+
return { continue: true, observed: true };
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
return { continue: true, observed: false, error: error instanceof Error ? error.message : String(error) };
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
function normalizeHookPayload(event, payload) {
|
|
2404
|
+
const text = JSON.stringify(payload ?? {});
|
|
2405
|
+
const base = {
|
|
2406
|
+
event,
|
|
2407
|
+
payloadLength: text.length,
|
|
2408
|
+
payloadSha256: createHash2("sha256").update(text).digest("hex")
|
|
2409
|
+
};
|
|
2410
|
+
if (event === "UserPromptSubmit" || event === "PermissionRequest") {
|
|
2411
|
+
return { ...base, redacted: true };
|
|
2412
|
+
}
|
|
2413
|
+
if (!isRecord(payload)) {
|
|
2414
|
+
return base;
|
|
2415
|
+
}
|
|
2416
|
+
return {
|
|
2417
|
+
...base,
|
|
2418
|
+
redacted: true,
|
|
2419
|
+
toolName: stringValue2(payload.tool_name) ?? stringValue2(payload.toolName) ?? stringValue2(payload.tool),
|
|
2420
|
+
matcher: stringValue2(payload.matcher),
|
|
2421
|
+
sessionIdHash: hashOptional(stringValue2(payload.session_id) ?? stringValue2(payload.sessionId)),
|
|
2422
|
+
command: summarizeCommand(payload)
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
function summarizeCommand(payload) {
|
|
2426
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : payload;
|
|
2427
|
+
const command = stringValue2(toolInput.command) ?? stringValue2(toolInput.cmd) ?? stringValue2(toolInput.input);
|
|
2428
|
+
return command ? redactSecrets(command.slice(0, 500)) : void 0;
|
|
2429
|
+
}
|
|
2430
|
+
function stringValue2(value) {
|
|
2431
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2432
|
+
}
|
|
2433
|
+
function hashOptional(value) {
|
|
2434
|
+
return value ? createHash2("sha256").update(value).digest("hex") : void 0;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// plugins/autonomous-pr-loop/hooks/observe-runner.ts
|
|
2438
|
+
function runObserveOnlyHook(event) {
|
|
2439
|
+
const input = readStdinJson();
|
|
2440
|
+
const repoRoot = process.env.AGENT_LOOP_REPO_ROOT;
|
|
2441
|
+
const result = observeCodexHook(event, input, repoRoot);
|
|
2442
|
+
if (result.error) {
|
|
2443
|
+
process.stderr.write(`agent-loop ${event} observe failed: ${result.error}
|
|
2444
|
+
`);
|
|
2445
|
+
}
|
|
2446
|
+
process.stdout.write(`${JSON.stringify({ continue: true })}
|
|
2447
|
+
`);
|
|
2448
|
+
}
|
|
2449
|
+
function readStdinJson() {
|
|
2450
|
+
const text = readFileSync2(0, "utf8");
|
|
2451
|
+
if (text.trim().length === 0) {
|
|
2452
|
+
return {};
|
|
2453
|
+
}
|
|
2454
|
+
try {
|
|
2455
|
+
return JSON.parse(text);
|
|
2456
|
+
} catch {
|
|
2457
|
+
return { rawLength: text.length };
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// plugins/autonomous-pr-loop/hooks/user-prompt-submit.ts
|
|
2462
|
+
runObserveOnlyHook("UserPromptSubmit");
|