@gr8ful/spf 0.15.0 → 0.17.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.
Files changed (64) hide show
  1. package/README.md +15 -5
  2. package/assets/skill/references/config.md +9 -5
  3. package/assets/skill/references/observability.md +57 -12
  4. package/assets/templates/ts-opencode.spf.config.yaml +54 -0
  5. package/dist/chains/index.js +1 -1
  6. package/dist/chains/simple_sdlc.d.ts +2 -2
  7. package/dist/chains/simple_sdlc.js +13 -13
  8. package/dist/chains/steps.d.ts +2 -2
  9. package/dist/chains/steps.js +35 -19
  10. package/dist/cli/commands/abort.d.ts +1 -1
  11. package/dist/cli/commands/abort.js +30 -3
  12. package/dist/cli/commands/doctor.js +109 -8
  13. package/dist/cli/commands/estimate.js +3 -3
  14. package/dist/cli/commands/events.js +4 -4
  15. package/dist/cli/commands/fanout.js +93 -21
  16. package/dist/cli/commands/loop.js +31 -32
  17. package/dist/cli/commands/migrate.js +8 -1
  18. package/dist/cli/commands/phases.js +2 -2
  19. package/dist/cli/commands/sessions.js +2 -2
  20. package/dist/cli/commands/trace.d.ts +28 -8
  21. package/dist/cli/commands/trace.js +28 -15
  22. package/dist/cli/commands/ui.js +15 -5
  23. package/dist/cli/commands/watch.js +27 -27
  24. package/dist/cli/index.js +3 -1
  25. package/dist/cli/interview.d.ts +1 -0
  26. package/dist/cli/interview.js +86 -4
  27. package/dist/core/agent_opencode.d.ts +247 -0
  28. package/dist/core/agent_opencode.js +590 -0
  29. package/dist/core/agents.d.ts +12 -12
  30. package/dist/core/agents.js +113 -46
  31. package/dist/core/console.d.ts +12 -12
  32. package/dist/core/console.js +25 -25
  33. package/dist/core/data_types.d.ts +126 -12
  34. package/dist/core/data_types.js +101 -4
  35. package/dist/core/fanout.d.ts +1 -1
  36. package/dist/core/fanout.js +1 -1
  37. package/dist/core/gates.js +14 -1
  38. package/dist/core/paths.d.ts +41 -4
  39. package/dist/core/paths.js +32 -3
  40. package/dist/core/quality.d.ts +7 -7
  41. package/dist/core/quality.js +16 -10
  42. package/dist/core/runner.d.ts +9 -3
  43. package/dist/core/runner.js +39 -27
  44. package/dist/core/session.d.ts +2 -2
  45. package/dist/core/session.js +39 -18
  46. package/dist/core/sqlite.d.ts +14 -7
  47. package/dist/core/sqlite.js +14 -7
  48. package/dist/core/trace_db.d.ts +118 -0
  49. package/dist/core/trace_db.js +278 -0
  50. package/dist/core/tracer.d.ts +64 -34
  51. package/dist/core/tracer.js +141 -69
  52. package/dist/core/watch.d.ts +4 -4
  53. package/dist/core/watch.js +2 -2
  54. package/dist/ui/server/app.js +10 -10
  55. package/dist/ui/server/db.d.ts +89 -21
  56. package/dist/ui/server/db.js +235 -99
  57. package/dist/ui/server/serve.d.ts +5 -1
  58. package/dist/ui/server/serve.js +4 -5
  59. package/package.json +1 -1
  60. package/web/assets/index-CQ3k1Y1-.css +1 -0
  61. package/web/assets/index-CU8tom6S.js +21 -0
  62. package/web/index.html +2 -2
  63. package/web/assets/index-CRujNW-1.js +0 -11
  64. package/web/assets/index-Cto6nuQL.css +0 -1
