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,272 @@
|
|
|
1
|
+
import { EpisodeBoundaryPolicy, resolveTrustedLocalDate } from './EpisodeBoundaryPolicy.js';
|
|
2
|
+
export function replayEpisodeBoundaries(input) {
|
|
3
|
+
const policy = new EpisodeBoundaryPolicy(input.config);
|
|
4
|
+
const state = input.initialState ? {
|
|
5
|
+
...input.initialState,
|
|
6
|
+
trustedLocalDates: [...input.initialState.trustedLocalDates],
|
|
7
|
+
} : {
|
|
8
|
+
eventCount: 0,
|
|
9
|
+
startedAt: input.episode.startedAt,
|
|
10
|
+
trustedLocalDates: [],
|
|
11
|
+
};
|
|
12
|
+
const logicalTurns = logicalTurnsFromPairs(input.pairs);
|
|
13
|
+
const detectedBoundaries = [];
|
|
14
|
+
const warnings = new Set();
|
|
15
|
+
const anomalies = new Set();
|
|
16
|
+
let pendingClosureAfter;
|
|
17
|
+
const pushBoundary = (boundary) => {
|
|
18
|
+
const item = { ...boundary, boundaryIndex: detectedBoundaries.length, detected: true };
|
|
19
|
+
detectedBoundaries.push(item);
|
|
20
|
+
for (const warning of item.warnings)
|
|
21
|
+
warnings.add(warning);
|
|
22
|
+
};
|
|
23
|
+
for (const turn of logicalTurns) {
|
|
24
|
+
const user = primaryUser(turn);
|
|
25
|
+
const imported = input.imported ?? isImportedTurn(turn);
|
|
26
|
+
let startedAfterClosure = false;
|
|
27
|
+
let effectiveBoundaryBeforeTurn = false;
|
|
28
|
+
if (pendingClosureAfter) {
|
|
29
|
+
pushBoundary({
|
|
30
|
+
beforeEventId: lastPair(pendingClosureAfter)?.link.eventId,
|
|
31
|
+
afterEventId: firstPair(turn)?.link.eventId,
|
|
32
|
+
primaryUserEventId: user?.event?.eventId,
|
|
33
|
+
relation: user?.link.relation,
|
|
34
|
+
reason: 'explicit_closure_boundary',
|
|
35
|
+
guardCodes: [],
|
|
36
|
+
warnings: [],
|
|
37
|
+
effective: true,
|
|
38
|
+
disposition: 'explicit',
|
|
39
|
+
});
|
|
40
|
+
pendingClosureAfter = undefined;
|
|
41
|
+
startedAfterClosure = true;
|
|
42
|
+
effectiveBoundaryBeforeTurn = true;
|
|
43
|
+
}
|
|
44
|
+
if (!user?.event) {
|
|
45
|
+
if (turn.some((pair) => !pair.event))
|
|
46
|
+
anomalies.add('raw_event_missing');
|
|
47
|
+
acceptTurn(state, turn, input.config.timezone, warnings);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const relation = user.link.relation;
|
|
51
|
+
if (startedAfterClosure) {
|
|
52
|
+
// The closure-after boundary already started this segment.
|
|
53
|
+
}
|
|
54
|
+
else if (state.eventCount > 0 && isHardSwitch(relation)) {
|
|
55
|
+
pushBoundary({
|
|
56
|
+
beforeEventId: previousAcceptedEventId(input.pairs, turn),
|
|
57
|
+
afterEventId: firstPair(turn)?.link.eventId,
|
|
58
|
+
primaryUserEventId: user.event.eventId,
|
|
59
|
+
relation,
|
|
60
|
+
reason: 'hard_topic_switch_boundary',
|
|
61
|
+
guardCodes: [],
|
|
62
|
+
warnings: [],
|
|
63
|
+
effective: true,
|
|
64
|
+
disposition: 'explicit',
|
|
65
|
+
});
|
|
66
|
+
effectiveBoundaryBeforeTurn = true;
|
|
67
|
+
}
|
|
68
|
+
else if (state.eventCount > 0 && relation === 'ambiguous_shift') {
|
|
69
|
+
pushBoundary({
|
|
70
|
+
beforeEventId: previousAcceptedEventId(input.pairs, turn),
|
|
71
|
+
afterEventId: firstPair(turn)?.link.eventId,
|
|
72
|
+
primaryUserEventId: user.event.eventId,
|
|
73
|
+
relation,
|
|
74
|
+
reason: 'ambiguous_shift_boundary',
|
|
75
|
+
guardCodes: [],
|
|
76
|
+
warnings: [],
|
|
77
|
+
effective: true,
|
|
78
|
+
disposition: 'soft_review',
|
|
79
|
+
});
|
|
80
|
+
effectiveBoundaryBeforeTurn = true;
|
|
81
|
+
}
|
|
82
|
+
else if (relation !== 'closes_episode') {
|
|
83
|
+
const result = policy.evaluate({
|
|
84
|
+
active: {
|
|
85
|
+
eventCount: state.eventCount,
|
|
86
|
+
startedAt: state.startedAt,
|
|
87
|
+
updatedAt: state.lastEventAt,
|
|
88
|
+
localDates: state.trustedLocalDates,
|
|
89
|
+
lastTrustedLocalDate: state.lastTrustedUserLocalDate,
|
|
90
|
+
},
|
|
91
|
+
primaryEvent: user.event,
|
|
92
|
+
imported,
|
|
93
|
+
});
|
|
94
|
+
for (const warning of result.warnings)
|
|
95
|
+
warnings.add(warning.code);
|
|
96
|
+
if (result.guardCodes.length) {
|
|
97
|
+
const effective = result.guardAction === 'enforce_new_episode';
|
|
98
|
+
pushBoundary({
|
|
99
|
+
beforeEventId: previousAcceptedEventId(input.pairs, turn),
|
|
100
|
+
afterEventId: firstPair(turn)?.link.eventId,
|
|
101
|
+
primaryUserEventId: user.event.eventId,
|
|
102
|
+
relation,
|
|
103
|
+
reason: guardReason(result.guardCodes),
|
|
104
|
+
guardCodes: result.guardCodes,
|
|
105
|
+
warnings: result.warnings.map((warning) => warning.code),
|
|
106
|
+
effective,
|
|
107
|
+
disposition: effective ? 'enforced' : 'shadow',
|
|
108
|
+
guardResult: result,
|
|
109
|
+
});
|
|
110
|
+
effectiveBoundaryBeforeTurn = effective;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (effectiveBoundaryBeforeTurn)
|
|
114
|
+
resetSegmentState(state, turn);
|
|
115
|
+
acceptTurn(state, turn, input.config.timezone, warnings);
|
|
116
|
+
if (relation === 'closes_episode')
|
|
117
|
+
pendingClosureAfter = turn;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
logicalTurns,
|
|
121
|
+
detectedBoundaries,
|
|
122
|
+
effectiveBoundaries: detectedBoundaries.filter((boundary) => boundary.effective),
|
|
123
|
+
warnings: [...warnings].sort(),
|
|
124
|
+
structuralAnomalies: [...anomalies].sort(),
|
|
125
|
+
runningState: state,
|
|
126
|
+
policyDisposition: !input.config.enabled || input.config.mode === 'off' ? 'disabled' : input.config.mode,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Reduces accepted evidence without evaluating a new boundary. */
|
|
130
|
+
export function replayEpisodeBoundaryState(input) {
|
|
131
|
+
const state = {
|
|
132
|
+
eventCount: 0,
|
|
133
|
+
startedAt: input.episode.startedAt,
|
|
134
|
+
trustedLocalDates: [],
|
|
135
|
+
};
|
|
136
|
+
const warnings = new Set();
|
|
137
|
+
for (const turn of logicalTurnsFromPairs(input.pairs))
|
|
138
|
+
acceptTurn(state, turn, input.timezone, warnings);
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
export function replayPendingTurnBoundary(input) {
|
|
142
|
+
const active = input.active;
|
|
143
|
+
const replay = replayEpisodeBoundaries({
|
|
144
|
+
episode: { startedAt: active?.startedAt },
|
|
145
|
+
pairs: input.pendingPairs,
|
|
146
|
+
config: input.config,
|
|
147
|
+
imported: input.imported,
|
|
148
|
+
initialState: active,
|
|
149
|
+
});
|
|
150
|
+
const guarded = replay.detectedBoundaries.find((boundary) => boundary.guardResult)?.guardResult;
|
|
151
|
+
if (guarded)
|
|
152
|
+
return guarded;
|
|
153
|
+
return new EpisodeBoundaryPolicy(input.config).evaluate({
|
|
154
|
+
active: active ? {
|
|
155
|
+
eventCount: active.eventCount,
|
|
156
|
+
startedAt: active.startedAt,
|
|
157
|
+
updatedAt: active.lastEventAt,
|
|
158
|
+
localDates: active.trustedLocalDates,
|
|
159
|
+
lastTrustedLocalDate: active.lastTrustedUserLocalDate,
|
|
160
|
+
} : undefined,
|
|
161
|
+
primaryEvent: input.primaryEvent,
|
|
162
|
+
imported: input.imported,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export function logicalTurnsFromPairs(pairs) {
|
|
166
|
+
const groups = [];
|
|
167
|
+
let current = [];
|
|
168
|
+
let currentTurnKey;
|
|
169
|
+
let currentHasUser = false;
|
|
170
|
+
for (const pair of pairs) {
|
|
171
|
+
const turnKey = pair.event ? turnKeyFor(pair.event) : undefined;
|
|
172
|
+
const explicitTurnChange = Boolean(currentTurnKey && turnKey && currentTurnKey !== turnKey);
|
|
173
|
+
const roleBoundary = pair.event?.role === 'user' && currentHasUser && !explicitTurnChange;
|
|
174
|
+
if (current.length && (explicitTurnChange || roleBoundary)) {
|
|
175
|
+
groups.push(current);
|
|
176
|
+
current = [];
|
|
177
|
+
currentTurnKey = undefined;
|
|
178
|
+
currentHasUser = false;
|
|
179
|
+
}
|
|
180
|
+
current.push(pair);
|
|
181
|
+
if (turnKey && !currentTurnKey)
|
|
182
|
+
currentTurnKey = turnKey;
|
|
183
|
+
if (pair.event?.role === 'user')
|
|
184
|
+
currentHasUser = true;
|
|
185
|
+
}
|
|
186
|
+
if (current.length)
|
|
187
|
+
groups.push(current);
|
|
188
|
+
return groups;
|
|
189
|
+
}
|
|
190
|
+
function acceptTurn(state, turn, timezone, warnings) {
|
|
191
|
+
for (const pair of turn) {
|
|
192
|
+
const event = pair.event;
|
|
193
|
+
const occurredAt = event?.occurredAt;
|
|
194
|
+
const hasOccurredAt = typeof occurredAt === 'number' && Number.isFinite(occurredAt);
|
|
195
|
+
const outOfOrder = hasOccurredAt && state.lastEventAt !== undefined && occurredAt < state.lastEventAt;
|
|
196
|
+
if (event?.role === 'user') {
|
|
197
|
+
const resolved = resolveTrustedLocalDate(event, timezone);
|
|
198
|
+
if (resolved.date && !outOfOrder) {
|
|
199
|
+
state.lastTrustedUserLocalDate = resolved.date;
|
|
200
|
+
if (!state.trustedLocalDates.includes(resolved.date))
|
|
201
|
+
state.trustedLocalDates.push(resolved.date);
|
|
202
|
+
}
|
|
203
|
+
else if (resolved.warning && resolved.warning.code !== 'trusted_local_date_unavailable') {
|
|
204
|
+
warnings.add(resolved.warning.code);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (hasOccurredAt) {
|
|
208
|
+
if (outOfOrder) {
|
|
209
|
+
warnings.add('out_of_order_timestamp');
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
state.lastEventAt = Math.max(state.lastEventAt ?? occurredAt, occurredAt);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (state.startedAt === undefined && hasOccurredAt)
|
|
216
|
+
state.startedAt = occurredAt;
|
|
217
|
+
state.eventCount += 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function resetSegmentState(state, turn) {
|
|
221
|
+
const firstTimestamp = turn
|
|
222
|
+
.map((pair) => pair.event?.occurredAt)
|
|
223
|
+
.find((value) => typeof value === 'number' && Number.isFinite(value));
|
|
224
|
+
state.eventCount = 0;
|
|
225
|
+
state.startedAt = firstTimestamp;
|
|
226
|
+
state.lastEventAt = undefined;
|
|
227
|
+
state.lastTrustedUserLocalDate = undefined;
|
|
228
|
+
state.trustedLocalDates = [];
|
|
229
|
+
}
|
|
230
|
+
function primaryUser(turn) {
|
|
231
|
+
return turn.find((pair) => pair.event?.role === 'user');
|
|
232
|
+
}
|
|
233
|
+
function firstPair(turn) {
|
|
234
|
+
return turn[0];
|
|
235
|
+
}
|
|
236
|
+
function lastPair(turn) {
|
|
237
|
+
return turn.at(-1);
|
|
238
|
+
}
|
|
239
|
+
function previousAcceptedEventId(allPairs, turn) {
|
|
240
|
+
const first = firstPair(turn);
|
|
241
|
+
if (!first)
|
|
242
|
+
return undefined;
|
|
243
|
+
const index = allPairs.indexOf(first);
|
|
244
|
+
return index > 0 ? allPairs[index - 1]?.link.eventId : undefined;
|
|
245
|
+
}
|
|
246
|
+
function turnKeyFor(event) {
|
|
247
|
+
if (event.turnId)
|
|
248
|
+
return `id:${event.turnId}`;
|
|
249
|
+
if (typeof event.turnSeq === 'number')
|
|
250
|
+
return `seq:${event.turnSeq}`;
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
function isHardSwitch(relation) {
|
|
254
|
+
return relation === 'hard_topic_switch' || relation === 'starts_new_topic' || relation === 'switches_topic';
|
|
255
|
+
}
|
|
256
|
+
function isImportedTurn(turn) {
|
|
257
|
+
return turn.some((pair) => {
|
|
258
|
+
const payload = pair.event?.payload;
|
|
259
|
+
return payload?.metadata?.imported === true || payload?.metadata?.sourceRef !== undefined;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function guardReason(codes) {
|
|
263
|
+
if (codes.includes('max_events_exceeded'))
|
|
264
|
+
return 'max_events_boundary';
|
|
265
|
+
if (codes.includes('trusted_local_date_changed'))
|
|
266
|
+
return 'trusted_local_date_boundary';
|
|
267
|
+
if (codes.includes('max_duration_exceeded'))
|
|
268
|
+
return 'max_duration_boundary';
|
|
269
|
+
if (codes.includes('max_idle_gap_exceeded'))
|
|
270
|
+
return 'max_idle_gap_boundary';
|
|
271
|
+
return codes[0] ?? 'policy_boundary';
|
|
272
|
+
}
|
|
@@ -2,6 +2,7 @@ export interface StableImportIdentityInput {
|
|
|
2
2
|
role: string;
|
|
3
3
|
text: string;
|
|
4
4
|
timestamp?: number;
|
|
5
|
+
sourcePosition?: string | number;
|
|
5
6
|
}
|
|
6
7
|
export declare function createStableImportIdentityFactory(sourceAgent: string, sourceSessionId: string): (input: StableImportIdentityInput) => string;
|
|
7
8
|
//# sourceMappingURL=EpisodeImportIdentity.d.ts.map
|
|
@@ -8,6 +8,7 @@ export function createStableImportIdentityFactory(sourceAgent, sourceSessionId)
|
|
|
8
8
|
sourceSessionId,
|
|
9
9
|
input.role,
|
|
10
10
|
input.timestamp ?? null,
|
|
11
|
+
input.sourcePosition ?? null,
|
|
11
12
|
createHash('sha256').update(normalizedText).digest('hex'),
|
|
12
13
|
])).digest('hex');
|
|
13
14
|
const occurrence = (occurrences.get(base) || 0) + 1;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type EpisodeReplayPair } from './EpisodeBoundaryReplayEngine.js';
|
|
2
|
+
import type { EpisodeClosureReceipt, MemoryEpisode } from './EpisodeTypes.js';
|
|
3
|
+
export interface EpisodeInvariantViolation {
|
|
4
|
+
reason: string;
|
|
5
|
+
severity: 'info' | 'warning' | 'critical';
|
|
6
|
+
recommendedAction: 'none' | 'inspect' | 'split-plan';
|
|
7
|
+
requiresManualReview: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function validateEpisodeInvariants(input: {
|
|
10
|
+
episode: MemoryEpisode;
|
|
11
|
+
pairs: EpisodeReplayPair[];
|
|
12
|
+
timezone?: string;
|
|
13
|
+
closureReceipts?: EpisodeClosureReceipt[];
|
|
14
|
+
dreamJobState?: string;
|
|
15
|
+
}): EpisodeInvariantViolation[];
|
|
16
|
+
//# sourceMappingURL=EpisodeInvariantValidator.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { resolveTrustedLocalDate } from './EpisodeBoundaryPolicy.js';
|
|
2
|
+
import { logicalTurnsFromPairs } from './EpisodeBoundaryReplayEngine.js';
|
|
3
|
+
export function validateEpisodeInvariants(input) {
|
|
4
|
+
const violations = [];
|
|
5
|
+
const add = (reason, severity = 'warning') => {
|
|
6
|
+
violations.push({
|
|
7
|
+
reason,
|
|
8
|
+
severity,
|
|
9
|
+
recommendedAction: ['closure_receipt_status_mismatch', 'dream_job_status_mismatch'].includes(reason) ? 'inspect' : severity === 'critical' ? 'split-plan' : 'inspect',
|
|
10
|
+
requiresManualReview: severity !== 'info',
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
const { episode, pairs } = input;
|
|
14
|
+
if (episode.eventCount !== pairs.length)
|
|
15
|
+
add('stored_actual_event_count_mismatch', 'critical');
|
|
16
|
+
if (!positionsAreContiguous(pairs))
|
|
17
|
+
add('duplicate_or_gapped_positions', 'critical');
|
|
18
|
+
if (pairs.length) {
|
|
19
|
+
if (episode.startEventId !== pairs[0].link.eventId || episode.endEventId !== pairs.at(-1)?.link.eventId) {
|
|
20
|
+
add('start_end_pointer_mismatch', 'critical');
|
|
21
|
+
}
|
|
22
|
+
const firstEvent = pairs[0].event;
|
|
23
|
+
const lastEvent = pairs.at(-1)?.event;
|
|
24
|
+
if (firstEvent?.globalSeq !== undefined && episode.startSeq !== undefined && firstEvent.globalSeq !== episode.startSeq)
|
|
25
|
+
add('start_end_seq_mismatch', 'critical');
|
|
26
|
+
if (lastEvent?.globalSeq !== undefined && episode.endSeq !== undefined && lastEvent.globalSeq !== episode.endSeq)
|
|
27
|
+
add('start_end_seq_mismatch', 'critical');
|
|
28
|
+
}
|
|
29
|
+
else if (episode.startEventId || episode.endEventId) {
|
|
30
|
+
add('empty_episode_pointer_mismatch', 'critical');
|
|
31
|
+
}
|
|
32
|
+
if (pairs.some((pair) => !pair.event))
|
|
33
|
+
add('raw_event_missing', 'critical');
|
|
34
|
+
if (pairs.some((pair) => scopeMismatch(episode, pair.event)))
|
|
35
|
+
add('event_scope_mismatch', 'critical');
|
|
36
|
+
for (const pair of pairs) {
|
|
37
|
+
if (pair.event?.role === 'user') {
|
|
38
|
+
const resolved = resolveTrustedLocalDate(pair.event, input.timezone);
|
|
39
|
+
if (resolved.warning?.code === 'invalid_trusted_local_date')
|
|
40
|
+
add('invalid_local_date', 'warning');
|
|
41
|
+
if (resolved.warning?.code === 'legacy_unknown_local_date_source')
|
|
42
|
+
add('legacy_unknown_local_date_source', 'warning');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const turns = logicalTurnsFromPairs(pairs);
|
|
46
|
+
const userTurns = turns.filter((turn) => turn.some((pair) => pair.event?.role === 'user'));
|
|
47
|
+
userTurns.forEach((turn, index) => {
|
|
48
|
+
const user = turn.find((pair) => pair.event?.role === 'user');
|
|
49
|
+
if (!user)
|
|
50
|
+
return;
|
|
51
|
+
const relation = user.link.relation;
|
|
52
|
+
if (['hard_topic_switch', 'starts_new_topic', 'switches_topic'].includes(relation) && index !== 0)
|
|
53
|
+
add('invalid_hard_shift_position', 'critical');
|
|
54
|
+
if (relation === 'ambiguous_shift' && index !== 0)
|
|
55
|
+
add('invalid_ambiguous_shift_position', 'critical');
|
|
56
|
+
if (relation === 'closes_episode' && index !== userTurns.length - 1)
|
|
57
|
+
add('continuation_after_closure', 'critical');
|
|
58
|
+
});
|
|
59
|
+
const receipts = input.closureReceipts || [];
|
|
60
|
+
if ((episode.status === 'sealed' || episode.status === 'soft_sealed') && receipts.length === 0)
|
|
61
|
+
add('closure_receipt_status_mismatch', 'critical');
|
|
62
|
+
if (episode.status === 'open' && receipts.length > 0 && userTurns.some((turn) => turn.some((pair) => pair.link.relation === 'closes_episode')))
|
|
63
|
+
add('closure_receipt_status_mismatch', 'critical');
|
|
64
|
+
if (input.dreamJobState && episode.status !== 'sealed' && ['pending', 'processing', 'retry_scheduled', 'failed_retryable'].includes(input.dreamJobState))
|
|
65
|
+
add('dream_job_status_mismatch', 'critical');
|
|
66
|
+
if (input.dreamJobState === 'processed' && episode.dreamStatus !== 'processed')
|
|
67
|
+
add('dream_job_status_mismatch', 'critical');
|
|
68
|
+
return stableViolations(violations);
|
|
69
|
+
}
|
|
70
|
+
function positionsAreContiguous(pairs) {
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
for (let index = 0; index < pairs.length; index += 1) {
|
|
73
|
+
const position = pairs[index].link.position;
|
|
74
|
+
if (seen.has(position) || position !== index + 1)
|
|
75
|
+
return false;
|
|
76
|
+
seen.add(position);
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function scopeMismatch(episode, event) {
|
|
81
|
+
if (!event)
|
|
82
|
+
return false;
|
|
83
|
+
if (event.projectId !== episode.projectId)
|
|
84
|
+
return true;
|
|
85
|
+
if (event.sessionId !== episode.sessionId)
|
|
86
|
+
return true;
|
|
87
|
+
if ((episode.conversationThreadId || '') !== (event.threadId || ''))
|
|
88
|
+
return true;
|
|
89
|
+
const metadata = event.payload?.metadata;
|
|
90
|
+
if (episode.sourceAgent && metadata?.sourceAgent !== undefined && metadata.sourceAgent !== episode.sourceAgent)
|
|
91
|
+
return true;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function stableViolations(violations) {
|
|
95
|
+
const byReason = new Map();
|
|
96
|
+
for (const violation of violations)
|
|
97
|
+
byReason.set(violation.reason, violation);
|
|
98
|
+
return [...byReason.values()].sort((left, right) => left.reason.localeCompare(right.reason));
|
|
99
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { MemoryEvent } from '../types/index.js';
|
|
2
|
+
import type { EpisodeStore } from './EpisodeStore.js';
|
|
3
|
+
import type { TurnRelation } from './EpisodeTypes.js';
|
|
4
|
+
import type { EpisodeBoundaryConfig } from './EpisodeBoundaryPolicy.js';
|
|
5
|
+
export declare const EPISODE_SPLIT_PLANNER_VERSION = "episode_split_preview.v1";
|
|
6
|
+
export interface EpisodeSplitPlanSegment {
|
|
7
|
+
segmentIndex: number;
|
|
8
|
+
eventIds?: string[];
|
|
9
|
+
eventIdsHash: string;
|
|
10
|
+
eventIdsOmitted: number;
|
|
11
|
+
startEventId?: string;
|
|
12
|
+
endEventId?: string;
|
|
13
|
+
eventCount: number;
|
|
14
|
+
reason: string;
|
|
15
|
+
startedAt?: number;
|
|
16
|
+
endedAt?: number;
|
|
17
|
+
userEventCount: number;
|
|
18
|
+
assistantEventCount: number;
|
|
19
|
+
toolEventCount: number;
|
|
20
|
+
systemEventCount: number;
|
|
21
|
+
}
|
|
22
|
+
export interface EpisodeSplitProposedBoundary {
|
|
23
|
+
boundaryIndex: number;
|
|
24
|
+
beforeEventId?: string;
|
|
25
|
+
afterEventId?: string;
|
|
26
|
+
primaryUserEventId?: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
relation?: TurnRelation;
|
|
29
|
+
effective?: boolean;
|
|
30
|
+
disposition?: 'explicit' | 'soft_review' | 'shadow' | 'enforced';
|
|
31
|
+
}
|
|
32
|
+
export interface EpisodeSplitImpactInventory {
|
|
33
|
+
eventCount: number;
|
|
34
|
+
userEventCount: number;
|
|
35
|
+
assistantEventCount: number;
|
|
36
|
+
toolEventCount: number;
|
|
37
|
+
systemEventCount: number;
|
|
38
|
+
hardShiftCount: number;
|
|
39
|
+
trustedLocalDates: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface EpisodeSplitPlan {
|
|
42
|
+
planId: string;
|
|
43
|
+
plannerVersion: string;
|
|
44
|
+
projectId: string;
|
|
45
|
+
episodeId: string;
|
|
46
|
+
sourceFingerprint: string;
|
|
47
|
+
policyFingerprint: string;
|
|
48
|
+
normalizedPolicy: {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
maxEvents: number;
|
|
51
|
+
maxDurationMs: number;
|
|
52
|
+
maxIdleGapMs: number;
|
|
53
|
+
splitOnTrustedLocalDateChange: boolean;
|
|
54
|
+
mode: string;
|
|
55
|
+
applyToLive: boolean;
|
|
56
|
+
applyToImports: boolean;
|
|
57
|
+
policyVersion: string;
|
|
58
|
+
timezone?: string;
|
|
59
|
+
};
|
|
60
|
+
proposedBoundaries: EpisodeSplitProposedBoundary[];
|
|
61
|
+
impactInventory: EpisodeSplitImpactInventory;
|
|
62
|
+
segments: EpisodeSplitPlanSegment[];
|
|
63
|
+
warnings: string[];
|
|
64
|
+
unresolvedEventCount: number;
|
|
65
|
+
missingRawEventIds: string[];
|
|
66
|
+
evidenceIntegrityStatus: 'ok' | 'missing_raw_events';
|
|
67
|
+
requiresManualReview: boolean;
|
|
68
|
+
applyableInCurrentVersion: false;
|
|
69
|
+
applyCommand: null;
|
|
70
|
+
}
|
|
71
|
+
export declare class EpisodeSplitPlanner {
|
|
72
|
+
private readonly store;
|
|
73
|
+
private readonly resolveEvent?;
|
|
74
|
+
private readonly liveBoundaryConfig;
|
|
75
|
+
private readonly liveConfigDiagnostics;
|
|
76
|
+
constructor(store: EpisodeStore, resolveEvent?: ((eventId: string) => MemoryEvent | null | undefined) | undefined, liveBoundaryConfig?: Partial<EpisodeBoundaryConfig>, liveConfigDiagnostics?: Array<{
|
|
77
|
+
code: string;
|
|
78
|
+
}>);
|
|
79
|
+
plan(options: {
|
|
80
|
+
projectId: string;
|
|
81
|
+
episodeId: string;
|
|
82
|
+
maxEvents?: number;
|
|
83
|
+
maxDurationMs?: number;
|
|
84
|
+
maxIdleGapMs?: number;
|
|
85
|
+
timezone?: string;
|
|
86
|
+
includeEventIds?: boolean;
|
|
87
|
+
}): EpisodeSplitPlan;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=EpisodeSplitPlanner.d.ts.map
|