@godspeedai/cognate-runtime-bun 0.1.1 → 0.2.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/README.md +158 -38
- package/package.json +13 -12
- package/src/index.ts +13 -2
- package/src/runtime.ts +121 -4
- package/src/service.ts +9 -3
package/README.md
CHANGED
|
@@ -1,40 +1,160 @@
|
|
|
1
1
|
# @godspeedai/cognate-runtime-bun
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
run
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
3
|
+
The Cognate reference runtime for Bun: durable agent runs, human-in-the-loop continuations,
|
|
4
|
+
artifacts, projections, and optional cognition, composed in-process behind a single
|
|
5
|
+
`RuntimeService`. Point it at a SQLite file and your application's history survives restarts; point it at PostgreSQL (or a pg0-managed local instance) and multiple processes share the same history safely.
|
|
6
|
+
|
|
7
|
+
**When to use this package:** to run agents for real — in a server, a worker, or a script. Pair it
|
|
8
|
+
with [`@godspeedai/cognate-client`](https://www.npmjs.com/package/@godspeedai/cognate-client) to
|
|
9
|
+
consume it from the same process, or with a transport adapter to serve it over the network.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add @godspeedai/cognate-runtime-bun
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires [Bun](https://bun.sh) >= 1.4.0 (the default store uses Bun's built-in `bun:sqlite`).
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createRuntime } from "@godspeedai/cognate-runtime-bun";
|
|
23
|
+
import type { AgentDefinition } from "@godspeedai/cognate-runtime-api";
|
|
24
|
+
|
|
25
|
+
const agent: AgentDefinition = {
|
|
26
|
+
id: "agent.order",
|
|
27
|
+
version: "1.0.0",
|
|
28
|
+
async run(_input, ctx) {
|
|
29
|
+
// Shared state: a snapshot, then JSON Patch deltas — clients derive the same state.
|
|
30
|
+
await ctx.emit("state.changed", { snapshot: { order: { id: "o-1", status: "open", total: 0 } } });
|
|
31
|
+
await ctx.emit("state.changed", { patch: [{ op: "replace", path: "/order/status", value: "paid" }] });
|
|
32
|
+
await ctx.emit("state.changed", { patch: [{ op: "replace", path: "/order/total", value: 42 }] });
|
|
33
|
+
return { done: true };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const caller = { actor: { id: "alice", kind: "user" }, tenant: "tenant-a" } as const;
|
|
38
|
+
|
|
39
|
+
const runtime = await createRuntime({
|
|
40
|
+
store: "./app.sqlite", // a durable file; use ":memory:" for an ephemeral runtime
|
|
41
|
+
// policy authorizes capability invocations; actions authorizes run/continuation actions.
|
|
42
|
+
policy: { authorize: () => ({ allow: true, policyId: "p", reason: "allowed" }) },
|
|
43
|
+
actions: { authorize: () => ({ allow: true, reason: "allowed" }) },
|
|
44
|
+
agents: [agent],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const started = await runtime.service.startRun(caller, {
|
|
48
|
+
agent: "agent.order",
|
|
49
|
+
input: {},
|
|
50
|
+
idempotencyKey: "order-1", // repeating the key returns the same run, never a duplicate
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await runtime.idle(); // background work queued so far has been processed
|
|
54
|
+
|
|
55
|
+
const run = await runtime.service.getRun(caller, { runId: started.runId });
|
|
56
|
+
console.log(run.status, run.output); // completed { done: true }
|
|
57
|
+
|
|
58
|
+
await runtime.close();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Suspending on a durable continuation
|
|
62
|
+
|
|
63
|
+
`ctx.waitFor` suspends the run until someone resolves the continuation it opens. The run's progress
|
|
64
|
+
is durable: it can sit waiting through restarts.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const refundAgent: AgentDefinition = {
|
|
68
|
+
id: "agent.refund",
|
|
69
|
+
version: "1.0.0",
|
|
70
|
+
async run(input, ctx) {
|
|
71
|
+
const { amount } = input as { amount: number };
|
|
72
|
+
const decision = (await ctx.waitFor("approve", {
|
|
73
|
+
type: "refund-approval",
|
|
74
|
+
condition: { kind: "human_approval" },
|
|
75
|
+
payload: { amount },
|
|
76
|
+
})) as { approved: boolean };
|
|
77
|
+
return { amount, approved: decision.approved };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const waiting = await runtime.service.startRun(caller, {
|
|
82
|
+
agent: "agent.refund",
|
|
83
|
+
input: { amount: 75 },
|
|
84
|
+
idempotencyKey: "refund-1",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Meanwhile — from a webhook, an admin UI, a shell — list and resolve open continuations.
|
|
88
|
+
const [continuation] = await runtime.service.listContinuations(caller, {
|
|
89
|
+
runId: waiting.runId,
|
|
90
|
+
status: "OPEN",
|
|
91
|
+
});
|
|
92
|
+
await runtime.service.resolveContinuation(caller, {
|
|
93
|
+
continuationId: continuation!.id,
|
|
94
|
+
action: "resume",
|
|
95
|
+
input: { approved: true },
|
|
96
|
+
idempotencyKey: "approve-1",
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The run re-enters the agent from the top, receives the recorded decision, and completes. Replays
|
|
101
|
+
never repeat recorded work: every `ctx.step`, `emit`, `invoke`, `artifact`, and `waitFor` result is
|
|
102
|
+
an event on the run's stream, and re-entry returns those results instead of re-executing.
|
|
103
|
+
|
|
104
|
+
## What you get from `createRuntime`
|
|
105
|
+
|
|
106
|
+
The returned `Runtime` exposes:
|
|
107
|
+
|
|
108
|
+
- `service` — the full `RuntimeService`: start/get/cancel/list runs, a reconnectable event stream
|
|
109
|
+
(`service.events(caller, { follow: true })`), continuations, direct capability `invoke`,
|
|
110
|
+
`getArtifact`, public projection reads, and shared run/thread state reads and updates.
|
|
111
|
+
- `store` — the underlying `DurableStore`, if you need direct access to the event log.
|
|
112
|
+
- `kernel` — the embedded capability kernel.
|
|
113
|
+
- `inspect(correlationId)` — the causal chain for a correlation id: runs, events, invocations,
|
|
114
|
+
artifacts, observations.
|
|
115
|
+
- `erase(eventId, reason, actor)` — erase an event's payload; derived views and the cognitive index
|
|
116
|
+
drop it too.
|
|
117
|
+
- `rebuildCognition()`, `pollApprovals()`, `idle(timeoutMs?)`, and `close()`.
|
|
118
|
+
|
|
119
|
+
Notable options beyond the quick start: `components` (capability-providing kernel components your
|
|
120
|
+
agents can `ctx.invoke`), `projections` ([`@godspeedai/cognate-projections`](https://www.npmjs.com/package/@godspeedai/cognate-projections)
|
|
121
|
+
kept current automatically), `cognition` (index completed runs and recall them via `ctx.recall`),
|
|
122
|
+
`outboxFor` (extra outbox intents committed atomically with each run event), `governance`, and
|
|
123
|
+
`tickMs` (background loop interval, default 10 ms).
|
|
124
|
+
|
|
125
|
+
## Limitations
|
|
126
|
+
|
|
127
|
+
- **One process, one writer.** All runs live in the process that opened the store; executions
|
|
128
|
+
within a tick run sequentially, so a slow agent delays others. To scale out, split runtimes per
|
|
129
|
+
store file.
|
|
130
|
+
- **Steps are at-least-once.** A step that was in flight during a crash re-executes after restart.
|
|
131
|
+
Keep steps idempotent, and put irreversible external calls behind a `ctx.waitFor` continuation so
|
|
132
|
+
they happen once, resolved deliberately.
|
|
133
|
+
- **Polling, not pushing, inside the process.** Background work (run execution, resume delivery,
|
|
134
|
+
continuation expiry) runs on a loop every `tickMs`.
|
|
135
|
+
- **Erasure has copies.** `runtime.erase` removes an event's payload from the log, derived views,
|
|
136
|
+
and the cognitive index — but content copied into continuation payloads, idempotency snapshots,
|
|
137
|
+
or recall results stays.
|
|
138
|
+
- **Public projections are tenant-partitioned.** Register projections that key their state
|
|
139
|
+
`<tenant>/<key>`; `service.readProjection` returns only the caller's tenant partition.
|
|
140
|
+
- **Denied means invisible.** Reading an artifact you are not allowed to see returns the same
|
|
141
|
+
"not found" as a missing one.
|
|
142
|
+
- **Recall degrades gracefully.** Without a `cognition` configuration, `ctx.recall` returns
|
|
143
|
+
`{ status: "degraded" }` instead of failing the run.
|
|
144
|
+
|
|
145
|
+
## Related packages
|
|
146
|
+
|
|
147
|
+
- [`@godspeedai/cognate-client`](https://www.npmjs.com/package/@godspeedai/cognate-client) — the
|
|
148
|
+
headless client for consuming this runtime; derives live run state for you.
|
|
149
|
+
- [`@godspeedai/cognate-runtime-api`](https://www.npmjs.com/package/@godspeedai/cognate-runtime-api) —
|
|
150
|
+
the service and agent-authoring contract this package implements.
|
|
151
|
+
- [`@godspeedai/cognate-store-sqlite`](https://www.npmjs.com/package/@godspeedai/cognate-store-sqlite) —
|
|
152
|
+
the durable store behind `store: "<path>"`.
|
|
153
|
+
- [`@godspeedai/cognate-store-memory`](https://www.npmjs.com/package/@godspeedai/cognate-store-memory) —
|
|
154
|
+
bring your own ephemeral store via `store: { store }`.
|
|
155
|
+
- [`@godspeedai/cognate`](https://www.npmjs.com/package/@godspeedai/cognate) — the SDK umbrella;
|
|
156
|
+
one install for the runtime and the client together.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godspeedai/cognate-runtime-bun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Bun reference runtime: composes kernel, durable store, runs, continuations, commitments, artifacts, and cognition behind RuntimeService.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,17 +22,18 @@
|
|
|
22
22
|
".": "./src/index.ts"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@godspeedai/cognate-artifacts": "^0.
|
|
26
|
-
"@godspeedai/cognate-capabilities": "^0.
|
|
27
|
-
"@godspeedai/cognate-cognitive-api": "^0.
|
|
28
|
-
"@godspeedai/cognate-continuations": "^0.
|
|
29
|
-
"@godspeedai/cognate-events": "^0.
|
|
30
|
-
"@godspeedai/cognate-kernel-api": "^0.
|
|
31
|
-
"@godspeedai/cognate-kernel-cordis": "^0.
|
|
32
|
-
"@godspeedai/cognate-projections": "^0.
|
|
33
|
-
"@godspeedai/cognate-runtime-api": "^0.
|
|
34
|
-
"@godspeedai/cognate-store-memory": "^0.
|
|
35
|
-
"@godspeedai/cognate-store-
|
|
25
|
+
"@godspeedai/cognate-artifacts": "^0.2.0",
|
|
26
|
+
"@godspeedai/cognate-capabilities": "^0.2.0",
|
|
27
|
+
"@godspeedai/cognate-cognitive-api": "^0.2.0",
|
|
28
|
+
"@godspeedai/cognate-continuations": "^0.2.0",
|
|
29
|
+
"@godspeedai/cognate-events": "^0.2.0",
|
|
30
|
+
"@godspeedai/cognate-kernel-api": "^0.2.0",
|
|
31
|
+
"@godspeedai/cognate-kernel-cordis": "^0.2.0",
|
|
32
|
+
"@godspeedai/cognate-projections": "^0.2.0",
|
|
33
|
+
"@godspeedai/cognate-runtime-api": "^0.2.0",
|
|
34
|
+
"@godspeedai/cognate-store-memory": "^0.2.0",
|
|
35
|
+
"@godspeedai/cognate-store-postgres": "^0.2.0",
|
|
36
|
+
"@godspeedai/cognate-store-sqlite": "^0.2.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@godspeedai/cognate-cognitive-memory": "workspace:*"
|
package/src/index.ts
CHANGED
|
@@ -34,9 +34,20 @@ export type PendingEvent = NewEvent & { readonly eventId: string; readonly strea
|
|
|
34
34
|
/** Outbox topic whose items represent RealityTrace observations; `inspect` reports their observation ids. */
|
|
35
35
|
export const OBSERVATION_OUTBOX_TOPIC = "observation.deliver";
|
|
36
36
|
|
|
37
|
+
/** Where the runtime's authoritative state lives (spec §13). SQLite stays the zero-dependency default;
|
|
38
|
+
* PostgreSQL removes the single-writer limit (multi-process runtimes share one database). */
|
|
39
|
+
export type RuntimeStoreConfig =
|
|
40
|
+
/** SQLite database file path, or `":memory:"` for an in-memory SQLite database. */
|
|
41
|
+
| string
|
|
42
|
+
| { readonly store: DurableStore }
|
|
43
|
+
| { readonly sqlite: { readonly path: string } }
|
|
44
|
+
| { readonly postgres: { readonly url: string; readonly schema?: string } }
|
|
45
|
+
| { readonly postgresPg0: { readonly name?: string; readonly schema?: string; readonly bin?: string } };
|
|
46
|
+
|
|
37
47
|
export interface RuntimeOptions {
|
|
38
|
-
/**
|
|
39
|
-
|
|
48
|
+
/** Where durable state lives: a SQLite path (default form), an explicit store, a PostgreSQL
|
|
49
|
+
* connection (`postgres`), or a pg0-managed local instance (`postgresPg0` starts it if needed). */
|
|
50
|
+
readonly store: RuntimeStoreConfig;
|
|
40
51
|
/** Kernel invocation policy for capabilities (default-deny if omitted). */
|
|
41
52
|
readonly policy: Policy;
|
|
42
53
|
readonly actions: ActionPolicy;
|
package/src/runtime.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Composition root: store, kernel, continuations, artifacts, projections, cognition, and the background loop.
|
|
2
2
|
import { ArtifactService } from "@godspeedai/cognate-artifacts";
|
|
3
3
|
import { ContinuationService } from "@godspeedai/cognate-continuations";
|
|
4
|
-
import { type DurableStore, OutboxDispatcher } from "@godspeedai/cognate-events";
|
|
4
|
+
import { type DurableStore, type Json, NotFoundError, OutboxDispatcher } from "@godspeedai/cognate-events";
|
|
5
5
|
import { createCordisKernel } from "@godspeedai/cognate-kernel-cordis";
|
|
6
6
|
import type { Principal } from "@godspeedai/cognate-kernel-api";
|
|
7
7
|
import { ProjectionRunner } from "@godspeedai/cognate-projections";
|
|
8
|
+
import { openPostgresStore, resolvePg0Url } from "@godspeedai/cognate-store-postgres";
|
|
8
9
|
import { openSqliteStore } from "@godspeedai/cognate-store-sqlite";
|
|
9
10
|
import { COGNITION_PROJECTION, cognitionProjection, recallFor } from "./cognition.ts";
|
|
10
11
|
import type { Core } from "./core.ts";
|
|
@@ -21,8 +22,23 @@ const DELIVERY = { baseMs: 50, maxMs: 5_000, maxAttempts: 10 };
|
|
|
21
22
|
const TOPICS = [RUN_EXECUTE_TOPIC, "continuation.resume"];
|
|
22
23
|
|
|
23
24
|
export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// The runtime closes only stores it opened itself; an injected store is owned by its creator.
|
|
26
|
+
// (The PostgreSQL store's close() ends its pool and LISTEN connection, so one call covers both.)
|
|
27
|
+
const config = options.store;
|
|
28
|
+
const ownsStore = typeof config === "string" || !("store" in config);
|
|
29
|
+
let store: DurableStore;
|
|
30
|
+
if (typeof config === "string") {
|
|
31
|
+
store = openSqliteStore({ path: config });
|
|
32
|
+
} else if ("store" in config) {
|
|
33
|
+
store = config.store;
|
|
34
|
+
} else if ("sqlite" in config) {
|
|
35
|
+
store = openSqliteStore({ path: config.sqlite.path });
|
|
36
|
+
} else if ("postgres" in config) {
|
|
37
|
+
store = (await openPostgresStore({ url: config.postgres.url, schema: config.postgres.schema })).store;
|
|
38
|
+
} else {
|
|
39
|
+
const url = await resolvePg0Url(config.postgresPg0);
|
|
40
|
+
store = (await openPostgresStore({ url, schema: config.postgresPg0.schema })).store;
|
|
41
|
+
}
|
|
26
42
|
const system: Principal = options.system ?? { id: "system", kind: "system" };
|
|
27
43
|
const governor = new Governor(options.actions, options.policy, options.governance ?? { mode: "off" });
|
|
28
44
|
const invoker = new Invoker(governor);
|
|
@@ -136,6 +152,107 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
|
|
155
|
+
// ── Erasure propagation (spec §32, DEBT-014) ─────────────────────────────────────
|
|
156
|
+
// Erasing one event must also reach its derived copies: idempotency result snapshots (redacted by
|
|
157
|
+
// the store's erasePayload itself), continuation records and their transition events, run.resumed
|
|
158
|
+
// resolution copies on the run stream, and recorded run.recall results referencing the erased
|
|
159
|
+
// run's view. Every erasure is recorded as a durable job before it runs, so an interrupted
|
|
160
|
+
// propagation resumes on the next erase call or runtime start instead of being reported complete.
|
|
161
|
+
const ERASURES = "erasures";
|
|
162
|
+
|
|
163
|
+
interface ErasureJob {
|
|
164
|
+
readonly id: string;
|
|
165
|
+
readonly streamId: string | null;
|
|
166
|
+
readonly runId: string | null;
|
|
167
|
+
readonly eventType: string | null;
|
|
168
|
+
readonly reason: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function propagateErasure(job: ErasureJob): Promise<void> {
|
|
172
|
+
const event = await store.getEvent(job.id);
|
|
173
|
+
if (event && !event.erasure) await store.erasePayload(job.id, job.reason, "erasure");
|
|
174
|
+
if (!job.runId) return;
|
|
175
|
+
await continuations.redactForRun(job.runId, { reason: job.reason });
|
|
176
|
+
// run.resumed events carry the resolution-input copy on the run stream itself.
|
|
177
|
+
for (const e of await store.readStream(runStream(job.runId))) {
|
|
178
|
+
if (e.eventType === "run.resumed" && !e.erasure) await store.erasePayload(e.eventId, job.reason, "erasure");
|
|
179
|
+
}
|
|
180
|
+
// A recorded run.recall whose items reference an erased run's view copies that view's source
|
|
181
|
+
// content (run.started input, run.completed output). Lineage is explicit (sourceRef), not a text
|
|
182
|
+
// guess. The closure is transitive: a recalling run's own terminal output may embed the recalled
|
|
183
|
+
// result (the agent returned it), and a third run may have recalled THAT view — so each erased
|
|
184
|
+
// terminal output makes its run a view source too. Visited runs terminate the loop.
|
|
185
|
+
if (job.eventType !== "run.started" && job.eventType !== "run.completed") return;
|
|
186
|
+
const viewSources: string[] = [job.runId];
|
|
187
|
+
const visited = new Set<string>();
|
|
188
|
+
while (viewSources.length > 0) {
|
|
189
|
+
const sourceRunId = viewSources.shift()!;
|
|
190
|
+
if (visited.has(sourceRunId)) continue;
|
|
191
|
+
visited.add(sourceRunId);
|
|
192
|
+
let afterPosition = 0;
|
|
193
|
+
for (;;) {
|
|
194
|
+
const page = await store.readAll({ afterPosition, limit: 500 });
|
|
195
|
+
if (page.length === 0) break;
|
|
196
|
+
for (const e of page) {
|
|
197
|
+
afterPosition = e.position;
|
|
198
|
+
if (e.eventType !== "run.recall" || e.erasure || !e.runId) continue;
|
|
199
|
+
const items = (e.payload as { readonly result?: { readonly items?: readonly { readonly sourceRef?: string }[] } } | null)?.result?.items;
|
|
200
|
+
if (!Array.isArray(items) || !items.some((item) => item.sourceRef === `run:${sourceRunId}`)) continue;
|
|
201
|
+
await store.erasePayload(e.eventId, job.reason, "erasure");
|
|
202
|
+
// The recalling run's terminal output may embed the recalled content; erase it and treat
|
|
203
|
+
// that run as a view source for its own recallers.
|
|
204
|
+
for (const terminal of await store.readStream(runStream(e.runId))) {
|
|
205
|
+
if ((terminal.eventType === "run.completed" || terminal.eventType === "run.failed") && !terminal.erasure) {
|
|
206
|
+
await store.erasePayload(terminal.eventId, job.reason, "erasure");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
viewSources.push(e.runId);
|
|
210
|
+
}
|
|
211
|
+
if (page.length < 500) break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function completeErasureJob(job: { readonly id: string; readonly version: number; readonly value: Record<string, Json> }): Promise<void> {
|
|
217
|
+
await store.commit({
|
|
218
|
+
appends: [],
|
|
219
|
+
records: [{ collection: ERASURES, id: job.id, expectedVersion: job.version, value: { ...job.value, status: "complete", completedAt: new Date().toISOString() } }],
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function resumeErasureJobs(): Promise<void> {
|
|
224
|
+
for (const stored of await store.listRecords(ERASURES, { status: "propagating" })) {
|
|
225
|
+
const value = stored.value;
|
|
226
|
+
await propagateErasure({
|
|
227
|
+
id: stored.id,
|
|
228
|
+
streamId: (value.streamId as string | null) ?? null,
|
|
229
|
+
runId: (value.runId as string | null) ?? null,
|
|
230
|
+
eventType: (value.eventType as string | null) ?? null,
|
|
231
|
+
reason: value.reason as string,
|
|
232
|
+
});
|
|
233
|
+
await completeErasureJob(stored);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function eraseWithPropagation(eventId: string, reason: string, actor: Principal): Promise<void> {
|
|
238
|
+
await resumeErasureJobs();
|
|
239
|
+
const target = await store.getEvent(eventId);
|
|
240
|
+
if (!target) throw new NotFoundError(`event ${eventId} not found`);
|
|
241
|
+
let job = await store.getRecord(ERASURES, eventId);
|
|
242
|
+
if (!job) {
|
|
243
|
+
await store.commit({
|
|
244
|
+
appends: [],
|
|
245
|
+
records: [{ collection: ERASURES, id: eventId, expectedVersion: 0, value: { status: "propagating", reason, actorId: actor.id, streamId: target.streamId, runId: target.runId, eventType: target.eventType, startedAt: new Date().toISOString() } }],
|
|
246
|
+
});
|
|
247
|
+
job = await store.getRecord(ERASURES, eventId);
|
|
248
|
+
}
|
|
249
|
+
if (!job) throw new Error(`erase: could not record the propagation job for ${eventId}`);
|
|
250
|
+
await propagateErasure({ id: eventId, streamId: target.streamId, runId: target.runId, eventType: target.eventType, reason });
|
|
251
|
+
await completeErasureJob(job);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
await resumeErasureJobs();
|
|
255
|
+
|
|
139
256
|
const timer = setInterval(kick, options.tickMs ?? 10);
|
|
140
257
|
kick();
|
|
141
258
|
|
|
@@ -203,7 +320,7 @@ export async function startRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
|
203
320
|
},
|
|
204
321
|
|
|
205
322
|
async erase(eventId, reason, actor) {
|
|
206
|
-
await
|
|
323
|
+
await eraseWithPropagation(eventId, reason, actor);
|
|
207
324
|
},
|
|
208
325
|
|
|
209
326
|
pollApprovals,
|
package/src/service.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// In-process RuntimeService (ADR-0011): authorization, tenancy, and typed errors over the durable core.
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import type { ContinuationRecord, TransitionOutcome } from "@godspeedai/cognate-continuations";
|
|
4
|
-
import { ConcurrencyError, fingerprint, IdempotencyMismatchError, type Json, type StoredEvent } from "@godspeedai/cognate-events";
|
|
4
|
+
import { ConcurrencyError, fingerprint, IdempotencyMismatchError, readProjectionSnapshot, type Json, type StoredEvent } from "@godspeedai/cognate-events";
|
|
5
5
|
import { applyStateUpdate, type Caller, type ContinuationView, foldState, type ObservationInput, type ObservationView, type RunView, type RuntimeService, ServiceError, type ServiceErrorCode, StatePatchError } from "@godspeedai/cognate-runtime-api";
|
|
6
6
|
import type { Core } from "./core.ts";
|
|
7
7
|
import type { ActionName } from "./governed-operation.ts";
|
|
@@ -218,9 +218,15 @@ export function createService(core: Core, publicProjections: ReadonlySet<string>
|
|
|
218
218
|
async readProjection(caller, request) {
|
|
219
219
|
if (!publicProjections.has(request.name)) throw new ServiceError("not_found", `projection ${request.name} not found`);
|
|
220
220
|
// Public projections are tenant-partitioned by key prefix `<tenant>/` (spec §32); an empty
|
|
221
|
-
// partition is [] so the caller cannot tell whether other tenants have data.
|
|
221
|
+
// partition is [] so the caller cannot tell whether other tenants have data. The checkpoint is
|
|
222
|
+
// read consistently with the rows (never ahead of them) so a client can tell whether its view
|
|
223
|
+
// covers the events it has observed (DEBT-021).
|
|
222
224
|
const prefix = `${caller.tenant}/`;
|
|
223
|
-
|
|
225
|
+
const snapshot = await readProjectionSnapshot(store, request.name);
|
|
226
|
+
return {
|
|
227
|
+
rows: snapshot.rows.filter((e) => e.key.startsWith(prefix)).map((e) => ({ key: e.key.slice(prefix.length), value: e.value })),
|
|
228
|
+
checkpoint: snapshot.checkpoint ?? null,
|
|
229
|
+
};
|
|
224
230
|
},
|
|
225
231
|
|
|
226
232
|
async listRuns(caller, request) {
|