cogmem 3.7.1 → 3.7.3
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 +12 -0
- package/MEMORY_ATLAS.md +2 -2
- package/MEMORY_MODEL.md +1 -1
- package/README.md +6 -5
- package/RELEASE_CHECKLIST.md +5 -5
- package/dist/atlas/MemoryAtlasService.d.ts +1 -0
- package/dist/atlas/MemoryAtlasService.js +36 -4
- package/dist/atlas/MemoryAtlasTypes.d.ts +7 -0
- package/dist/belief/BeliefStore.d.ts +3 -1
- package/dist/belief/BeliefStore.js +10 -2
- package/dist/bin/episode.js +107 -16
- package/dist/bin/import-support.js +28 -11
- package/dist/config/CogmemConfig.d.ts +1 -0
- package/dist/config/CogmemConfig.js +45 -0
- package/dist/core/MemoryGraph.js +25 -8
- package/dist/dream/DreamScheduler.d.ts +6 -1
- package/dist/dream/DreamScheduler.js +36 -6
- package/dist/engine/CognitiveGraphCompiler.js +1 -1
- package/dist/engine/ConsolidationPipeline.js +4 -4
- package/dist/engine/DeepWritePromotionPolicy.d.ts +2 -0
- package/dist/engine/DeepWritePromotionPolicy.js +25 -4
- package/dist/engine/DreamCuratorWorker.d.ts +3 -0
- package/dist/engine/DreamCuratorWorker.js +18 -3
- package/dist/engine/FactCompiler.js +2 -2
- package/dist/entity/EntityGovernanceService.js +1 -1
- package/dist/episode/EpisodeActiveScopeGuard.d.ts +3 -0
- package/dist/episode/EpisodeActiveScopeGuard.js +46 -0
- package/dist/episode/EpisodeAssembler.d.ts +20 -2
- package/dist/episode/EpisodeAssembler.js +458 -98
- package/dist/episode/EpisodeBoundaryAuditService.d.ts +70 -0
- package/dist/episode/EpisodeBoundaryAuditService.js +276 -0
- package/dist/episode/EpisodeBoundaryPolicy.d.ts +75 -0
- package/dist/episode/EpisodeBoundaryPolicy.js +189 -0
- package/dist/episode/EpisodeBoundaryReplayEngine.d.ts +65 -0
- package/dist/episode/EpisodeBoundaryReplayEngine.js +272 -0
- package/dist/episode/EpisodeImportIdentity.d.ts +1 -0
- package/dist/episode/EpisodeImportIdentity.js +1 -0
- package/dist/episode/EpisodeInvariantValidator.d.ts +16 -0
- package/dist/episode/EpisodeInvariantValidator.js +99 -0
- package/dist/episode/EpisodeSplitPlanner.d.ts +89 -0
- package/dist/episode/EpisodeSplitPlanner.js +225 -0
- package/dist/episode/EpisodeStore.d.ts +67 -2
- package/dist/episode/EpisodeStore.js +443 -59
- package/dist/episode/EpisodeTypes.d.ts +2 -2
- package/dist/episode/TurnRelationClassifier.d.ts +9 -0
- package/dist/episode/TurnRelationClassifier.js +42 -12
- package/dist/episode/index.d.ts +3 -0
- package/dist/episode/index.js +3 -0
- package/dist/factory.d.ts +62 -3
- package/dist/factory.js +308 -58
- package/dist/mcp/CoreMcpTools.js +103 -8
- package/dist/mcp/server.js +1 -1
- package/dist/migrations/0028_episode_boundary_guardrails.d.ts +3 -0
- package/dist/migrations/0028_episode_boundary_guardrails.js +47 -0
- package/dist/migrations/0029_episode_active_scope_guard.d.ts +3 -0
- package/dist/migrations/0029_episode_active_scope_guard.js +16 -0
- package/dist/migrations/0030_memory_event_local_date_source.d.ts +3 -0
- package/dist/migrations/0030_memory_event_local_date_source.js +111 -0
- package/dist/migrations/0031_episode_boundary_integrity_repair.d.ts +7 -0
- package/dist/migrations/0031_episode_boundary_integrity_repair.js +124 -0
- package/dist/migrations/SchemaMigrationRunner.d.ts +2 -0
- package/dist/migrations/SchemaMigrationRunner.js +46 -15
- package/dist/migrations/index.d.ts +6 -1
- package/dist/migrations/index.js +15 -1
- package/dist/public.d.ts +1 -1
- package/dist/public.js +1 -1
- package/dist/recall/BrainRecall.js +1 -1
- package/dist/store/DeepWriteCandidateStore.d.ts +21 -3
- package/dist/store/DeepWriteCandidateStore.js +118 -11
- package/dist/store/EntityStore.d.ts +4 -0
- package/dist/store/EntityStore.js +24 -12
- package/dist/store/EventStore.d.ts +7 -2
- package/dist/store/EventStore.js +144 -23
- package/dist/store/FactStore.js +6 -5
- package/dist/store/MemoryAtlasStore.d.ts +1 -0
- package/dist/store/MemoryAtlasStore.js +54 -0
- package/dist/types/ExtensionPoints.d.ts +3 -1
- package/dist/types/index.d.ts +1 -0
- package/examples/hermes-backend/AGENTS.md +1 -1
- package/examples/hermes-backend/README.md +1 -1
- package/examples/hermes-backend/SKILL.md +2 -2
- package/examples/hermes-backend/references/operations.md +2 -2
- package/examples/openclaw-backend/AGENTS.md +1 -1
- package/examples/openclaw-backend/README.md +1 -1
- package/examples/openclaw-backend/SKILL.md +2 -2
- package/examples/openclaw-backend/references/operations.md +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { MemoryEvent } from '../types/index.js';
|
|
2
|
+
import { type EpisodeBoundaryConfig } from './EpisodeBoundaryPolicy.js';
|
|
3
|
+
import type { EpisodeStore } from './EpisodeStore.js';
|
|
4
|
+
import type { EpisodeStatus } from './EpisodeTypes.js';
|
|
5
|
+
export interface EpisodeBoundaryAuditItem {
|
|
6
|
+
episodeId: string;
|
|
7
|
+
projectId: string;
|
|
8
|
+
status: EpisodeStatus;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
threadId?: string;
|
|
11
|
+
sourceAgent?: string;
|
|
12
|
+
storedEventCount: number;
|
|
13
|
+
actualLinkedEventCount: number;
|
|
14
|
+
startedAt: number;
|
|
15
|
+
firstEventAt?: number;
|
|
16
|
+
lastEventAt?: number;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
maxEventGapMs: number;
|
|
19
|
+
maxUserTurnGapMs: number;
|
|
20
|
+
maxBoundaryIdleGapMs: number;
|
|
21
|
+
trustedLocalDates: string[];
|
|
22
|
+
localDateConfidence: 'trusted' | 'unknown';
|
|
23
|
+
crossesTrustedLocalDate: boolean;
|
|
24
|
+
storedRelationCounts: Record<string, number>;
|
|
25
|
+
strongShiftCount: number;
|
|
26
|
+
ambiguousShiftCount: number;
|
|
27
|
+
userEventCount: number;
|
|
28
|
+
assistantEventCount: number;
|
|
29
|
+
toolEventCount: number;
|
|
30
|
+
systemEventCount: number;
|
|
31
|
+
outOfOrderEventCount: number;
|
|
32
|
+
sourceFingerprint: string;
|
|
33
|
+
detectedBoundaryCount: number;
|
|
34
|
+
effectiveBoundaryCount: number;
|
|
35
|
+
unresolvedEventCount: number;
|
|
36
|
+
missingRawEventIds: string[];
|
|
37
|
+
evidenceIntegrityStatus: 'ok' | 'missing_raw_events';
|
|
38
|
+
requiresManualReview: boolean;
|
|
39
|
+
severity: 'info' | 'warning' | 'critical';
|
|
40
|
+
reasons: string[];
|
|
41
|
+
warnings: string[];
|
|
42
|
+
recommendedAction: 'none' | 'split-plan' | 'inspect';
|
|
43
|
+
}
|
|
44
|
+
export interface EpisodeBoundaryAuditResult {
|
|
45
|
+
items: EpisodeBoundaryAuditItem[];
|
|
46
|
+
nextCursor?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare class EpisodeBoundaryAuditService {
|
|
49
|
+
private readonly store;
|
|
50
|
+
private readonly resolveEvent?;
|
|
51
|
+
private readonly liveBoundaryConfig;
|
|
52
|
+
private readonly liveConfigDiagnostics;
|
|
53
|
+
constructor(store: EpisodeStore, resolveEvent?: ((eventId: string) => MemoryEvent | null | undefined) | undefined, liveBoundaryConfig?: Partial<EpisodeBoundaryConfig>, liveConfigDiagnostics?: Array<{
|
|
54
|
+
code: string;
|
|
55
|
+
}>);
|
|
56
|
+
audit(options: {
|
|
57
|
+
projectId: string;
|
|
58
|
+
episodeId?: string;
|
|
59
|
+
status?: EpisodeStatus;
|
|
60
|
+
limit?: number;
|
|
61
|
+
cursor?: string;
|
|
62
|
+
maxEvents?: number;
|
|
63
|
+
maxDurationMs?: number;
|
|
64
|
+
maxIdleGapMs?: number;
|
|
65
|
+
timezone?: string;
|
|
66
|
+
}): EpisodeBoundaryAuditResult;
|
|
67
|
+
private requireProjectEpisode;
|
|
68
|
+
private auditEpisode;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=EpisodeBoundaryAuditService.d.ts.map
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { DEFAULT_EPISODE_BOUNDARY_CONFIG } from './EpisodeBoundaryPolicy.js';
|
|
3
|
+
import { normalizeEpisodeBoundaryConfig, resolveTrustedLocalDate } from './EpisodeBoundaryPolicy.js';
|
|
4
|
+
import { replayEpisodeBoundaries } from './EpisodeBoundaryReplayEngine.js';
|
|
5
|
+
import { validateEpisodeInvariants } from './EpisodeInvariantValidator.js';
|
|
6
|
+
export class EpisodeBoundaryAuditService {
|
|
7
|
+
store;
|
|
8
|
+
resolveEvent;
|
|
9
|
+
liveBoundaryConfig;
|
|
10
|
+
liveConfigDiagnostics;
|
|
11
|
+
constructor(store, resolveEvent, liveBoundaryConfig = {}, liveConfigDiagnostics = []) {
|
|
12
|
+
this.store = store;
|
|
13
|
+
this.resolveEvent = resolveEvent;
|
|
14
|
+
this.liveBoundaryConfig = liveBoundaryConfig;
|
|
15
|
+
this.liveConfigDiagnostics = liveConfigDiagnostics;
|
|
16
|
+
}
|
|
17
|
+
audit(options) {
|
|
18
|
+
if (!options.projectId)
|
|
19
|
+
throw new Error('projectId is required');
|
|
20
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 100), 1000));
|
|
21
|
+
const page = options.episodeId ? { episodes: [this.requireProjectEpisode(options.projectId, options.episodeId)], nextCursor: undefined } : this.store.listEpisodesForBoundaryAudit({
|
|
22
|
+
projectId: options.projectId,
|
|
23
|
+
statuses: options.status ? [options.status] : undefined,
|
|
24
|
+
limit,
|
|
25
|
+
cursor: options.cursor,
|
|
26
|
+
});
|
|
27
|
+
const overrideWarnings = boundaryOverrideWarnings(options);
|
|
28
|
+
const normalized = normalizeEpisodeBoundaryConfig(configWithDefinedOverrides(this.liveBoundaryConfig, options));
|
|
29
|
+
const aggregateCountCheck = options.maxEvents !== undefined || this.liveBoundaryConfig.maxEvents !== undefined || normalized.config.maxEvents !== DEFAULT_EPISODE_BOUNDARY_CONFIG.maxEvents;
|
|
30
|
+
const boundaryDiagnostics = this.liveConfigDiagnostics
|
|
31
|
+
.map((item) => item.code)
|
|
32
|
+
.filter((code) => code.startsWith('invalid_episode_boundary_'));
|
|
33
|
+
const items = page.episodes.map((episode) => this.auditEpisode(episode, normalized.config, [...boundaryDiagnostics, ...overrideWarnings, ...normalized.diagnostics.map((item) => item.code)], aggregateCountCheck));
|
|
34
|
+
return { items, nextCursor: page.nextCursor };
|
|
35
|
+
}
|
|
36
|
+
requireProjectEpisode(projectId, episodeId) {
|
|
37
|
+
const episode = this.store.getEpisode(episodeId);
|
|
38
|
+
if (!episode || episode.projectId !== projectId)
|
|
39
|
+
throw new Error(`episode_project_mismatch:${episodeId}`);
|
|
40
|
+
return episode;
|
|
41
|
+
}
|
|
42
|
+
auditEpisode(episode, config, configWarnings = [], aggregateCountCheck = true) {
|
|
43
|
+
const links = this.store.listEventLinks(episode.episodeId);
|
|
44
|
+
const pairs = links.map((link) => ({ link, event: this.resolveEvent?.(link.eventId) || undefined }));
|
|
45
|
+
const events = pairs.map((item) => item.event).filter((event) => Boolean(event));
|
|
46
|
+
const missingRawEventIds = pairs.filter((item) => !item.event).map((item) => item.link.eventId);
|
|
47
|
+
const times = events.map((event) => event.occurredAt).filter((value) => typeof value === 'number');
|
|
48
|
+
const dates = [...new Set(events.filter((event) => event.role === 'user').map((event) => trustedLocalDate(event, config.timezone)).filter((value) => Boolean(value)))];
|
|
49
|
+
const relationCounts = {};
|
|
50
|
+
for (const link of links)
|
|
51
|
+
relationCounts[link.relation] = (relationCounts[link.relation] || 0) + 1;
|
|
52
|
+
const userTimes = events.filter((event) => event.role === 'user' && Number.isFinite(event.occurredAt)).map((event) => event.occurredAt);
|
|
53
|
+
const reasons = [];
|
|
54
|
+
const warnings = [...configWarnings];
|
|
55
|
+
warnings.push(...dateWarningCodes(pairs, config.timezone));
|
|
56
|
+
const range = timeRange(times);
|
|
57
|
+
const durationMs = range ? range.max - range.min : 0;
|
|
58
|
+
const maxEventGapMs = maxGap(times);
|
|
59
|
+
const maxUserTurnGapMs = maxGap(userTimes);
|
|
60
|
+
const maxBoundaryIdleGapMs = maxBoundaryIdleGap(pairs);
|
|
61
|
+
const outOfOrderEventCount = outOfOrderCount(events);
|
|
62
|
+
const replay = replayEpisodeBoundaries({ episode, pairs, config });
|
|
63
|
+
const violations = validateEpisodeInvariants({
|
|
64
|
+
episode, pairs, timezone: config.timezone,
|
|
65
|
+
closureReceipts: this.store.listClosureReceipts({ episodeId: episode.episodeId, limit: 1 }),
|
|
66
|
+
dreamJobState: this.store.getDreamJobState(episode.episodeId),
|
|
67
|
+
});
|
|
68
|
+
warnings.push(...replay.warnings, ...replay.structuralAnomalies);
|
|
69
|
+
warnings.push(...replay.detectedBoundaries.filter((boundary) => !boundary.effective).map((boundary) => `detected_boundary:${boundary.reason}`));
|
|
70
|
+
for (const boundary of replay.effectiveBoundaries)
|
|
71
|
+
reasons.push(...auditReasonsForBoundary(boundary));
|
|
72
|
+
reasons.push(...violations.map((violation) => violation.reason));
|
|
73
|
+
if (episode.eventCount !== links.length)
|
|
74
|
+
reasons.push('stored_actual_event_count_mismatch');
|
|
75
|
+
if (missingRawEventIds.length > 0)
|
|
76
|
+
reasons.push('unresolved_raw_events');
|
|
77
|
+
if (outOfOrderEventCount > 0)
|
|
78
|
+
reasons.push('out_of_order_events');
|
|
79
|
+
if ((relationCounts.ambiguous_shift || 0) > 1)
|
|
80
|
+
reasons.push('repeated_ambiguous_shifts');
|
|
81
|
+
if (hardShiftCount(relationCounts) > 1)
|
|
82
|
+
reasons.push('multiple_hard_topic_switch_relations');
|
|
83
|
+
const hasClosureTurn = replay.logicalTurns.some((turn) => turn.some((pair) => pair.event?.role === 'user' && pair.link.relation === 'closes_episode'));
|
|
84
|
+
const importedEpisode = replay.logicalTurns.some(isImportedTurn);
|
|
85
|
+
if (aggregateCountCheck && config.maxEvents !== undefined && links.length > config.maxEvents && !hasClosureTurn && !(importedEpisode && config.applyToImports === false)) {
|
|
86
|
+
reasons.push('event_count_exceeds_max');
|
|
87
|
+
}
|
|
88
|
+
if (config.splitOnTrustedLocalDateChange && dates.length > 1)
|
|
89
|
+
reasons.push('multiple_trusted_local_dates');
|
|
90
|
+
if (!events.some((event) => event.role === 'user'))
|
|
91
|
+
reasons.push('zero_user_event_episode');
|
|
92
|
+
if (!dates.length)
|
|
93
|
+
warnings.push('trusted_local_date_unavailable');
|
|
94
|
+
const stableReasons = [...new Set(reasons)].sort();
|
|
95
|
+
const stableWarnings = [...new Set(warnings)].sort();
|
|
96
|
+
const critical = stableReasons.some((reason) => [
|
|
97
|
+
'stored_actual_event_count_mismatch', 'event_count_exceeds_max', 'duration_exceeds_max',
|
|
98
|
+
'boundary_idle_gap_exceeds_max', 'multiple_trusted_local_dates', 'unresolved_raw_events',
|
|
99
|
+
].includes(reason)) || violations.some((violation) => violation.severity === 'critical');
|
|
100
|
+
return {
|
|
101
|
+
episodeId: episode.episodeId,
|
|
102
|
+
projectId: episode.projectId,
|
|
103
|
+
status: episode.status,
|
|
104
|
+
sessionId: episode.sessionId,
|
|
105
|
+
threadId: episode.conversationThreadId,
|
|
106
|
+
sourceAgent: episode.sourceAgent,
|
|
107
|
+
storedEventCount: episode.eventCount,
|
|
108
|
+
actualLinkedEventCount: links.length,
|
|
109
|
+
startedAt: episode.startedAt,
|
|
110
|
+
firstEventAt: range?.min,
|
|
111
|
+
lastEventAt: range?.max,
|
|
112
|
+
durationMs,
|
|
113
|
+
maxEventGapMs,
|
|
114
|
+
maxUserTurnGapMs,
|
|
115
|
+
maxBoundaryIdleGapMs,
|
|
116
|
+
trustedLocalDates: dates,
|
|
117
|
+
localDateConfidence: dates.length ? 'trusted' : 'unknown',
|
|
118
|
+
crossesTrustedLocalDate: dates.length > 1,
|
|
119
|
+
storedRelationCounts: relationCounts,
|
|
120
|
+
strongShiftCount: hardShiftCount(relationCounts),
|
|
121
|
+
ambiguousShiftCount: relationCounts.ambiguous_shift || 0,
|
|
122
|
+
userEventCount: events.filter((event) => event.role === 'user').length,
|
|
123
|
+
assistantEventCount: events.filter((event) => event.role === 'assistant' || event.role === 'agent').length,
|
|
124
|
+
toolEventCount: events.filter((event) => event.role === 'tool').length,
|
|
125
|
+
systemEventCount: events.filter((event) => event.role === 'system').length,
|
|
126
|
+
outOfOrderEventCount,
|
|
127
|
+
sourceFingerprint: sourceFingerprint(pairs.map((item) => ({ ...item.link, event: item.event }))),
|
|
128
|
+
detectedBoundaryCount: replay.detectedBoundaries.length,
|
|
129
|
+
effectiveBoundaryCount: replay.effectiveBoundaries.length,
|
|
130
|
+
unresolvedEventCount: missingRawEventIds.length,
|
|
131
|
+
missingRawEventIds: missingRawEventIds.slice(0, 50),
|
|
132
|
+
evidenceIntegrityStatus: missingRawEventIds.length ? 'missing_raw_events' : 'ok',
|
|
133
|
+
requiresManualReview: critical || missingRawEventIds.length > 0 || replay.structuralAnomalies.length > 0 || violations.some((violation) => violation.requiresManualReview),
|
|
134
|
+
severity: critical ? 'critical' : reasons.length ? 'warning' : 'info',
|
|
135
|
+
reasons: stableReasons,
|
|
136
|
+
warnings: stableWarnings,
|
|
137
|
+
recommendedAction: critical && !stableReasons.some((reason) => /raw_event|scope_mismatch|pointer_mismatch|dream.*mismatch|receipt.*mismatch|duplicate_or_gapped_positions|duplicate_active|empty_episode/u.test(reason))
|
|
138
|
+
? 'split-plan' : stableReasons.length ? 'inspect' : 'none',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function maxGap(values) {
|
|
143
|
+
const ordered = values.filter(Number.isFinite).sort((a, b) => a - b);
|
|
144
|
+
let max = 0;
|
|
145
|
+
for (let index = 1; index < ordered.length; index += 1)
|
|
146
|
+
max = Math.max(max, ordered[index] - ordered[index - 1]);
|
|
147
|
+
return max;
|
|
148
|
+
}
|
|
149
|
+
function timeRange(values) {
|
|
150
|
+
let min;
|
|
151
|
+
let max;
|
|
152
|
+
for (const value of values) {
|
|
153
|
+
if (!Number.isFinite(value))
|
|
154
|
+
continue;
|
|
155
|
+
min = min === undefined ? value : Math.min(min, value);
|
|
156
|
+
max = max === undefined ? value : Math.max(max, value);
|
|
157
|
+
}
|
|
158
|
+
return min === undefined || max === undefined ? undefined : { min, max };
|
|
159
|
+
}
|
|
160
|
+
function maxBoundaryIdleGap(pairs) {
|
|
161
|
+
let max = 0;
|
|
162
|
+
let runningMax;
|
|
163
|
+
for (const pair of pairs) {
|
|
164
|
+
const event = pair.event;
|
|
165
|
+
if (event?.role === 'user' && typeof event.occurredAt === 'number' && Number.isFinite(event.occurredAt) && runningMax !== undefined) {
|
|
166
|
+
max = Math.max(max, Math.max(0, event.occurredAt - runningMax));
|
|
167
|
+
}
|
|
168
|
+
if (typeof event?.occurredAt === 'number' && Number.isFinite(event.occurredAt)) {
|
|
169
|
+
runningMax = Math.max(runningMax ?? event.occurredAt, event.occurredAt);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return max;
|
|
173
|
+
}
|
|
174
|
+
function auditReasonsForBoundary(boundary) {
|
|
175
|
+
if (boundary.guardCodes.length)
|
|
176
|
+
return boundary.guardCodes.map(auditReasonForGuard);
|
|
177
|
+
return [auditReasonForBoundary(boundary.reason)];
|
|
178
|
+
}
|
|
179
|
+
function auditReasonForGuard(code) {
|
|
180
|
+
if (code === 'max_events_exceeded')
|
|
181
|
+
return 'event_count_exceeds_max';
|
|
182
|
+
if (code === 'max_duration_exceeded')
|
|
183
|
+
return 'duration_exceeds_max';
|
|
184
|
+
if (code === 'max_idle_gap_exceeded')
|
|
185
|
+
return 'boundary_idle_gap_exceeds_max';
|
|
186
|
+
if (code === 'trusted_local_date_changed')
|
|
187
|
+
return 'multiple_trusted_local_dates';
|
|
188
|
+
return code;
|
|
189
|
+
}
|
|
190
|
+
function auditReasonForBoundary(reason) {
|
|
191
|
+
if (reason === 'max_events_boundary')
|
|
192
|
+
return 'event_count_exceeds_max';
|
|
193
|
+
if (reason === 'max_duration_boundary')
|
|
194
|
+
return 'duration_exceeds_max';
|
|
195
|
+
if (reason === 'max_idle_gap_boundary')
|
|
196
|
+
return 'boundary_idle_gap_exceeds_max';
|
|
197
|
+
if (reason === 'trusted_local_date_boundary')
|
|
198
|
+
return 'multiple_trusted_local_dates';
|
|
199
|
+
return reason;
|
|
200
|
+
}
|
|
201
|
+
function outOfOrderCount(events) {
|
|
202
|
+
let runningMax;
|
|
203
|
+
let count = 0;
|
|
204
|
+
for (const event of events) {
|
|
205
|
+
if (typeof event.occurredAt !== 'number')
|
|
206
|
+
continue;
|
|
207
|
+
if (runningMax !== undefined && event.occurredAt < runningMax)
|
|
208
|
+
count += 1;
|
|
209
|
+
runningMax = Math.max(runningMax ?? event.occurredAt, event.occurredAt);
|
|
210
|
+
}
|
|
211
|
+
return count;
|
|
212
|
+
}
|
|
213
|
+
function hardShiftCount(counts) {
|
|
214
|
+
return (counts.hard_topic_switch || 0) + (counts.starts_new_topic || 0) + (counts.switches_topic || 0);
|
|
215
|
+
}
|
|
216
|
+
function sourceFingerprint(items) {
|
|
217
|
+
const hash = createHash('sha256');
|
|
218
|
+
for (const item of items) {
|
|
219
|
+
hash.update(JSON.stringify([
|
|
220
|
+
item.eventId,
|
|
221
|
+
item.position,
|
|
222
|
+
item.relation,
|
|
223
|
+
item.event?.role,
|
|
224
|
+
item.event?.turnId,
|
|
225
|
+
item.event?.turnSeq,
|
|
226
|
+
item.event?.eventOrdinal,
|
|
227
|
+
item.event?.occurredAt,
|
|
228
|
+
item.event?.localDate,
|
|
229
|
+
item.event?.localDateSource,
|
|
230
|
+
item.event?.contentHash,
|
|
231
|
+
]));
|
|
232
|
+
}
|
|
233
|
+
return hash.digest('hex');
|
|
234
|
+
}
|
|
235
|
+
function trustedLocalDate(event, timezone) {
|
|
236
|
+
return resolveTrustedLocalDate(event, timezone).date;
|
|
237
|
+
}
|
|
238
|
+
function dateWarningCodes(pairs, timezone) {
|
|
239
|
+
return [...new Set(pairs
|
|
240
|
+
.filter((item) => item.event?.role === 'user')
|
|
241
|
+
.map((item) => resolveTrustedLocalDate(item.event, timezone).warning?.code)
|
|
242
|
+
.filter((code) => code === 'invalid_trusted_local_date'))];
|
|
243
|
+
}
|
|
244
|
+
function isImportedTurn(group) {
|
|
245
|
+
return group.some((item) => {
|
|
246
|
+
const payload = item.event?.payload;
|
|
247
|
+
return payload?.metadata?.imported === true || payload?.metadata?.sourceRef !== undefined;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function configWithDefinedOverrides(base, overrides) {
|
|
251
|
+
const config = { ...base };
|
|
252
|
+
if (overrides.maxEvents !== undefined && validThreshold(overrides.maxEvents, 20, 500))
|
|
253
|
+
config.maxEvents = overrides.maxEvents;
|
|
254
|
+
if (overrides.maxDurationMs !== undefined && validThreshold(overrides.maxDurationMs, 300_000, 86_400_000))
|
|
255
|
+
config.maxDurationMs = overrides.maxDurationMs;
|
|
256
|
+
if (overrides.maxIdleGapMs !== undefined && validThreshold(overrides.maxIdleGapMs, 300_000, 86_400_000))
|
|
257
|
+
config.maxIdleGapMs = overrides.maxIdleGapMs;
|
|
258
|
+
if (typeof overrides.timezone === 'string' && overrides.timezone.trim())
|
|
259
|
+
config.timezone = overrides.timezone;
|
|
260
|
+
return config;
|
|
261
|
+
}
|
|
262
|
+
function validThreshold(value, min, max) {
|
|
263
|
+
return value !== undefined && Number.isFinite(value) && value >= min && value <= max;
|
|
264
|
+
}
|
|
265
|
+
function boundaryOverrideWarnings(overrides) {
|
|
266
|
+
const warnings = [];
|
|
267
|
+
if (overrides.maxEvents !== undefined && !validThreshold(overrides.maxEvents, 20, 500))
|
|
268
|
+
warnings.push('invalid_episode_boundary_max_events');
|
|
269
|
+
if (overrides.maxDurationMs !== undefined && !validThreshold(overrides.maxDurationMs, 300_000, 86_400_000))
|
|
270
|
+
warnings.push('invalid_episode_boundary_max_duration_ms');
|
|
271
|
+
if (overrides.maxIdleGapMs !== undefined && !validThreshold(overrides.maxIdleGapMs, 300_000, 86_400_000))
|
|
272
|
+
warnings.push('invalid_episode_boundary_max_idle_gap_ms');
|
|
273
|
+
if (overrides.timezone !== undefined && !overrides.timezone.trim())
|
|
274
|
+
warnings.push('invalid_episode_boundary_timezone');
|
|
275
|
+
return warnings;
|
|
276
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { MemoryEvent } from '../types/index.js';
|
|
2
|
+
export type EpisodeBoundaryMode = 'off' | 'shadow' | 'enforce';
|
|
3
|
+
export type EpisodeBoundaryGuardCode = 'max_events_exceeded' | 'max_duration_exceeded' | 'max_idle_gap_exceeded' | 'trusted_local_date_changed';
|
|
4
|
+
export interface EpisodeBoundaryWarning {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EpisodeBoundaryConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
mode: EpisodeBoundaryMode;
|
|
11
|
+
maxEvents: number;
|
|
12
|
+
maxDurationMs: number;
|
|
13
|
+
maxIdleGapMs: number;
|
|
14
|
+
splitOnTrustedLocalDateChange: boolean;
|
|
15
|
+
auditDecisions: boolean;
|
|
16
|
+
applyToLive: boolean;
|
|
17
|
+
applyToImports: boolean;
|
|
18
|
+
policyVersion: string;
|
|
19
|
+
timezone?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface EpisodeBoundaryMetrics {
|
|
22
|
+
activeEventCount: number;
|
|
23
|
+
activeStartedAt?: number;
|
|
24
|
+
activeUpdatedAt?: number;
|
|
25
|
+
elapsedMs?: number;
|
|
26
|
+
idleGapMs?: number;
|
|
27
|
+
trustedLocalDates: string[];
|
|
28
|
+
lastTrustedLocalDate?: string;
|
|
29
|
+
currentTrustedLocalDate?: string;
|
|
30
|
+
outOfOrderTimestamp?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface EpisodeBoundaryGuardResult {
|
|
33
|
+
mode: EpisodeBoundaryMode;
|
|
34
|
+
guardAction: 'none' | 'shadow_new_episode' | 'enforce_new_episode';
|
|
35
|
+
guardCodes: EpisodeBoundaryGuardCode[];
|
|
36
|
+
metrics: EpisodeBoundaryMetrics;
|
|
37
|
+
warnings: EpisodeBoundaryWarning[];
|
|
38
|
+
policyVersion: string;
|
|
39
|
+
}
|
|
40
|
+
export declare const DEFAULT_EPISODE_BOUNDARY_CONFIG: EpisodeBoundaryConfig;
|
|
41
|
+
export interface EpisodeBoundaryConfigDiagnostic {
|
|
42
|
+
severity: 'warning' | 'error';
|
|
43
|
+
code: string;
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function normalizeEpisodeBoundaryConfig(input?: Partial<EpisodeBoundaryConfig>): {
|
|
47
|
+
config: EpisodeBoundaryConfig;
|
|
48
|
+
diagnostics: EpisodeBoundaryConfigDiagnostic[];
|
|
49
|
+
};
|
|
50
|
+
export declare class EpisodeBoundaryPolicy {
|
|
51
|
+
readonly config: EpisodeBoundaryConfig;
|
|
52
|
+
constructor(config?: Partial<EpisodeBoundaryConfig>);
|
|
53
|
+
evaluate(input: {
|
|
54
|
+
active?: {
|
|
55
|
+
eventCount: number;
|
|
56
|
+
startedAt?: number;
|
|
57
|
+
updatedAt?: number;
|
|
58
|
+
localDates?: string[];
|
|
59
|
+
lastTrustedLocalDate?: string;
|
|
60
|
+
};
|
|
61
|
+
primaryEvent: Pick<MemoryEvent, 'role' | 'occurredAt' | 'localDate' | 'payload'>;
|
|
62
|
+
imported?: boolean;
|
|
63
|
+
}): EpisodeBoundaryGuardResult;
|
|
64
|
+
}
|
|
65
|
+
export declare function isTrustedLocalDate(value: string | undefined): value is string;
|
|
66
|
+
export declare function resolveTrustedLocalDate(event: {
|
|
67
|
+
occurredAt?: number;
|
|
68
|
+
localDate?: string;
|
|
69
|
+
localDateSource?: 'explicit' | 'generated_utc' | 'legacy_unknown';
|
|
70
|
+
payload?: unknown;
|
|
71
|
+
} | undefined, timezone?: string): {
|
|
72
|
+
date?: string;
|
|
73
|
+
warning?: EpisodeBoundaryWarning;
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=EpisodeBoundaryPolicy.d.ts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export const DEFAULT_EPISODE_BOUNDARY_CONFIG = {
|
|
2
|
+
enabled: true,
|
|
3
|
+
mode: 'enforce',
|
|
4
|
+
maxEvents: 100,
|
|
5
|
+
maxDurationMs: 7_200_000,
|
|
6
|
+
maxIdleGapMs: 1_800_000,
|
|
7
|
+
splitOnTrustedLocalDateChange: true,
|
|
8
|
+
auditDecisions: true,
|
|
9
|
+
applyToLive: true,
|
|
10
|
+
applyToImports: true,
|
|
11
|
+
policyVersion: 'episode_boundary.v1',
|
|
12
|
+
};
|
|
13
|
+
export function normalizeEpisodeBoundaryConfig(input = {}) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
const config = { ...DEFAULT_EPISODE_BOUNDARY_CONFIG, ...input };
|
|
16
|
+
config.enabled = boundedBoolean(input.enabled, DEFAULT_EPISODE_BOUNDARY_CONFIG.enabled, 'enabled', diagnostics);
|
|
17
|
+
config.auditDecisions = boundedBoolean(input.auditDecisions, DEFAULT_EPISODE_BOUNDARY_CONFIG.auditDecisions, 'audit_decisions', diagnostics);
|
|
18
|
+
config.applyToLive = boundedBoolean(input.applyToLive, DEFAULT_EPISODE_BOUNDARY_CONFIG.applyToLive, 'apply_to_live', diagnostics);
|
|
19
|
+
config.applyToImports = boundedBoolean(input.applyToImports, DEFAULT_EPISODE_BOUNDARY_CONFIG.applyToImports, 'apply_to_imports', diagnostics);
|
|
20
|
+
config.splitOnTrustedLocalDateChange = boundedBoolean(input.splitOnTrustedLocalDateChange, DEFAULT_EPISODE_BOUNDARY_CONFIG.splitOnTrustedLocalDateChange, 'split_on_trusted_local_date_change', diagnostics);
|
|
21
|
+
if (input.mode === 'off' || input.mode === 'shadow' || input.mode === 'enforce' || input.mode === undefined) {
|
|
22
|
+
config.mode = input.mode ?? DEFAULT_EPISODE_BOUNDARY_CONFIG.mode;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
severity: 'warning',
|
|
27
|
+
code: 'invalid_episode_boundary_mode',
|
|
28
|
+
message: 'episode_boundary.mode must be off, shadow, or enforce. Falling back to shadow.',
|
|
29
|
+
});
|
|
30
|
+
config.mode = 'shadow';
|
|
31
|
+
}
|
|
32
|
+
config.maxEvents = boundedInt(input.maxEvents, 20, 500, DEFAULT_EPISODE_BOUNDARY_CONFIG.maxEvents, 'max_events', diagnostics);
|
|
33
|
+
config.maxDurationMs = boundedInt(input.maxDurationMs, 300_000, 86_400_000, DEFAULT_EPISODE_BOUNDARY_CONFIG.maxDurationMs, 'max_duration_ms', diagnostics);
|
|
34
|
+
config.maxIdleGapMs = boundedInt(input.maxIdleGapMs, 300_000, 86_400_000, DEFAULT_EPISODE_BOUNDARY_CONFIG.maxIdleGapMs, 'max_idle_gap_ms', diagnostics);
|
|
35
|
+
if (input.policyVersion !== undefined && (typeof input.policyVersion !== 'string' || input.policyVersion.trim() === '')) {
|
|
36
|
+
diagnostics.push({ severity: 'warning', code: 'invalid_episode_boundary_policy_version', message: 'episode_boundary.policy_version must be a non-empty string.' });
|
|
37
|
+
config.policyVersion = DEFAULT_EPISODE_BOUNDARY_CONFIG.policyVersion;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
config.policyVersion = input.policyVersion ?? DEFAULT_EPISODE_BOUNDARY_CONFIG.policyVersion;
|
|
41
|
+
}
|
|
42
|
+
if (input.timezone !== undefined && (typeof input.timezone !== 'string' || input.timezone.trim() === '')) {
|
|
43
|
+
diagnostics.push({ severity: 'warning', code: 'invalid_episode_boundary_timezone', message: 'episode_boundary.timezone must be a non-empty valid IANA timezone.' });
|
|
44
|
+
config.timezone = undefined;
|
|
45
|
+
}
|
|
46
|
+
else if (input.timezone !== undefined) {
|
|
47
|
+
try {
|
|
48
|
+
new Intl.DateTimeFormat('en-US', { timeZone: input.timezone }).format(0);
|
|
49
|
+
config.timezone = input.timezone;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
diagnostics.push({ severity: 'warning', code: 'invalid_episode_boundary_timezone', message: 'episode_boundary.timezone must be a valid IANA timezone.' });
|
|
53
|
+
config.timezone = undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { config, diagnostics };
|
|
57
|
+
}
|
|
58
|
+
export class EpisodeBoundaryPolicy {
|
|
59
|
+
config;
|
|
60
|
+
constructor(config = {}) {
|
|
61
|
+
this.config = normalizeEpisodeBoundaryConfig(config).config;
|
|
62
|
+
}
|
|
63
|
+
evaluate(input) {
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const localDates = [...new Set(input.active?.localDates || [])];
|
|
66
|
+
const shouldEvaluate = Boolean(input.active
|
|
67
|
+
&& input.primaryEvent.role === 'user'
|
|
68
|
+
&& this.config.enabled
|
|
69
|
+
&& this.config.mode !== 'off'
|
|
70
|
+
&& (input.imported ? this.config.applyToImports : this.config.applyToLive));
|
|
71
|
+
const localDate = shouldEvaluate && this.config.splitOnTrustedLocalDateChange
|
|
72
|
+
? trustedLocalDate(input.primaryEvent, this.config.timezone, warnings)
|
|
73
|
+
: undefined;
|
|
74
|
+
const currentTime = input.primaryEvent.occurredAt;
|
|
75
|
+
const outOfOrderTimestamp = shouldEvaluate && input.active?.updatedAt !== undefined
|
|
76
|
+
&& currentTime !== undefined && currentTime < input.active.updatedAt;
|
|
77
|
+
if (outOfOrderTimestamp)
|
|
78
|
+
warnings.push({ code: 'out_of_order_timestamp', message: 'Event timestamp is earlier than the active episode update time.' });
|
|
79
|
+
const metrics = {
|
|
80
|
+
activeEventCount: input.active?.eventCount ?? 0,
|
|
81
|
+
activeStartedAt: input.active?.startedAt,
|
|
82
|
+
activeUpdatedAt: input.active?.updatedAt,
|
|
83
|
+
elapsedMs: input.active?.startedAt !== undefined && input.primaryEvent.occurredAt !== undefined
|
|
84
|
+
? input.primaryEvent.occurredAt - input.active.startedAt : undefined,
|
|
85
|
+
idleGapMs: input.active?.updatedAt !== undefined && input.primaryEvent.occurredAt !== undefined
|
|
86
|
+
? input.primaryEvent.occurredAt - input.active.updatedAt : undefined,
|
|
87
|
+
trustedLocalDates: localDates,
|
|
88
|
+
lastTrustedLocalDate: input.active?.lastTrustedLocalDate,
|
|
89
|
+
currentTrustedLocalDate: localDate,
|
|
90
|
+
outOfOrderTimestamp,
|
|
91
|
+
};
|
|
92
|
+
const guardCodes = [];
|
|
93
|
+
if (shouldEvaluate && input.active) {
|
|
94
|
+
if (input.active.eventCount >= this.config.maxEvents)
|
|
95
|
+
guardCodes.push('max_events_exceeded');
|
|
96
|
+
// A late arrival belongs to historical/review handling. It must not
|
|
97
|
+
// retroactively seal the current segment through clock/date guards.
|
|
98
|
+
if (!outOfOrderTimestamp) {
|
|
99
|
+
if ((metrics.elapsedMs ?? 0) > this.config.maxDurationMs)
|
|
100
|
+
guardCodes.push('max_duration_exceeded');
|
|
101
|
+
if ((metrics.idleGapMs ?? 0) > this.config.maxIdleGapMs)
|
|
102
|
+
guardCodes.push('max_idle_gap_exceeded');
|
|
103
|
+
if (this.config.splitOnTrustedLocalDateChange && localDate && input.active.lastTrustedLocalDate && input.active.lastTrustedLocalDate !== localDate) {
|
|
104
|
+
guardCodes.push('trusted_local_date_changed');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
mode: this.config.mode,
|
|
110
|
+
guardAction: guardCodes.length ? (this.config.mode === 'shadow' ? 'shadow_new_episode' : 'enforce_new_episode') : 'none',
|
|
111
|
+
guardCodes,
|
|
112
|
+
metrics,
|
|
113
|
+
warnings,
|
|
114
|
+
policyVersion: this.config.policyVersion,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function boundedInt(value, min, max, fallback, name, diagnostics) {
|
|
119
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
|
|
120
|
+
if (value !== undefined)
|
|
121
|
+
diagnostics.push({
|
|
122
|
+
severity: 'warning',
|
|
123
|
+
code: `invalid_episode_boundary_${name}`,
|
|
124
|
+
message: `episode_boundary.${name} must be between ${min} and ${max}.`,
|
|
125
|
+
});
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
128
|
+
return Math.trunc(value);
|
|
129
|
+
}
|
|
130
|
+
function boundedBoolean(value, fallback, name, diagnostics) {
|
|
131
|
+
if (value === undefined)
|
|
132
|
+
return fallback;
|
|
133
|
+
if (typeof value === 'boolean')
|
|
134
|
+
return value;
|
|
135
|
+
diagnostics.push({
|
|
136
|
+
severity: 'warning',
|
|
137
|
+
code: `invalid_episode_boundary_${name}`,
|
|
138
|
+
message: `episode_boundary.${name} must be a boolean.`,
|
|
139
|
+
});
|
|
140
|
+
return fallback;
|
|
141
|
+
}
|
|
142
|
+
export function isTrustedLocalDate(value) {
|
|
143
|
+
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
144
|
+
return false;
|
|
145
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
146
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
147
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
148
|
+
}
|
|
149
|
+
export function resolveTrustedLocalDate(event, timezone) {
|
|
150
|
+
if (!event)
|
|
151
|
+
return { warning: { code: 'trusted_local_date_unavailable', message: 'No trusted local date source was available.' } };
|
|
152
|
+
if (event.localDate && !isTrustedLocalDate(event.localDate)) {
|
|
153
|
+
return { warning: { code: 'invalid_trusted_local_date', message: 'Trusted local date must use YYYY-MM-DD.' } };
|
|
154
|
+
}
|
|
155
|
+
const metadata = event.payload?.metadata;
|
|
156
|
+
const legacyMetadataSource = metadata?.localDateSource === 'event_store_utc_default' ? 'generated_utc' : metadata?.localDateSource;
|
|
157
|
+
const localDateSource = event.localDateSource ?? legacyMetadataSource ?? (event.localDate ? 'explicit' : undefined);
|
|
158
|
+
if (event.localDate && localDateSource === 'explicit')
|
|
159
|
+
return { date: event.localDate };
|
|
160
|
+
if (event.localDate && localDateSource === 'legacy_unknown') {
|
|
161
|
+
return { warning: { code: 'legacy_unknown_local_date_source', message: 'Legacy local date source is not trusted for boundary decisions.' } };
|
|
162
|
+
}
|
|
163
|
+
if (timezone && typeof event.occurredAt === 'number' && Number.isFinite(event.occurredAt)) {
|
|
164
|
+
const utcDate = new Date(event.occurredAt).toISOString().slice(0, 10);
|
|
165
|
+
if (!event.localDate || (localDateSource === 'generated_utc' && event.localDate === utcDate))
|
|
166
|
+
return { date: localDateInTimezone(event.occurredAt, timezone) };
|
|
167
|
+
}
|
|
168
|
+
if (event.localDate && localDateSource === 'generated_utc' && isTrustedLocalDate(event.localDate))
|
|
169
|
+
return { date: event.localDate };
|
|
170
|
+
return { warning: { code: 'trusted_local_date_unavailable', message: 'No trusted local date source was available.' } };
|
|
171
|
+
}
|
|
172
|
+
function localDateInTimezone(occurredAt, timezone) {
|
|
173
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
174
|
+
timeZone: timezone,
|
|
175
|
+
year: 'numeric',
|
|
176
|
+
month: '2-digit',
|
|
177
|
+
day: '2-digit',
|
|
178
|
+
}).formatToParts(occurredAt);
|
|
179
|
+
const year = parts.find((part) => part.type === 'year')?.value;
|
|
180
|
+
const month = parts.find((part) => part.type === 'month')?.value;
|
|
181
|
+
const day = parts.find((part) => part.type === 'day')?.value;
|
|
182
|
+
return year && month && day ? `${year}-${month}-${day}` : new Date(occurredAt).toISOString().slice(0, 10);
|
|
183
|
+
}
|
|
184
|
+
function trustedLocalDate(event, timezone, warnings) {
|
|
185
|
+
const resolved = resolveTrustedLocalDate(event, timezone);
|
|
186
|
+
if (resolved.warning)
|
|
187
|
+
warnings.push(resolved.warning);
|
|
188
|
+
return resolved.date;
|
|
189
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { MemoryEvent } from '../types/index.js';
|
|
2
|
+
import type { EpisodeBoundaryConfig, EpisodeBoundaryGuardCode, EpisodeBoundaryGuardResult } from './EpisodeBoundaryPolicy.js';
|
|
3
|
+
import type { EpisodeEventLink, EpisodeStatus, TurnRelation } from './EpisodeTypes.js';
|
|
4
|
+
export type EpisodeReplayPair = {
|
|
5
|
+
link: EpisodeEventLink;
|
|
6
|
+
event?: MemoryEvent;
|
|
7
|
+
};
|
|
8
|
+
export interface EpisodeBoundaryReplayState {
|
|
9
|
+
eventCount: number;
|
|
10
|
+
startedAt?: number;
|
|
11
|
+
lastEventAt?: number;
|
|
12
|
+
lastTrustedUserLocalDate?: string;
|
|
13
|
+
trustedLocalDates: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface EpisodeBoundaryReplayBoundary {
|
|
16
|
+
boundaryIndex: number;
|
|
17
|
+
beforeEventId?: string;
|
|
18
|
+
afterEventId?: string;
|
|
19
|
+
primaryUserEventId?: string;
|
|
20
|
+
relation?: TurnRelation;
|
|
21
|
+
reason: string;
|
|
22
|
+
guardCodes: EpisodeBoundaryGuardCode[];
|
|
23
|
+
warnings: string[];
|
|
24
|
+
detected: boolean;
|
|
25
|
+
effective: boolean;
|
|
26
|
+
disposition: 'explicit' | 'soft_review' | 'shadow' | 'enforced';
|
|
27
|
+
guardResult?: EpisodeBoundaryGuardResult;
|
|
28
|
+
}
|
|
29
|
+
export interface EpisodeBoundaryReplayResult {
|
|
30
|
+
logicalTurns: EpisodeReplayPair[][];
|
|
31
|
+
detectedBoundaries: EpisodeBoundaryReplayBoundary[];
|
|
32
|
+
effectiveBoundaries: EpisodeBoundaryReplayBoundary[];
|
|
33
|
+
warnings: string[];
|
|
34
|
+
structuralAnomalies: string[];
|
|
35
|
+
runningState: EpisodeBoundaryReplayState;
|
|
36
|
+
policyDisposition: 'disabled' | 'shadow' | 'enforce';
|
|
37
|
+
}
|
|
38
|
+
export declare function replayEpisodeBoundaries(input: {
|
|
39
|
+
episode: {
|
|
40
|
+
startedAt?: number;
|
|
41
|
+
status?: EpisodeStatus;
|
|
42
|
+
};
|
|
43
|
+
pairs: EpisodeReplayPair[];
|
|
44
|
+
config: EpisodeBoundaryConfig;
|
|
45
|
+
imported?: boolean;
|
|
46
|
+
live?: boolean;
|
|
47
|
+
initialState?: EpisodeBoundaryReplayState;
|
|
48
|
+
}): EpisodeBoundaryReplayResult;
|
|
49
|
+
/** Reduces accepted evidence without evaluating a new boundary. */
|
|
50
|
+
export declare function replayEpisodeBoundaryState(input: {
|
|
51
|
+
episode: {
|
|
52
|
+
startedAt?: number;
|
|
53
|
+
};
|
|
54
|
+
pairs: EpisodeReplayPair[];
|
|
55
|
+
timezone?: string;
|
|
56
|
+
}): EpisodeBoundaryReplayState;
|
|
57
|
+
export declare function replayPendingTurnBoundary(input: {
|
|
58
|
+
config: EpisodeBoundaryConfig;
|
|
59
|
+
active?: EpisodeBoundaryReplayState;
|
|
60
|
+
pendingPairs: EpisodeReplayPair[];
|
|
61
|
+
primaryEvent: MemoryEvent;
|
|
62
|
+
imported?: boolean;
|
|
63
|
+
}): EpisodeBoundaryGuardResult;
|
|
64
|
+
export declare function logicalTurnsFromPairs(pairs: EpisodeReplayPair[]): EpisodeReplayPair[][];
|
|
65
|
+
//# sourceMappingURL=EpisodeBoundaryReplayEngine.d.ts.map
|