okengine 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +7 -6
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +27 -1
- package/src/compiler/extract.ts +268 -12
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +289 -2
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +103 -1
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +15 -4
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +312 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/journal.ts +104 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -424,7 +424,9 @@ export interface SqlStoreHandle {
|
|
|
424
424
|
exists(table: TableHandle | unknown, idOrWhere: string | WhereMap): Promise<boolean>;
|
|
425
425
|
/**
|
|
426
426
|
* Insert when no row matches `matchOn`; never touches an existing match
|
|
427
|
-
* unless `options.onExisting` is `"update"`.
|
|
427
|
+
* unless `options.onExisting` is `"update"`. A match that is only the
|
|
428
|
+
* primary key is one `INSERT … ON CONFLICT` round trip on postgres and
|
|
429
|
+
* pglite. Memory SQL, and any other predicate, stays a read then a write.
|
|
428
430
|
*
|
|
429
431
|
* @param table - Table
|
|
430
432
|
* @param matchOn - Equality map or Drizzle condition identifying the row
|
|
@@ -685,6 +687,101 @@ export function createSqlStoreHandle(
|
|
|
685
687
|
return coerceCompiledWhere(table, compileWhere(where));
|
|
686
688
|
}
|
|
687
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Primary-key equality only. Any other predicate cannot use `ON CONFLICT`.
|
|
692
|
+
*
|
|
693
|
+
* @param compiled - Match clause
|
|
694
|
+
* @param pkCol - Primary key SQL name
|
|
695
|
+
*/
|
|
696
|
+
function primaryKeyEquality(
|
|
697
|
+
compiled: ReturnType<typeof compileWhere>,
|
|
698
|
+
pkCol: string,
|
|
699
|
+
): { readonly value: unknown } | undefined {
|
|
700
|
+
if (compiled.predicates.length !== 1) return undefined;
|
|
701
|
+
const pred = compiled.predicates[0];
|
|
702
|
+
if (!pred || pred.op !== "=" || pred.column !== pkCol) return undefined;
|
|
703
|
+
return { value: pred.value };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* One round trip: `INSERT … ON CONFLICT (pk)`.
|
|
708
|
+
*
|
|
709
|
+
* `DO NOTHING` when the existing row must stay. `DO UPDATE` captures the
|
|
710
|
+
* pre-image in a CTE so CDC still sees before and after.
|
|
711
|
+
*
|
|
712
|
+
* @param table - Table
|
|
713
|
+
* @param name - SQL table name
|
|
714
|
+
* @param pkCol - Primary key SQL name
|
|
715
|
+
* @param pkValue - Bound primary key
|
|
716
|
+
* @param values - Insert / update payload
|
|
717
|
+
* @param onExisting - `"update"` rewrites the existing row
|
|
718
|
+
*/
|
|
719
|
+
async function upsertOnPrimaryKey(
|
|
720
|
+
table: TableHandle | unknown,
|
|
721
|
+
name: string,
|
|
722
|
+
pkCol: string,
|
|
723
|
+
pkValue: unknown,
|
|
724
|
+
values: SqlRow,
|
|
725
|
+
onExisting: "update" | undefined,
|
|
726
|
+
): Promise<UpsertResult> {
|
|
727
|
+
const prepared = prepareInsertRow(table, values);
|
|
728
|
+
if (!(pkCol in prepared)) prepared[pkCol] = pkValue;
|
|
729
|
+
const cols = Object.keys(prepared);
|
|
730
|
+
const colList = cols.map(quoteIdent).join(", ");
|
|
731
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
732
|
+
const params = cols.map((col) => prepared[col]);
|
|
733
|
+
const quotedPk = quoteIdent(pkCol);
|
|
734
|
+
const quotedName = quoteIdent(name);
|
|
735
|
+
const setEntries =
|
|
736
|
+
onExisting === "update"
|
|
737
|
+
? Object.entries(prepareUpdateRow(table, values)).filter(([col]) => col !== pkCol)
|
|
738
|
+
: [];
|
|
739
|
+
if (onExisting !== "update" || setEntries.length === 0) {
|
|
740
|
+
const rows = await query(
|
|
741
|
+
`INSERT INTO ${quotedName} (${colList}) VALUES (${placeholders})` +
|
|
742
|
+
` ON CONFLICT (${quotedPk}) DO NOTHING RETURNING *`,
|
|
743
|
+
params,
|
|
744
|
+
);
|
|
745
|
+
if (rows.length === 0) {
|
|
746
|
+
return { status: onExisting === "update" ? "changed" : "already-existed" };
|
|
747
|
+
}
|
|
748
|
+
notifySqlCdc({
|
|
749
|
+
tableName: name,
|
|
750
|
+
op: "insert",
|
|
751
|
+
before: null,
|
|
752
|
+
after: toJs(table, rows)[0] ?? null,
|
|
753
|
+
});
|
|
754
|
+
return { status: "upserted" };
|
|
755
|
+
}
|
|
756
|
+
const setSql = setEntries
|
|
757
|
+
.map(([col]) => `${quoteIdent(col)} = EXCLUDED.${quoteIdent(col)}`)
|
|
758
|
+
.join(", ");
|
|
759
|
+
// The old row is a CTE, then read from an outer SELECT. INSERT …
|
|
760
|
+
// RETURNING cannot see that CTE on PGlite, so the before image would
|
|
761
|
+
// be null and an update would look like an insert.
|
|
762
|
+
const result = await query(
|
|
763
|
+
`WITH __oke_old AS (` +
|
|
764
|
+
`SELECT * FROM ${quotedName} WHERE ${quotedPk} = ? FOR UPDATE` +
|
|
765
|
+
`), __oke_new AS (` +
|
|
766
|
+
`INSERT INTO ${quotedName} (${colList}) VALUES (${placeholders})` +
|
|
767
|
+
` ON CONFLICT (${quotedPk}) DO UPDATE SET ${setSql}` +
|
|
768
|
+
` RETURNING *` +
|
|
769
|
+
`) SELECT (SELECT row_to_json(__oke_old) FROM __oke_old) AS __oke_before_data,` +
|
|
770
|
+
` (SELECT row_to_json(__oke_new) FROM __oke_new) AS __oke_after_data`,
|
|
771
|
+
[pkValue, ...params],
|
|
772
|
+
);
|
|
773
|
+
const raw = result[0];
|
|
774
|
+
const before = raw ? jsonImageToJs(table, raw.__oke_before_data) : null;
|
|
775
|
+
const after = raw ? jsonImageToJs(table, raw.__oke_after_data) : null;
|
|
776
|
+
notifySqlCdc({
|
|
777
|
+
tableName: name,
|
|
778
|
+
op: before ? "update" : "insert",
|
|
779
|
+
before,
|
|
780
|
+
after,
|
|
781
|
+
});
|
|
782
|
+
return { status: before ? "changed" : "upserted" };
|
|
783
|
+
}
|
|
784
|
+
|
|
688
785
|
/**
|
|
689
786
|
* Postgres `row_to_json` images come back as JSON strings on pglite and
|
|
690
787
|
* objects on Bun.SQL — normalize to the JS row mapping (snake→camel).
|
|
@@ -1026,6 +1123,19 @@ export function createSqlStoreHandle(
|
|
|
1026
1123
|
if (!compiled.clause) {
|
|
1027
1124
|
throw new Error("upsert() requires at least one matchOn predicate");
|
|
1028
1125
|
}
|
|
1126
|
+
const pkCol = resolvePkColumn(table);
|
|
1127
|
+
const pkMatch = primaryKeyEquality(compiled, pkCol);
|
|
1128
|
+
// Memory SQL has no ON CONFLICT. Postgres and PGlite do.
|
|
1129
|
+
if (pkMatch && connection.driverId !== "memory") {
|
|
1130
|
+
return upsertOnPrimaryKey(
|
|
1131
|
+
table,
|
|
1132
|
+
name,
|
|
1133
|
+
pkCol,
|
|
1134
|
+
pkMatch.value,
|
|
1135
|
+
values,
|
|
1136
|
+
upsertOptions?.onExisting,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1029
1139
|
const found = await query(
|
|
1030
1140
|
`SELECT 1 AS "ok" FROM ${quoteIdent(name)} WHERE ${compiled.clause} LIMIT 1`,
|
|
1031
1141
|
compiled.params,
|
package/src/i18n/catalogs/ar.ts
CHANGED
|
@@ -40,6 +40,10 @@ export const builtinAr = {
|
|
|
40
40
|
cause: 'التدفق "{flow}" يضمّن عبر "{resource}" دون الإعلان عنه.',
|
|
41
41
|
fix: 'أضف "{resource}" إلى effects.embeds لهذا التدفق.',
|
|
42
42
|
},
|
|
43
|
+
"1010": {
|
|
44
|
+
cause: 'التدفق "{flow}" يقرر "{resource}" دون الإعلان عنه.',
|
|
45
|
+
fix: 'أضف "{resource}" إلى effects.decides لهذا التدفق.',
|
|
46
|
+
},
|
|
43
47
|
"1240": {
|
|
44
48
|
cause: 'التدفق "{flow}" يُصدِر الإشارة "{resource}" بلا مشترك.',
|
|
45
49
|
fix: "أضف on({resource}, …) أو عيّن الإشارة '{'optional: true'}'.",
|
|
@@ -90,6 +94,7 @@ export const builtinAr = {
|
|
|
90
94
|
IdempotencyKeyInvalid: "يجب أن يكون Idempotency-Key من 16 إلى 255 حرفًا ASCII قابلًا للطباعة.",
|
|
91
95
|
IdempotencyKeyReused: "استُخدم Idempotency-Key هذا من قبل مع طلب مختلف.",
|
|
92
96
|
IdempotencyInProgress: "ما زال Idempotency-Key هذا قيد التنفيذ. أعد المحاولة بعد المهلة.",
|
|
97
|
+
JournalLeaseBusy: "هذا التشغيل مقفل لدى عامل آخر. أعد المحاولة بعد المهلة.",
|
|
93
98
|
InvalidQuery: "الاستعلام غير صالح.",
|
|
94
99
|
UnsupportedMediaType: "نوع المحتوى هذا غير مدعوم.",
|
|
95
100
|
AuthFailed: "فشلت المصادقة.",
|
package/src/i18n/catalogs/en.ts
CHANGED
|
@@ -40,6 +40,10 @@ export const builtinEn = {
|
|
|
40
40
|
cause: 'Flow "{flow}" embeds with "{resource}" without declaring it.',
|
|
41
41
|
fix: "Add \"{resource}\" to this flow''s effects.embeds.",
|
|
42
42
|
},
|
|
43
|
+
"1010": {
|
|
44
|
+
cause: 'Flow "{flow}" decides "{resource}" without declaring it.',
|
|
45
|
+
fix: "Add \"{resource}\" to this flow''s effects.decides.",
|
|
46
|
+
},
|
|
43
47
|
"1240": {
|
|
44
48
|
cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
|
|
45
49
|
fix: "Add on({resource}, …) or mark the signal '{'optional: true'}'.",
|
|
@@ -90,6 +94,7 @@ export const builtinEn = {
|
|
|
90
94
|
IdempotencyKeyInvalid: "Idempotency-Key must be 16–255 printable ASCII characters.",
|
|
91
95
|
IdempotencyKeyReused: "This Idempotency-Key was already used with a different request.",
|
|
92
96
|
IdempotencyInProgress: "This Idempotency-Key is still running. Retry after the given delay.",
|
|
97
|
+
JournalLeaseBusy: "This run is locked by another worker. Retry after the given delay.",
|
|
93
98
|
InvalidQuery: "The query is not valid.",
|
|
94
99
|
UnsupportedMediaType: "That content type is not supported.",
|
|
95
100
|
AuthFailed: "Authentication failed.",
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable agent-run event log on the journal driver.
|
|
3
|
+
*
|
|
4
|
+
* Memory, file, and Postgres keep the same rows so a follower on another
|
|
5
|
+
* instance, or after a restart, resumes from `Last-Event-ID`. The instance
|
|
6
|
+
* that holds the run's journal lease is the only writer. A repeated seq is
|
|
7
|
+
* an error.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { appendFile, mkdir, readdir, rm } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
/** Header stored beside the rows. */
|
|
14
|
+
export interface AgentEventHeaderRecord {
|
|
15
|
+
readonly runId: string;
|
|
16
|
+
readonly threadId: string;
|
|
17
|
+
/** Agent that opened the run. Absent on logs written before this field. */
|
|
18
|
+
readonly agent?: string;
|
|
19
|
+
/** Parent agent run, when this run is a nested tool. */
|
|
20
|
+
readonly parentRunId?: string;
|
|
21
|
+
readonly tenant: string | null;
|
|
22
|
+
readonly gates: readonly string[];
|
|
23
|
+
readonly userId: string | null;
|
|
24
|
+
readonly operatorId: string | null;
|
|
25
|
+
/** Epoch ms the run was opened. Sweep uses this for abandoned runs. */
|
|
26
|
+
readonly openedAt?: number;
|
|
27
|
+
readonly finishedAt?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Epoch ms until which a sweep claim is held.
|
|
30
|
+
* Another instance may reclaim the run after this instant.
|
|
31
|
+
* Postgres stores the claim on `sweep_claim_at` instead.
|
|
32
|
+
*/
|
|
33
|
+
readonly sweepClaimUntil?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One stored SSE row. */
|
|
37
|
+
export interface AgentEventRowRecord {
|
|
38
|
+
readonly seq: number;
|
|
39
|
+
readonly event: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One run's durable log. */
|
|
43
|
+
export interface AgentEventRecord {
|
|
44
|
+
readonly header: AgentEventHeaderRecord;
|
|
45
|
+
readonly rows: readonly AgentEventRowRecord[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Thrown when a writer inserts a seq that is already stored.
|
|
50
|
+
* The lease holder reads `MAX(seq)` before writing, so this is a bug.
|
|
51
|
+
*/
|
|
52
|
+
export class AgentEventDuplicateSeqError extends Error {
|
|
53
|
+
/**
|
|
54
|
+
* @param runId - Agent run
|
|
55
|
+
* @param seq - Seq that was already stored
|
|
56
|
+
*/
|
|
57
|
+
constructor(runId: string, seq: number) {
|
|
58
|
+
super(`agent events: duplicate seq ${seq} for run "${runId}"`);
|
|
59
|
+
this.name = "AgentEventDuplicateSeqError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Persistence for {@link AgentEventRecord}. */
|
|
64
|
+
export interface AgentEventStore {
|
|
65
|
+
/** Load one run, or undefined when it was never opened. */
|
|
66
|
+
read(runId: string): Promise<AgentEventRecord | undefined>;
|
|
67
|
+
/** Rows with `seq` greater than `afterSeq`, in order. */
|
|
68
|
+
readAfter(runId: string, afterSeq: number): Promise<readonly AgentEventRowRecord[]>;
|
|
69
|
+
/** Highest stored seq, or 0 when the run has no rows. */
|
|
70
|
+
maxSeq(runId: string): Promise<number>;
|
|
71
|
+
/** Header for one run, or undefined when it was never opened. */
|
|
72
|
+
readHeader(runId: string): Promise<AgentEventHeaderRecord | undefined>;
|
|
73
|
+
/** True when this run already stored `oke.events.truncated`. */
|
|
74
|
+
truncated(runId: string): Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* Claim the right to close this run.
|
|
77
|
+
* False when another instance holds an unexpired claim.
|
|
78
|
+
* A claim older than {@link AGENT_EVENT_SWEEP_CLAIM_MS} can be taken again.
|
|
79
|
+
*
|
|
80
|
+
* @param runId - Agent run
|
|
81
|
+
* @param now - Clock. Defaults to `Date.now()`.
|
|
82
|
+
*/
|
|
83
|
+
claim(runId: string, now?: number): Promise<boolean>;
|
|
84
|
+
/** Every header, including runs this process did not open. */
|
|
85
|
+
listHeaders(): Promise<readonly AgentEventHeaderRecord[]>;
|
|
86
|
+
/** Create or replace the header. Existing rows stay. */
|
|
87
|
+
writeHeader(header: AgentEventHeaderRecord): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Append one row. `seq` is already assigned.
|
|
90
|
+
* A duplicate seq throws {@link AgentEventDuplicateSeqError}.
|
|
91
|
+
*/
|
|
92
|
+
append(runId: string, row: AgentEventRowRecord): Promise<void>;
|
|
93
|
+
/** Drop a finished run. */
|
|
94
|
+
remove(runId: string): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** How long a sweep claim blocks other instances. */
|
|
98
|
+
export const AGENT_EVENT_SWEEP_CLAIM_MS = 30_000;
|
|
99
|
+
|
|
100
|
+
/** In-memory event store. */
|
|
101
|
+
export function createMemoryAgentEventStore(): AgentEventStore {
|
|
102
|
+
const runs = new Map<string, { header: AgentEventHeaderRecord; rows: AgentEventRowRecord[] }>();
|
|
103
|
+
const claimedUntil = new Map<string, number>();
|
|
104
|
+
return {
|
|
105
|
+
async read(runId) {
|
|
106
|
+
const run = runs.get(runId);
|
|
107
|
+
if (!run) return undefined;
|
|
108
|
+
return { header: run.header, rows: run.rows.map((row) => ({ ...row })) };
|
|
109
|
+
},
|
|
110
|
+
async readAfter(runId, afterSeq) {
|
|
111
|
+
const run = runs.get(runId);
|
|
112
|
+
if (!run) return [];
|
|
113
|
+
return run.rows.filter((row) => row.seq > afterSeq).map((row) => ({ ...row }));
|
|
114
|
+
},
|
|
115
|
+
async maxSeq(runId) {
|
|
116
|
+
const run = runs.get(runId);
|
|
117
|
+
if (!run || run.rows.length === 0) return 0;
|
|
118
|
+
return run.rows.reduce((max, row) => Math.max(max, row.seq), 0);
|
|
119
|
+
},
|
|
120
|
+
async readHeader(runId) {
|
|
121
|
+
return runs.get(runId)?.header;
|
|
122
|
+
},
|
|
123
|
+
async truncated(runId) {
|
|
124
|
+
const run = runs.get(runId);
|
|
125
|
+
if (!run) return false;
|
|
126
|
+
return run.rows.some((row) => isTruncationEvent(row.event));
|
|
127
|
+
},
|
|
128
|
+
async claim(runId, now = Date.now()) {
|
|
129
|
+
if (!runs.has(runId)) return false;
|
|
130
|
+
const until = claimedUntil.get(runId);
|
|
131
|
+
if (until !== undefined && now < until) return false;
|
|
132
|
+
claimedUntil.set(runId, now + AGENT_EVENT_SWEEP_CLAIM_MS);
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
async listHeaders() {
|
|
136
|
+
return [...runs.values()].map((run) => run.header);
|
|
137
|
+
},
|
|
138
|
+
async writeHeader(header) {
|
|
139
|
+
const run = runs.get(header.runId);
|
|
140
|
+
if (run) run.header = header;
|
|
141
|
+
else runs.set(header.runId, { header, rows: [] });
|
|
142
|
+
},
|
|
143
|
+
async append(runId, row) {
|
|
144
|
+
const run = runs.get(runId);
|
|
145
|
+
if (!run) return;
|
|
146
|
+
if (run.rows.some((stored) => stored.seq === row.seq)) {
|
|
147
|
+
throw new AgentEventDuplicateSeqError(runId, row.seq);
|
|
148
|
+
}
|
|
149
|
+
run.rows.push(row);
|
|
150
|
+
},
|
|
151
|
+
async remove(runId) {
|
|
152
|
+
runs.delete(runId);
|
|
153
|
+
claimedUntil.delete(runId);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface JsonlLine {
|
|
159
|
+
readonly kind: "header" | "row";
|
|
160
|
+
readonly header?: AgentEventHeaderRecord;
|
|
161
|
+
readonly seq?: number;
|
|
162
|
+
readonly event?: unknown;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Append-only event store. One JSONL file per run, guarded by a write lock.
|
|
167
|
+
* Header updates append a new header line. Rows are never rewritten.
|
|
168
|
+
*
|
|
169
|
+
* Single-process only. Do not use it for large logs or more than one
|
|
170
|
+
* process. Production logs belong on the Postgres journal driver.
|
|
171
|
+
*
|
|
172
|
+
* @param dir - Directory of `{runId}.jsonl` files
|
|
173
|
+
*/
|
|
174
|
+
export function createFileAgentEventStore(dir: string): AgentEventStore {
|
|
175
|
+
const locks = new Map<string, Promise<void>>();
|
|
176
|
+
|
|
177
|
+
const withLock = async <T>(runId: string, fn: () => Promise<T>): Promise<T> => {
|
|
178
|
+
const prev = locks.get(runId) ?? Promise.resolve();
|
|
179
|
+
let release: () => void = () => undefined;
|
|
180
|
+
const gate = new Promise<void>((resolve) => {
|
|
181
|
+
release = resolve;
|
|
182
|
+
});
|
|
183
|
+
const tail = prev.then(() => gate);
|
|
184
|
+
locks.set(runId, tail);
|
|
185
|
+
await prev;
|
|
186
|
+
try {
|
|
187
|
+
return await fn();
|
|
188
|
+
} finally {
|
|
189
|
+
release();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const fileOf = (runId: string): string => join(dir, `${encodeURIComponent(runId)}.jsonl`);
|
|
194
|
+
|
|
195
|
+
const readLines = async (runId: string): Promise<JsonlLine[]> => {
|
|
196
|
+
const file = Bun.file(fileOf(runId));
|
|
197
|
+
if (!(await file.exists())) return [];
|
|
198
|
+
const text = await file.text();
|
|
199
|
+
const lines: JsonlLine[] = [];
|
|
200
|
+
for (const line of text.split("\n")) {
|
|
201
|
+
if (!line) continue;
|
|
202
|
+
lines.push(JSON.parse(line) as JsonlLine);
|
|
203
|
+
}
|
|
204
|
+
return lines;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const headerOf = (lines: readonly JsonlLine[]): AgentEventHeaderRecord | undefined => {
|
|
208
|
+
let header: AgentEventHeaderRecord | undefined;
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
if (line.kind === "header" && line.header) header = line.header;
|
|
211
|
+
}
|
|
212
|
+
return header;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const rowsOf = (lines: readonly JsonlLine[]): AgentEventRowRecord[] => {
|
|
216
|
+
const rows: AgentEventRowRecord[] = [];
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
if (line.kind === "row" && line.seq !== undefined) {
|
|
219
|
+
rows.push({ seq: line.seq, event: line.event });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return rows;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const appendLine = async (runId: string, line: JsonlLine): Promise<void> => {
|
|
226
|
+
await mkdir(dir, { recursive: true });
|
|
227
|
+
await appendFile(fileOf(runId), `${JSON.stringify(line)}\n`, "utf8");
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
async read(runId) {
|
|
232
|
+
return withLock(runId, async () => {
|
|
233
|
+
const lines = await readLines(runId);
|
|
234
|
+
const header = headerOf(lines);
|
|
235
|
+
if (!header) return undefined;
|
|
236
|
+
return { header, rows: rowsOf(lines) };
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
async readAfter(runId, afterSeq) {
|
|
240
|
+
return withLock(runId, async () => {
|
|
241
|
+
const rows = rowsOf(await readLines(runId));
|
|
242
|
+
return rows.filter((row) => row.seq > afterSeq);
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
async maxSeq(runId) {
|
|
246
|
+
return withLock(runId, async () => {
|
|
247
|
+
const rows = rowsOf(await readLines(runId));
|
|
248
|
+
return rows.reduce((max, row) => Math.max(max, row.seq), 0);
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
async readHeader(runId) {
|
|
252
|
+
return withLock(runId, async () => headerOf(await readLines(runId)));
|
|
253
|
+
},
|
|
254
|
+
async truncated(runId) {
|
|
255
|
+
return withLock(runId, async () =>
|
|
256
|
+
rowsOf(await readLines(runId)).some((row) => isTruncationEvent(row.event)),
|
|
257
|
+
);
|
|
258
|
+
},
|
|
259
|
+
async claim(runId, now = Date.now()) {
|
|
260
|
+
return withLock(runId, async () => {
|
|
261
|
+
const header = headerOf(await readLines(runId));
|
|
262
|
+
if (!header) return false;
|
|
263
|
+
if (header.sweepClaimUntil !== undefined && now < header.sweepClaimUntil) return false;
|
|
264
|
+
await appendLine(runId, {
|
|
265
|
+
kind: "header",
|
|
266
|
+
header: { ...header, sweepClaimUntil: now + AGENT_EVENT_SWEEP_CLAIM_MS },
|
|
267
|
+
});
|
|
268
|
+
return true;
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
async listHeaders() {
|
|
272
|
+
let names: string[] = [];
|
|
273
|
+
try {
|
|
274
|
+
names = await readdir(dir);
|
|
275
|
+
} catch {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
const headers: AgentEventHeaderRecord[] = [];
|
|
279
|
+
for (const name of names) {
|
|
280
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
281
|
+
const runId = decodeURIComponent(name.slice(0, -".jsonl".length));
|
|
282
|
+
const header = headerOf(await readLines(runId));
|
|
283
|
+
if (header) headers.push(header);
|
|
284
|
+
}
|
|
285
|
+
return headers;
|
|
286
|
+
},
|
|
287
|
+
async writeHeader(header) {
|
|
288
|
+
await withLock(header.runId, () => appendLine(header.runId, { kind: "header", header }));
|
|
289
|
+
},
|
|
290
|
+
async append(runId, row) {
|
|
291
|
+
await withLock(runId, async () => {
|
|
292
|
+
const rows = rowsOf(await readLines(runId));
|
|
293
|
+
if (rows.some((stored) => stored.seq === row.seq)) {
|
|
294
|
+
throw new AgentEventDuplicateSeqError(runId, row.seq);
|
|
295
|
+
}
|
|
296
|
+
await appendLine(runId, { kind: "row", seq: row.seq, event: row.event });
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
async remove(runId) {
|
|
300
|
+
await withLock(runId, async () => {
|
|
301
|
+
await rm(fileOf(runId), { force: true });
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** SQL client the postgres event store needs. */
|
|
308
|
+
export interface AgentEventSql {
|
|
309
|
+
query(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
|
|
310
|
+
exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Postgres event store on the journal connection.
|
|
315
|
+
*
|
|
316
|
+
* Followers read `seq > last`. A duplicate primary key is an error.
|
|
317
|
+
*
|
|
318
|
+
* @param sql - Journal SQL client
|
|
319
|
+
*/
|
|
320
|
+
export async function createPostgresAgentEventStore(sql: AgentEventSql): Promise<AgentEventStore> {
|
|
321
|
+
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_agent_run (
|
|
322
|
+
run_id TEXT PRIMARY KEY,
|
|
323
|
+
header TEXT NOT NULL,
|
|
324
|
+
sweep_claim_at BIGINT
|
|
325
|
+
)`);
|
|
326
|
+
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_agent_event (
|
|
327
|
+
run_id TEXT NOT NULL,
|
|
328
|
+
seq INTEGER NOT NULL,
|
|
329
|
+
event TEXT NOT NULL,
|
|
330
|
+
event_type TEXT,
|
|
331
|
+
event_name TEXT,
|
|
332
|
+
PRIMARY KEY (run_id, seq)
|
|
333
|
+
)`);
|
|
334
|
+
await sql.exec(`ALTER TABLE oke_agent_run ADD COLUMN IF NOT EXISTS sweep_claim_at BIGINT`);
|
|
335
|
+
await sql.exec(`ALTER TABLE oke_agent_event ADD COLUMN IF NOT EXISTS event_type TEXT`);
|
|
336
|
+
await sql.exec(`ALTER TABLE oke_agent_event ADD COLUMN IF NOT EXISTS event_name TEXT`);
|
|
337
|
+
|
|
338
|
+
const isDuplicate = (err: unknown): boolean => {
|
|
339
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
340
|
+
return /duplicate key|unique constraint|PRIMARY KEY/i.test(message);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const readHeader = async (runId: string): Promise<AgentEventHeaderRecord | undefined> => {
|
|
344
|
+
const headers = await sql.query(`SELECT header FROM oke_agent_run WHERE run_id = ?`, [runId]);
|
|
345
|
+
const headerRow = headers[0];
|
|
346
|
+
if (!headerRow) return undefined;
|
|
347
|
+
return JSON.parse(String(headerRow.header)) as AgentEventHeaderRecord;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
async read(runId) {
|
|
352
|
+
const headers = await sql.query(`SELECT header FROM oke_agent_run WHERE run_id = ?`, [runId]);
|
|
353
|
+
const headerRow = headers[0];
|
|
354
|
+
if (!headerRow) return undefined;
|
|
355
|
+
const header = JSON.parse(String(headerRow.header)) as AgentEventHeaderRecord;
|
|
356
|
+
const rows = await readAfter(sql, runId, 0);
|
|
357
|
+
return { header, rows };
|
|
358
|
+
},
|
|
359
|
+
async readAfter(runId, afterSeq) {
|
|
360
|
+
return readAfter(sql, runId, afterSeq);
|
|
361
|
+
},
|
|
362
|
+
async maxSeq(runId) {
|
|
363
|
+
const rows = await sql.query(
|
|
364
|
+
`SELECT COALESCE(MAX(seq), 0) AS max_seq FROM oke_agent_event WHERE run_id = ?`,
|
|
365
|
+
[runId],
|
|
366
|
+
);
|
|
367
|
+
return Number(rows[0]?.max_seq ?? 0);
|
|
368
|
+
},
|
|
369
|
+
readHeader,
|
|
370
|
+
async truncated(runId) {
|
|
371
|
+
const rows = await sql.query(
|
|
372
|
+
`SELECT 1 AS hit FROM oke_agent_event WHERE run_id = ? AND event_type = ? AND event_name = ? LIMIT 1`,
|
|
373
|
+
[runId, "CUSTOM", "oke.events.truncated"],
|
|
374
|
+
);
|
|
375
|
+
return rows.length > 0;
|
|
376
|
+
},
|
|
377
|
+
async claim(runId, now = Date.now()) {
|
|
378
|
+
const current = await readHeader(runId);
|
|
379
|
+
if (!current) return false;
|
|
380
|
+
const until = now + AGENT_EVENT_SWEEP_CLAIM_MS;
|
|
381
|
+
const updated = await sql.exec(
|
|
382
|
+
`UPDATE oke_agent_run SET sweep_claim_at = ? WHERE run_id = ? AND (sweep_claim_at IS NULL OR sweep_claim_at <= ?)`,
|
|
383
|
+
[until, runId, now],
|
|
384
|
+
);
|
|
385
|
+
return updated.changes === 1;
|
|
386
|
+
},
|
|
387
|
+
async listHeaders() {
|
|
388
|
+
const rows = await sql.query(`SELECT header FROM oke_agent_run`);
|
|
389
|
+
return rows.map((row) => JSON.parse(String(row.header)) as AgentEventHeaderRecord);
|
|
390
|
+
},
|
|
391
|
+
async writeHeader(header) {
|
|
392
|
+
await sql.exec(
|
|
393
|
+
`INSERT INTO oke_agent_run (run_id, header) VALUES (?, ?)
|
|
394
|
+
ON CONFLICT (run_id) DO UPDATE SET header = EXCLUDED.header`,
|
|
395
|
+
[header.runId, JSON.stringify(header)],
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
async append(runId, row) {
|
|
399
|
+
try {
|
|
400
|
+
const columns = eventColumns(row.event);
|
|
401
|
+
await sql.exec(
|
|
402
|
+
`INSERT INTO oke_agent_event (run_id, seq, event, event_type, event_name) VALUES (?, ?, ?, ?, ?)`,
|
|
403
|
+
[runId, row.seq, JSON.stringify(row.event), columns.type, columns.name],
|
|
404
|
+
);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if (isDuplicate(err)) throw new AgentEventDuplicateSeqError(runId, row.seq);
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
async remove(runId) {
|
|
411
|
+
await sql.exec(`DELETE FROM oke_agent_event WHERE run_id = ?`, [runId]);
|
|
412
|
+
await sql.exec(`DELETE FROM oke_agent_run WHERE run_id = ?`, [runId]);
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function eventColumns(event: unknown): {
|
|
418
|
+
readonly type: string | null;
|
|
419
|
+
readonly name: string | null;
|
|
420
|
+
} {
|
|
421
|
+
if (!event || typeof event !== "object") return { type: null, name: null };
|
|
422
|
+
const row = event as { type?: unknown; name?: unknown };
|
|
423
|
+
return {
|
|
424
|
+
type: typeof row.type === "string" ? row.type : null,
|
|
425
|
+
name: typeof row.name === "string" ? row.name : null,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function isTruncationEvent(event: unknown): boolean {
|
|
430
|
+
if (!event || typeof event !== "object") return false;
|
|
431
|
+
const row = event as { type?: unknown; name?: unknown };
|
|
432
|
+
return row.type === "CUSTOM" && row.name === "oke.events.truncated";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function readAfter(
|
|
436
|
+
sql: AgentEventSql,
|
|
437
|
+
runId: string,
|
|
438
|
+
afterSeq: number,
|
|
439
|
+
): Promise<AgentEventRowRecord[]> {
|
|
440
|
+
const rows = await sql.query(
|
|
441
|
+
`SELECT seq, event FROM oke_agent_event WHERE run_id = ? AND seq > ? ORDER BY seq`,
|
|
442
|
+
[runId, afterSeq],
|
|
443
|
+
);
|
|
444
|
+
return rows.map((row) => ({
|
|
445
|
+
seq: Number(row.seq),
|
|
446
|
+
event: JSON.parse(String(row.event)),
|
|
447
|
+
}));
|
|
448
|
+
}
|