@tangleai/store 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @tangleai/store
2
2
 
3
+ ## 0.25.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Enforce semantic template binding targets, monotone caps and tool subsets at admission and instantiation. Pin and capture host message adapter versions, check child graph bindings, and preserve optional no-change instantiation.
8
+
9
+ Enforce workflow concurrency through Jaren scheduling, retain failed physical request costs in durable receipts, restore switch outputs from committed branches, and preserve hierarchical message paths. Advance the runtime checkpoint ABI for the changed execution semantics.
10
+
11
+ Compose durable human waits through nested graphs, switches and loop iterations.
12
+ Derive interaction ids from full paths, reconcile the current reserved resume
13
+ segment, and reject conflicting response bytes under an existing key. Drain
14
+ started host lifetimes before reporting failure or suspension.
15
+
16
+ Enforce physical-request context and retained trace quotas, retain the shared
17
+ account's provider-total or estimated token charge, and claim-fence cumulative
18
+ active-time settlement across resumable segments. Quota refusals roll back the
19
+ attempted payload while retaining actual failure costs.
20
+ - Updated dependencies
21
+ - @tangleai/mas@0.25.0
22
+ - @tangleai/config@0.25.0
23
+ - @tangleai/core@0.25.0
24
+ - @tangleai/documents@0.25.0
25
+ - @tangleai/memory@0.25.0
26
+ - @tangleai/outcomes@0.25.0
27
+
3
28
  ## 0.24.1
4
29
 
5
30
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangleai/store",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "Tangle AI persistence — the MemoryStore contract over SQLite via @jarenjs/db, plus the run/event log the DAG surface reads",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -58,15 +58,15 @@
58
58
  },
59
59
  "sideEffects": false,
60
60
  "dependencies": {
61
- "@tangleai/documents": "^0.24.1",
62
- "@tangleai/core": "^0.24.1",
63
- "@tangleai/config": "^0.24.1",
61
+ "@tangleai/documents": "^0.25.0",
62
+ "@tangleai/core": "^0.25.0",
63
+ "@tangleai/config": "^0.25.0",
64
64
  "@jarenjs/db": "0.86.0",
65
65
  "@jarenjs/validate": "0.86.0",
66
- "@tangleai/mas": "^0.24.1",
66
+ "@tangleai/mas": "^0.25.0",
67
67
  "@jarenjs/core": "0.86.0",
68
- "@tangleai/memory": "^0.24.1",
69
- "@tangleai/outcomes": "^0.24.1"
68
+ "@tangleai/memory": "^0.25.0",
69
+ "@tangleai/outcomes": "^0.25.0"
70
70
  },
71
71
  "private": false,
72
72
  "types": "./src/index.d.ts",
