@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-persister.d.ts","sourceRoot":"","sources":["../../../../src/state/persistence/session-persister.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sCAAsC,CAAC;AAWrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAE5E,OAAO,KAAK,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"session-persister.d.ts","sourceRoot":"","sources":["../../../../src/state/persistence/session-persister.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/main/main.js
CHANGED
|
@@ -107,6 +107,19 @@ const SESSION_MENU_CLOSE_MS = 200;
|
|
|
107
107
|
* assistant is running — the Usage tab renders the in-memory list — so the delay is invisible.
|
|
108
108
|
*/
|
|
109
109
|
const LEDGER_FLUSH_DEBOUNCE_MS = 2000;
|
|
110
|
+
/**
|
|
111
|
+
* Reporter id for this element's OWN session persister in `brokenSources` (GENC-1511).
|
|
112
|
+
*
|
|
113
|
+
* Named rather than inline so the report and the recovery cannot drift apart — a typo in one of
|
|
114
|
+
* the two strings would silently make the session's recovery unable to withdraw its own warning,
|
|
115
|
+
* leaving the modal up for good.
|
|
116
|
+
*/
|
|
117
|
+
const SESSION_PERSISTENCE_SOURCE = 'session';
|
|
118
|
+
/**
|
|
119
|
+
* What a host gets if it reports a failure without naming a source. Fine for a host with a single
|
|
120
|
+
* store; a host owning several should name each, so one recovering does not withdraw the others.
|
|
121
|
+
*/
|
|
122
|
+
const DEFAULT_PERSISTENCE_SOURCE = 'host';
|
|
110
123
|
/** Drag-to-resize bounds for the chat input (composer), in px. */
|
|
111
124
|
const COMPOSER_MIN_HEIGHT_PX = 48;
|
|
112
125
|
const COMPOSER_MAX_HEIGHT_PX = 400;
|
|
@@ -312,6 +325,39 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
312
325
|
this.downloadingDebugLog = false;
|
|
313
326
|
/** Active tab inside the settings modal. */
|
|
314
327
|
this.settingsModalTab = 'settings';
|
|
328
|
+
/**
|
|
329
|
+
* GENC-1511 — persistence has failed repeatedly and it is no longer saving. Drives a BLOCKING
|
|
330
|
+
* modal with a single Reload action: the whole failure class this ticket exists for is one the
|
|
331
|
+
* user cannot see, and a passive indicator is close to invisible (Will worked for 3h17m past
|
|
332
|
+
* the point his session stopped saving, with nothing on screen to tell him).
|
|
333
|
+
*
|
|
334
|
+
* Set only at a moment when reloading is SAFE — the persister gates it on the active agent
|
|
335
|
+
* being resumable, so we never advise a reload while an agent's in-flight work would be lost.
|
|
336
|
+
*/
|
|
337
|
+
this.persistenceBroken = false;
|
|
338
|
+
/**
|
|
339
|
+
* Which reporters currently consider persistence broken, newest last, each with the copy it
|
|
340
|
+
* asked for. {@link FoundationAiAssistant.persistenceBroken} is DERIVED from this (`size > 0`) rather than being the
|
|
341
|
+
* source of truth.
|
|
342
|
+
*
|
|
343
|
+
* A plain boolean was wrong the moment there was more than one reporter, and review caught it:
|
|
344
|
+
* this element's own persister covers `sessions/<key>`, while a host reports its own keyspaces
|
|
345
|
+
* (Create's `projects/<id>.json`). With one flag, a SESSION save landing after its own broken
|
|
346
|
+
* episode called `reportPersistenceRecovered()` and closed the modal while the PROJECT store
|
|
347
|
+
* was still refusing writes — and because Create's own re-entry guard only clears on a
|
|
348
|
+
* successful project write, the warning could then never be raised again for the life of the
|
|
349
|
+
* page. The user carried on against a dead store with the warning cancelled by an unrelated
|
|
350
|
+
* subsystem: the exact failure this modal exists to prevent, on the keyspace that holds the app
|
|
351
|
+
* rather than the transcript.
|
|
352
|
+
*
|
|
353
|
+
* A `Map` and not a `Set` because the copy has to survive the same collision. The winner is the
|
|
354
|
+
* most recently reported still-broken source (insertion order, so it is deterministic), and when
|
|
355
|
+
* one recovers the copy falls back to whoever is left instead of reverting to the session
|
|
356
|
+
* default — which would otherwise state something untrue of the failure still in progress.
|
|
357
|
+
*/
|
|
358
|
+
this.brokenSources = new Map();
|
|
359
|
+
/** Swallows the native dialog's Escape route. Stored, not inline, so it can be removed again. */
|
|
360
|
+
this.onPersistenceModalCancel = (e) => e.preventDefault();
|
|
315
361
|
/** Light-DOM nodes assigned to settings modal slots (via `slotted()` bindings). */
|
|
316
362
|
this.settingsModelSlotted = [];
|
|
317
363
|
this.settingsAppSlotted = [];
|
|
@@ -2053,6 +2099,70 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
2053
2099
|
var _a;
|
|
2054
2100
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setInputValue(value);
|
|
2055
2101
|
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Showing and hiding the modal hangs off the PROPERTY, so `persistenceBroken` is the single
|
|
2104
|
+
* entry point — for this element's own persister callback and for a host reporting a failure in
|
|
2105
|
+
* a keyspace this element knows nothing about (Create's project payload, GENC-1511 §17).
|
|
2106
|
+
*
|
|
2107
|
+
* It has to be here rather than at the call site because of the `cancel` listener: `cancel`
|
|
2108
|
+
* does not bubble, so it cannot be bound from the template, and a caller that shows the modal
|
|
2109
|
+
* without attaching it gets a modal Escape closes — which defeats the whole point. Routing
|
|
2110
|
+
* every path through one place makes a dismissible variant unreachable.
|
|
2111
|
+
*/
|
|
2112
|
+
persistenceBrokenChanged(_old, broken) {
|
|
2113
|
+
var _a;
|
|
2114
|
+
if (!broken) {
|
|
2115
|
+
this.detachPersistenceEscapeGuard();
|
|
2116
|
+
(_a = this.persistenceModal) === null || _a === void 0 ? void 0 : _a.close();
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
DOM.queueUpdate(() => {
|
|
2120
|
+
const modal = this.persistenceModal;
|
|
2121
|
+
// Re-read `persistenceBroken`: this callback is DEFERRED, so the state it exists to act on
|
|
2122
|
+
// may have changed since it was queued. A `true → false` landing in the same batch runs the
|
|
2123
|
+
// `!broken` branch above against a modal that was never shown — both statements no-op — and
|
|
2124
|
+
// this callback would then raise a blocking, deliberately NON-DISMISSIBLE "your work isn't
|
|
2125
|
+
// being saved" modal for a problem that had already fixed itself, with no further change
|
|
2126
|
+
// event coming to close it. The only way out would be a reload nobody needed, which is a
|
|
2127
|
+
// worse outcome than the missed warning this defer exists to avoid.
|
|
2128
|
+
//
|
|
2129
|
+
// Reachable because `maybeReportBroken` fires BEFORE `provider.save` in `persistSession`, so
|
|
2130
|
+
// a report and the successful write that withdraws it can share a tick — and per-source
|
|
2131
|
+
// recovery makes `true → false` far more common than it was (PR #2472 review).
|
|
2132
|
+
if (!modal || modal.open || !this.persistenceBroken)
|
|
2133
|
+
return;
|
|
2134
|
+
this.attachPersistenceEscapeGuard(modal);
|
|
2135
|
+
modal.show();
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Make the modal survive Escape. Bound imperatively because `cancel` does not bubble.
|
|
2140
|
+
*
|
|
2141
|
+
* Attach/detach rather than fire-and-forget: a broken → recovered → broken cycle is reachable
|
|
2142
|
+
* (that is what {@link FoundationAiAssistant.reportPersistenceRecovered} is for), and re-showing previously added a
|
|
2143
|
+
* fresh anonymous closure each time, accumulating duplicate listeners on the dialog.
|
|
2144
|
+
*
|
|
2145
|
+
* A missing `mainElement` is LOGGED rather than ignored. `Dialog` declares it non-optional, so
|
|
2146
|
+
* if it is ever absent the old optional-chain quietly produced an Escape-dismissible modal —
|
|
2147
|
+
* failing open on the one property this modal must never lose.
|
|
2148
|
+
*/
|
|
2149
|
+
attachPersistenceEscapeGuard(modal) {
|
|
2150
|
+
const dialog = modal.mainElement;
|
|
2151
|
+
if (!dialog) {
|
|
2152
|
+
logger.error('Persistence modal has no dialog element — Escape cannot be blocked, so the blocking ' +
|
|
2153
|
+
'warning is dismissible. The user may close it and carry on against a dead store.');
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
this.detachPersistenceEscapeGuard();
|
|
2157
|
+
dialog.addEventListener('cancel', this.onPersistenceModalCancel);
|
|
2158
|
+
this.persistenceEscapeGuardTarget = dialog;
|
|
2159
|
+
}
|
|
2160
|
+
/** Remove the Escape guard, if one is bound. Safe to call when there is none. */
|
|
2161
|
+
detachPersistenceEscapeGuard() {
|
|
2162
|
+
var _a;
|
|
2163
|
+
(_a = this.persistenceEscapeGuardTarget) === null || _a === void 0 ? void 0 : _a.removeEventListener('cancel', this.onPersistenceModalCancel);
|
|
2164
|
+
this.persistenceEscapeGuardTarget = undefined;
|
|
2165
|
+
}
|
|
2056
2166
|
syncShowHalo() {
|
|
2057
2167
|
if (!this.busy) {
|
|
2058
2168
|
this.showHalo = 'no';
|
|
@@ -3995,8 +4105,94 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
3995
4105
|
getDriver: () => getDriver(key),
|
|
3996
4106
|
getStore: () => (hasSessionStore(key) ? getSessionStore(key) : undefined),
|
|
3997
4107
|
getDiagnosticEntries: () => this.buildDiagnosticEntries(),
|
|
4108
|
+
onPersistenceBroken: (d) => this.onPersistenceBroken(d),
|
|
4109
|
+
onPersistenceRecovered: () => this.onPersistenceRecovered(),
|
|
3998
4110
|
};
|
|
3999
4111
|
}
|
|
4112
|
+
/**
|
|
4113
|
+
* Tell the user their work is no longer being saved, and block until they reload (GENC-1511).
|
|
4114
|
+
*
|
|
4115
|
+
* Public because the assistant is not the only thing that persists. Its own persister covers
|
|
4116
|
+
* `sessions/<key>`; a host may own other keyspaces whose failure is just as costly — Create's
|
|
4117
|
+
* `projects/<id>.json` holds the entity model, handlers and VFS, i.e. the app itself — and had
|
|
4118
|
+
* no way to reach this modal (§17 of the GENC-1511 plan).
|
|
4119
|
+
*
|
|
4120
|
+
* Both copy fields are optional and default to the session wording in the template, so the
|
|
4121
|
+
* common case stays a bare call. Idempotent per source while already broken: re-reporting the
|
|
4122
|
+
* same source refreshes its copy, and the property change only shows a modal that is not open.
|
|
4123
|
+
*
|
|
4124
|
+
* @param overrides - `message` replaces the body; `detail` adds/replaces the second line;
|
|
4125
|
+
* `source` identifies the reporter so its own recovery withdraws only its own warning. Give
|
|
4126
|
+
* each independent keyspace its own id — see `brokenSources` for what sharing one costs.
|
|
4127
|
+
*/
|
|
4128
|
+
reportPersistenceBroken(overrides) {
|
|
4129
|
+
var _a;
|
|
4130
|
+
const source = (_a = overrides === null || overrides === void 0 ? void 0 : overrides.source) !== null && _a !== void 0 ? _a : DEFAULT_PERSISTENCE_SOURCE;
|
|
4131
|
+
// Delete before set so a repeat report moves this source to the END of the insertion order:
|
|
4132
|
+
// the newest failure is the one whose copy shows.
|
|
4133
|
+
this.brokenSources.delete(source);
|
|
4134
|
+
this.brokenSources.set(source, { message: overrides === null || overrides === void 0 ? void 0 : overrides.message, detail: overrides === null || overrides === void 0 ? void 0 : overrides.detail });
|
|
4135
|
+
this.syncPersistenceBrokenCopy();
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Withdraw one reporter's warning — its save landed, or the host's own store recovered.
|
|
4139
|
+
*
|
|
4140
|
+
* Only `source`'s entry is removed, so a session recovery cannot cancel a host's still-live
|
|
4141
|
+
* project failure (GENC-1511, caught in review; see `brokenSources`). The modal closes
|
|
4142
|
+
* only once nothing is broken; while anything remains, the copy falls back to the newest
|
|
4143
|
+
* survivor.
|
|
4144
|
+
*
|
|
4145
|
+
* @param source - must match the id the failure was reported under.
|
|
4146
|
+
*/
|
|
4147
|
+
reportPersistenceRecovered(source = DEFAULT_PERSISTENCE_SOURCE) {
|
|
4148
|
+
if (!this.brokenSources.delete(source))
|
|
4149
|
+
return;
|
|
4150
|
+
this.syncPersistenceBrokenCopy();
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* Recompute the derived flag and the displayed copy from `brokenSources`.
|
|
4154
|
+
*
|
|
4155
|
+
* Copy order is last-in-wins, which is both deterministic (`Map` iterates in insertion order)
|
|
4156
|
+
* and the behaviour a reader expects — the most recent failure is the one described. Setting
|
|
4157
|
+
* `persistenceBroken` last means the modal is only ever shown or closed once the copy backing
|
|
4158
|
+
* it is already correct.
|
|
4159
|
+
*/
|
|
4160
|
+
syncPersistenceBrokenCopy() {
|
|
4161
|
+
const newest = [...this.brokenSources.values()].pop();
|
|
4162
|
+
this.persistenceBrokenMessage = newest === null || newest === void 0 ? void 0 : newest.message;
|
|
4163
|
+
this.persistenceBrokenDetail = newest === null || newest === void 0 ? void 0 : newest.detail;
|
|
4164
|
+
this.persistenceBroken = this.brokenSources.size > 0;
|
|
4165
|
+
}
|
|
4166
|
+
/** This element's own persister said the session snapshot is not landing (GENC-1511). */
|
|
4167
|
+
onPersistenceBroken(d) {
|
|
4168
|
+
logger.error('Persistence is not saving — prompting the user to reload:', d.reason);
|
|
4169
|
+
// Composed here, not in the template: the count and the timestamp are facts about the CHAT
|
|
4170
|
+
// session, and a host reporting a different keyspace has its own sentence to write.
|
|
4171
|
+
const when = d.lastSavedAt
|
|
4172
|
+
? ` — the last successful save was at ${new Date(d.lastSavedAt).toLocaleTimeString()}`
|
|
4173
|
+
: '';
|
|
4174
|
+
const detail = d.unsavedMessages > 0
|
|
4175
|
+
? `${d.unsavedMessages} message${d.unsavedMessages === 1 ? '' : 's'} ` +
|
|
4176
|
+
`have not been saved${when}.`
|
|
4177
|
+
: undefined;
|
|
4178
|
+
this.reportPersistenceBroken({ detail, source: SESSION_PERSISTENCE_SOURCE });
|
|
4179
|
+
}
|
|
4180
|
+
/**
|
|
4181
|
+
* A session save landed after a broken episode — withdraw OUR warning only (GENC-1511).
|
|
4182
|
+
*
|
|
4183
|
+
* The explicit source is the whole point: a host's project store may still be failing, and
|
|
4184
|
+
* before this was per-source a session recovery closed that modal too.
|
|
4185
|
+
*/
|
|
4186
|
+
onPersistenceRecovered() {
|
|
4187
|
+
this.reportPersistenceRecovered(SESSION_PERSISTENCE_SOURCE);
|
|
4188
|
+
}
|
|
4189
|
+
/**
|
|
4190
|
+
* The modal's only action. A reload re-reads the stored snapshot, which is plain data parsed
|
|
4191
|
+
* from the server, so it also clears the serialisation fault that is the most likely cause.
|
|
4192
|
+
*/
|
|
4193
|
+
reloadForPersistence() {
|
|
4194
|
+
window.location.reload();
|
|
4195
|
+
}
|
|
4000
4196
|
/**
|
|
4001
4197
|
* First-load persistence init for a freshly created store (GENC-1351). Restores
|
|
4002
4198
|
* the user's remembered persistence-toggle choice — kept independently of the
|
|
@@ -4585,6 +4781,15 @@ __decorate([
|
|
|
4585
4781
|
__decorate([
|
|
4586
4782
|
observable
|
|
4587
4783
|
], FoundationAiAssistant.prototype, "settingsModalTab", void 0);
|
|
4784
|
+
__decorate([
|
|
4785
|
+
observable
|
|
4786
|
+
], FoundationAiAssistant.prototype, "persistenceBroken", void 0);
|
|
4787
|
+
__decorate([
|
|
4788
|
+
observable
|
|
4789
|
+
], FoundationAiAssistant.prototype, "persistenceBrokenMessage", void 0);
|
|
4790
|
+
__decorate([
|
|
4791
|
+
observable
|
|
4792
|
+
], FoundationAiAssistant.prototype, "persistenceBrokenDetail", void 0);
|
|
4588
4793
|
__decorate([
|
|
4589
4794
|
observable
|
|
4590
4795
|
], FoundationAiAssistant.prototype, "settingsModelSlotted", void 0);
|
|
@@ -352,6 +352,94 @@ const blockedBannerTemplate = html `
|
|
|
352
352
|
`;
|
|
353
353
|
// ─── Public factory ───────────────────────────────────────────────────────────
|
|
354
354
|
/** @internal */
|
|
355
|
+
/**
|
|
356
|
+
* GENC-1511 — blocking "not saving" modal.
|
|
357
|
+
*
|
|
358
|
+
* Deliberately has NO way out but Reload. The failure it reports is invisible by nature — a
|
|
359
|
+
* prod session wrote nothing for 3h17m with a normal-looking UI throughout — and anything the
|
|
360
|
+
* user can dismiss is an invitation to carry on producing work that will be lost. So:
|
|
361
|
+
* `show-close-icon` off, our own `bottom` slot replacing the default close button, and the
|
|
362
|
+
* native dialog's Escape route cancelled on `mainElement` in `persistenceBrokenChanged`.
|
|
363
|
+
*
|
|
364
|
+
* It is only ever shown at a point where reloading is SAFE — the persister gates on the active
|
|
365
|
+
* agent being resumable — so the advice cannot cost the user in-flight work it is unable to carry.
|
|
366
|
+
*
|
|
367
|
+
* ⚠ `showCloseIcon` is set as a PROPERTY (`:showCloseIcon`), not via the `?show-close-icon`
|
|
368
|
+
* boolean attribute. It is an `@attr({ mode: 'boolean' })` whose FIELD default is `true`, so the
|
|
369
|
+
* property reflects the attribute ON at construction and the boolean binding's "absent means
|
|
370
|
+
* false" never wins. Measured live 2026-08-20: the close icon rendered anyway
|
|
371
|
+
* (`showCloseIcon=true`, `hasAttribute=true`) until the property was set directly. Do not
|
|
372
|
+
* "simplify" it back to `?show-close-icon` — this modal must not be dismissible.
|
|
373
|
+
*/
|
|
374
|
+
const persistenceBrokenModalTemplate = (designSystemPrefix) => {
|
|
375
|
+
const modalTag = `${designSystemPrefix}-modal`;
|
|
376
|
+
const buttonTag = `${designSystemPrefix}-button`;
|
|
377
|
+
const iconTag = `${designSystemPrefix}-icon`;
|
|
378
|
+
return html `
|
|
379
|
+
<${modalTag}
|
|
380
|
+
class="persistence-broken-modal"
|
|
381
|
+
part="persistence-broken-modal"
|
|
382
|
+
${ref('persistenceModal')}
|
|
383
|
+
initial-width="480"
|
|
384
|
+
:showCloseIcon=${() => false}
|
|
385
|
+
>
|
|
386
|
+
<div slot="top" class="persistence-broken-header" part="persistence-broken-header">
|
|
387
|
+
<h2 class="persistence-broken-title">
|
|
388
|
+
<${iconTag} name="triangle-exclamation"></${iconTag}> Your work isn't being saved
|
|
389
|
+
</h2>
|
|
390
|
+
</div>
|
|
391
|
+
<div class="persistence-broken-content" part="persistence-broken-content">
|
|
392
|
+
<p>
|
|
393
|
+
${(x) => {
|
|
394
|
+
var _a;
|
|
395
|
+
return (_a = x.persistenceBrokenMessage) !== null && _a !== void 0 ? _a : "We've encountered a problem and this session has stopped saving to the server. " +
|
|
396
|
+
'Anything you do from here will be lost, so please reload before carrying on.';
|
|
397
|
+
}}
|
|
398
|
+
</p>
|
|
399
|
+
<!--
|
|
400
|
+
Quantified where the reporter can manage it, because "some work" is not actionable.
|
|
401
|
+
Composed by the CALLER, not here: the assistant's persister counts unsaved messages, a
|
|
402
|
+
host counting something else has its own sentence, and a template that knew about
|
|
403
|
+
"messages" could only ever serve the first. Omitted entirely when nobody supplied one.
|
|
404
|
+
-->
|
|
405
|
+
${when((x) => !!x.persistenceBrokenDetail, html `
|
|
406
|
+
<p class="persistence-broken-detail">${(x) => x.persistenceBrokenDetail}</p>
|
|
407
|
+
`)}
|
|
408
|
+
<p class="persistence-broken-detail">
|
|
409
|
+
Reloading restores the last saved point.${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.allowDebugDownload) === true; }, html `
|
|
410
|
+
You can download a record of this conversation first.
|
|
411
|
+
`)}
|
|
412
|
+
</p>
|
|
413
|
+
</div>
|
|
414
|
+
<div slot="bottom" class="persistence-broken-actions" part="persistence-broken-actions">
|
|
415
|
+
<!--
|
|
416
|
+
The download lives HERE, not in Settings. The copy used to point at the Settings panel,
|
|
417
|
+
which this modal makes unreachable by design — offering a record the user cannot get to.
|
|
418
|
+
Gated on the host's own allowDebugDownload flag, same as the Settings entry, so a host
|
|
419
|
+
that turned it off does not get it smuggled back in through the failure path; the
|
|
420
|
+
sentence above is gated on the same flag so the copy never promises a missing button.
|
|
421
|
+
-->
|
|
422
|
+
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.allowDebugDownload) === true; }, html `
|
|
423
|
+
<${buttonTag}
|
|
424
|
+
appearance="outline"
|
|
425
|
+
part="persistence-broken-download"
|
|
426
|
+
?disabled=${(x) => x.downloadingDebugLog}
|
|
427
|
+
@click=${(x) => x.downloadDebugLog()}
|
|
428
|
+
>
|
|
429
|
+
${(x) => (x.downloadingDebugLog ? 'Preparing…' : 'Download agent log')}
|
|
430
|
+
</${buttonTag}>
|
|
431
|
+
`)}
|
|
432
|
+
<${buttonTag}
|
|
433
|
+
appearance="accent"
|
|
434
|
+
part="persistence-broken-reload"
|
|
435
|
+
@click=${(x) => x.reloadForPersistence()}
|
|
436
|
+
>
|
|
437
|
+
Reload the page
|
|
438
|
+
</${buttonTag}>
|
|
439
|
+
</div>
|
|
440
|
+
</${modalTag}>
|
|
441
|
+
`;
|
|
442
|
+
};
|
|
355
443
|
export const FoundationAiAssistantTemplate = (designSystemPrefix) => {
|
|
356
444
|
const buttonTag = `${designSystemPrefix}-button`;
|
|
357
445
|
const textareaTag = `${designSystemPrefix}-text-area`;
|
|
@@ -761,5 +849,6 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
761
849
|
></ai-halo-overlay>
|
|
762
850
|
</div>
|
|
763
851
|
${SettingsModalTemplate(designSystemPrefix)}
|
|
852
|
+
${persistenceBrokenModalTemplate(designSystemPrefix)}
|
|
764
853
|
`;
|
|
765
854
|
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
+
import { DOM } from '@genesislcap/web-core';
|
|
3
|
+
import { FoundationAiAssistant } from './main';
|
|
4
|
+
// Hold a reference so the custom-element registration isn't tree-shaken.
|
|
5
|
+
FoundationAiAssistant;
|
|
6
|
+
// GENC-1511 — the blocking "not saving" modal has MORE THAN ONE reporter: this element's own
|
|
7
|
+
// session persister (`sessions/<key>`), and a host reporting its own keyspaces through the public
|
|
8
|
+
// API (Create's `projects/<id>.json`, which holds the app rather than the transcript).
|
|
9
|
+
//
|
|
10
|
+
// It was first built on a single `persistenceBroken` boolean, and review caught what that costs:
|
|
11
|
+
// whichever reporter recovered FIRST closed the modal for both, and the host's own re-entry guard
|
|
12
|
+
// then kept it from ever being raised again. This suite pins the per-source behaviour so that
|
|
13
|
+
// collision cannot come back.
|
|
14
|
+
//
|
|
15
|
+
// Like `blocked-state.test.ts`, this exercises the METHODS the template bindings read rather than
|
|
16
|
+
// the rendered DOM: `document.createElement` upgrades the element (constructor) without connecting
|
|
17
|
+
// it, so `connectedCallback` never runs and nothing subscribes to `agenticActivityBus` — which
|
|
18
|
+
// would otherwise hold the test runner's event loop open.
|
|
19
|
+
const Suite = createLogicSuite('FoundationAiAssistant persistence-broken sources');
|
|
20
|
+
/** A fresh, unconnected element. Nothing here needs a session store. */
|
|
21
|
+
const element = () => document.createElement('foundation-ai-assistant');
|
|
22
|
+
/** The modal's visible copy, as the template bindings read it. */
|
|
23
|
+
const copy = (el) => ({
|
|
24
|
+
broken: el.persistenceBroken,
|
|
25
|
+
message: el.persistenceBrokenMessage,
|
|
26
|
+
detail: el.persistenceBrokenDetail,
|
|
27
|
+
});
|
|
28
|
+
const SESSION = 'session';
|
|
29
|
+
const PROJECT = 'project';
|
|
30
|
+
Suite('a single reporter raises and withdraws its own warning', () => {
|
|
31
|
+
const el = element();
|
|
32
|
+
assert.is(el.persistenceBroken, false);
|
|
33
|
+
el.reportPersistenceBroken({ detail: '2 messages have not been saved.', source: SESSION });
|
|
34
|
+
assert.is(el.persistenceBroken, true);
|
|
35
|
+
assert.is(el.persistenceBrokenDetail, '2 messages have not been saved.');
|
|
36
|
+
el.reportPersistenceRecovered(SESSION);
|
|
37
|
+
assert.is(el.persistenceBroken, false);
|
|
38
|
+
assert.is(el.persistenceBrokenMessage, undefined);
|
|
39
|
+
assert.is(el.persistenceBrokenDetail, undefined);
|
|
40
|
+
});
|
|
41
|
+
Suite('one source recovering does NOT withdraw another still-broken source', () => {
|
|
42
|
+
const el = element();
|
|
43
|
+
el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
|
|
44
|
+
el.reportPersistenceBroken({ message: 'project copy', source: PROJECT });
|
|
45
|
+
// The session's own save lands. Its warning goes; the project's must not.
|
|
46
|
+
el.reportPersistenceRecovered(SESSION);
|
|
47
|
+
assert.is(el.persistenceBroken, true, 'modal must stay up while the project store is failing');
|
|
48
|
+
assert.is(el.persistenceBrokenMessage, 'project copy', 'and must still describe THAT failure');
|
|
49
|
+
el.reportPersistenceRecovered(PROJECT);
|
|
50
|
+
assert.is(el.persistenceBroken, false, 'only once nothing is broken');
|
|
51
|
+
});
|
|
52
|
+
Suite('a later report does not overwrite an earlier source with its own copy', () => {
|
|
53
|
+
const el = element();
|
|
54
|
+
el.reportPersistenceBroken({
|
|
55
|
+
message: 'project copy',
|
|
56
|
+
detail: 'project detail',
|
|
57
|
+
source: PROJECT,
|
|
58
|
+
});
|
|
59
|
+
// The session reports with NO message override — it wants the template's session default.
|
|
60
|
+
// Before per-source tracking this assigned `undefined` over the project wording, so the modal
|
|
61
|
+
// claimed "this session has stopped saving" about a failed PROJECT write.
|
|
62
|
+
el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
|
|
63
|
+
assert.is(el.persistenceBrokenMessage, undefined, 'newest source wins while it is broken');
|
|
64
|
+
assert.is(el.persistenceBrokenDetail, 'session detail');
|
|
65
|
+
// ...and when the newest recovers, the copy falls BACK to the survivor rather than staying
|
|
66
|
+
// on the recovered source's wording (or reverting to the default).
|
|
67
|
+
el.reportPersistenceRecovered(SESSION);
|
|
68
|
+
assert.is(el.persistenceBrokenMessage, 'project copy');
|
|
69
|
+
assert.is(el.persistenceBrokenDetail, 'project detail');
|
|
70
|
+
assert.is(el.persistenceBroken, true);
|
|
71
|
+
});
|
|
72
|
+
Suite('recovering an unknown or already-recovered source is a no-op', () => {
|
|
73
|
+
const el = element();
|
|
74
|
+
el.reportPersistenceBroken({ message: 'project copy', source: PROJECT });
|
|
75
|
+
el.reportPersistenceRecovered(SESSION); // never reported
|
|
76
|
+
assert.equal(copy(el), { broken: true, message: 'project copy', detail: undefined });
|
|
77
|
+
el.reportPersistenceRecovered(PROJECT);
|
|
78
|
+
el.reportPersistenceRecovered(PROJECT); // twice
|
|
79
|
+
assert.equal(copy(el), { broken: false, message: undefined, detail: undefined });
|
|
80
|
+
});
|
|
81
|
+
Suite('re-reporting the same source refreshes its copy without duplicating it', () => {
|
|
82
|
+
const el = element();
|
|
83
|
+
el.reportPersistenceBroken({ detail: '2 messages have not been saved.', source: SESSION });
|
|
84
|
+
el.reportPersistenceBroken({ detail: '5 messages have not been saved.', source: SESSION });
|
|
85
|
+
assert.is(el.persistenceBrokenDetail, '5 messages have not been saved.');
|
|
86
|
+
// One recovery must clear it — a second entry for the same source would leave it stuck on.
|
|
87
|
+
el.reportPersistenceRecovered(SESSION);
|
|
88
|
+
assert.is(el.persistenceBroken, false);
|
|
89
|
+
});
|
|
90
|
+
Suite('a host that omits the source gets a default it can still recover', () => {
|
|
91
|
+
const el = element();
|
|
92
|
+
el.reportPersistenceBroken({ message: 'host copy' });
|
|
93
|
+
assert.is(el.persistenceBroken, true);
|
|
94
|
+
el.reportPersistenceRecovered();
|
|
95
|
+
assert.is(el.persistenceBroken, false);
|
|
96
|
+
});
|
|
97
|
+
Suite("a host's default source is independent of the session's", () => {
|
|
98
|
+
const el = element();
|
|
99
|
+
el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
|
|
100
|
+
el.reportPersistenceBroken({ message: 'host copy' });
|
|
101
|
+
el.reportPersistenceRecovered(); // the host's
|
|
102
|
+
assert.is(el.persistenceBroken, true, 'the session failure is still live');
|
|
103
|
+
assert.is(el.persistenceBrokenDetail, 'session detail');
|
|
104
|
+
});
|
|
105
|
+
// GENC-1511 (PR #2472 review) — the deferred show is a separate race from the source tracking.
|
|
106
|
+
// `persistenceBrokenChanged` queues the show via `DOM.queueUpdate`, and the queued callback used
|
|
107
|
+
// not to re-read the flag. A `true → false` inside the same batch runs the `!broken` branch against
|
|
108
|
+
// a modal that was never shown (both statements no-op), and the callback then raised a blocking,
|
|
109
|
+
// NON-DISMISSIBLE modal for a problem that had already fixed itself — with no further change event
|
|
110
|
+
// left to close it, so the only way out was a reload nobody needed.
|
|
111
|
+
//
|
|
112
|
+
// Reachable because `maybeReportBroken` fires before `provider.save` in `persistSession`, so a
|
|
113
|
+
// report and the write that withdraws it can share a tick.
|
|
114
|
+
/** Minimal stand-in for the design-system modal, recording what the callback did to it. */
|
|
115
|
+
function modalStub() {
|
|
116
|
+
const calls = { show: 0, close: 0 };
|
|
117
|
+
const dialog = document.createElement('dialog');
|
|
118
|
+
return {
|
|
119
|
+
calls,
|
|
120
|
+
modal: {
|
|
121
|
+
open: false,
|
|
122
|
+
mainElement: dialog,
|
|
123
|
+
show() {
|
|
124
|
+
calls.show += 1;
|
|
125
|
+
this.open = true;
|
|
126
|
+
},
|
|
127
|
+
close() {
|
|
128
|
+
calls.close += 1;
|
|
129
|
+
this.open = false;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Drain the FAST update queue SYNCHRONOUSLY.
|
|
136
|
+
*
|
|
137
|
+
* `DOM.processUpdates()` and not an `await setTimeout(0)`: the queue lands on
|
|
138
|
+
* `requestAnimationFrame`, which a zero-delay timer does not reliably cover. A timer-based flush
|
|
139
|
+
* made these tests pass or fail on scheduling luck — the "does not show" assertion passed for the
|
|
140
|
+
* wrong reason, and its positive control passed only because earlier tests had pumped the queue.
|
|
141
|
+
*/
|
|
142
|
+
const flush = () => DOM.processUpdates();
|
|
143
|
+
const withModal = (el, modal) => {
|
|
144
|
+
el.persistenceModal = modal;
|
|
145
|
+
};
|
|
146
|
+
// POSITIVE CONTROL for the test below. Without it, a queue that never drains in this environment
|
|
147
|
+
// would make the "does not show" assertion pass for entirely the wrong reason.
|
|
148
|
+
Suite('the deferred show DOES raise the modal when the failure is still live', () => {
|
|
149
|
+
const el = element();
|
|
150
|
+
const { calls, modal } = modalStub();
|
|
151
|
+
withModal(el, modal);
|
|
152
|
+
el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
|
|
153
|
+
flush();
|
|
154
|
+
assert.is(calls.show, 1, 'precondition: the queued callback runs in this environment');
|
|
155
|
+
assert.is(modal.open, true);
|
|
156
|
+
});
|
|
157
|
+
Suite('a recovery inside the same batch cancels the deferred show', () => {
|
|
158
|
+
const el = element();
|
|
159
|
+
const { calls, modal } = modalStub();
|
|
160
|
+
withModal(el, modal);
|
|
161
|
+
el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
|
|
162
|
+
el.reportPersistenceRecovered(SESSION); // lands before the queue flushes
|
|
163
|
+
flush();
|
|
164
|
+
assert.is(el.persistenceBroken, false);
|
|
165
|
+
assert.is(calls.show, 0, 'must not raise a blocking modal for a resolved failure');
|
|
166
|
+
assert.is(modal.open, false);
|
|
167
|
+
});
|
|
168
|
+
Suite('a recovery of ONE source still shows for the source left broken', () => {
|
|
169
|
+
const el = element();
|
|
170
|
+
const { calls, modal } = modalStub();
|
|
171
|
+
withModal(el, modal);
|
|
172
|
+
el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
|
|
173
|
+
el.reportPersistenceBroken({ message: 'project copy', source: PROJECT });
|
|
174
|
+
el.reportPersistenceRecovered(SESSION);
|
|
175
|
+
flush();
|
|
176
|
+
assert.is(el.persistenceBroken, true);
|
|
177
|
+
assert.is(calls.show, 1, 'the project failure still warrants the modal');
|
|
178
|
+
assert.is(el.persistenceBrokenMessage, 'project copy');
|
|
179
|
+
});
|
|
180
|
+
Suite.run();
|
|
@@ -42,6 +42,9 @@ export const META_EVENT_IMPORTANCE = {
|
|
|
42
42
|
'context.threshold-crossed': 'high',
|
|
43
43
|
'context.compacted': 'high',
|
|
44
44
|
'persistence.restore-orphan': 'high',
|
|
45
|
+
// A save that did not land is silent data loss in progress: the transcript on screen
|
|
46
|
+
// is no longer backed by the store, and every later turn widens the gap. GENC-1511.
|
|
47
|
+
'persistence.save-failed': 'high',
|
|
45
48
|
'assistant.connected': 'normal',
|
|
46
49
|
'assistant.disconnected': 'normal',
|
|
47
50
|
'assistant.popout': 'normal',
|