@smithers-orchestrator/db 0.24.2 → 0.25.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/package.json +5 -5
- package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +1 -0
- package/src/adapter.js +240 -67
- package/src/assertJsonPayloadWithinBounds.js +1 -1
- package/src/assertNoReservedColumns.js +38 -0
- package/src/dialect.js +2 -31
- package/src/docWatcher.js +162 -0
- package/src/frame-codec.js +5 -1
- package/src/getSmithersSchemaSignature.js +70 -0
- package/src/index.d.ts +60 -12
- package/src/index.js +3 -0
- package/src/internal-schema/index.js +1 -0
- package/src/internal-schema/smithersDocs.js +27 -0
- package/src/internal-schema/smithersScorers.js +2 -0
- package/src/internal-schema.js +1 -0
- package/src/react-output.js +3 -10
- package/src/runState/DeriveRunStateInput.ts +4 -0
- package/src/runState/ReasonBlocked.ts +4 -1
- package/src/runState/RunState.ts +1 -0
- package/src/runState/computeRunStateFromRow.js +21 -0
- package/src/runState/deriveRunState.js +39 -10
- package/src/schema-migrations.js +346 -14
- package/src/sha256Hex.js +14 -0
- package/src/sql-message-storage.js +13 -0
- package/src/zodToCreateTableSQL.js +12 -4
- package/src/zodToTable.js +20 -5
- package/src/frame-codec/index.js +0 -15
- package/src/output/index.js +0 -14
- package/src/storage/InMemoryStorage.js +0 -484
- package/src/storage/StorageService.js +0 -7
- package/src/storage/StorageServiceShape.ts +0 -122
- package/src/storage/StorageServiceTypes.ts +0 -150
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sha256Hex } from "./sha256Hex.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Durability file-watcher seam for `_smithers_docs`.
|
|
7
|
+
*
|
|
8
|
+
* Watches a directory of `*.md` docs and upserts each into `_smithers_docs`
|
|
9
|
+
* (via a {@link import("./adapter.js").SmithersDb}), so a doc edited on disk —
|
|
10
|
+
* by a human, an agent, or a `git` checkout — surfaces through the gateway's
|
|
11
|
+
* `listTickets` RPC without a separate import step. The doc identity (`path` PK)
|
|
12
|
+
* is the filename WITHOUT its `.md` extension, matching the id a `createTicket`
|
|
13
|
+
* caller supplies, so a file and an RPC-created row for the same id reconcile to
|
|
14
|
+
* one row.
|
|
15
|
+
*
|
|
16
|
+
* Reconciliation is ONE-DIRECTIONAL (file → DB) and last-write-wins on a
|
|
17
|
+
* content-hash mismatch: a file is upserted only when `sha256(content)` differs
|
|
18
|
+
* from the stored `content_hash` (a no-op otherwise, so a noisy `fs.watch` does
|
|
19
|
+
* not churn the DB or bump `updated_at_ms`). The watcher NEVER writes the DB
|
|
20
|
+
* back out to disk and NEVER materializes a tombstone as a file.
|
|
21
|
+
*
|
|
22
|
+
* Soft-deletes (tombstones) are RESPECTED: a row with `deleted_at_ms` set is NOT
|
|
23
|
+
* resurrected merely because its `*.md` file still lingers on disk. The watcher
|
|
24
|
+
* compares the file's `sha256(content)` against the tombstoned row's stored
|
|
25
|
+
* `content_hash` — only a genuine post-deletion change (a real re-create / edit,
|
|
26
|
+
* i.e. a hash MISMATCH) revives the doc (file wins). An identical leftover file
|
|
27
|
+
* leaves the tombstone intact, so a deleted ticket stays deleted across re-syncs
|
|
28
|
+
* and gateway restarts. This prevents tombstone resurrection while preserving
|
|
29
|
+
* last-write-wins for live docs and legitimate re-creates.
|
|
30
|
+
*
|
|
31
|
+
* @param {import("./adapter.js").SmithersDb} adapter
|
|
32
|
+
* @param {{
|
|
33
|
+
* dir: string,
|
|
34
|
+
* kind?: string,
|
|
35
|
+
* defaultStatus?: string | null,
|
|
36
|
+
* nowMs?: () => number,
|
|
37
|
+
* onError?: (error: unknown) => void,
|
|
38
|
+
* }} options
|
|
39
|
+
* @returns {{ dir: string, sync: () => Promise<void>, syncFile: (file: string) => Promise<void>, close: () => void }}
|
|
40
|
+
*/
|
|
41
|
+
export function watchDocsDirectory(adapter, options) {
|
|
42
|
+
const dir = options.dir;
|
|
43
|
+
const kind = options.kind ?? "ticket";
|
|
44
|
+
const defaultStatus = options.defaultStatus ?? null;
|
|
45
|
+
const now = options.nowMs ?? (() => Date.now());
|
|
46
|
+
const onError = options.onError ?? ((error) => {
|
|
47
|
+
console.warn(`[docWatcher] ${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// The watched directory must exist before `fs.watch`; create it so wiring the
|
|
51
|
+
// seam against a not-yet-populated docs dir is safe (idempotent).
|
|
52
|
+
if (!existsSync(dir)) {
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Map a `*.md` filename to its doc `path` (id) — drop the extension. */
|
|
57
|
+
function pathForFile(file) {
|
|
58
|
+
return file.endsWith(".md") ? file.slice(0, -3) : file;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reconcile ONE `*.md` file into `_smithers_docs`. Reads the file, hashes it,
|
|
63
|
+
* and upserts only when the hash differs from the stored row (last-write-wins).
|
|
64
|
+
* A vanished/non-`.md`/unreadable file is ignored — the watcher never deletes
|
|
65
|
+
* a row from a missing file (soft-deletes are the gateway's job, not the
|
|
66
|
+
* filesystem's), so a transient editor swap can't tombstone a doc.
|
|
67
|
+
* @param {string} file
|
|
68
|
+
*/
|
|
69
|
+
async function syncFile(file) {
|
|
70
|
+
if (!file.endsWith(".md")) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const full = join(dir, file);
|
|
74
|
+
let content;
|
|
75
|
+
try {
|
|
76
|
+
if (!existsSync(full) || !statSync(full).isFile()) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
content = readFileSync(full, "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
onError(error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const path = pathForFile(file);
|
|
86
|
+
const contentHash = sha256Hex(content);
|
|
87
|
+
try {
|
|
88
|
+
const existing = await adapter.getDoc(path);
|
|
89
|
+
if (existing && existing.contentHash === contentHash) {
|
|
90
|
+
// Identical content to the stored row:
|
|
91
|
+
// - live row → no-op (a duplicate `fs.watch` event never bumps
|
|
92
|
+
// updated_at_ms or churns the DB);
|
|
93
|
+
// - tombstone → RESPECT the soft-delete. A lingering, unchanged
|
|
94
|
+
// file is NOT a recreation, so we leave `deleted_at_ms` intact
|
|
95
|
+
// instead of resurrecting the deleted doc. Only a genuine
|
|
96
|
+
// post-deletion edit (hash MISMATCH, handled below) revives it.
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await adapter.upsertDoc({
|
|
100
|
+
path,
|
|
101
|
+
kind,
|
|
102
|
+
content,
|
|
103
|
+
contentHash,
|
|
104
|
+
// Preserve an existing row's status; a brand-new file gets the default.
|
|
105
|
+
status: existing && existing.status != null ? existing.status : defaultStatus,
|
|
106
|
+
updatedAtMs: now(),
|
|
107
|
+
deletedAtMs: null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
onError(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reconcile every `*.md` currently in the directory. */
|
|
116
|
+
async function sync() {
|
|
117
|
+
let files;
|
|
118
|
+
try {
|
|
119
|
+
files = readdirSync(dir);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
onError(error);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
await syncFile(file);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Initial reconcile, then watch for changes. Each event re-syncs just the
|
|
131
|
+
// touched file; errors are reported, never thrown, so a watcher hiccup never
|
|
132
|
+
// takes down the gateway that wired it.
|
|
133
|
+
void sync();
|
|
134
|
+
|
|
135
|
+
let watcher = null;
|
|
136
|
+
try {
|
|
137
|
+
watcher = watch(dir, { persistent: false }, (_event, filename) => {
|
|
138
|
+
if (typeof filename === "string" && filename.length > 0) {
|
|
139
|
+
void syncFile(filename);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
void sync();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
watcher.on("error", onError);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
onError(error);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
dir,
|
|
153
|
+
sync,
|
|
154
|
+
syncFile,
|
|
155
|
+
close: () => {
|
|
156
|
+
if (watcher) {
|
|
157
|
+
watcher.close();
|
|
158
|
+
watcher = null;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
package/src/frame-codec.js
CHANGED
|
@@ -240,7 +240,11 @@ function applyOps(root, ops) {
|
|
|
240
240
|
current = insertAtPath(current, op.path, op.value);
|
|
241
241
|
continue;
|
|
242
242
|
}
|
|
243
|
-
|
|
243
|
+
if (op.op === "remove") {
|
|
244
|
+
current = removeAtPath(current, op.path);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Invalid frame delta op: ${String(op.op)}`);
|
|
244
248
|
}
|
|
245
249
|
return current;
|
|
246
250
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getTableName } from "drizzle-orm";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import * as internalSchema from "./internal-schema.js";
|
|
4
|
+
import { schemaSignature } from "./schema-signature.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {value is { queryAll: (statement: string, params?: readonly unknown[]) => Promise<readonly Record<string, unknown>[]> }}
|
|
9
|
+
*/
|
|
10
|
+
function isStorageLike(value) {
|
|
11
|
+
return Boolean(value &&
|
|
12
|
+
typeof value === "object" &&
|
|
13
|
+
typeof /** @type {any} */ (value).queryAll === "function");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {unknown} value
|
|
18
|
+
*/
|
|
19
|
+
function resolveStorage(value) {
|
|
20
|
+
if (isStorageLike(value)) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
const storage = /** @type {{ internalStorage?: unknown } | null | undefined} */ (value)?.internalStorage;
|
|
24
|
+
if (isStorageLike(storage)) {
|
|
25
|
+
return storage;
|
|
26
|
+
}
|
|
27
|
+
throw new Error("getSmithersSchemaSignature requires a SmithersDb or SqlMessageStorage instance");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns {Array<import("drizzle-orm").Table>}
|
|
32
|
+
*/
|
|
33
|
+
function internalTables() {
|
|
34
|
+
return Object.values(internalSchema)
|
|
35
|
+
.filter((table) => {
|
|
36
|
+
try {
|
|
37
|
+
return typeof getTableName(/** @type {any} */ (table)) === "string";
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.sort((left, right) => getTableName(/** @type {any} */ (left)).localeCompare(getTableName(/** @type {any} */ (right))));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return the durable Smithers schema head and a stable hash of the internal
|
|
48
|
+
* table catalog. The migration head is the client persistence schemaVersion;
|
|
49
|
+
* the signature lets clients or operators detect a same-head table-shape drift.
|
|
50
|
+
*
|
|
51
|
+
* @param {unknown} adapterOrStorage
|
|
52
|
+
* @returns {Promise<{ schemaVersion: string; signature: string; components: Record<string, string> }>}
|
|
53
|
+
*/
|
|
54
|
+
export async function getSmithersSchemaSignature(adapterOrStorage) {
|
|
55
|
+
const storage = resolveStorage(adapterOrStorage);
|
|
56
|
+
const rows = await storage.queryAll("SELECT id FROM _smithers_schema_migrations ORDER BY id");
|
|
57
|
+
const headId = rows
|
|
58
|
+
.map((row) => row.id)
|
|
59
|
+
.filter((id) => typeof id === "string")
|
|
60
|
+
.at(-1) ?? "0000";
|
|
61
|
+
const schemaVersion = headId.match(/^\d+/)?.[0] ?? headId;
|
|
62
|
+
const components = Object.fromEntries(internalTables().map((table) => [
|
|
63
|
+
getTableName(table),
|
|
64
|
+
schemaSignature(table),
|
|
65
|
+
]));
|
|
66
|
+
const signature = createHash("sha256")
|
|
67
|
+
.update(JSON.stringify({ headId, schemaVersion, components }))
|
|
68
|
+
.digest("hex");
|
|
69
|
+
return { schemaVersion, signature, components };
|
|
70
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -15,6 +15,21 @@ type SchemaRegistryEntry$1 = {
|
|
|
15
15
|
zodSchema: zod.ZodObject;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
type Dialect = "sqlite" | "postgres";
|
|
19
|
+
declare const SQLITE: "sqlite";
|
|
20
|
+
declare const POSTGRES: "postgres";
|
|
21
|
+
declare function isDialect(value: unknown): value is Dialect;
|
|
22
|
+
declare function quoteIdentifier(identifier: string): string;
|
|
23
|
+
declare function translatePlaceholders(dialect: Dialect, sql: string): string;
|
|
24
|
+
declare function columnType(dialect: Dialect, sqliteType: string): string;
|
|
25
|
+
declare function translateDdl(dialect: Dialect, ddl: string): string;
|
|
26
|
+
declare function tableColumnsSql(dialect: Dialect, table: string): {
|
|
27
|
+
sql: string;
|
|
28
|
+
params: ReadonlyArray<string>;
|
|
29
|
+
};
|
|
30
|
+
declare function beginTransactionSql(dialect: Dialect): string;
|
|
31
|
+
declare function jsonExtractText(dialect: Dialect, columnSql: string, jsonPath: string): string;
|
|
32
|
+
|
|
18
33
|
type SignalQuery$1 = {
|
|
19
34
|
signalName?: string;
|
|
20
35
|
correlationId?: string | null;
|
|
@@ -385,6 +400,9 @@ type SqliteParam = string | number | bigint | boolean | Uint8Array | null | unde
|
|
|
385
400
|
/**
|
|
386
401
|
* @typedef {{ cacheKey: string; createdAtMs?: number; nodeId: string; outputTable: string }} CacheRowLike
|
|
387
402
|
*/
|
|
403
|
+
/**
|
|
404
|
+
* @typedef {{ path: string; kind: string; content: string; contentHash: string; updatedAtMs: number; deletedAtMs?: number | null }} DocRow
|
|
405
|
+
*/
|
|
388
406
|
declare const DB_ALERT_ID_MAX_LENGTH: 256;
|
|
389
407
|
declare const DB_ALERT_POLICY_NAME_MAX_LENGTH: 256;
|
|
390
408
|
declare const DB_ALERT_MESSAGE_MAX_LENGTH: 4096;
|
|
@@ -432,10 +450,11 @@ declare class SmithersDb {
|
|
|
432
450
|
*/
|
|
433
451
|
clearFrameCacheForRun(runId: string): void;
|
|
434
452
|
/**
|
|
435
|
-
|
|
453
|
+
* @param {string} queryString
|
|
454
|
+
* @param {unknown[]} [params]
|
|
436
455
|
* @returns {RunnableEffect<unknown[], SmithersError>}
|
|
437
456
|
*/
|
|
438
|
-
rawQuery(queryString: string): RunnableEffect<unknown[], SmithersError$1>;
|
|
457
|
+
rawQuery(queryString: string, params?: unknown[]): RunnableEffect<unknown[], SmithersError$1>;
|
|
439
458
|
/**
|
|
440
459
|
* @param {string} currentFiberThread
|
|
441
460
|
* @returns {boolean}
|
|
@@ -890,6 +909,29 @@ declare class SmithersDb {
|
|
|
890
909
|
*/
|
|
891
910
|
listToolCalls(runId: string, nodeId: string, iteration: number): RunnableEffect<Array<Record<string, unknown>>, SmithersError$1>;
|
|
892
911
|
/**
|
|
912
|
+
* @param {DocRow} row
|
|
913
|
+
* @returns {RunnableEffect<void, SmithersError>}
|
|
914
|
+
*/
|
|
915
|
+
upsertDocRow(row: DocRow): RunnableEffect<void, SmithersError$1>;
|
|
916
|
+
/**
|
|
917
|
+
* @param {string} path
|
|
918
|
+
* @param {{ includeDeleted?: boolean }} [options]
|
|
919
|
+
* @returns {RunnableEffect<DocRow | undefined, SmithersError>}
|
|
920
|
+
*/
|
|
921
|
+
getDoc(path: string, options?: {
|
|
922
|
+
includeDeleted?: boolean;
|
|
923
|
+
}): RunnableEffect<DocRow | undefined, SmithersError$1>;
|
|
924
|
+
/**
|
|
925
|
+
* @param {{ kind?: string; includeDeleted?: boolean; updatedAfterMs?: number; limit?: number }} [options]
|
|
926
|
+
* @returns {RunnableEffect<DocRow[], SmithersError>}
|
|
927
|
+
*/
|
|
928
|
+
listDocs(options?: {
|
|
929
|
+
kind?: string;
|
|
930
|
+
includeDeleted?: boolean;
|
|
931
|
+
updatedAfterMs?: number;
|
|
932
|
+
limit?: number;
|
|
933
|
+
}): RunnableEffect<DocRow[], SmithersError$1>;
|
|
934
|
+
/**
|
|
893
935
|
* @param {Record<string, unknown>} row
|
|
894
936
|
* @returns {RunnableEffect<void, SmithersError>}
|
|
895
937
|
*/
|
|
@@ -912,15 +954,6 @@ declare class SmithersDb {
|
|
|
912
954
|
/**
|
|
913
955
|
* @param {string} runId
|
|
914
956
|
* @param {EventHistoryQuery} [query]
|
|
915
|
-
* @returns {{ whereSql: string; params: Array<string | number> }}
|
|
916
|
-
*/
|
|
917
|
-
buildEventHistoryWhere(runId: string, query?: EventHistoryQuery): {
|
|
918
|
-
whereSql: string;
|
|
919
|
-
params: Array<string | number>;
|
|
920
|
-
};
|
|
921
|
-
/**
|
|
922
|
-
* @param {string} runId
|
|
923
|
-
* @param {EventHistoryQuery} [query]
|
|
924
957
|
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
925
958
|
*/
|
|
926
959
|
listEventHistory(runId: string, query?: EventHistoryQuery): RunnableEffect<Array<Record<string, unknown>>, SmithersError$1>;
|
|
@@ -1281,6 +1314,14 @@ type CacheRowLike = {
|
|
|
1281
1314
|
nodeId: string;
|
|
1282
1315
|
outputTable: string;
|
|
1283
1316
|
};
|
|
1317
|
+
type DocRow = {
|
|
1318
|
+
path: string;
|
|
1319
|
+
kind: string;
|
|
1320
|
+
content: string;
|
|
1321
|
+
contentHash: string;
|
|
1322
|
+
updatedAtMs: number;
|
|
1323
|
+
deletedAtMs?: number | null;
|
|
1324
|
+
};
|
|
1284
1325
|
|
|
1285
1326
|
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} _BunSQLiteDatabase */
|
|
1286
1327
|
/** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} _SmithersError */
|
|
@@ -4899,6 +4940,7 @@ declare const smithersCron: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
4899
4940
|
};
|
|
4900
4941
|
dialect: "sqlite";
|
|
4901
4942
|
}>;
|
|
4943
|
+
declare const smithersDocs: any;
|
|
4902
4944
|
|
|
4903
4945
|
/** @typedef {import("drizzle-orm").AnyColumn} AnyColumn */
|
|
4904
4946
|
/** @typedef {import("./output/OutputKey.ts").OutputKey} _OutputKey */
|
|
@@ -5009,6 +5051,12 @@ type BunSQLiteDatabase$1 = drizzle_orm_bun_sqlite.BunSQLiteDatabase;
|
|
|
5009
5051
|
declare function schemaSignature(table: _Table$1): string;
|
|
5010
5052
|
type _Table$1 = drizzle_orm.Table;
|
|
5011
5053
|
|
|
5054
|
+
declare function getSmithersSchemaSignature(adapterOrStorage: unknown): Promise<{
|
|
5055
|
+
schemaVersion: string;
|
|
5056
|
+
signature: string;
|
|
5057
|
+
components: Record<string, string>;
|
|
5058
|
+
}>;
|
|
5059
|
+
|
|
5012
5060
|
/**
|
|
5013
5061
|
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
5014
5062
|
* @param {_Table} inputTable
|
|
@@ -5226,4 +5274,4 @@ declare function camelToSnake(str: any): any;
|
|
|
5226
5274
|
|
|
5227
5275
|
type SchemaRegistryEntry = SchemaRegistryEntry$1;
|
|
5228
5276
|
|
|
5229
|
-
export { type AlertRow, type AlertSeverity, type AlertStatus, type AnyColumn, type ApprovalRow, type AttemptRow, type CacheRow, type CacheRowLike, type CountRow, DB_ALERT_ALLOWED_SEVERITIES, DB_ALERT_ALLOWED_STATUSES, DB_ALERT_ID_MAX_LENGTH, DB_ALERT_MESSAGE_MAX_LENGTH, DB_ALERT_POLICY_NAME_MAX_LENGTH, DB_RUN_ALLOWED_STATUSES, DB_RUN_ID_MAX_LENGTH, DB_RUN_WORKFLOW_NAME_MAX_LENGTH, type EventHistoryQuery, FRAME_KEYFRAME_INTERVAL, type FrameDelta, type FrameDeltaOp, type FrameEncoding, type FrameRow, type HumanRequestRow, type JsonBounds, type JsonPath, type JsonPathSegment, NODE_DIFF_MAX_BYTES, NodeDiffCache, type NodeDiffCacheResult, type NodeDiffCacheRow$1 as NodeDiffCacheRow, NodeDiffTooLargeError, type NodeRow, type OutputKey, type OutputSnapshot, type PendingHumanRequestRow, type RalphRow, type RunAncestryRow, type RunRow, type RunnableEffect, type SchemaRegistryEntry, type SignalQuery, type SignalRow, SmithersDb, type SmithersError$1 as SmithersError, SqlMessageStorage, type SqlMessageStorageEventHistoryQuery, type SqliteParam, type SqliteWriteRetryOptions, type StaleRunRecord, type _BunSQLiteDatabase, type _NodeDiffCacheRow, type _OutputKey, type _SmithersDb, type _SmithersError, applyFrameDelta, applyFrameDeltaJson, assertJsonPayloadWithinBounds, assertMaxBytes, assertMaxJsonDepth, assertMaxStringLength, assertOptionalArrayMaxLength, assertOptionalStringMaxLength, assertPositiveFiniteInteger, assertPositiveFiniteNumber, buildKeyWhere, buildOutputRow, camelToSnake, describeSchemaShape, encodeFrameDelta, ensureSmithersTables, ensureSmithersTablesEffect, ensureSqlMessageStorage, ensureSqlMessageStorageEffect, getAgentOutputSchema, getKeyColumns, getSqlMessageStorage, isRetryableSqliteWriteError, loadInput, loadInputEffect, loadOutputs, loadOutputsEffect, normalizeFrameEncoding, parseFrameDelta, schemaSignature, selectOutputRow, selectOutputRowEffect, serializeFrameDelta, smithersAlerts, smithersApprovals, smithersAttempts, smithersCache, smithersCron, smithersEvents, smithersFrames, smithersHumanRequests, smithersNodeDiffs, smithersNodes, smithersRalph, smithersRuns, smithersSandboxes, smithersSignals, smithersTimeTravelAudit, smithersToolCalls, smithersVectors, stripAutoColumns, syncZodTableSchema, unwrapZodType, upsertOutputRow, upsertOutputRowEffect, validateExistingOutput, validateInput, validateOutput, withSqliteWriteRetry, withSqliteWriteRetryEffect, zodSchemaColumns, zodToCreateTableSQL, zodToTable };
|
|
5277
|
+
export { type AlertRow, type AlertSeverity, type AlertStatus, type AnyColumn, type ApprovalRow, type AttemptRow, type CacheRow, type CacheRowLike, type CountRow, DB_ALERT_ALLOWED_SEVERITIES, DB_ALERT_ALLOWED_STATUSES, DB_ALERT_ID_MAX_LENGTH, DB_ALERT_MESSAGE_MAX_LENGTH, DB_ALERT_POLICY_NAME_MAX_LENGTH, DB_RUN_ALLOWED_STATUSES, DB_RUN_ID_MAX_LENGTH, DB_RUN_WORKFLOW_NAME_MAX_LENGTH, type Dialect, type DocRow, type EventHistoryQuery, FRAME_KEYFRAME_INTERVAL, type FrameDelta, type FrameDeltaOp, type FrameEncoding, type FrameRow, type HumanRequestRow, type JsonBounds, type JsonPath, type JsonPathSegment, NODE_DIFF_MAX_BYTES, NodeDiffCache, type NodeDiffCacheResult, type NodeDiffCacheRow$1 as NodeDiffCacheRow, NodeDiffTooLargeError, type NodeRow, type OutputKey, type OutputSnapshot, POSTGRES, type PendingHumanRequestRow, type RalphRow, type RunAncestryRow, type RunRow, type RunnableEffect, SQLITE, type SchemaRegistryEntry, type SignalQuery, type SignalRow, SmithersDb, type SmithersError$1 as SmithersError, SqlMessageStorage, type SqlMessageStorageEventHistoryQuery, type SqliteParam, type SqliteWriteRetryOptions, type StaleRunRecord, type _BunSQLiteDatabase, type _NodeDiffCacheRow, type _OutputKey, type _SmithersDb, type _SmithersError, applyFrameDelta, applyFrameDeltaJson, assertJsonPayloadWithinBounds, assertMaxBytes, assertMaxJsonDepth, assertMaxStringLength, assertOptionalArrayMaxLength, assertOptionalStringMaxLength, assertPositiveFiniteInteger, assertPositiveFiniteNumber, beginTransactionSql, buildKeyWhere, buildOutputRow, camelToSnake, columnType, describeSchemaShape, encodeFrameDelta, ensureSmithersTables, ensureSmithersTablesEffect, ensureSqlMessageStorage, ensureSqlMessageStorageEffect, getAgentOutputSchema, getKeyColumns, getSmithersSchemaSignature, getSqlMessageStorage, isDialect, isRetryableSqliteWriteError, jsonExtractText, loadInput, loadInputEffect, loadOutputs, loadOutputsEffect, normalizeFrameEncoding, parseFrameDelta, quoteIdentifier, schemaSignature, selectOutputRow, selectOutputRowEffect, serializeFrameDelta, smithersAlerts, smithersApprovals, smithersAttempts, smithersCache, smithersCron, smithersDocs, smithersEvents, smithersFrames, smithersHumanRequests, smithersNodeDiffs, smithersNodes, smithersRalph, smithersRuns, smithersSandboxes, smithersSignals, smithersTimeTravelAudit, smithersToolCalls, smithersVectors, stripAutoColumns, syncZodTableSchema, tableColumnsSql, translateDdl, translatePlaceholders, unwrapZodType, upsertOutputRow, upsertOutputRowEffect, validateExistingOutput, validateInput, validateOutput, withSqliteWriteRetry, withSqliteWriteRetryEffect, zodSchemaColumns, zodToCreateTableSQL, zodToTable };
|
package/src/index.js
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
// @smithers-type-exports-end
|
|
4
4
|
|
|
5
5
|
export * from "./adapter.js";
|
|
6
|
+
export * from "./assertNoReservedColumns.js";
|
|
7
|
+
export * from "./dialect.js";
|
|
6
8
|
export * from "./ensure.js";
|
|
7
9
|
export * from "./input-bounds.js";
|
|
8
10
|
export * from "./frame-codec.js";
|
|
11
|
+
export * from "./getSmithersSchemaSignature.js";
|
|
9
12
|
export * from "./input.js";
|
|
10
13
|
export * from "./internal-schema.js";
|
|
11
14
|
export * from "./output.js";
|
|
@@ -19,6 +19,7 @@ export { smithersMemoryThreads } from "./smithersMemoryThreads.js";
|
|
|
19
19
|
export { smithersMemoryMessages } from "./smithersMemoryMessages.js";
|
|
20
20
|
export { smithersVectors } from "./smithersVectors.js";
|
|
21
21
|
export { smithersCron } from "./smithersCron.js";
|
|
22
|
+
export { smithersDocs } from "./smithersDocs.js";
|
|
22
23
|
export { smithersSchemaMigrations } from "./smithersSchemaMigrations.js";
|
|
23
24
|
export { smithersWorkspaceStates } from "./smithersWorkspaceStates.js";
|
|
24
25
|
export { smithersWorkspaceCheckpoints } from "./smithersWorkspaceCheckpoints.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `_smithers_docs` — durable markdown work-docs (tickets, plans, specs,
|
|
5
|
+
* proposals) the gateway lists/creates/updates/soft-deletes via the
|
|
6
|
+
* `listTickets`/`createTicket`/`updateTicket`/`deleteTicket` RPCs and that the
|
|
7
|
+
* file-watcher seam (`watchDocsDirectory`) upserts from a `.md` directory.
|
|
8
|
+
*
|
|
9
|
+
* - `path` PK; the doc identity (a file path or stable id).
|
|
10
|
+
* - `kind` one of `ticket | plan | spec | proposal` (default `ticket`).
|
|
11
|
+
* - `content` the full markdown body.
|
|
12
|
+
* - `contentHash` `sha256(content)` — the watcher's last-write-wins compare key.
|
|
13
|
+
* - `status` rides the sync row so a ticket's status survives reload
|
|
14
|
+
* (LOCKED Path A); free-form text (e.g. `todo`/`in-progress`/`done`).
|
|
15
|
+
* - `updatedAtMs` last write (Unix epoch ms).
|
|
16
|
+
* - `deletedAtMs` soft-delete tombstone; NULL means live. `listTickets`
|
|
17
|
+
* filters tombstones and the watcher never materializes them.
|
|
18
|
+
*/
|
|
19
|
+
export const smithersDocs = sqliteTable("_smithers_docs", {
|
|
20
|
+
path: text("path").primaryKey(),
|
|
21
|
+
kind: text("kind").notNull().default("ticket"),
|
|
22
|
+
content: text("content").notNull(),
|
|
23
|
+
contentHash: text("content_hash").notNull(),
|
|
24
|
+
status: text("status"),
|
|
25
|
+
updatedAtMs: integer("updated_at_ms").notNull(),
|
|
26
|
+
deletedAtMs: integer("deleted_at_ms"),
|
|
27
|
+
});
|
|
@@ -14,6 +14,8 @@ export const smithersScorers = sqliteTable("_smithers_scorers", {
|
|
|
14
14
|
metaJson: text("meta_json"),
|
|
15
15
|
inputJson: text("input_json"),
|
|
16
16
|
outputJson: text("output_json"),
|
|
17
|
+
groundTruthJson: text("ground_truth_json"),
|
|
18
|
+
contextJson: text("context_json"),
|
|
17
19
|
latencyMs: real("latency_ms"),
|
|
18
20
|
scoredAtMs: integer("scored_at_ms").notNull(),
|
|
19
21
|
durationMs: real("duration_ms"),
|
package/src/internal-schema.js
CHANGED
|
@@ -270,3 +270,4 @@ export const smithersSchemaMigrations = sqliteTable("_smithers_schema_migrations
|
|
|
270
270
|
});
|
|
271
271
|
export { smithersWorkspaceStates } from "./internal-schema/smithersWorkspaceStates.js";
|
|
272
272
|
export { smithersWorkspaceCheckpoints } from "./internal-schema/smithersWorkspaceCheckpoints.js";
|
|
273
|
+
export { smithersDocs } from "./internal-schema/smithersDocs.js";
|
package/src/react-output.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export function stripAutoColumns(payload) {
|
|
5
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
6
|
-
return payload;
|
|
7
|
-
}
|
|
8
|
-
const { runId: _runId, nodeId: _nodeId, iteration: _iteration, ...rest } = payload;
|
|
9
|
-
return rest;
|
|
10
|
-
}
|
|
1
|
+
// Re-export the canonical stripAutoColumns from the output barrel so this
|
|
2
|
+
// React-facing subpath does not carry a third, drifting copy of the helper.
|
|
3
|
+
export { stripAutoColumns } from "./output.js";
|
|
@@ -5,6 +5,10 @@ export type DeriveRunStateInput = {
|
|
|
5
5
|
pendingApproval?: { nodeId: string; requestedAtMs: number } | null;
|
|
6
6
|
pendingTimer?: { nodeId: string; firesAtMs: number } | null;
|
|
7
7
|
pendingEvent?: { nodeId: string; correlationKey: string } | null;
|
|
8
|
+
parkedEventBlock?:
|
|
9
|
+
| { kind: "approval-decided-resume-required"; nodeId: string }
|
|
10
|
+
| { kind: "external-trigger" }
|
|
11
|
+
| null;
|
|
8
12
|
now?: number;
|
|
9
13
|
staleThresholdMs?: number;
|
|
10
14
|
};
|
|
@@ -2,9 +2,12 @@ export type ReasonBlocked =
|
|
|
2
2
|
| { kind: "approval"; nodeId: string; requestedAt: string }
|
|
3
3
|
| { kind: "event"; nodeId: string; correlationKey: string }
|
|
4
4
|
| { kind: "timer"; nodeId: string; wakeAt: string }
|
|
5
|
+
| { kind: "approval-decided-resume-required"; nodeId: string }
|
|
6
|
+
| { kind: "external-trigger" }
|
|
5
7
|
| {
|
|
6
8
|
kind: "provider";
|
|
7
9
|
nodeId: string;
|
|
8
10
|
code: "rate-limit" | "auth" | "timeout";
|
|
9
11
|
}
|
|
10
|
-
| { kind: "tool"; nodeId: string; toolName: string; code: string }
|
|
12
|
+
| { kind: "tool"; nodeId: string; toolName: string; code: string }
|
|
13
|
+
| { kind: "quota"; quotaBlockedCount: number; resetAtMs?: number };
|
package/src/runState/RunState.ts
CHANGED
|
@@ -17,6 +17,7 @@ export async function computeRunStateFromRow(adapter, run, options = {}) {
|
|
|
17
17
|
let pendingApproval = null;
|
|
18
18
|
let pendingTimer = null;
|
|
19
19
|
let pendingEvent = null;
|
|
20
|
+
let parkedEventBlock = null;
|
|
20
21
|
|
|
21
22
|
if (run.status === "waiting-approval") {
|
|
22
23
|
pendingApproval = await loadPendingApproval(adapter, run.runId);
|
|
@@ -24,6 +25,9 @@ export async function computeRunStateFromRow(adapter, run, options = {}) {
|
|
|
24
25
|
pendingTimer = await loadPendingTimer(adapter, run.runId);
|
|
25
26
|
} else if (run.status === "waiting-event") {
|
|
26
27
|
pendingEvent = await loadPendingEvent(adapter, run.runId);
|
|
28
|
+
if (pendingEvent == null) {
|
|
29
|
+
parkedEventBlock = await loadParkedEventBlock(adapter, run.runId);
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
return deriveRunState({
|
|
@@ -31,6 +35,7 @@ export async function computeRunStateFromRow(adapter, run, options = {}) {
|
|
|
31
35
|
pendingApproval,
|
|
32
36
|
pendingTimer,
|
|
33
37
|
pendingEvent,
|
|
38
|
+
parkedEventBlock,
|
|
34
39
|
now: options.now,
|
|
35
40
|
staleThresholdMs: options.staleThresholdMs,
|
|
36
41
|
});
|
|
@@ -100,3 +105,19 @@ async function loadPendingEvent(adapter, runId) {
|
|
|
100
105
|
}
|
|
101
106
|
return null;
|
|
102
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {SmithersDb} adapter
|
|
111
|
+
* @param {string} runId
|
|
112
|
+
*/
|
|
113
|
+
async function loadParkedEventBlock(adapter, runId) {
|
|
114
|
+
const nodes = await adapter.listNodes(runId);
|
|
115
|
+
const pending = nodes.find((node) => node.state === "pending");
|
|
116
|
+
if (pending) {
|
|
117
|
+
return {
|
|
118
|
+
kind: "approval-decided-resume-required",
|
|
119
|
+
nodeId: pending.nodeId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { kind: "external-trigger" };
|
|
123
|
+
}
|
|
@@ -13,6 +13,7 @@ export function deriveRunState(input) {
|
|
|
13
13
|
pendingApproval = null,
|
|
14
14
|
pendingTimer = null,
|
|
15
15
|
pendingEvent = null,
|
|
16
|
+
parkedEventBlock = null,
|
|
16
17
|
now = Date.now(),
|
|
17
18
|
staleThresholdMs = RUN_STATE_HEARTBEAT_STALE_MS,
|
|
18
19
|
} = input;
|
|
@@ -55,17 +56,45 @@ export function deriveRunState(input) {
|
|
|
55
56
|
}
|
|
56
57
|
: { ...base, state: "waiting-timer" };
|
|
57
58
|
case "waiting-event":
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
if (pendingEvent) {
|
|
60
|
+
return {
|
|
61
|
+
...base,
|
|
62
|
+
state: "waiting-event",
|
|
63
|
+
blocked: {
|
|
64
|
+
kind: "event",
|
|
65
|
+
nodeId: pendingEvent.nodeId,
|
|
66
|
+
correlationKey: pendingEvent.correlationKey,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return parkedEventBlock
|
|
71
|
+
? { ...base, state: "waiting-event", blocked: parkedEventBlock }
|
|
68
72
|
: { ...base, state: "waiting-event" };
|
|
73
|
+
case "waiting-quota": {
|
|
74
|
+
let quotaMeta;
|
|
75
|
+
if (run.errorJson) {
|
|
76
|
+
try { quotaMeta = JSON.parse(run.errorJson); } catch { /* ignore */ }
|
|
77
|
+
}
|
|
78
|
+
const quotaBlockedCount = typeof quotaMeta?.quotaBlockedCount === "number"
|
|
79
|
+
? quotaMeta.quotaBlockedCount
|
|
80
|
+
: undefined;
|
|
81
|
+
const resetAtMs = typeof quotaMeta?.resetAtMs === "number"
|
|
82
|
+
? quotaMeta.resetAtMs
|
|
83
|
+
: undefined;
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
state: "waiting-quota",
|
|
87
|
+
...(quotaBlockedCount != null
|
|
88
|
+
? {
|
|
89
|
+
blocked: {
|
|
90
|
+
kind: "quota",
|
|
91
|
+
quotaBlockedCount,
|
|
92
|
+
...(resetAtMs != null ? { resetAtMs } : {}),
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
69
98
|
case "running":
|
|
70
99
|
return classifyRunning(run, now, staleThresholdMs, base);
|
|
71
100
|
default:
|