@alma-harness/runtime 0.11.0 → 0.12.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 +160 -5
- package/dist/agent.d.ts +2 -1
- package/dist/agent.js +7 -1
- package/dist/agent.js.map +1 -1
- package/dist/chunk-6MFSZ4VS.js +274 -0
- package/dist/chunk-6MFSZ4VS.js.map +1 -0
- package/dist/chunk-EGQL2P7U.js +179 -0
- package/dist/chunk-EGQL2P7U.js.map +1 -0
- package/dist/chunk-II7NDZRL.js +31 -0
- package/dist/chunk-II7NDZRL.js.map +1 -0
- package/dist/deployment.d.ts +88 -0
- package/dist/deployment.js +130 -0
- package/dist/deployment.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/maintenance-core-DDX5ryx7.d.ts +73 -0
- package/dist/maintenance.d.ts +32 -0
- package/dist/maintenance.js +118 -0
- package/dist/maintenance.js.map +1 -0
- package/dist/memory.js +5 -3
- package/dist/memory.js.map +1 -1
- package/dist/postgres.js +4 -100
- package/dist/postgres.js.map +1 -1
- package/dist/projections.d.ts +100 -0
- package/dist/projections.js +203 -0
- package/dist/projections.js.map +1 -0
- package/package.json +27 -15
- package/dist/chunk-3OEFEOI4.js +0 -10
- package/dist/chunk-3OEFEOI4.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,49 @@
|
|
|
1
1
|
# @alma-harness/runtime
|
|
2
2
|
|
|
3
|
-
Official storage composition
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Official storage composition, a deployment that derives governed runners, and a
|
|
4
|
+
simple text conversation facade. The root export contains types only. Use
|
|
5
|
+
`/deployment` to configure once, `/agent` for query/resume, `/memory` or
|
|
6
|
+
`/postgres` for storage, and the lower-level factories for advanced routing.
|
|
7
|
+
|
|
8
|
+
## Configure once: `/deployment`
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createRuntimeDeployment } from '@alma-harness/runtime/deployment';
|
|
12
|
+
const deployment = createRuntimeDeployment({
|
|
13
|
+
runtime, clients: { anthropic, openai }, prices, priceVersion,
|
|
14
|
+
consumers: ['product'], caps, retentionMs: 60_000,
|
|
15
|
+
limits: { deadlineMs: 8_000, runTimeoutMs: 120_000, stepTimeoutMs: 60_000, maxCalls: 24,
|
|
16
|
+
maxInputChars: 64_000, maxRequestChars: 1_000_000, maxOutputChars: 100_000 },
|
|
17
|
+
onBackgroundError: reportAccountingIssue,
|
|
18
|
+
});
|
|
19
|
+
const titles = deployment.singleCall({ policy, policyVersion, outputContractVersion, system: [], maxTokens: 64 });
|
|
20
|
+
const pick = deployment.structuredCall({ policy, policyVersion, outputContractVersion, system, maxTokens, tools, toolChoice });
|
|
21
|
+
const chat = deployment.conversation({ policy, policyVersion, configRevision, resultContractVersion, system, tools,
|
|
22
|
+
caps: perTurnCaps, limits: { maxCalls: 8 } });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Each factory returns the runner that `createSingleCallRunner`,
|
|
26
|
+
`createStructuredCallRunner` or `createConversationRunner` would build from the same
|
|
27
|
+
values. They keep their validation, bindings and replay (spec: runtime-deployment).
|
|
28
|
+
|
|
29
|
+
- **Owned by the deployment:** the runtime's stores and audit log, `clients`,
|
|
30
|
+
`prices`, `priceVersion`, `consumers`, the result retention and a conversation's
|
|
31
|
+
`step`. An operation passing any of them, even as `undefined`, throws `TypeError`.
|
|
32
|
+
A product that needs a different store, audit sink or price table for one
|
|
33
|
+
operation composes that runner explicitly.
|
|
34
|
+
- **Overridable per operation:** `caps` replaces the deployment's caps whole (no
|
|
35
|
+
merging), `limits` merge field by field, `onBackgroundError` replaces.
|
|
36
|
+
- **Required limits:** single-call and structured need `deadlineMs`,
|
|
37
|
+
`maxInputChars` and `maxOutputChars`. A conversation needs `runTimeoutMs` (root),
|
|
38
|
+
`stepTimeoutMs` (each step), `maxCalls`, `maxInputChars`, `maxRequestChars` and
|
|
39
|
+
`maxOutputChars`. There are no defaults: text replay binds versions, not limits,
|
|
40
|
+
so a default that moved in a release would change admitted work silently.
|
|
41
|
+
- **Snapshot:** prices, caps, limits, consumers, the client registry and both levels
|
|
42
|
+
of the store map are copied at construction. The deployment owns no pools and
|
|
43
|
+
never closes the runtime.
|
|
44
|
+
- **Your versioning duty is unchanged:** bump `policyVersion`, `priceVersion`,
|
|
45
|
+
`outputContractVersion`, `configRevision` or `inputRevision` when what they name
|
|
46
|
+
changes.
|
|
6
47
|
|
|
7
48
|
## Start and resume a conversation
|
|
8
49
|
|
|
@@ -17,7 +58,7 @@ const agent = createConversationAgent({
|
|
|
17
58
|
instructions: 'You are a helpful assistant.',
|
|
18
59
|
intent: { tier: 'standard', sensitivity: 'personal' },
|
|
19
60
|
limits: { caps: { perTurnUsd: 0.25 }, retentionMs: 60_000 },
|
|
20
|
-
|
|
61
|
+
onBackgroundError: reportAccountingIssue, // audit defaults to runtime.stores.audit
|
|
21
62
|
});
|
|
22
63
|
const user = agent.forUser({ org, uid }); // verified by the host
|
|
23
64
|
const first = await user.query({ prompt: 'Hello', messageId: 'delivery-1' });
|
|
@@ -32,7 +73,8 @@ const observed = await user.read({ sessionId: first.sessionId, messageId: 'deliv
|
|
|
32
73
|
|
|
33
74
|
The complete [quickstart](../../examples/quickstart/src/agent.ts) shows storage,
|
|
34
75
|
tools and configuration. The facade wires all stores, one model's routing and
|
|
35
|
-
internal versions; it does not own/close the supplied runtime.
|
|
76
|
+
internal versions; it does not own/close the supplied runtime. `audit` is optional and
|
|
77
|
+
defaults to `runtime.stores.audit`; omitting it never disables auditing. Optional `tools`,
|
|
36
78
|
`hooks` and `volatile` configure trusted behavior. Model/client/prices, audit,
|
|
37
79
|
intent, budget and retention remain explicit deployment choices. Keep one
|
|
38
80
|
immutable revision for the entire configuration, including trusted capabilities.
|
|
@@ -128,6 +170,11 @@ remains five seconds and idle timeout ten seconds. Generic driver overrides and
|
|
|
128
170
|
public pool handles are not provided. Migrations have their own two max1 pools,
|
|
129
171
|
always closed on completion/failure; application sizing does not tune migrations.
|
|
130
172
|
|
|
173
|
+
`runtime.stores.audit` writes to the audit tables the migration already installs in
|
|
174
|
+
the execution namespace, on the execution pool, so audit writes share that budget.
|
|
175
|
+
Audit history a host kept in another schema stays there; retention must cover the
|
|
176
|
+
execution namespace, as it already must for its settlement rows.
|
|
177
|
+
|
|
131
178
|
Budget `sum(runtime instances × (execution + roots))` plus migrations, retention,
|
|
132
179
|
backup, other pools and an administrative reserve against database capacity.
|
|
133
180
|
Count every replica/worker class and reuse a runtime across its turns/observers.
|
|
@@ -142,6 +189,110 @@ before `close()`; it attempts both pools and is idempotent. Closing neither abor
|
|
|
142
189
|
nor releases uncertain admissions. Resolve `reconciliation_required` through the
|
|
143
190
|
existing operator procedure, never by issuing a new delivery key.
|
|
144
191
|
|
|
192
|
+
## Maintenance: one entry point for interrupted work
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { createPostgresMaintenance } from '@alma-harness/runtime/maintenance';
|
|
196
|
+
const maintenance = createPostgresMaintenance({ schema, rootSchema, stepResultPolicy, rootResultPolicy, onPoolError,
|
|
197
|
+
discovery: { connectionString: discoveryURL }, worker: { connectionString: workerURL } });
|
|
198
|
+
const report = await maintenance.maintain({ limit: 50, signal }); // schedule it; the host names no scope
|
|
199
|
+
await maintenance.close();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`maintain` finds the scopes with due work and sweeps them, taking the least
|
|
203
|
+
recently visited first (spec: runtime-maintenance). It can find:
|
|
204
|
+
|
|
205
|
+
- expired admissions, which become uncertain and are never released;
|
|
206
|
+
- expired roots and executions, which are fenced;
|
|
207
|
+
- executions that can complete from their retained results;
|
|
208
|
+
- pending usage.
|
|
209
|
+
|
|
210
|
+
The report holds counts only. Nothing is logged, and the sweep never
|
|
211
|
+
dispatches: it has no model client.
|
|
212
|
+
|
|
213
|
+
**Provision two new logins, both without superuser, BYPASSRLS or ownership:**
|
|
214
|
+
|
|
215
|
+
- **Discovery:** a member of `alma_maintenance` only. That role is created by
|
|
216
|
+
`migratePostgresRuntime`. It sees identifiers, states and times across
|
|
217
|
+
scopes, never content.
|
|
218
|
+
- **Worker:** a member of `alma_app` only, and not the login of your request
|
|
219
|
+
path. It works one scope at a time with the application's authority.
|
|
220
|
+
|
|
221
|
+
Neither login may be a member of `alma_retention`, or of the other's role.
|
|
222
|
+
Every call checks this and throws before doing any work. Change these
|
|
223
|
+
memberships with maintenance stopped.
|
|
224
|
+
|
|
225
|
+
Scopes are claimed with session advisory locks, so concurrent callers skip each
|
|
226
|
+
other's scopes. Each scope gets a budget of 50 items per call for each kind of
|
|
227
|
+
work, and its own cursors. A scope that never clears comes back on the next lap
|
|
228
|
+
without starving the others. Retention and erasure stay with the `alma_retention`
|
|
229
|
+
operator. The memory runtime has no maintenance.
|
|
230
|
+
|
|
231
|
+
## Receipt projections
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { createPostgresProjections } from '@alma-harness/runtime/projections';
|
|
235
|
+
const projections = createPostgresProjections({ schema, rootSchema, stepResultPolicy, rootResultPolicy, onPoolError,
|
|
236
|
+
discovery: { connectionString: projectionURL }, worker: { connectionString: workerURL } });
|
|
237
|
+
const report = await projections.drain('cost-dashboard', async delivery => {
|
|
238
|
+
// { scope, consumer, settlementId, receipt, attribution, signal }; deduplicate on (scope, consumer, settlementId)
|
|
239
|
+
}, { limit: 50 });
|
|
240
|
+
await projections.quiesce(scope, () => erasure.erase(scope, request), { timeoutMs: 30_000 });
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`drain` finds the scopes with pending governed receipts for one consumer and
|
|
244
|
+
delivers each receipt with its session attribution (spec: receipt-projections).
|
|
245
|
+
Attribution is one of `available` (with labels), `erased` or `none`; erased is
|
|
246
|
+
never reported as none, so never fall back to labels you kept. A receipt is
|
|
247
|
+
acknowledged only after the handler resolves. A crash in between redelivers the
|
|
248
|
+
same key, so commit the deduplication and the effect in one transaction.
|
|
249
|
+
|
|
250
|
+
Delivery is fair and never loses a receipt:
|
|
251
|
+
|
|
252
|
+
- a receipt that always fails is passed over and retried on the next lap;
|
|
253
|
+
- laps are finite, and a late settlement is reached on a later lap.
|
|
254
|
+
|
|
255
|
+
The report holds counts only.
|
|
256
|
+
|
|
257
|
+
**Logins.** Provision two, as for maintenance:
|
|
258
|
+
|
|
259
|
+
- a **discovery** login that is a member of `alma_projection` only. It sees
|
|
260
|
+
pending receipt identities and times, never payloads;
|
|
261
|
+
- a **worker** login that is a member of `alma_app` only, and not your request
|
|
262
|
+
path's login.
|
|
263
|
+
|
|
264
|
+
Every call checks both. The database does not separate consumers from each
|
|
265
|
+
other. Give each consuming service its own pair of logins, and drain only the
|
|
266
|
+
consumer names that service owns.
|
|
267
|
+
|
|
268
|
+
**Erasure: quiesce, and fence any destination that stores labels.**
|
|
269
|
+
`quiesce` waits for a drain working the scope, holds new ones off, and runs
|
|
270
|
+
your erasure. If the erasure reports `complete: false`, `quiesce` rejects with
|
|
271
|
+
`IncompleteErasureError`. A drain whose claim is lost aborts `delivery.signal`
|
|
272
|
+
and starts no further receipt.
|
|
273
|
+
|
|
274
|
+
No Alma lock can stop a write a handler already began. So a destination that
|
|
275
|
+
keeps label values **must** serialize with erasure through gate rows. The
|
|
276
|
+
empty session `''` is the scope gate.
|
|
277
|
+
|
|
278
|
+
```sql
|
|
279
|
+
create table gates (org text, uid text, session text, erased boolean not null default false, primary key (org, uid, session));
|
|
280
|
+
-- publication, one transaction:
|
|
281
|
+
insert into gates (org, uid, session) values ($org, $uid, ''), ($org, $uid, $session) on conflict do nothing;
|
|
282
|
+
select erased from gates where org = $org and uid = $uid and session in ('', $session) for share; -- held until commit
|
|
283
|
+
-- write the labels only if no row is erased; otherwise write the receipt without them. Then commit.
|
|
284
|
+
-- erasure, inside quiesce, one transaction, two statements:
|
|
285
|
+
insert into gates (org, uid, session, erased) values ($org, $uid, $session, true)
|
|
286
|
+
on conflict (org, uid, session) do update set erased = true; -- waits for a publication holding the gate
|
|
287
|
+
update your_rows set labels = null where org = $org and uid = $uid and session = $session; -- a new snapshot, after the wait
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Keep the clearing in its own statement. In READ COMMITTED, a single statement
|
|
291
|
+
keeps the snapshot it took before waiting on the gate, so it would miss the row
|
|
292
|
+
the waiting publication just committed. For a scope-wide erasure, do the same
|
|
293
|
+
with the `''` gate and every row of the scope. A consumer that stores receipts
|
|
294
|
+
without labels needs no fence.
|
|
295
|
+
|
|
145
296
|
## Conversation operations
|
|
146
297
|
|
|
147
298
|
`@alma-harness/runtime/operations` exports `createConversationOperations`. Supply
|
|
@@ -158,3 +309,7 @@ blocked; release never marks an incomplete root successful. Expiry sweeps are
|
|
|
158
309
|
explicitly scope-wide. A stale digest requires reinspection, while lost resolution
|
|
159
310
|
ACK retries retain the same resolutionId. This is operator infrastructure, not a
|
|
160
311
|
request-path fallback or a remote-worker fencing implementation.
|
|
312
|
+
|
|
313
|
+
`runtime.stores.labels` is the session label store (spec: session-labels), in
|
|
314
|
+
memory or in the execution namespace on PostgreSQL; `migratePostgresRuntime`
|
|
315
|
+
installs it. Wire it into erasure as `createMemoryErasure({ sessionLabels })`.
|
package/dist/agent.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ interface ConversationAgentOptions {
|
|
|
13
13
|
prices: ModelPrice[];
|
|
14
14
|
};
|
|
15
15
|
instructions: string;
|
|
16
|
-
audit:
|
|
16
|
+
/** Omitted means `runtime.stores.audit`; never disables auditing (spec: runtime-deployment). */
|
|
17
|
+
audit?: ConversationConfig["step"]["audit"];
|
|
17
18
|
intent: SimpleConversationInput["intent"];
|
|
18
19
|
tools?: ConversationConfig["tools"];
|
|
19
20
|
hooks?: ConversationConfig["hooks"];
|
package/dist/agent.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAuditLog
|
|
3
|
+
} from "./chunk-II7NDZRL.js";
|
|
4
|
+
|
|
1
5
|
// src/agent.ts
|
|
2
6
|
import { createHash } from "crypto";
|
|
3
7
|
import { settlementIdentifier, settlementScope } from "@alma-harness/core";
|
|
@@ -38,6 +42,8 @@ function createConversationAgent(value) {
|
|
|
38
42
|
}
|
|
39
43
|
if (limits.caps === void 0 || limits.caps === null) throw new TypeError("Explicit conversation budget required");
|
|
40
44
|
if (typeof options.instructions !== "string" || typeof options.onBackgroundError !== "function") throw new TypeError("Conversation instructions and error observer are required");
|
|
45
|
+
const audit = options.audit ?? options.runtime.stores.audit;
|
|
46
|
+
if (!isAuditLog(audit)) throw new TypeError("Conversation agent requires an audit log");
|
|
41
47
|
const version = `${FORMAT}:${digest([FORMAT, id, revision])}`;
|
|
42
48
|
const runTimeoutMs = limits.runTimeoutMs ?? 12e4;
|
|
43
49
|
const config = {
|
|
@@ -61,7 +67,7 @@ function createConversationAgent(value) {
|
|
|
61
67
|
step: {
|
|
62
68
|
...options.runtime.stores.step,
|
|
63
69
|
clients: { [ref.provider]: model.client },
|
|
64
|
-
audit
|
|
70
|
+
audit,
|
|
65
71
|
runTimeoutMs,
|
|
66
72
|
maxRequestChars: limits.maxRequestChars ?? 1e6,
|
|
67
73
|
maxOutputChars: limits.maxOutputChars ?? 1e5,
|
package/dist/agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/agent.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { settlementIdentifier, settlementScope, type ModelPrice, type ModelRef, type Scope, type SingleDispatchModelClient } from \"@alma-harness/core\";\nimport { conversationRootKey, createConversationDescriptor, createConversationRunner, type ConversationConfig, type ConversationView, type SimpleConversationInput } from \"@alma-harness/conversation\";\nimport type { Runtime } from \"./index\";\n\n// DECISION: default changes require a format bump, preserving admitted bindings.\nconst FORMAT = \"conversation-agent-v1\";\nconst digest = (parts: unknown[]) => createHash(\"sha256\").update(JSON.stringify(parts)).digest(\"hex\");\nexport interface ConversationAgentOptions {\n id: string;\n /** Immutable host configuration: includes prices, tools and trusted callbacks. */\n revision: string;\n runtime: Runtime;\n model: { ref: ModelRef; client: SingleDispatchModelClient; prices: ModelPrice[] };\n instructions: string;\n audit: ConversationConfig[\"step\"][\"audit\"];\n intent: SimpleConversationInput[\"intent\"];\n tools?: ConversationConfig[\"tools\"];\n hooks?: ConversationConfig[\"hooks\"];\n volatile?: ConversationConfig[\"volatile\"];\n limits: {\n caps: ConversationConfig[\"caps\"];\n retentionMs: number;\n runTimeoutMs?: number;\n maxCalls?: number;\n maxInputChars?: number;\n maxRequestChars?: number;\n maxOutputChars?: number;\n };\n onBackgroundError: ConversationConfig[\"step\"][\"onBackgroundError\"];\n}\nexport interface ConversationQuery { prompt: string; messageId: string; resume?: string; signal?: AbortSignal }\nexport interface ConversationQueryKey { sessionId: string; messageId: string }\nexport interface ConversationQueryResult extends ConversationQueryKey { rootKey: string; view: ConversationView }\nexport interface ConversationQueryObservation extends ConversationQueryKey { rootKey: string; view: ConversationView | null }\nexport interface UserConversationAgent {\n query(input: ConversationQuery): Promise<ConversationQueryResult>;\n read(key: ConversationQueryKey): Promise<ConversationQueryObservation>;\n}\nexport interface ConversationAgent { forUser(scope: Scope): UserConversationAgent }\nexport class ConversationResumeError extends Error {\n readonly code = \"conversation_resume_not_found\";\n constructor() { super(\"Conversation session not found in the bound scope\"); this.name = \"ConversationResumeError\"; }\n}\n\n/** Inspect data descriptors before reading values; a request cannot invoke getters. */\nfunction fields(value: unknown, allowed: readonly string[]): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new TypeError(\"Invalid conversation agent input\");\n return Object.fromEntries(Reflect.ownKeys(value).map(key => {\n const d = Object.getOwnPropertyDescriptor(value, key)!;\n if (typeof key !== \"string\" || !allowed.includes(key) || !d.enumerable || !(\"value\" in d)) throw new TypeError(\"Invalid conversation agent field\");\n return [key, d.value];\n }));\n}\nconst scopeCopy = (value: Scope): Scope => settlementScope(fields(value, [\"org\", \"uid\"]) as unknown as Scope);\n\n/** Deterministic even when the first response was lost. Never derives identity from text or revision. */\nexport function conversationAgentSessionId(agentId: string, scope: Scope, messageId: string): string {\n const s = scopeCopy(scope);\n return `agent-session-v1:${digest([\"agent-session-v1\", settlementIdentifier(agentId), s.org, s.uid, settlementIdentifier(messageId)])}`;\n}\n\n/** A text facade over the canonical engine. Owns no stores, pools, retries or session registry. */\nexport function createConversationAgent(value: ConversationAgentOptions): ConversationAgent {\n const options = fields(value, [\"id\", \"revision\", \"runtime\", \"model\", \"instructions\", \"audit\", \"intent\", \"tools\", \"hooks\", \"volatile\", \"limits\", \"onBackgroundError\"]) as unknown as ConversationAgentOptions;\n const id = settlementIdentifier(options.id), revision = settlementIdentifier(options.revision);\n const model = fields(options.model, [\"ref\", \"client\", \"prices\"]) as unknown as ConversationAgentOptions[\"model\"];\n const ref = structuredClone(model.ref), intent = structuredClone(options.intent);\n const limits = fields(options.limits, [\"caps\", \"retentionMs\", \"runTimeoutMs\", \"maxCalls\", \"maxInputChars\", \"maxRequestChars\", \"maxOutputChars\"]) as unknown as ConversationAgentOptions[\"limits\"];\n if (![\"anthropic\", \"openai\", \"openrouter\"].includes(ref.provider)) throw new TypeError(\"Invalid conversation model provider\");\n settlementIdentifier(ref.id);\n for (const key of [\"runTimeoutMs\", \"maxCalls\", \"maxInputChars\", \"maxRequestChars\", \"maxOutputChars\"] as const) {\n const n = limits[key];\n if (n !== undefined && (!Number.isSafeInteger(n) || n < 1)) throw new TypeError(\"Invalid conversation agent limit\");\n }\n if (limits.caps === undefined || limits.caps === null) throw new TypeError(\"Explicit conversation budget required\");\n if (typeof options.instructions !== \"string\" || typeof options.onBackgroundError !== \"function\") throw new TypeError(\"Conversation instructions and error observer are required\");\n const version = `${FORMAT}:${digest([FORMAT, id, revision])}`;\n const runTimeoutMs = limits.runTimeoutMs ?? 120000;\n const config: ConversationConfig = {\n ...options.runtime.stores,\n system: [{ text: options.instructions, volatility: \"stable\" }],\n policy: { resolve: () => ({ model: { ...ref }, rationale: \"configured conversation agent model\" }) },\n prices: structuredClone(model.prices), caps: structuredClone(limits.caps), consumers: [],\n policyVersion: version, priceVersion: version, configRevision: version, resultContractVersion: FORMAT,\n resultRetentionMs: limits.retentionMs, runTimeoutMs, maxCalls: limits.maxCalls ?? 24, maxInputChars: limits.maxInputChars ?? 64000,\n ...(options.tools !== undefined ? { tools: options.tools } : {}),\n ...(options.hooks !== undefined ? { hooks: options.hooks } : {}),\n ...(options.volatile !== undefined ? { volatile: options.volatile } : {}),\n step: { ...options.runtime.stores.step, clients: { [ref.provider]: model.client }, audit: options.audit, runTimeoutMs,\n maxRequestChars: limits.maxRequestChars ?? 1000000, maxOutputChars: limits.maxOutputChars ?? 100000,\n onBackgroundError: options.onBackgroundError },\n };\n // Reuse canonical validation, including intent, before any request can perform I/O.\n createConversationDescriptor(config, { scope: { org: \"validation\", uid: \"validation\" }, sessionId: \"validation\", idempotencyKey: \"validation\",\n input: { role: \"user\", blocks: [] }, intent }, new Date(Date.now() + runTimeoutMs).toISOString());\n const runner = createConversationRunner(config), admissions = options.runtime.stores.admissions;\n return { forUser(value) {\n const scope = scopeCopy(value);\n return {\n async query(value) {\n const q = fields(value, [\"prompt\", \"messageId\", \"resume\", \"signal\"]);\n const messageId = settlementIdentifier(q.messageId);\n const sessionId = q.resume === undefined ? conversationAgentSessionId(id, scope, messageId) : settlementIdentifier(q.resume);\n if (typeof q.prompt !== \"string\" || q.prompt.length > config.maxInputChars) throw new TypeError(\"Invalid conversation prompt\");\n const input: SimpleConversationInput = { scope: { ...scope }, sessionId, idempotencyKey: messageId,\n input: { role: \"user\", blocks: [{ type: \"text\", text: q.prompt }] }, intent: structuredClone(intent),\n ...(q.signal !== undefined ? { signal: q.signal as AbortSignal } : {}) };\n // Validate the canonical envelope before the resume lookup, not after I/O.\n const root = createConversationDescriptor(config, input, new Date(Date.now() + runTimeoutMs).toISOString());\n if (q.resume !== undefined && !(await admissions.list({ ...scope }, sessionId, { limit: 1 })).length) throw new ConversationResumeError();\n return { sessionId, messageId, rootKey: root.key, view: await runner.runTurn(input) };\n },\n async read(value) {\n const key = fields(value, [\"sessionId\", \"messageId\"]);\n const sessionId = settlementIdentifier(key.sessionId), messageId = settlementIdentifier(key.messageId);\n const rootKey = conversationRootKey(scope, sessionId, messageId);\n return { sessionId, messageId, rootKey, view: await runner.read({ ...scope }, rootKey) };\n },\n };\n } };\n}\n"],"mappings":"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","names":["value"]}
|
|
1
|
+
{"version":3,"sources":["../src/agent.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { settlementIdentifier, settlementScope, type ModelPrice, type ModelRef, type Scope, type SingleDispatchModelClient } from \"@alma-harness/core\";\nimport { conversationRootKey, createConversationDescriptor, createConversationRunner, type ConversationConfig, type ConversationView, type SimpleConversationInput } from \"@alma-harness/conversation\";\nimport type { Runtime } from \"./index\";\nimport { isAuditLog } from \"./policies\";\n\n// DECISION: default changes require a format bump, preserving admitted bindings.\nconst FORMAT = \"conversation-agent-v1\";\nconst digest = (parts: unknown[]) => createHash(\"sha256\").update(JSON.stringify(parts)).digest(\"hex\");\nexport interface ConversationAgentOptions {\n id: string;\n /** Immutable host configuration: includes prices, tools and trusted callbacks. */\n revision: string;\n runtime: Runtime;\n model: { ref: ModelRef; client: SingleDispatchModelClient; prices: ModelPrice[] };\n instructions: string;\n /** Omitted means `runtime.stores.audit`; never disables auditing (spec: runtime-deployment). */\n audit?: ConversationConfig[\"step\"][\"audit\"];\n intent: SimpleConversationInput[\"intent\"];\n tools?: ConversationConfig[\"tools\"];\n hooks?: ConversationConfig[\"hooks\"];\n volatile?: ConversationConfig[\"volatile\"];\n limits: {\n caps: ConversationConfig[\"caps\"];\n retentionMs: number;\n runTimeoutMs?: number;\n maxCalls?: number;\n maxInputChars?: number;\n maxRequestChars?: number;\n maxOutputChars?: number;\n };\n onBackgroundError: ConversationConfig[\"step\"][\"onBackgroundError\"];\n}\nexport interface ConversationQuery { prompt: string; messageId: string; resume?: string; signal?: AbortSignal }\nexport interface ConversationQueryKey { sessionId: string; messageId: string }\nexport interface ConversationQueryResult extends ConversationQueryKey { rootKey: string; view: ConversationView }\nexport interface ConversationQueryObservation extends ConversationQueryKey { rootKey: string; view: ConversationView | null }\nexport interface UserConversationAgent {\n query(input: ConversationQuery): Promise<ConversationQueryResult>;\n read(key: ConversationQueryKey): Promise<ConversationQueryObservation>;\n}\nexport interface ConversationAgent { forUser(scope: Scope): UserConversationAgent }\nexport class ConversationResumeError extends Error {\n readonly code = \"conversation_resume_not_found\";\n constructor() { super(\"Conversation session not found in the bound scope\"); this.name = \"ConversationResumeError\"; }\n}\n\n/** Inspect data descriptors before reading values; a request cannot invoke getters. */\nfunction fields(value: unknown, allowed: readonly string[]): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new TypeError(\"Invalid conversation agent input\");\n return Object.fromEntries(Reflect.ownKeys(value).map(key => {\n const d = Object.getOwnPropertyDescriptor(value, key)!;\n if (typeof key !== \"string\" || !allowed.includes(key) || !d.enumerable || !(\"value\" in d)) throw new TypeError(\"Invalid conversation agent field\");\n return [key, d.value];\n }));\n}\nconst scopeCopy = (value: Scope): Scope => settlementScope(fields(value, [\"org\", \"uid\"]) as unknown as Scope);\n\n/** Deterministic even when the first response was lost. Never derives identity from text or revision. */\nexport function conversationAgentSessionId(agentId: string, scope: Scope, messageId: string): string {\n const s = scopeCopy(scope);\n return `agent-session-v1:${digest([\"agent-session-v1\", settlementIdentifier(agentId), s.org, s.uid, settlementIdentifier(messageId)])}`;\n}\n\n/** A text facade over the canonical engine. Owns no stores, pools, retries or session registry. */\nexport function createConversationAgent(value: ConversationAgentOptions): ConversationAgent {\n const options = fields(value, [\"id\", \"revision\", \"runtime\", \"model\", \"instructions\", \"audit\", \"intent\", \"tools\", \"hooks\", \"volatile\", \"limits\", \"onBackgroundError\"]) as unknown as ConversationAgentOptions;\n const id = settlementIdentifier(options.id), revision = settlementIdentifier(options.revision);\n const model = fields(options.model, [\"ref\", \"client\", \"prices\"]) as unknown as ConversationAgentOptions[\"model\"];\n const ref = structuredClone(model.ref), intent = structuredClone(options.intent);\n const limits = fields(options.limits, [\"caps\", \"retentionMs\", \"runTimeoutMs\", \"maxCalls\", \"maxInputChars\", \"maxRequestChars\", \"maxOutputChars\"]) as unknown as ConversationAgentOptions[\"limits\"];\n if (![\"anthropic\", \"openai\", \"openrouter\"].includes(ref.provider)) throw new TypeError(\"Invalid conversation model provider\");\n settlementIdentifier(ref.id);\n for (const key of [\"runTimeoutMs\", \"maxCalls\", \"maxInputChars\", \"maxRequestChars\", \"maxOutputChars\"] as const) {\n const n = limits[key];\n if (n !== undefined && (!Number.isSafeInteger(n) || n < 1)) throw new TypeError(\"Invalid conversation agent limit\");\n }\n if (limits.caps === undefined || limits.caps === null) throw new TypeError(\"Explicit conversation budget required\");\n if (typeof options.instructions !== \"string\" || typeof options.onBackgroundError !== \"function\") throw new TypeError(\"Conversation instructions and error observer are required\");\n const audit = options.audit ?? options.runtime.stores.audit;\n if (!isAuditLog(audit)) throw new TypeError(\"Conversation agent requires an audit log\");\n const version = `${FORMAT}:${digest([FORMAT, id, revision])}`;\n const runTimeoutMs = limits.runTimeoutMs ?? 120000;\n const config: ConversationConfig = {\n ...options.runtime.stores,\n system: [{ text: options.instructions, volatility: \"stable\" }],\n policy: { resolve: () => ({ model: { ...ref }, rationale: \"configured conversation agent model\" }) },\n prices: structuredClone(model.prices), caps: structuredClone(limits.caps), consumers: [],\n policyVersion: version, priceVersion: version, configRevision: version, resultContractVersion: FORMAT,\n resultRetentionMs: limits.retentionMs, runTimeoutMs, maxCalls: limits.maxCalls ?? 24, maxInputChars: limits.maxInputChars ?? 64000,\n ...(options.tools !== undefined ? { tools: options.tools } : {}),\n ...(options.hooks !== undefined ? { hooks: options.hooks } : {}),\n ...(options.volatile !== undefined ? { volatile: options.volatile } : {}),\n step: { ...options.runtime.stores.step, clients: { [ref.provider]: model.client }, audit, runTimeoutMs,\n maxRequestChars: limits.maxRequestChars ?? 1000000, maxOutputChars: limits.maxOutputChars ?? 100000,\n onBackgroundError: options.onBackgroundError },\n };\n // Reuse canonical validation, including intent, before any request can perform I/O.\n createConversationDescriptor(config, { scope: { org: \"validation\", uid: \"validation\" }, sessionId: \"validation\", idempotencyKey: \"validation\",\n input: { role: \"user\", blocks: [] }, intent }, new Date(Date.now() + runTimeoutMs).toISOString());\n const runner = createConversationRunner(config), admissions = options.runtime.stores.admissions;\n return { forUser(value) {\n const scope = scopeCopy(value);\n return {\n async query(value) {\n const q = fields(value, [\"prompt\", \"messageId\", \"resume\", \"signal\"]);\n const messageId = settlementIdentifier(q.messageId);\n const sessionId = q.resume === undefined ? conversationAgentSessionId(id, scope, messageId) : settlementIdentifier(q.resume);\n if (typeof q.prompt !== \"string\" || q.prompt.length > config.maxInputChars) throw new TypeError(\"Invalid conversation prompt\");\n const input: SimpleConversationInput = { scope: { ...scope }, sessionId, idempotencyKey: messageId,\n input: { role: \"user\", blocks: [{ type: \"text\", text: q.prompt }] }, intent: structuredClone(intent),\n ...(q.signal !== undefined ? { signal: q.signal as AbortSignal } : {}) };\n // Validate the canonical envelope before the resume lookup, not after I/O.\n const root = createConversationDescriptor(config, input, new Date(Date.now() + runTimeoutMs).toISOString());\n if (q.resume !== undefined && !(await admissions.list({ ...scope }, sessionId, { limit: 1 })).length) throw new ConversationResumeError();\n return { sessionId, messageId, rootKey: root.key, view: await runner.runTurn(input) };\n },\n async read(value) {\n const key = fields(value, [\"sessionId\", \"messageId\"]);\n const sessionId = settlementIdentifier(key.sessionId), messageId = settlementIdentifier(key.messageId);\n const rootKey = conversationRootKey(scope, sessionId, messageId);\n return { sessionId, messageId, rootKey, view: await runner.read({ ...scope }, rootKey) };\n },\n };\n } };\n}\n"],"mappings":"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","names":["value"]}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/maintenance-core.ts
|
|
2
|
+
import { createGovernedStepRunner, createSingleCallRunner } from "@alma-harness/single-call";
|
|
3
|
+
var BUDGET = 50;
|
|
4
|
+
var LEASE_MS = 1e4;
|
|
5
|
+
var usageBefore = (a, b) => {
|
|
6
|
+
const x = Date.parse(a.receivedAt), y = Date.parse(b.receivedAt);
|
|
7
|
+
return x < y || x === y && a.id <= b.id;
|
|
8
|
+
};
|
|
9
|
+
async function resumeLap(stored, start) {
|
|
10
|
+
if (stored?.bound && stored.budget !== null && stored.budget > 0) return { after: stored.after, bound: stored.bound, budget: stored.budget };
|
|
11
|
+
const s = await start();
|
|
12
|
+
return s ? { after: stored?.bound ? null : stored?.after ?? null, bound: s.bound, budget: s.budget } : null;
|
|
13
|
+
}
|
|
14
|
+
var refuse = () => {
|
|
15
|
+
throw new Error("Maintenance never dispatches");
|
|
16
|
+
};
|
|
17
|
+
function createMaintenance(options) {
|
|
18
|
+
const { stores, discovery } = options, step = stores.step;
|
|
19
|
+
const text = createSingleCallRunner({
|
|
20
|
+
...step,
|
|
21
|
+
audit: stores.audit,
|
|
22
|
+
clients: {},
|
|
23
|
+
policy: { resolve: refuse },
|
|
24
|
+
policyVersion: "alma-maintenance",
|
|
25
|
+
priceVersion: "alma-maintenance",
|
|
26
|
+
outputContractVersion: "alma-maintenance",
|
|
27
|
+
prices: [],
|
|
28
|
+
caps: {},
|
|
29
|
+
consumers: [],
|
|
30
|
+
system: [],
|
|
31
|
+
maxTokens: 1,
|
|
32
|
+
maxInputChars: 1,
|
|
33
|
+
maxOutputChars: 1e6,
|
|
34
|
+
deadlineMs: 1e3,
|
|
35
|
+
resultRetentionMs: 1e3,
|
|
36
|
+
onBackgroundError() {
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const governed = createGovernedStepRunner({
|
|
40
|
+
...step,
|
|
41
|
+
audit: stores.audit,
|
|
42
|
+
clients: {},
|
|
43
|
+
runTimeoutMs: 1e3,
|
|
44
|
+
maxRequestChars: 1,
|
|
45
|
+
maxOutputChars: 1e6,
|
|
46
|
+
onBackgroundError() {
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
async function work(scope, claim, signal, r) {
|
|
50
|
+
const live = () => !signal?.aborted && claim.alive();
|
|
51
|
+
let budget = BUDGET;
|
|
52
|
+
for (const store of [stores.admissions, step.trees, step.executions]) {
|
|
53
|
+
if (!live() || budget < 1) break;
|
|
54
|
+
try {
|
|
55
|
+
const n = (await store.reconcileExpired(scope, { limit: budget })).length;
|
|
56
|
+
budget -= n;
|
|
57
|
+
if (store === stores.admissions) r.expired.admissions += n;
|
|
58
|
+
else if (store === step.trees) r.expired.roots += n;
|
|
59
|
+
else r.expired.executions += n;
|
|
60
|
+
} catch {
|
|
61
|
+
r.failed++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const cursors = await discovery.cursors(scope);
|
|
65
|
+
let executions = null;
|
|
66
|
+
try {
|
|
67
|
+
const l = await resumeLap(cursors.executions, () => discovery.executionLap(scope));
|
|
68
|
+
if (l) {
|
|
69
|
+
const want = Math.min(BUDGET, l.budget), items = await discovery.recoverable(scope, l.after, l.bound, want);
|
|
70
|
+
let attempted = 0;
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
if (!live()) break;
|
|
73
|
+
try {
|
|
74
|
+
const view = item.rootKey ? await governed.recover({ scope, rootKey: item.rootKey, operationKey: item.operationKey }, { leaseMs: LEASE_MS }) : await text.recover(scope, item.operationKey, { leaseMs: LEASE_MS });
|
|
75
|
+
if (view?.status === "completed") r.recovered++;
|
|
76
|
+
} catch {
|
|
77
|
+
r.failed++;
|
|
78
|
+
}
|
|
79
|
+
l.after = { createdAt: item.createdAt, operationKey: item.operationKey };
|
|
80
|
+
l.budget--;
|
|
81
|
+
attempted++;
|
|
82
|
+
}
|
|
83
|
+
executions = attempted < items.length ? l : items.length < want || l.budget < 1 ? null : l;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
r.failed++;
|
|
87
|
+
executions = cursors.executions;
|
|
88
|
+
}
|
|
89
|
+
let usage = null;
|
|
90
|
+
try {
|
|
91
|
+
const l = await resumeLap(cursors.usage, () => discovery.usageLap(scope));
|
|
92
|
+
if (l && live()) {
|
|
93
|
+
const want = Math.min(BUDGET, l.budget);
|
|
94
|
+
const page = await step.inbox.listPending(scope, { limit: want, ...l.after ? { after: l.after } : {} });
|
|
95
|
+
const rows = page.filter((row) => usageBefore({ receivedAt: row.receivedAt, id: row.input.id }, l.bound));
|
|
96
|
+
let attempted = 0;
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
if (!live()) break;
|
|
99
|
+
const query = { limit: 1, ...l.after ? { after: l.after } : {} };
|
|
100
|
+
try {
|
|
101
|
+
const root = (await discovery.members(scope, [row.input.execution.operationKey])).get(row.input.execution.operationKey);
|
|
102
|
+
const result = root ? await governed.reconcileUsage({ scope, rootKey: root }, query) : await text.reconcileUsage(scope, query, { skip: async (key) => (await discovery.members(scope, [key])).has(key) });
|
|
103
|
+
r.acknowledged += result.acknowledged;
|
|
104
|
+
} catch {
|
|
105
|
+
r.failed++;
|
|
106
|
+
}
|
|
107
|
+
l.after = { receivedAt: row.receivedAt, id: row.input.id };
|
|
108
|
+
l.budget--;
|
|
109
|
+
attempted++;
|
|
110
|
+
}
|
|
111
|
+
usage = attempted < rows.length ? l : rows.length < want || l.budget < 1 ? null : l;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
r.failed++;
|
|
115
|
+
usage = cursors.usage;
|
|
116
|
+
}
|
|
117
|
+
await discovery.saveCursors(scope, { executions, usage });
|
|
118
|
+
}
|
|
119
|
+
let queue = Promise.resolve();
|
|
120
|
+
return {
|
|
121
|
+
maintain(value) {
|
|
122
|
+
const run = queue.then(() => sweep(value), () => sweep(value));
|
|
123
|
+
queue = run.catch(() => void 0);
|
|
124
|
+
return run;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
async function sweep(value) {
|
|
128
|
+
if (!value || !Number.isSafeInteger(value.limit) || value.limit < 1 || value.limit > 1e3) throw new TypeError("Invalid maintenance limit");
|
|
129
|
+
const signal = value.signal;
|
|
130
|
+
await discovery.verify();
|
|
131
|
+
const r = { scopes: 0, skipped: 0, expired: { admissions: 0, roots: 0, executions: 0 }, recovered: 0, acknowledged: 0, failed: 0 };
|
|
132
|
+
for (const scope of await discovery.nextScopes(value.limit)) {
|
|
133
|
+
if (signal?.aborted) break;
|
|
134
|
+
const claim = await discovery.claim(scope);
|
|
135
|
+
if (!claim) {
|
|
136
|
+
r.skipped++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
r.scopes++;
|
|
140
|
+
try {
|
|
141
|
+
await work(scope, claim, signal, r);
|
|
142
|
+
} catch {
|
|
143
|
+
r.failed++;
|
|
144
|
+
} finally {
|
|
145
|
+
await claim.release();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return r;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/scope-rotation.ts
|
|
153
|
+
import pg from "pg";
|
|
154
|
+
var ROLES = ["alma_app", "alma_maintenance", "alma_projection", "alma_retention"];
|
|
155
|
+
function scopeRotation(pool, options) {
|
|
156
|
+
const { role, lockPrefix, workerURL } = options;
|
|
157
|
+
const lockKey = `hashtextextended(jsonb_build_array('${lockPrefix}',$1::text,$2::text)::text,0)`;
|
|
158
|
+
async function tx(fn) {
|
|
159
|
+
const c = await pool.connect();
|
|
160
|
+
try {
|
|
161
|
+
await c.query(`begin; set local role ${role}`);
|
|
162
|
+
const r = await fn(c);
|
|
163
|
+
await c.query("commit");
|
|
164
|
+
return r;
|
|
165
|
+
} catch (e) {
|
|
166
|
+
await c.query("rollback").catch(() => void 0);
|
|
167
|
+
throw e;
|
|
168
|
+
} finally {
|
|
169
|
+
c.release();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const memberships = async (c) => (await c.query(`select session_user as who,
|
|
173
|
+
${ROLES.map((r) => `pg_has_role(session_user,'${r}','MEMBER') as ${r}`).join(", ")}`)).rows[0];
|
|
174
|
+
async function hold(c, scope, onLost) {
|
|
175
|
+
let alive = true;
|
|
176
|
+
const listeners = /* @__PURE__ */ new Set([onLost]);
|
|
177
|
+
const lost = () => {
|
|
178
|
+
if (!alive) return;
|
|
179
|
+
alive = false;
|
|
180
|
+
for (const l of listeners) {
|
|
181
|
+
try {
|
|
182
|
+
l();
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
c.on("error", lost);
|
|
188
|
+
c.on("end", lost);
|
|
189
|
+
return {
|
|
190
|
+
alive: () => alive,
|
|
191
|
+
onLost(fn) {
|
|
192
|
+
listeners.add(fn);
|
|
193
|
+
if (!alive) fn();
|
|
194
|
+
},
|
|
195
|
+
async release() {
|
|
196
|
+
let unlocked = false;
|
|
197
|
+
try {
|
|
198
|
+
unlocked = (await c.query(`select pg_advisory_unlock(${lockKey}) ok`, [scope.org, scope.uid])).rows[0].ok === true;
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
c.off("error", lost);
|
|
202
|
+
c.off("end", lost);
|
|
203
|
+
c.release(!unlocked);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
tx,
|
|
209
|
+
/** Membership, not inheritance: an indirect or non-inherited grant is still authority a SET ROLE can take. */
|
|
210
|
+
async verify() {
|
|
211
|
+
const c = await pool.connect();
|
|
212
|
+
let d;
|
|
213
|
+
try {
|
|
214
|
+
d = await memberships(c);
|
|
215
|
+
} finally {
|
|
216
|
+
c.release();
|
|
217
|
+
}
|
|
218
|
+
const w = new pg.Client({ connectionString: workerURL });
|
|
219
|
+
await w.connect();
|
|
220
|
+
let k;
|
|
221
|
+
try {
|
|
222
|
+
k = await memberships(w);
|
|
223
|
+
} finally {
|
|
224
|
+
await w.end();
|
|
225
|
+
}
|
|
226
|
+
const others = (m, own) => ROLES.some((r) => r !== own && m[r]);
|
|
227
|
+
if (!d[role] || others(d, role) || !k.alma_app || others(k, "alma_app") || d.who === k.who)
|
|
228
|
+
throw new TypeError(`Requires a discovery login in ${role} only and a separate worker login in alma_app only`);
|
|
229
|
+
},
|
|
230
|
+
async claim(scope) {
|
|
231
|
+
const c = await pool.connect();
|
|
232
|
+
try {
|
|
233
|
+
if (!(await c.query(`select pg_try_advisory_lock(${lockKey}) got`, [scope.org, scope.uid])).rows[0].got) {
|
|
234
|
+
c.release();
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
c.release(true);
|
|
239
|
+
throw e;
|
|
240
|
+
}
|
|
241
|
+
return hold(c, scope, () => void 0);
|
|
242
|
+
},
|
|
243
|
+
/** Waits for the scope's claim (a working drain holds it) under lock_timeout, then runs `fn` while holding it. */
|
|
244
|
+
async quiesce(scope, fn, timeoutMs) {
|
|
245
|
+
const c = await pool.connect();
|
|
246
|
+
try {
|
|
247
|
+
await c.query("select set_config('lock_timeout', $1, false)", [String(timeoutMs)]);
|
|
248
|
+
await c.query(`select pg_advisory_lock(${lockKey})`, [scope.org, scope.uid]);
|
|
249
|
+
await c.query("select set_config('lock_timeout', '0', false)");
|
|
250
|
+
} catch (e) {
|
|
251
|
+
c.release(true);
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
let lost = false;
|
|
255
|
+
const claim = await hold(c, scope, () => {
|
|
256
|
+
lost = true;
|
|
257
|
+
});
|
|
258
|
+
try {
|
|
259
|
+
const result = await fn();
|
|
260
|
+
if (lost) throw new Error("Quiesce claim lost while the callback ran: retry under a new quiesce");
|
|
261
|
+
return result;
|
|
262
|
+
} finally {
|
|
263
|
+
await claim.release();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export {
|
|
270
|
+
resumeLap,
|
|
271
|
+
createMaintenance,
|
|
272
|
+
scopeRotation
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=chunk-6MFSZ4VS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/maintenance-core.ts","../src/scope-rotation.ts"],"sourcesContent":["import type { Scope, UsageInboxQuery } from \"@alma-harness/core\";\nimport { createGovernedStepRunner, createSingleCallRunner } from \"@alma-harness/single-call\";\nimport type { RuntimeStores } from \"./index\";\n\nexport type ExecutionCursor = { createdAt: string; operationKey: string };\nexport type UsageCursor = NonNullable<UsageInboxQuery[\"after\"]>;\nexport interface RecoverableExecution { operationKey: string; createdAt: string; rootKey: string | null }\nexport interface ScopeClaim { alive(): boolean; release(): Promise<void>; onLost?(fn: () => void): void }\n/** A finite lap: items after `after` up to `bound`, at most `budget` more attempts (spec: receipt-projections). A null bound is a cursor written before laps existed. */\nexport interface Lap<C> { after: C | null; bound: C | null; budget: number | null }\nexport interface MaintenanceCursors { executions: Lap<ExecutionCursor> | null; usage: Lap<UsageCursor> | null }\n/** Cross-scope metadata only: identifiers, states, times and the maintenance cursors (spec: runtime-maintenance). */\nexport interface MaintenanceDiscovery {\n /** Throws unless the logins keep discovery and scoped work apart. */\n verify(): Promise<void>;\n nextScopes(limit: number): Promise<Scope[]>;\n claim(scope: Scope): Promise<ScopeClaim | null>;\n recoverable(scope: Scope, after: ExecutionCursor | null, bound: ExecutionCursor, limit: number): Promise<RecoverableExecution[]>;\n /** The greatest eligible execution and how many are eligible up to it; null when none is. */\n executionLap(scope: Scope): Promise<{ bound: ExecutionCursor; budget: number } | null>;\n usageLap(scope: Scope): Promise<{ bound: UsageCursor; budget: number } | null>;\n /** Root keys of the given operation keys that are members of a root. */\n members(scope: Scope, operationKeys: string[]): Promise<Map<string, string>>;\n cursors(scope: Scope): Promise<MaintenanceCursors>;\n saveCursors(scope: Scope, cursors: MaintenanceCursors): Promise<void>;\n}\nexport interface MaintenanceReport {\n scopes: number; skipped: number; expired: { admissions: number; roots: number; executions: number };\n recovered: number; acknowledged: number; failed: number;\n}\nconst BUDGET = 50, LEASE_MS = 10_000;\nconst usageBefore = (a: UsageCursor, b: UsageCursor) => { const x = Date.parse(a.receivedAt), y = Date.parse(b.receivedAt); return x < y || (x === y && a.id <= b.id); };\n/** Resumes a lap, or starts one with a bound and budget captured now; an old cursor keeps its position. */\nexport async function resumeLap<C>(stored: Lap<C> | null, start: () => Promise<{ bound: C; budget: number } | null>): Promise<{ after: C | null; bound: C; budget: number } | null> {\n if (stored?.bound && stored.budget !== null && stored.budget > 0) return { after: stored.after, bound: stored.bound, budget: stored.budget };\n const s = await start();\n return s ? { after: stored?.bound ? null : stored?.after ?? null, bound: s.bound, budget: s.budget } : null;\n}\nconst refuse = (): never => { throw new Error(\"Maintenance never dispatches\"); };\n\n/** Store work per scope; no client, no release, no content in the report. */\nexport function createMaintenance(options: { stores: RuntimeStores; discovery: MaintenanceDiscovery }) {\n const { stores, discovery } = options, step = stores.step;\n // Recovery reads the journal, retained result and receipts only: an empty client registry makes dispatch impossible.\n const text = createSingleCallRunner({ ...step, audit: stores.audit, clients: {}, policy: { resolve: refuse }, policyVersion: \"alma-maintenance\",\n priceVersion: \"alma-maintenance\", outputContractVersion: \"alma-maintenance\", prices: [], caps: {}, consumers: [], system: [],\n maxTokens: 1, maxInputChars: 1, maxOutputChars: 1_000_000, deadlineMs: 1000, resultRetentionMs: 1000, onBackgroundError() {} });\n const governed = createGovernedStepRunner({ ...step, audit: stores.audit, clients: {}, runTimeoutMs: 1000, maxRequestChars: 1,\n maxOutputChars: 1_000_000, onBackgroundError() {} });\n\n async function work(scope: Scope, claim: ScopeClaim, signal: AbortSignal | undefined, r: MaintenanceReport) {\n const live = () => !signal?.aborted && claim.alive();\n let budget = BUDGET;\n // Each category fails on its own: a broken store in one never starves the others.\n for (const store of [stores.admissions, step.trees, step.executions] as const) {\n if (!live() || budget < 1) break;\n try {\n const n = (await store.reconcileExpired(scope, { limit: budget })).length; budget -= n;\n if (store === stores.admissions) r.expired.admissions += n; else if (store === step.trees) r.expired.roots += n; else r.expired.executions += n;\n } catch { r.failed++; }\n }\n const cursors = await discovery.cursors(scope);\n let executions: Lap<ExecutionCursor> | null = null;\n try {\n const l = await resumeLap(cursors.executions, () => discovery.executionLap(scope));\n if (l) {\n const want = Math.min(BUDGET, l.budget), items = await discovery.recoverable(scope, l.after, l.bound, want);\n let attempted = 0;\n for (const item of items) {\n if (!live()) break;\n try {\n const view = item.rootKey ? await governed.recover({ scope, rootKey: item.rootKey, operationKey: item.operationKey }, { leaseMs: LEASE_MS })\n : await text.recover(scope, item.operationKey, { leaseMs: LEASE_MS });\n if (view?.status === \"completed\") r.recovered++;\n } catch { r.failed++; }\n l.after = { createdAt: item.createdAt, operationKey: item.operationKey }; l.budget--; attempted++;\n }\n // An interrupted page resumes; otherwise the lap ends when its range ran out or its budget is spent.\n executions = attempted < items.length ? l : items.length < want || l.budget < 1 ? null : l;\n }\n } catch { r.failed++; executions = cursors.executions; }\n let usage: Lap<UsageCursor> | null = null;\n try {\n const l = await resumeLap(cursors.usage, () => discovery.usageLap(scope));\n if (l && live()) {\n const want = Math.min(BUDGET, l.budget);\n const page = await step.inbox.listPending(scope, { limit: want, ...(l.after ? { after: l.after } : {}) });\n const rows = page.filter(row => usageBefore({ receivedAt: row.receivedAt, id: row.input.id }, l.bound));\n // One official call per row, aimed at it by its predecessor in this page. If another acknowledger\n // removed it, the call scans the next existing row instead, and that path's own check (membership,\n // via `skip` on the text path) still decides. No target is passed over by a sliding window.\n let attempted = 0;\n for (const row of rows) {\n if (!live()) break;\n const query = { limit: 1, ...(l.after ? { after: l.after } : {}) };\n try {\n const root = (await discovery.members(scope, [row.input.execution.operationKey])).get(row.input.execution.operationKey);\n const result = root ? await governed.reconcileUsage({ scope, rootKey: root }, query)\n : await text.reconcileUsage(scope, query, { skip: async key => (await discovery.members(scope, [key])).has(key) });\n r.acknowledged += result.acknowledged;\n } catch { r.failed++; }\n l.after = { receivedAt: row.receivedAt, id: row.input.id }; l.budget--; attempted++;\n }\n usage = attempted < rows.length ? l : rows.length < want || l.budget < 1 ? null : l;\n }\n } catch { r.failed++; usage = cursors.usage; }\n await discovery.saveCursors(scope, { executions, usage });\n }\n\n // One sweep at a time per instance: each holds a claim connection plus one query connection.\n let queue: Promise<unknown> = Promise.resolve();\n return {\n maintain(value: { limit: number; signal?: AbortSignal }): Promise<MaintenanceReport> {\n const run = queue.then(() => sweep(value), () => sweep(value)); queue = run.catch(() => undefined); return run;\n },\n };\n async function sweep(value: { limit: number; signal?: AbortSignal }): Promise<MaintenanceReport> {\n if (!value || !Number.isSafeInteger(value.limit) || value.limit < 1 || value.limit > 1000) throw new TypeError(\"Invalid maintenance limit\");\n const signal = value.signal;\n await discovery.verify();\n const r: MaintenanceReport = { scopes: 0, skipped: 0, expired: { admissions: 0, roots: 0, executions: 0 }, recovered: 0, acknowledged: 0, failed: 0 };\n for (const scope of await discovery.nextScopes(value.limit)) {\n if (signal?.aborted) break;\n const claim = await discovery.claim(scope);\n if (!claim) { r.skipped++; continue; }\n r.scopes++;\n try { await work(scope, claim, signal, r); } catch { r.failed++; } finally { await claim.release(); }\n }\n return r;\n }\n}\n","import pg from \"pg\";\nimport type { Scope } from \"@alma-harness/core\";\nimport type { ScopeClaim } from \"./maintenance-core\";\n\nconst ROLES = [\"alma_app\", \"alma_maintenance\", \"alma_projection\", \"alma_retention\"] as const;\ntype Role = typeof ROLES[number];\n/** Cross-scope discovery on its own login, shared by maintenance and projections (specs: runtime-maintenance, receipt-projections). */\nexport function scopeRotation(pool: pg.Pool, options: { role: Role; lockPrefix: string; workerURL: string }) {\n const { role, lockPrefix, workerURL } = options;\n const lockKey = `hashtextextended(jsonb_build_array('${lockPrefix}',$1::text,$2::text)::text,0)`;\n async function tx<T>(fn: (c: pg.PoolClient) => Promise<T>): Promise<T> {\n const c = await pool.connect();\n try { await c.query(`begin; set local role ${role}`); const r = await fn(c); await c.query(\"commit\"); return r; }\n catch (e) { await c.query(\"rollback\").catch(() => undefined); throw e; }\n finally { c.release(); }\n }\n const memberships = async (c: pg.ClientBase): Promise<{ who: string } & Record<Role, boolean>> => (await c.query(`select session_user as who,\n ${ROLES.map(r => `pg_has_role(session_user,'${r}','MEMBER') as ${r}`).join(\", \")}`)).rows[0];\n /** One dedicated connection per claim, released only after a confirmed unlock; otherwise destroyed. */\n async function hold(c: pg.PoolClient, scope: Scope, onLost: () => void): Promise<ScopeClaim> {\n let alive = true; const listeners = new Set<() => void>([onLost]);\n const lost = () => { if (!alive) return; alive = false; for (const l of listeners) { try { l(); } catch { /* A listener cannot stop the release. */ } } };\n c.on(\"error\", lost); c.on(\"end\", lost);\n return {\n alive: () => alive,\n onLost(fn: () => void) { listeners.add(fn); if (!alive) fn(); },\n async release() {\n let unlocked = false;\n try { unlocked = (await c.query<{ ok: boolean }>(`select pg_advisory_unlock(${lockKey}) ok`, [scope.org, scope.uid])).rows[0]!.ok === true; } catch { /* Destroyed below. */ }\n c.off(\"error\", lost); c.off(\"end\", lost);\n // An unconfirmed unlock must never ride a pooled connection into its next checkout.\n c.release(!unlocked);\n },\n };\n }\n return {\n tx,\n /** Membership, not inheritance: an indirect or non-inherited grant is still authority a SET ROLE can take. */\n async verify() {\n const c = await pool.connect();\n let d; try { d = await memberships(c); } finally { c.release(); }\n const w = new pg.Client({ connectionString: workerURL }); await w.connect();\n let k; try { k = await memberships(w); } finally { await w.end(); }\n const others = (m: Record<Role, boolean>, own: Role) => ROLES.some(r => r !== own && m[r]);\n if (!d[role] || others(d, role) || !k.alma_app || others(k, \"alma_app\") || d.who === k.who)\n throw new TypeError(`Requires a discovery login in ${role} only and a separate worker login in alma_app only`);\n },\n async claim(scope: Scope): Promise<ScopeClaim | null> {\n const c = await pool.connect();\n try {\n if (!(await c.query<{ got: boolean }>(`select pg_try_advisory_lock(${lockKey}) got`, [scope.org, scope.uid])).rows[0]!.got) { c.release(); return null; }\n } catch (e) { c.release(true); throw e; }\n return hold(c, scope, () => undefined);\n },\n /** Waits for the scope's claim (a working drain holds it) under lock_timeout, then runs `fn` while holding it. */\n async quiesce<T>(scope: Scope, fn: () => Promise<T>, timeoutMs: number): Promise<T> {\n const c = await pool.connect();\n try {\n await c.query(\"select set_config('lock_timeout', $1, false)\", [String(timeoutMs)]);\n await c.query(`select pg_advisory_lock(${lockKey})`, [scope.org, scope.uid]);\n await c.query(\"select set_config('lock_timeout', '0', false)\");\n } catch (e) { c.release(true); throw e; }\n let lost = false;\n const claim = await hold(c, scope, () => { lost = true; });\n try {\n const result = await fn();\n if (lost) throw new Error(\"Quiesce claim lost while the callback ran: retry under a new quiesce\");\n return result;\n } finally { await claim.release(); }\n },\n };\n}\n"],"mappings":"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","names":[]}
|