@@ -0,0 +1,278 @@
1
+ /**
2
+ * TraceDb: the async storage interface the trace database (`Tracer`'s writes,
3
+ * `SfDb`'s reads) is built against — one interface, two backends.
4
+ *
5
+ * WHY ASYNC, WHEN `core/sqlite.ts`'s `Database` IS SYNCHRONOUS: Cloudflare
6
+ * D1's only way to reach a database from a plain Node process (this CLI —
7
+ * not a Cloudflare Worker, which would get a real binding) is its HTTP REST
8
+ * API — a network call, inescapably async. Rather than fake synchrony over
9
+ * that (shelling out to curl, a busy-wait/sync-XHR trick), every caller
10
+ * upstream of storage (`Tracer`'s write methods, `SfDb`'s read methods) is
11
+ * async too — see those modules' own headers for how far that propagates.
12
+ * `LocalTraceDb` below pays that cost for nothing (every call still runs
13
+ * synchronously, immediately; only the return value is wrapped as a settled
14
+ * `Promise`) so the local path stays byte-for-byte the same behavior it
15
+ * always had, just awaited.
16
+ *
17
+ * `query(sql)` mirrors `core/sqlite.ts`'s `Statement` shape exactly, only
18
+ * every method returns a `Promise` — so a caller migrating from `Database`
19
+ * changes `.get(...)` to `await .get(...)` and nothing else.
20
+ */
21
+ import { mkdirSync } from "node:fs";
22
+ import path from "node:path";
23
+ import { Database } from "./sqlite.js";
24
+ /**
25
+ * Bound on one D1 HTTP request, same `AbortController` + unref'd `setTimeout`
26
+ * pattern `core/otel.ts`'s `flush()` uses for its own fetch calls — mirrored
27
+ * rather than reinvented (see `D1TraceDb.post` below). Set well above otel's
28
+ * own `SEND_TIMEOUT_MS` (2s): telemetry there is lossy and fire-and-forget,
29
+ * so a short timeout just means "drop this batch." A D1 query here is a
30
+ * REAL trace write/read a caller is awaiting — too short a timeout would
31
+ * turn ordinary network jitter into spurious failures — but it must still be
32
+ * BOUNDED, or a hung Cloudflare connection hangs the ADW run indefinitely.
33
+ */
34
+ const D1_REQUEST_TIMEOUT_MS = 15_000;
35
+ // ── local (sqlite, via core/sqlite.ts's synchronous Database) ──────────────
36
+ /**
37
+ * A thin async wrapper over the existing synchronous `Database` — every call
38
+ * is dispatched immediately, synchronously, and its result (or thrown error)
39
+ * is handed back as an already-settled `Promise` (via an `async` wrapper, so
40
+ * a synchronous throw becomes a rejection rather than escaping the `Promise`
41
+ * contract). Zero behavior change from `Database` itself; only the signature
42
+ * is async, so this backend and `D1TraceDb` satisfy the exact same `TraceDb`
43
+ * interface.
44
+ *
45
+ * `core/sqlite.ts` is deliberately left untouched — `cli/commands/abort.ts`
46
+ * and `cli/commands/migrate.ts` still open a plain `Database` directly for
47
+ * local-file-specific operations (marking a session aborted, physically
48
+ * relocating the db file) that have no D1 equivalent. See those files' own
49
+ * D1 guards.
50
+ */
51
+ export class LocalTraceDb {
52
+ db;
53
+ constructor(dbPath, options) {
54
+ if (!options?.readonly)
55
+ mkdirSync(path.dirname(dbPath), { recursive: true });
56
+ this.db = new Database(dbPath, options);
57
+ }
58
+ query(sql) {
59
+ const stmt = this.db.query(sql);
60
+ // `async` (not a bare `Promise.resolve(...)` wrapper) so a synchronous
61
+ // throw from the underlying `Database` call — a readonly-connection
62
+ // write, a constraint violation — becomes a REJECTED promise like every
63
+ // other `TraceDb` method, rather than an exception thrown synchronously
64
+ // out of a function whose declared return type is `Promise<...>`. A
65
+ // caller that does `const p = db.query(sql).run(); await p` (not
66
+ // `await db.query(sql).run()` inline) would otherwise never get the
67
+ // chance to catch it.
68
+ return {
69
+ get: async (...params) => stmt.get(...params),
70
+ all: async (...params) => stmt.all(...params),
71
+ run: async (...params) => stmt.run(...params),
72
+ };
73
+ }
74
+ async exec(sql) {
75
+ this.db.exec(sql);
76
+ }
77
+ async close() {
78
+ this.db.close();
79
+ }
80
+ }
81
+ // ── remote (Cloudflare D1, via its HTTP REST API) ──────────────────────────
82
+ /**
83
+ * D1 IS NOT LOCAL WAL SQLITE — DO NOT ASSUME THE SAME LIVE-READ GUARANTEE.
84
+ *
85
+ * `core/tracer.ts`'s header describes the local backend's contract: WAL mode
86
+ * lets `spf ui` read the trace db WHILE an ADW process is still writing to
87
+ * it, in the same instant, because both sides share one file on one
88
+ * filesystem. A D1-backed repo has no such single file to share — `spf ui`
89
+ * and a running chain each speak to D1 over independent HTTP calls, and (per
90
+ * Cloudflare's own docs, https://developers.cloudflare.com/d1/best-practices/read-replication/)
91
+ * a D1 database with read replication enabled offers SEQUENTIAL consistency,
92
+ * not read-your-own-writes by default: a read immediately after a write can
93
+ * land on a replica that has not caught up yet. D1's Sessions API closes that
94
+ * gap with a "bookmark" a caller can pin subsequent reads to, but this
95
+ * adapter does not use it (a bookmark is a client-scoped promise across ONE
96
+ * lightweight connection; a Tracer that mints a fresh request per write and
97
+ * an SfDb serving unrelated browser requests have no session to share it
98
+ * through). This is a deliberate, documented trade-off (SPF #66) — not a bug
99
+ * to fix here: a D1-backed repo's UI may briefly show a slightly-stale trace
100
+ * while a chain is actively writing. Nothing about phase/gate/run OUTCOMES
101
+ * depends on that live read — see tracer.ts's header, "SQLite is the source
102
+ * of truth; nothing downstream of it can affect a phase, a gate, or a run
103
+ * outcome" holds exactly the same way against D1.
104
+ */
105
+ export class D1TraceDb {
106
+ accountId;
107
+ apiToken;
108
+ databaseId;
109
+ fetchImpl;
110
+ baseUrl;
111
+ constructor(config,
112
+ /** Injectable so tests never touch the real network — defaults to the global `fetch`. */
113
+ fetchImpl = fetch) {
114
+ this.databaseId = config.database_id;
115
+ this.accountId = readEnv(config.account_id_env, "D1 account id");
116
+ this.apiToken = readEnv(config.api_token_env, "D1 API token");
117
+ this.fetchImpl = fetchImpl;
118
+ this.baseUrl = `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/d1/database/${this.databaseId}/query`;
119
+ }
120
+ query(sql) {
121
+ return {
122
+ get: async (...params) => {
123
+ const rows = await this.runOne(sql, params);
124
+ return rows.results[0] ?? null;
125
+ },
126
+ all: async (...params) => {
127
+ const rows = await this.runOne(sql, params);
128
+ return rows.results;
129
+ },
130
+ run: async (...params) => {
131
+ const rows = await this.runOne(sql, params);
132
+ return { changes: rows.meta.changes ?? 0, lastInsertRowid: rows.meta.last_row_id ?? 0 };
133
+ },
134
+ };
135
+ }
136
+ /**
137
+ * D1's HTTP `/query` endpoint accepts exactly one statement per `sql`
138
+ * field (see the Workers binding's `prepare()`/`batch()` split — the HTTP
139
+ * endpoint's single-request shape mirrors `prepare()`, not `exec()`);
140
+ * `exec()` here — schema DDL and additive `ALTER TABLE` migrations, always
141
+ * multiple statements — splits the text into individual statements and
142
+ * sends them as one `batch` request instead, which D1 documents as
143
+ * running sequentially and atomically. The split is naive (`;` at
144
+ * statement end, one statement per line-ish chunk) because this only ever
145
+ * runs against `tracer.ts`'s own hand-written `SCHEMA`/`MIGRATIONS`
146
+ * constants — never arbitrary or user-supplied SQL.
147
+ */
148
+ async exec(sql) {
149
+ const statements = splitStatements(sql);
150
+ if (statements.length === 0)
151
+ return;
152
+ const body = { batch: statements.map((s) => ({ sql: s })) };
153
+ await this.post(body);
154
+ }
155
+ close() {
156
+ // Stateless HTTP — no connection held open to release.
157
+ return Promise.resolve();
158
+ }
159
+ async runOne(sql, params) {
160
+ const body = { sql, params: params.length > 0 ? params : undefined };
161
+ const results = await this.post(body);
162
+ const first = results[0];
163
+ if (!first)
164
+ throw new Error(`D1 query returned no result set: ${sql}`);
165
+ return { results: (first.results ?? []), meta: first.meta ?? {} };
166
+ }
167
+ async post(body) {
168
+ // Same shape as `otel.ts`'s `flush()`: an `AbortController` whose timer
169
+ // is unref'd (never the reason this process lingers) and always cleared,
170
+ // win or lose. Unlike otel, a timeout here IS a thrown error — this
171
+ // request is a real trace read/write a caller is awaiting, not a
172
+ // best-effort fire-and-forget send.
173
+ //
174
+ // The timer is NOT cleared the moment `fetch` resolves with a `Response`
175
+ // — resolving only means headers have arrived; the body can still be
176
+ // in flight. `response.text()` is read inside this SAME try, under the
177
+ // SAME `controller.signal`, so a server that sends headers and then
178
+ // stalls the body still gets aborted at `D1_REQUEST_TIMEOUT_MS` (per the
179
+ // fetch spec, aborting a request's signal also aborts an in-progress
180
+ // body read) instead of hanging forever — exactly what this timeout
181
+ // exists to prevent.
182
+ const controller = new AbortController();
183
+ const timer = setTimeout(() => controller.abort(), D1_REQUEST_TIMEOUT_MS);
184
+ timer.unref?.();
185
+ let response;
186
+ let text;
187
+ try {
188
+ response = await this.fetchImpl(this.baseUrl, {
189
+ method: "POST",
190
+ headers: {
191
+ Authorization: `Bearer ${this.apiToken}`,
192
+ "Content-Type": "application/json",
193
+ },
194
+ body: JSON.stringify(body),
195
+ signal: controller.signal,
196
+ });
197
+ text = await response.text();
198
+ }
199
+ catch (error) {
200
+ const timedOut = error.name === "AbortError";
201
+ throw new Error(timedOut
202
+ ? `D1 request timed out after ${D1_REQUEST_TIMEOUT_MS}ms`
203
+ : `D1 request failed: ${error.message ?? error}`);
204
+ }
205
+ finally {
206
+ clearTimeout(timer);
207
+ }
208
+ let parsed;
209
+ try {
210
+ parsed = text ? JSON.parse(text) : undefined;
211
+ }
212
+ catch {
213
+ // A non-JSON body (e.g. a Cloudflare edge error page) falls through to
214
+ // the HTTP-status branch below with the raw text as context.
215
+ }
216
+ if (!response.ok) {
217
+ const message = parsed?.errors?.map((e) => e.message).join("; ") || text.slice(0, 500) || response.statusText;
218
+ throw new Error(`D1 HTTP ${response.status}: ${message}`);
219
+ }
220
+ if (!parsed || parsed.success !== true) {
221
+ const message = parsed?.errors?.map((e) => e.message).join("; ") || "D1 reported failure with no error message";
222
+ throw new Error(`D1 query failed: ${message}`);
223
+ }
224
+ // A `success:true` envelope that omits `result` entirely (seen from some
225
+ // D1 edge responses) is still success — treat it as zero statements
226
+ // rather than throwing `TypeError: parsed.result is not iterable`.
227
+ const result = parsed.result ?? [];
228
+ for (const item of result) {
229
+ if (item.success === false) {
230
+ throw new Error(`D1 statement failed: ${parsed.errors?.map((e) => e.message).join("; ") || "unknown error"}`);
231
+ }
232
+ }
233
+ return result;
234
+ }
235
+ }
236
+ /**
237
+ * Split a `;`-terminated multi-statement string into individual statements,
238
+ * dropping blanks and comment-only lines.
239
+ *
240
+ * `tracer.ts`'s `SCHEMA` carries `--`-style trailing comments on several
241
+ * column definitions (e.g. `archived INTEGER DEFAULT 0 -- ...; never by a
242
+ * run`), and those comments themselves contain `;` — a naive `sql.split(";")`
243
+ * shreds the enclosing `CREATE TABLE` into fragments right in the middle of
244
+ * a comment, producing statements no SQL engine accepts (an unterminated
245
+ * `CREATE TABLE ... (`, an orphaned comment tail). This strips every `--` to
246
+ * end-of-line FIRST, so a `;` inside a comment can no longer be mistaken for
247
+ * a statement terminator, then splits what's left. Still only sound for
248
+ * `tracer.ts`'s own hand-written SCHEMA/MIGRATIONS constants — not general
249
+ * SQL (a `;` or `--` inside a quoted string literal is not accounted for,
250
+ * and neither constant ever uses one).
251
+ */
252
+ function splitStatements(sql) {
253
+ const withoutLineComments = sql
254
+ .split("\n")
255
+ .map((line) => {
256
+ const commentAt = line.indexOf("--");
257
+ return commentAt === -1 ? line : line.slice(0, commentAt);
258
+ })
259
+ .join("\n");
260
+ return withoutLineComments
261
+ .split(";")
262
+ .map((s) => s.trim())
263
+ .filter((s) => s.length > 0);
264
+ }
265
+ function readEnv(name, what) {
266
+ const value = (process.env[name] ?? "").trim();
267
+ if (!value) {
268
+ throw new Error(`${what} is not set — the observability.db config names env var ${JSON.stringify(name)}, but it is empty or unset`);
269
+ }
270
+ return value;
271
+ }
272
+ /** The one place `Tracer` and `SfDb` both go to get the right backend for `observability.db`'s resolved kind. */
273
+ export function createTraceDb(resolved, options) {
274
+ if (resolved.kind === "sqlite") {
275
+ return new LocalTraceDb(resolved.path, options?.readonly ? { readonly: true } : undefined);
276
+ }
277
+ return new D1TraceDb(resolved, options?.fetchImpl);
278
+ }
@@ -1,13 +1,28 @@
1
1
  /**
2
- * Tracer: every event lands in JSONL and SQLite AS IT HAPPENS.
2
+ * Tracer: every event lands in JSONL and the trace db AS IT HAPPENS.
3
3
  *
4
- * Files are the raw record; spf.db is the queryable mirror the UI polls.
5
- * WAL mode so the UI can read while ADW processes write.
4
+ * Files are the raw record; the trace db (local sqlite, or remote D1 see
5
+ * `core/trace_db.ts`) is the queryable mirror the UI polls. For the LOCAL
6
+ * backend, WAL mode lets the UI read while ADW processes write, in the same
7
+ * instant, because both sides share one file on one filesystem.
8
+ *
9
+ * THE D1 BACKEND DOES NOT MAKE THAT SAME PROMISE — see `core/trace_db.ts`'s
10
+ * `D1TraceDb` doc comment for the consistency model it actually has and why
11
+ * this is a deliberate, documented trade-off (SPF #66) rather than a gap to
12
+ * close here.
6
13
  *
7
14
  * No push transport in the CONTROL flow — that is always, still, and only:
8
- * agents -> sqlite -> web ui. SQLite is the source of truth; nothing
15
+ * agents -> trace db -> web ui. The trace db is the source of truth; nothing
9
16
  * downstream of it can affect a phase, a gate, or a run outcome.
10
17
  *
18
+ * WHY EVERY WRITE METHOD BELOW IS ASYNC: the D1 backend's only way to reach
19
+ * a database from this plain Node CLI is its HTTP REST API — a network call.
20
+ * `core/trace_db.ts`'s `TraceDb` interface is async for that reason, and
21
+ * every write method here is a thin `await this.db...` over it — see that
22
+ * module's header. For the LOCAL backend this costs nothing but the syntax:
23
+ * `LocalTraceDb` wraps the exact same synchronous `Database` calls this file
24
+ * always made, immediately resolved.
25
+ *
11
26
  * The one amendment: when (and only when) `observability.otel` is configured,
12
27
  * each write method below ends with a single fan-out line to an optional
13
28
  * OtelExporter — a lossy, allowlisted PROJECTION of what was just written,
@@ -21,43 +36,58 @@
21
36
  * `processStart`/`processEnd` (pids) — have NO fan-out line on purpose. Do not
22
37
  * add one.
23
38
  */
