oracular 0.10.0 → 0.12.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/dist/index.d.cts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Version of the serialized state shape, stored in every state as `state.version`. Bumped only
3
- * when `EngineState` changes incompatibly, and every bump ships its migration: {@link deserialize}
3
+ * when `EngineState` changes shape, and every bump ships its migration: {@link deserialize}
4
4
  * upgrades states saved by earlier versions of this library on its own (version 1, written by
5
- * oracular 0.1.0 to 0.2.x, and version 2, written by 0.3.0 to 0.5.x, become version 3), reads a
6
- * state one version newer than its own as its own (a version bump only adds fields, so a rollback
7
- * by one release keeps the user's history), and calls your `migrate` function only for a version
8
- * it does not know.
5
+ * oracular 0.1.0 to 0.2.x, version 2, written by 0.3.0 to 0.5.x, and version 3, written by 0.6.0
6
+ * to 0.11.x, become version 4), reads a state one version newer than its own as its own (a
7
+ * version bump only adds fields, so a rollback by one release keeps the user's history), and
8
+ * calls your `migrate` function only for a version it does not know.
9
9
  *
10
10
  * @example
11
11
  * ```ts
@@ -21,7 +21,7 @@
21
21
  * });
22
22
  * ```
23
23
  */
24
- declare const STATE_VERSION: 3;
24
+ declare const STATE_VERSION: 4;
25
25
  /**
26
26
  * Version of the recorder format: the `v` field of every record that {@link createRecord} and
27
27
  * {@link createEndRecord} produce. A new optional field does not change it; readers ignore fields
@@ -230,6 +230,20 @@ interface EpisodeState {
230
230
  lastEventAt: number | null;
231
231
  intents: Record<string, EpisodeIntentRecord>;
232
232
  }
233
+ /**
234
+ * The engine's clock: the latest event time it trusts, and one event stamped behind it that is
235
+ * not yet confirmed as a clock correction. See `observe` for the rule.
236
+ */
237
+ interface ClockState {
238
+ /** The latest event time trusted so far; null before the first event. */
239
+ at: number | null;
240
+ /**
241
+ * The stamp of the last event that arrived behind `at`, held until the next event decides: a
242
+ * second event behind `at` confirms the clock was corrected and every anchor ahead of this
243
+ * time moves back to it; an event at or after `at` makes it a glitch that changed nothing.
244
+ */
245
+ behindAt: number | null;
246
+ }
233
247
  /**
234
248
  * The whole engine state: plain JSON with no functions, class instances or hidden fields.
235
249
  * Persist it with `serialize`, restore it with `deserialize`.
@@ -247,6 +261,7 @@ interface EngineState {
247
261
  config: EngineConfig;
248
262
  intents: Record<string, IntentState>;
249
263
  episode: EpisodeState;
264
+ clock: ClockState;
250
265
  }
251
266
  /**
252
267
  * What the app feeds to `observe`. Any event name is accepted; only names that appear in some
@@ -359,7 +374,7 @@ interface Evaluation {
359
374
  /**
360
375
  * Turns a stored state of a version this library cannot migrate itself into a current
361
376
  * `EngineState`. Receives the parsed JSON as-is and the version it declares. States saved by
362
- * oracular 0.1.0 to 0.5.x (versions 1 and 2) never reach it, nor does a state one version newer
377
+ * oracular 0.1.0 to 0.11.x (versions 1 to 3) never reach it, nor does a state one version newer
363
378
  * than `STATE_VERSION`: `deserialize` handles both on its own.
364
379
  *
365
380
  * @example
@@ -384,7 +399,7 @@ interface DeserializeOptions {
384
399
  config?: EngineConfig;
385
400
  /**
386
401
  * Called when the stored version is neither `STATE_VERSION`, nor one this library migrates
387
- * itself (states from oracular 0.1.0 to 0.5.x are upgraded without it), nor the next version,
402
+ * itself (states from oracular 0.1.0 to 0.11.x are upgraded without it), nor the next version,
388
403
  * which it reads as its own.
389
404
  */
390
405
  migrate?: Migrate;
@@ -637,6 +652,15 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
637
652
  * or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
638
653
  * previous episode first.
639
654
  *
655
+ * The clock. An event at or after the latest event time moves the engine's clock there, and
656
+ * anything stamped ahead of it (a reveal stamped by a clock that was ahead) is pinned to it. An
657
+ * event stamped *behind* the clock counts now, at the clock's time, and its stamp is held: if the
658
+ * next event is also behind the clock, the clock was corrected, and every anchor ahead of the
659
+ * held stamp moves back to it so decay and cooldowns resume from the corrected time; if the next
660
+ * event is at or after the clock, the held stamp was one glitch and nothing moved. So one event
661
+ * stamped years ahead cannot hold an intent past the two events that follow it, and one stamped
662
+ * years behind cannot wipe the evidence or close the session.
663
+ *
640
664
  * @param state - The current state. Not mutated.
641
665
  * @param event - `name` is matched against every intent's signals and outcome; `at` is the
642
666
  * event's timestamp, in the same unit as your durations; `props` are accepted and ignored,
@@ -831,7 +855,7 @@ declare function serialize(state: EngineState): string;
831
855
  * before those checks existed still loads),
