@kontextmind/kxm 0.6.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/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,1560 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { VnextConfigError, validateRunEvent, vnextCanonicalJson, type JsonValue, type VnextConfigIssue, type VnextConfigOptions } from "./vnext-config.ts";
|
|
6
|
+
import { vnextUserStateRoot } from "./vnext-bindings.ts";
|
|
7
|
+
|
|
8
|
+
/* ------------------------------------------------------------------ *
|
|
9
|
+
* Runtime registry (per-user, platform state root)
|
|
10
|
+
* ------------------------------------------------------------------ */
|
|
11
|
+
|
|
12
|
+
export interface VnextRuntimePaths {
|
|
13
|
+
stateRoot: string;
|
|
14
|
+
runtimeDir: string;
|
|
15
|
+
registryDb: string;
|
|
16
|
+
projectsDir: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function vnextRuntimePaths(options: { stateRoot?: string; env?: NodeJS.ProcessEnv; homeDir?: string } = {}): VnextRuntimePaths {
|
|
20
|
+
const stateRoot = options.stateRoot
|
|
21
|
+
? resolve(options.stateRoot)
|
|
22
|
+
: vnextUserStateRoot({ ...(options.env ? { env: options.env } : {}), ...(options.homeDir ? { homeDir: options.homeDir } : {}) });
|
|
23
|
+
const runtimeDir = join(stateRoot, "runtime");
|
|
24
|
+
return {
|
|
25
|
+
stateRoot,
|
|
26
|
+
runtimeDir,
|
|
27
|
+
registryDb: join(runtimeDir, "registry.db"),
|
|
28
|
+
projectsDir: join(runtimeDir, "projects"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runtimeIssue(phase: VnextConfigIssue["phase"], code: string, file: string, message: string): VnextConfigIssue {
|
|
33
|
+
return { phase, code, file, message };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function runtimeError(code: string, file: string, message: string): VnextConfigError {
|
|
37
|
+
return new VnextConfigError([runtimeIssue("semantic", code, file, message)]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function projectRuntimeKey(projectRoot: string): string {
|
|
41
|
+
// Canonicalize through the filesystem like the repository binding store so
|
|
42
|
+
// reaching a project through a link cannot mint a second key for it.
|
|
43
|
+
let canonical: string;
|
|
44
|
+
try {
|
|
45
|
+
canonical = realpathSync.native(resolve(projectRoot));
|
|
46
|
+
} catch {
|
|
47
|
+
canonical = resolve(projectRoot);
|
|
48
|
+
}
|
|
49
|
+
const folded = process.platform === "win32" ? canonical.toLocaleLowerCase("en-US") : canonical;
|
|
50
|
+
return createHash("sha256").update(folded, "utf8").digest("hex").slice(0, 24);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function checkedParent(path: string, description: string): void {
|
|
54
|
+
const parent = dirname(path);
|
|
55
|
+
if (!existsSync(parent)) mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
56
|
+
const stat = lstatSync(parent, { throwIfNoEntry: false });
|
|
57
|
+
if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
58
|
+
throw runtimeError("runtime_path_invalid", description, `${description} parent must be a regular directory, not a link`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
import {
|
|
63
|
+
openDatabase,
|
|
64
|
+
withDatabaseTransaction,
|
|
65
|
+
type DatabaseSchemaSpec,
|
|
66
|
+
} from "./database.ts";
|
|
67
|
+
|
|
68
|
+
export type VnextDatabaseSchema = DatabaseSchemaSpec;
|
|
69
|
+
export { openDatabase };
|
|
70
|
+
|
|
71
|
+
/* ----------------------------- registry ---------------------------- */
|
|
72
|
+
|
|
73
|
+
export interface VnextSupervisorRecord {
|
|
74
|
+
runtimeId: string;
|
|
75
|
+
pid: number;
|
|
76
|
+
port: number;
|
|
77
|
+
tokenHash: string;
|
|
78
|
+
startedAt: string;
|
|
79
|
+
heartbeatAt: string;
|
|
80
|
+
state: "starting" | "running" | "stopping" | "stopped";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface VnextProjectRegistration {
|
|
84
|
+
projectId: string;
|
|
85
|
+
projectRoot: string;
|
|
86
|
+
projectKey: string;
|
|
87
|
+
homeRuntimeId: string;
|
|
88
|
+
configRevision?: string;
|
|
89
|
+
registeredAt: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const VNEXT_REGISTRY_SCHEMA_VERSION = 1;
|
|
93
|
+
|
|
94
|
+
const REGISTRY_TABLES = {
|
|
95
|
+
supervisor: ["singleton_id", "runtime_id", "pid", "port", "token_hash", "started_at", "heartbeat_at", "state"],
|
|
96
|
+
projects: ["project_id", "project_root", "project_key", "home_runtime_id", "config_revision", "registered_at"],
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
const REGISTRY_SCHEMA = `
|
|
100
|
+
CREATE TABLE supervisor (
|
|
101
|
+
singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
|
|
102
|
+
runtime_id TEXT NOT NULL,
|
|
103
|
+
pid INTEGER NOT NULL,
|
|
104
|
+
port INTEGER NOT NULL,
|
|
105
|
+
token_hash TEXT NOT NULL,
|
|
106
|
+
started_at TEXT NOT NULL,
|
|
107
|
+
heartbeat_at TEXT NOT NULL,
|
|
108
|
+
state TEXT NOT NULL
|
|
109
|
+
) STRICT;
|
|
110
|
+
CREATE TABLE projects (
|
|
111
|
+
project_id TEXT PRIMARY KEY,
|
|
112
|
+
project_root TEXT NOT NULL,
|
|
113
|
+
project_key TEXT NOT NULL UNIQUE,
|
|
114
|
+
home_runtime_id TEXT NOT NULL,
|
|
115
|
+
config_revision TEXT,
|
|
116
|
+
registered_at TEXT NOT NULL
|
|
117
|
+
) STRICT;
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
export class VnextRuntimeRegistry {
|
|
121
|
+
readonly path: string;
|
|
122
|
+
private readonly database: DatabaseSync;
|
|
123
|
+
|
|
124
|
+
constructor(path: string) {
|
|
125
|
+
this.path = resolve(path);
|
|
126
|
+
this.database = openDatabase(this.path, "runtime registry", {
|
|
127
|
+
schema: REGISTRY_SCHEMA,
|
|
128
|
+
version: VNEXT_REGISTRY_SCHEMA_VERSION,
|
|
129
|
+
tables: REGISTRY_TABLES,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
close(): void {
|
|
134
|
+
this.database.close();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Atomically claim or refresh the supervisor singleton. Returns the record that now owns it. */
|
|
138
|
+
claimSupervisor(record: { runtimeId: string; pid: number; port: number; tokenHash: string; now: string }): { claimed: boolean; record: VnextSupervisorRecord } {
|
|
139
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
140
|
+
try {
|
|
141
|
+
const existing = this.readSupervisorRow();
|
|
142
|
+
if (!existing) {
|
|
143
|
+
this.database.prepare(`
|
|
144
|
+
INSERT INTO supervisor (singleton_id, runtime_id, pid, port, token_hash, started_at, heartbeat_at, state)
|
|
145
|
+
VALUES (1, ?, ?, ?, ?, ?, ?, 'running')
|
|
146
|
+
`).run(record.runtimeId, record.pid, record.port, record.tokenHash, record.now, record.now);
|
|
147
|
+
this.database.exec("COMMIT");
|
|
148
|
+
return { claimed: true, record: this.supervisor()! };
|
|
149
|
+
}
|
|
150
|
+
if (existing.runtimeId === record.runtimeId && existing.pid === record.pid) {
|
|
151
|
+
this.database.prepare("UPDATE supervisor SET heartbeat_at = ?, state = 'running' WHERE singleton_id = 1").run(record.now);
|
|
152
|
+
this.database.exec("COMMIT");
|
|
153
|
+
return { claimed: true, record: this.supervisor()! };
|
|
154
|
+
}
|
|
155
|
+
this.database.exec("ROLLBACK");
|
|
156
|
+
return { claimed: false, record: existing };
|
|
157
|
+
} catch (error) {
|
|
158
|
+
try { this.database.exec("ROLLBACK"); } catch { /* already rolled back */ }
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Take over a dead supervisor's singleton, preserving its logical runtime identity and project home bindings. */
|
|
164
|
+
takeoverSupervisor(record: { runtimeId: string; pid: number; port: number; tokenHash: string; now: string; observedDeadPid: number; observedHeartbeatAt: string }): VnextSupervisorRecord {
|
|
165
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
166
|
+
try {
|
|
167
|
+
// The logical runtime id is stable across process restarts: the new
|
|
168
|
+
// process inherits it so immutable homeRuntimeId bindings stay valid.
|
|
169
|
+
// The takeover applies only to the exact record we observed as dead.
|
|
170
|
+
const existing = this.readSupervisorRow();
|
|
171
|
+
const runtimeId = existing?.runtimeId ?? record.runtimeId;
|
|
172
|
+
// True compare-and-swap: the takeover applies only if the observed dead
|
|
173
|
+
// record (pid AND heartbeat) is unchanged. A refreshed incumbent wins.
|
|
174
|
+
const updated = this.database.prepare(`
|
|
175
|
+
UPDATE supervisor
|
|
176
|
+
SET runtime_id = ?, pid = ?, port = ?, token_hash = ?, started_at = ?, heartbeat_at = ?, state = 'running'
|
|
177
|
+
WHERE singleton_id = 1 AND pid = ? AND heartbeat_at = ?
|
|
178
|
+
`).run(runtimeId, record.pid, record.port, record.tokenHash, record.now, record.now, record.observedDeadPid, record.observedHeartbeatAt);
|
|
179
|
+
if (updated.changes !== 1) {
|
|
180
|
+
this.database.exec("ROLLBACK");
|
|
181
|
+
throw runtimeError("runtime_supervisor_conflict", "supervisor", "another process claimed the supervisor singleton first");
|
|
182
|
+
}
|
|
183
|
+
this.database.exec("COMMIT");
|
|
184
|
+
return this.supervisor()!;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
try { this.database.exec("ROLLBACK"); } catch { /* already rolled back */ }
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
markStopping(pid: number, now: string): void {
|
|
192
|
+
// Only the owning process may mark the record; a losing takeover must
|
|
193
|
+
// never clobber the winner's liveness.
|
|
194
|
+
this.database.prepare("UPDATE supervisor SET state = 'stopping', heartbeat_at = ? WHERE singleton_id = 1 AND pid = ?").run(now, pid);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
markStopped(pid: number, now: string): void {
|
|
198
|
+
this.database.prepare("UPDATE supervisor SET state = 'stopped', heartbeat_at = ? WHERE singleton_id = 1 AND pid = ?").run(now, pid);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
heartbeat(pid: number, now: string): number {
|
|
202
|
+
const result = this.database.prepare("UPDATE supervisor SET heartbeat_at = ? WHERE singleton_id = 1 AND pid = ?").run(now, pid);
|
|
203
|
+
return Number(result.changes);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private readSupervisorRow(): VnextSupervisorRecord | undefined {
|
|
207
|
+
const row = this.database.prepare("SELECT runtime_id, pid, port, token_hash, started_at, heartbeat_at, state FROM supervisor WHERE singleton_id = 1").get() as
|
|
208
|
+
| { runtime_id: string; pid: number; port: number; token_hash: string; started_at: string; heartbeat_at: string; state: VnextSupervisorRecord["state"] }
|
|
209
|
+
| undefined;
|
|
210
|
+
return row
|
|
211
|
+
? {
|
|
212
|
+
runtimeId: row.runtime_id,
|
|
213
|
+
pid: row.pid,
|
|
214
|
+
port: row.port,
|
|
215
|
+
tokenHash: row.token_hash,
|
|
216
|
+
startedAt: row.started_at,
|
|
217
|
+
heartbeatAt: row.heartbeat_at,
|
|
218
|
+
state: row.state,
|
|
219
|
+
}
|
|
220
|
+
: undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
supervisor(): VnextSupervisorRecord | undefined {
|
|
224
|
+
return this.readSupervisorRow();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Register or revalidate a project's home binding. Home Runtime is immutable. */
|
|
228
|
+
registerProject(registration: { projectId: string; projectRoot: string; homeRuntimeId: string; configRevision?: string; now: string }): VnextProjectRegistration {
|
|
229
|
+
const projectRoot = resolve(registration.projectRoot);
|
|
230
|
+
const projectKey = projectRuntimeKey(projectRoot);
|
|
231
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
232
|
+
try {
|
|
233
|
+
const byId = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_id = ?").get(registration.projectId) as
|
|
234
|
+
| { project_id: string; project_root: string; project_key: string; home_runtime_id: string; config_revision: string | null; registered_at: string }
|
|
235
|
+
| undefined;
|
|
236
|
+
const byKey = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_key = ?").get(projectKey) as
|
|
237
|
+
| { project_id: string; project_root: string; project_key: string; home_runtime_id: string; config_revision: string | null; registered_at: string }
|
|
238
|
+
| undefined;
|
|
239
|
+
const existing = byId ?? byKey;
|
|
240
|
+
if (existing) {
|
|
241
|
+
const problems: string[] = [];
|
|
242
|
+
if (byId && byId.project_key !== projectKey) problems.push(`project ${registration.projectId} is already bound to a different control root`);
|
|
243
|
+
if (byKey && byKey.project_id !== registration.projectId) problems.push(`control root is already bound to a different project id ${byKey.project_id}`);
|
|
244
|
+
if (existing.home_runtime_id !== registration.homeRuntimeId) problems.push(`project ${registration.projectId} home runtime is immutable and cannot be rebound`);
|
|
245
|
+
if (problems.length > 0) {
|
|
246
|
+
this.database.exec("ROLLBACK");
|
|
247
|
+
throw runtimeError("project_home_conflict", ".kxm/project.yaml", problems.join("; "));
|
|
248
|
+
}
|
|
249
|
+
const result: VnextProjectRegistration = {
|
|
250
|
+
projectId: existing.project_id,
|
|
251
|
+
projectRoot: existing.project_root,
|
|
252
|
+
projectKey: existing.project_key,
|
|
253
|
+
homeRuntimeId: existing.home_runtime_id,
|
|
254
|
+
...(existing.config_revision !== null ? { configRevision: existing.config_revision } : {}),
|
|
255
|
+
registeredAt: existing.registered_at,
|
|
256
|
+
};
|
|
257
|
+
this.database.exec("COMMIT");
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
this.database.prepare(`
|
|
261
|
+
INSERT INTO projects (project_id, project_root, project_key, home_runtime_id, config_revision, registered_at)
|
|
262
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
263
|
+
`).run(registration.projectId, projectRoot, projectKey, registration.homeRuntimeId, registration.configRevision ?? null, registration.now);
|
|
264
|
+
this.database.exec("COMMIT");
|
|
265
|
+
return {
|
|
266
|
+
projectId: registration.projectId,
|
|
267
|
+
projectRoot,
|
|
268
|
+
projectKey,
|
|
269
|
+
homeRuntimeId: registration.homeRuntimeId,
|
|
270
|
+
...(registration.configRevision !== undefined ? { configRevision: registration.configRevision } : {}),
|
|
271
|
+
registeredAt: registration.now,
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
try { this.database.exec("ROLLBACK"); } catch { /* already rolled back */ }
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
project(projectId: string): VnextProjectRegistration | undefined {
|
|
280
|
+
const row = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_id = ?").get(projectId) as
|
|
281
|
+
| { project_id: string; project_root: string; project_key: string; home_runtime_id: string; config_revision: string | null; registered_at: string }
|
|
282
|
+
| undefined;
|
|
283
|
+
return row
|
|
284
|
+
? {
|
|
285
|
+
projectId: row.project_id,
|
|
286
|
+
projectRoot: row.project_root,
|
|
287
|
+
projectKey: row.project_key,
|
|
288
|
+
homeRuntimeId: row.home_runtime_id,
|
|
289
|
+
...(row.config_revision !== null ? { configRevision: row.config_revision } : {}),
|
|
290
|
+
registeredAt: row.registered_at,
|
|
291
|
+
}
|
|
292
|
+
: undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
projectByRoot(projectRoot: string): VnextProjectRegistration | undefined {
|
|
296
|
+
const key = projectRuntimeKey(projectRoot);
|
|
297
|
+
const row = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_key = ?").get(key) as
|
|
298
|
+
| { project_id: string; project_root: string; project_key: string; home_runtime_id: string; config_revision: string | null; registered_at: string }
|
|
299
|
+
| undefined;
|
|
300
|
+
return row
|
|
301
|
+
? {
|
|
302
|
+
projectId: row.project_id,
|
|
303
|
+
projectRoot: row.project_root,
|
|
304
|
+
projectKey: row.project_key,
|
|
305
|
+
homeRuntimeId: row.home_runtime_id,
|
|
306
|
+
...(row.config_revision !== null ? { configRevision: row.config_revision } : {}),
|
|
307
|
+
registeredAt: row.registered_at,
|
|
308
|
+
}
|
|
309
|
+
: undefined;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* --------------------------- event store --------------------------- */
|
|
314
|
+
|
|
315
|
+
export type VnextRunStatus = "created" | "preparing" | "running" | "waiting" | "blocked_uncertain" | "cancelling" | "cancelled" | "completed" | "failed";
|
|
316
|
+
|
|
317
|
+
export const VNEXT_RUN_EVENT_SCHEMA = "kxm.run-event.v1";
|
|
318
|
+
export const VNEXT_ABSENT_MEMORY_REVISION = "ctxrev_absent";
|
|
319
|
+
|
|
320
|
+
export interface VnextRunRecord {
|
|
321
|
+
runId: string;
|
|
322
|
+
projectId: string;
|
|
323
|
+
homeRuntimeId: string;
|
|
324
|
+
workflowId: string;
|
|
325
|
+
promptSha256: string;
|
|
326
|
+
status: VnextRunStatus;
|
|
327
|
+
configRevision: string;
|
|
328
|
+
memoryRevision: string;
|
|
329
|
+
executorPolicyRevision: string;
|
|
330
|
+
toolPolicyRevision: string;
|
|
331
|
+
createdAt: string;
|
|
332
|
+
updatedAt: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface VnextRunEvent {
|
|
336
|
+
schema: typeof VNEXT_RUN_EVENT_SCHEMA;
|
|
337
|
+
eventId: string;
|
|
338
|
+
eventType: string;
|
|
339
|
+
projectId: string;
|
|
340
|
+
runId: string;
|
|
341
|
+
homeRuntimeId: string;
|
|
342
|
+
sequence: number;
|
|
343
|
+
commandId?: string;
|
|
344
|
+
occurredAt: string;
|
|
345
|
+
recordedAt: string;
|
|
346
|
+
monotonicNs: string;
|
|
347
|
+
configRevision: string;
|
|
348
|
+
memoryRevision: string;
|
|
349
|
+
executorPolicyRevision: string;
|
|
350
|
+
toolPolicyRevision: string;
|
|
351
|
+
payload: Record<string, unknown>;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface VnextRunPlanRow {
|
|
355
|
+
runId: string;
|
|
356
|
+
runPlanHash: string;
|
|
357
|
+
envelope: string;
|
|
358
|
+
pinnedSequence: number;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface VnextRunStateRow {
|
|
362
|
+
runId: string;
|
|
363
|
+
lastSequence: number;
|
|
364
|
+
state: string;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export interface VnextAttemptCapabilityRow {
|
|
368
|
+
attemptId: string;
|
|
369
|
+
runId: string;
|
|
370
|
+
assignmentId: string;
|
|
371
|
+
stepId: string;
|
|
372
|
+
stepAttempt: number;
|
|
373
|
+
producerId: string;
|
|
374
|
+
capabilityHash: string;
|
|
375
|
+
state: "issued" | "settled" | "revoked";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export type VnextGateKind = "command" | "artifacts-exist";
|
|
379
|
+
export type VnextGateExpect = "pass" | "fail";
|
|
380
|
+
export type VnextGateCompleteness = "complete" | "incomplete" | "no-start";
|
|
381
|
+
export type VnextGateStopCause = "none" | "timeout" | "cancel" | "error";
|
|
382
|
+
export type VnextGateSignalsAttempted = "none" | "term" | "term-kill";
|
|
383
|
+
export type VnextGateErrorClass = "validation" | "spawn-error" | "stream-error" | "stop-error" | "recording-error" | "lost-close";
|
|
384
|
+
export type VnextGateEvidenceOutcome = "passed" | "implementation-failure" | "repro-missing";
|
|
385
|
+
|
|
386
|
+
export interface VnextGateAttemptRow {
|
|
387
|
+
attemptId: string;
|
|
388
|
+
runId: string;
|
|
389
|
+
projectId: string;
|
|
390
|
+
homeRuntimeId: string;
|
|
391
|
+
stepId: string;
|
|
392
|
+
stepAttempt: number;
|
|
393
|
+
assignmentId: string;
|
|
394
|
+
effectId: string;
|
|
395
|
+
gateId: string;
|
|
396
|
+
gateKind: VnextGateKind;
|
|
397
|
+
expect: VnextGateExpect;
|
|
398
|
+
gateDefinitionHash: string;
|
|
399
|
+
registryHash: string;
|
|
400
|
+
runPlanHash: string;
|
|
401
|
+
controlProjectKey: string;
|
|
402
|
+
producerId: "kxm-gate";
|
|
403
|
+
intentEventId: string;
|
|
404
|
+
contentHash: string;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export interface VnextGateObservationRow {
|
|
408
|
+
observationId: string;
|
|
409
|
+
attemptId: string;
|
|
410
|
+
runId: string;
|
|
411
|
+
projectId: string;
|
|
412
|
+
homeRuntimeId: string;
|
|
413
|
+
stepId: string;
|
|
414
|
+
stepAttempt: number;
|
|
415
|
+
assignmentId: string;
|
|
416
|
+
effectId: string;
|
|
417
|
+
completeness: VnextGateCompleteness;
|
|
418
|
+
spawned: 0 | 1;
|
|
419
|
+
pid: number | null;
|
|
420
|
+
exitCode: number | null;
|
|
421
|
+
signal: string | null;
|
|
422
|
+
exitObserved: 0 | 1;
|
|
423
|
+
closeObserved: 0 | 1;
|
|
424
|
+
stopCause: VnextGateStopCause;
|
|
425
|
+
signalsAttempted: VnextGateSignalsAttempted;
|
|
426
|
+
errorClass: VnextGateErrorClass | null;
|
|
427
|
+
stdoutSha256: string | null;
|
|
428
|
+
stdoutBytes: number | null;
|
|
429
|
+
stdoutComplete: 0 | 1 | null;
|
|
430
|
+
stderrSha256: string | null;
|
|
431
|
+
stderrBytes: number | null;
|
|
432
|
+
stderrComplete: 0 | 1 | null;
|
|
433
|
+
checkedCount: number | null;
|
|
434
|
+
failedCount: number | null;
|
|
435
|
+
elapsedMs: number;
|
|
436
|
+
startedAt: string;
|
|
437
|
+
finishedAt: string | null;
|
|
438
|
+
recordedEventId: string;
|
|
439
|
+
contentHash: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export interface VnextGateEvidenceRow {
|
|
443
|
+
evidenceId: string;
|
|
444
|
+
attemptId: string;
|
|
445
|
+
observationId: string;
|
|
446
|
+
runId: string;
|
|
447
|
+
projectId: string;
|
|
448
|
+
homeRuntimeId: string;
|
|
449
|
+
stepId: string;
|
|
450
|
+
stepAttempt: number;
|
|
451
|
+
assignmentId: string;
|
|
452
|
+
effectId: string;
|
|
453
|
+
evidenceKey: string | null;
|
|
454
|
+
kind: "gate";
|
|
455
|
+
expect: VnextGateExpect;
|
|
456
|
+
outcome: VnextGateEvidenceOutcome;
|
|
457
|
+
settledEventId: string;
|
|
458
|
+
contentHash: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface VnextGateRowCounts {
|
|
462
|
+
attempts: number;
|
|
463
|
+
observations: number;
|
|
464
|
+
evidence: number;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export interface VnextCommandRecord {
|
|
468
|
+
commandId: string;
|
|
469
|
+
runId: string;
|
|
470
|
+
kind: string;
|
|
471
|
+
result: Record<string, unknown>;
|
|
472
|
+
recordedAt: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export const VNEXT_EVENT_STORE_SCHEMA_VERSION = 3;
|
|
476
|
+
|
|
477
|
+
const EVENT_STORE_TABLES = {
|
|
478
|
+
runs: ["run_id", "project_id", "home_runtime_id", "workflow_id", "prompt_sha256", "status", "config_revision", "memory_revision", "executor_policy_revision", "tool_policy_revision", "created_at", "updated_at"],
|
|
479
|
+
events: ["project_id", "run_id", "sequence", "event_id", "event_type", "command_id", "occurred_at", "recorded_at", "monotonic_ns", "config_revision", "memory_revision", "executor_policy_revision", "tool_policy_revision", "payload", "schema", "home_runtime_id"],
|
|
480
|
+
commands: ["command_id", "run_id", "kind", "result", "recorded_at"],
|
|
481
|
+
run_plans: ["run_id", "run_plan_hash", "envelope", "pinned_sequence"],
|
|
482
|
+
run_state: ["run_id", "last_sequence", "state"],
|
|
483
|
+
attempt_capabilities: ["attempt_id", "run_id", "assignment_id", "step_id", "step_attempt", "producer_id", "capability_hash", "state"],
|
|
484
|
+
gate_attempts: [
|
|
485
|
+
"attempt_id", "run_id", "project_id", "home_runtime_id", "step_id", "step_attempt", "assignment_id", "effect_id",
|
|
486
|
+
"gate_id", "gate_kind", "expect", "gate_definition_hash", "registry_hash", "run_plan_hash", "control_project_key",
|
|
487
|
+
"producer_id", "intent_event_id", "content_hash",
|
|
488
|
+
],
|
|
489
|
+
gate_observations: [
|
|
490
|
+
"observation_id", "attempt_id", "run_id", "project_id", "home_runtime_id", "step_id", "step_attempt", "assignment_id",
|
|
491
|
+
"effect_id", "completeness", "spawned", "pid", "exit_code", "signal", "exit_observed", "close_observed", "stop_cause",
|
|
492
|
+
"signals_attempted", "error_class", "stdout_sha256", "stdout_bytes", "stdout_complete", "stderr_sha256", "stderr_bytes",
|
|
493
|
+
"stderr_complete", "checked_count", "failed_count", "elapsed_ms", "started_at", "finished_at", "recorded_event_id",
|
|
494
|
+
"content_hash",
|
|
495
|
+
],
|
|
496
|
+
gate_evidence: [
|
|
497
|
+
"evidence_id", "attempt_id", "observation_id", "run_id", "project_id", "home_runtime_id", "step_id", "step_attempt",
|
|
498
|
+
"assignment_id", "effect_id", "evidence_key", "kind", "expect", "outcome", "settled_event_id", "content_hash",
|
|
499
|
+
],
|
|
500
|
+
} as const;
|
|
501
|
+
|
|
502
|
+
const EVENT_STORE_SCHEMA = `
|
|
503
|
+
CREATE TABLE runs (
|
|
504
|
+
run_id TEXT PRIMARY KEY,
|
|
505
|
+
project_id TEXT NOT NULL,
|
|
506
|
+
home_runtime_id TEXT NOT NULL,
|
|
507
|
+
workflow_id TEXT NOT NULL,
|
|
508
|
+
prompt_sha256 TEXT NOT NULL,
|
|
509
|
+
status TEXT NOT NULL,
|
|
510
|
+
config_revision TEXT NOT NULL,
|
|
511
|
+
memory_revision TEXT NOT NULL,
|
|
512
|
+
executor_policy_revision TEXT NOT NULL,
|
|
513
|
+
tool_policy_revision TEXT NOT NULL,
|
|
514
|
+
created_at TEXT NOT NULL,
|
|
515
|
+
updated_at TEXT NOT NULL
|
|
516
|
+
) STRICT;
|
|
517
|
+
CREATE TABLE events (
|
|
518
|
+
project_id TEXT NOT NULL,
|
|
519
|
+
run_id TEXT NOT NULL,
|
|
520
|
+
sequence INTEGER NOT NULL,
|
|
521
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
522
|
+
event_type TEXT NOT NULL,
|
|
523
|
+
command_id TEXT,
|
|
524
|
+
occurred_at TEXT NOT NULL,
|
|
525
|
+
recorded_at TEXT NOT NULL,
|
|
526
|
+
monotonic_ns TEXT NOT NULL,
|
|
527
|
+
config_revision TEXT NOT NULL,
|
|
528
|
+
memory_revision TEXT NOT NULL,
|
|
529
|
+
executor_policy_revision TEXT NOT NULL,
|
|
530
|
+
tool_policy_revision TEXT NOT NULL,
|
|
531
|
+
payload TEXT NOT NULL,
|
|
532
|
+
schema TEXT NOT NULL,
|
|
533
|
+
home_runtime_id TEXT NOT NULL,
|
|
534
|
+
PRIMARY KEY (project_id, run_id, sequence)
|
|
535
|
+
) STRICT;
|
|
536
|
+
CREATE INDEX events_run ON events(run_id, sequence);
|
|
537
|
+
CREATE TABLE commands (
|
|
538
|
+
command_id TEXT PRIMARY KEY,
|
|
539
|
+
run_id TEXT NOT NULL,
|
|
540
|
+
kind TEXT NOT NULL,
|
|
541
|
+
result TEXT NOT NULL,
|
|
542
|
+
recorded_at TEXT NOT NULL
|
|
543
|
+
) STRICT;
|
|
544
|
+
CREATE TABLE run_plans (
|
|
545
|
+
run_id TEXT PRIMARY KEY REFERENCES runs(run_id),
|
|
546
|
+
run_plan_hash TEXT NOT NULL,
|
|
547
|
+
envelope TEXT NOT NULL,
|
|
548
|
+
pinned_sequence INTEGER NOT NULL
|
|
549
|
+
) STRICT;
|
|
550
|
+
CREATE TABLE run_state (
|
|
551
|
+
run_id TEXT PRIMARY KEY REFERENCES runs(run_id),
|
|
552
|
+
last_sequence INTEGER NOT NULL,
|
|
553
|
+
state TEXT NOT NULL
|
|
554
|
+
) STRICT;
|
|
555
|
+
CREATE TABLE attempt_capabilities (
|
|
556
|
+
attempt_id TEXT PRIMARY KEY,
|
|
557
|
+
run_id TEXT NOT NULL,
|
|
558
|
+
assignment_id TEXT NOT NULL,
|
|
559
|
+
step_id TEXT NOT NULL,
|
|
560
|
+
step_attempt INTEGER NOT NULL,
|
|
561
|
+
producer_id TEXT NOT NULL,
|
|
562
|
+
capability_hash TEXT NOT NULL UNIQUE,
|
|
563
|
+
state TEXT NOT NULL CHECK (state IN ('issued','settled','revoked'))
|
|
564
|
+
) STRICT;
|
|
565
|
+
CREATE TABLE gate_attempts (
|
|
566
|
+
attempt_id TEXT PRIMARY KEY REFERENCES attempt_capabilities(attempt_id),
|
|
567
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id),
|
|
568
|
+
project_id TEXT NOT NULL,
|
|
569
|
+
home_runtime_id TEXT NOT NULL,
|
|
570
|
+
step_id TEXT NOT NULL,
|
|
571
|
+
step_attempt INTEGER NOT NULL,
|
|
572
|
+
assignment_id TEXT NOT NULL,
|
|
573
|
+
effect_id TEXT NOT NULL UNIQUE,
|
|
574
|
+
gate_id TEXT NOT NULL,
|
|
575
|
+
gate_kind TEXT NOT NULL CHECK (gate_kind IN ('command','artifacts-exist')),
|
|
576
|
+
expect TEXT NOT NULL CHECK (expect IN ('pass','fail')),
|
|
577
|
+
gate_definition_hash TEXT NOT NULL,
|
|
578
|
+
registry_hash TEXT NOT NULL,
|
|
579
|
+
run_plan_hash TEXT NOT NULL,
|
|
580
|
+
control_project_key TEXT NOT NULL,
|
|
581
|
+
producer_id TEXT NOT NULL CHECK (producer_id = 'kxm-gate'),
|
|
582
|
+
intent_event_id TEXT NOT NULL UNIQUE REFERENCES events(event_id),
|
|
583
|
+
content_hash TEXT NOT NULL
|
|
584
|
+
) STRICT;
|
|
585
|
+
CREATE INDEX gate_attempts_run ON gate_attempts(run_id);
|
|
586
|
+
CREATE INDEX gate_attempts_project ON gate_attempts(project_id);
|
|
587
|
+
CREATE TABLE gate_observations (
|
|
588
|
+
observation_id TEXT PRIMARY KEY,
|
|
589
|
+
attempt_id TEXT NOT NULL UNIQUE REFERENCES gate_attempts(attempt_id),
|
|
590
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id),
|
|
591
|
+
project_id TEXT NOT NULL,
|
|
592
|
+
home_runtime_id TEXT NOT NULL,
|
|
593
|
+
step_id TEXT NOT NULL,
|
|
594
|
+
step_attempt INTEGER NOT NULL,
|
|
595
|
+
assignment_id TEXT NOT NULL,
|
|
596
|
+
effect_id TEXT NOT NULL,
|
|
597
|
+
completeness TEXT NOT NULL CHECK (completeness IN ('complete','incomplete','no-start')),
|
|
598
|
+
spawned INTEGER NOT NULL CHECK (spawned IN (0, 1)),
|
|
599
|
+
pid INTEGER,
|
|
600
|
+
exit_code INTEGER,
|
|
601
|
+
signal TEXT,
|
|
602
|
+
exit_observed INTEGER NOT NULL CHECK (exit_observed IN (0, 1)),
|
|
603
|
+
close_observed INTEGER NOT NULL CHECK (close_observed IN (0, 1)),
|
|
604
|
+
stop_cause TEXT NOT NULL CHECK (stop_cause IN ('none','timeout','cancel','error')),
|
|
605
|
+
signals_attempted TEXT NOT NULL CHECK (signals_attempted IN ('none','term','term-kill')),
|
|
606
|
+
error_class TEXT CHECK (error_class IN ('validation','spawn-error','stream-error','stop-error','recording-error','lost-close')),
|
|
607
|
+
stdout_sha256 TEXT,
|
|
608
|
+
stdout_bytes INTEGER,
|
|
609
|
+
stdout_complete INTEGER CHECK (stdout_complete IN (0, 1)),
|
|
610
|
+
stderr_sha256 TEXT,
|
|
611
|
+
stderr_bytes INTEGER,
|
|
612
|
+
stderr_complete INTEGER CHECK (stderr_complete IN (0, 1)),
|
|
613
|
+
checked_count INTEGER,
|
|
614
|
+
failed_count INTEGER,
|
|
615
|
+
elapsed_ms INTEGER NOT NULL,
|
|
616
|
+
started_at TEXT NOT NULL,
|
|
617
|
+
finished_at TEXT,
|
|
618
|
+
recorded_event_id TEXT NOT NULL UNIQUE REFERENCES events(event_id),
|
|
619
|
+
content_hash TEXT NOT NULL
|
|
620
|
+
) STRICT;
|
|
621
|
+
CREATE INDEX gate_observations_run ON gate_observations(run_id);
|
|
622
|
+
CREATE TABLE gate_evidence (
|
|
623
|
+
evidence_id TEXT PRIMARY KEY,
|
|
624
|
+
attempt_id TEXT NOT NULL UNIQUE REFERENCES gate_attempts(attempt_id),
|
|
625
|
+
observation_id TEXT NOT NULL UNIQUE REFERENCES gate_observations(observation_id),
|
|
626
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id),
|
|
627
|
+
project_id TEXT NOT NULL,
|
|
628
|
+
home_runtime_id TEXT NOT NULL,
|
|
629
|
+
step_id TEXT NOT NULL,
|
|
630
|
+
step_attempt INTEGER NOT NULL,
|
|
631
|
+
assignment_id TEXT NOT NULL,
|
|
632
|
+
effect_id TEXT NOT NULL,
|
|
633
|
+
evidence_key TEXT,
|
|
634
|
+
kind TEXT NOT NULL CHECK (kind = 'gate'),
|
|
635
|
+
expect TEXT NOT NULL CHECK (expect IN ('pass','fail')),
|
|
636
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('passed','implementation-failure','repro-missing')),
|
|
637
|
+
settled_event_id TEXT NOT NULL UNIQUE REFERENCES events(event_id),
|
|
638
|
+
content_hash TEXT NOT NULL
|
|
639
|
+
) STRICT;
|
|
640
|
+
CREATE UNIQUE INDEX gate_evidence_key ON gate_evidence(run_id, step_id, step_attempt, evidence_key) WHERE evidence_key IS NOT NULL;
|
|
641
|
+
CREATE INDEX gate_evidence_run ON gate_evidence(run_id);
|
|
642
|
+
`;
|
|
643
|
+
|
|
644
|
+
export class VnextRunEventStore {
|
|
645
|
+
readonly path: string;
|
|
646
|
+
private readonly database: DatabaseSync;
|
|
647
|
+
|
|
648
|
+
constructor(path: string) {
|
|
649
|
+
this.path = resolve(path);
|
|
650
|
+
this.database = openDatabase(this.path, "run event store", {
|
|
651
|
+
schema: EVENT_STORE_SCHEMA,
|
|
652
|
+
version: VNEXT_EVENT_STORE_SCHEMA_VERSION,
|
|
653
|
+
tables: EVENT_STORE_TABLES,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
close(): void {
|
|
658
|
+
this.database.close();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Run one immutable transaction, rolling back on any failure. */
|
|
662
|
+
transaction<T>(work: () => T): T {
|
|
663
|
+
return withDatabaseTransaction(this.database, work);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
command(commandId: string): VnextCommandRecord | undefined {
|
|
667
|
+
const row = this.database.prepare("SELECT command_id, run_id, kind, result, recorded_at FROM commands WHERE command_id = ?").get(commandId) as
|
|
668
|
+
| { command_id: string; run_id: string; kind: string; result: string; recorded_at: string }
|
|
669
|
+
| undefined;
|
|
670
|
+
return row
|
|
671
|
+
? { commandId: row.command_id, runId: row.run_id, kind: row.kind, result: JSON.parse(row.result) as Record<string, unknown>, recordedAt: row.recorded_at }
|
|
672
|
+
: undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
insertCommand(record: VnextCommandRecord): void {
|
|
676
|
+
this.database.prepare("INSERT INTO commands (command_id, run_id, kind, result, recorded_at) VALUES (?, ?, ?, ?, ?)")
|
|
677
|
+
.run(record.commandId, record.runId, record.kind, JSON.stringify(record.result), record.recordedAt);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
insertRun(record: VnextRunRecord): void {
|
|
681
|
+
this.database.prepare(`
|
|
682
|
+
INSERT INTO runs (run_id, project_id, home_runtime_id, workflow_id, prompt_sha256, status, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, created_at, updated_at)
|
|
683
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
684
|
+
`).run(
|
|
685
|
+
record.runId,
|
|
686
|
+
record.projectId,
|
|
687
|
+
record.homeRuntimeId,
|
|
688
|
+
record.workflowId,
|
|
689
|
+
record.promptSha256,
|
|
690
|
+
record.status,
|
|
691
|
+
record.configRevision,
|
|
692
|
+
record.memoryRevision,
|
|
693
|
+
record.executorPolicyRevision,
|
|
694
|
+
record.toolPolicyRevision,
|
|
695
|
+
record.createdAt,
|
|
696
|
+
record.updatedAt,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
updateRunStatus(runId: string, status: VnextRunStatus, updatedAt: string): void {
|
|
701
|
+
this.database.prepare("UPDATE runs SET status = ?, updated_at = ? WHERE run_id = ?").run(status, updatedAt, runId);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
run(runId: string): VnextRunRecord | undefined {
|
|
705
|
+
const row = this.database.prepare(`
|
|
706
|
+
SELECT run_id, project_id, home_runtime_id, workflow_id, prompt_sha256, status, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, created_at, updated_at
|
|
707
|
+
FROM runs WHERE run_id = ?
|
|
708
|
+
`).get(runId) as
|
|
709
|
+
| {
|
|
710
|
+
run_id: string;
|
|
711
|
+
project_id: string;
|
|
712
|
+
home_runtime_id: string;
|
|
713
|
+
workflow_id: string;
|
|
714
|
+
prompt_sha256: string;
|
|
715
|
+
status: VnextRunStatus;
|
|
716
|
+
config_revision: string;
|
|
717
|
+
memory_revision: string;
|
|
718
|
+
executor_policy_revision: string;
|
|
719
|
+
tool_policy_revision: string;
|
|
720
|
+
created_at: string;
|
|
721
|
+
updated_at: string;
|
|
722
|
+
}
|
|
723
|
+
| undefined;
|
|
724
|
+
return row ? runFromRow(row) : undefined;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
runsForProject(projectId: string, limit = 50): VnextRunRecord[] {
|
|
728
|
+
const rows = this.database.prepare(`
|
|
729
|
+
SELECT run_id, project_id, home_runtime_id, workflow_id, prompt_sha256, status, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, created_at, updated_at
|
|
730
|
+
FROM runs WHERE project_id = ? ORDER BY created_at DESC, run_id DESC LIMIT ?
|
|
731
|
+
`).all(projectId, limit) as Array<Parameters<typeof runFromRow>[0]>;
|
|
732
|
+
return rows.map(runFromRow);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
nextSequence(runId: string): number {
|
|
736
|
+
const row = this.database.prepare("SELECT COALESCE(MAX(sequence), 0) AS max_sequence FROM events WHERE run_id = ?").get(runId) as { max_sequence: number };
|
|
737
|
+
return row.max_sequence + 1;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
appendEvent(event: VnextRunEvent): void {
|
|
741
|
+
validateRunEvent(event, "run-event");
|
|
742
|
+
const run = this.run(event.runId);
|
|
743
|
+
if (!run) throw runtimeError("run_unknown", event.runId, "run does not exist in this event store");
|
|
744
|
+
if (event.homeRuntimeId !== run.homeRuntimeId || event.projectId !== run.projectId) {
|
|
745
|
+
throw runtimeError("run_event_owner_mismatch", event.runId, "event homeRuntimeId or projectId does not match the run");
|
|
746
|
+
}
|
|
747
|
+
this.database.prepare(`
|
|
748
|
+
INSERT INTO events (project_id, run_id, sequence, event_id, event_type, command_id, occurred_at, recorded_at, monotonic_ns, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, payload, schema, home_runtime_id)
|
|
749
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
750
|
+
`).run(
|
|
751
|
+
event.projectId,
|
|
752
|
+
event.runId,
|
|
753
|
+
event.sequence,
|
|
754
|
+
event.eventId,
|
|
755
|
+
event.eventType,
|
|
756
|
+
event.commandId ?? null,
|
|
757
|
+
event.occurredAt,
|
|
758
|
+
event.recordedAt,
|
|
759
|
+
event.monotonicNs,
|
|
760
|
+
event.configRevision,
|
|
761
|
+
event.memoryRevision,
|
|
762
|
+
event.executorPolicyRevision,
|
|
763
|
+
event.toolPolicyRevision,
|
|
764
|
+
JSON.stringify(event.payload),
|
|
765
|
+
event.schema,
|
|
766
|
+
event.homeRuntimeId,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
events(runId: string, afterSequence = 0, limit = 200): VnextRunEvent[] {
|
|
771
|
+
const rows = this.database.prepare(`
|
|
772
|
+
SELECT project_id, run_id, sequence, event_id, event_type, command_id, occurred_at, recorded_at, monotonic_ns, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, payload, schema, home_runtime_id
|
|
773
|
+
FROM events WHERE run_id = ? AND sequence > ? ORDER BY sequence ASC LIMIT ?
|
|
774
|
+
`).all(runId, afterSequence, limit) as Array<{
|
|
775
|
+
project_id: string;
|
|
776
|
+
run_id: string;
|
|
777
|
+
sequence: number;
|
|
778
|
+
event_id: string;
|
|
779
|
+
event_type: string;
|
|
780
|
+
command_id: string | null;
|
|
781
|
+
occurred_at: string;
|
|
782
|
+
recorded_at: string;
|
|
783
|
+
monotonic_ns: string;
|
|
784
|
+
config_revision: string;
|
|
785
|
+
memory_revision: string;
|
|
786
|
+
executor_policy_revision: string;
|
|
787
|
+
tool_policy_revision: string;
|
|
788
|
+
payload: string;
|
|
789
|
+
schema: typeof VNEXT_RUN_EVENT_SCHEMA;
|
|
790
|
+
home_runtime_id: string;
|
|
791
|
+
}>;
|
|
792
|
+
return rows.map((row) => ({
|
|
793
|
+
schema: row.schema,
|
|
794
|
+
eventId: row.event_id,
|
|
795
|
+
eventType: row.event_type,
|
|
796
|
+
projectId: row.project_id,
|
|
797
|
+
runId: row.run_id,
|
|
798
|
+
homeRuntimeId: row.home_runtime_id,
|
|
799
|
+
sequence: row.sequence,
|
|
800
|
+
...(row.command_id !== null ? { commandId: row.command_id } : {}),
|
|
801
|
+
occurredAt: row.occurred_at,
|
|
802
|
+
recordedAt: row.recorded_at,
|
|
803
|
+
monotonicNs: row.monotonic_ns,
|
|
804
|
+
configRevision: row.config_revision,
|
|
805
|
+
memoryRevision: row.memory_revision,
|
|
806
|
+
executorPolicyRevision: row.executor_policy_revision,
|
|
807
|
+
toolPolicyRevision: row.tool_policy_revision,
|
|
808
|
+
payload: JSON.parse(row.payload) as Record<string, unknown>,
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
insertRunPlan(row: VnextRunPlanRow): void {
|
|
813
|
+
this.database.prepare("INSERT INTO run_plans (run_id, run_plan_hash, envelope, pinned_sequence) VALUES (?, ?, ?, ?)")
|
|
814
|
+
.run(row.runId, row.runPlanHash, row.envelope, row.pinnedSequence);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
runPlan(runId: string): VnextRunPlanRow | undefined {
|
|
818
|
+
const row = this.database.prepare("SELECT run_id, run_plan_hash, envelope, pinned_sequence FROM run_plans WHERE run_id = ?").get(runId) as
|
|
819
|
+
| { run_id: string; run_plan_hash: string; envelope: string; pinned_sequence: number }
|
|
820
|
+
| undefined;
|
|
821
|
+
return row
|
|
822
|
+
? { runId: row.run_id, runPlanHash: row.run_plan_hash, envelope: row.envelope, pinnedSequence: row.pinned_sequence }
|
|
823
|
+
: undefined;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
runState(runId: string): VnextRunStateRow | undefined {
|
|
827
|
+
const row = this.database.prepare("SELECT run_id, last_sequence, state FROM run_state WHERE run_id = ?").get(runId) as
|
|
828
|
+
| { run_id: string; last_sequence: number; state: string }
|
|
829
|
+
| undefined;
|
|
830
|
+
return row ? { runId: row.run_id, lastSequence: row.last_sequence, state: row.state } : undefined;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
upsertRunState(row: VnextRunStateRow): void {
|
|
834
|
+
this.database.prepare(`
|
|
835
|
+
INSERT INTO run_state (run_id, last_sequence, state) VALUES (?, ?, ?)
|
|
836
|
+
ON CONFLICT(run_id) DO UPDATE SET last_sequence = excluded.last_sequence, state = excluded.state
|
|
837
|
+
`).run(row.runId, row.lastSequence, row.state);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
insertCapability(row: VnextAttemptCapabilityRow): void {
|
|
841
|
+
this.database.prepare(`
|
|
842
|
+
INSERT INTO attempt_capabilities (attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state)
|
|
843
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
844
|
+
`).run(row.attemptId, row.runId, row.assignmentId, row.stepId, row.stepAttempt, row.producerId, row.capabilityHash, row.state);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
capabilityByHash(capabilityHash: string): VnextAttemptCapabilityRow | undefined {
|
|
848
|
+
return capabilityFromRow(this.database.prepare(`
|
|
849
|
+
SELECT attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state
|
|
850
|
+
FROM attempt_capabilities WHERE capability_hash = ?
|
|
851
|
+
`).get(capabilityHash) as CapabilitySqlRow | undefined);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
capabilityByAttempt(attemptId: string): VnextAttemptCapabilityRow | undefined {
|
|
855
|
+
return capabilityFromRow(this.database.prepare(`
|
|
856
|
+
SELECT attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state
|
|
857
|
+
FROM attempt_capabilities WHERE attempt_id = ?
|
|
858
|
+
`).get(attemptId) as CapabilitySqlRow | undefined);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
settleCapability(attemptId: string, state: "settled" | "revoked"): void {
|
|
862
|
+
const updated = this.database.prepare("UPDATE attempt_capabilities SET state = ? WHERE attempt_id = ?").run(state, attemptId);
|
|
863
|
+
if (Number(updated.changes) !== 1) {
|
|
864
|
+
throw runtimeError("capability_unknown", attemptId, "attempt capability does not exist");
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
insertGateAttempt(row: VnextGateAttemptRow): void {
|
|
869
|
+
assertGateAttemptRow(row);
|
|
870
|
+
this.assertEventType(row.runId, row.intentEventId, ["effect.intent_recorded"]);
|
|
871
|
+
const capability = this.capabilityByAttempt(row.attemptId);
|
|
872
|
+
if (
|
|
873
|
+
!capability
|
|
874
|
+
|| capability.runId !== row.runId
|
|
875
|
+
|| capability.assignmentId !== row.assignmentId
|
|
876
|
+
|| capability.stepId !== row.stepId
|
|
877
|
+
|| capability.stepAttempt !== row.stepAttempt
|
|
878
|
+
) {
|
|
879
|
+
throw gateRowInvalid(row.attemptId, "gate attempt identity does not match attempt_capabilities");
|
|
880
|
+
}
|
|
881
|
+
this.database.prepare(`
|
|
882
|
+
INSERT INTO gate_attempts (
|
|
883
|
+
attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
|
|
884
|
+
gate_id, gate_kind, expect, gate_definition_hash, registry_hash, run_plan_hash, control_project_key,
|
|
885
|
+
producer_id, intent_event_id, content_hash
|
|
886
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
887
|
+
`).run(
|
|
888
|
+
row.attemptId, row.runId, row.projectId, row.homeRuntimeId, row.stepId, row.stepAttempt, row.assignmentId, row.effectId,
|
|
889
|
+
row.gateId, row.gateKind, row.expect, row.gateDefinitionHash, row.registryHash, row.runPlanHash, row.controlProjectKey,
|
|
890
|
+
row.producerId, row.intentEventId, row.contentHash,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
insertGateObservation(row: VnextGateObservationRow): void {
|
|
895
|
+
assertGateObservationRow(row);
|
|
896
|
+
this.assertEventType(row.runId, row.recordedEventId, ["effect.observed", "effect.blocked_uncertain"]);
|
|
897
|
+
const attempt = this.gateAttempt(row.attemptId);
|
|
898
|
+
if (!attempt) throw gateRowInvalid(row.attemptId, "gate observation has no matching gate attempt");
|
|
899
|
+
assertSharedIdentity(row, attempt, row.observationId);
|
|
900
|
+
assertClosedGateObservation(attempt.gateKind, row, row.observationId);
|
|
901
|
+
this.database.prepare(`
|
|
902
|
+
INSERT INTO gate_observations (
|
|
903
|
+
observation_id, attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
|
|
904
|
+
completeness, spawned, pid, exit_code, signal, exit_observed, close_observed, stop_cause, signals_attempted,
|
|
905
|
+
error_class, stdout_sha256, stdout_bytes, stdout_complete, stderr_sha256, stderr_bytes, stderr_complete,
|
|
906
|
+
checked_count, failed_count, elapsed_ms, started_at, finished_at, recorded_event_id, content_hash
|
|
907
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
908
|
+
`).run(
|
|
909
|
+
row.observationId, row.attemptId, row.runId, row.projectId, row.homeRuntimeId, row.stepId, row.stepAttempt,
|
|
910
|
+
row.assignmentId, row.effectId, row.completeness, row.spawned, row.pid, row.exitCode, row.signal, row.exitObserved,
|
|
911
|
+
row.closeObserved, row.stopCause, row.signalsAttempted, row.errorClass, row.stdoutSha256, row.stdoutBytes,
|
|
912
|
+
row.stdoutComplete, row.stderrSha256, row.stderrBytes, row.stderrComplete, row.checkedCount, row.failedCount,
|
|
913
|
+
row.elapsedMs, row.startedAt, row.finishedAt, row.recordedEventId, row.contentHash,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
insertGateEvidence(row: VnextGateEvidenceRow): void {
|
|
918
|
+
assertGateEvidenceRow(row);
|
|
919
|
+
this.assertEventType(row.runId, row.settledEventId, ["effect.settled"]);
|
|
920
|
+
const attempt = this.gateAttempt(row.attemptId);
|
|
921
|
+
if (!attempt) throw gateRowInvalid(row.attemptId, "gate evidence has no matching gate attempt");
|
|
922
|
+
assertSharedIdentity(row, attempt, row.evidenceId);
|
|
923
|
+
const observation = this.gateObservationForAttempt(row.attemptId);
|
|
924
|
+
if (!observation || observation.observationId !== row.observationId) {
|
|
925
|
+
throw gateRowInvalid(row.evidenceId, "gate evidence observation_id does not match the attempt observation");
|
|
926
|
+
}
|
|
927
|
+
this.database.prepare(`
|
|
928
|
+
INSERT INTO gate_evidence (
|
|
929
|
+
evidence_id, attempt_id, observation_id, run_id, project_id, home_runtime_id, step_id, step_attempt,
|
|
930
|
+
assignment_id, effect_id, evidence_key, kind, expect, outcome, settled_event_id, content_hash
|
|
931
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
932
|
+
`).run(
|
|
933
|
+
row.evidenceId, row.attemptId, row.observationId, row.runId, row.projectId, row.homeRuntimeId, row.stepId,
|
|
934
|
+
row.stepAttempt, row.assignmentId, row.effectId, row.evidenceKey, row.kind, row.expect, row.outcome,
|
|
935
|
+
row.settledEventId, row.contentHash,
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
gateAttempt(attemptId: string): VnextGateAttemptRow | undefined {
|
|
940
|
+
const row = this.database.prepare(`
|
|
941
|
+
SELECT attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
|
|
942
|
+
gate_id, gate_kind, expect, gate_definition_hash, registry_hash, run_plan_hash, control_project_key,
|
|
943
|
+
producer_id, intent_event_id, content_hash
|
|
944
|
+
FROM gate_attempts WHERE attempt_id = ?
|
|
945
|
+
`).get(attemptId) as GateAttemptSql | undefined;
|
|
946
|
+
return row ? gateAttemptFromSql(row) : undefined;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
gateAttemptsForRun(runId: string): VnextGateAttemptRow[] {
|
|
950
|
+
const rows = this.database.prepare(`
|
|
951
|
+
SELECT attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
|
|
952
|
+
gate_id, gate_kind, expect, gate_definition_hash, registry_hash, run_plan_hash, control_project_key,
|
|
953
|
+
producer_id, intent_event_id, content_hash
|
|
954
|
+
FROM gate_attempts WHERE run_id = ? ORDER BY step_attempt ASC, attempt_id ASC
|
|
955
|
+
`).all(runId) as GateAttemptSql[];
|
|
956
|
+
return rows.map(gateAttemptFromSql);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
gateAttemptsForProject(projectId: string): VnextGateAttemptRow[] {
|
|
960
|
+
const rows = this.database.prepare(`
|
|
961
|
+
SELECT attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
|
|
962
|
+
gate_id, gate_kind, expect, gate_definition_hash, registry_hash, run_plan_hash, control_project_key,
|
|
963
|
+
producer_id, intent_event_id, content_hash
|
|
964
|
+
FROM gate_attempts WHERE project_id = ? ORDER BY run_id ASC, step_attempt ASC, attempt_id ASC
|
|
965
|
+
`).all(projectId) as GateAttemptSql[];
|
|
966
|
+
return rows.map(gateAttemptFromSql);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
issuedOrRevokedCapabilities(): VnextAttemptCapabilityRow[] {
|
|
970
|
+
const rows = this.database.prepare(`
|
|
971
|
+
SELECT attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state
|
|
972
|
+
FROM attempt_capabilities
|
|
973
|
+
WHERE state IN ('issued','revoked')
|
|
974
|
+
ORDER BY run_id ASC, step_attempt ASC, attempt_id ASC
|
|
975
|
+
`).all() as CapabilitySqlRow[];
|
|
976
|
+
const mapped: VnextAttemptCapabilityRow[] = [];
|
|
977
|
+
for (const row of rows) {
|
|
978
|
+
const capability = capabilityFromRow(row);
|
|
979
|
+
if (!capability) {
|
|
980
|
+
throw runtimeError("gate_recovery_corrupt", "attempt_capabilities", "issued or revoked capability row could not be read");
|
|
981
|
+
}
|
|
982
|
+
mapped.push(capability);
|
|
983
|
+
}
|
|
984
|
+
return mapped;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
gateObservationForAttempt(attemptId: string): VnextGateObservationRow | undefined {
|
|
988
|
+
const row = this.database.prepare(`
|
|
989
|
+
SELECT observation_id, attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id,
|
|
990
|
+
effect_id, completeness, spawned, pid, exit_code, signal, exit_observed, close_observed, stop_cause,
|
|
991
|
+
signals_attempted, error_class, stdout_sha256, stdout_bytes, stdout_complete, stderr_sha256, stderr_bytes,
|
|
992
|
+
stderr_complete, checked_count, failed_count, elapsed_ms, started_at, finished_at, recorded_event_id, content_hash
|
|
993
|
+
FROM gate_observations WHERE attempt_id = ?
|
|
994
|
+
`).get(attemptId) as GateObservationSql | undefined;
|
|
995
|
+
return row ? gateObservationFromSql(row) : undefined;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
gateObservationsForRun(runId: string): VnextGateObservationRow[] {
|
|
999
|
+
const rows = this.database.prepare(`
|
|
1000
|
+
SELECT observation_id, attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id,
|
|
1001
|
+
effect_id, completeness, spawned, pid, exit_code, signal, exit_observed, close_observed, stop_cause,
|
|
1002
|
+
signals_attempted, error_class, stdout_sha256, stdout_bytes, stdout_complete, stderr_sha256, stderr_bytes,
|
|
1003
|
+
stderr_complete, checked_count, failed_count, elapsed_ms, started_at, finished_at, recorded_event_id, content_hash
|
|
1004
|
+
FROM gate_observations WHERE run_id = ? ORDER BY step_attempt ASC, observation_id ASC
|
|
1005
|
+
`).all(runId) as GateObservationSql[];
|
|
1006
|
+
return rows.map(gateObservationFromSql);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
gateEvidenceForRun(runId: string): VnextGateEvidenceRow[] {
|
|
1010
|
+
const rows = this.database.prepare(`
|
|
1011
|
+
SELECT evidence_id, attempt_id, observation_id, run_id, project_id, home_runtime_id, step_id, step_attempt,
|
|
1012
|
+
assignment_id, effect_id, evidence_key, kind, expect, outcome, settled_event_id, content_hash
|
|
1013
|
+
FROM gate_evidence WHERE run_id = ? ORDER BY step_attempt ASC, evidence_id ASC
|
|
1014
|
+
`).all(runId) as GateEvidenceSql[];
|
|
1015
|
+
return rows.map(gateEvidenceFromSql);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
gateEvidenceForAttempt(attemptId: string): VnextGateEvidenceRow | undefined {
|
|
1019
|
+
const row = this.database.prepare(`
|
|
1020
|
+
SELECT evidence_id, attempt_id, observation_id, run_id, project_id, home_runtime_id, step_id, step_attempt,
|
|
1021
|
+
assignment_id, effect_id, evidence_key, kind, expect, outcome, settled_event_id, content_hash
|
|
1022
|
+
FROM gate_evidence WHERE attempt_id = ?
|
|
1023
|
+
`).get(attemptId) as GateEvidenceSql | undefined;
|
|
1024
|
+
return row ? gateEvidenceFromSql(row) : undefined;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
gateRowCounts(runId: string): VnextGateRowCounts {
|
|
1028
|
+
const attempts = this.database.prepare("SELECT COUNT(*) AS n FROM gate_attempts WHERE run_id = ?").get(runId) as { n: number };
|
|
1029
|
+
const observations = this.database.prepare("SELECT COUNT(*) AS n FROM gate_observations WHERE run_id = ?").get(runId) as { n: number };
|
|
1030
|
+
const evidence = this.database.prepare("SELECT COUNT(*) AS n FROM gate_evidence WHERE run_id = ?").get(runId) as { n: number };
|
|
1031
|
+
return { attempts: Number(attempts.n), observations: Number(observations.n), evidence: Number(evidence.n) };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
private assertEventType(runId: string, eventId: string, allowed: readonly string[]): void {
|
|
1035
|
+
const row = this.database.prepare("SELECT run_id, event_type FROM events WHERE event_id = ?").get(eventId) as
|
|
1036
|
+
| { run_id: string; event_type: string }
|
|
1037
|
+
| undefined;
|
|
1038
|
+
if (!row || row.run_id !== runId || !allowed.includes(row.event_type)) {
|
|
1039
|
+
throw gateRowInvalid(eventId, `referenced event is not ${allowed.join("|")} for this run`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function runFromRow(row: {
|
|
1045
|
+
run_id: string;
|
|
1046
|
+
project_id: string;
|
|
1047
|
+
home_runtime_id: string;
|
|
1048
|
+
workflow_id: string;
|
|
1049
|
+
prompt_sha256: string;
|
|
1050
|
+
status: VnextRunStatus;
|
|
1051
|
+
config_revision: string;
|
|
1052
|
+
memory_revision: string;
|
|
1053
|
+
executor_policy_revision: string;
|
|
1054
|
+
tool_policy_revision: string;
|
|
1055
|
+
created_at: string;
|
|
1056
|
+
updated_at: string;
|
|
1057
|
+
}): VnextRunRecord {
|
|
1058
|
+
if (typeof row.memory_revision !== "string" || row.memory_revision.length === 0) {
|
|
1059
|
+
throw runtimeError("runtime_schema_shape_invalid", row.run_id, "memory_revision is required");
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
runId: row.run_id,
|
|
1063
|
+
projectId: row.project_id,
|
|
1064
|
+
homeRuntimeId: row.home_runtime_id,
|
|
1065
|
+
workflowId: row.workflow_id,
|
|
1066
|
+
promptSha256: row.prompt_sha256,
|
|
1067
|
+
status: row.status,
|
|
1068
|
+
configRevision: row.config_revision,
|
|
1069
|
+
memoryRevision: row.memory_revision,
|
|
1070
|
+
executorPolicyRevision: row.executor_policy_revision,
|
|
1071
|
+
toolPolicyRevision: row.tool_policy_revision,
|
|
1072
|
+
createdAt: row.created_at,
|
|
1073
|
+
updatedAt: row.updated_at,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** Compute the immutable identity for a new run. */
|
|
1078
|
+
export function newVnextRunId(): string {
|
|
1079
|
+
return `run_${randomUUID().replaceAll("-", "")}`;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
export function newVnextEventId(): string {
|
|
1083
|
+
return `evt_${randomUUID().replaceAll("-", "")}`;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
export function newVnextCommandId(): string {
|
|
1087
|
+
return `cmd_${randomUUID().replaceAll("-", "")}`;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
export function newVnextAssignmentId(): string {
|
|
1091
|
+
return `asg_${randomUUID().replaceAll("-", "")}`;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
export function newVnextAttemptId(): string {
|
|
1095
|
+
return `att_${randomUUID().replaceAll("-", "")}`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export function newVnextEffectId(): string {
|
|
1099
|
+
return `eff_${randomUUID().replaceAll("-", "")}`;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export function newVnextObservationId(): string {
|
|
1103
|
+
return `obs_${randomUUID().replaceAll("-", "")}`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export function newVnextEvidenceId(): string {
|
|
1107
|
+
return `gev_${randomUUID().replaceAll("-", "")}`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
1111
|
+
const OPAQUE = /^[a-z][a-z0-9]{1,15}_[A-Za-z0-9][A-Za-z0-9_-]{5,127}$/;
|
|
1112
|
+
const FORBIDDEN_TEXT_KEYS = new Set(["stdout", "stderr", "error", "errorText", "stdoutText", "stderrText", "errorMessage", "message"]);
|
|
1113
|
+
const GATE_ATTEMPT_KEYS = [
|
|
1114
|
+
"attemptId", "runId", "projectId", "homeRuntimeId", "stepId", "stepAttempt", "assignmentId", "effectId",
|
|
1115
|
+
"gateId", "gateKind", "expect", "gateDefinitionHash", "registryHash", "runPlanHash", "controlProjectKey",
|
|
1116
|
+
"producerId", "intentEventId", "contentHash",
|
|
1117
|
+
] as const;
|
|
1118
|
+
const GATE_OBSERVATION_KEYS = [
|
|
1119
|
+
"observationId", "attemptId", "runId", "projectId", "homeRuntimeId", "stepId", "stepAttempt", "assignmentId",
|
|
1120
|
+
"effectId", "completeness", "spawned", "pid", "exitCode", "signal", "exitObserved", "closeObserved", "stopCause",
|
|
1121
|
+
"signalsAttempted", "errorClass", "stdoutSha256", "stdoutBytes", "stdoutComplete", "stderrSha256", "stderrBytes",
|
|
1122
|
+
"stderrComplete", "checkedCount", "failedCount", "elapsedMs", "startedAt", "finishedAt", "recordedEventId", "contentHash",
|
|
1123
|
+
] as const;
|
|
1124
|
+
const GATE_EVIDENCE_KEYS = [
|
|
1125
|
+
"evidenceId", "attemptId", "observationId", "runId", "projectId", "homeRuntimeId", "stepId", "stepAttempt",
|
|
1126
|
+
"assignmentId", "effectId", "evidenceKey", "kind", "expect", "outcome", "settledEventId", "contentHash",
|
|
1127
|
+
] as const;
|
|
1128
|
+
|
|
1129
|
+
export function gateRowContentHash(table: "gate_attempts" | "gate_observations" | "gate_evidence", row: object): string {
|
|
1130
|
+
const copy = { ...(row as Record<string, unknown>) };
|
|
1131
|
+
delete copy.contentHash;
|
|
1132
|
+
void table;
|
|
1133
|
+
return `sha256:${createHash("sha256").update(vnextCanonicalJson(copy as JsonValue), "utf8").digest("hex")}`;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
export type VnextGateObservationFacts = Pick<
|
|
1137
|
+
VnextGateObservationRow,
|
|
1138
|
+
| "completeness"
|
|
1139
|
+
| "spawned"
|
|
1140
|
+
| "pid"
|
|
1141
|
+
| "exitCode"
|
|
1142
|
+
| "signal"
|
|
1143
|
+
| "exitObserved"
|
|
1144
|
+
| "closeObserved"
|
|
1145
|
+
| "stopCause"
|
|
1146
|
+
| "signalsAttempted"
|
|
1147
|
+
| "errorClass"
|
|
1148
|
+
| "stdoutSha256"
|
|
1149
|
+
| "stdoutBytes"
|
|
1150
|
+
| "stdoutComplete"
|
|
1151
|
+
| "stderrSha256"
|
|
1152
|
+
| "stderrBytes"
|
|
1153
|
+
| "stderrComplete"
|
|
1154
|
+
| "checkedCount"
|
|
1155
|
+
| "failedCount"
|
|
1156
|
+
>;
|
|
1157
|
+
|
|
1158
|
+
export function assertClosedGateObservation(
|
|
1159
|
+
kind: VnextGateKind,
|
|
1160
|
+
row: VnextGateObservationFacts,
|
|
1161
|
+
id = "observation",
|
|
1162
|
+
): void {
|
|
1163
|
+
if (row.completeness === "incomplete") return;
|
|
1164
|
+
if (row.completeness === "no-start") {
|
|
1165
|
+
const legitimateStartFailure = (row.errorClass === "validation" || row.errorClass === "spawn-error") && row.stopCause === "none";
|
|
1166
|
+
const requestedStop = row.stopCause === "cancel" && row.errorClass === null;
|
|
1167
|
+
if (
|
|
1168
|
+
row.spawned !== 0
|
|
1169
|
+
|| row.pid !== null
|
|
1170
|
+
|| row.exitCode !== null
|
|
1171
|
+
|| row.signal !== null
|
|
1172
|
+
|| row.exitObserved !== 0
|
|
1173
|
+
|| row.closeObserved !== 0
|
|
1174
|
+
|| row.signalsAttempted !== "none"
|
|
1175
|
+
|| row.stdoutSha256 !== null
|
|
1176
|
+
|| row.stdoutBytes !== null
|
|
1177
|
+
|| row.stdoutComplete !== null
|
|
1178
|
+
|| row.stderrSha256 !== null
|
|
1179
|
+
|| row.stderrBytes !== null
|
|
1180
|
+
|| row.stderrComplete !== null
|
|
1181
|
+
|| row.checkedCount !== null
|
|
1182
|
+
|| row.failedCount !== null
|
|
1183
|
+
|| (!legitimateStartFailure && !requestedStop)
|
|
1184
|
+
) {
|
|
1185
|
+
throw gateRowInvalid(id, "no-start observation forbids PID/exit/close/stream/check facts and requires a legitimate no-start cause");
|
|
1186
|
+
}
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (kind === "command") {
|
|
1190
|
+
if (
|
|
1191
|
+
row.spawned !== 1
|
|
1192
|
+
|| typeof row.pid !== "number"
|
|
1193
|
+
|| typeof row.exitCode !== "number"
|
|
1194
|
+
|| row.signal !== null
|
|
1195
|
+
|| row.exitObserved !== 1
|
|
1196
|
+
|| row.closeObserved !== 1
|
|
1197
|
+
|| row.stopCause !== "none"
|
|
1198
|
+
|| row.signalsAttempted !== "none"
|
|
1199
|
+
|| row.stdoutComplete !== 1
|
|
1200
|
+
|| row.stderrComplete !== 1
|
|
1201
|
+
|| typeof row.stdoutSha256 !== "string"
|
|
1202
|
+
|| typeof row.stderrSha256 !== "string"
|
|
1203
|
+
|| typeof row.stdoutBytes !== "number"
|
|
1204
|
+
|| typeof row.stderrBytes !== "number"
|
|
1205
|
+
|| row.checkedCount !== null
|
|
1206
|
+
|| row.failedCount !== null
|
|
1207
|
+
) {
|
|
1208
|
+
throw gateRowInvalid(id, "complete command observation requires spawn, numeric exit, no signal, exit/close, complete streams, and stopCause none");
|
|
1209
|
+
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (
|
|
1213
|
+
row.spawned !== 0
|
|
1214
|
+
|| row.pid !== null
|
|
1215
|
+
|| row.exitCode !== null
|
|
1216
|
+
|| row.signal !== null
|
|
1217
|
+
|| row.exitObserved !== 0
|
|
1218
|
+
|| row.closeObserved !== 0
|
|
1219
|
+
|| row.stopCause !== "none"
|
|
1220
|
+
|| row.signalsAttempted !== "none"
|
|
1221
|
+
|| row.stdoutSha256 !== null
|
|
1222
|
+
|| row.stdoutBytes !== null
|
|
1223
|
+
|| row.stdoutComplete !== null
|
|
1224
|
+
|| row.stderrSha256 !== null
|
|
1225
|
+
|| row.stderrBytes !== null
|
|
1226
|
+
|| row.stderrComplete !== null
|
|
1227
|
+
|| typeof row.checkedCount !== "number"
|
|
1228
|
+
|| typeof row.failedCount !== "number"
|
|
1229
|
+
|| row.checkedCount < 0
|
|
1230
|
+
|| row.failedCount < 0
|
|
1231
|
+
|| row.failedCount > row.checkedCount
|
|
1232
|
+
) {
|
|
1233
|
+
throw gateRowInvalid(id, "complete artifacts observation requires consistent counts and no command facts");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export function computeGateEvidenceOutcome(
|
|
1238
|
+
kind: VnextGateKind,
|
|
1239
|
+
expect: VnextGateExpect,
|
|
1240
|
+
observation: VnextGateObservationFacts,
|
|
1241
|
+
): VnextGateEvidenceOutcome {
|
|
1242
|
+
assertClosedGateObservation(kind, observation);
|
|
1243
|
+
if (observation.completeness !== "complete") {
|
|
1244
|
+
throw gateRowInvalid("observation", "evaluated settlement requires a complete observation");
|
|
1245
|
+
}
|
|
1246
|
+
if (kind === "artifacts-exist") {
|
|
1247
|
+
if (expect === "fail") throw gateRowInvalid("artifacts-exist", "artifacts-exist cannot expect fail");
|
|
1248
|
+
return observation.failedCount === 0 ? "passed" : "implementation-failure";
|
|
1249
|
+
}
|
|
1250
|
+
if (expect === "pass") return observation.exitCode === 0 ? "passed" : "implementation-failure";
|
|
1251
|
+
return observation.exitCode === 0 ? "repro-missing" : "passed";
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function gateRowInvalid(file: string, message: string): VnextConfigError {
|
|
1255
|
+
return runtimeError("gate_row_invalid", file, message);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function assertExactRowKeys(row: object, keys: readonly string[], id: string): void {
|
|
1259
|
+
const actual = Object.keys(row);
|
|
1260
|
+
if (actual.length !== keys.length || keys.some((key) => !Object.prototype.hasOwnProperty.call(row, key))) {
|
|
1261
|
+
throw gateRowInvalid(id, "gate row does not have the exact key set");
|
|
1262
|
+
}
|
|
1263
|
+
for (const key of actual) {
|
|
1264
|
+
if (FORBIDDEN_TEXT_KEYS.has(key)) throw gateRowInvalid(id, "gate row must not carry raw stdout/stderr/error text");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function assertId(value: string, prefix: string, id: string, label: string): void {
|
|
1269
|
+
if (typeof value !== "string" || !value.startsWith(prefix) || !OPAQUE.test(value)) {
|
|
1270
|
+
throw gateRowInvalid(id, `${label} is not a valid ${prefix} opaque id`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function assertSha(value: string, id: string, label: string): void {
|
|
1275
|
+
if (typeof value !== "string" || !SHA256.test(value)) throw gateRowInvalid(id, `${label} is not a sha256 hash`);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function assertFlag(value: unknown, id: string, label: string): asserts value is 0 | 1 {
|
|
1279
|
+
if (value !== 0 && value !== 1) throw gateRowInvalid(id, `${label} must be 0 or 1`);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function assertNullInt(value: unknown, id: string, label: string): void {
|
|
1283
|
+
if (value !== null && (typeof value !== "number" || !Number.isInteger(value))) {
|
|
1284
|
+
throw gateRowInvalid(id, `${label} must be an integer or null`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function assertGateAttemptRow(row: VnextGateAttemptRow): void {
|
|
1289
|
+
assertExactRowKeys(row, GATE_ATTEMPT_KEYS, row.attemptId);
|
|
1290
|
+
assertId(row.attemptId, "att_", row.attemptId, "attemptId");
|
|
1291
|
+
assertId(row.effectId, "eff_", row.attemptId, "effectId");
|
|
1292
|
+
assertId(row.intentEventId, "evt_", row.attemptId, "intentEventId");
|
|
1293
|
+
assertSha(row.gateDefinitionHash, row.attemptId, "gateDefinitionHash");
|
|
1294
|
+
assertSha(row.registryHash, row.attemptId, "registryHash");
|
|
1295
|
+
assertSha(row.runPlanHash, row.attemptId, "runPlanHash");
|
|
1296
|
+
if (row.producerId !== "kxm-gate") throw gateRowInvalid(row.attemptId, "producerId must be kxm-gate");
|
|
1297
|
+
if (row.gateKind !== "command" && row.gateKind !== "artifacts-exist") {
|
|
1298
|
+
throw gateRowInvalid(row.attemptId, "gateKind is invalid");
|
|
1299
|
+
}
|
|
1300
|
+
if (row.expect !== "pass" && row.expect !== "fail") throw gateRowInvalid(row.attemptId, "expect is invalid");
|
|
1301
|
+
if (row.gateKind === "artifacts-exist" && row.expect === "fail") {
|
|
1302
|
+
throw gateRowInvalid(row.attemptId, "artifacts-exist cannot expect fail");
|
|
1303
|
+
}
|
|
1304
|
+
if (typeof row.controlProjectKey !== "string" || row.controlProjectKey.length === 0) {
|
|
1305
|
+
throw gateRowInvalid(row.attemptId, "controlProjectKey is required");
|
|
1306
|
+
}
|
|
1307
|
+
if (gateRowContentHash("gate_attempts", row) !== row.contentHash) {
|
|
1308
|
+
throw gateRowInvalid(row.attemptId, "contentHash does not match the canonical row");
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function assertGateObservationRow(row: VnextGateObservationRow): void {
|
|
1313
|
+
assertExactRowKeys(row, GATE_OBSERVATION_KEYS, row.observationId);
|
|
1314
|
+
assertId(row.observationId, "obs_", row.observationId, "observationId");
|
|
1315
|
+
assertId(row.attemptId, "att_", row.observationId, "attemptId");
|
|
1316
|
+
assertId(row.effectId, "eff_", row.observationId, "effectId");
|
|
1317
|
+
assertId(row.recordedEventId, "evt_", row.observationId, "recordedEventId");
|
|
1318
|
+
if (!["complete", "incomplete", "no-start"].includes(row.completeness)) {
|
|
1319
|
+
throw gateRowInvalid(row.observationId, "completeness is invalid");
|
|
1320
|
+
}
|
|
1321
|
+
assertFlag(row.spawned, row.observationId, "spawned");
|
|
1322
|
+
assertFlag(row.exitObserved, row.observationId, "exitObserved");
|
|
1323
|
+
assertFlag(row.closeObserved, row.observationId, "closeObserved");
|
|
1324
|
+
assertNullInt(row.pid, row.observationId, "pid");
|
|
1325
|
+
assertNullInt(row.exitCode, row.observationId, "exitCode");
|
|
1326
|
+
assertNullInt(row.stdoutBytes, row.observationId, "stdoutBytes");
|
|
1327
|
+
assertNullInt(row.stderrBytes, row.observationId, "stderrBytes");
|
|
1328
|
+
assertNullInt(row.checkedCount, row.observationId, "checkedCount");
|
|
1329
|
+
assertNullInt(row.failedCount, row.observationId, "failedCount");
|
|
1330
|
+
if (typeof row.elapsedMs !== "number" || !Number.isInteger(row.elapsedMs) || row.elapsedMs < 0) {
|
|
1331
|
+
throw gateRowInvalid(row.observationId, "elapsedMs is invalid");
|
|
1332
|
+
}
|
|
1333
|
+
if (row.stdoutSha256 !== null) assertSha(row.stdoutSha256, row.observationId, "stdoutSha256");
|
|
1334
|
+
if (row.stderrSha256 !== null) assertSha(row.stderrSha256, row.observationId, "stderrSha256");
|
|
1335
|
+
if (row.stdoutComplete !== null && row.stdoutComplete !== 0 && row.stdoutComplete !== 1) {
|
|
1336
|
+
throw gateRowInvalid(row.observationId, "stdoutComplete must be 0, 1, or null");
|
|
1337
|
+
}
|
|
1338
|
+
if (row.stderrComplete !== null && row.stderrComplete !== 0 && row.stderrComplete !== 1) {
|
|
1339
|
+
throw gateRowInvalid(row.observationId, "stderrComplete must be 0, 1, or null");
|
|
1340
|
+
}
|
|
1341
|
+
if (gateRowContentHash("gate_observations", row) !== row.contentHash) {
|
|
1342
|
+
throw gateRowInvalid(row.observationId, "contentHash does not match the canonical row");
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function assertGateEvidenceRow(row: VnextGateEvidenceRow): void {
|
|
1347
|
+
assertExactRowKeys(row, GATE_EVIDENCE_KEYS, row.evidenceId);
|
|
1348
|
+
assertId(row.evidenceId, "gev_", row.evidenceId, "evidenceId");
|
|
1349
|
+
assertId(row.attemptId, "att_", row.evidenceId, "attemptId");
|
|
1350
|
+
assertId(row.observationId, "obs_", row.evidenceId, "observationId");
|
|
1351
|
+
assertId(row.effectId, "eff_", row.evidenceId, "effectId");
|
|
1352
|
+
assertId(row.settledEventId, "evt_", row.evidenceId, "settledEventId");
|
|
1353
|
+
if (row.kind !== "gate") throw gateRowInvalid(row.evidenceId, "kind must be gate");
|
|
1354
|
+
if (!["passed", "implementation-failure", "repro-missing"].includes(row.outcome)) {
|
|
1355
|
+
throw gateRowInvalid(row.evidenceId, "outcome is invalid");
|
|
1356
|
+
}
|
|
1357
|
+
if (gateRowContentHash("gate_evidence", row) !== row.contentHash) {
|
|
1358
|
+
throw gateRowInvalid(row.evidenceId, "contentHash does not match the canonical row");
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function assertSharedIdentity(
|
|
1363
|
+
row: { runId: string; projectId: string; homeRuntimeId: string; stepId: string; stepAttempt: number; assignmentId: string; effectId: string },
|
|
1364
|
+
attempt: VnextGateAttemptRow,
|
|
1365
|
+
id: string,
|
|
1366
|
+
): void {
|
|
1367
|
+
if (
|
|
1368
|
+
row.runId !== attempt.runId
|
|
1369
|
+
|| row.projectId !== attempt.projectId
|
|
1370
|
+
|| row.homeRuntimeId !== attempt.homeRuntimeId
|
|
1371
|
+
|| row.stepId !== attempt.stepId
|
|
1372
|
+
|| row.stepAttempt !== attempt.stepAttempt
|
|
1373
|
+
|| row.assignmentId !== attempt.assignmentId
|
|
1374
|
+
|| row.effectId !== attempt.effectId
|
|
1375
|
+
) {
|
|
1376
|
+
throw gateRowInvalid(id, "row identity does not match the gate attempt");
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
type GateAttemptSql = {
|
|
1381
|
+
attempt_id: string;
|
|
1382
|
+
run_id: string;
|
|
1383
|
+
project_id: string;
|
|
1384
|
+
home_runtime_id: string;
|
|
1385
|
+
step_id: string;
|
|
1386
|
+
step_attempt: number;
|
|
1387
|
+
assignment_id: string;
|
|
1388
|
+
effect_id: string;
|
|
1389
|
+
gate_id: string;
|
|
1390
|
+
gate_kind: VnextGateKind;
|
|
1391
|
+
expect: VnextGateExpect;
|
|
1392
|
+
gate_definition_hash: string;
|
|
1393
|
+
registry_hash: string;
|
|
1394
|
+
run_plan_hash: string;
|
|
1395
|
+
control_project_key: string;
|
|
1396
|
+
producer_id: "kxm-gate";
|
|
1397
|
+
intent_event_id: string;
|
|
1398
|
+
content_hash: string;
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
function gateAttemptFromSql(row: GateAttemptSql): VnextGateAttemptRow {
|
|
1402
|
+
return {
|
|
1403
|
+
attemptId: row.attempt_id,
|
|
1404
|
+
runId: row.run_id,
|
|
1405
|
+
projectId: row.project_id,
|
|
1406
|
+
homeRuntimeId: row.home_runtime_id,
|
|
1407
|
+
stepId: row.step_id,
|
|
1408
|
+
stepAttempt: row.step_attempt,
|
|
1409
|
+
assignmentId: row.assignment_id,
|
|
1410
|
+
effectId: row.effect_id,
|
|
1411
|
+
gateId: row.gate_id,
|
|
1412
|
+
gateKind: row.gate_kind,
|
|
1413
|
+
expect: row.expect,
|
|
1414
|
+
gateDefinitionHash: row.gate_definition_hash,
|
|
1415
|
+
registryHash: row.registry_hash,
|
|
1416
|
+
runPlanHash: row.run_plan_hash,
|
|
1417
|
+
controlProjectKey: row.control_project_key,
|
|
1418
|
+
producerId: row.producer_id,
|
|
1419
|
+
intentEventId: row.intent_event_id,
|
|
1420
|
+
contentHash: row.content_hash,
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
type GateObservationSql = {
|
|
1425
|
+
observation_id: string;
|
|
1426
|
+
attempt_id: string;
|
|
1427
|
+
run_id: string;
|
|
1428
|
+
project_id: string;
|
|
1429
|
+
home_runtime_id: string;
|
|
1430
|
+
step_id: string;
|
|
1431
|
+
step_attempt: number;
|
|
1432
|
+
assignment_id: string;
|
|
1433
|
+
effect_id: string;
|
|
1434
|
+
completeness: VnextGateCompleteness;
|
|
1435
|
+
spawned: number;
|
|
1436
|
+
pid: number | null;
|
|
1437
|
+
exit_code: number | null;
|
|
1438
|
+
signal: string | null;
|
|
1439
|
+
exit_observed: number;
|
|
1440
|
+
close_observed: number;
|
|
1441
|
+
stop_cause: VnextGateStopCause;
|
|
1442
|
+
signals_attempted: VnextGateSignalsAttempted;
|
|
1443
|
+
error_class: VnextGateErrorClass | null;
|
|
1444
|
+
stdout_sha256: string | null;
|
|
1445
|
+
stdout_bytes: number | null;
|
|
1446
|
+
stdout_complete: number | null;
|
|
1447
|
+
stderr_sha256: string | null;
|
|
1448
|
+
stderr_bytes: number | null;
|
|
1449
|
+
stderr_complete: number | null;
|
|
1450
|
+
checked_count: number | null;
|
|
1451
|
+
failed_count: number | null;
|
|
1452
|
+
elapsed_ms: number;
|
|
1453
|
+
started_at: string;
|
|
1454
|
+
finished_at: string | null;
|
|
1455
|
+
recorded_event_id: string;
|
|
1456
|
+
content_hash: string;
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
function gateObservationFromSql(row: GateObservationSql): VnextGateObservationRow {
|
|
1460
|
+
return {
|
|
1461
|
+
observationId: row.observation_id,
|
|
1462
|
+
attemptId: row.attempt_id,
|
|
1463
|
+
runId: row.run_id,
|
|
1464
|
+
projectId: row.project_id,
|
|
1465
|
+
homeRuntimeId: row.home_runtime_id,
|
|
1466
|
+
stepId: row.step_id,
|
|
1467
|
+
stepAttempt: row.step_attempt,
|
|
1468
|
+
assignmentId: row.assignment_id,
|
|
1469
|
+
effectId: row.effect_id,
|
|
1470
|
+
completeness: row.completeness,
|
|
1471
|
+
spawned: row.spawned === 1 ? 1 : 0,
|
|
1472
|
+
pid: row.pid,
|
|
1473
|
+
exitCode: row.exit_code,
|
|
1474
|
+
signal: row.signal,
|
|
1475
|
+
exitObserved: row.exit_observed === 1 ? 1 : 0,
|
|
1476
|
+
closeObserved: row.close_observed === 1 ? 1 : 0,
|
|
1477
|
+
stopCause: row.stop_cause,
|
|
1478
|
+
signalsAttempted: row.signals_attempted,
|
|
1479
|
+
errorClass: row.error_class,
|
|
1480
|
+
stdoutSha256: row.stdout_sha256,
|
|
1481
|
+
stdoutBytes: row.stdout_bytes,
|
|
1482
|
+
stdoutComplete: row.stdout_complete === null ? null : row.stdout_complete === 1 ? 1 : 0,
|
|
1483
|
+
stderrSha256: row.stderr_sha256,
|
|
1484
|
+
stderrBytes: row.stderr_bytes,
|
|
1485
|
+
stderrComplete: row.stderr_complete === null ? null : row.stderr_complete === 1 ? 1 : 0,
|
|
1486
|
+
checkedCount: row.checked_count,
|
|
1487
|
+
failedCount: row.failed_count,
|
|
1488
|
+
elapsedMs: row.elapsed_ms,
|
|
1489
|
+
startedAt: row.started_at,
|
|
1490
|
+
finishedAt: row.finished_at,
|
|
1491
|
+
recordedEventId: row.recorded_event_id,
|
|
1492
|
+
contentHash: row.content_hash,
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
type GateEvidenceSql = {
|
|
1497
|
+
evidence_id: string;
|
|
1498
|
+
attempt_id: string;
|
|
1499
|
+
observation_id: string;
|
|
1500
|
+
run_id: string;
|
|
1501
|
+
project_id: string;
|
|
1502
|
+
home_runtime_id: string;
|
|
1503
|
+
step_id: string;
|
|
1504
|
+
step_attempt: number;
|
|
1505
|
+
assignment_id: string;
|
|
1506
|
+
effect_id: string;
|
|
1507
|
+
evidence_key: string | null;
|
|
1508
|
+
kind: "gate";
|
|
1509
|
+
expect: VnextGateExpect;
|
|
1510
|
+
outcome: VnextGateEvidenceOutcome;
|
|
1511
|
+
settled_event_id: string;
|
|
1512
|
+
content_hash: string;
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
function gateEvidenceFromSql(row: GateEvidenceSql): VnextGateEvidenceRow {
|
|
1516
|
+
return {
|
|
1517
|
+
evidenceId: row.evidence_id,
|
|
1518
|
+
attemptId: row.attempt_id,
|
|
1519
|
+
observationId: row.observation_id,
|
|
1520
|
+
runId: row.run_id,
|
|
1521
|
+
projectId: row.project_id,
|
|
1522
|
+
homeRuntimeId: row.home_runtime_id,
|
|
1523
|
+
stepId: row.step_id,
|
|
1524
|
+
stepAttempt: row.step_attempt,
|
|
1525
|
+
assignmentId: row.assignment_id,
|
|
1526
|
+
effectId: row.effect_id,
|
|
1527
|
+
evidenceKey: row.evidence_key,
|
|
1528
|
+
kind: row.kind,
|
|
1529
|
+
expect: row.expect,
|
|
1530
|
+
outcome: row.outcome,
|
|
1531
|
+
settledEventId: row.settled_event_id,
|
|
1532
|
+
contentHash: row.content_hash,
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
type CapabilitySqlRow = {
|
|
1537
|
+
attempt_id: string;
|
|
1538
|
+
run_id: string;
|
|
1539
|
+
assignment_id: string;
|
|
1540
|
+
step_id: string;
|
|
1541
|
+
step_attempt: number;
|
|
1542
|
+
producer_id: string;
|
|
1543
|
+
capability_hash: string;
|
|
1544
|
+
state: VnextAttemptCapabilityRow["state"];
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
function capabilityFromRow(row: CapabilitySqlRow | undefined): VnextAttemptCapabilityRow | undefined {
|
|
1548
|
+
return row
|
|
1549
|
+
? {
|
|
1550
|
+
attemptId: row.attempt_id,
|
|
1551
|
+
runId: row.run_id,
|
|
1552
|
+
assignmentId: row.assignment_id,
|
|
1553
|
+
stepId: row.step_id,
|
|
1554
|
+
stepAttempt: row.step_attempt,
|
|
1555
|
+
producerId: row.producer_id,
|
|
1556
|
+
capabilityHash: row.capability_hash,
|
|
1557
|
+
state: row.state,
|
|
1558
|
+
}
|
|
1559
|
+
: undefined;
|
|
1560
|
+
}
|