oracular 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +6 -5
- package/dist/cli.js +63 -30
- package/dist/index.cjs +54 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -7
- package/dist/index.d.ts +16 -7
- package/dist/index.js +54 -32
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +13 -6
- package/dist/presets.cjs.map +1 -1
- package/dist/presets.d.cts +0 -17
- package/dist/presets.d.ts +0 -17
- package/dist/presets.js +13 -6
- package/dist/presets.js.map +1 -1
- package/package.json +1 -1
- package/schema/config.schema.json +4 -3
package/dist/index.d.cts
CHANGED
|
@@ -629,7 +629,9 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
|
|
|
629
629
|
* `w × log2(1 + 1/n)`, counted since the episode started or the intent's outcome last arrived.
|
|
630
630
|
* That is the default when `config.episodeTimeout` is set, and `IntentConfig.saturate` overrides
|
|
631
631
|
* it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
|
|
632
|
-
* outcome of: give the
|
|
632
|
+
* outcome of: give the signals remembered since the episode started or the outcome last arrived a
|
|
633
|
+
* hit and count the episode toward the base rate (a repeated outcome with nothing seen since
|
|
634
|
+
* counts nothing), credit
|
|
633
635
|
* the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
|
|
634
636
|
* each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
|
|
635
637
|
* or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
|
|
@@ -824,7 +826,9 @@ declare function serialize(state: EngineState): string;
|
|
|
824
826
|
/**
|
|
825
827
|
* Restores a state saved with {@link serialize}.
|
|
826
828
|
*
|
|
827
|
-
* Always pass the app's current `config`: it is validated and replaces the stored snapshot
|
|
829
|
+
* Always pass the app's current `config`: it is validated and replaces the stored snapshot (the
|
|
830
|
+
* stored one is read without the checks that refuse a config that cannot work, so a state saved
|
|
831
|
+
* before those checks existed still loads),
|
|
828
832
|
* intents and signals no longer declared are dropped (α and β are recomputed from the kept
|
|
829
833
|
* contributions), and newly declared intents start at their prior. States saved by oracular
|
|
830
834
|
* 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
|
|
@@ -863,10 +867,13 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
|
|
|
863
867
|
* @param config - Intents with their priors and signals, plus engine-wide options. Validated
|
|
864
868
|
* strictly: unknown keys, non-finite numbers and out-of-range values throw.
|
|
865
869
|
* @returns A new, JSON-serializable state. `config` is not mutated.
|
|
866
|
-
* @throws `TypeError` when a value has the wrong type
|
|
870
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
871
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on (the reset would wipe what
|
|
872
|
+
* the signal just added).
|
|
867
873
|
* @throws `RangeError` when a number is out of range, for example a `prior` outside (0, 1), a
|
|
868
|
-
* signal weight of 0,
|
|
869
|
-
* `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
874
|
+
* signal weight of 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight,
|
|
875
|
+
* `priorStrength`, `learning.gain` or `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
876
|
+
* overflow the arithmetic).
|
|
870
877
|
* @example
|
|
871
878
|
* ```ts
|
|
872
879
|
* const state = createState({
|
|
@@ -929,9 +936,11 @@ declare const ms: Units;
|
|
|
929
936
|
*
|
|
930
937
|
* @param config - The config to check. Typed `unknown` so parsed JSON can be passed as it is.
|
|
931
938
|
* @returns A fresh, normalized {@link EngineConfig}. `config` is not mutated.
|
|
932
|
-
* @throws `TypeError` when a value has the wrong type
|
|
939
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
940
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on.
|
|
933
941
|
* @throws `RangeError` when a number is out of range: a `prior` outside (0, 1), a signal weight of
|
|
934
|
-
* 0,
|
|
942
|
+
* 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight, `priorStrength`,
|
|
943
|
+
* `learning.gain` or `learning.priorEpisodes` beyond 1e6.
|
|
935
944
|
* @example
|
|
936
945
|
* ```ts
|
|
937
946
|
* import { readFileSync } from 'node:fs';
|
package/dist/index.d.ts
CHANGED
|
@@ -629,7 +629,9 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
|
|
|
629
629
|
* `w × log2(1 + 1/n)`, counted since the episode started or the intent's outcome last arrived.
|
|
630
630
|
* That is the default when `config.episodeTimeout` is set, and `IntentConfig.saturate` overrides
|
|
631
631
|
* it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
|
|
632
|
-
* outcome of: give the
|
|
632
|
+
* outcome of: give the signals remembered since the episode started or the outcome last arrived a
|
|
633
|
+
* hit and count the episode toward the base rate (a repeated outcome with nothing seen since
|
|
634
|
+
* counts nothing), credit
|
|
633
635
|
* the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
|
|
634
636
|
* each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
|
|
635
637
|
* or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
|
|
@@ -824,7 +826,9 @@ declare function serialize(state: EngineState): string;
|
|
|
824
826
|
/**
|
|
825
827
|
* Restores a state saved with {@link serialize}.
|
|
826
828
|
*
|
|
827
|
-
* Always pass the app's current `config`: it is validated and replaces the stored snapshot
|
|
829
|
+
* Always pass the app's current `config`: it is validated and replaces the stored snapshot (the
|
|
830
|
+
* stored one is read without the checks that refuse a config that cannot work, so a state saved
|
|
831
|
+
* before those checks existed still loads),
|
|
828
832
|
* intents and signals no longer declared are dropped (α and β are recomputed from the kept
|
|
829
833
|
* contributions), and newly declared intents start at their prior. States saved by oracular
|
|
830
834
|
* 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
|
|
@@ -863,10 +867,13 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
|
|
|
863
867
|
* @param config - Intents with their priors and signals, plus engine-wide options. Validated
|
|
864
868
|
* strictly: unknown keys, non-finite numbers and out-of-range values throw.
|
|
865
869
|
* @returns A new, JSON-serializable state. `config` is not mutated.
|
|
866
|
-
* @throws `TypeError` when a value has the wrong type
|
|
870
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
871
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on (the reset would wipe what
|
|
872
|
+
* the signal just added).
|
|
867
873
|
* @throws `RangeError` when a number is out of range, for example a `prior` outside (0, 1), a
|
|
868
|
-
* signal weight of 0,
|
|
869
|
-
* `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
874
|
+
* signal weight of 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight,
|
|
875
|
+
* `priorStrength`, `learning.gain` or `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
876
|
+
* overflow the arithmetic).
|
|
870
877
|
* @example
|
|
871
878
|
* ```ts
|
|
872
879
|
* const state = createState({
|
|
@@ -929,9 +936,11 @@ declare const ms: Units;
|
|
|
929
936
|
*
|
|
930
937
|
* @param config - The config to check. Typed `unknown` so parsed JSON can be passed as it is.
|
|
931
938
|
* @returns A fresh, normalized {@link EngineConfig}. `config` is not mutated.
|
|
932
|
-
* @throws `TypeError` when a value has the wrong type
|
|
939
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
940
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on.
|
|
933
941
|
* @throws `RangeError` when a number is out of range: a `prior` outside (0, 1), a signal weight of
|
|
934
|
-
* 0,
|
|
942
|
+
* 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight, `priorStrength`,
|
|
943
|
+
* `learning.gain` or `learning.priorEpisodes` beyond 1e6.
|
|
935
944
|
* @example
|
|
936
945
|
* ```ts
|
|
937
946
|
* import { readFileSync } from 'node:fs';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/math.ts
|
|
2
2
|
function decayFactor(elapsed, halfLife) {
|
|
3
|
-
if (!(elapsed > 0)
|
|
3
|
+
if (!(elapsed > 0)) return 1;
|
|
4
4
|
return 2 ** (-elapsed / halfLife);
|
|
5
5
|
}
|
|
6
6
|
function betaMean(alpha, beta) {
|
|
@@ -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
|
|
81
|
-
return (successes +
|
|
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,12 +136,11 @@ 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
|
-
const learnedAt = Math.max(state.learnedAt, at);
|
|
140
139
|
const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
|
|
141
|
-
if (d === 1) return
|
|
140
|
+
if (d === 1) return at === state.learnedAt ? state : { ...state, learnedAt: at };
|
|
142
141
|
return {
|
|
143
142
|
...state,
|
|
144
|
-
learnedAt,
|
|
143
|
+
learnedAt: at,
|
|
145
144
|
learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
|
|
146
145
|
episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
|
|
147
146
|
};
|
|
@@ -153,12 +152,13 @@ function signalWeights(state, intent, learning, signal) {
|
|
|
153
152
|
}
|
|
154
153
|
const record2 = getOwn(state.learned, signal) ?? NONE;
|
|
155
154
|
const nWith = record2.hits + record2.misses;
|
|
156
|
-
const qWith = laplace(record2.hits, nWith);
|
|
157
155
|
const totalEpisodes = state.episodes.hits + state.episodes.misses;
|
|
158
156
|
const nWithout = Math.max(0, totalEpisodes - nWith);
|
|
159
157
|
const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
|
|
160
|
-
const
|
|
161
|
-
const
|
|
158
|
+
const base = totalEpisodes === 0 ? 0 : state.episodes.hits / totalEpisodes;
|
|
159
|
+
const qWith = smoothRate(record2.hits, nWith, base);
|
|
160
|
+
const qWithout = smoothRate(outcomesWithout, nWithout, base);
|
|
161
|
+
const baseline = learning.lift === "association-rule" ? smoothRate(record2.hits + outcomesWithout, nWith + nWithout, base) : qWithout;
|
|
162
162
|
const score = learningScore(lift(qWith, baseline, learning.lift), learning.lift);
|
|
163
163
|
const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * score;
|
|
164
164
|
const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
|
|
@@ -250,13 +250,12 @@ function withContributions(state, intent, contributions) {
|
|
|
250
250
|
function decayIntent(state, intent, at) {
|
|
251
251
|
if (state.updatedAt === null) return { ...state, updatedAt: at };
|
|
252
252
|
const d = decayFactor(at - state.updatedAt, intent.halfLife);
|
|
253
|
-
|
|
254
|
-
if (d === 1) return updatedAt === state.updatedAt ? state : { ...state, updatedAt };
|
|
253
|
+
if (d === 1) return at === state.updatedAt ? state : { ...state, updatedAt: at };
|
|
255
254
|
const contributions = mapRecord(state.contributions, (c) => ({
|
|
256
255
|
alpha: c.alpha * d,
|
|
257
256
|
beta: c.beta * d
|
|
258
257
|
}));
|
|
259
|
-
return withContributions({ ...state, updatedAt }, intent, contributions);
|
|
258
|
+
return withContributions({ ...state, updatedAt: at }, intent, contributions);
|
|
260
259
|
}
|
|
261
260
|
function applySignal(state, intent, signal, weight) {
|
|
262
261
|
const prev = getOwn(state.contributions, signal) ?? { alpha: 0, beta: 0 };
|
|
@@ -273,6 +272,7 @@ var DEFAULT_Z = 1.28;
|
|
|
273
272
|
var DEFAULT_GAIN = 2;
|
|
274
273
|
var DEFAULT_PRIOR_EPISODES = 10;
|
|
275
274
|
var MAX_MAGNITUDE = 1e6;
|
|
275
|
+
var MAX_Z = 10;
|
|
276
276
|
var CONFIG_KEYS = ["intents", "episodeTimeout", "learning", "reveal"];
|
|
277
277
|
var INTENT_KEYS = [
|
|
278
278
|
"prior",
|
|
@@ -357,14 +357,17 @@ function expectKeyName(name, path) {
|
|
|
357
357
|
if (name === "__proto__") typeError(path, 'must not use the reserved name "__proto__"');
|
|
358
358
|
return name;
|
|
359
359
|
}
|
|
360
|
-
function normalizeRevealOptions(value, path) {
|
|
360
|
+
function normalizeRevealOptions(value, path, strict = true) {
|
|
361
361
|
const record2 = expectRecord(value, path);
|
|
362
362
|
expectKeys(record2, REVEAL_KEYS, path);
|
|
363
363
|
const out = {};
|
|
364
364
|
if (record2.threshold !== void 0) {
|
|
365
|
-
out.threshold = expectClosedUnit(record2.threshold, `${path}.threshold`);
|
|
365
|
+
out.threshold = strict ? expectOpenUnit(record2.threshold, `${path}.threshold`) : expectClosedUnit(record2.threshold, `${path}.threshold`);
|
|
366
|
+
}
|
|
367
|
+
if (record2.z !== void 0) {
|
|
368
|
+
out.z = expectNonNegative(record2.z, `${path}.z`);
|
|
369
|
+
if (strict && out.z > MAX_Z) rangeError(`${path}.z`, `must be at most ${MAX_Z}`);
|
|
366
370
|
}
|
|
367
|
-
if (record2.z !== void 0) out.z = expectNonNegative(record2.z, `${path}.z`);
|
|
368
371
|
if (record2.cooldown !== void 0) {
|
|
369
372
|
out.cooldown = expectNonNegative(record2.cooldown, `${path}.cooldown`);
|
|
370
373
|
}
|
|
@@ -394,7 +397,7 @@ function normalizeLearning(value, path) {
|
|
|
394
397
|
}
|
|
395
398
|
return out;
|
|
396
399
|
}
|
|
397
|
-
function normalizeIntentConfig(value, path) {
|
|
400
|
+
function normalizeIntentConfig(value, path, strict = true) {
|
|
398
401
|
const record2 = expectRecord(value, path);
|
|
399
402
|
expectKeys(record2, INTENT_KEYS, path);
|
|
400
403
|
const signalsRecord = expectRecord(record2.signals, `${path}.signals`);
|
|
@@ -436,11 +439,18 @@ function normalizeIntentConfig(value, path) {
|
|
|
436
439
|
if (record2.attribution !== void 0) {
|
|
437
440
|
out.attribution = expectPositive(record2.attribution, `${path}.attribution`);
|
|
438
441
|
}
|
|
439
|
-
if (record2.reveal !== void 0)
|
|
440
|
-
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal
|
|
442
|
+
if (record2.reveal !== void 0) {
|
|
443
|
+
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal`, strict);
|
|
444
|
+
}
|
|
445
|
+
if (strict && out.outcome !== void 0 && hasOwn(signals, out.outcome) && out.resetOnOutcome !== false) {
|
|
446
|
+
typeError(
|
|
447
|
+
`${path}.outcome`,
|
|
448
|
+
"is also one of its signals: the reset on outcome would wipe what it adds; set resetOnOutcome: false to keep it"
|
|
449
|
+
);
|
|
450
|
+
}
|
|
441
451
|
return out;
|
|
442
452
|
}
|
|
443
|
-
function normalizeConfig(value) {
|
|
453
|
+
function normalizeConfig(value, strict = true) {
|
|
444
454
|
const record2 = expectRecord(value, "config");
|
|
445
455
|
expectKeys(record2, CONFIG_KEYS, "config");
|
|
446
456
|
const intentsRecord = expectRecord(record2.intents, "config.intents");
|
|
@@ -451,7 +461,8 @@ function normalizeConfig(value) {
|
|
|
451
461
|
expectKeyName(name, "config.intents");
|
|
452
462
|
intents[name] = normalizeIntentConfig(
|
|
453
463
|
intentsRecord[name],
|
|
454
|
-
`config.intents[${JSON.stringify(name)}]
|
|
464
|
+
`config.intents[${JSON.stringify(name)}]`,
|
|
465
|
+
strict
|
|
455
466
|
);
|
|
456
467
|
}
|
|
457
468
|
const out = { intents };
|
|
@@ -460,8 +471,9 @@ function normalizeConfig(value) {
|
|
|
460
471
|
}
|
|
461
472
|
if (record2.learning !== void 0)
|
|
462
473
|
out.learning = normalizeLearning(record2.learning, "config.learning");
|
|
463
|
-
if (record2.reveal !== void 0)
|
|
464
|
-
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal");
|
|
474
|
+
if (record2.reveal !== void 0) {
|
|
475
|
+
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal", strict);
|
|
476
|
+
}
|
|
465
477
|
return out;
|
|
466
478
|
}
|
|
467
479
|
function normalizeExplainOptions(value, path) {
|
|
@@ -491,12 +503,12 @@ function splitEvaluateOptions(value, path) {
|
|
|
491
503
|
else explain2[key] = v;
|
|
492
504
|
}
|
|
493
505
|
return {
|
|
494
|
-
reveal: normalizeRevealOptions(reveal, path),
|
|
506
|
+
reveal: normalizeRevealOptions(reveal, path, false),
|
|
495
507
|
explain: normalizeExplainOptions(explain2, path)
|
|
496
508
|
};
|
|
497
509
|
}
|
|
498
510
|
function resolveReveal(config, intent, opts, normalized = false) {
|
|
499
|
-
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options");
|
|
511
|
+
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options", false);
|
|
500
512
|
return {
|
|
501
513
|
threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
|
|
502
514
|
z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
|
|
@@ -534,11 +546,7 @@ function timedOutAt(episode, timeout, at) {
|
|
|
534
546
|
return at - episode.lastEventAt > timeout ? episode.lastEventAt + timeout : null;
|
|
535
547
|
}
|
|
536
548
|
function extendEpisode(episode, at) {
|
|
537
|
-
return {
|
|
538
|
-
startedAt: episode.startedAt ?? at,
|
|
539
|
-
lastEventAt: episode.lastEventAt === null ? at : Math.max(episode.lastEventAt, at),
|
|
540
|
-
intents: episode.intents
|
|
541
|
-
};
|
|
549
|
+
return { startedAt: episode.startedAt ?? at, lastEventAt: at, intents: episode.intents };
|
|
542
550
|
}
|
|
543
551
|
function episodeRecord(episode, intent) {
|
|
544
552
|
return getOwn(episode.intents, intent) ?? { seen: [], counts: {}, resolved: false };
|
|
@@ -596,7 +604,7 @@ function assessIntent(state, name, intent, learning, at) {
|
|
|
596
604
|
}
|
|
597
605
|
function cooldownEnd(since, cooldown, at) {
|
|
598
606
|
if (since === null) return null;
|
|
599
|
-
const end = since + cooldown;
|
|
607
|
+
const end = Math.min(since, at) + cooldown;
|
|
600
608
|
return at < end ? end : null;
|
|
601
609
|
}
|
|
602
610
|
function buildPrediction(is, name, reveal, at) {
|
|
@@ -724,6 +732,7 @@ function observe(state, event) {
|
|
|
724
732
|
let is = getIntentState(s, intentName, intent);
|
|
725
733
|
is = decayIntent(is, intent, at);
|
|
726
734
|
is = settlePending(is, intent, learning, at);
|
|
735
|
+
is = pinAnchors(is, at);
|
|
727
736
|
if (isSignal) {
|
|
728
737
|
const { effective } = signalWeights(is, intent, learning, name);
|
|
729
738
|
const n = occurrences(episode, intentName, name) + 1;
|
|
@@ -733,7 +742,8 @@ function observe(state, event) {
|
|
|
733
742
|
if (!isOutcome) episode = markSeen(episode, intentName, name);
|
|
734
743
|
}
|
|
735
744
|
if (isOutcome) {
|
|
736
|
-
|
|
745
|
+
const record2 = episodeRecord(episode, intentName);
|
|
746
|
+
if (!record2.resolved || record2.seen.length > 0) is = recordEpisode(is, record2.seen, 1);
|
|
737
747
|
is = creditPending(is, intent, at);
|
|
738
748
|
episode = markResolved(episode, intentName);
|
|
739
749
|
is = { ...is, lastOutcomeAt: at };
|
|
@@ -743,6 +753,16 @@ function observe(state, event) {
|
|
|
743
753
|
}
|
|
744
754
|
return { ...s, intents, episode };
|
|
745
755
|
}
|
|
756
|
+
function pinAnchors(is, at) {
|
|
757
|
+
const reveal = is.lastRevealedAt !== null && is.lastRevealedAt > at;
|
|
758
|
+
const outcome = is.lastOutcomeAt !== null && is.lastOutcomeAt > at;
|
|
759
|
+
if (!reveal && !outcome) return is;
|
|
760
|
+
return {
|
|
761
|
+
...is,
|
|
762
|
+
lastRevealedAt: reveal ? at : is.lastRevealedAt,
|
|
763
|
+
lastOutcomeAt: outcome ? at : is.lastOutcomeAt
|
|
764
|
+
};
|
|
765
|
+
}
|
|
746
766
|
function endEpisode(state, at) {
|
|
747
767
|
const s = requireState(state);
|
|
748
768
|
const t = expectTimestamp(at, "at");
|
|
@@ -1060,7 +1080,9 @@ function parseState(value) {
|
|
|
1060
1080
|
}
|
|
1061
1081
|
return {
|
|
1062
1082
|
version: STATE_VERSION,
|
|
1063
|
-
|
|
1083
|
+
// Without the strict checks: a config this release would refuse at createState still loads
|
|
1084
|
+
// when it is only stored, and the config passed in replaces it anyway.
|
|
1085
|
+
config: normalizeConfig(r.config, false),
|
|
1064
1086
|
intents: mapOwn(r.intents, "state.intents", intentState),
|
|
1065
1087
|
episode: episodeState(r.episode, "state.episode")
|
|
1066
1088
|
};
|