@volter/world-core 3.0.166 → 3.0.167

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/src/changeset.d.ts +9 -0
  2. package/dist/src/changeset.js +5 -2
  3. package/dist/src/completeness.d.ts +36 -0
  4. package/dist/src/completeness.js +46 -0
  5. package/dist/src/derived-core.d.ts +27 -0
  6. package/dist/src/derived-core.js +31 -2
  7. package/dist/src/events.d.ts +6 -1
  8. package/dist/src/events.js +58 -13
  9. package/dist/src/index.d.ts +4 -2
  10. package/dist/src/index.js +3 -2
  11. package/dist/src/openai-wire.js +0 -1
  12. package/dist/src/pack-fetch.d.ts +1 -3
  13. package/dist/src/pack-fetch.js +103 -43
  14. package/dist/src/runtime.d.ts +5 -2
  15. package/dist/src/runtime.js +3 -1
  16. package/dist/src/scenario-consume.d.ts +43 -0
  17. package/dist/src/scenario-consume.js +111 -0
  18. package/dist/src/scenario-serve.d.ts +16 -0
  19. package/dist/src/scenario-serve.js +47 -0
  20. package/dist/src/scenario-timeline.d.ts +131 -0
  21. package/dist/src/scenario-timeline.js +242 -0
  22. package/dist/src/scenario.d.ts +15 -93
  23. package/dist/src/scenario.js +34 -323
  24. package/dist/src/sockets.d.ts +8 -4
  25. package/dist/src/sockets.js +1 -1
  26. package/dist/src/world-clock.d.ts +3 -0
  27. package/dist/src/world-clock.js +14 -9
  28. package/package.json +1 -1
  29. package/src/changeset.ts +13 -3
  30. package/src/completeness.ts +57 -0
  31. package/src/derived-core.ts +41 -2
  32. package/src/events.ts +58 -11
  33. package/src/index.ts +5 -3
  34. package/src/openai-wire.ts +0 -1
  35. package/src/pack-fetch.ts +81 -39
  36. package/src/runtime.ts +5 -2
  37. package/src/scenario-consume.ts +123 -0
  38. package/src/scenario-serve.ts +47 -0
  39. package/src/scenario-timeline.ts +269 -0
  40. package/src/scenario.ts +43 -341
  41. package/src/sockets.ts +5 -5
  42. package/src/world-clock.ts +16 -9
@@ -21,6 +21,9 @@ export type LedgerPosition = {
21
21
  stateService: string;
22
22
  count: number;
23
23
  lastActionId: string | null;
24
+ /** the twin's history view and its position in it: what a branch at this mark starts from (ADR 0036 §5) */
25
+ view?: string;
26
+ position?: number;
24
27
  };
25
28
  export declare const MARKER_KIND = "volter.world.marker.v1";
26
29
  export declare const CHANGESET_KIND = "volter.world.changeset.v1";
@@ -33,6 +36,10 @@ export type WorldMarker = {
33
36
  /** Every ledger that existed at capture time. A ledger absent here is treated as position 0
34
37
  * (a twin that recorded its first action AFTER the mark contributes its whole ledger). */
35
38
  ledgers: LedgerPosition[];
39
+ /** the World clock's instant at the mark, or null when it ran on the machine's time (a run's position, ADR 0036 §5) */
40
+ clock?: string | null;
41
+ /** whether the machine's fault door took the app's disk with the mark */
42
+ disk?: boolean;
36
43
  };
37
44
  /** The id reserved for the synthetic "everything this world has ever recorded" base. */
38
45
  export declare const WORLD_BOOT_MARKER_ID = "world-boot";
@@ -176,6 +183,8 @@ export declare function captureMarker(opts: {
176
183
  world: string;
177
184
  ledgers: LedgerRef[];
178
185
  createdAt?: string;
186
+ clock?: string | null;
187
+ disk?: boolean;
179
188
  }): WorldMarker;
180
189
  /** A marker taken in world A says nothing about world B's ledgers — comparing them would
181
190
  * produce a confident, wrong delta. Refuse. */
@@ -1,7 +1,7 @@
1
1
  import { parseExactJson } from "./exact-json.js";
2
2
  import { resolveReferences } from "./references.js";
