skynet-graph 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +22 -0
- package/LICENSE +661 -0
- package/README.md +245 -0
- package/bin/sg +20 -0
- package/concepts/README.md +23 -0
- package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
- package/concepts/_substrate/Claim/Confidence.json +10 -0
- package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
- package/concepts/_substrate/Claim/Freshness.json +5 -0
- package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
- package/concepts/_substrate/Claim/Verification.json +6 -0
- package/concepts/_substrate/Claim.json +4 -0
- package/concepts/_substrate/Frontier/InBeam.json +6 -0
- package/concepts/_substrate/Frontier/Stuck.json +6 -0
- package/concepts/_substrate/Frontier.json +5 -0
- package/concepts/_substrate/Intake/ToTask.json +22 -0
- package/concepts/_substrate/Intake/Typed.json +10 -0
- package/concepts/_substrate/Intake.json +27 -0
- package/concepts/_substrate/Task/Answer.json +12 -0
- package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
- package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
- package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
- package/concepts/_substrate/Task/Complexity.json +6 -0
- package/concepts/_substrate/Task/EvalComplexity.json +13 -0
- package/concepts/_substrate/Task/ReportUp.json +6 -0
- package/concepts/_substrate/Task/Rollup.json +10 -0
- package/concepts/_substrate/Task.json +5 -0
- package/concepts/clinical/Diagnosis/Medication.json +6 -0
- package/concepts/clinical/Diagnosis.json +15 -0
- package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
- package/concepts/clinical/Observation/LabValue.json +5 -0
- package/concepts/clinical/Observation.json +6 -0
- package/concepts/common/Document/pathBasket.json +6 -0
- package/concepts/common/Document.json +3 -0
- package/concepts/common/Edge/Distance.json +13 -0
- package/concepts/common/Edge/Stay/LongStay.json +8 -0
- package/concepts/common/Edge/Stay.json +8 -0
- package/concepts/common/Edge/Travel/LongTravel.json +16 -0
- package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
- package/concepts/common/Edge/Travel/targetNode.json +7 -0
- package/concepts/common/Edge/Travel.json +7 -0
- package/concepts/common/Edge.json +3 -0
- package/concepts/common/Vertice.json +3 -0
- package/concepts/supply/Fulfillment.json +10 -0
- package/concepts/supply/Inventory/Reorder.json +7 -0
- package/concepts/supply/Inventory.json +5 -0
- package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
- package/concepts/supply/Procurement.json +8 -0
- package/concepts/supply/Transport.json +5 -0
- package/doc/API.md +556 -0
- package/doc/CAPABILITIES.md +373 -0
- package/doc/MODELISATION.md +511 -0
- package/doc/architecture.md +267 -0
- package/doc/concept-as-graph.md +379 -0
- package/doc/concept-learning.md +168 -0
- package/doc/creative-loop-map.md +202 -0
- package/doc/original-2016-doc.md +148 -0
- package/doc/usage.md +501 -0
- package/lib/authoring/abstract.js +401 -0
- package/lib/authoring/abstraction.js +190 -0
- package/lib/authoring/adapt.js +393 -0
- package/lib/authoring/ancestry.js +216 -0
- package/lib/authoring/author.js +147 -0
- package/lib/authoring/bounded-merge.js +57 -0
- package/lib/authoring/canon.js +158 -0
- package/lib/authoring/clock.js +123 -0
- package/lib/authoring/combinator.js +92 -0
- package/lib/authoring/compete.js +76 -0
- package/lib/authoring/compose-hotspot.js +314 -0
- package/lib/authoring/compress.js +279 -0
- package/lib/authoring/concept-net.js +254 -0
- package/lib/authoring/concepts.js +66 -0
- package/lib/authoring/context-project.js +277 -0
- package/lib/authoring/contract.js +476 -0
- package/lib/authoring/corpus-pack.js +125 -0
- package/lib/authoring/cost-probe.js +86 -0
- package/lib/authoring/crystallize.js +555 -0
- package/lib/authoring/dag-decompose.js +256 -0
- package/lib/authoring/dataset-adapter.js +253 -0
- package/lib/authoring/debug-provider.js +92 -0
- package/lib/authoring/decompose.js +343 -0
- package/lib/authoring/emittability.js +236 -0
- package/lib/authoring/equilibrium.js +160 -0
- package/lib/authoring/extract.js +153 -0
- package/lib/authoring/forest.js +55 -0
- package/lib/authoring/forge-fallback.js +116 -0
- package/lib/authoring/givens.js +121 -0
- package/lib/authoring/glossary.js +177 -0
- package/lib/authoring/grammar-graph.js +191 -0
- package/lib/authoring/granularity.js +80 -0
- package/lib/authoring/graph-net.js +108 -0
- package/lib/authoring/ground.js +148 -0
- package/lib/authoring/higher-order.js +85 -0
- package/lib/authoring/hotspot.js +132 -0
- package/lib/authoring/hysteresis.js +85 -0
- package/lib/authoring/index.js +106 -0
- package/lib/authoring/lattice-morphism.js +61 -0
- package/lib/authoring/lattice-pack.js +148 -0
- package/lib/authoring/leaf-io.js +77 -0
- package/lib/authoring/library.js +156 -0
- package/lib/authoring/lifecycle.js +92 -0
- package/lib/authoring/loop.js +193 -0
- package/lib/authoring/master-loop.js +132 -0
- package/lib/authoring/mdl.js +209 -0
- package/lib/authoring/memo-stability.js +122 -0
- package/lib/authoring/method-explorer.js +138 -0
- package/lib/authoring/method-pack.js +181 -0
- package/lib/authoring/method.js +202 -0
- package/lib/authoring/mine.js +510 -0
- package/lib/authoring/mount.js +119 -0
- package/lib/authoring/negotiate.js +100 -0
- package/lib/authoring/parametric.js +139 -0
- package/lib/authoring/reaggregate.js +75 -0
- package/lib/authoring/rebalance.js +157 -0
- package/lib/authoring/recall.js +135 -0
- package/lib/authoring/registry.js +350 -0
- package/lib/authoring/relearn.js +147 -0
- package/lib/authoring/retention.js +89 -0
- package/lib/authoring/segment-proxy.js +114 -0
- package/lib/authoring/serve-leaf.js +64 -0
- package/lib/authoring/slot-aware-serve.js +77 -0
- package/lib/authoring/sound-invoke.js +59 -0
- package/lib/authoring/split-serve.js +74 -0
- package/lib/authoring/ste.js +239 -0
- package/lib/authoring/stock.js +101 -0
- package/lib/authoring/store.js +94 -0
- package/lib/authoring/supervise.js +138 -0
- package/lib/authoring/support.js +131 -0
- package/lib/authoring/task-mirror.js +51 -0
- package/lib/authoring/typed-loop.js +160 -0
- package/lib/authoring/validate.js +569 -0
- package/lib/authoring/widen.js +88 -0
- package/lib/combos/appliance.js +189 -0
- package/lib/combos/critique.js +377 -0
- package/lib/combos/defaults.js +86 -0
- package/lib/combos/durable-runner.js +115 -0
- package/lib/combos/forge.js +209 -0
- package/lib/combos/index.js +86 -0
- package/lib/combos/learning-library.js +253 -0
- package/lib/combos/mixture-serve.js +193 -0
- package/lib/combos/plan-loop.js +192 -0
- package/lib/combos/proxy-cache.js +272 -0
- package/lib/combos/self-mod.js +94 -0
- package/lib/durable/audit.js +102 -0
- package/lib/durable/checkpoint-store.js +550 -0
- package/lib/durable/fold.js +62 -0
- package/lib/durable/interpreter.js +242 -0
- package/lib/durable/xlate.js +185 -0
- package/lib/graph/Graph.js +2486 -0
- package/lib/graph/expr.js +307 -0
- package/lib/graph/index.js +26 -0
- package/lib/graph/log.js +134 -0
- package/lib/graph/objects/Concept.js +382 -0
- package/lib/graph/objects/Entity.js +503 -0
- package/lib/graph/objects/Node.js +57 -0
- package/lib/graph/objects/PathMap.js +557 -0
- package/lib/graph/objects/Segment.js +100 -0
- package/lib/graph/tasks/stabilize.js +59 -0
- package/lib/graph/tasks/taskflow.js +247 -0
- package/lib/index.js +122 -0
- package/lib/load.js +132 -0
- package/lib/providers/backends.js +117 -0
- package/lib/providers/borderline.js +129 -0
- package/lib/providers/cache.js +226 -0
- package/lib/providers/canonicalize.js +198 -0
- package/lib/providers/constat.js +107 -0
- package/lib/providers/geo.js +53 -0
- package/lib/providers/index.js +116 -0
- package/lib/providers/intake.js +263 -0
- package/lib/providers/llm-local.js +88 -0
- package/lib/providers/llm.js +236 -0
- package/lib/providers/local-host.js +197 -0
- package/lib/providers/merge-consistency.js +162 -0
- package/lib/providers/nogood.js +149 -0
- package/lib/providers/reason-loop.js +223 -0
- package/lib/providers/semiring.js +285 -0
- package/lib/providers/solver-fork.js +163 -0
- package/lib/providers/stats.js +228 -0
- package/lib/providers/verify.js +120 -0
- package/lib/runtime/index.js +135 -0
- package/lib/runtime/invoke-pool.js +92 -0
- package/lib/runtime/protocol.js +210 -0
- package/lib/runtime/transport-socket.js +77 -0
- package/lib/runtime/worker-entry.js +31 -0
- package/lib/sg/cli.js +601 -0
- package/lib/sg/log-sinks.js +214 -0
- package/lib/sg/mcp.js +395 -0
- package/lib/sg/proxy-run.js +66 -0
- package/lib/sg/serve-demo.js +185 -0
- package/lib/sg/serve.js +205 -0
- package/lib/sg/trace.js +118 -0
- package/lib/studio/protocol.js +45 -0
- package/lib/studio/public/app.js +310 -0
- package/lib/studio/public/components/ConceptEditor.js +43 -0
- package/lib/studio/public/components/ConceptTree.js +43 -0
- package/lib/studio/public/components/CorpusPanel.js +68 -0
- package/lib/studio/public/components/ForkTree.js +36 -0
- package/lib/studio/public/components/GrammarGraph.js +99 -0
- package/lib/studio/public/components/GraphCanvas.js +156 -0
- package/lib/studio/public/components/Inspector.js +48 -0
- package/lib/studio/public/components/LearningPanel.js +85 -0
- package/lib/studio/public/components/PromptConsole.js +46 -0
- package/lib/studio/public/components/ProviderTrace.js +39 -0
- package/lib/studio/public/components/SessionSplit.js +47 -0
- package/lib/studio/public/components/TilingOverlay.js +40 -0
- package/lib/studio/public/components/Timeline.js +57 -0
- package/lib/studio/public/index.html +26 -0
- package/lib/studio/public/styles.css +185 -0
- package/lib/studio/public/ws.js +58 -0
- package/lib/studio/server.js +130 -0
- package/lib/studio/session.js +397 -0
- package/lib/studio/studio.js +149 -0
- package/package.json +42 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Nathanael Braun
|
|
3
|
+
* @author : Nathanael BRAUN <pp9ping@gmail.com>
|
|
4
|
+
*
|
|
5
|
+
* AGPL-3.0-or-later. See <https://www.gnu.org/licenses/>.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
/**
|
|
9
|
+
* checkpoint-store — LAYER A of the durable executor (the conception's "execute" half). A THIN durable
|
|
10
|
+
* substrate behind the `CheckpointStore` interface (study `doc/WIP/studies/2026-06-28-rocinante-convergence.md`
|
|
11
|
+
* §5). It holds three things and NOTHING domain-specific:
|
|
12
|
+
*
|
|
13
|
+
* · the durable MARKING — tokens(runId, recordId, placeId, status, payload, attempts, leaseUntil): each
|
|
14
|
+
* case = a token walked through a workflow-net; its position is durable (C2). >1
|
|
15
|
+
* token/record at a fan-out. This generalizes rocinante's entering/processing/
|
|
16
|
+
* leaving (one author, n8tz) to named "places".
|
|
17
|
+
* · the content-addressed MEMO — memo(key=FactsDigest, output): a step replays safely keyed on the canonical
|
|
18
|
+
* input digest, NOT positionally (C5 — skynet's K1 barrier; the durable sibling of
|
|
19
|
+
* `lib/providers/cache.js`).
|
|
20
|
+
* · the createdRefs ROLLBACK set — undo a crashed step's partial external effects on resume (rocinante's
|
|
21
|
+
* `_createdRefs` orphan-scan, generalized).
|
|
22
|
+
*
|
|
23
|
+
* It is NOT the skynet engine and holds NO belief: it is the durable plumbing UNDER the belief-view (the B4
|
|
24
|
+
* line). Layer B (the token-flow interpreter) walks records through a compiled net VIA this interface; the
|
|
25
|
+
* skynet-native typed routing / JTMS-at-merge live there, not here.
|
|
26
|
+
*
|
|
27
|
+
* Two backends, ONE contract (`tests/_checkpoint-suite.js`): `createMemoryCheckpointStore` (zero-dep, the
|
|
28
|
+
* reference — pure, runs in the unit suite) and `createSqliteCheckpointStore` (the durable default — one file
|
|
29
|
+
* via the built-in `node:sqlite`, lazy-required so the experimental module loads only on opt-in). Both are
|
|
30
|
+
* pluggable to pg-boss/Postgres or BullMQ/Redis at scale behind the same interface.
|
|
31
|
+
*
|
|
32
|
+
* Crash-safety has two paths that converge on "in-flight → ready, undo partial effects":
|
|
33
|
+
* · LEASE EXPIRY — automatic, per-token, online: a worker dies, its lease lapses, `claim` re-offers it.
|
|
34
|
+
* · rollbackInflight(runId) — explicit, on process boot: reset ALL leased tokens at once + return their
|
|
35
|
+
* createdRefs for the host to undo (the orphan-scan).
|
|
36
|
+
*
|
|
37
|
+
* `now` is injected (default `Date.now`) so lease-expiry is deterministically testable.
|
|
38
|
+
*
|
|
39
|
+
* STATUSES: ready (claimable) · leased (held, claimable iff lease expired) · done (reached a sink) ·
|
|
40
|
+
* failed (dead-lettered) · consumed (fired a fan-out transition — kept for the parentId audit trail) ·
|
|
41
|
+
* joined (a map child PARKED at a fan-IN place, its contribution delivered — kept for audit; the
|
|
42
|
+
* completing arrival spawns ONE collector token carrying the projected sibling payloads).
|
|
43
|
+
*
|
|
44
|
+
* THE FAN-IN / JOIN (`joinArrive`) — the durable JTMS-at-merge point (study §4B, "map = fan-out + a cardinality
|
|
45
|
+
* JOIN"). Layer A owns only the SYNCHRONISATION (atomic: count arrivals of a fan-out GROUP, designate exactly one
|
|
46
|
+
* collector when the cardinality is met); the FOLD operator (monoid / micro-task) is Layer B's domain logic. The
|
|
47
|
+
* group key is the `parentId` the fan-out stamped on every child — so siblings of one map are identified without a
|
|
48
|
+
* coloured net (single-world JTMS forbids N-cases-in-one-world). It is a BOUNDED PROJECTION (N retained-for-audit
|
|
49
|
+
* inputs → 1 collector), never a destructive ref-swap.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
const READY = 'ready', LEASED = 'leased', DONE = 'done', FAILED = 'failed', CONSUMED = 'consumed', JOINED = 'joined';
|
|
53
|
+
|
|
54
|
+
function clone( x ) { return x == null ? x : JSON.parse(JSON.stringify(x)); }
|
|
55
|
+
|
|
56
|
+
// shape a stored row into the plain token returned to callers (backend-agnostic)
|
|
57
|
+
function viewToken( row ) {
|
|
58
|
+
const t = {
|
|
59
|
+
id: row.id, runId: row.runId, recordId: row.recordId, placeId: row.placeId,
|
|
60
|
+
status: row.status, payload: clone(row.payload), attempts: row.attempts, leaseUntil: row.leaseUntil,
|
|
61
|
+
};
|
|
62
|
+
if ( row.parentId != null ) t.parentId = row.parentId;
|
|
63
|
+
if ( row.groupId != null ) t.groupId = row.groupId;
|
|
64
|
+
if ( row.reason != null ) t.reason = row.reason;
|
|
65
|
+
if ( row.leaseId != null ) t.leaseId = row.leaseId; // the FENCING token (Kleppmann) — see holds()
|
|
66
|
+
return t;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// a writer (move/fail/track) may only mutate a token it currently HOLDS: the row must still be leased AND the
|
|
70
|
+
// caller's fencing token must match the row's current lease. This rejects a zombie worker whose lease lapsed
|
|
71
|
+
// and was re-claimed by another worker (same token id, bumped leaseId) — no marking corruption.
|
|
72
|
+
function holds( row, token ) {
|
|
73
|
+
return !!row && row.status === LEASED && row.leaseId != null && row.leaseId === (token && token.leaseId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// is a row claimable now? ready, or a leased row whose lease has lapsed (the worker is assumed crashed).
|
|
77
|
+
function isClaimable( row, now ) {
|
|
78
|
+
return row.status === READY || (row.status === LEASED && (row.leaseUntil == null || row.leaseUntil <= now));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ===========================================================================================================
|
|
82
|
+
// MEMORY reference impl — the zero-dep contract reference. Synchronous ⇒ each method is atomic by construction.
|
|
83
|
+
// ===========================================================================================================
|
|
84
|
+
|
|
85
|
+
function createMemoryCheckpointStore( opts ) {
|
|
86
|
+
opts = opts || {};
|
|
87
|
+
const now = opts.now || Date.now;
|
|
88
|
+
const defaults = { startPlace: opts.startPlace || 'start', failPlace: opts.failPlace || 'failed' };
|
|
89
|
+
|
|
90
|
+
const runs = new Map(); // runId -> { start, sinks:Set, fail }
|
|
91
|
+
const rows = new Map(); // tokenId -> row
|
|
92
|
+
const byRun = new Map(); // runId -> Set<tokenId> (claim/marking locality)
|
|
93
|
+
const memo = new Map(); // key -> output
|
|
94
|
+
let seq = 0, leaseSeq = 0;
|
|
95
|
+
const nextId = () => 't' + (++seq);
|
|
96
|
+
|
|
97
|
+
function runOf( runId ) {
|
|
98
|
+
let r = runs.get(runId);
|
|
99
|
+
if ( !r ) { r = { start: defaults.startPlace, sinks: new Set(), fail: defaults.failPlace }; runs.set(runId, r); byRun.set(runId, new Set()); }
|
|
100
|
+
return r;
|
|
101
|
+
}
|
|
102
|
+
function rowsOf( runId ) {
|
|
103
|
+
const out = [], ids = byRun.get(runId);
|
|
104
|
+
if ( ids ) for ( const id of ids ) out.push(rows.get(id));
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
// groupId = the FAN-IN grouping key (NESTED fold): defaults to parentId (1-level byte-identical), RE-stamped to
|
|
108
|
+
// the outer group when an inner collector parks at the OUTER join. parentId stays audit truth; groupId is additive.
|
|
109
|
+
function add( row ) { if ( row.groupId === undefined ) row.groupId = row.parentId; rows.set(row.id, row); byRun.get(row.runId).add(row.id); return row; }
|
|
110
|
+
|
|
111
|
+
// shared fan-in completion: spawn ONE collector at `foldPlace` over the parked survivors of `group` once every
|
|
112
|
+
// shard is accounted for (joined survivors + — in survivors mode — dropped FAILED siblings). Used by joinArrive
|
|
113
|
+
// AND joinFail (the survivors symmetric path). The `_partial`/`_dropped`/`_expected` markers ride the collector
|
|
114
|
+
// ONLY in survivors mode (failfast stays byte-identical). Returns { ready, collector, n, partial } | { ready:false }.
|
|
115
|
+
function memTryCollect( runId, recordId, group, joinPlace, foldPlace, expected, survivors ) {
|
|
116
|
+
const gidOf = ( x ) => x.groupId != null ? x.groupId : x.parentId; // scan by the grouping key (nested-safe)
|
|
117
|
+
const arrived = rowsOf(runId).filter(( x ) => gidOf(x) === group && x.placeId === joinPlace && x.status === JOINED );
|
|
118
|
+
const dropped = survivors ? rowsOf(runId).filter(( x ) => gidOf(x) === group && x.status === FAILED ).length : 0;
|
|
119
|
+
if ( group == null || expected == null || (arrived.length + dropped) < expected )
|
|
120
|
+
return { ready: false, arrived: arrived.length, expected: expected };
|
|
121
|
+
if ( rowsOf(runId).some(( x ) => gidOf(x) === group && x.placeId === foldPlace ) ) // already collected — exactly one spawn
|
|
122
|
+
return { ready: false, arrived: arrived.length, expected: expected, collected: true };
|
|
123
|
+
const siblings = arrived.map(( x ) => clone(x.payload)); // id-order = fan-out order; Layer B sorts by _i
|
|
124
|
+
const payload = survivors
|
|
125
|
+
? { _group: String(group), _n: expected, _siblings: siblings, _expected: expected, _dropped: dropped, _partial: dropped > 0 }
|
|
126
|
+
: { _group: String(group), _n: expected, _siblings: siblings };
|
|
127
|
+
const collector = add({ id: nextId(), runId, recordId, placeId: foldPlace, status: runOf(runId).sinks.has(foldPlace) ? DONE : READY,
|
|
128
|
+
payload, attempts: 0, leaseUntil: null, leaseId: null, parentId: group, created: [], reason: null });
|
|
129
|
+
return { ready: true, collector: viewToken(collector), n: siblings.length, partial: dropped > 0 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
ensureRun( runId, def ) {
|
|
134
|
+
def = def || {};
|
|
135
|
+
const r = runOf(runId);
|
|
136
|
+
if ( def.start ) r.start = def.start;
|
|
137
|
+
if ( def.fail ) r.fail = def.fail;
|
|
138
|
+
if ( def.sinks ) r.sinks = new Set(def.sinks);
|
|
139
|
+
r.def = clone(def);
|
|
140
|
+
return this;
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
inject( runId, records ) {
|
|
144
|
+
const r = runOf(runId);
|
|
145
|
+
return (records || []).map(( rec ) => {
|
|
146
|
+
const rid = rec && (rec.recordId != null ? rec.recordId : rec.id);
|
|
147
|
+
const recordId = rid == null ? null : String(rid); // an opaque STRING handle (consistent across backends)
|
|
148
|
+
const payload = rec && rec.payload !== undefined ? rec.payload : rec;
|
|
149
|
+
return viewToken(add({ id: nextId(), runId, recordId, placeId: r.start, status: READY,
|
|
150
|
+
payload: clone(payload), attempts: 0, leaseUntil: null, leaseId: null, parentId: null, created: [], reason: null }));
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
claim( runId, o ) {
|
|
155
|
+
o = o || {};
|
|
156
|
+
const lease = o.lease || 30000, limit = o.limit || 1, maxAttempts = o.maxAttempts || Infinity;
|
|
157
|
+
const t = now(), out = [];
|
|
158
|
+
const ids = byRun.get(runId);
|
|
159
|
+
if ( !ids ) return out;
|
|
160
|
+
for ( const id of ids ) { // insertion order ⇒ FIFO (no priority; v0)
|
|
161
|
+
if ( out.length >= limit ) break;
|
|
162
|
+
const row = rows.get(id);
|
|
163
|
+
if ( !isClaimable(row, t) ) continue;
|
|
164
|
+
if ( row.attempts >= maxAttempts ) { // poison token → dead-letter, don't re-lease
|
|
165
|
+
row.status = FAILED; row.placeId = runOf(runId).fail; row.reason = 'max-attempts'; row.leaseUntil = null; row.leaseId = null;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
row.status = LEASED; row.leaseUntil = t + lease; row.attempts += 1; row.leaseId = ++leaseSeq;
|
|
169
|
+
out.push(viewToken(row));
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
move( token, toPlace, o ) {
|
|
175
|
+
o = o || {};
|
|
176
|
+
const row = rows.get(token && token.id);
|
|
177
|
+
if ( !holds(row, token) ) return null; // stale / re-claimed / unknown → reject (no corruption)
|
|
178
|
+
const r = runOf(row.runId);
|
|
179
|
+
if ( o.created ) row.created = (row.created || []).concat(o.created);
|
|
180
|
+
if ( o.payload ) row.payload = Object.assign({}, row.payload, o.payload);
|
|
181
|
+
|
|
182
|
+
if ( Array.isArray(toPlace) ) { // FAN-OUT 1->N: consume the source, spawn children
|
|
183
|
+
row.status = CONSUMED; row.leaseUntil = null; row.leaseId = null;
|
|
184
|
+
const pl = o.payloads; // optional per-child payloads (the map elem case); else clone the parent
|
|
185
|
+
return toPlace.map(( place, i ) => viewToken(add({ id: nextId(), runId: row.runId, recordId: row.recordId,
|
|
186
|
+
placeId: place, status: r.sinks.has(place) ? DONE : READY, payload: clone(pl ? pl[i] : row.payload),
|
|
187
|
+
attempts: 0, leaseUntil: null, leaseId: null, parentId: row.id, created: [], reason: null })));
|
|
188
|
+
}
|
|
189
|
+
row.placeId = toPlace; // 1->1: relabel (identity walks the net)
|
|
190
|
+
row.status = r.sinks.has(toPlace) ? DONE : READY;
|
|
191
|
+
row.leaseUntil = null; row.leaseId = null;
|
|
192
|
+
return viewToken(row);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// FAN-IN: park a held map child at `joinPlace` (its contribution delivered); if all `expected` siblings of
|
|
196
|
+
// its fan-out GROUP (parentId) have now arrived, atomically spawn ONE collector token at `foldPlace`
|
|
197
|
+
// carrying the siblings' payloads (id-order = fan-out order), and return { ready:true, collector }. Else
|
|
198
|
+
// { ready:false }. Synchronous ⇒ exactly one arrival can be the completing one (no double collector).
|
|
199
|
+
joinArrive( token, joinPlace, o ) {
|
|
200
|
+
o = o || {};
|
|
201
|
+
const expected = o.expected, foldPlace = o.foldPlace, failPlace = o.failPlace, survivors = o.mode === 'survivors';
|
|
202
|
+
const row = rows.get(token && token.id);
|
|
203
|
+
if ( !holds(row, token) ) return null; // fencing — only the lease holder may fire the join
|
|
204
|
+
const group = o.group != null ? o.group : row.parentId; // NESTED: the grouping key (top of the _groups stack)
|
|
205
|
+
const gidOf = ( x ) => x.groupId != null ? x.groupId : x.parentId;
|
|
206
|
+
// FAIL-FAST (failfast mode only): a sibling already FAILED → this arrival joins the failure rather than
|
|
207
|
+
// folding a partial as complete (the "no wrong derivation" line). SURVIVORS tolerates a dropped sibling.
|
|
208
|
+
if ( !survivors && failPlace != null && rowsOf(row.runId).some(( x ) => gidOf(x) === group && x.status === FAILED ) ) {
|
|
209
|
+
row.status = FAILED; row.placeId = failPlace; row.reason = 'group-failed'; row.leaseUntil = null; row.leaseId = null;
|
|
210
|
+
return { ready: false, failed: true };
|
|
211
|
+
}
|
|
212
|
+
row.placeId = joinPlace; row.status = JOINED; row.groupId = group; row.leaseUntil = null; row.leaseId = null; // park + stamp the grouping key
|
|
213
|
+
return memTryCollect(row.runId, row.recordId, group, joinPlace, foldPlace, expected, survivors);
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// SURVIVORS recovery (C-fail++): dead-letter JUST this shard (a tolerated DROP — groupId kept so the
|
|
217
|
+
// survivors scan counts it), then try to COMPLETE the group over the survivors. The symmetric primitive to
|
|
218
|
+
// failGroup (which fails the WHOLE group): joinFail drops one shard and still folds the rest + a _partial marker.
|
|
219
|
+
joinFail( token, joinPlace, o ) {
|
|
220
|
+
o = o || {};
|
|
221
|
+
const expected = o.expected, foldPlace = o.foldPlace, reason = o.reason;
|
|
222
|
+
const row = rows.get(token && token.id);
|
|
223
|
+
if ( !holds(row, token) ) return null;
|
|
224
|
+
const group = o.group != null ? o.group : row.parentId;
|
|
225
|
+
const failPlace = o.failPlace != null ? o.failPlace : runOf(row.runId).fail;
|
|
226
|
+
row.status = FAILED; row.placeId = failPlace; row.groupId = group; row.reason = reason == null ? 'shard-dropped' : String(reason); row.leaseUntil = null; row.leaseId = null;
|
|
227
|
+
return Object.assign({ failed: 1, group: group == null ? null : String(group) },
|
|
228
|
+
memTryCollect(row.runId, row.recordId, group, joinPlace, foldPlace, expected, true));
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// FAIL-FAST a whole fan-out group: the failing child (held) + all its parked siblings → failed. A still-
|
|
232
|
+
// in-flight sibling fails when it later reaches the join (the joinArrive group-failed guard). So one failed
|
|
233
|
+
// shard fails the record's map-reduce — sound (no silent partial); richer recovery (retry/partial) = C-fail++.
|
|
234
|
+
failGroup( token, joinPlace, failPlace, reason ) {
|
|
235
|
+
const row = rows.get(token && token.id);
|
|
236
|
+
if ( !holds(row, token) ) return null;
|
|
237
|
+
const group = row.parentId;
|
|
238
|
+
row.status = FAILED; row.placeId = failPlace; row.reason = reason == null ? null : String(reason); row.leaseUntil = null; row.leaseId = null;
|
|
239
|
+
let failed = 1;
|
|
240
|
+
for ( const x of rowsOf(row.runId) )
|
|
241
|
+
if ( x.parentId === group && x.id !== row.id && x.status === JOINED ) {
|
|
242
|
+
x.status = FAILED; x.placeId = failPlace; x.reason = 'sibling-failed'; x.leaseUntil = null; x.leaseId = null; failed++;
|
|
243
|
+
}
|
|
244
|
+
return { failed: failed, group: group == null ? null : String(group) };
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
fail( token, reason ) {
|
|
248
|
+
const row = rows.get(token && token.id);
|
|
249
|
+
if ( !holds(row, token) ) return null; // only the lease holder may dead-letter
|
|
250
|
+
row.status = FAILED; row.placeId = runOf(row.runId).fail; row.reason = reason == null ? null : String(reason);
|
|
251
|
+
row.leaseUntil = null; row.leaseId = null;
|
|
252
|
+
return viewToken(row);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
track( token, refs ) {
|
|
256
|
+
const row = rows.get(token && token.id);
|
|
257
|
+
if ( holds(row, token) && refs ) row.created = (row.created || []).concat(refs);
|
|
258
|
+
return this;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
memoGet( key ) { return memo.has(key) ? clone(memo.get(key)) : undefined; },
|
|
262
|
+
memoSet( key, output ) { memo.set(key, clone(output)); return this; },
|
|
263
|
+
|
|
264
|
+
rollbackInflight( runId ) {
|
|
265
|
+
const reset = [], created = [];
|
|
266
|
+
for ( const row of rowsOf(runId) ) {
|
|
267
|
+
if ( row.status !== LEASED ) continue; // only IN-FLIGHT (mid-step crash); done/failed/consumed untouched
|
|
268
|
+
if ( row.created && row.created.length ) created.push.apply(created, row.created);
|
|
269
|
+
row.created = []; row.status = READY; row.leaseUntil = null; row.leaseId = null; // fence out the crashed holder
|
|
270
|
+
reset.push(row.id);
|
|
271
|
+
}
|
|
272
|
+
return { reset, created };
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
marking( runId ) {
|
|
276
|
+
const m = {};
|
|
277
|
+
for ( const row of rowsOf(runId) ) (m[row.placeId] = m[row.placeId] || []).push(viewToken(row));
|
|
278
|
+
return m;
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
stats( runId ) {
|
|
282
|
+
const s = { ready: 0, leased: 0, done: 0, failed: 0, consumed: 0, joined: 0, total: 0 };
|
|
283
|
+
for ( const row of rowsOf(runId) ) { s[row.status] = (s[row.status] || 0) + 1; s.total++; }
|
|
284
|
+
return s;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
close() {},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ===========================================================================================================
|
|
292
|
+
// SQLite impl — the DURABLE DEFAULT. One file via the built-in `node:sqlite` (Node 22+; lazy-required so the
|
|
293
|
+
// experimental module + its warning load ONLY when a host opts into durability). Mirrors the memory contract
|
|
294
|
+
// row-for-row; the marking + content-memo + a lease/claim queue all live in the one transactional file (best
|
|
295
|
+
// C6: zero server). Pluggable to pg-boss/Postgres or BullMQ/Redis at scale behind this same interface.
|
|
296
|
+
// ===========================================================================================================
|
|
297
|
+
|
|
298
|
+
const DDL = [
|
|
299
|
+
'CREATE TABLE IF NOT EXISTS runs (runId TEXT PRIMARY KEY, def TEXT, startPlace TEXT, failPlace TEXT, sinks TEXT)',
|
|
300
|
+
'CREATE TABLE IF NOT EXISTS tokens (' +
|
|
301
|
+
'id INTEGER PRIMARY KEY AUTOINCREMENT, runId TEXT, recordId TEXT, placeId TEXT, status TEXT, ' +
|
|
302
|
+
'payload TEXT, attempts INTEGER, leaseUntil INTEGER, leaseId INTEGER, parentId TEXT, groupId TEXT, created TEXT, reason TEXT)',
|
|
303
|
+
'CREATE INDEX IF NOT EXISTS tokens_run_status ON tokens(runId, status)',
|
|
304
|
+
'CREATE TABLE IF NOT EXISTS memo (key TEXT PRIMARY KEY, output TEXT)',
|
|
305
|
+
'CREATE TABLE IF NOT EXISTS seqs (name TEXT PRIMARY KEY, val INTEGER)',
|
|
306
|
+
].join(';');
|
|
307
|
+
|
|
308
|
+
// a DB row (JSON columns are strings) → the canonical row shape `viewToken`/`holds`/`isClaimable` expect.
|
|
309
|
+
function parseRow( r ) {
|
|
310
|
+
if ( !r ) return null;
|
|
311
|
+
return {
|
|
312
|
+
id: String(r.id), runId: r.runId, recordId: r.recordId, placeId: r.placeId, status: r.status,
|
|
313
|
+
payload: r.payload == null ? null : JSON.parse(r.payload), attempts: r.attempts,
|
|
314
|
+
leaseUntil: r.leaseUntil == null ? null : r.leaseUntil, leaseId: r.leaseId == null ? null : r.leaseId,
|
|
315
|
+
parentId: r.parentId == null ? null : r.parentId, groupId: r.groupId == null ? null : r.groupId,
|
|
316
|
+
created: r.created == null ? [] : JSON.parse(r.created), reason: r.reason == null ? null : r.reason,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function createSqliteCheckpointStore( opts ) {
|
|
321
|
+
opts = opts || {};
|
|
322
|
+
const now = opts.now || Date.now;
|
|
323
|
+
const file = opts.file || ':memory:';
|
|
324
|
+
const startDefault = opts.startPlace || 'start', failDefault = opts.failPlace || 'failed';
|
|
325
|
+
|
|
326
|
+
const { DatabaseSync } = require('node:sqlite'); // lazy: loads the experimental module on opt-in only
|
|
327
|
+
const db = new DatabaseSync(file);
|
|
328
|
+
// DURABILITY POLICY, EXPLICIT (P4): WAL journaling + synchronous=FULL — every commit is fsynced, so BOTH a hard
|
|
329
|
+
// process crash (kill -9, proven in tests/integration/combos-durable-kill-matrix.test.js) AND an OS/power loss
|
|
330
|
+
// replay to the last committed step (no effect lost or duplicated). FULL is SQLite's default; stating it here
|
|
331
|
+
// makes the guarantee a documented decision, not an implicit one. A host may trade the power-loss guarantee for
|
|
332
|
+
// throughput with `synchronous:'NORMAL'` (still process-crash-safe under WAL).
|
|
333
|
+
const sync = String(opts.synchronous || 'FULL').toUpperCase() === 'NORMAL' ? 'NORMAL' : 'FULL';
|
|
334
|
+
db.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = ' + sync + '; PRAGMA foreign_keys = ON');
|
|
335
|
+
db.exec(DDL);
|
|
336
|
+
db.exec("INSERT OR IGNORE INTO seqs(name, val) VALUES ('lease', 0)");
|
|
337
|
+
|
|
338
|
+
const q = {
|
|
339
|
+
getRun: db.prepare('SELECT * FROM runs WHERE runId = ?'),
|
|
340
|
+
putRun: db.prepare('INSERT INTO runs(runId, def, startPlace, failPlace, sinks) VALUES (?,?,?,?,?) ' +
|
|
341
|
+
'ON CONFLICT(runId) DO UPDATE SET def=excluded.def, startPlace=excluded.startPlace, failPlace=excluded.failPlace, sinks=excluded.sinks'),
|
|
342
|
+
insTok: db.prepare('INSERT INTO tokens(runId, recordId, placeId, status, payload, attempts, leaseUntil, leaseId, parentId, groupId, created, reason) ' +
|
|
343
|
+
'VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'),
|
|
344
|
+
byId: db.prepare('SELECT * FROM tokens WHERE id = ?'),
|
|
345
|
+
claimable: db.prepare('SELECT * FROM tokens WHERE runId = ? AND (status = \'ready\' OR (status = \'leased\' AND (leaseUntil IS NULL OR leaseUntil <= ?))) ORDER BY id LIMIT ?'),
|
|
346
|
+
lease: db.prepare('UPDATE tokens SET status=\'leased\', leaseUntil=?, attempts=attempts+1, leaseId=? WHERE id=?'),
|
|
347
|
+
deadLetter: db.prepare('UPDATE tokens SET status=\'failed\', placeId=?, reason=?, leaseUntil=NULL, leaseId=NULL WHERE id=?'),
|
|
348
|
+
relabel: db.prepare('UPDATE tokens SET placeId=?, status=?, leaseUntil=NULL, leaseId=NULL, payload=?, created=? WHERE id=?'),
|
|
349
|
+
consume: db.prepare('UPDATE tokens SET status=\'consumed\', leaseUntil=NULL, leaseId=NULL, payload=?, created=? WHERE id=?'),
|
|
350
|
+
joinPark: db.prepare('UPDATE tokens SET placeId=?, status=\'joined\', groupId=?, leaseUntil=NULL, leaseId=NULL WHERE id=?'),
|
|
351
|
+
joinedOf: db.prepare('SELECT * FROM tokens WHERE runId=? AND groupId=? AND placeId=? AND status=\'joined\' ORDER BY id'),
|
|
352
|
+
atPlaceOf: db.prepare('SELECT id FROM tokens WHERE runId=? AND groupId=? AND placeId=? LIMIT 1'),
|
|
353
|
+
groupFailed: db.prepare('SELECT id FROM tokens WHERE runId=? AND groupId=? AND status=\'failed\' LIMIT 1'),
|
|
354
|
+
failedCountOf: db.prepare('SELECT COUNT(*) n FROM tokens WHERE runId=? AND groupId=? AND status=\'failed\''),
|
|
355
|
+
failAt: db.prepare('UPDATE tokens SET status=\'failed\', placeId=?, reason=?, leaseUntil=NULL, leaseId=NULL WHERE id=?'),
|
|
356
|
+
failAtGroup: db.prepare('UPDATE tokens SET status=\'failed\', placeId=?, groupId=?, reason=?, leaseUntil=NULL, leaseId=NULL WHERE id=?'),
|
|
357
|
+
failSiblings: db.prepare('UPDATE tokens SET status=\'failed\', placeId=?, reason=\'sibling-failed\', leaseUntil=NULL, leaseId=NULL WHERE runId=? AND groupId=? AND status=\'joined\' AND id != ?'),
|
|
358
|
+
setCreated: db.prepare('UPDATE tokens SET created=? WHERE id=?'),
|
|
359
|
+
failTok: db.prepare('UPDATE tokens SET status=\'failed\', placeId=?, reason=?, leaseUntil=NULL, leaseId=NULL WHERE id=?'),
|
|
360
|
+
inflight: db.prepare('SELECT * FROM tokens WHERE runId = ? AND status = \'leased\''),
|
|
361
|
+
resetTok: db.prepare('UPDATE tokens SET status=\'ready\', leaseUntil=NULL, leaseId=NULL, created=\'[]\' WHERE id=?'),
|
|
362
|
+
allOf: db.prepare('SELECT * FROM tokens WHERE runId = ?'),
|
|
363
|
+
statsOf: db.prepare('SELECT status, COUNT(*) n FROM tokens WHERE runId = ? GROUP BY status'),
|
|
364
|
+
bumpLease: db.prepare('UPDATE seqs SET val = val + 1 WHERE name = \'lease\''),
|
|
365
|
+
getLease: db.prepare('SELECT val FROM seqs WHERE name = \'lease\''),
|
|
366
|
+
memoGet: db.prepare('SELECT output FROM memo WHERE key = ?'),
|
|
367
|
+
memoSet: db.prepare('INSERT INTO memo(key, output) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET output=excluded.output'),
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
function getRunCfg( runId, create ) {
|
|
371
|
+
const r = q.getRun.get(runId);
|
|
372
|
+
if ( r ) return { start: r.startPlace || startDefault, fail: r.failPlace || failDefault, sinks: new Set(JSON.parse(r.sinks || '[]')) };
|
|
373
|
+
if ( create ) { q.putRun.run(runId, '{}', startDefault, failDefault, '[]'); return { start: startDefault, fail: failDefault, sinks: new Set() }; }
|
|
374
|
+
return { start: startDefault, fail: failDefault, sinks: new Set() };
|
|
375
|
+
}
|
|
376
|
+
function nextLease() { q.bumpLease.run(); return q.getLease.get().val; }
|
|
377
|
+
function tx( fn ) { db.exec('BEGIN IMMEDIATE'); try { const r = fn(); db.exec('COMMIT'); return r; } catch ( e ) { db.exec('ROLLBACK'); throw e; } }
|
|
378
|
+
function insert( runId, recordId, placeId, status, payload, parentId, groupId ) {
|
|
379
|
+
const gid = groupId !== undefined ? groupId : parentId; // groupId defaults to parentId (1-level byte-identical)
|
|
380
|
+
const info = q.insTok.run(runId, recordId, placeId, status, JSON.stringify(payload == null ? null : payload),
|
|
381
|
+
0, null, null, parentId == null ? null : String(parentId), gid == null ? null : String(gid), '[]', null);
|
|
382
|
+
return viewToken(parseRow(q.byId.get(info.lastInsertRowid)));
|
|
383
|
+
}
|
|
384
|
+
// shared fan-in completion (sqlite) — the symmetric helper to memTryCollect, inside the caller's tx() so the
|
|
385
|
+
// completion check + the single collector spawn are ATOMIC (exactly one arrival/drop can complete the group).
|
|
386
|
+
function sqlTryCollect( runId, recordId, group, joinPlace, foldPlace, expected, survivors ) {
|
|
387
|
+
const cfg = getRunCfg(runId, false);
|
|
388
|
+
const arrived = q.joinedOf.all(runId, group, joinPlace);
|
|
389
|
+
const dropped = survivors ? (q.failedCountOf.get(runId, group).n || 0) : 0;
|
|
390
|
+
if ( group == null || expected == null || (arrived.length + dropped) < expected )
|
|
391
|
+
return { ready: false, arrived: arrived.length, expected: expected };
|
|
392
|
+
if ( q.atPlaceOf.get(runId, group, foldPlace) ) // already collected — exactly one spawn
|
|
393
|
+
return { ready: false, arrived: arrived.length, expected: expected, collected: true };
|
|
394
|
+
const siblings = arrived.map(( dbRow ) => parseRow(dbRow).payload); // id-order = fan-out order
|
|
395
|
+
const payload = survivors
|
|
396
|
+
? { _group: String(group), _n: expected, _siblings: siblings, _expected: expected, _dropped: dropped, _partial: dropped > 0 }
|
|
397
|
+
: { _group: String(group), _n: expected, _siblings: siblings };
|
|
398
|
+
const collector = insert(runId, recordId, foldPlace, cfg.sinks.has(foldPlace) ? DONE : READY, payload, group);
|
|
399
|
+
return { ready: true, collector: collector, n: siblings.length, partial: dropped > 0 };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
ensureRun( runId, def ) {
|
|
404
|
+
def = def || {};
|
|
405
|
+
q.putRun.run(runId, JSON.stringify(def), def.start || startDefault, def.fail || failDefault, JSON.stringify(def.sinks || []));
|
|
406
|
+
return this;
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
inject( runId, records ) {
|
|
410
|
+
const cfg = getRunCfg(runId, true);
|
|
411
|
+
return tx(() => (records || []).map(( rec ) => {
|
|
412
|
+
const rid = rec && (rec.recordId != null ? rec.recordId : rec.id);
|
|
413
|
+
const recordId = rid == null ? null : String(rid); // a numeric id bound to a TEXT column else coerces to '0.0'
|
|
414
|
+
const payload = rec && rec.payload !== undefined ? rec.payload : rec;
|
|
415
|
+
return insert(runId, recordId, cfg.start, READY, payload, null);
|
|
416
|
+
}));
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
claim( runId, o ) {
|
|
420
|
+
o = o || {};
|
|
421
|
+
const lease = o.lease || 30000, limit = o.limit || 1, maxAttempts = o.maxAttempts || Infinity;
|
|
422
|
+
const t = now(), cfg = getRunCfg(runId, false);
|
|
423
|
+
return tx(() => {
|
|
424
|
+
const cand = q.claimable.all(runId, t, limit);
|
|
425
|
+
const out = [];
|
|
426
|
+
for ( const dbRow of cand ) {
|
|
427
|
+
if ( dbRow.attempts >= maxAttempts ) { q.deadLetter.run(cfg.fail, 'max-attempts', dbRow.id); continue; }
|
|
428
|
+
const lid = nextLease();
|
|
429
|
+
q.lease.run(t + lease, lid, dbRow.id);
|
|
430
|
+
out.push(viewToken(parseRow(q.byId.get(dbRow.id))));
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
move( token, toPlace, o ) {
|
|
437
|
+
o = o || {};
|
|
438
|
+
return tx(() => {
|
|
439
|
+
const row = parseRow(q.byId.get(token && token.id));
|
|
440
|
+
if ( !holds(row, token) ) return null;
|
|
441
|
+
const cfg = getRunCfg(row.runId, false);
|
|
442
|
+
const created = o.created ? row.created.concat(o.created) : row.created;
|
|
443
|
+
const payload = o.payload ? Object.assign({}, row.payload, o.payload) : row.payload;
|
|
444
|
+
if ( Array.isArray(toPlace) ) {
|
|
445
|
+
q.consume.run(JSON.stringify(payload == null ? null : payload), JSON.stringify(created), token.id);
|
|
446
|
+
const pl = o.payloads; // optional per-child payloads (the map elem case)
|
|
447
|
+
return toPlace.map(( place, i ) => insert(row.runId, row.recordId, place, cfg.sinks.has(place) ? DONE : READY, pl ? pl[i] : payload, row.id));
|
|
448
|
+
}
|
|
449
|
+
const status = cfg.sinks.has(toPlace) ? DONE : READY;
|
|
450
|
+
q.relabel.run(toPlace, status, JSON.stringify(payload == null ? null : payload), JSON.stringify(created), token.id);
|
|
451
|
+
return viewToken(parseRow(q.byId.get(token.id)));
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
joinArrive( token, joinPlace, o ) {
|
|
456
|
+
o = o || {};
|
|
457
|
+
const expected = o.expected, foldPlace = o.foldPlace, failPlace = o.failPlace, survivors = o.mode === 'survivors';
|
|
458
|
+
return tx(() => {
|
|
459
|
+
const row = parseRow(q.byId.get(token && token.id));
|
|
460
|
+
if ( !holds(row, token) ) return null; // fencing
|
|
461
|
+
const group = o.group != null ? String(o.group) : row.parentId; // NESTED: top of the _groups stack
|
|
462
|
+
// FAIL-FAST (failfast mode only): a sibling already failed → this arrival joins the failure. SURVIVORS
|
|
463
|
+
// tolerates a dropped sibling and folds the rest + a _partial marker.
|
|
464
|
+
if ( !survivors && failPlace != null && group != null && q.groupFailed.get(row.runId, group) ) {
|
|
465
|
+
q.failAt.run(failPlace, 'group-failed', token.id);
|
|
466
|
+
return { ready: false, failed: true };
|
|
467
|
+
}
|
|
468
|
+
q.joinPark.run(joinPlace, group, token.id); // park + stamp the grouping key (nested-safe)
|
|
469
|
+
return sqlTryCollect(row.runId, row.recordId, group, joinPlace, foldPlace, expected, survivors);
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
// SURVIVORS recovery (C-fail++): dead-letter JUST this shard (a tolerated DROP, parentId kept), then try to
|
|
474
|
+
// complete the group over the survivors — the symmetric primitive to failGroup, atomic inside one tx().
|
|
475
|
+
joinFail( token, joinPlace, o ) {
|
|
476
|
+
o = o || {};
|
|
477
|
+
const expected = o.expected, foldPlace = o.foldPlace, reason = o.reason;
|
|
478
|
+
return tx(() => {
|
|
479
|
+
const row = parseRow(q.byId.get(token && token.id));
|
|
480
|
+
if ( !holds(row, token) ) return null;
|
|
481
|
+
const group = o.group != null ? String(o.group) : row.parentId;
|
|
482
|
+
const failPlace = o.failPlace != null ? o.failPlace : getRunCfg(row.runId, false).fail;
|
|
483
|
+
q.failAtGroup.run(failPlace, group, reason == null ? 'shard-dropped' : String(reason), token.id); // stamp groupId so the survivors scan counts the drop
|
|
484
|
+
return Object.assign({ failed: 1, group: group == null ? null : String(group) },
|
|
485
|
+
sqlTryCollect(row.runId, row.recordId, group, joinPlace, foldPlace, expected, true));
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
failGroup( token, joinPlace, failPlace, reason ) {
|
|
490
|
+
return tx(() => {
|
|
491
|
+
const row = parseRow(q.byId.get(token && token.id));
|
|
492
|
+
if ( !holds(row, token) ) return null;
|
|
493
|
+
const group = row.parentId;
|
|
494
|
+
q.failAt.run(failPlace, reason == null ? null : String(reason), token.id);
|
|
495
|
+
let failed = 1;
|
|
496
|
+
if ( group != null ) { const info = q.failSiblings.run(failPlace, row.runId, group, token.id); failed += (info.changes || 0); }
|
|
497
|
+
return { failed: failed, group: group == null ? null : String(group) };
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
fail( token, reason ) {
|
|
502
|
+
return tx(() => {
|
|
503
|
+
const row = parseRow(q.byId.get(token && token.id));
|
|
504
|
+
if ( !holds(row, token) ) return null;
|
|
505
|
+
q.failTok.run(getRunCfg(row.runId, false).fail, reason == null ? null : String(reason), token.id);
|
|
506
|
+
return viewToken(parseRow(q.byId.get(token.id)));
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
track( token, refs ) {
|
|
511
|
+
tx(() => {
|
|
512
|
+
const row = parseRow(q.byId.get(token && token.id));
|
|
513
|
+
if ( holds(row, token) && refs ) q.setCreated.run(JSON.stringify(row.created.concat(refs)), token.id);
|
|
514
|
+
});
|
|
515
|
+
return this;
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
memoGet( key ) { const r = q.memoGet.get(key); return r ? JSON.parse(r.output) : undefined; },
|
|
519
|
+
memoSet( key, output ) { q.memoSet.run(key, JSON.stringify(output == null ? null : output)); return this; },
|
|
520
|
+
|
|
521
|
+
rollbackInflight( runId ) {
|
|
522
|
+
return tx(() => {
|
|
523
|
+
const reset = [], created = [];
|
|
524
|
+
for ( const dbRow of q.inflight.all(runId) ) {
|
|
525
|
+
const row = parseRow(dbRow);
|
|
526
|
+
if ( row.created && row.created.length ) created.push.apply(created, row.created);
|
|
527
|
+
q.resetTok.run(row.id); // ready, leaseId/leaseUntil null, created cleared
|
|
528
|
+
reset.push(row.id);
|
|
529
|
+
}
|
|
530
|
+
return { reset, created };
|
|
531
|
+
});
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
marking( runId ) {
|
|
535
|
+
const m = {};
|
|
536
|
+
for ( const dbRow of q.allOf.all(runId) ) { const tk = viewToken(parseRow(dbRow)); (m[tk.placeId] = m[tk.placeId] || []).push(tk); }
|
|
537
|
+
return m;
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
stats( runId ) {
|
|
541
|
+
const s = { ready: 0, leased: 0, done: 0, failed: 0, consumed: 0, joined: 0, total: 0 };
|
|
542
|
+
for ( const r of q.statsOf.all(runId) ) { s[r.status] = r.n; s.total += r.n; }
|
|
543
|
+
return s;
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
close() { try { db.close(); } catch ( e ) {} },
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
module.exports = { createMemoryCheckpointStore, createSqliteCheckpointStore, statuses: { READY, LEASED, DONE, FAILED, CONSUMED, JOINED } };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Nathanael Braun
|
|
3
|
+
* @author : Nathanael BRAUN <pp9ping@gmail.com>
|
|
4
|
+
*
|
|
5
|
+
* AGPL-3.0-or-later. See <https://www.gnu.org/licenses/>.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
/**
|
|
9
|
+
* fold — the PURE half of the durable executor's fan-in JOIN (the JTMS-at-merge point, study §9.3). When a
|
|
10
|
+
* `map` fan-out's children rejoin (`checkpoint-store.js#joinArrive` spawns ONE collector carrying the sibling
|
|
11
|
+
* payloads), Layer B reduces them to a single value. Two reduce kinds:
|
|
12
|
+
* · a declared MONOID (this module) — a pure, content-memoizable tree-reduce: `{ monoid, key?, into? }`;
|
|
13
|
+
* · a micro-TASK (handled in `interpreter.js`) — a small-LLM reconciliation when the join is not pure logic
|
|
14
|
+
* (§0.1: "some joins may be small-LLM micro-tasks rather than pure logic").
|
|
15
|
+
*
|
|
16
|
+
* "DECLARE THE ACC ALGEBRA" (study §9.3 / §12): the fold loop is delegated to `semiring.js#reduceSemiring`, which
|
|
17
|
+
* is the coherence-theorem-validated commutative-monoid REDUCE (E1: monoid variance ≈ 0). So a fold over a
|
|
18
|
+
* COMMUTATIVE monoid is ORDER-INDEPENDENT — the durable executor's cross-record interleaving is non-deterministic
|
|
19
|
+
* THROUGHPUT, yet the folded BELIEF is deterministic. For the two NON-commutative monoids here (concat / merge)
|
|
20
|
+
* we sort the siblings by their element index `_i` first, so the result is deterministic regardless of the
|
|
21
|
+
* (non-semantic) arrival order. Either way: non-deterministic schedule, deterministic value.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { reduceSemiring } = require('../providers/semiring.js');
|
|
25
|
+
|
|
26
|
+
// Each monoid: prep (per-value normaliser) + plus (⊕) + zero (a THUNK — a fresh identity per fold, never a
|
|
27
|
+
// shared mutable) + commutative (does arrival order matter?). All are associative; commutative ones inherit
|
|
28
|
+
// reduceSemiring's order-independence, the rest are made deterministic by the `_i` sort in foldSiblings.
|
|
29
|
+
const MONOIDS = {
|
|
30
|
+
sum: { prep: Number, plus: ( a, b ) => a + b, zero: () => 0, commutative: true },
|
|
31
|
+
product: { prep: Number, plus: ( a, b ) => a * b, zero: () => 1, commutative: true },
|
|
32
|
+
min: { prep: Number, plus: ( a, b ) => Math.min(a, b), zero: () => Infinity, commutative: true },
|
|
33
|
+
max: { prep: Number, plus: ( a, b ) => Math.max(a, b), zero: () => -Infinity, commutative: true },
|
|
34
|
+
count: { prep: () => 1, plus: ( a, b ) => a + b, zero: () => 0, commutative: true },
|
|
35
|
+
and: { prep: ( v ) => !!v, plus: ( a, b ) => !!(a && b), zero: () => true, commutative: true },
|
|
36
|
+
or: { prep: ( v ) => !!v, plus: ( a, b ) => !!(a || b), zero: () => false, commutative: true },
|
|
37
|
+
concat: { prep: ( v ) => (Array.isArray(v) ? v : [v]), plus: ( a, b ) => a.concat(b), zero: () => [], commutative: false },
|
|
38
|
+
merge: { prep: ( v ) => (v && typeof v === 'object' ? v : {}), plus: ( a, b ) => Object.assign({}, a, b), zero: () => ({}), commutative: false },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function monoids() { return Object.keys(MONOIDS); }
|
|
42
|
+
function isCommutative( name ) { return !!(MONOIDS[name] && MONOIDS[name].commutative); }
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reduce the collected sibling payloads to one output object via a declared monoid.
|
|
46
|
+
* @param siblings the fan-out children's payloads (the collector token's `_siblings`)
|
|
47
|
+
* @param reduce { monoid:'<name>', key?:'<factKey to fold; default the whole payload>', into?:'<output key>' }
|
|
48
|
+
* @returns { [into]: <folded value>, _n } — the bounded projection (N inputs → 1 value)
|
|
49
|
+
*/
|
|
50
|
+
function foldSiblings( siblings, reduce ) {
|
|
51
|
+
const m = MONOIDS[reduce && reduce.monoid];
|
|
52
|
+
if ( !m ) throw new Error('foldSiblings: unknown monoid "' + (reduce && reduce.monoid) + '" (have: ' + monoids().join(',') + ')');
|
|
53
|
+
const key = reduce.key, into = reduce.into || reduce.key || 'result';
|
|
54
|
+
// sort by element index for a deterministic fold even under a NON-commutative ⊕ (arrival order is non-semantic)
|
|
55
|
+
const ordered = (siblings || []).slice().sort(( a, b ) => ((a && a._i) || 0) - ((b && b._i) || 0));
|
|
56
|
+
const values = ordered.map(( p ) => m.prep(key == null ? p : (p == null ? undefined : p[key])));
|
|
57
|
+
const r = reduceSemiring(values, { plus: m.plus, zero: m.zero() }); // the declared acc algebra (semiring.js)
|
|
58
|
+
const out = {}; out[into] = r.acc; out._n = ordered.length;
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { foldSiblings, monoids, isCommutative, MONOIDS };
|