Shared State
What It Is
Shared state is how multiple nodes, embedded DAGs, scatter clones, or turns accumulate into the same durable structure without threading every value through inputs, outputs, and gather mappings.
In Dagonizer, shared state is explicit: create a Store, inject it into the nodes that need it, and checkpoint it when the store must resume alongside the parent state. The DAG stays pure topology; the shared service is ordinary constructor wiring.
How It Works
Stores are injected into node constructors and live outside the JSON-LD topology. Nodes read and write the same store instance while the DAG remains a graph of placements and routes. Checkpoint capture can snapshot named stores beside state so resume restores both control flow and shared data.
Two mechanisms cross the scatter boundary in Dagonizer. The choice depends on the data-flow shape.
Diagrams, Examples, and Outputs
The Archivist is the full browser example: parent and embedded DAG placements share graph-backed memory through injected services while the topology remains JSON-LD.
The Archivist parent DAG
55 placements{
"@context": {
"@version": 1.1,
"name": {
"@id": "https://noocodec.dev/ontology/dag/name"
},
"version": {
"@id": "https://noocodec.dev/ontology/dag/version"
},
"entrypoints": {
"@id": "https://noocodec.dev/ontology/dag/entrypoints",
"@container": "@index"
},
"nodes": {
"@id": "https://noocodec.dev/ontology/dag/nodes",
"@container": "@set"
},
"outputs": {
"@id": "https://noocodec.dev/ontology/dag/outputs"
},
"node": {
"@id": "https://noocodec.dev/ontology/dag/node"
},
"dag": {
"@id": "https://noocodec.dev/ontology/dag/dag"
},
"body": {
"@id": "https://noocodec.dev/ontology/dag/body"
},
"source": {
"@id": "https://noocodec.dev/ontology/dag/source"
},
"sources": {
"@id": "https://noocodec.dev/ontology/dag/sources",
"@container": "@index"
},
"itemKey": {
"@id": "https://noocodec.dev/ontology/dag/itemKey"
},
"execution": {
"@id": "https://noocodec.dev/ontology/dag/execution"
},
"concurrency": {
"@id": "https://noocodec.dev/ontology/dag/concurrency"
},
"throttle": {
"@id": "https://noocodec.dev/ontology/dag/throttle"
},
"reservoir": {
"@id": "https://noocodec.dev/ontology/dag/reservoir"
},
"gather": {
"@id": "https://noocodec.dev/ontology/dag/gather"
},
"dagReference": {
"@id": "https://noocodec.dev/ontology/dag/dagReference",
"@type": "@id"
},
"DagReference": {
"@id": "https://noocodec.dev/ontology/dag/DagReference"
},
"from": {
"@id": "https://noocodec.dev/ontology/dag/from"
},
"path": {
"@id": "https://noocodec.dev/ontology/dag/path"
},
"candidates": {
"@id": "https://noocodec.dev/ontology/dag/candidates",
"@container": "@set"
},
"candidateDag": {
"@id": "https://noocodec.dev/ontology/dag/candidateDag",
"@type": "@id"
},
"selectedDag": {
"@id": "https://noocodec.dev/ontology/dag/selectedDag",
"@type": "@id"
},
"resultField": {
"@id": "https://noocodec.dev/ontology/dag/resultField"
},
"policy": {
"@id": "https://noocodec.dev/ontology/dag/policy"
},
"reducer": {
"@id": "https://noocodec.dev/ontology/dag/reducer"
},
"outcome": {
"@id": "https://noocodec.dev/ontology/dag/outcome"
},
"phase": {
"@id": "https://noocodec.dev/ontology/dag/phase"
},
"stateMapping": {
"@id": "https://noocodec.dev/ontology/dag/stateMapping"
},
"container": {
"@id": "https://noocodec.dev/ontology/dag/container"
},
"DAG": {
"@id": "https://noocodec.dev/ontology/dag/DAG"
},
"Placement": {
"@id": "https://noocodec.dev/ontology/dag/Placement"
},
"SingleNode": {
"@id": "https://noocodec.dev/ontology/dag/SingleNode"
},
"ScatterNode": {
"@id": "https://noocodec.dev/ontology/dag/ScatterNode"
},
"EmbeddedDAGNode": {
"@id": "https://noocodec.dev/ontology/dag/EmbeddedDAGNode"
},
"GatherNode": {
"@id": "https://noocodec.dev/ontology/dag/GatherNode"
},
"TerminalNode": {
"@id": "https://noocodec.dev/ontology/dag/TerminalNode"
},
"PhaseNode": {
"@id": "https://noocodec.dev/ontology/dag/PhaseNode"
}
},
"@id": "urn:noocodec:dag:the-archivist",
"@type": "DAG",
"name": "the-archivist",
"version": "6.0",
"entrypoints": {
"main": "urn:noocodec:dag:the-archivist/node/park-for-input"
},
"nodes": [
{
"@id": "urn:noocodec:dag:the-archivist/node/setup",
"@type": "PhaseNode",
"name": "setup",
"node": "urn:noocodec:node:pre-run-setup",
"phase": "pre"
},
{
"@id": "urn:noocodec:dag:the-archivist/node/park-for-input",
"@type": "SingleNode",
"name": "park-for-input",
"node": "urn:noocodec:node:park-for-input",
"outputs": {
"parked": "urn:noocodec:dag:the-archivist/node/park-for-input",
"resumed": "urn:noocodec:dag:the-archivist/node/recall-context"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recall-context",
"@type": "SingleNode",
"name": "recall-context",
"node": "urn:noocodec:node:recall-context",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/classify-intent"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/classify-intent",
"@type": "SingleNode",
"name": "classify-intent",
"node": "urn:noocodec:node:classify-intent",
"outputs": {
"lookup-author": "urn:noocodec:dag:the-archivist/node/author-search",
"find-reviews": "urn:noocodec:dag:the-archivist/node/reviews-extract",
"describe-book": "urn:noocodec:dag:the-archivist/node/describe-extract",
"recommend-similar": "urn:noocodec:dag:the-archivist/node/recommend-similar",
"recall-memories": "urn:noocodec:dag:the-archivist/node/memory-recall",
"on-topic": "urn:noocodec:dag:the-archivist/node/on-topic-search",
"recommend-top-rated": "urn:noocodec:dag:the-archivist/node/recommend-extract",
"off-topic": "urn:noocodec:dag:the-archivist/node/decline-off-topic",
"retry": "urn:noocodec:dag:the-archivist/node/classify-intent",
"salvage": "urn:noocodec:dag:the-archivist/node/classify-intent-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/classify-intent-salvage",
"@type": "SingleNode",
"name": "classify-intent-salvage",
"node": "urn:noocodec:node:classify-intent-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/on-topic-search"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/on-topic-search",
"@type": "EmbeddedDAGNode",
"name": "on-topic-search",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/compose-loop",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"dag": "urn:noocodec:dag:book-search-scatter",
"stateMapping": {
"output": {
"terms": "terms",
"toolPlan": "toolPlan",
"candidates": "candidates",
"shortlist": "shortlist",
"priorContext": "priorContext",
"failureCause": "failureCause"
}
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/author-search",
"@type": "EmbeddedDAGNode",
"name": "author-search",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/group-by-year",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"dag": "urn:noocodec:dag:book-search-scatter",
"stateMapping": {
"output": {
"terms": "terms",
"toolPlan": "toolPlan",
"candidates": "candidates",
"shortlist": "shortlist",
"priorContext": "priorContext",
"failureCause": "failureCause"
}
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/group-by-year",
"@type": "SingleNode",
"name": "group-by-year",
"node": "urn:noocodec:node:group-by-year",
"outputs": {
"ordered": "urn:noocodec:dag:the-archivist/node/compose-loop"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-extract",
"@type": "SingleNode",
"name": "reviews-extract",
"node": "urn:noocodec:node:extract-query",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
"retry": "urn:noocodec:dag:the-archivist/node/reviews-extract",
"salvage": "urn:noocodec:dag:the-archivist/node/reviews-extract-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-extract-salvage",
"@type": "SingleNode",
"name": "reviews-extract-salvage",
"node": "urn:noocodec:node:extract-query-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
"@type": "SingleNode",
"name": "reviews-decide-tools",
"node": "urn:noocodec:node:decide-tools",
"outputs": {
"tools": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
"no-tools": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
"retry": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
"salvage": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools-salvage",
"@type": "SingleNode",
"name": "reviews-decide-tools-salvage",
"node": "urn:noocodec:node:decide-tools-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
"@type": "SingleNode",
"name": "reviews-build-worksets",
"node": "urn:noocodec:node:build-book-worksets",
"outputs": {
"ready": "urn:noocodec:dag:the-archivist/node/reviews-scatter"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-scatter",
"@type": "ScatterNode",
"name": "reviews-scatter",
"source": "bookWorksets",
"body": {
"dag": {
"@type": "DagReference",
"from": "item",
"path": "dagIri",
"candidates": [
"urn:noocodec:tool:web_search_books",
"urn:noocodec:tool:google_books_search",
"urn:noocodec:tool:subject_search",
"urn:noocodec:tool:wikipedia_summary"
]
}
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/reviews-gather",
"error": "urn:noocodec:dag:the-archivist/node/reviews-gather",
"empty": "urn:noocodec:dag:the-archivist/node/reviews-rank"
},
"itemKey": "currentItem",
"reducer": "any-success",
"execution": {
"mode": "item",
"concurrency": 4
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-gather",
"@type": "GatherNode",
"name": "reviews-gather",
"sources": {
"urn:noocodec:dag:the-archivist/node/reviews-scatter": {}
},
"gather": {
"strategy": "tool-candidate-merge"
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/reviews-rank",
"error": "urn:noocodec:dag:the-archivist/node/reviews-rank",
"empty": "urn:noocodec:dag:the-archivist/node/reviews-rank"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-rank",
"@type": "SingleNode",
"name": "reviews-rank",
"node": "urn:noocodec:node:rank-by-rating",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/reviews-merge"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-merge",
"@type": "SingleNode",
"name": "reviews-merge",
"node": "urn:noocodec:node:merge-candidates",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/reviews-record",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-record",
"@type": "SingleNode",
"name": "reviews-record",
"node": "urn:noocodec:node:record-findings",
"outputs": {
"recorded": "urn:noocodec:dag:the-archivist/node/reviews-gate"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-gate",
"@type": "SingleNode",
"name": "reviews-gate",
"node": "urn:noocodec:node:has-citations-gate",
"outputs": {
"pass": "urn:noocodec:dag:the-archivist/node/reviews-recall",
"fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-recall",
"@type": "SingleNode",
"name": "reviews-recall",
"node": "urn:noocodec:node:recall-past-visits",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-extract",
"@type": "SingleNode",
"name": "recommend-extract",
"node": "urn:noocodec:node:extract-query",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
"retry": "urn:noocodec:dag:the-archivist/node/recommend-extract",
"salvage": "urn:noocodec:dag:the-archivist/node/recommend-extract-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-extract-salvage",
"@type": "SingleNode",
"name": "recommend-extract-salvage",
"node": "urn:noocodec:node:extract-query-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
"@type": "SingleNode",
"name": "recommend-decide-tools",
"node": "urn:noocodec:node:decide-tools",
"outputs": {
"tools": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
"no-tools": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
"retry": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
"salvage": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools-salvage",
"@type": "SingleNode",
"name": "recommend-decide-tools-salvage",
"node": "urn:noocodec:node:decide-tools-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
"@type": "SingleNode",
"name": "recommend-build-worksets",
"node": "urn:noocodec:node:build-book-worksets",
"outputs": {
"ready": "urn:noocodec:dag:the-archivist/node/recommend-scatter"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-scatter",
"@type": "ScatterNode",
"name": "recommend-scatter",
"source": "bookWorksets",
"body": {
"dag": {
"@type": "DagReference",
"from": "item",
"path": "dagIri",
"candidates": [
"urn:noocodec:tool:web_search_books",
"urn:noocodec:tool:google_books_search",
"urn:noocodec:tool:subject_search",
"urn:noocodec:tool:wikipedia_summary"
]
}
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/recommend-gather",
"error": "urn:noocodec:dag:the-archivist/node/recommend-gather",
"empty": "urn:noocodec:dag:the-archivist/node/recommend-rank"
},
"itemKey": "currentItem",
"reducer": "any-success",
"execution": {
"mode": "item",
"concurrency": 4
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-gather",
"@type": "GatherNode",
"name": "recommend-gather",
"sources": {
"urn:noocodec:dag:the-archivist/node/recommend-scatter": {}
},
"gather": {
"strategy": "tool-candidate-merge"
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/recommend-rank",
"error": "urn:noocodec:dag:the-archivist/node/recommend-rank",
"empty": "urn:noocodec:dag:the-archivist/node/recommend-rank"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-rank",
"@type": "SingleNode",
"name": "recommend-rank",
"node": "urn:noocodec:node:rank-by-rating",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/recommend-merge"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-merge",
"@type": "SingleNode",
"name": "recommend-merge",
"node": "urn:noocodec:node:merge-candidates",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/recommend-record",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-record",
"@type": "SingleNode",
"name": "recommend-record",
"node": "urn:noocodec:node:record-findings",
"outputs": {
"recorded": "urn:noocodec:dag:the-archivist/node/recommend-gate"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-gate",
"@type": "SingleNode",
"name": "recommend-gate",
"node": "urn:noocodec:node:has-citations-gate",
"outputs": {
"pass": "urn:noocodec:dag:the-archivist/node/recommend-recall",
"fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-recall",
"@type": "SingleNode",
"name": "recommend-recall",
"node": "urn:noocodec:node:recall-past-visits",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-extract",
"@type": "SingleNode",
"name": "describe-extract",
"node": "urn:noocodec:node:extract-query",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
"retry": "urn:noocodec:dag:the-archivist/node/describe-extract",
"salvage": "urn:noocodec:dag:the-archivist/node/describe-extract-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-extract-salvage",
"@type": "SingleNode",
"name": "describe-extract-salvage",
"node": "urn:noocodec:node:extract-query-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/describe-decide-tools"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
"@type": "SingleNode",
"name": "describe-decide-tools",
"node": "urn:noocodec:node:decide-tools",
"outputs": {
"tools": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
"no-tools": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
"retry": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
"salvage": "urn:noocodec:dag:the-archivist/node/describe-decide-tools-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-decide-tools-salvage",
"@type": "SingleNode",
"name": "describe-decide-tools-salvage",
"node": "urn:noocodec:node:decide-tools-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/describe-build-worksets"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
"@type": "SingleNode",
"name": "describe-build-worksets",
"node": "urn:noocodec:node:build-book-worksets",
"outputs": {
"ready": "urn:noocodec:dag:the-archivist/node/describe-scatter"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-scatter",
"@type": "ScatterNode",
"name": "describe-scatter",
"source": "bookWorksets",
"body": {
"dag": {
"@type": "DagReference",
"from": "item",
"path": "dagIri",
"candidates": [
"urn:noocodec:tool:web_search_books",
"urn:noocodec:tool:google_books_search",
"urn:noocodec:tool:subject_search",
"urn:noocodec:tool:wikipedia_summary"
]
}
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/describe-gather",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"itemKey": "currentItem",
"reducer": "any-success",
"execution": {
"mode": "item",
"concurrency": 4
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-gather",
"@type": "GatherNode",
"name": "describe-gather",
"sources": {
"urn:noocodec:dag:the-archivist/node/describe-scatter": {}
},
"gather": {
"strategy": "tool-candidate-merge"
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/describe-pick",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-pick",
"@type": "SingleNode",
"name": "describe-pick",
"node": "urn:noocodec:node:pick-best-match",
"outputs": {
"picked": "urn:noocodec:dag:the-archivist/node/describe-merge"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-merge",
"@type": "SingleNode",
"name": "describe-merge",
"node": "urn:noocodec:node:merge-candidates",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/describe-record",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-record",
"@type": "SingleNode",
"name": "describe-record",
"node": "urn:noocodec:node:record-findings",
"outputs": {
"recorded": "urn:noocodec:dag:the-archivist/node/describe-gate"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-gate",
"@type": "SingleNode",
"name": "describe-gate",
"node": "urn:noocodec:node:has-citations-gate",
"outputs": {
"pass": "urn:noocodec:dag:the-archivist/node/describe-recall",
"fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-recall",
"@type": "SingleNode",
"name": "describe-recall",
"node": "urn:noocodec:node:recall-past-visits",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-similar",
"@type": "SingleNode",
"name": "recommend-similar",
"node": "urn:noocodec:node:recommend-similar",
"outputs": {
"seeded": "urn:noocodec:dag:the-archivist/node/similar-search",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/similar-search",
"@type": "EmbeddedDAGNode",
"name": "similar-search",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/compose-loop",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"dag": "urn:noocodec:dag:book-search-scatter",
"stateMapping": {
"output": {
"terms": "terms",
"toolPlan": "toolPlan",
"candidates": "candidates",
"shortlist": "shortlist",
"priorContext": "priorContext",
"failureCause": "failureCause"
}
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-loop",
"@type": "EmbeddedDAGNode",
"name": "compose-loop",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"dag": "urn:noocodec:dag:compose-retry-loop",
"stateMapping": {
"output": {
"draft": "draft",
"approvalState": "approvalState"
}
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
"@type": "SingleNode",
"name": "respond-to-visitor",
"node": "urn:noocodec:node:respond-to-visitor",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/end"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/memory-recall",
"@type": "SingleNode",
"name": "memory-recall",
"node": "urn:noocodec:node:recall-memories",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/compose-memory-recall"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-memory-recall",
"@type": "SingleNode",
"name": "compose-memory-recall",
"node": "urn:noocodec:node:compose-memory-response",
"outputs": {
"drafted": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
"retry": "urn:noocodec:dag:the-archivist/node/compose-memory-recall",
"salvage": "urn:noocodec:dag:the-archivist/node/compose-memory-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-memory-salvage",
"@type": "SingleNode",
"name": "compose-memory-salvage",
"node": "urn:noocodec:node:compose-memory-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/respond-to-visitor"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/decline-off-topic",
"@type": "SingleNode",
"name": "decline-off-topic",
"node": "urn:noocodec:node:decline-off-topic",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/end"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-empty",
"@type": "SingleNode",
"name": "compose-empty",
"node": "urn:noocodec:node:compose-empty",
"outputs": {
"drafted": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
"retry": "urn:noocodec:dag:the-archivist/node/compose-empty",
"salvage": "urn:noocodec:dag:the-archivist/node/compose-empty-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-empty-salvage",
"@type": "SingleNode",
"name": "compose-empty-salvage",
"node": "urn:noocodec:node:compose-empty-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/respond-to-visitor"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/end",
"@type": "TerminalNode",
"name": "end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% the-archivist (v6.0)
entry_main(["main"])
entry_main --> urn_noocodec_dag_the-archivist/node/park-for-input
urn_noocodec_dag_the-archivist/node/setup(["setup (pre)"])
urn_noocodec_dag_the-archivist/node/park-for-input["park-for-input"]
urn_noocodec_dag_the-archivist/node/park-for-input -->|parked| urn_noocodec_dag_the-archivist/node/park-for-input
urn_noocodec_dag_the-archivist/node/park-for-input -->|resumed| urn_noocodec_dag_the-archivist/node/recall-context
urn_noocodec_dag_the-archivist/node/recall-context["recall-context"]
urn_noocodec_dag_the-archivist/node/recall-context -->|recalled| urn_noocodec_dag_the-archivist/node/classify-intent
urn_noocodec_dag_the-archivist/node/classify-intent["classify-intent"]
urn_noocodec_dag_the-archivist/node/classify-intent -->|lookup-author| urn_noocodec_dag_the-archivist/node/author-search
urn_noocodec_dag_the-archivist/node/classify-intent -->|find-reviews| urn_noocodec_dag_the-archivist/node/reviews-extract
urn_noocodec_dag_the-archivist/node/classify-intent -->|describe-book| urn_noocodec_dag_the-archivist/node/describe-extract
urn_noocodec_dag_the-archivist/node/classify-intent -->|recommend-similar| urn_noocodec_dag_the-archivist/node/recommend-similar
urn_noocodec_dag_the-archivist/node/classify-intent -->|recall-memories| urn_noocodec_dag_the-archivist/node/memory-recall
urn_noocodec_dag_the-archivist/node/classify-intent -->|on-topic| urn_noocodec_dag_the-archivist/node/on-topic-search
urn_noocodec_dag_the-archivist/node/classify-intent -->|recommend-top-rated| urn_noocodec_dag_the-archivist/node/recommend-extract
urn_noocodec_dag_the-archivist/node/classify-intent -->|off-topic| urn_noocodec_dag_the-archivist/node/decline-off-topic
urn_noocodec_dag_the-archivist/node/classify-intent -->|retry| urn_noocodec_dag_the-archivist/node/classify-intent
urn_noocodec_dag_the-archivist/node/classify-intent -->|salvage| urn_noocodec_dag_the-archivist/node/classify-intent-salvage
urn_noocodec_dag_the-archivist/node/classify-intent-salvage["classify-intent-salvage"]
urn_noocodec_dag_the-archivist/node/classify-intent-salvage -->|done| urn_noocodec_dag_the-archivist/node/on-topic-search
urn_noocodec_dag_the-archivist/node/on-topic-search[["on-topic-search"]]
urn_noocodec_dag_the-archivist/node/on-topic-search -->|success| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/on-topic-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/author-search[["author-search"]]
urn_noocodec_dag_the-archivist/node/author-search -->|success| urn_noocodec_dag_the-archivist/node/group-by-year
urn_noocodec_dag_the-archivist/node/author-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/group-by-year["group-by-year"]
urn_noocodec_dag_the-archivist/node/group-by-year -->|ordered| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/reviews-extract["reviews-extract"]
urn_noocodec_dag_the-archivist/node/reviews-extract -->|success| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
urn_noocodec_dag_the-archivist/node/reviews-extract -->|retry| urn_noocodec_dag_the-archivist/node/reviews-extract
urn_noocodec_dag_the-archivist/node/reviews-extract -->|salvage| urn_noocodec_dag_the-archivist/node/reviews-extract-salvage
urn_noocodec_dag_the-archivist/node/reviews-extract-salvage["reviews-extract-salvage"]
urn_noocodec_dag_the-archivist/node/reviews-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
urn_noocodec_dag_the-archivist/node/reviews-decide-tools["reviews-decide-tools"]
urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage
urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage["reviews-decide-tools-salvage"]
urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
urn_noocodec_dag_the-archivist/node/reviews-build-worksets["reviews-build-worksets"]
urn_noocodec_dag_the-archivist/node/reviews-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/reviews-scatter
urn_noocodec_dag_the-archivist/node/reviews-scatter[/"reviews-scatter"/]
urn_noocodec_dag_the-archivist/node/reviews-scatter -->|success| urn_noocodec_dag_the-archivist/node/reviews-gather
urn_noocodec_dag_the-archivist/node/reviews-scatter -->|error| urn_noocodec_dag_the-archivist/node/reviews-gather
urn_noocodec_dag_the-archivist/node/reviews-scatter -->|empty| urn_noocodec_dag_the-archivist/node/reviews-rank
urn_noocodec_dag_the-archivist/node/reviews-gather{"reviews-gather"}
urn_noocodec_dag_the-archivist/node/reviews-gather -->|success| urn_noocodec_dag_the-archivist/node/reviews-rank
urn_noocodec_dag_the-archivist/node/reviews-gather -->|error| urn_noocodec_dag_the-archivist/node/reviews-rank
urn_noocodec_dag_the-archivist/node/reviews-gather -->|empty| urn_noocodec_dag_the-archivist/node/reviews-rank
urn_noocodec_dag_the-archivist/node/reviews-rank["reviews-rank"]
urn_noocodec_dag_the-archivist/node/reviews-rank -->|ranked| urn_noocodec_dag_the-archivist/node/reviews-merge
urn_noocodec_dag_the-archivist/node/reviews-merge["reviews-merge"]
urn_noocodec_dag_the-archivist/node/reviews-merge -->|ranked| urn_noocodec_dag_the-archivist/node/reviews-record
urn_noocodec_dag_the-archivist/node/reviews-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/reviews-record["reviews-record"]
urn_noocodec_dag_the-archivist/node/reviews-record -->|recorded| urn_noocodec_dag_the-archivist/node/reviews-gate
urn_noocodec_dag_the-archivist/node/reviews-gate["reviews-gate"]
urn_noocodec_dag_the-archivist/node/reviews-gate -->|pass| urn_noocodec_dag_the-archivist/node/reviews-recall
urn_noocodec_dag_the-archivist/node/reviews-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/reviews-recall["reviews-recall"]
urn_noocodec_dag_the-archivist/node/reviews-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/recommend-extract["recommend-extract"]
urn_noocodec_dag_the-archivist/node/recommend-extract -->|success| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
urn_noocodec_dag_the-archivist/node/recommend-extract -->|retry| urn_noocodec_dag_the-archivist/node/recommend-extract
urn_noocodec_dag_the-archivist/node/recommend-extract -->|salvage| urn_noocodec_dag_the-archivist/node/recommend-extract-salvage
urn_noocodec_dag_the-archivist/node/recommend-extract-salvage["recommend-extract-salvage"]
urn_noocodec_dag_the-archivist/node/recommend-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
urn_noocodec_dag_the-archivist/node/recommend-decide-tools["recommend-decide-tools"]
urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage
urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage["recommend-decide-tools-salvage"]
urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
urn_noocodec_dag_the-archivist/node/recommend-build-worksets["recommend-build-worksets"]
urn_noocodec_dag_the-archivist/node/recommend-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/recommend-scatter
urn_noocodec_dag_the-archivist/node/recommend-scatter[/"recommend-scatter"/]
urn_noocodec_dag_the-archivist/node/recommend-scatter -->|success| urn_noocodec_dag_the-archivist/node/recommend-gather
urn_noocodec_dag_the-archivist/node/recommend-scatter -->|error| urn_noocodec_dag_the-archivist/node/recommend-gather
urn_noocodec_dag_the-archivist/node/recommend-scatter -->|empty| urn_noocodec_dag_the-archivist/node/recommend-rank
urn_noocodec_dag_the-archivist/node/recommend-gather{"recommend-gather"}
urn_noocodec_dag_the-archivist/node/recommend-gather -->|success| urn_noocodec_dag_the-archivist/node/recommend-rank
urn_noocodec_dag_the-archivist/node/recommend-gather -->|error| urn_noocodec_dag_the-archivist/node/recommend-rank
urn_noocodec_dag_the-archivist/node/recommend-gather -->|empty| urn_noocodec_dag_the-archivist/node/recommend-rank
urn_noocodec_dag_the-archivist/node/recommend-rank["recommend-rank"]
urn_noocodec_dag_the-archivist/node/recommend-rank -->|ranked| urn_noocodec_dag_the-archivist/node/recommend-merge
urn_noocodec_dag_the-archivist/node/recommend-merge["recommend-merge"]
urn_noocodec_dag_the-archivist/node/recommend-merge -->|ranked| urn_noocodec_dag_the-archivist/node/recommend-record
urn_noocodec_dag_the-archivist/node/recommend-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/recommend-record["recommend-record"]
urn_noocodec_dag_the-archivist/node/recommend-record -->|recorded| urn_noocodec_dag_the-archivist/node/recommend-gate
urn_noocodec_dag_the-archivist/node/recommend-gate["recommend-gate"]
urn_noocodec_dag_the-archivist/node/recommend-gate -->|pass| urn_noocodec_dag_the-archivist/node/recommend-recall
urn_noocodec_dag_the-archivist/node/recommend-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/recommend-recall["recommend-recall"]
urn_noocodec_dag_the-archivist/node/recommend-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/describe-extract["describe-extract"]
urn_noocodec_dag_the-archivist/node/describe-extract -->|success| urn_noocodec_dag_the-archivist/node/describe-decide-tools
urn_noocodec_dag_the-archivist/node/describe-extract -->|retry| urn_noocodec_dag_the-archivist/node/describe-extract
urn_noocodec_dag_the-archivist/node/describe-extract -->|salvage| urn_noocodec_dag_the-archivist/node/describe-extract-salvage
urn_noocodec_dag_the-archivist/node/describe-extract-salvage["describe-extract-salvage"]
urn_noocodec_dag_the-archivist/node/describe-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/describe-decide-tools
urn_noocodec_dag_the-archivist/node/describe-decide-tools["describe-decide-tools"]
urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/describe-build-worksets
urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/describe-build-worksets
urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/describe-decide-tools
urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage
urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage["describe-decide-tools-salvage"]
urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/describe-build-worksets
urn_noocodec_dag_the-archivist/node/describe-build-worksets["describe-build-worksets"]
urn_noocodec_dag_the-archivist/node/describe-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/describe-scatter
urn_noocodec_dag_the-archivist/node/describe-scatter[/"describe-scatter"/]
urn_noocodec_dag_the-archivist/node/describe-scatter -->|success| urn_noocodec_dag_the-archivist/node/describe-gather
urn_noocodec_dag_the-archivist/node/describe-scatter -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-scatter -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-gather{"describe-gather"}
urn_noocodec_dag_the-archivist/node/describe-gather -->|success| urn_noocodec_dag_the-archivist/node/describe-pick
urn_noocodec_dag_the-archivist/node/describe-gather -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-gather -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-pick["describe-pick"]
urn_noocodec_dag_the-archivist/node/describe-pick -->|picked| urn_noocodec_dag_the-archivist/node/describe-merge
urn_noocodec_dag_the-archivist/node/describe-merge["describe-merge"]
urn_noocodec_dag_the-archivist/node/describe-merge -->|ranked| urn_noocodec_dag_the-archivist/node/describe-record
urn_noocodec_dag_the-archivist/node/describe-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-record["describe-record"]
urn_noocodec_dag_the-archivist/node/describe-record -->|recorded| urn_noocodec_dag_the-archivist/node/describe-gate
urn_noocodec_dag_the-archivist/node/describe-gate["describe-gate"]
urn_noocodec_dag_the-archivist/node/describe-gate -->|pass| urn_noocodec_dag_the-archivist/node/describe-recall
urn_noocodec_dag_the-archivist/node/describe-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-recall["describe-recall"]
urn_noocodec_dag_the-archivist/node/describe-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/recommend-similar["recommend-similar"]
urn_noocodec_dag_the-archivist/node/recommend-similar -->|seeded| urn_noocodec_dag_the-archivist/node/similar-search
urn_noocodec_dag_the-archivist/node/recommend-similar -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/similar-search[["similar-search"]]
urn_noocodec_dag_the-archivist/node/similar-search -->|success| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/similar-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/compose-loop[["compose-loop"]]
urn_noocodec_dag_the-archivist/node/compose-loop -->|success| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/compose-loop -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/respond-to-visitor["respond-to-visitor"]
urn_noocodec_dag_the-archivist/node/respond-to-visitor -->|success| urn_noocodec_dag_the-archivist/node/end
urn_noocodec_dag_the-archivist/node/memory-recall["memory-recall"]
urn_noocodec_dag_the-archivist/node/memory-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-memory-recall
urn_noocodec_dag_the-archivist/node/compose-memory-recall["compose-memory-recall"]
urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|drafted| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|retry| urn_noocodec_dag_the-archivist/node/compose-memory-recall
urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|salvage| urn_noocodec_dag_the-archivist/node/compose-memory-salvage
urn_noocodec_dag_the-archivist/node/compose-memory-salvage["compose-memory-salvage"]
urn_noocodec_dag_the-archivist/node/compose-memory-salvage -->|done| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/decline-off-topic["decline-off-topic"]
urn_noocodec_dag_the-archivist/node/decline-off-topic -->|success| urn_noocodec_dag_the-archivist/node/end
urn_noocodec_dag_the-archivist/node/compose-empty["compose-empty"]
urn_noocodec_dag_the-archivist/node/compose-empty -->|drafted| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/compose-empty -->|retry| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/compose-empty -->|salvage| urn_noocodec_dag_the-archivist/node/compose-empty-salvage
urn_noocodec_dag_the-archivist/node/compose-empty-salvage["compose-empty-salvage"]
urn_noocodec_dag_the-archivist/node/compose-empty-salvage -->|done| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/end((("end")))- DAGBuilder -
.embed()for embedding a sub-DAG once and.scatter()for 1→N fork over a source - Checkpoint and Resume - pair
Checkpoint.capturewith store snapshots to resume shared state alongside parent state - State Accessors - how dotted paths resolve on
inputsandgatherpaths - Subclassing State - extend
NodeStateBasefor domain-specific parent state - Example 10: Shared State isolates the same store pattern in a small runnable.
What It Lets You Do
Use when
Use shared state when multiple nodes, embedded DAGs, scatter clones, or turns need to accumulate into the same durable structure. Use stateMapping and gather for point-to-point transfer; use a store when the structure is shared, growing, or independently checkpointed.
Code Samples
API surface
| Symbol | Source | Role |
|---|---|---|
Store | @studnicky/dagonizer/contracts | Async key/value contract |
BaseStore | @studnicky/dagonizer/store | Abstract base with snapshot/restore plumbing |
MemoryStore | @studnicky/dagonizer/store | In-memory reference implementation |
TypedStore<Schema> | @studnicky/dagonizer/store | Wrapper that narrows keys and value types |
StoreError | @studnicky/dagonizer/store | Discriminated error with classification.reason |
RemoteStore, RemoteStoreEndpointType, RemoteStoreLeaseType | @studnicky/dagonizer/contracts | Distributed coordination primitives |
Constructor wiring
The focused examples/10-shared-state.ts runner isolates the same pattern in a small executable: create one MemoryStore, pass it into each node constructor, and let parent and child placements write through the same instance.
/**
* 10-shared-state/dags: pure module — nodes, and DAG consts.
* No side effects, no dispatcher, no execute.
* Imported by examples/10-shared-state.ts (the executable entry point).
*/
import {
Batch,
DAG_CONTEXT,
MonadicNode,
NodeOutput,
NodeStateBase,
RoutedBatch,
Validator,
} from '@studnicky/dagonizer';
import type { DAGType, SchemaObjectType } from '@studnicky/dagonizer';
import { MemoryStore } from '@studnicky/dagonizer/store';
import type { StoreInterface } from '@studnicky/dagonizer/contracts';
// ---------------------------------------------------------------------------
// Nodes: each appends its own name to the store's 'entries' key
// ---------------------------------------------------------------------------
export class StepANode extends MonadicNode<NodeStateBase, 'done'> {
private readonly log: StoreInterface;
readonly name = 'step-a';
readonly '@id' = 'urn:noocodec:node:step-a';
readonly outputs = ['done'] as const;
override get outputSchema(): Record<'done', SchemaObjectType> {
return { 'done': { 'type': 'object' } };
}
constructor(log: StoreInterface) {
super();
this.log = log;
}
override async execute(batch: Batch<NodeStateBase>) {
await this.log.update('entries', (current) => {
const existing = (typeof current === 'string' ? current : '').split(',').filter(Boolean);
return [...existing, 'step-a'].join(',');
});
return RoutedBatch.create(NodeOutput.create('done').output, batch);
}
}
export class StepBNode extends MonadicNode<NodeStateBase, 'done'> {
private readonly log: StoreInterface;
readonly name = 'step-b';
readonly '@id' = 'urn:noocodec:node:step-b';
readonly outputs = ['done'] as const;
override get outputSchema(): Record<'done', SchemaObjectType> {
return { 'done': { 'type': 'object' } };
}
constructor(log: StoreInterface) {
super();
this.log = log;
}
override async execute(batch: Batch<NodeStateBase>) {
await this.log.update('entries', (current) => {
const existing = (typeof current === 'string' ? current : '').split(',').filter(Boolean);
return [...existing, 'step-b'].join(',');
});
return RoutedBatch.create(NodeOutput.create('done').output, batch);
}
}
export class ChildStepNode extends MonadicNode<NodeStateBase, 'done'> {
private readonly log: StoreInterface;
readonly name = 'child-step';
readonly '@id' = 'urn:noocodec:node:child-step';
readonly outputs = ['done'] as const;
override get outputSchema(): Record<'done', SchemaObjectType> {
return { 'done': { 'type': 'object' } };
}
constructor(log: StoreInterface) {
super();
this.log = log;
}
override async execute(batch: Batch<NodeStateBase>) {
await this.log.update('entries', (current) => {
const existing = (typeof current === 'string' ? current : '').split(',').filter(Boolean);
return [...existing, 'child-step'].join(',');
});
return RoutedBatch.create(NodeOutput.create('done').output, batch);
}
}
// ---------------------------------------------------------------------------
// DAGs: child DAG placed inside the parent
// ---------------------------------------------------------------------------
// #region child-dag
export const childDag: DAGType = {
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:sub-flow',
'@type': 'DAG',
"name": 'sub-flow',
"version": '1',
"entrypoints": { "main": 'urn:noocodec:dag:sub-flow/node/child-step' },
"nodes": [
{
'@id': 'urn:noocodec:dag:sub-flow/node/child-step',
'@type': 'SingleNode',
"name": 'child-step',
"node": 'urn:noocodec:node:child-step',
"outputs": { "done": 'urn:noocodec:dag:sub-flow/node/child-end' },
},
{
'@id': 'urn:noocodec:dag:sub-flow/node/child-end',
'@type': 'TerminalNode',
"name": 'child-end',
"outcome": 'completed',
},
],
};
// #endregion child-dag
// #region parent-dag
// run-child is an EmbeddedDAGNode whose body is the registered 'sub-flow' DAG.
// The child uses the same injected log store, so child-step appends to the same
// MemoryStore between step-a and step-b.
export const parentDag: DAGType = {
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:main-flow',
'@type': 'DAG',
"name": 'main-flow',
"version": '1',
"entrypoints": { "main": 'urn:noocodec:dag:main-flow/node/step-a' },
"nodes": [
{
'@id': 'urn:noocodec:dag:main-flow/node/step-a',
'@type': 'SingleNode',
"name": 'step-a',
"node": 'urn:noocodec:node:step-a',
"outputs": { "done": 'urn:noocodec:dag:main-flow/node/run-child' },
},
{
'@id': 'urn:noocodec:dag:main-flow/node/run-child',
'@type': 'EmbeddedDAGNode',
"name": 'run-child',
"dag": 'urn:noocodec:dag:sub-flow',
"outputs": {
"success": 'urn:noocodec:dag:main-flow/node/step-b',
"error": 'urn:noocodec:dag:main-flow/node/step-b',
},
},
{
'@id': 'urn:noocodec:dag:main-flow/node/step-b',
'@type': 'SingleNode',
"name": 'step-b',
"node": 'urn:noocodec:node:step-b',
"outputs": { "done": 'urn:noocodec:dag:main-flow/node/end' },
},
{
'@id': 'urn:noocodec:dag:main-flow/node/end',
'@type': 'TerminalNode',
"name": 'end',
"outcome": 'completed',
},
],
};
// #endregion parent-dag
// Re-export MemoryStore so the executable entry point can import it from here
// without adding a second direct import of the store subpath.
export { MemoryStore };
// ---------------------------------------------------------------------------
// TypedStore: schema-narrowed wrapper
// ---------------------------------------------------------------------------
// #region typed-store
import { TypedStore } from '@studnicky/dagonizer/store';
import { StoreError } from '@studnicky/dagonizer/store';
import type { RemoteStoreInterface } from '@studnicky/dagonizer/contracts';
interface PipelineSchema {
tokenBudget: number;
messages: string[];
lastNodeName: string;
}
const PipelineTokenBudgetSchema = { '$id': 'urn:example:PipelineSchema/tokenBudget', 'type': 'number' } as const;
const PipelineMessagesSchema = { '$id': 'urn:example:PipelineSchema/messages', 'type': 'array', 'items': { 'type': 'string' } } as const;
const PipelineLastNodeNameSchema = { '$id': 'urn:example:PipelineSchema/lastNodeName', 'type': 'string' } as const;
const pipelineValidators = {
tokenBudget: Validator.compile<PipelineSchema['tokenBudget']>(PipelineTokenBudgetSchema),
messages: Validator.compile<PipelineSchema['messages']>(PipelineMessagesSchema),
lastNodeName: Validator.compile<PipelineSchema['lastNodeName']>(PipelineLastNodeNameSchema),
};
export class StoreDemos {
static async typed(): Promise<void> {
const inner = new MemoryStore();
const typed = new TypedStore<PipelineSchema>(inner, pipelineValidators);
await typed.set('tokenBudget', 4096);
const budget = await typed.get('tokenBudget'); // number | null
await typed.update('messages', (msgs) => [...(msgs ?? []), 'hello']);
// TypeScript rejects wrong keys and wrong value types at compile time.
// await typed.set('unknown', 'x'); // TS error: key not in schema
// await typed.set('tokenBudget', 'not a num'); // TS error: expected number
const raw: StoreInterface = typed.inner;
await raw.set('someFlag', true);
if (budget !== null) {
await typed.set('tokenBudget', budget);
}
}
// #endregion typed-store
// ---------------------------------------------------------------------------
// StoreInterface concurrency: lost-update vs atomic update
// ---------------------------------------------------------------------------
// #region store-concurrency
static async concurrency(): Promise<void> {
const store = new MemoryStore();
// Race: two paths increment independently. Both read 0, both write 1. Final: 1 (lost update).
const rawCounter = await store.get('counter');
const current = typeof rawCounter === 'number' ? rawCounter : 0;
await store.set('counter', current + 1);
// Atomic: update holds the RMW as one indivisible operation. Final: 2.
await store.update('counter', (n) => (typeof n === 'number' ? n : 0) + 1);
await store.update('counter', (n) => (typeof n === 'number' ? n : 0) + 1);
}
// #endregion store-concurrency
// ---------------------------------------------------------------------------
// StoreError discrimination
// ---------------------------------------------------------------------------
// #region store-error-discrimination
static async errorDiscrimination(store: RemoteStoreInterface): Promise<void> {
try {
await store.acquireLease('run-abc', 5_000, 1_000);
} catch (err) {
if (err instanceof StoreError && err.classification.reason === 'LEASE_DENIED') {
const { subject, holder } = err.classification;
process.stdout.write(`lease for ${subject} held by ${holder}\n`);
}
}
}
// #endregion store-error-discrimination
}
// ---------------------------------------------------------------------------
// Services node: reads from constructor-injected dependencies
// ---------------------------------------------------------------------------
// #region services-node
interface AppServices {
logger: { info(msg: string): void; error(meta: object, msg: string): void };
cache: { get(key: string): Promise<unknown> };
db: { query(sql: string): Promise<unknown> };
}
export class DbFetchNode extends MonadicNode<NodeStateBase, 'success' | 'error'> {
private readonly services: AppServices;
readonly name = 'db-fetch';
readonly '@id' = 'urn:noocodec:node:db-fetch';
readonly outputs = ['success', 'error'] as const;
override get outputSchema(): Record<'success' | 'error', SchemaObjectType> {
return { 'success': { 'type': 'object' }, 'error': { 'type': 'object' } };
}
constructor(services: AppServices) {
super();
this.services = services;
}
override async execute(batch: Batch<NodeStateBase>) {
this.services.logger.info('fetch start');
const cached = await this.services.cache.get('key');
if (cached !== null) {
return RoutedBatch.create(NodeOutput.create('success').output, batch);
}
try {
await this.services.db.query('SELECT 1');
return RoutedBatch.create(NodeOutput.create('success').output, batch);
} catch (error) {
this.services.logger.error({ err: error }, 'fetch failed');
return RoutedBatch.create(NodeOutput.create('error').output, batch);
}
}
}
// #endregion services-nodeconst logStore = new MemoryStore();
const dispatcher = new Dagonizer<NodeStateBase>();Concurrency contract for Stores
Every Store method returns a Promise. There is no sync variant. Always await store calls.
update(key, fn) is atomic within a single store instance. The callback receives the current value (or undefined when the key is absent) and returns the new value. Implementations are responsible for delivering this atomicity. MemoryStore overrides update to access #data without any intermediate await, so no concurrent microtask can interleave between the read and the write. The base-class default has two await points and does not satisfy the atomicity contract on its own.
set + get is NOT atomic. If two concurrent paths each call get then set, the second write silently discards the first. Use update for every read-modify-write:
static async concurrency(): Promise<void> {
const store = new MemoryStore();
// Race: two paths increment independently. Both read 0, both write 1. Final: 1 (lost update).
const rawCounter = await store.get('counter');
const current = typeof rawCounter === 'number' ? rawCounter : 0;
await store.set('counter', current + 1);
// Atomic: update holds the RMW as one indivisible operation. Final: 2.
await store.update('counter', (n) => (typeof n === 'number' ? n : 0) + 1);
await store.update('counter', (n) => (typeof n === 'number' ? n : 0) + 1);
}set is last-write-wins. When two concurrent callers call set without coordination, whichever completes last persists. Avoid set for any value that two nodes write independently; use update instead.
Stores do not synchronize across process boundaries. The concurrency contract is per-instance, in-process. Distributed stores use the fully async contract; plugin authors implement cross-process atomicity inside update (single-step backing access, SQL transactions, Redis WATCH/MULTI, etc.).
Details for Nerds
Runnable DAG that exercises shared state
The Archivist creates a memory service and passes it into nodes through the shared services record. The parent DAG embeds search and compose sub-DAGs; nodes in the parent and child placements read and write the same memory service without threading every value through inputs and outputs.
When to use what
| Need | Use | Why |
|---|---|---|
| Embed a registered sub-DAG exactly once and transfer specific fields in/out | inputs / outputs on .embed() | Single-direction, isolated, checkpoint-friendly without extra wiring |
| Scatter across an array and seed each clone with a parent field | inputs option on .scatter() (stateMapping.input) | Parent field copied into each clone state before the body runs |
| Multiple nodes accumulate growing shared state (agent memory, RAG context, audit log) | MemoryStore (or another Store) injected into each node's constructor | Cross-node and cross-scatter; survives execution boundaries within a run |
RDF graph patterns (RecallContextNode, RecordFindingsNode, etc.) need a Store that is also a TripleStore | RdfStore from @studnicky/dagonizer-patterns-graph | Implements both contracts; key-value side reifies as triples; quad side exposes native RDF |
Known, fixed key set; compile-time safety without explicit <T> at every call | TypedStore<Schema> wrapping any Store | Keys and value types inferred from the schema |
| Long-running flow that survives restart | MemoryStore.snapshot() via Checkpoint.capture({ stores }) | Resume captures shared state alongside parent state |
| Mid-flight introspection by an external observer | Store instance held outside the dispatcher | The same instance lives outside the topology; read it concurrently without touching execution |
inputs and outputs on .embed() (and stateMapping.input on .scatter()) are field copies at a single placement boundary. Use them when the relationship between parent and child is a pure point-to-point transfer with a defined input and output.
A Store is a live, shared, mutable map. Use it when multiple placements accumulate to the same structure (a message list, a token budget, an event log) and that accumulation must persist across placement boundaries without threading every value through state-mapping options at every hop.
Parent and child DAGs
export const childDag: DAGType = {
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:sub-flow',
'@type': 'DAG',
"name": 'sub-flow',
"version": '1',
"entrypoints": { "main": 'urn:noocodec:dag:sub-flow/node/child-step' },
"nodes": [
{
'@id': 'urn:noocodec:dag:sub-flow/node/child-step',
'@type': 'SingleNode',
"name": 'child-step',
"node": 'urn:noocodec:node:child-step',
"outputs": { "done": 'urn:noocodec:dag:sub-flow/node/child-end' },
},
{
'@id': 'urn:noocodec:dag:sub-flow/node/child-end',
'@type': 'TerminalNode',
"name": 'child-end',
"outcome": 'completed',
},
],
};// run-child is an EmbeddedDAGNode whose body is the registered 'sub-flow' DAG.
// The child uses the same injected log store, so child-step appends to the same
// MemoryStore between step-a and step-b.
export const parentDag: DAGType = {
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:main-flow',
'@type': 'DAG',
"name": 'main-flow',
"version": '1',
"entrypoints": { "main": 'urn:noocodec:dag:main-flow/node/step-a' },
"nodes": [
{
'@id': 'urn:noocodec:dag:main-flow/node/step-a',
'@type': 'SingleNode',
"name": 'step-a',
"node": 'urn:noocodec:node:step-a',
"outputs": { "done": 'urn:noocodec:dag:main-flow/node/run-child' },
},
{
'@id': 'urn:noocodec:dag:main-flow/node/run-child',
'@type': 'EmbeddedDAGNode',
"name": 'run-child',
"dag": 'urn:noocodec:dag:sub-flow',
"outputs": {
"success": 'urn:noocodec:dag:main-flow/node/step-b',
"error": 'urn:noocodec:dag:main-flow/node/step-b',
},
},
{
'@id': 'urn:noocodec:dag:main-flow/node/step-b',
'@type': 'SingleNode',
"name": 'step-b',
"node": 'urn:noocodec:node:step-b',
"outputs": { "done": 'urn:noocodec:dag:main-flow/node/end' },
},
{
'@id': 'urn:noocodec:dag:main-flow/node/end',
'@type': 'TerminalNode',
"name": 'end',
"outcome": 'completed',
},
],
};step-a, child-step, and step-b all call this.log.update('entries', ...) against the same constructor-injected store. The resulting entries value is step-a,child-step,step-b, ordered by execution.
The production-shaped version is The Archivist: RecordFindingsNode, RecallContextNode, and memory-digest nodes all receive graph-backed memory through constructor wiring, while the DAG remains pure topology.
RdfStore: RDF-backed shared state for graph patterns
RdfStore from @studnicky/dagonizer-patterns-graph implements both Store and TripleStore. Plugin authors using the graph node patterns (RecallContextNode, RecordFindingsNode, MemoryDigestNode) pass an RdfStore directly as services.memory: it satisfies both the pattern's TripleStore requirement and the engine's Store contract for snapshot/restore.
The Store side exposes set, get, has, delete, update, snapshot, and restore. The TripleStore side exposes assert, ask, select, count, clearGraph, and triples. The Store-side set(key, value) reifies as a single triple under urn:dagonizer:store:{key}. The subject prefix and value predicate are configurable via RdfStoreOptions. No external dependencies; the backing is a plain Quad[].
Pattern nodes that need a TripleStore accept it as a constructor argument. See @studnicky/dagonizer-patterns-graph for RdfStoreOptions, subclassing guidance, and snapshot trade-offs.
TypedStore: narrowing for known key sets
TypedStore<Schema> wraps any Store and constrains the key and value types to a declared schema. Applications with a fixed, known key set use TypedStore to get inferred types at every call site without specifying <T> explicitly. Applications with dynamic or open-ended keys use Store directly.
import { TypedStore } from '@studnicky/dagonizer/store';
import { StoreError } from '@studnicky/dagonizer/store';
import type { RemoteStoreInterface } from '@studnicky/dagonizer/contracts';
interface PipelineSchema {
tokenBudget: number;
messages: string[];
lastNodeName: string;
}
const PipelineTokenBudgetSchema = { '$id': 'urn:example:PipelineSchema/tokenBudget', 'type': 'number' } as const;
const PipelineMessagesSchema = { '$id': 'urn:example:PipelineSchema/messages', 'type': 'array', 'items': { 'type': 'string' } } as const;
const PipelineLastNodeNameSchema = { '$id': 'urn:example:PipelineSchema/lastNodeName', 'type': 'string' } as const;
const pipelineValidators = {
tokenBudget: Validator.compile<PipelineSchema['tokenBudget']>(PipelineTokenBudgetSchema),
messages: Validator.compile<PipelineSchema['messages']>(PipelineMessagesSchema),
lastNodeName: Validator.compile<PipelineSchema['lastNodeName']>(PipelineLastNodeNameSchema),
};
export class StoreDemos {
static async typed(): Promise<void> {
const inner = new MemoryStore();
const typed = new TypedStore<PipelineSchema>(inner, pipelineValidators);
await typed.set('tokenBudget', 4096);
const budget = await typed.get('tokenBudget'); // number | null
await typed.update('messages', (msgs) => [...(msgs ?? []), 'hello']);
// TypeScript rejects wrong keys and wrong value types at compile time.
// await typed.set('unknown', 'x'); // TS error: key not in schema
// await typed.set('tokenBudget', 'not a num'); // TS error: expected number
const raw: StoreInterface = typed.inner;
await raw.set('someFlag', true);
if (budget !== null) {
await typed.set('tokenBudget', budget);
}
}TypedStore is a wrapper, not a subclass of BaseStore. It does not satisfy the Store interface (its set signature is narrower). Pass typed.inner anywhere a Store is expected.
Authoring a custom store
Extend BaseStore and implement six protected abstract methods plus two protected abstract get accessors. Subclasses must override update to satisfy the atomicity contract; the base-class default is safe only when no concurrent calls touch the same key.
import type { JsonValueType } from '@studnicky/dagonizer/entities';
import type { StoreSnapshotEntryType } from '@studnicky/dagonizer/store';
import { BaseStore, type BaseStoreOptionsType } from '@studnicky/dagonizer/store';
/**
* MapStore: a fully functional custom store backed by a plain Map.
*
* `update` is atomic within a single instance: the Map read and write are
* synchronous with no intervening await, so concurrent microtasks cannot
* interleave between them.
*
* In production, swap `this.#data` operations for calls to a Redis, Postgres,
* or other client. Override `connect`/`disconnect` for connection lifecycle.
* The snapshot type and version strings are the stable discriminants for the
* resume path — bump `snapshotVersion` when the storage shape changes.
*/
export class MapStore extends BaseStore {
readonly #data: Map<string, JsonValueType>;
constructor(options: BaseStoreOptionsType = {}) {
super(options);
this.#data = new Map<string, JsonValueType>();
}
protected get snapshotType(): string { return 'map-store'; }
protected get snapshotVersion(): number { return 1; }
/**
* Atomic read-modify-write. Reads #data directly so the body contains
* no `await` and cannot interleave with another `update` on the same
* instance. The base-class default uses performGet + performSet,
* which has two await points and is not safe under concurrent calls.
*/
override async update(
key: string,
fn: (current: JsonValueType | undefined) => JsonValueType,
): Promise<JsonValueType> {
const qualified = this.qualifyKey(key);
const stored = this.#data.get(qualified) ?? null;
const current = stored === null ? undefined : stored;
const next = fn(current);
this.#data.set(qualified, next);
return next;
}
protected async performGet(key: string): Promise<JsonValueType | null> {
return this.#data.get(key) ?? null;
}
protected async performSet(key: string, value: JsonValueType): Promise<void> {
this.#data.set(key, value);
}
protected async performHas(key: string): Promise<boolean> {
return this.#data.has(key);
}
protected async performDelete(key: string): Promise<boolean> {
return this.#data.delete(key);
}
protected async *performEntriesStream(): AsyncIterable<StoreSnapshotEntryType> {
for (const [key, value] of this.#data.entries()) {
yield { key, value };
}
}
protected async performRestoreEntry(entry: StoreSnapshotEntryType): Promise<void> {
this.#data.set(entry.key, entry.value);
}
protected async performClear(): Promise<void> {
this.#data.clear();
}
}MapStore is backed by a real Map<string, JsonValueType>. Its update override reads and writes #data synchronously — no await between the read and the write — so no concurrent microtask can interleave. In production, swap the Map operations for calls to a Redis, Postgres, or any other storage client; the six perform* hooks stay identical regardless of backing.
All six perform* hooks receive the qualified key (namespace prefix already applied by BaseStore). Call this.qualifyKey(key) in the update override to ensure namespace consistency.
The snapshot envelope ({ version, type, entries }) is assembled by BaseStore.snapshot(). BaseStore.restore() validates type and version against snapshotType and snapshotVersion before calling performRestoreEntries. A mismatch throws StoreError(INCOMPATIBLE_SNAPSHOT).
The type string is the stable discriminant for the resume path; include a version suffix (such as 'redis-store-v1') so bumping snapshotVersion to 2 lets restore code distinguish old snapshots from new ones by both fields.
Checkpoint integration
Checkpoint.capture is the async factory for checkpoints that include named stores. It accepts the DAG IRI/CURIE string in the dagName parameter, an execution result, optional stores map, and optional execution policy. Store snapshots and restores run through the shared batch executor, so applications can set execution.concurrency, execution.throttle, and execution.timing for remote or expensive stores.
{
const logStore = new MemoryStore();
const dispatcher = new Dagonizer<NodeStateBase>();
dispatcher.registerNode(new StepANode(logStore));
dispatcher.registerNode(new StepBNode(logStore));
dispatcher.registerNode(new ChildStepNode(logStore));
dispatcher.registerDAG(childDag);
dispatcher.registerDAG(parentDag);
// Abort mid-run: abort after step-a to produce a checkpoint-worthy cursor.
const ctl = new AbortController();
const execution = dispatcher.execute('urn:noocodec:dag:main-flow', new NodeStateBase(), { "signal": ctl.signal });
let seen = 0;
for await (const _event of execution) {
seen++;
if (seen === 1) ctl.abort(new Error('checkpoint'));
}
const partial = await execution;
if (partial.cursor === null) {
process.stdout.write('\nPart 2: run completed before abort; no cursor\n');
} else {
// Capture checkpoint: snapshot the store alongside the parent state.
const ckpt = await Checkpoint.capture('urn:noocodec:dag:main-flow', partial, { "stores": { "log": logStore } });
const json = ckpt.toJson();
process.stdout.write('\nPart 2: Checkpoint captured:\n');
process.stdout.write(` cursor = "${partial.cursor}"\n`);
process.stdout.write(` log at capture = "${await logStore.get('entries') ?? ''}"\n`);
// Resume: restore store from checkpoint, then resume execution.
const freshLog = new MemoryStore();
const ckpt2 = Checkpoint.load(JSON.parse(json));
await ckpt2.restoreStores({ "log": freshLog });
const restoredEntries = await freshLog.get('entries') ?? '';
process.stdout.write(` log after restoreStores = "${restoredEntries}"\n`);
const resumeDispatcher = new Dagonizer<NodeStateBase>();
resumeDispatcher.registerNode(new StepANode(freshLog));
resumeDispatcher.registerNode(new StepBNode(freshLog));
resumeDispatcher.registerNode(new ChildStepNode(freshLog));
resumeDispatcher.registerDAG(childDag);
resumeDispatcher.registerDAG(parentDag);
const { dagName, state, cursor } = ckpt2.restoreState(
CheckpointRestoreAdapter.wrap((snap) => NodeStateBase.restore(snap)),
);
await resumeDispatcher.resume(dagName, state, cursor);
const finalEntries = await freshLog.get('entries') ?? '';
process.stdout.write(` log after resume = "${finalEntries}"\n`);
// → "step-a,child-step,step-b" (all three present, none duplicated)
}
}Failure modes:
- Missing store in restore map: if the checkpoint names a store (e.g.
'memory') butrestoreStoresreceives a map that does not include that key, it throwsDAGErrornaming the missing stores. Loud failure is preferable to silent desync. - Incompatible snapshot:
BaseStore.restorethrowsStoreError(INCOMPATIBLE_SNAPSHOT)whensnapshot.typeorsnapshot.versiondoes not match the store instance'ssnapshotTypeorsnapshotVersion.snapshotVersionis the versioning hook. - Extra stores in restore map: stores present in the map but absent from the checkpoint are a no-op. The application added a store that was not tracked at capture time; the engine accepts this silently.
CheckpointData.stores is required in the schema. Any checkpoint payload lacking the field is rejected by Checkpoint.load.
Distributed execution: RemoteStore
RemoteStore extends Store with three coordination primitives for plugins whose backing lives over the network or is replicated across processes. Local MemoryStore and single-node-durable stores implement Store directly; plugins that talk over HTTP, gRPC, or WebSocket implement RemoteStore. Import it from @studnicky/dagonizer/contracts.
The engine consumes a RemoteStore through the Store surface. The extra methods are optional coordination hooks available to the dispatcher when distributed execution is active.
Additional surface
| Method or Property | Description |
|---|---|
endpoint | RemoteStoreEndpointType with url (stable target identifier) and region (placement hint; '' when no region applies). |
acquireLease(subject, ttlMs, maxWaitMs) | Acquire exclusive write authority for subject scoped to ttlMs ms. Waits up to maxWaitMs for an existing holder before throwing StoreError(LEASE_DENIED). |
releaseLease(lease) | Release a previously-acquired lease. Idempotent: releasing an expired lease is a no-op. |
health(timeoutMs) | Health probe. Returns true when reachable within timeoutMs. Returns false, never throws, on transport failure, so the dispatcher can route around an unhealthy store. |
Authoring a remote store
Extend BaseStore and implement RemoteStore:
import { BaseStore } from '@studnicky/dagonizer/store';
import type { StoreSnapshotEntryType } from '@studnicky/dagonizer/store';
import type {
RemoteStoreInterface,
RemoteStoreEndpointType,
RemoteStoreLeaseType,
} from '@studnicky/dagonizer';
import type { JsonValueType } from '@studnicky/dagonizer/entities';
/**
* GrpcStore: stub RemoteStoreInterface backed by an in-memory map.
* Network methods (connect, disconnect, health, acquireLease, releaseLease)
* log instead of making real gRPC calls — illustrating the contract surface.
*/
export class GrpcStore extends BaseStore implements RemoteStoreInterface {
readonly endpoint: RemoteStoreEndpointType;
readonly #data = new Map<string, JsonValueType>();
constructor(url: string, region: string = '') {
super({ namespace: 'archivist' });
this.endpoint = { url, region };
}
// ── RemoteStoreInterface distributed contract ─────────────────────────────────────
override async connect(): Promise<void> {
process.stdout.write(`[GrpcStore] connect -> ${this.endpoint.url}\n`);
}
override async disconnect(): Promise<void> {
process.stdout.write(`[GrpcStore] disconnect -> ${this.endpoint.url}\n`);
}
async health(timeoutMs: number): Promise<boolean> {
process.stdout.write(`[GrpcStore] health probe (timeout=${timeoutMs}ms)\n`);
// Stub: always healthy in the example.
return true;
}
async acquireLease(subject: string, ttlMs: number, maxWaitMs: number): Promise<RemoteStoreLeaseType> {
process.stdout.write(`[GrpcStore] acquireLease subject=${subject} ttl=${ttlMs}ms maxWait=${maxWaitMs}ms\n`);
return {
token: `token:${subject}:${Date.now()}`,
expiresAt: Date.now() + ttlMs,
subject,
};
}
async releaseLease(lease: RemoteStoreLeaseType): Promise<void> {
process.stdout.write(`[GrpcStore] releaseLease token=${lease.token}\n`);
}
// ── BaseStore abstract hooks ──────────────────────────────────────────────
protected get snapshotType(): string { return 'grpc-store'; }
protected get snapshotVersion(): number { return 1; }
protected async performGet(key: string): Promise<JsonValueType | null> {
return this.#data.get(key) ?? null;
}
protected async performSet(key: string, value: JsonValueType): Promise<void> {
this.#data.set(key, value);
}
protected async performHas(key: string): Promise<boolean> {
return this.#data.has(key);
}
protected async performDelete(key: string): Promise<boolean> {
return this.#data.delete(key);
}
protected async *performEntriesStream(): AsyncIterable<StoreSnapshotEntryType> {
for (const [key, value] of this.#data.entries()) {
yield { key, value };
}
}
protected async performRestoreEntry(entry: StoreSnapshotEntryType): Promise<void> {
this.#data.set(entry.key, entry.value);
}
protected async performClear(): Promise<void> {
this.#data.clear();
}
// Override update for atomic RMW — in-memory direct access is safe.
override async update(key: string, fn: (current: JsonValueType | undefined) => JsonValueType): Promise<JsonValueType> {
const qualified = this.qualifyKey(key);
const stored = this.#data.get(qualified) ?? null;
const current = stored === null ? undefined : stored;
const next = fn(current);
this.#data.set(qualified, next);
return next;
}
}region is required. Stores without a region constraint set it to '' at construction. All RemoteStore fields are concrete types: no undefined, no optional properties in the lease or endpoint shapes.
Error taxonomy for remote failures
Three StoreErrorClassification reasons cover remote-specific failure modes:
| Reason | When |
|---|---|
LEASE_DENIED | acquireLease finds an active holder and maxWaitMs expires before release. Fields: subject, holder. |
LEASE_EXPIRED | A write or release is attempted with an already-expired token. Fields: subject, token. |
UNREACHABLE | Transport failure: endpoint does not respond within the health budget. Fields: endpoint, cause. |
Discriminate by reason:
static async errorDiscrimination(store: RemoteStoreInterface): Promise<void> {
try {
await store.acquireLease('run-abc', 5_000, 1_000);
} catch (err) {
if (err instanceof StoreError && err.classification.reason === 'LEASE_DENIED') {
const { subject, holder } = err.classification;
process.stdout.write(`lease for ${subject} held by ${holder}\n`);
}
}
}See Reference: Store for the full interface.
Related Concepts
- DAGBuilder -
.embed()for embedding a sub-DAG once and.scatter()for 1→N fork over a source - Checkpoint and Resume - pair
Checkpoint.capturewith store snapshots to resume shared state alongside parent state - State Accessors - how dotted paths resolve on
inputsandgatherpaths - Subclassing State - extend
NodeStateBasefor domain-specific parent state - Example 10: Shared State
- Reference: Store
- Reference: Checkpoint
- Reference: Contracts