3
3
  import { withHistoryLocks } from "./ancestry.js";
4
- import { captureParentHistory, historyChanges, historyEntries, historyPrefix, readHistoryView } from "./history.js";
4
+ import { captureHistory, captureParentHistory, historyChanges, historyEntries, historyPrefix, readHistoryView } from "./history.js";
5
5
  import { worldPaths } from "./storage.js";
6
6
  // The changeset primitive — "commit" for operational reality (docs/concepts/the-model.md, v0).
7
7
  //
@@ -84,14 +84,17 @@ export function worldBootMarker(world, createdAt) {
84
84
  export function captureMarker(opts) {
85
85
  const ledgers = opts.ledgers.map((ledger) => {
86
86
  const rows = listActions(ledger.stateService, ledger.controlRoot);
87
+ const { view, position } = captureHistory(ledger.stateService, ledger.controlRoot);
87
88
  return {
88
89
  service: ledger.service,
89
90
  stateService: ledger.stateService,
90
91
  count: rows.length,
91
92
  lastActionId: rows.length ? rows[rows.length - 1].id : null,
93
+ view,
94
+ position,
92
95
  };
93
96
  });
94
- return { kind: MARKER_KIND, id: opts.id, world: opts.world, createdAt: opts.createdAt ?? new Date().toISOString(), ledgers };
97
+ return { kind: MARKER_KIND, id: opts.id, world: opts.world, createdAt: opts.createdAt ?? new Date().toISOString(), ledgers, ...(opts.clock !== undefined ? { clock: opts.clock } : {}), ...(opts.disk ? { disk: true } : {}) };
95
98
  }
96
99
  function positionKey(service, stateService) {
97
100
  return `${service}\u0000${stateService}`;
@@ -0,0 +1,36 @@
1
+ /** What a relay or a twin notes; the World's time is added as it is appended. */
2
+ type CompletenessNote = {
3
+ kind: 'egress';
4
+ destination: string;
5
+ outcome: 'refused' | 'passed';
6
+ rule?: string;
7
+ } | {
8
+ kind: 'unimplemented';
9
+ vendor: string;
10
+ method: string;
11
+ path: string;
12
+ operation?: string;
13
+ } | {
14
+ kind: 'machine';
15
+ layer: string;
16
+ fault: string;
17
+ target?: string;
18
+ };
19
+ /** Append one record to the World's completeness file named in `env`; a process outside a World notes nothing. */
20
+ export declare function noteCompleteness(env: Record<string, string | undefined>, record: CompletenessNote): void;
21
+ export type Completeness = {
22
+ /** zero when the World was the whole internet the run reached */
23
+ total: number;
24
+ egress: Record<string, {
25
+ refused: number;
26
+ passed: number;
27
+ }>;
28
+ unimplemented: Record<string, Record<string, number>>;
29
+ /** machine faults the run placed and no fault door carried, by layer and kind (ADR 0036 §4) */
30
+ machine: Record<string, number>;
31
+ };
32
+ /** How many records the completeness file holds: where a run starts counting. */
33
+ export declare function completenessMark(file: string): number;
34
+ /** What reached past the World after the file's `mark`, counted per destination and per vendor's operation. */
35
+ export declare function completenessSince(file: string, mark: number): Completeness;
36
+ export {};
@@ -0,0 +1,46 @@
1
+ // A run's completeness (ADR 0036 §9; architecture, "Scenarios"): the World is the application's internet closure, so
2
+ // what reached past it is what the World is missing. The relay notes every destination no twin serves (refused, or
3
+ // passed to the real internet where the World lets it), a twin every request to an operation it does not implement
4
+ // (answered as the vendor answers an unknown request), and a run every machine fault no fault door carried. All are
5
+ // appended, at World time, to the instance's completeness file (TWIN_WORLD_COMPLETENESS_FILE), and a run counts them.
6
+ import { getActiveWorldStore } from "./world-store.js";
7
+ import clockForm from '../world-clock.cjs';
8
+ const WORLD_COMPLETENESS_ENV = 'TWIN_WORLD_COMPLETENESS_FILE';
9
+ /** The World's time in `env` (its clock file's, else the machine's). */
10
+ function worldTimeIn(env) {
11
+ const file = env.TWIN_WORLD_CLOCK_FILE;
12
+ const held = file ? getActiveWorldStore().read(file) : null;
13
+ return new Date(held ? clockForm.clockNowMs(clockForm.parseClock(held), Date.now()) : Date.now()).toISOString();
14
+ }
15
+ /** Append one record to the World's completeness file named in `env`; a process outside a World notes nothing. */
16
+ export function noteCompleteness(env, record) {
17
+ const file = env[WORLD_COMPLETENESS_ENV];
18
+ if (!file)
19
+ return;
20
+ try {
21
+ getActiveWorldStore().append(file, `${JSON.stringify({ at: worldTimeIn(env), ...record })}\n`);
22
+ }
23
+ catch { /* the answer still goes out */ }
24
+ }
25
+ /** How many records the completeness file holds: where a run starts counting. */
26
+ export function completenessMark(file) {
27
+ return (getActiveWorldStore().read(file) ?? '').split('\n').filter((line) => line.trim()).length;
28
+ }
29
+ /** What reached past the World after the file's `mark`, counted per destination and per vendor's operation. */
30
+ export function completenessSince(file, mark) {
31
+ const out = { total: 0, egress: {}, unimplemented: {}, machine: {} };
32
+ for (const line of (getActiveWorldStore().read(file) ?? '').split('\n').filter((l) => l.trim()).slice(mark)) {
33
+ const r = JSON.parse(line);
34
+ out.total += 1;
35
+ if (r.kind === 'egress')
36
+ (out.egress[r.destination] ??= { refused: 0, passed: 0 })[r.outcome] += 1;
37
+ else if (r.kind === 'machine')
38
+ out.machine[`${r.layer} ${r.fault}`] = (out.machine[`${r.layer} ${r.fault}`] ?? 0) + 1;
39
+ else {
40
+ const ops = out.unimplemented[r.vendor] ??= {};
41
+ const key = r.operation ?? `${r.method} ${r.path}`;
42
+ ops[key] = (ops[key] ?? 0) + 1;
43
+ }
44
+ }
45
+ return out;
46
+ }
@@ -17,6 +17,7 @@ import { type TwinAction } from './actions.js';
17
17
  import type { DerivedCall, DerivedCoreOutcome, DerivedHandler, DerivedOperation } from './derived.js';
18
18
  import { type PackDescriptor } from './packRegistry.js';
19
19
  import type { ScenarioDecision, ScenarioHandler } from './scenario.js';
20
+ import { type ScenarioLedger } from './scenario-consume.js';
20
21
  /** How a stored field gets its value when the server assigns it. `now` is the world clock in the
21
22
  * manifest's time format; `id` the subject's id; a `value` is stored as given. */
22
23
  /** A field the server assigns: the World clock's now, the subject's id, a fixed value, or a template over the stored
@@ -1018,6 +1019,21 @@ export type SemanticsContext = {
1018
1019
  /** What the World's scenario decided for this turn (a model vendor's answer: the handler matched, or the miss), on an
1019
1020
  * operation the pack's `semantics/scenario.ts` names; undefined elsewhere, or when the World carries no scenario. */
1020
1021
  scenario: ScenarioDecision | undefined;
1022
+ /** The vendor's judgement at one of its decision points (a fraud score, an identity check, deliverability), for the
1023
+ * subject it is keyed to: what the World's scenario decided, else `byDefault`, the vendor's normal outcome; recorded in
1024
+ * the twin's log either way (architecture, "Scenarios"). The handler follows it with what the vendor does next. */
1025
+ decide<T>(point: string, subject: string, byDefault: T): Promise<T>;
1026
+ /** A fault the World's scenario placed at a point of the pack's own (one item of a batch) for `subject`, recorded as
1027
+ * landed; undefined when none is placed. */
1028
+ fault(point: string, subject: string): Promise<{
1029
+ kind: string;
1030
+ status?: number;
1031
+ message?: string;
1032
+ errorType?: string;
1033
+ } | undefined>;
1034
+ /** A World fact (an exchange rate, a holiday, a place) the twin answers with in its own shape, recorded as answered;
1035
+ * undefined when the World holds none. */
1036
+ fact(kind: string, key: string): Promise<unknown>;
1021
1037
  /** A message the vendor sends the application's server and decides by its answer, waited on for at most `within`
1022
1038
  * milliseconds (a real-time authorization request): its status and body, or why there was none. */
1023
1039
  ask(url: string, init: RequestInit, within: number): Promise<ApplicationAnswer>;
@@ -1295,6 +1311,17 @@ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically
1295
1311
  export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'tenant' | 'deploymentObject' | 'accountKey' | 'at' | 'over'>;
1296
1312
  /** A derived pack's handler: one operation, by its operationId, over the contract's context. */
1297
1313
  export type Handler = (ctx: HandlerContext) => Promise<Response>;
1314
+ /** What a context consumes the World's scenario through (scenario-consume.ts): its twin's bookkeeping, at its time. */
1315
+ export declare function scenarioLedgerOf(ctx: object): ScenarioLedger;
1316
+ /** `GET /twin/scenario`: what the World's scenario placed on this vendor, and what the twin consumed of it. */
1317
+ export declare function scenarioRecords(m: DerivedManifest, root?: string): {
1318
+ vendor: string;
1319
+ placed: unknown[];
1320
+ consumed: {
1321
+ decisions: unknown[];
1322
+ faults: unknown[];
1323
+ };
1324
+ };
1298
1325
  export declare function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScope): Promise<SemanticsContext>;
1299
1326
  /** A pack's semantics handlers as the dispatch's handlers. */
1300
1327
  export declare function bindSemantics(m: DerivedManifest, handlers: Record<string, Semantics>, scope?: CoreScope): Record<string, DerivedHandler>;
@@ -38,6 +38,8 @@ import { packReferences } from "./references.js";
38
38
  import { twinPublicBase, withRequestScopes } from "./twin-fetch.js";
39
39
  import { inVendorMove, isReadOnlyRequest, runAsVendorMove, writesRefused } from "./request-scope.js";
40
40
  import { getPack } from "./packRegistry.js";
41
+ import { decide, fact, faultAt, listLag, recordFault, serviceLedger } from "./scenario-consume.js";
42
+ import { durationMs, placedOn } from "./scenario-timeline.js";
41
43
  import { bodyTextOf, checkJsonBody, takeParsedBody } from "./derived.js";
42
44
  /** What a move to `to` stores in the field: a boolean field's value is a boolean, and a derived field's
43
45
  * value is written by the move's effects (a state name is not a timestamp), so it stores nothing itself. */
@@ -998,7 +1000,7 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
998
1000
  const endpoint = (kind) => ({ ...kind, storedAs: storedType(m, kind.storedAs), ...(kind.joins ? { joins: Object.fromEntries(Object.entries(kind.joins).map(([name, join]) => [name, { ...join, storedAs: storedType(m, join.storedAs) }])) } : {}) });
999
1001
  const events = { ...m.events, ...(m.events.store ? { store: storedType(m, m.events.store) } : {}), endpoints: 'storedAs' in m.events.endpoints ? endpoint(m.events.endpoints) : m.events.endpoints.map(endpoint) };
1000
1002
  const write = { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
1001
- await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined, scopeOfCall.get(call)?.application);
1003
+ await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined, scopeOfCall.get(call)?.application, m.vendor);
1002
1004
  }
