@mindot/will 0.1.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/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/core/snapshot.manager.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SnapshotManager — first-class state serialization for the simulation core.
|
|
7
|
+
*
|
|
8
|
+
* Follows the same pattern as DefaultMetricCollector:
|
|
9
|
+
* - Implements TickMiddleware via an onTick method
|
|
10
|
+
* - Wired into DefaultSimulation via orchestratorConfig.onAfterTick
|
|
11
|
+
* - Always active (configurable interval, not optional existence)
|
|
12
|
+
*
|
|
13
|
+
* Responsibilities:
|
|
14
|
+
* 1. Serialize simulation state at configurable intervals
|
|
15
|
+
* 2. Compute deltas between snapshots
|
|
16
|
+
* 3. Feed the ReplayRecorder with structured tick data
|
|
17
|
+
* 4. Persist snapshots to disk
|
|
18
|
+
* 5. Provide restore points for StateManager.restore()
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { logger } from '#core/logger'
|
|
22
|
+
import type { Tick, ReadonlySimulationState, SimulationContext, SimulationState } from '#core/types'
|
|
23
|
+
import type { Serializer, SerializedState, DeltaSnapshot } from '#core/serialization'
|
|
24
|
+
import { BunStorageAdapter, type StorageAdapter } from '#core/abstracts'
|
|
25
|
+
import { DefaultSerializer, DeltaEncoder } from '#core/serialization'
|
|
26
|
+
import { wallClock } from '#core/wall.clock'
|
|
27
|
+
|
|
28
|
+
export interface SnapshotManagerConfig {
|
|
29
|
+
/** How often to take full snapshots (ticks). 1 = every tick. */
|
|
30
|
+
snapshotInterval?: number
|
|
31
|
+
/** How often to persist to disk (ticks). 0 = never. */
|
|
32
|
+
persistInterval?: number
|
|
33
|
+
/** Base path for persisted snapshots */
|
|
34
|
+
persistPath?: string
|
|
35
|
+
/** Whether to compute deltas between snapshots */
|
|
36
|
+
computeDeltas?: boolean
|
|
37
|
+
/** Maximum in-memory snapshots to retain */
|
|
38
|
+
maxInMemorySnapshots?: number
|
|
39
|
+
/** Storage adapter */
|
|
40
|
+
storage?: StorageAdapter
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SnapshotEntry {
|
|
44
|
+
tick: Tick
|
|
45
|
+
state: SerializedState
|
|
46
|
+
delta?: DeltaSnapshot
|
|
47
|
+
timestamp: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class SnapshotManager {
|
|
51
|
+
private _serializer: Serializer
|
|
52
|
+
private _deltaEncoder: DeltaEncoder
|
|
53
|
+
private _config: Required<SnapshotManagerConfig>
|
|
54
|
+
private _storage: StorageAdapter
|
|
55
|
+
|
|
56
|
+
private _snapshots: SnapshotEntry[] = []
|
|
57
|
+
/**
|
|
58
|
+
* Deserialized form of the *immediately preceding* snapshot, kept as the
|
|
59
|
+
* baseline for the next delta. Caching the deserialized state (rather than a
|
|
60
|
+
* SerializedState) lets us encode the next delta without re-stringifying and
|
|
61
|
+
* re-deserializing the baseline every snapshot.
|
|
62
|
+
*/
|
|
63
|
+
private _prevState?: SimulationState
|
|
64
|
+
private _ticksSinceSnapshot = 0
|
|
65
|
+
private _ticksSincePersist = 0
|
|
66
|
+
private _currentTick: Tick = 0
|
|
67
|
+
|
|
68
|
+
/** Callback for feeding replay recorder — set by DefaultSimulation */
|
|
69
|
+
onSnapshot?: ( tick: Tick, state: SerializedState, delta?: DeltaSnapshot ) => void
|
|
70
|
+
|
|
71
|
+
constructor( config: SnapshotManagerConfig = {} ){
|
|
72
|
+
this._serializer = new DefaultSerializer({ includeChecksum: true, prettyPrint: false })
|
|
73
|
+
this._deltaEncoder = new DeltaEncoder()
|
|
74
|
+
this._storage = config.storage ?? new BunStorageAdapter()
|
|
75
|
+
|
|
76
|
+
this._config = {
|
|
77
|
+
snapshotInterval: config.snapshotInterval ?? 10,
|
|
78
|
+
persistInterval: config.persistInterval ?? 100,
|
|
79
|
+
persistPath: config.persistPath ?? './data/snapshots',
|
|
80
|
+
computeDeltas: config.computeDeltas ?? true,
|
|
81
|
+
maxInMemorySnapshots: config.maxInMemorySnapshots ?? 500,
|
|
82
|
+
storage: this._storage, // satisfies Required<>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this._config.persistInterval > 0
|
|
86
|
+
&& this._storage.ensureDir?.( this._config.persistPath ).catch(() => {})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Implements the TickMiddleware signature.
|
|
91
|
+
* Wire this as orchestratorConfig.onAfterTick to enable state serialization.
|
|
92
|
+
* Follows the exact same pattern as DefaultMetricCollector.onTick.
|
|
93
|
+
*/
|
|
94
|
+
onTick = ( tick: Tick, state: ReadonlySimulationState, _context: SimulationContext ): void => {
|
|
95
|
+
this._currentTick = tick
|
|
96
|
+
this._ticksSinceSnapshot++
|
|
97
|
+
this._ticksSincePersist++
|
|
98
|
+
|
|
99
|
+
// Take a snapshot at the configured interval
|
|
100
|
+
if( this._ticksSinceSnapshot < this._config.snapshotInterval ) return
|
|
101
|
+
|
|
102
|
+
this._ticksSinceSnapshot = 0
|
|
103
|
+
|
|
104
|
+
// Serialize current state
|
|
105
|
+
const serialized = this._serializer.serialize(
|
|
106
|
+
state as unknown as SimulationState,
|
|
107
|
+
'json'
|
|
108
|
+
) as string
|
|
109
|
+
|
|
110
|
+
const parsed = JSON.parse( serialized ) as SerializedState
|
|
111
|
+
|
|
112
|
+
// Compute the delta against the *immediately preceding* snapshot (FN13).
|
|
113
|
+
// The previous code only advanced the baseline when `delta` was falsy
|
|
114
|
+
// (`if( !delta )`), which — after the very first snapshot — pinned the
|
|
115
|
+
// baseline to tick 0 forever: every later delta was computed against that
|
|
116
|
+
// one stale state, growing without bound and, once the tick-0 entry was
|
|
117
|
+
// evicted by the ring buffer, referencing a base that no longer existed.
|
|
118
|
+
// Now each delta describes a single interval of change against the prior
|
|
119
|
+
// snapshot, and the baseline advances every time.
|
|
120
|
+
let delta: DeltaSnapshot | undefined
|
|
121
|
+
let currState: SimulationState | undefined
|
|
122
|
+
if( this._config.computeDeltas ){
|
|
123
|
+
currState = this._serializer.deserialize( serialized )
|
|
124
|
+
if( this._prevState )
|
|
125
|
+
delta = this._deltaEncoder.encode( this._prevState, currState )
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const entry: SnapshotEntry = {
|
|
129
|
+
tick,
|
|
130
|
+
state: parsed,
|
|
131
|
+
delta,
|
|
132
|
+
timestamp: wallClock(),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Store in memory
|
|
136
|
+
this._snapshots.push( entry )
|
|
137
|
+
if( this._snapshots.length > this._config.maxInMemorySnapshots )
|
|
138
|
+
this._snapshots.shift()
|
|
139
|
+
|
|
140
|
+
// Advance the delta baseline on every snapshot so the next delta is
|
|
141
|
+
// computed against this one. Each stored entry also keeps the full
|
|
142
|
+
// serialized `state`, so it remains a self-contained keyframe for
|
|
143
|
+
// restore() even after its predecessor is evicted.
|
|
144
|
+
if( currState ) this._prevState = currState
|
|
145
|
+
|
|
146
|
+
// Feed external consumers (e.g., ReplayRecorder)
|
|
147
|
+
this.onSnapshot?.( tick, parsed, delta )
|
|
148
|
+
|
|
149
|
+
// Persist to disk
|
|
150
|
+
if( this._config.persistInterval > 0
|
|
151
|
+
&& this._ticksSincePersist >= this._config.persistInterval ){
|
|
152
|
+
this._ticksSincePersist = 0
|
|
153
|
+
this._persistSnapshot( entry ).catch( err => {
|
|
154
|
+
logger.error(`[SnapshotManager] Persist failed at tick ${tick}:`, err )
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Query API ────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Retrieve a snapshot by tick.
|
|
163
|
+
*/
|
|
164
|
+
getSnapshot( tick: Tick ): SerializedState | undefined {
|
|
165
|
+
return this._snapshots.find( s => s.tick === tick )?.state
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get all in-memory snapshots.
|
|
170
|
+
*/
|
|
171
|
+
getAllSnapshots(): ReadonlyArray<SnapshotEntry> {
|
|
172
|
+
return this._snapshots
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get the most recent snapshot.
|
|
177
|
+
*/
|
|
178
|
+
getLatestSnapshot(): SnapshotEntry | undefined {
|
|
179
|
+
return this._snapshots[ this._snapshots.length - 1 ]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Restore simulation state from a snapshot.
|
|
184
|
+
* Returns the deserialized state ready for StateManager.restore().
|
|
185
|
+
*/
|
|
186
|
+
restoreState( tick: Tick ): SimulationState | undefined {
|
|
187
|
+
const snapshot = this.getSnapshot( tick )
|
|
188
|
+
if( !snapshot ) return undefined
|
|
189
|
+
return this._serializer.deserialize( JSON.stringify( snapshot ) )
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Export all snapshots to a file.
|
|
194
|
+
*/
|
|
195
|
+
async exportAll( path: string ): Promise<void> {
|
|
196
|
+
const output = {
|
|
197
|
+
snapshots: this._snapshots.map( s => ({
|
|
198
|
+
tick: s.tick,
|
|
199
|
+
state: s.state,
|
|
200
|
+
delta: s.delta,
|
|
201
|
+
timestamp: s.timestamp,
|
|
202
|
+
})),
|
|
203
|
+
exportedAt: wallClock(),
|
|
204
|
+
}
|
|
205
|
+
await this._storage.write( path, JSON.stringify( output ) )
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the number of in-memory snapshots.
|
|
210
|
+
*/
|
|
211
|
+
get snapshotCount(): number {
|
|
212
|
+
return this._snapshots.length
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Restore from storage ──────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Load the most recently persisted snapshot from the storage adapter and
|
|
219
|
+
* deserialize it into a live SimulationState, ready for StateManager.restore().
|
|
220
|
+
*
|
|
221
|
+
* Returns undefined when no snapshot has been persisted yet (first boot).
|
|
222
|
+
*
|
|
223
|
+
* Works with any StorageAdapter — BunStorageAdapter (filesystem) or
|
|
224
|
+
* PostgresStorageAdapter (Postgres). The sentinel file `latest.json` is written
|
|
225
|
+
* alongside each snapshot so we can locate the most recent one without scanning.
|
|
226
|
+
*/
|
|
227
|
+
async loadLatestFromStorage(): Promise<SimulationState | undefined> {
|
|
228
|
+
if( this._config.persistInterval === 0 ) return undefined
|
|
229
|
+
|
|
230
|
+
const sentinelPath = `${this._config.persistPath}/latest.json`
|
|
231
|
+
try {
|
|
232
|
+
const exists = await this._storage.exists( sentinelPath )
|
|
233
|
+
if( !exists ) return undefined
|
|
234
|
+
|
|
235
|
+
const raw = await this._storage.read( sentinelPath )
|
|
236
|
+
const { path } = JSON.parse( raw ) as { path: string; tick: number }
|
|
237
|
+
|
|
238
|
+
const snapExists = await this._storage.exists( path )
|
|
239
|
+
if( !snapExists ) return undefined
|
|
240
|
+
|
|
241
|
+
const snapRaw = await this._storage.read( path )
|
|
242
|
+
const parsed = JSON.parse( snapRaw ) as SerializedState
|
|
243
|
+
return this._serializer.deserialize( JSON.stringify( parsed ) )
|
|
244
|
+
}
|
|
245
|
+
catch( err ){
|
|
246
|
+
logger.warn( `[SnapshotManager] Could not load latest snapshot:`, err )
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Force an immediate snapshot of `state` and persist it to disk.
|
|
253
|
+
*
|
|
254
|
+
* Called at session end (pauseWill / archiveWill) to guarantee the latest
|
|
255
|
+
* simulation state — including freshly-flushed episodic memories — is
|
|
256
|
+
* written to disk even if the scheduled persist interval has not elapsed.
|
|
257
|
+
*
|
|
258
|
+
* Unlike the normal onTick path, this does NOT push an entry into the
|
|
259
|
+
* in-memory ring buffer (no replay entry needed for a shutdown snapshot)
|
|
260
|
+
* and does NOT advance the delta baseline (`_prevState`; no more ticks will follow).
|
|
261
|
+
*
|
|
262
|
+
* No-op when persistInterval === 0 (disk persistence disabled).
|
|
263
|
+
*/
|
|
264
|
+
async persistNow( state: ReadonlySimulationState ): Promise<void> {
|
|
265
|
+
if( this._config.persistInterval === 0 ) return
|
|
266
|
+
|
|
267
|
+
const serialized = this._serializer.serialize(
|
|
268
|
+
state as unknown as SimulationState,
|
|
269
|
+
'json'
|
|
270
|
+
) as string
|
|
271
|
+
|
|
272
|
+
const parsed = JSON.parse( serialized ) as SerializedState
|
|
273
|
+
const entry: SnapshotEntry = {
|
|
274
|
+
tick: this._currentTick,
|
|
275
|
+
state: parsed,
|
|
276
|
+
delta: undefined,
|
|
277
|
+
timestamp: wallClock(),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await this._persistSnapshot( entry )
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Internal ─────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
private async _persistSnapshot( entry: SnapshotEntry ): Promise<void> {
|
|
286
|
+
const path = `${this._config.persistPath}/snapshot-${String( entry.tick ).padStart( 8, '0')}.json`
|
|
287
|
+
await this._storage.write( path, JSON.stringify( entry.state ) )
|
|
288
|
+
|
|
289
|
+
// Write a sentinel so loadLatestFromStorage() can find this snapshot on restart.
|
|
290
|
+
const sentinelPath = `${this._config.persistPath}/latest.json`
|
|
291
|
+
await this._storage.write( sentinelPath, JSON.stringify({ path, tick: entry.tick }) )
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/core/state.manager.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import type { SimulationEntity, EntityInput, SimulationState, StateCommands, Tick, Timestamp, RestoreOptions } from '#core/types'
|
|
6
|
+
import { deepFreeze } from '#core/utils'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether entities are deep-frozen on write (R3). Enforcing the read-only
|
|
10
|
+
* contract at runtime costs a recursive freeze per write, so it is on
|
|
11
|
+
* everywhere *except* production — dev, test and CI catch any illegal mutation
|
|
12
|
+
* loudly, and production runs the exact code paths those caught. Override
|
|
13
|
+
* explicitly with `WILL_FREEZE_STATE` (`1`/`true` to force on, `0`/`false` off).
|
|
14
|
+
*/
|
|
15
|
+
function resolveFreezeState(): boolean {
|
|
16
|
+
const flag = process.env.WILL_FREEZE_STATE
|
|
17
|
+
if( flag !== undefined && flag !== '' )
|
|
18
|
+
return flag !== '0' && flag.toLowerCase() !== 'false'
|
|
19
|
+
return process.env.NODE_ENV !== 'production'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StateManager {
|
|
23
|
+
readonly currentTick: Tick
|
|
24
|
+
readonly currentTime: Timestamp
|
|
25
|
+
|
|
26
|
+
updateClock( tick: Tick, time: Timestamp ): void
|
|
27
|
+
|
|
28
|
+
getEntity<T extends SimulationEntity>( id: string ): T | undefined
|
|
29
|
+
setEntity( entity: EntityInput ): void
|
|
30
|
+
deleteEntity( id: string ): boolean
|
|
31
|
+
getAllEntities(): IterableIterator<SimulationEntity>
|
|
32
|
+
|
|
33
|
+
/** O(1) — backed by an internal type index */
|
|
34
|
+
getEntitiesByType( type: string ): SimulationEntity[]
|
|
35
|
+
|
|
36
|
+
getMetric( key: string ): number | undefined
|
|
37
|
+
setMetric( key: string, value: number ): void
|
|
38
|
+
incrementMetric( key: string, delta?: number ): number
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply a batch of commands atomically.
|
|
42
|
+
* Called by the Orchestrator after all engines have returned their
|
|
43
|
+
* EngineResult for a tick (double-buffer commit phase).
|
|
44
|
+
*/
|
|
45
|
+
applyCommands( commands: StateCommands ): void
|
|
46
|
+
|
|
47
|
+
snapshot(): SimulationState
|
|
48
|
+
/**
|
|
49
|
+
* Accepts optional RestoreOptions for partial
|
|
50
|
+
* restore. Use { entities: true, metrics: false } to rewind entity
|
|
51
|
+
* state while preserving metric counters.
|
|
52
|
+
*/
|
|
53
|
+
restore( snapshot: SimulationState, options?: RestoreOptions ): void
|
|
54
|
+
clear(): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class DefaultStateManager implements StateManager {
|
|
58
|
+
private _entities = new Map<string, SimulationEntity>()
|
|
59
|
+
private _metrics = new Map<string, number>()
|
|
60
|
+
private _typeIndex = new Map<string, Set<string>>() // type → entity ids
|
|
61
|
+
private _tick: Tick = 0
|
|
62
|
+
private _time: Timestamp = 0
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Copy-on-write guard (R3-b / FIX F8). A handed-out snapshot shares the live
|
|
66
|
+
* `_entities` / `_metrics` map by reference instead of being eagerly copied;
|
|
67
|
+
* these flags mark a map as still observed by an outstanding snapshot. The
|
|
68
|
+
* NEXT mutation clones that map once (see _ownEntities / _ownMetrics) before
|
|
69
|
+
* writing, so the snapshot keeps its point-in-time values. This turns
|
|
70
|
+
* snapshot() into O(1) and collapses the per-tick double copy (pre- + post-
|
|
71
|
+
* commit) into a single clone on the first write — and zero clones on an
|
|
72
|
+
* idle tick or a read-only snapshot consumer. Entities are deep-frozen on
|
|
73
|
+
* write (R3-a), so sharing a reference can never leak a mutable value.
|
|
74
|
+
*/
|
|
75
|
+
private _entitiesShared = false
|
|
76
|
+
private _metricsShared = false
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* When true, every entity stored is deep-frozen so the read-only snapshot
|
|
80
|
+
* contract (R3) is enforced at runtime, not just in the type system.
|
|
81
|
+
* Resolved once per manager from the environment — see resolveFreezeState.
|
|
82
|
+
*/
|
|
83
|
+
private readonly _freeze = resolveFreezeState()
|
|
84
|
+
|
|
85
|
+
get currentTick(): Tick { return this._tick }
|
|
86
|
+
get currentTime(): Timestamp { return this._time }
|
|
87
|
+
|
|
88
|
+
updateClock( tick: Tick, time: Timestamp ): void {
|
|
89
|
+
this._tick = tick
|
|
90
|
+
this._time = time
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Entity CRUD ──────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
getEntity<T extends SimulationEntity>( id: string ): T | undefined {
|
|
96
|
+
return this._entities.get( id ) as T | undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setEntity( entity: EntityInput ): void {
|
|
100
|
+
const
|
|
101
|
+
existing = this._entities.get( entity.id ),
|
|
102
|
+
// Sim clock time (fed each tick by updateClock), NOT wall time — this is the
|
|
103
|
+
// single place every entity's createdAt/updatedAt is stamped, so sourcing it
|
|
104
|
+
// from the sim clock makes all entity timestamps deterministic on replay (R2).
|
|
105
|
+
now = this._time
|
|
106
|
+
|
|
107
|
+
// Maintain type index — handle type changes
|
|
108
|
+
if( existing && existing.type !== entity.type )
|
|
109
|
+
this._typeIndex.get( existing.type )?.delete( entity.id )
|
|
110
|
+
|
|
111
|
+
if( !this._typeIndex.has( entity.type ) )
|
|
112
|
+
this._typeIndex.set( entity.type, new Set() )
|
|
113
|
+
|
|
114
|
+
this._typeIndex.get( entity.type )!.add( entity.id )
|
|
115
|
+
|
|
116
|
+
// A fresh object every write — setEntity never mutates an existing entity
|
|
117
|
+
// in place. Combined with the copy-on-write clone below, the live map only
|
|
118
|
+
// ever changes by slot replacement on a map no snapshot still observes.
|
|
119
|
+
const stored: SimulationEntity = {
|
|
120
|
+
...entity,
|
|
121
|
+
createdAt: existing?.createdAt ?? entity.createdAt ?? now,
|
|
122
|
+
updatedAt: now,
|
|
123
|
+
updatedAtTick: this._tick,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// CoW: clone the map once if a snapshot still references it, so this write
|
|
127
|
+
// can't reach back and mutate that point-in-time view (R3-b / FIX F8).
|
|
128
|
+
this._ownEntities()
|
|
129
|
+
|
|
130
|
+
// Freeze so anything that later reads this entity (engine snapshots, stored
|
|
131
|
+
// rollback snapshots) cannot mutate it and silently corrupt determinism (R3).
|
|
132
|
+
this._entities.set( entity.id, this._freeze ? deepFreeze( stored ) : stored )
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
deleteEntity( id: string ): boolean {
|
|
136
|
+
const existing = this._entities.get( id )
|
|
137
|
+
if( !existing ) return false
|
|
138
|
+
|
|
139
|
+
this._typeIndex.get( existing.type )?.delete( id )
|
|
140
|
+
// CoW: clone before removing the slot so an outstanding snapshot still
|
|
141
|
+
// sees the entity (R3-b / FIX F8).
|
|
142
|
+
this._ownEntities()
|
|
143
|
+
return this._entities.delete( id )
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getAllEntities(): IterableIterator<SimulationEntity> {
|
|
147
|
+
return this._entities.values()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getEntitiesByType( type: string ): SimulationEntity[] {
|
|
151
|
+
const ids = this._typeIndex.get( type )
|
|
152
|
+
if( !ids ) return []
|
|
153
|
+
|
|
154
|
+
const result: SimulationEntity[] = []
|
|
155
|
+
for( const id of ids ){
|
|
156
|
+
const entity = this._entities.get( id )
|
|
157
|
+
entity && result.push( entity )
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Metrics ──────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
getMetric( key: string ): number | undefined {
|
|
166
|
+
return this._metrics.get( key )
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setMetric( key: string, value: number ): void {
|
|
170
|
+
this._ownMetrics()
|
|
171
|
+
this._metrics.set( key, value )
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
incrementMetric( key: string, delta: number = 1 ): number {
|
|
175
|
+
const next = ( this._metrics.get( key ) ?? 0 ) + delta
|
|
176
|
+
this._ownMetrics()
|
|
177
|
+
this._metrics.set( key, next )
|
|
178
|
+
return next
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Command application (double-buffer commit) ───────────
|
|
182
|
+
|
|
183
|
+
applyCommands( commands: StateCommands ): void {
|
|
184
|
+
if( commands.set )
|
|
185
|
+
for( const entity of commands.set )
|
|
186
|
+
this.setEntity( entity )
|
|
187
|
+
|
|
188
|
+
if( commands.delete )
|
|
189
|
+
for( const id of commands.delete )
|
|
190
|
+
this.deleteEntity( id )
|
|
191
|
+
|
|
192
|
+
if( commands.metrics )
|
|
193
|
+
for( const [ key, value ] of commands.metrics )
|
|
194
|
+
typeof value === 'number'
|
|
195
|
+
? this.setMetric( key, value )
|
|
196
|
+
: this.incrementMetric( key, ( value as { delta: number } ).delta )
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Snapshot / restore ───────────────────────────────────
|
|
200
|
+
|
|
201
|
+
snapshot(): SimulationState {
|
|
202
|
+
// Copy-on-write (R3-b / FIX F8): hand out the live maps by reference and
|
|
203
|
+
// mark them shared instead of eagerly cloning. The next mutation clones
|
|
204
|
+
// before writing (_ownEntities / _ownMetrics), so this snapshot stays a
|
|
205
|
+
// genuine point-in-time view. Callers must treat it as read-only — the
|
|
206
|
+
// ReadonlySimulationState type enforces that for the per-tick consumers,
|
|
207
|
+
// and no code mutates a snapshot's maps directly.
|
|
208
|
+
this._entitiesShared = true
|
|
209
|
+
this._metricsShared = true
|
|
210
|
+
return {
|
|
211
|
+
tick: this._tick,
|
|
212
|
+
time: this._time,
|
|
213
|
+
entities: this._entities,
|
|
214
|
+
metrics: this._metrics,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** CoW: clone `_entities` once if an outstanding snapshot still shares it. */
|
|
219
|
+
private _ownEntities(): void {
|
|
220
|
+
if( !this._entitiesShared ) return
|
|
221
|
+
this._entities = new Map( this._entities )
|
|
222
|
+
this._entitiesShared = false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** CoW: clone `_metrics` once if an outstanding snapshot still shares it. */
|
|
226
|
+
private _ownMetrics(): void {
|
|
227
|
+
if( !this._metricsShared ) return
|
|
228
|
+
this._metrics = new Map( this._metrics )
|
|
229
|
+
this._metricsShared = false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Allows selective rollback of entities, metrics, or clock state.
|
|
234
|
+
* Default behavior (no options) is full restore — backward compatible.
|
|
235
|
+
*/
|
|
236
|
+
restore( snapshot: SimulationState, options: RestoreOptions = {} ): void {
|
|
237
|
+
const restoreEntities = options.entities ?? true
|
|
238
|
+
const restoreMetrics = options.metrics ?? true
|
|
239
|
+
const restoreClock = options.clock ?? true
|
|
240
|
+
|
|
241
|
+
if( restoreClock ){
|
|
242
|
+
this._tick = snapshot.tick
|
|
243
|
+
this._time = snapshot.time
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if( restoreEntities ){
|
|
247
|
+
// Fresh map the manager owns outright — clear any CoW share (R3-b).
|
|
248
|
+
this._entities = new Map( snapshot.entities )
|
|
249
|
+
this._entitiesShared = false
|
|
250
|
+
|
|
251
|
+
// Rebuild type index from restored entities
|
|
252
|
+
this._typeIndex.clear()
|
|
253
|
+
for( const entity of this._entities.values() ){
|
|
254
|
+
// Restored entities may be fresh (e.g. deserialized from disk) and thus
|
|
255
|
+
// unfrozen — re-establish the R3 immutability guarantee. Idempotent for
|
|
256
|
+
// entities that came from an already-frozen in-process snapshot.
|
|
257
|
+
if( this._freeze ) deepFreeze( entity )
|
|
258
|
+
|
|
259
|
+
if( !this._typeIndex.has( entity.type ) )
|
|
260
|
+
this._typeIndex.set( entity.type, new Set() )
|
|
261
|
+
|
|
262
|
+
this._typeIndex.get( entity.type )!.add( entity.id )
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if( restoreMetrics ){
|
|
267
|
+
this._metrics = new Map( snapshot.metrics )
|
|
268
|
+
this._metricsShared = false
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
clear(): void {
|
|
273
|
+
// Replace (not .clear()) the maps: an outstanding snapshot may still share
|
|
274
|
+
// them under CoW, and emptying them in place would wipe that view (R3-b).
|
|
275
|
+
this._entities = new Map()
|
|
276
|
+
this._metrics = new Map()
|
|
277
|
+
this._entitiesShared = false
|
|
278
|
+
this._metricsShared = false
|
|
279
|
+
this._typeIndex.clear()
|
|
280
|
+
this._tick = 0
|
|
281
|
+
this._time = 0
|
|
282
|
+
}
|
|
283
|
+
}
|