package/src/mas-jobs.js CHANGED
@@ -102,7 +102,7 @@ export async function ensurePendingMasSegments(db, masStore) {
102
102
  for (const run of runs) {
103
103
  counts.examined += 1;
104
104
  const trace = await masStore.readTrace(run.id);
105
- const responded = trace?.interactions.find((interaction) => interaction.status === 'responded' && interaction.resumeSegment !== null);
105
+ const responded = trace?.interactions.find((interaction) => interaction.status === 'responded' && interaction.resumeSegment === run.segment + 1);
106
106
  if (responded === undefined || responded.resumeSegment === null) {
107
107
  counts.skipped += 1;
108
108
  continue;
@@ -8,7 +8,7 @@
8
8
  * worker claim epoch (`TMAS2005` for a zombie's stale commit), and node
9
9
  * completion as ONE transaction writing the terminal attempt, outbound
10
10
  * messages, next state revision, budget snapshot and artifact rows —
11
- * all or nothing, exactly D6. Every record validates against the
11
+ * all or nothing. Every record validates against the
12
12
  * generated runtime contracts before it is written; persistence never
13
13
  * invents a shape. Semantic idempotency: `beginNodeAttempt` returns the
14
14
  * stored completion for a key it has already committed and refuses an
package/src/mas-store.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * worker claim epoch (`TMAS2005` for a zombie's stale commit), and node
9
9
  * completion as ONE transaction writing the terminal attempt, outbound
10
10
  * messages, next state revision, budget snapshot and artifact rows —
11
- * all or nothing, exactly D6. Every record validates against the
11
+ * all or nothing. Every record validates against the
12
12
  * generated runtime contracts before it is written; persistence never
13
13
  * invents a shape. Semantic idempotency: `beginNodeAttempt` returns the
14
14
  * stored completion for a key it has already committed and refuses an
@@ -47,6 +47,27 @@ export function createMasStore(db, options = {}) {
47
47
  throw error;
48
48
  }
49
49
  }
50
+ async function readTraceFrom(reader, runId) {
51
+ const run = await reader.collection('mas_runs').get(runId);
52
+ if (run === undefined)
53
+ return undefined;
54
+ const view = {
55
+ run: structuredClone(run),
56
+ attempts: asRows(await reader.collection('mas_node_attempts').execute(matching({ runId }))).map((row) => structuredClone(row)),
57
+ messages: asRows(await reader.collection('mas_messages').execute(matching({ runId }))).map((row) => structuredClone(row)),
58
+ stateRevisions: asRows(await reader.collection('mas_state_revisions').execute(matching({ runId }))).map((row) => structuredClone(row)),
59
+ interactions: asRows(await reader.collection('mas_interactions').execute(matching({ runId }))).map((row) => structuredClone(row)),
60
+ artifacts: asRows(await reader.collection('mas_trace_artifacts').execute(matching({ runId }))).map((row) => structuredClone(row)),
61
+ };
62
+ return view;
63
+ }
64
+ async function enforceTraceLimit(txn, runId) {
65
+ const trace = await readTraceFrom(txn, runId);
66
+ const limit = trace?.run.budget.limits?.traceBytes;
67
+ if (limit !== undefined && new TextEncoder().encode(JSON.stringify(trace)).byteLength > limit) {
68
+ throw new MasRollback(refuse('TMAS2009', '/limits/traceBytes', 'the retained trace byte budget is spent; the completion was rolled back'));
69
+ }
70
+ }
50
71
  async function putImmutable(collection, key, value, keyMember) {
51
72
  return atomically(async (txn) => {
52
73
  const handle = txn.collection(collection);
@@ -221,13 +242,24 @@ export function createMasStore(db, options = {}) {
221
242
  if (!transition.ok)
222
243
  return { ok: false, issue: transition.issue };
223
244
  const next = { ...run, status: transition.status };
245
+ if (command.settlement) {
246
+ const receipt = command.settlement;
247
+ if (receipt.claimSeq !== run.claim.seq)
248
+ return refuse('TMAS2005', '/claim', 'a stale segment cannot settle the budget');
249
+ if (Object.values(receipt.spent).some(n => !Number.isFinite(n) || n < 0))
250
+ return refuse('TMAS2009', '/budget/spent', 'invalid budget settlement');
251
+ next.budget = { limits: run.budget.limits, spent: { turns: Math.max(run.budget.spent.turns, receipt.spent.turns), tokens: Math.max(run.budget.spent.tokens, receipt.spent.tokens), ms: Math.max(run.budget.spent.ms, receipt.spent.ms) } };
252
+ }
224
253
  if (command.kind === 'complete')
225
254
  next.output = command.output;
226
255
  if (command.kind === 'fail')
227
256
  next.failure = command.failure;
228
257
  if (command.kind === 'queue-segment')
229
258
  next.segment = run.segment + 1;
230
- return writeRun(txn, next);
259
+ const written = await writeRun(txn, next);
260
+ if (written.ok && (command.kind === 'complete' || command.kind === 'wait'))
261
+ await enforceTraceLimit(txn, runId);
262
+ return written;
231
263
  });
232
264
  },
233
265
  async putRunFsm(runId, controlId, snapshot) {
@@ -235,7 +267,10 @@ export function createMasStore(db, options = {}) {
235
267
  const run = await readRun(txn, runId);
236
268
  if (run === undefined)
237
269
  return refuse('TMAS2002', '/id', `run '${runId}' does not exist`);
238
- return writeRun(txn, { ...run, fsm: { ...run.fsm, [controlId]: snapshot } });
270
+ const written = await writeRun(txn, { ...run, fsm: { ...run.fsm, [controlId]: snapshot } });
271
+ if (written.ok)
272
+ await enforceTraceLimit(txn, runId);
273
+ return written;
239
274
  });
240
275
  },
241
276
  async beginNodeAttempt(plan) {
@@ -351,12 +386,13 @@ export function createMasStore(db, options = {}) {
351
386
  spent: {
352
387
  turns: spent.turns + plan.spend.turns,
353
388
  tokens: spent.tokens + plan.spend.tokens,
354
- ms: spent.ms + plan.spend.ms,
389
+ ms: Math.max(spent.ms + plan.spend.ms, plan.activeMs ?? 0),
355
390
  },
356
391
  },
357
392
  });
358
393
  if (!written.ok)
359
394
  throw new MasRollback({ ok: false, issue: written.issue });
395
+ await enforceTraceLimit(txn, plan.runId);
360
396
  return {
361
397
  ok: true,
362
398
  value: {
@@ -382,12 +418,19 @@ export function createMasStore(db, options = {}) {
382
418
  if (current.status !== 'running') {
383
419
  return refuse('TMAS2003', '/status', `only a running attempt can move to '${plan.status}'; '${plan.attemptId}' is '${current.status}'`);
384
420
  }
385
- const next = { ...current, status: plan.status, error: plan.error, finishedAt: now() };
421
+ const next = { ...current, ...plan.receipt, status: plan.status, error: plan.error, finishedAt: now() };
386
422
  const outcome = validateRuntimeRecord('masNodeAttempt', next);
387
423
  if (!outcome.valid) {
388
424
  return refuse('TMAS2004', `/attempt${outcome.issues[0]?.path ?? ''}`, outcome.issues[0]?.detail ?? '');
389
425
  }
390
426
  await handle.put(next);
427
+ if (plan.receipt !== undefined) {
428
+ const charged = plan.receipt.spend, spent = run.budget.spent;
429
+ const written = await writeRun(txn, { ...run, budget: { limits: run.budget.limits,
430
+ spent: { turns: spent.turns + charged.turns, tokens: spent.tokens + charged.tokens, ms: Math.max(spent.ms + charged.ms, plan.activeMs ?? 0) } } });
431
+ if (!written.ok)
432
+ throw new MasRollback({ ok: false, issue: written.issue });
433
+ }
391
434
  return { ok: true, value: structuredClone(next) };
392
435
  });
393
436
  },
@@ -396,7 +439,7 @@ export function createMasStore(db, options = {}) {
396
439
  const run = await readRun(txn, plan.runId);
397
440
  if (run === undefined)
398
441
  return refuse('TMAS2002', '/id', `run '${plan.runId}' does not exist`);
399
- const id = `${plan.runId}:i:${plan.node}`;
442
+ const id = `${plan.runId}:i:${plan.path}`;
400
443
  const handle = txn.collection('mas_interactions');
401
444
  const existing = await handle.get(id);
402
445
  if (existing !== undefined)
@@ -423,6 +466,7 @@ export function createMasStore(db, options = {}) {
423
466
  return refuse('TMAS2004', `/interaction${outcome.issues[0]?.path ?? ''}`, outcome.issues[0]?.detail ?? '');
424
467
  }
425
468
  await handle.put(interaction);
469
+ await enforceTraceLimit(txn, plan.runId);
426
470
  return { ok: true, value: structuredClone(interaction) };
427
471
  });
428
472
  },
@@ -437,6 +481,8 @@ export function createMasStore(db, options = {}) {
437
481
  if (current === undefined)
438
482
  return refuse('TMAS2007', '/id', `interaction '${id}' does not exist`);
439
483
  if (current.status === 'responded' && current.responseKey === responseKey) {
484
+ if (!equalsJson(current.response, response))
485
+ return refuse('TMAS2007', '/response', 'the response key already identifies different response bytes');
440
486
  return { ok: true, value: structuredClone(current) };
441
487
  }
442
488
  const transition = planInteractionTransition(current.status, 'responded');
@@ -506,19 +552,6 @@ export function createMasStore(db, options = {}) {
506
552
  const latest = rows.at(-1);
507
553
  return latest === undefined ? undefined : { id: latest.id, value: structuredClone(latest.value) };
508
554
  },
509
- async readTrace(runId) {
510
- const run = await runs().get(runId);
511
- if (run === undefined)
512
- return undefined;
513
- const view = {
514
- run: structuredClone(run),
515
- attempts: asRows(await attempts().execute(matching({ runId }))).map((row) => structuredClone(row)),
516
- messages: asRows(await db.collection('mas_messages').execute(matching({ runId }))).map((row) => structuredClone(row)),
517
- stateRevisions: asRows(await db.collection('mas_state_revisions').execute(matching({ runId }))).map((row) => structuredClone(row)),
518
- interactions: asRows(await db.collection('mas_interactions').execute(matching({ runId }))).map((row) => structuredClone(row)),
519
- artifacts: asRows(await db.collection('mas_trace_artifacts').execute(matching({ runId }))).map((row) => structuredClone(row)),
520
- };
521
- return view;
522
- },
555
+ async readTrace(runId) { return readTraceFrom(db, runId); },
523
556
  };
524
557
  }