1003
1005
  if (m.sockets?.length)
1004
1006
  socketWrite(m.service, root, { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
@@ -1439,7 +1441,15 @@ export async function serveCore(m, call, scope = {}) {
1439
1441
  case 'list': {
1440
1442
  if (!m.list)
1441
1443
  return { unmodeled: 'no manifest list configuration' };
1442
- const rows = stored(m, resource, root).filter(underParent).map((r) => view(m, resource, r));
1444
+ // a `lag` the World's scenario placed: a write younger than it is missing from lists, as an eventually consistent
1445
+ // index misses it (architecture, "Scenarios"); recorded when it withholds one
1446
+ const ledger = serviceLedger({ vendor: m.vendor, service: m.service, root, at, recording: !(scope.readOnly === true || isReadOnlyRequest(call.request)) });
1447
+ const lag = listLag(ledger, op.id);
1448
+ const settled = (r) => !lag || Date.parse(r.updatedAt) <= Date.parse(at) - durationMs(lag.by);
1449
+ const listed = stored(m, resource, root).filter(underParent);
1450
+ if (lag && listed.some((r) => !settled(r)))
1451
+ await recordFault(ledger, lag, { operation: op.id, withheld: listed.filter((r) => !settled(r)).map((r) => r.id) });
1452
+ const rows = listed.filter(settled).map((r) => view(m, resource, r));
1443
1453
  const arranged = arrangeRows(m, resource, params, rows);
1444
1454
  if (!Array.isArray(arranged))
1445
1455
  return { served: vendorError(m, arranged) };
@@ -1542,6 +1552,20 @@ const coreReadsBody = (type) => {
1542
1552
  const media = (type ?? '').split(';')[0].trim().toLowerCase();
1543
1553
  return media === '' || media === 'application/json' || media.endsWith('+json') || media === 'application/x-www-form-urlencoded' || media.startsWith('multipart/') || media.startsWith('text/');
1544
1554
  };
1555
+ const ledgers = new WeakMap();
1556
+ /** What a context consumes the World's scenario through (scenario-consume.ts): its twin's bookkeeping, at its time. */
1557
+ export function scenarioLedgerOf(ctx) {
1558
+ const ledger = ledgers.get(ctx);
1559
+ if (!ledger)
1560
+ throw new Error('a scenario ledger is a context the kernel opened');
1561
+ return ledger();
1562
+ }
1563
+ /** `GET /twin/scenario`: what the World's scenario placed on this vendor, and what the twin consumed of it. */
1564
+ export function scenarioRecords(m, root) {
1565
+ const placed = placedOn(m.vendor);
1566
+ const kept = (type) => resourcesOfType(m.service, storedType(m, type), root).map((r) => ownFields(r));
1567
+ return { vendor: m.vendor, placed, consumed: { decisions: kept('_decision'), faults: kept('_fault') } };
1568
+ }
1545
1569
  export async function contextFor(m, call, scope) {
1546
1570
  const root = scope.root;
1547
1571
  const asAsked = call;
@@ -1611,6 +1635,7 @@ export async function contextFor(m, call, scope) {
1611
1635
  const row = context.row(link.resource, link.id);
1612
1636
  return row && row.deleted !== true ? row : undefined;
1613
1637
  };
1638
+ const ledger = () => ({ vendor: m.vendor, service: m.service, root, at, recording: !(scope.readOnly === true || isReadOnlyRequest(call.request)), rows: (type) => context.rowsRaw(type), record: (type, fields, id) => context.record(type, fields, id) });
1614
1639
  const context = {
1615
1640
  call,
1616
1641
  params,
@@ -1620,6 +1645,9 @@ export async function contextFor(m, call, scope) {
1620
1645
  root,
1621
1646
  publicBase: twinPublicBase(call.request),
1622
1647
  scenario: scenarioDecisionOf(call.request),
1648
+ decide: (point, subject, byDefault) => decide(ledger(), point, subject, byDefault),
1649
+ fault: (point, subject) => faultAt(ledger(), point, subject),
1650
+ fact: (kind, key) => fact(ledger(), kind, key),
1623
1651
  ask: (url, init, within) => askApplication(url, init, within, scope.application),
1624
1652
  worldEnv: (name) => worldEnvValue(name),
1625
1653
  asVendor: (fn) => runAsVendorMove(fn),
@@ -1903,6 +1931,7 @@ export async function contextFor(m, call, scope) {
1903
1931
  };
1904
1932
  if (batchable(m, root))
1905
1933
  bindBatch(m, call, context, params, root, at, rawRow);
1934
+ ledgers.set(context, ledger);
1906
1935
  return context;
1907
1936
  }
1908
1937
  /**
@@ -201,4 +201,9 @@ export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{
201
201
  export declare function askApplication(url: string, init: RequestInit, within: number, standIn?: ApplicationStandIn): Promise<ApplicationAnswer>;
202
202
  /** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
203
203
  * that takes it, and recorded. */
204
- export declare function deliverEvents(service: string, decl: EventsDecl, write: EventWrite, data: (type: string) => Promise<Record<string, unknown>>, transport?: EventTransport, values?: (type: string) => Promise<Record<string, unknown>>, application?: ApplicationStandIn): Promise<EventDelivery[]>;
204
+ export declare function deliverEvents(service: string, decl: EventsDecl, write: EventWrite, data: (type: string) => Promise<Record<string, unknown>>, transport?: EventTransport, values?: (type: string) => Promise<Record<string, unknown>>, application?: ApplicationStandIn, vendor?: string): Promise<EventDelivery[]>;
205
+ /** Who hears that a twin held a packet for later (its pack's fetch, which sets the timer a running clock needs). */
206
+ export declare function onHeldDelivery(service: string, root: string | undefined, listener: () => void): void;
207
+ /** Send what a `webhook-delay` held that the World's clock has reached, in the order it fell due, and wait until it is
208
+ * sent; answer the instant the next held packet falls due, if any (ADR 0036 §4: the mover of the clock waits on it). */
209
+ export declare function releaseHeldDeliveries(service: string, root: string | undefined, record?: string, transport?: EventTransport): Promise<string | null>;
@@ -14,6 +14,9 @@ import { deliveryTraceHeaders } from "./trace-context.js";
14
14
  import { sendRecordedEvent } from "./event-delivery.js";
15
15
  import { getActiveWorldStore, withWorldStore } from "./world-store.js";
16
16
  import { worldPaths } from "./storage.js";
17
+ import { worldNow } from "./world-clock.js";
18
+ import { recordFault, serviceLedger, webhookFault } from "./scenario-consume.js";
19
+ import { durationMs } from "./scenario-timeline.js";
17
20
  /** Whether an endpoint takes an event by its `match`: every field of one match (any, when several) equal to the event's
18
21
  * value, or, a list, holding it. */
19
22
  function matches(row, match, filled) {
@@ -289,7 +292,7 @@ export async function askApplication(url, init, within, standIn) {
289
292
  }
290
293
  /** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
291
294
  * that takes it, and recorded. */
292
- export async function deliverEvents(service, decl, write, data, transport = worldTransport, values, application) {
295
+ export async function deliverEvents(service, decl, write, data, transport = worldTransport, values, application, vendor) {
293
296
  if (write.storedType === decl.store)
294
297
  return [];
295
298
  const dispatch = application ? async (url, body, headers) => {
@@ -298,11 +301,11 @@ export async function deliverEvents(service, decl, write, data, transport = worl
298
301
  } : transport;
299
302
  const sent = [];
300
303
  for (const type of eventTypesOf(decl, write.operation))
301
- sent.push(...(await deliverEventType(service, decl, write, data, dispatch, values, type)));
304
+ sent.push(...(await deliverEventType(service, decl, write, data, dispatch, values, type, vendor)));
302
305
  return sent;
303
306
  }
304
307
  /** One event type a write sends: to the live endpoints that take it, stored when the vendor keeps its events. */
305
- async function deliverEventType(service, decl, write, data, transport, values, type) {
308
+ async function deliverEventType(service, decl, write, data, transport, values, type, vendor) {
306
309
  const all = twinResources(service, write.root);
307
310
  const kinds = Array.isArray(decl.endpoints) ? decl.endpoints : [decl.endpoints];
308
311
  // an endpoint's row with its joined rows under their names (`app.hook_url` reads the installation's App)
@@ -371,16 +374,35 @@ async function deliverEventType(service, decl, write, data, transport, values, t
371
374
  // kept with the write, by an id of the delivery itself (two made at once never take the same one)
372
375
  const record = decl.record ?? '_world_event_delivery';
373
376
  const id = `${record.slice(1)}_v1_${seq}`;
374
- await applyTwinWrite(service, {
375
- operation: `${record.slice(1)}.record`, subjectType: record, subjectId: id,
376
- fields: { url, body, headers, event_type: type, sent_at: millis,
377
- delivery_protocol: 1, event_id: String(filled.$id),
378
- endpoint_type: kind.storedAs, endpoint_id: String(row.id),
379
- delivery_state: 'queued', attempt_count: 0, current_attempt: null,
380
- transport_status: null, finished_at: null, accepted_at: null,
381
- }, occurredAt: write.occurredAt, actor: { kind: 'system' },
382
- }, write.root);
377
+ const packet = { url, body, headers, event_type: type, sent_at: millis,
378
+ delivery_protocol: 1, event_id: String(filled.$id),
379
+ endpoint_type: kind.storedAs, endpoint_id: String(row.id),
380
+ delivery_state: 'queued', attempt_count: 0, current_attempt: null,
381
+ transport_status: null, finished_at: null, accepted_at: null,
382
+ };
383
+ await applyTwinWrite(service, { operation: `${record.slice(1)}.record`, subjectType: record, subjectId: id, fields: packet, occurredAt: write.occurredAt, actor: { kind: 'system' } }, write.root);
384
+ // a webhook fault the World's scenario placed (architecture, "Scenarios"): dropped, held until the World's clock
385
+ // reaches its delay (sent when the clock's mover reaches it, or by a running clock's timer: releaseHeldDeliveries),
386
+ // or sent twice, recorded as landed
387
+ const ledger = vendor ? serviceLedger({ vendor, service, root: write.root, at: write.occurredAt, recording: true }) : undefined;
388
+ const fault = ledger ? webhookFault(ledger, type) : undefined;
389
+ if (ledger && fault)
390
+ await recordFault(ledger, fault, { event: type, delivery: id });
391
+ if (fault?.kind === 'webhook-drop' || fault?.kind === 'webhook-delay') {
392
+ const held = fault.kind === 'webhook-delay' ? { delivery_state: 'held', due_at: millis + durationMs(fault.by) } : { delivery_state: 'dropped' };
393
+ await applyTwinWrite(service, { operation: `${record.slice(1)}.record`, subjectType: record, subjectId: id, fields: held, occurredAt: write.occurredAt, actor: { kind: 'system' } }, write.root);
394
+ sent.pop();
395
+ if (fault.kind === 'webhook-delay')
396
+ heldListeners.get(worldPaths(service, write.root).root)?.();
397
+ continue;
398
+ }
383
399
  recorded.push({ type: record, id });
400
+ if (fault?.kind === 'webhook-duplicate') {
401
+ const twin = `${id}_duplicate`;
402
+ await applyTwinWrite(service, { operation: `${record.slice(1)}.record`, subjectType: record, subjectId: twin, fields: packet, occurredAt: write.occurredAt, actor: { kind: 'system' } }, write.root);
403
+ sent.push(sent.at(-1));
404
+ recorded.push({ type: record, id: twin });
405
+ }
384
406
  }
385
407
  // sent after the write's answer, as a vendor sends it: the caller never waits on its receiver, and a receiver that is
386
408
  // slow or down fails no request
@@ -391,6 +413,11 @@ async function deliverEventType(service, decl, write, data, transport, values, t
391
413
  const store = getActiveWorldStore(), deliveryRoot = worldPaths(service, write.root).root;
392
414
  for (const d of sent)
393
415
  deliveryObserver?.({ ...d, service });
416
+ dispatchRecorded(service, store, deliveryRoot, recorded, transport, trace);
417
+ return sent;
418
+ }
419
+ /** Each recorded packet sent after the write's answer, keeping its request's store and root. */
420
+ function dispatchRecorded(service, store, deliveryRoot, recorded, transport, trace) {
394
421
  const roots = pendingDeliveries.get(store) ?? new Map();
395
422
  pendingDeliveries.set(store, roots);
396
423
  const pending = roots.get(deliveryRoot) ?? new Set();
@@ -415,5 +442,23 @@ async function deliverEventType(service, decl, write, data, transport, values, t
415
442
  }
416
443
  }, 0);
417
444
  }
418
- return sent;
445
+ }
446
+ const heldListeners = new Map();
447
+ /** Who hears that a twin held a packet for later (its pack's fetch, which sets the timer a running clock needs). */
448
+ export function onHeldDelivery(service, root, listener) {
449
+ heldListeners.set(worldPaths(service, root).root, listener);
450
+ }
451
+ /** Send what a `webhook-delay` held that the World's clock has reached, in the order it fell due, and wait until it is
452
+ * sent; answer the instant the next held packet falls due, if any (ADR 0036 §4: the mover of the clock waits on it). */
453
+ export async function releaseHeldDeliveries(service, root, record = '_world_event_delivery', transport = worldTransport) {
454
+ const now = Date.parse(worldNow());
455
+ const held = twinResources(service, root).filter((r) => r.type === record && r.deleted !== true && r.delivery_state === 'held').sort((a, b) => Number(a.due_at) - Number(b.due_at));
456
+ const due = held.filter((r) => Number(r.due_at) <= now);
457
+ for (const r of due)
458
+ await applyTwinWrite(service, { operation: `${record.slice(1)}.release`, subjectType: record, subjectId: r.id, fields: { delivery_state: 'queued' }, occurredAt: worldNow(), actor: { kind: 'system' } }, root);
459
+ const deliveryRoot = worldPaths(service, root).root;
460
+ dispatchRecorded(service, getActiveWorldStore(), deliveryRoot, due.map((r) => ({ type: record, id: r.id })), transport, {});
461
+ await settleEventDeliveries(deliveryRoot);
462
+ const next = held.find((r) => Number(r.due_at) > now);
463
+ return next ? new Date(Number(next.due_at)).toISOString() : null;
419
464
  }
@@ -76,8 +76,10 @@ export * as mysql from './mysql/index.js';
76
76
  export * as live from './live/index.js';
77
77
  export * as mongodb from './mongodb/index.js';
78
78
  export * as git from './git/index.js';
79
- export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from './scenario.js';
80
- export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
79
+ export { scenarioFaultResult, ScenarioError } from './scenario.js';
80
+ export { parseScenario, resolveScenario, WORLD_SCENARIO_ENV } from './scenario-timeline.js';
81
+ export type { ResolvedScenario, Scenario } from './scenario-timeline.js';
82
+ export type { PackScenarioAdapter, ScenarioDecision, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord } from './scenario.js';
81
83
  export { verifyAttestationBundle } from './attestation.js';
82
84
  export { cookieOf, signedIn, browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.js';
83
85
  export { readHostPort } from './host-port.js';
package/dist/src/index.js CHANGED
@@ -69,8 +69,9 @@ export * as mysql from "./mysql/index.js";
69
69
  export * as live from "./live/index.js";
70
70
  export * as mongodb from "./mongodb/index.js";
71
71
  export * as git from "./git/index.js";
72
- // ── the scenario grammar (architecture, "Behaviour") ───────────────────────────────────────────
73
- export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from "./scenario.js";
72
+ // ── the scenario grammar (architecture, "Scenarios") ───────────────────────────────────────────
73
+ export { scenarioFaultResult, ScenarioError } from "./scenario.js";
74
+ export { parseScenario, resolveScenario, WORLD_SCENARIO_ENV } from "./scenario-timeline.js";
74
75
  export { verifyAttestationBundle } from "./attestation.js";
75
76
  export { cookieOf, signedIn, browserSessionToken, startBrowserSession, endBrowserSession } from "./browser-session.js";
76
77
  export { readHostPort } from "./host-port.js";
@@ -303,7 +303,6 @@ export function scenario(vendor, operations, faultBody, readers = {}) {
303
303
  toolResultFor: (r, c) => nonEmpty(c) && r.results.includes(c),
304
304
  hasTool: (r, c) => nonEmpty(c) && r.tools.includes(c),
305
305
  },
306
- text: (r) => r.text,
307
306
  // source: https://platform.openai.com/docs/api-reference/chat/create "none means the model will not call any tool and instead generates a message"
308
307
  admits: (r, respond) => !(r.toolsForbidden && obj(respond).toolCalls !== undefined),
309
308
  validateOn: (on) => {
@@ -75,14 +75,12 @@ export type LanePart = {
75
75
  };
76
76
  };
77
77
  /** Where a pack serves: its World's root, whether it was started read-only, the World instant when a caller pins it, the
78
- * World's managed Postgres the runtime binds a pack declaring `managedDatabase` to (a handler's `ctx.engine`), and the
79
- * World's scenario file for a model vendor's turns (`scenarioPath`, the runtime's `--scenario`). */
78
+ * and the World's managed Postgres the runtime binds a pack declaring `managedDatabase` to (a handler's `ctx.engine`). */
80
79
  export type PackFetchOptions = {
81
80
  root?: string;
82
81
  readOnly?: boolean;
83
82
  clock?: () => string;
84
83
  database?: string;
85
- scenarioPath?: string;
86
84
  /** the application a walk stands in for: this fetch's requests and declared events are answered by it */
87
85
  application?: ApplicationStandIn;
88
86
  };