@genesislcap/ai-assistant 15.15.1 → 15.15.2
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/ai-assistant.api.json +367 -0
- package/dist/ai-assistant.d.ts +129 -1
- package/dist/chat-driver.cjs +3 -0
- package/dist/chat-driver.cjs.map +2 -2
- package/dist/chat-driver.mjs +3 -0
- package/dist/chat-driver.mjs.map +2 -2
- package/dist/custom-elements.json +232 -3
- package/dist/dts/main/main.d.ts +129 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts +0 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +2 -0
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +1 -0
- package/dist/dts/state/debug-event-log.d.ts +1 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persister.d.ts +76 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/esm/main/main.js +205 -0
- package/dist/esm/main/main.template.js +89 -0
- package/dist/esm/main/persistence-broken-sources.test.js +180 -0
- package/dist/esm/state/debug-event-log.js +3 -0
- package/dist/esm/state/persistence/session-persister.js +224 -43
- package/dist/esm/state/persistence/session-persister.test.js +237 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/main/main.template.ts +101 -0
- package/src/main/main.ts +230 -0
- package/src/main/persistence-broken-sources.test.ts +219 -0
- package/src/state/debug-event-log.ts +4 -0
- package/src/state/persistence/session-persister.test.ts +302 -33
- package/src/state/persistence/session-persister.ts +260 -52
|
@@ -28,6 +28,13 @@ const DEFAULT_DIAGNOSTICS_DEBOUNCE_MS = 4000;
|
|
|
28
28
|
* (GENC-1351 §5.9) and set no `notResumableMessage`. Paired with the composer
|
|
29
29
|
* pre-filled with the flow-activating prompt, so "send it again" is one keystroke.
|
|
30
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* Consecutive failed saves before persistence is declared broken (GENC-1511). Two, not one: a
|
|
33
|
+
* single failure is usually a transient blip that the next autosave clears on its own, and a
|
|
34
|
+
* modal about a problem that has already fixed itself trains people to dismiss the one that
|
|
35
|
+
* matters. At the 1 s autosave debounce this is ~2 s of exposure.
|
|
36
|
+
*/
|
|
37
|
+
const SAVE_FAILURES_BEFORE_BROKEN = 2;
|
|
31
38
|
const DEFAULT_NOT_RESUMABLE_MESSAGE = "Sorry, I didn't manage to finish the task from your previous message. Send it again if you'd like me to try it again";
|
|
32
39
|
/**
|
|
33
40
|
* Owns **restore-core** persistence for one session (GENC-1351 §5.10) — the
|
|
@@ -55,6 +62,26 @@ export class SessionPersister {
|
|
|
55
62
|
* empty). Seeded on restore. See {@link reconcileAgentSnapshots}.
|
|
56
63
|
*/
|
|
57
64
|
this._cache = {};
|
|
65
|
+
/**
|
|
66
|
+
* How many messages the last SUCCESSFUL write persisted. Two jobs, both GENC-1511:
|
|
67
|
+
* it is the monotonic floor for a freeze boundary (the stored transcript must never
|
|
68
|
+
* shrink), and it is what lets a running flow skip a re-write when its boundary has
|
|
69
|
+
* not moved. Content-keyed, so unlike the `interrupted` latch it replaces it can never
|
|
70
|
+
* become a one-way "never write again" state.
|
|
71
|
+
*/
|
|
72
|
+
this._persistedMessages = 0;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the CURRENT non-resumable episode has already stamped its `interrupted`
|
|
75
|
+
* marker. Reset by every resumable save, so each new episode stamps exactly once and
|
|
76
|
+
* the "boundary unmoved" skip below can never swallow the stamp itself (GENC-1511).
|
|
77
|
+
*/
|
|
78
|
+
this._interruptedStamped = false;
|
|
79
|
+
/** Consecutive failed saves; reset by any success. GENC-1511. */
|
|
80
|
+
this._saveFailures = 0;
|
|
81
|
+
/** Whether this broken episode has already been reported to the host. */
|
|
82
|
+
this._brokenReported = false;
|
|
83
|
+
/** Provider's reason for the most recent failure, for the host's message. */
|
|
84
|
+
this._lastFailureReason = '';
|
|
58
85
|
/**
|
|
59
86
|
* The loaded preferences blob (GENC-1351 §5.10 Phase 2), owned here so it's
|
|
60
87
|
* **shared per session** — the element's `applyPreferenceBaseline` reconciler reads
|
|
@@ -130,6 +157,90 @@ export class SessionPersister {
|
|
|
130
157
|
this._saveTimer = undefined;
|
|
131
158
|
}
|
|
132
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Record a save outcome and, when persistence has been failing, decide whether now is the
|
|
162
|
+
* moment to tell the user (GENC-1511).
|
|
163
|
+
*
|
|
164
|
+
* The gate is `resumable`, NOT "is a turn running". `busy` was the obvious choice and it is
|
|
165
|
+
* useless here: a bootstrap journey is ONE turn — measured at 2817 s containing 82 widget
|
|
166
|
+
* interactions — so waiting for a turn boundary would hold the warning for 47 minutes. What
|
|
167
|
+
* actually matters is whether a reload can be resumed from the stored snapshot, and that is
|
|
168
|
+
* exactly what `resumable` already answers for the freeze. Bootstrap keeps everything in its
|
|
169
|
+
* machine context, so it is always resumable and gets told immediately; UI Builder keeps
|
|
170
|
+
* in-flight work in an out-of-band VFS buffer, so it waits for `idle`/`done` — which is also
|
|
171
|
+
* the point at which its files have been flushed and a reload costs nothing.
|
|
172
|
+
*/
|
|
173
|
+
recordSaveOutcome(ok, reason, savedAt) {
|
|
174
|
+
var _a, _b;
|
|
175
|
+
if (ok) {
|
|
176
|
+
const wasBroken = this._brokenSince !== undefined;
|
|
177
|
+
this._saveFailures = 0;
|
|
178
|
+
this._brokenSince = undefined;
|
|
179
|
+
this._lastSavedAt = savedAt !== null && savedAt !== void 0 ? savedAt : this._lastSavedAt;
|
|
180
|
+
if (wasBroken) {
|
|
181
|
+
// Withdraw the warning: a save landing means the store is caught up, which we have
|
|
182
|
+
// watched happen twice in the wild — a broken window is not necessarily permanent.
|
|
183
|
+
this._brokenReported = false;
|
|
184
|
+
(_b = (_a = this.deps).onPersistenceRecovered) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this._saveFailures += 1;
|
|
189
|
+
this._lastFailureReason = reason;
|
|
190
|
+
if (this._saveFailures >= SAVE_FAILURES_BEFORE_BROKEN && this._brokenSince === undefined) {
|
|
191
|
+
this._brokenSince = Date.now();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Surface a broken episode if it is now safe to advise a reload.
|
|
196
|
+
*
|
|
197
|
+
* Gated on `resumable` and NOTHING else. An earlier draft added a 90 s backstop so a flow that
|
|
198
|
+
* errored and stuck mid-state could not hold the warning forever — that was wrong, and the
|
|
199
|
+
* review caught it: the modal's only action is a reload, so firing it while the agent is
|
|
200
|
+
* non-resumable forces the user to destroy exactly the out-of-band, in-flight work the gate
|
|
201
|
+
* exists to protect. Being told late is bad; being told to do the damaging thing is worse.
|
|
202
|
+
* Surfacing a stuck flow earlier needs non-blocking warning UI, not this modal.
|
|
203
|
+
*/
|
|
204
|
+
maybeReportBroken(resumable, liveMessages) {
|
|
205
|
+
var _a, _b;
|
|
206
|
+
if (this._brokenSince === undefined || this._brokenReported || !resumable)
|
|
207
|
+
return;
|
|
208
|
+
this._brokenReported = true;
|
|
209
|
+
(_b = (_a = this.deps).onPersistenceBroken) === null || _b === void 0 ? void 0 : _b.call(_a, {
|
|
210
|
+
unsavedMessages: Math.max(0, liveMessages - this._persistedMessages),
|
|
211
|
+
lastSavedAt: this._lastSavedAt,
|
|
212
|
+
reason: this._lastFailureReason,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Non-resumable-workflow gate (GENC-1351 §5.9): consult the active agent (the flow-owner
|
|
217
|
+
* during a locked flow, like `serialize`). A thrown predicate fails safe to persisting; no
|
|
218
|
+
* live active agent (e.g. the post-restore, pre-reactivation save) leaves it resumable and
|
|
219
|
+
* persists normally.
|
|
220
|
+
*
|
|
221
|
+
* Extracted from `persistSession` so that method stays under the complexity cap, and because
|
|
222
|
+
* this answer now has two consumers: the freeze decision, and whether it is safe to advise the
|
|
223
|
+
* user to reload (GENC-1511).
|
|
224
|
+
*/
|
|
225
|
+
resolveResumable(liveActive, key, agentSnapshots, messages, s) {
|
|
226
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
227
|
+
if (!(liveActive === null || liveActive === void 0 ? void 0 : liveActive.resumable))
|
|
228
|
+
return true;
|
|
229
|
+
try {
|
|
230
|
+
return yield liveActive.resumable({
|
|
231
|
+
agentName: liveActive.name,
|
|
232
|
+
sessionKey: key,
|
|
233
|
+
state: agentSnapshots[liveActive.name],
|
|
234
|
+
messages,
|
|
235
|
+
isFlowOwner: s.flowOwnerAgentName === liveActive.name,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
logger.error('Session save: resumable() threw — persisting (fail-safe):', e);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
133
244
|
/**
|
|
134
245
|
* Capture the current session to durable storage (GENC-1351 §5.1). Reads the
|
|
135
246
|
* full transcript from the driver and the restorable scalars from the store, and
|
|
@@ -137,7 +248,7 @@ export class SessionPersister {
|
|
|
137
248
|
*/
|
|
138
249
|
persistSession() {
|
|
139
250
|
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
251
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
141
252
|
const { provider, enabled } = this.deps.getPersistence();
|
|
142
253
|
if (!enabled || !provider)
|
|
143
254
|
return;
|
|
@@ -175,53 +286,44 @@ export class SessionPersister {
|
|
|
175
286
|
// image forward (GENC-1351 §6). Pure + unit-tested.
|
|
176
287
|
const { snapshots: agentSnapshots, cache } = reconcileAgentSnapshots(liveSnapshots, s.flowOwnerAgentName, this._cache);
|
|
177
288
|
this._cache = cache;
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
if (liveActive === null || liveActive === void 0 ? void 0 : liveActive.resumable) {
|
|
184
|
-
try {
|
|
185
|
-
resumable = yield liveActive.resumable({
|
|
186
|
-
agentName: liveActive.name,
|
|
187
|
-
sessionKey: key,
|
|
188
|
-
state: agentSnapshots[liveActive.name],
|
|
189
|
-
messages,
|
|
190
|
-
isFlowOwner: s.flowOwnerAgentName === liveActive.name,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
catch (e) {
|
|
194
|
-
logger.error('Session save: resumable() threw — persisting (fail-safe):', e);
|
|
195
|
-
resumable = true;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
289
|
+
const resumable = yield this.resolveResumable(liveActive, key, agentSnapshots, messages, s);
|
|
290
|
+
// Report a broken episode at the first SAFE moment, which is what `resumable` decides.
|
|
291
|
+
// Deliberately before either branch acts, so the warning does not wait on the very write
|
|
292
|
+
// that is failing.
|
|
293
|
+
this.maybeReportBroken(resumable, messages.length);
|
|
198
294
|
if (!resumable) {
|
|
199
|
-
// FREEZE
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
295
|
+
// FREEZE THE FLOW STATE, NOT THE TRANSCRIPT (GENC-1511).
|
|
296
|
+
//
|
|
297
|
+
// The intent of §5.9 is the one stated on `PersistedSession.interrupted`: the
|
|
298
|
+
// conversation is persisted and restored in full, and only the running agent's
|
|
299
|
+
// flow-state is dropped. The original implementation did the opposite whenever a
|
|
300
|
+
// snapshot already existed — it froze at the **stored** blob (`existing ?? …`),
|
|
301
|
+
// so everything since the last resumable save was silently rolled back, transcript
|
|
302
|
+
// included, and `if (existing?.interrupted) return` then latched that rollback in
|
|
303
|
+
// for the life of the page. A UI-builder session spent 3h17m in that state: 256
|
|
304
|
+
// loads, zero writes, and a reload that took the user back to a mid-bootstrap
|
|
305
|
+
// snapshot from three hours earlier.
|
|
306
|
+
//
|
|
307
|
+
// So the freeze target is now ALWAYS the live transcript through the last user
|
|
308
|
+
// message — the conversation plus the prompt that started the flow, with the
|
|
309
|
+
// agent's in-flight turns and flow-state dropped. Advancing on each new boundary
|
|
310
|
+
// means a long-running flow can no longer cost the user the work that preceded it.
|
|
208
311
|
const { resumeMessage, resumePrompt } = resolveNonResumableNotice({
|
|
209
312
|
notResumableMessage: liveActive === null || liveActive === void 0 ? void 0 : liveActive.notResumableMessage,
|
|
210
313
|
flowActivationPrompt: s.flowActivationPrompt,
|
|
211
314
|
messages,
|
|
212
315
|
defaultMessage: DEFAULT_NOT_RESUMABLE_MESSAGE,
|
|
213
316
|
});
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const frozen = existing !== null && existing !== void 0 ? existing : createPersistedSession({
|
|
317
|
+
// Never regress the stored transcript. `keepThroughLastUserMessage` alone can be
|
|
318
|
+
// SHORTER than what is already persisted — an agent that goes non-resumable without
|
|
319
|
+
// a fresh user prompt (auto-advance, a resumed journey step) would otherwise drop
|
|
320
|
+
// completed exchanges that were already safely stored. Taking the max of the two
|
|
321
|
+
// makes the boundary monotonic: it advances with each new prompt and never rewinds.
|
|
322
|
+
const boundary = Math.max(keepThroughLastUserMessage(messages).length, this._persistedMessages);
|
|
323
|
+
const frozen = createPersistedSession({
|
|
222
324
|
sessionKey: key,
|
|
223
325
|
host: window.location.host,
|
|
224
|
-
messages:
|
|
326
|
+
messages: messages.slice(0, Math.min(boundary, messages.length)),
|
|
225
327
|
activeAgentName: (_d = s.activeAgent) === null || _d === void 0 ? void 0 : _d.name,
|
|
226
328
|
pinnedAgentName: s.pinnedAgentName,
|
|
227
329
|
flowOwnerAgentName: null,
|
|
@@ -232,6 +334,16 @@ export class SessionPersister {
|
|
|
232
334
|
activeProviderName: s.activeProviderName,
|
|
233
335
|
agentSnapshots: {},
|
|
234
336
|
});
|
|
337
|
+
// Skip the write when the frozen boundary has not moved — a flow autosaves on every
|
|
338
|
+
// transcript change, and while it runs the last user message (and therefore the
|
|
339
|
+
// frozen content) is unchanged, so re-writing it would put one ~MB PUT on the wire
|
|
340
|
+
// per tool call for nothing. Keyed on CONTENT, unlike the old `interrupted` latch:
|
|
341
|
+
// it re-arms the moment the boundary moves, and the resumable path below clears it,
|
|
342
|
+
// so it can never become the one-way "never write again" state it replaces. It also
|
|
343
|
+
// drops the `provider.load` this branch used to issue on every single autosave.
|
|
344
|
+
if (this._persistedMessages === frozen.messages.length && this._interruptedStamped) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
235
347
|
this.logMeta('persistence.suppressed', {
|
|
236
348
|
agent: (_e = liveActive === null || liveActive === void 0 ? void 0 : liveActive.name) !== null && _e !== void 0 ? _e : null,
|
|
237
349
|
isFlowOwner: s.flowOwnerAgentName === (liveActive === null || liveActive === void 0 ? void 0 : liveActive.name),
|
|
@@ -240,9 +352,35 @@ export class SessionPersister {
|
|
|
240
352
|
});
|
|
241
353
|
try {
|
|
242
354
|
yield provider.save(key, Object.assign(Object.assign({}, frozen), { interrupted: { message: resumeMessage, prompt: resumePrompt } }));
|
|
355
|
+
this._persistedMessages = frozen.messages.length;
|
|
356
|
+
this._interruptedStamped = true;
|
|
357
|
+
this.recordSaveOutcome(true, '', frozen.savedAt);
|
|
243
358
|
logger.debug('Flow non-resumable — froze snapshot + stamped interrupted marker', { key });
|
|
244
359
|
}
|
|
245
360
|
catch (e) {
|
|
361
|
+
// GENC-1511: this catch used to be the ONE swallowed failure left after the save-failed
|
|
362
|
+
// event was added to the resumable branch — so a session that broke while an agent was
|
|
363
|
+
// mid-flow still reported nothing at all, which is precisely the blind spot the
|
|
364
|
+
// user-facing warning exists to cover.
|
|
365
|
+
this.recordSaveOutcome(false, (_f = e === null || e === void 0 ? void 0 : e.message) !== null && _f !== void 0 ? _f : String(e));
|
|
366
|
+
// Deliberately NOT reporting from here. This is the non-resumable branch and a reload is
|
|
367
|
+
// the modal's only action, so advising one now would destroy the in-flight work the
|
|
368
|
+
// snapshot could not carry. `recordSaveOutcome` has already set `_brokenSince`, so the
|
|
369
|
+
// next `persistSession` whose `resolveResumable()` returns true reports it — late, but at
|
|
370
|
+
// a point where the advice is safe to take.
|
|
371
|
+
//
|
|
372
|
+
// (An earlier version called `maybeReportBroken(false, …)` here. Review caught that it was
|
|
373
|
+
// an unconditional no-op — that method returns immediately on `!resumable` — and that its
|
|
374
|
+
// comment pointed at a 90 s backstop which had itself been removed earlier in review.)
|
|
375
|
+
this.logMeta('persistence.save-failed', {
|
|
376
|
+
messages: frozen.messages.length,
|
|
377
|
+
agents: [],
|
|
378
|
+
active: (_g = liveActive === null || liveActive === void 0 ? void 0 : liveActive.name) !== null && _g !== void 0 ? _g : null,
|
|
379
|
+
hasSerialize: !!(liveActive === null || liveActive === void 0 ? void 0 : liveActive.serialize),
|
|
380
|
+
frozen: true,
|
|
381
|
+
error: (_h = e === null || e === void 0 ? void 0 : e.name) !== null && _h !== void 0 ? _h : typeof e,
|
|
382
|
+
message: (_j = e === null || e === void 0 ? void 0 : e.message) !== null && _j !== void 0 ? _j : String(e),
|
|
383
|
+
});
|
|
246
384
|
logger.error('Session save (interrupted marker) failed:', e);
|
|
247
385
|
}
|
|
248
386
|
return;
|
|
@@ -253,7 +391,7 @@ export class SessionPersister {
|
|
|
253
391
|
sessionKey: key,
|
|
254
392
|
host: window.location.host,
|
|
255
393
|
messages: [...messages],
|
|
256
|
-
activeAgentName: (
|
|
394
|
+
activeAgentName: (_k = s.activeAgent) === null || _k === void 0 ? void 0 : _k.name,
|
|
257
395
|
pinnedAgentName: s.pinnedAgentName,
|
|
258
396
|
flowOwnerAgentName: s.flowOwnerAgentName,
|
|
259
397
|
sessionCostUsd: s.sessionUsage.costUsd,
|
|
@@ -266,18 +404,56 @@ export class SessionPersister {
|
|
|
266
404
|
// Observability (GENC-1351): record what each autosave captured for stateful
|
|
267
405
|
// agents — `agents: []` here means no machine snapshot was taken (the bug to
|
|
268
406
|
// watch for: the active agent's `serialize()` returned nothing).
|
|
269
|
-
|
|
407
|
+
//
|
|
408
|
+
// GENC-1511: this event is emitted AFTER the write resolves, never before. It used
|
|
409
|
+
// to be logged ahead of `provider.save`, which made it a record of an ATTEMPT while
|
|
410
|
+
// reading as a record of a SAVE — and the failure below was swallowed into
|
|
411
|
+
// `logger.error`, which is not wired to Sentry. A session whose writes had been
|
|
412
|
+
// failing for three hours therefore showed 52 cheerful `persistence.saved` events
|
|
413
|
+
// and no failures anywhere: not in the debug log, not in Loki, not in Sentry. The
|
|
414
|
+
// whole incident was invisible until the box's nginx access log was read by hand.
|
|
415
|
+
const captured = {
|
|
270
416
|
messages: messages.length,
|
|
271
417
|
agents: Object.keys(agentSnapshots),
|
|
272
|
-
active: (
|
|
418
|
+
active: (_l = liveActive === null || liveActive === void 0 ? void 0 : liveActive.name) !== null && _l !== void 0 ? _l : null,
|
|
273
419
|
hasSerialize: !!(liveActive === null || liveActive === void 0 ? void 0 : liveActive.serialize),
|
|
274
420
|
agentSnapshots,
|
|
275
|
-
}
|
|
421
|
+
};
|
|
276
422
|
try {
|
|
277
423
|
yield provider.save(key, snapshot);
|
|
424
|
+
this._persistedMessages = messages.length;
|
|
425
|
+
this._interruptedStamped = false;
|
|
426
|
+
this.recordSaveOutcome(true, '', snapshot.savedAt);
|
|
427
|
+
this.logMeta('persistence.saved', captured);
|
|
278
428
|
logger.debug('Session saved', { key, messages: messages.length });
|
|
279
429
|
}
|
|
280
430
|
catch (e) {
|
|
431
|
+
// `high` importance, and deliberately verbose: this is the one event that says
|
|
432
|
+
// "the transcript on screen is no longer backed by the store". `name` separates a
|
|
433
|
+
// provider/transport rejection from a serialisation fault — a `TypeError` here
|
|
434
|
+
// means the body never reached the wire at all, because a provider builds its
|
|
435
|
+
// request body inside the `fetch` init, so a throw while serialising creates no
|
|
436
|
+
// request and leaves no server-side trace.
|
|
437
|
+
this.recordSaveOutcome(false, (_m = e === null || e === void 0 ? void 0 : e.message) !== null && _m !== void 0 ? _m : String(e));
|
|
438
|
+
// Report straight away rather than waiting for the next attempt: we are on the resumable
|
|
439
|
+
// path, so a reload is safe NOW, and a one-cycle delay is a second of extra exposure for
|
|
440
|
+
// no benefit.
|
|
441
|
+
this.maybeReportBroken(true, messages.length);
|
|
442
|
+
// Deliberately NOT `...captured`: that carries the raw `agentSnapshots` object, and
|
|
443
|
+
// `recordMetaEvent` stores the detail BY REFERENCE. When the save failed *because* that
|
|
444
|
+
// object is circular — the fault this catch exists for — the high-importance failure event
|
|
445
|
+
// became circular too, so `assembleDebugLog` threw while stringifying it and
|
|
446
|
+
// `downloadDebugLog` could not produce the log this modal tells the user to save. Names
|
|
447
|
+
// and scalars only; the success event above may keep the snapshots, because a landed write
|
|
448
|
+
// has already proved them serialisable.
|
|
449
|
+
this.logMeta('persistence.save-failed', {
|
|
450
|
+
messages: captured.messages,
|
|
451
|
+
agents: captured.agents,
|
|
452
|
+
active: captured.active,
|
|
453
|
+
hasSerialize: captured.hasSerialize,
|
|
454
|
+
error: (_o = e === null || e === void 0 ? void 0 : e.name) !== null && _o !== void 0 ? _o : typeof e,
|
|
455
|
+
message: (_p = e === null || e === void 0 ? void 0 : e.message) !== null && _p !== void 0 ? _p : String(e),
|
|
456
|
+
});
|
|
281
457
|
logger.error('Session save failed:', e);
|
|
282
458
|
}
|
|
283
459
|
});
|
|
@@ -346,6 +522,9 @@ export class SessionPersister {
|
|
|
346
522
|
// by the history change below, before any agent re-activates) preserves the
|
|
347
523
|
// machine snapshot instead of overwriting it with an empty one (GENC-1351).
|
|
348
524
|
this._cache = Object.assign({}, snapshot.agentSnapshots);
|
|
525
|
+
// Seed the monotonic freeze floor too (GENC-1511): a flow that goes non-resumable
|
|
526
|
+
// straight after a restore must not freeze BELOW what the restore just brought back.
|
|
527
|
+
this._persistedMessages = trimmed.length;
|
|
349
528
|
// Observability (GENC-1351): record exactly what the restore primed, so a
|
|
350
529
|
// post-reload debug-log export shows whether a stateful agent's snapshot
|
|
351
530
|
// actually came back (meta events reset on reload, so this is the only window
|
|
@@ -478,6 +657,8 @@ export class SessionPersister {
|
|
|
478
657
|
clearRestoreCore() {
|
|
479
658
|
return __awaiter(this, void 0, void 0, function* () {
|
|
480
659
|
this._cache = {};
|
|
660
|
+
this._persistedMessages = 0;
|
|
661
|
+
this._interruptedStamped = false;
|
|
481
662
|
const { provider } = this.deps.getPersistence();
|
|
482
663
|
const key = this.stateKey;
|
|
483
664
|
if (!provider)
|