832
856
  * intents and signals no longer declared are dropped (α and β are recomputed from the kept
833
857
  * contributions), and newly declared intents start at their prior. States saved by oracular
834
- * 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
858
+ * 0.1.0 to 0.11.x are upgraded to the current shape on their own, and a state one version newer
835
859
  * than {@link STATE_VERSION} (written by the next release, read after a rollback) is read as the
836
860
  * current one, ignoring the fields it adds; `migrate` is only for a version this library does
837
861
  * not know.
@@ -952,4 +976,4 @@ declare const ms: Units;
952
976
  */
953
977
  declare function validateConfig(config: unknown): EngineConfig;
954
978
 
955
- export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
979
+ export { type ClockState, type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Version of the serialized state shape, stored in every state as `state.version`. Bumped only
3
- * when `EngineState` changes incompatibly, and every bump ships its migration: {@link deserialize}
3
+ * when `EngineState` changes shape, and every bump ships its migration: {@link deserialize}
4
4
  * upgrades states saved by earlier versions of this library on its own (version 1, written by
5
- * oracular 0.1.0 to 0.2.x, and version 2, written by 0.3.0 to 0.5.x, become version 3), reads a
6
- * state one version newer than its own as its own (a version bump only adds fields, so a rollback
7
- * by one release keeps the user's history), and calls your `migrate` function only for a version
8
- * it does not know.
5
+ * oracular 0.1.0 to 0.2.x, version 2, written by 0.3.0 to 0.5.x, and version 3, written by 0.6.0
6
+ * to 0.11.x, become version 4), reads a state one version newer than its own as its own (a
7
+ * version bump only adds fields, so a rollback by one release keeps the user's history), and
8
+ * calls your `migrate` function only for a version it does not know.
9
9
  *
10
10
  * @example
11
11
  * ```ts
@@ -21,7 +21,7 @@
21
21
  * });
22
22
  * ```
23
23
  */
24
- declare const STATE_VERSION: 3;
24
+ declare const STATE_VERSION: 4;
25
25
  /**
26
26
  * Version of the recorder format: the `v` field of every record that {@link createRecord} and
27
27
  * {@link createEndRecord} produce. A new optional field does not change it; readers ignore fields
@@ -230,6 +230,20 @@ interface EpisodeState {
230
230
  lastEventAt: number | null;
231
231
  intents: Record<string, EpisodeIntentRecord>;
232
232
  }
233
+ /**
234
+ * The engine's clock: the latest event time it trusts, and one event stamped behind it that is
235
+ * not yet confirmed as a clock correction. See `observe` for the rule.
236
+ */
237
+ interface ClockState {
238
+ /** The latest event time trusted so far; null before the first event. */
239
+ at: number | null;
240
+ /**
241
+ * The stamp of the last event that arrived behind `at`, held until the next event decides: a
242
+ * second event behind `at` confirms the clock was corrected and every anchor ahead of this
243
+ * time moves back to it; an event at or after `at` makes it a glitch that changed nothing.
244
+ */
245
+ behindAt: number | null;
246
+ }
233
247
  /**
234
248
  * The whole engine state: plain JSON with no functions, class instances or hidden fields.
235
249
  * Persist it with `serialize`, restore it with `deserialize`.
@@ -247,6 +261,7 @@ interface EngineState {
247
261
  config: EngineConfig;
248
262
  intents: Record<string, IntentState>;
249
263
  episode: EpisodeState;
264
+ clock: ClockState;
250
265
  }
251
266
  /**
252
267
  * What the app feeds to `observe`. Any event name is accepted; only names that appear in some
@@ -359,7 +374,7 @@ interface Evaluation {
359
374
  /**
360
375
  * Turns a stored state of a version this library cannot migrate itself into a current
361
376
  * `EngineState`. Receives the parsed JSON as-is and the version it declares. States saved by
362
- * oracular 0.1.0 to 0.5.x (versions 1 and 2) never reach it, nor does a state one version newer
377
+ * oracular 0.1.0 to 0.11.x (versions 1 to 3) never reach it, nor does a state one version newer
363
378
  * than `STATE_VERSION`: `deserialize` handles both on its own.
364
379
  *
365
380
  * @example
@@ -384,7 +399,7 @@ interface DeserializeOptions {
384
399
  config?: EngineConfig;
385
400
  /**
386
401
  * Called when the stored version is neither `STATE_VERSION`, nor one this library migrates
387
- * itself (states from oracular 0.1.0 to 0.5.x are upgraded without it), nor the next version,
402
+ * itself (states from oracular 0.1.0 to 0.11.x are upgraded without it), nor the next version,
388
403
  * which it reads as its own.
389
404
  */
390
405
  migrate?: Migrate;
@@ -637,6 +652,15 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
637
652
  * or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
638
653
  * previous episode first.
639
654
  *
655
+ * The clock. An event at or after the latest event time moves the engine's clock there, and
656
+ * anything stamped ahead of it (a reveal stamped by a clock that was ahead) is pinned to it. An
657
+ * event stamped *behind* the clock counts now, at the clock's time, and its stamp is held: if the
658
+ * next event is also behind the clock, the clock was corrected, and every anchor ahead of the
659
+ * held stamp moves back to it so decay and cooldowns resume from the corrected time; if the next
660
+ * event is at or after the clock, the held stamp was one glitch and nothing moved. So one event
661
+ * stamped years ahead cannot hold an intent past the two events that follow it, and one stamped
662
+ * years behind cannot wipe the evidence or close the session.
663
+ *
640
664
  * @param state - The current state. Not mutated.
641
665
  * @param event - `name` is matched against every intent's signals and outcome; `at` is the
642
666
  * event's timestamp, in the same unit as your durations; `props` are accepted and ignored,
@@ -831,7 +855,7 @@ declare function serialize(state: EngineState): string;
831
855
  * before those checks existed still loads),
832
856
  * intents and signals no longer declared are dropped (α and β are recomputed from the kept
833
857
  * contributions), and newly declared intents start at their prior. States saved by oracular
834
- * 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
858
+ * 0.1.0 to 0.11.x are upgraded to the current shape on their own, and a state one version newer
835
859
  * than {@link STATE_VERSION} (written by the next release, read after a rollback) is read as the
836
860
  * current one, ignoring the fields it adds; `migrate` is only for a version this library does
837
861
  * not know.
@@ -952,4 +976,4 @@ declare const ms: Units;
952
976
  */
953
977
  declare function validateConfig(config: unknown): EngineConfig;
954
978
 
955
- export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
979
+ export { type ClockState, type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
package/dist/index.js CHANGED
@@ -77,8 +77,8 @@ function lowerBound(alpha, beta, z, bound) {
77
77
  function repeatFactor(n) {
78
78
  return Math.log1p(1 / n) / Math.LN2;
79
79
  }
80
- function laplace(successes, total) {
81
- return (successes + 1) / (total + 2);
80
+ function smoothRate(successes, total, rate) {
81
+ return (successes + 2 * rate) / (total + 2);
82
82
  }
83
83
  function lift(q, b, type = "normalized-difference") {
84
84
  if (type === "absolute-difference") return q - b;
@@ -136,8 +136,9 @@ function mapRecord(record2, fn) {
136
136
  var NONE = { hits: 0, misses: 0 };
137
137
  function decayLearned(state, learning, at) {
138
138
  if (state.learnedAt === null) return { ...state, learnedAt: at };
139
+ if (!(at > state.learnedAt)) return state;
139
140
  const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
140
- if (d === 1) return at === state.learnedAt ? state : { ...state, learnedAt: at };
141
+ if (d === 1) return { ...state, learnedAt: at };
141
142
  return {
142
143
  ...state,
143
144
  learnedAt: at,
@@ -152,12 +153,13 @@ function signalWeights(state, intent, learning, signal) {
152
153
  }
153
154
  const record2 = getOwn(state.learned, signal) ?? NONE;
154
155
  const nWith = record2.hits + record2.misses;
155
- const qWith = laplace(record2.hits, nWith);
156
156
  const totalEpisodes = state.episodes.hits + state.episodes.misses;
157
157
  const nWithout = Math.max(0, totalEpisodes - nWith);
158
158
  const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
159
- const qWithout = laplace(outcomesWithout, nWithout);
160
- const baseline = learning.lift === "association-rule" ? (record2.hits + outcomesWithout + 2) / (nWith + nWithout + 4) : qWithout;
159
+ const base = totalEpisodes === 0 ? 0 : state.episodes.hits / totalEpisodes;
160
+ const qWith = smoothRate(record2.hits, nWith, base);
161
+ const qWithout = smoothRate(outcomesWithout, nWithout, base);
162
+ const baseline = learning.lift === "association-rule" ? smoothRate(record2.hits + outcomesWithout, nWith + nWithout, base) : qWithout;
161
163
  const score = learningScore(lift(qWith, baseline, learning.lift), learning.lift);
162
164
  const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * score;
163
165
  const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
@@ -248,8 +250,9 @@ function withContributions(state, intent, contributions) {
248
250
  }
249
251
  function decayIntent(state, intent, at) {
250
252
  if (state.updatedAt === null) return { ...state, updatedAt: at };
253
+ if (!(at > state.updatedAt)) return state;
251
254
  const d = decayFactor(at - state.updatedAt, intent.halfLife);
252
- if (d === 1) return at === state.updatedAt ? state : { ...state, updatedAt: at };
255
+ if (d === 1) return { ...state, updatedAt: at };
253
256
  const contributions = mapRecord(state.contributions, (c) => ({
254
257
  alpha: c.alpha * d,
255
258
  beta: c.beta * d
@@ -567,7 +570,7 @@ function markResolved(episode, intent) {
567
570
  }
568
571
 
569
572
  // src/version.ts
570
- var STATE_VERSION = 3;
573
+ var STATE_VERSION = 4;
571
574
  var RECORD_VERSION = 1;
572
575
 
573
576
  // src/state.ts
@@ -577,10 +580,16 @@ function createState(config) {
577
580
  for (const [name, intent] of Object.entries(normalized.intents)) {
578
581
  intents[name] = initIntent(intent);
579
582
  }
580
- return { version: STATE_VERSION, config: normalized, intents, episode: emptyEpisode() };
583
+ return {
584
+ version: STATE_VERSION,
585
+ config: normalized,
586
+ intents,
587
+ episode: emptyEpisode(),
588
+ clock: { at: null, behindAt: null }
589
+ };
581
590
  }
582
591
  function requireState(state) {
583
- if (!isRecord(state) || state.version !== STATE_VERSION || !isRecord(state.config) || !isRecord(state.intents) || !isRecord(state.episode)) {
592
+ if (!isRecord(state) || state.version !== STATE_VERSION || !isRecord(state.config) || !isRecord(state.intents) || !isRecord(state.episode) || !isRecord(state.clock)) {
584
593
  throw new TypeError("oracular: state must be an EngineState from createState or deserialize");
585
594
  }
586
595
  return state;
@@ -717,8 +726,10 @@ function explain(state, intent, at, opts) {
717
726
 
718
727
  // src/observe.ts
719
728
  function observe(state, event) {
720
- let s = requireState(state);
721
- const { name, at } = validateEvent(event);
729
+ const { name, at: stamp } = validateEvent(event);
730
+ const synced = syncClock(requireState(state), stamp);
731
+ let s = synced.state;
732
+ const at = synced.at;
722
733
  const closeAt = timedOutAt(s.episode, s.config.episodeTimeout, at);
723
734
  if (closeAt !== null) s = closeEpisode(s, closeAt);
724
735
  const learning = resolveLearning(s.config);
@@ -731,7 +742,6 @@ function observe(state, event) {
731
742
  let is = getIntentState(s, intentName, intent);
732
743
  is = decayIntent(is, intent, at);
733
744
  is = settlePending(is, intent, learning, at);
734
- is = pinAnchors(is, at);
735
745
  if (isSignal) {
736
746
  const { effective } = signalWeights(is, intent, learning, name);
737
747
  const n = occurrences(episode, intentName, name) + 1;
@@ -752,15 +762,36 @@ function observe(state, event) {
752
762
  }
753
763
  return { ...s, intents, episode };
754
764
  }
755
- function pinAnchors(is, at) {
756
- const reveal = is.lastRevealedAt !== null && is.lastRevealedAt > at;
757
- const outcome = is.lastOutcomeAt !== null && is.lastOutcomeAt > at;
758
- if (!reveal && !outcome) return is;
759
- return {
760
- ...is,
761
- lastRevealedAt: reveal ? at : is.lastRevealedAt,
762
- lastOutcomeAt: outcome ? at : is.lastOutcomeAt
763
- };
765
+ function syncClock(state, stamp) {
766
+ const { at, behindAt } = state.clock;
767
+ if (at === null || stamp >= at) return { state: pinTo(state, stamp), at: stamp };
768
+ if (behindAt !== null) return syncClock(pinTo(state, behindAt), stamp);
769
+ return { state: { ...state, clock: { at, behindAt: stamp } }, at };
770
+ }
771
+ var INTENT_ANCHORS = ["updatedAt", "learnedAt", "lastRevealedAt", "lastOutcomeAt"];
772
+ var EPISODE_ANCHORS = ["startedAt", "lastEventAt"];
773
+ function pinTo(state, t) {
774
+ let intents = state.intents;
775
+ for (const [name, is] of Object.entries(state.intents)) {
776
+ let pinned = pinKeys(is, INTENT_ANCHORS, t);
777
+ if (is.pending.some((p) => p.endedAt > t)) {
778
+ pinned = {
779
+ ...pinned,
780
+ pending: is.pending.map((p) => p.endedAt > t ? { ...p, endedAt: t } : p)
781
+ };
782
+ }
783
+ if (pinned !== is) intents = { ...intents, [name]: pinned };
784
+ }
785
+ const episode = pinKeys(state.episode, EPISODE_ANCHORS, t);
786
+ return { ...state, intents, episode, clock: { at: t, behindAt: null } };
787
+ }
788
+ function pinKeys(obj, keys, t) {
789
+ let out = obj;
790
+ for (const key of keys) {
791
+ const value = obj[key];
792
+ if (typeof value === "number" && value > t) out = { ...out, [key]: t };
793
+ }
794
+ return out;
764
795
  }
765
796
  function endEpisode(state, at) {
766
797
  const s = requireState(state);
@@ -886,7 +917,8 @@ function createEndRecord(at, opts) {
886
917
  // src/migrate.ts
887
918
  var MIGRATIONS = {
888
919
  1: fromV1,
889
- 2: fromV2
920
+ 2: fromV2,
921
+ 3: fromV3
890
922
  };
891
923
  function canMigrate(version) {
892
924
  return hasOwn(MIGRATIONS, String(version));
@@ -927,6 +959,9 @@ function fromV2(stored) {
927
959
  }
928
960
  return { ...stored, version: 3, intents };
929
961
  }
962
+ function fromV3(stored) {
963
+ return { ...stored, version: 4, clock: { at: null, behindAt: null } };
964
+ }
930
965
 
931
966
  // src/serialize.ts
932
967
  function serialize(state) {
@@ -1072,6 +1107,13 @@ function episodeState(value, path) {
1072
1107
  intents: mapOwn(r.intents, `${path}.intents`, episodeIntent)
1073
1108
  };
1074
1109
  }
1110
+ function clockState(value, path) {
1111
+ const r = record(value, path);
1112
+ return {
1113
+ at: nullOrFinite(r.at, `${path}.at`),
1114
+ behindAt: nullOrFinite(r.behindAt, `${path}.behindAt`)
1115
+ };
1116
+ }
1075
1117
  function parseState(value) {
1076
1118
  const r = record(value, "state");
1077
1119
  if (r.version !== STATE_VERSION) {
@@ -1083,7 +1125,8 @@ function parseState(value) {
1083
1125
  // when it is only stored, and the config passed in replaces it anyway.
1084
1126
  config: normalizeConfig(r.config, false),
1085
1127
  intents: mapOwn(r.intents, "state.intents", intentState),
1086
- episode: episodeState(r.episode, "state.episode")
1128
+ episode: episodeState(r.episode, "state.episode"),
1129
+ clock: clockState(r.clock, "state.clock")
1087
1130
  };
1088
1131
  }
1089
1132
  function requireComputable(state) {
@@ -1157,9 +1200,24 @@ function reconcile(raw, config) {
1157
1200
  startedAt: raw.episode.startedAt,
1158
1201
  lastEventAt: raw.episode.startedAt === null ? null : raw.episode.lastEventAt,
1159
1202
  intents: episodeIntents
1160
- }
1203
+ },
1204
+ clock: trustedClock(raw.clock, intents, raw.episode)
1161
1205
  };
1162
1206
  }
1207
+ function trustedClock(clock, intents, episode) {
1208
+ let at = clock.at;
1209
+ const see = (value) => {
1210
+ if (value !== null && (at === null || value > at)) at = value;
1211
+ };
1212
+ see(episode.startedAt);
1213
+ see(episode.lastEventAt);
1214
+ for (const is of Object.values(intents)) {
1215
+ see(is.updatedAt);
1216
+ see(is.lastOutcomeAt);
1217
+ }
1218
+ const behindAt = clock.behindAt !== null && at !== null && clock.behindAt < at ? clock.behindAt : null;
1219
+ return at === clock.at && behindAt === clock.behindAt ? clock : { at, behindAt };
1220
+ }
1163
1221
 
1164
1222
  // src/units.ts
1165
1223
  var ms = {