24
- import { Database } from "./sqlite.ts";
25
- import type { AgentConfig, EventRecord, GateReport, Phase } from "./data_types.ts";
39
+ import type { AgentConfig, EventRecord, GateReport, NormalizedObservabilityDb, Phase } from "./data_types.ts";
26
40
  import type { OtelExporter } from "./otel.ts";
41
+ import { type TraceDb } from "./trace_db.ts";
27
42
  export declare class Tracer {
28
- db: Database;
29
- dbPath: string;
43
+ db: TraceDb;
44
+ /** Absolute local sqlite path — `null` for a `kind:"d1"` config (`Console.sessionFinished`'s "db" line falls back to `runner.ts`'s `describeObservabilityDb` for a human label in that case). */
45
+ dbPath: string | null;
30
46
  eventsJsonl: string;
31
47
  /** `null` unless `observability.otel` is configured — see the header. */
32
48
  otel: OtelExporter | null;
33
- constructor(dbPath: string, eventsJsonl: string, otel?: OtelExporter | null);
49
+ private constructor();
50
+ /**
51
+ * Open (or create) the trace db, run schema + migrations, and return a
52
+ * ready-to-use Tracer. The one constructor path — a plain `new Tracer(...)`
53
+ * cannot do this work itself because opening a D1-backed db means awaited
54
+ * HTTP calls before the schema exists, and a constructor cannot be async.
55
+ *
56
+ * `dbConfig` accepts either a bare local sqlite path (sugar for
57
+ * `{kind:"sqlite",path}` — every test's shorthand, and what every caller
58
+ * before SPF #66 passed) or a normalized `observability.db` descriptor
59
+ * (`paths.resolveDataPaths(...).db` — sqlite or d1).
60
+ */
61
+ static open(dbConfig: NormalizedObservabilityDb | string, eventsJsonl: string, otel?: OtelExporter | null, traceDbOptions?: {
62
+ fetchImpl?: typeof fetch;
63
+ }): Promise<Tracer>;
34
64
  /**
35
- * Close the sqlite handle. A one-shot CLI process never needs this — it
65
+ * Close the trace db handle. A one-shot CLI process never needs this — it
36
66
  * exits right after its one Tracer anyway — but `spf watch`'s daemon loop
37
- * builds a fresh Tracer (and a fresh `new Database(dbPath)`) per claimed
38
- * issue, in-process, for the life of the daemon; without this, every
39
- * issue's handle stayed open forever. Called from `session.ts`'s
40
- * `finalize()`, once a run's own dispatch has fully settled — see its
41
- * comment for why that timing is safe.
67
+ * builds a fresh Tracer (and a fresh local db connection, or a fresh
68
+ * stateless D1 client) per claimed issue, in-process, for the life of the
69
+ * daemon; without this, every issue's local handle stayed open forever.
70
+ * Called from `session.ts`'s `finalize()`, once a run's own dispatch has
71
+ * fully settled — see its comment for why that timing is safe. A no-op for
72
+ * `D1TraceDb` (stateless HTTP — nothing to release).
42
73
  */
43
- close(): void;
74
+ close(): Promise<void>;
44
75
  /** Additive column migrations, so a db from an older SPF still opens. */
45
76
  private migrate;
46
77
  /**
47
- * The ONE door to the optional otel projection, and the only reason a fan-out
48
- * line is safe to put at the end of a synchronous write method: it is a
49
- * no-op when unconfigured, and it swallows everything. An exporter bug, a
50
- * malformed span, an exhausted queue none of it may ever surface as a
51
- * failed phase, because export is not allowed to dispose of anything. The
78
+ * The ONE door to the optional otel projection. It is a no-op when
79
+ * unconfigured, and it swallows everything: an exporter bug, a malformed
80
+ * span, an exhausted queue — none of it may ever surface as a failed
81
+ * phase, because export is not allowed to dispose of anything. The
52
82
  * exporter's own methods are synchronous enqueues; the network happens later,
53
83
  * on an unref'd timer.
54
84
  */
55
85
  private fanOut;
56
- event(record: EventRecord): string;
57
- sessionStart(adwId: string, engineer: string, adwName?: string | null): void;
58
- sessionRequest(adwId: string, request: string): void;
59
- sessionFinish(adwId: string, ok: boolean): void;
60
- sessionAddUsage(adwId: string, tokens: number, cost: number): void;
86
+ event(record: EventRecord): Promise<string>;
87
+ sessionStart(adwId: string, engineer: string, adwName?: string | null): Promise<void>;
88
+ sessionRequest(adwId: string, request: string): Promise<void>;
89
+ sessionFinish(adwId: string, ok: boolean): Promise<void>;
90
+ sessionAddUsage(adwId: string, tokens: number, cost: number): Promise<void>;
61
91
  /**
62
92
  * Record a live process for this run.
63
93
  *
@@ -66,11 +96,11 @@ export declare class Tracer {
66
96
  * belongs to. Writing it here makes the trace the answer to "what is this
67
97
  * run running, and how do I stop it".
68
98
  */
69
- processStart(adwId: string, kind: string, name: string, pid: number, command: string): void;
99
+ processStart(adwId: string, kind: string, name: string, pid: number, command: string): Promise<void>;
70
100
  /** Mark the newest live row for this pid as finished. */
71
- processEnd(adwId: string, pid: number): void;
101
+ processEnd(adwId: string, pid: number): Promise<void>;
72
102
  /** Close out every live row for a run — called when the session ends. */
73
- processesEndAll(adwId: string): void;
103
+ processesEndAll(adwId: string): Promise<void>;
74
104
  /**
75
105
  * Highest seq already recorded for this session; 0 when it is new.
76
106
  *
@@ -79,11 +109,11 @@ export declare class Tracer {
79
109
  * ordering) and `phase_id` (silently overwriting a row through the
80
110
  * phase_upsert conflict clause).
81
111
  */
82
- maxPhaseSeq(adwId: string): number;
83
- phaseUpsert(phase: Phase): void;
84
- envelopeRow(phase: Phase, agent: string, outputType: string, payloadJson: string, valid: boolean, attempt: number): void;
112
+ maxPhaseSeq(adwId: string): Promise<number>;
113
+ phaseUpsert(phase: Phase): Promise<void>;
114
+ envelopeRow(phase: Phase, agent: string, outputType: string, payloadJson: string, valid: boolean, attempt: number): Promise<void>;
85
115
  /** The report carries both the verdict and the evidence behind it. */
86
- gateRow(phase: Phase, gate: string, report: GateReport, attempt: number): void;
116
+ gateRow(phase: Phase, gate: string, report: GateReport, attempt: number): Promise<void>;
87
117
  /**
88
118
  * The agent's config row is the source of truth for its label and color.
89
119
  *
@@ -91,5 +121,5 @@ export declare class Tracer {
91
121
  * wants one number per agent — the latest — and a session that runs the
92
122
  * same agent twice overwrites it, exactly like model and session_id.
93
123
  */
94
- agentSessionRow(adwId: string, agent: AgentConfig, sessionId: string, contextTokens?: number, contextWindow?: number): void;
124
+ agentSessionRow(adwId: string, agent: AgentConfig, sessionId: string, contextTokens?: number, contextWindow?: number): Promise<void>;
95
125
  }