@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.
Files changed (31) hide show
  1. package/dist/ai-assistant.api.json +367 -0
  2. package/dist/ai-assistant.d.ts +129 -1
  3. package/dist/chat-driver.cjs +3 -0
  4. package/dist/chat-driver.cjs.map +2 -2
  5. package/dist/chat-driver.mjs +3 -0
  6. package/dist/chat-driver.mjs.map +2 -2
  7. package/dist/custom-elements.json +232 -3
  8. package/dist/dts/main/main.d.ts +129 -0
  9. package/dist/dts/main/main.d.ts.map +1 -1
  10. package/dist/dts/main/main.template.d.ts +0 -1
  11. package/dist/dts/main/main.template.d.ts.map +1 -1
  12. package/dist/dts/main/persistence-broken-sources.test.d.ts +2 -0
  13. package/dist/dts/main/persistence-broken-sources.test.d.ts.map +1 -0
  14. package/dist/dts/state/debug-event-log.d.ts +1 -1
  15. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  16. package/dist/dts/state/persistence/session-persister.d.ts +76 -0
  17. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  18. package/dist/esm/main/main.js +205 -0
  19. package/dist/esm/main/main.template.js +89 -0
  20. package/dist/esm/main/persistence-broken-sources.test.js +180 -0
  21. package/dist/esm/state/debug-event-log.js +3 -0
  22. package/dist/esm/state/persistence/session-persister.js +224 -43
  23. package/dist/esm/state/persistence/session-persister.test.js +237 -3
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +17 -17
  26. package/src/main/main.template.ts +101 -0
  27. package/src/main/main.ts +230 -0
  28. package/src/main/persistence-broken-sources.test.ts +219 -0
  29. package/src/state/debug-event-log.ts +4 -0
  30. package/src/state/persistence/session-persister.test.ts +302 -33
  31. package/src/state/persistence/session-persister.ts +260 -52
@@ -1,5 +1,6 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
+ import { clearMetaEventRegistry, getMetaEvents } from '../debug-event-log';
3
4
  import { clearDiagnosticsCursorRegistry } from './diagnostics-cursors';
4
5
  import { clearPersisterRegistry } from './persister-registry';
5
6
  import { SessionPersister } from './session-persister';
@@ -14,8 +15,10 @@ const tick = () => new Promise((r) => {
14
15
  setTimeout(r, 0);
15
16
  });
16
17
  function makePersister(over = {}) {
17
- const f = Object.assign({ saved: [], cleared: 0, loadResult: null, loadedHistory: [], primed: [], loadSessionCalls: [], inputValues: [], history: [], activeAgent: undefined, enabled: true, restoring: false, diagnosticEntries: [], appended: [], savedPrefs: [], loadPrefsResult: undefined, diagnosticsCapable: true }, over);
18
+ const f = Object.assign({ saved: [], cleared: 0, loadResult: null, loadedHistory: [], primed: [], loadSessionCalls: [], inputValues: [], history: [], activeAgent: undefined, enabled: true, restoring: false, diagnosticEntries: [], appended: [], savedPrefs: [], loadPrefsResult: undefined, diagnosticsCapable: true, saveError: undefined, brokenReports: [], recovered: 0 }, over);
18
19
  const provider = Object.assign({ saveDebounceMs: 0, diagnosticsDebounceMs: 0, save: (_k, snap) => __awaiter(this, void 0, void 0, function* () {
20
+ if (f.saveError)
21
+ throw f.saveError;
19
22
  f.saved.push(snap);
20
23
  }), load: () => __awaiter(this, void 0, void 0, function* () { return f.loadResult; }), clear: () => __awaiter(this, void 0, void 0, function* () {
21
24
  f.cleared += 1;
@@ -73,6 +76,10 @@ function makePersister(over = {}) {
73
76
  getDriver: () => driver,
74
77
  getStore: () => store,
75
78
  getDiagnosticEntries: () => f.diagnosticEntries,
79
+ onPersistenceBroken: (d) => f.brokenReports.push(d),
80
+ onPersistenceRecovered: () => {
81
+ f.recovered += 1;
82
+ },
76
83
  };
77
84
  return { p: new SessionPersister(deps), f };
78
85
  }
@@ -80,6 +87,7 @@ const Suite = createLogicSuite('SessionPersister');
80
87
  Suite.before.each(() => {
81
88
  clearPersisterRegistry();
82
89
  clearDiagnosticsCursorRegistry();
90
+ clearMetaEventRegistry();
83
91
  });
84
92
  Suite('restore() hydrates the store + driver and appends a "Restored" divider', () => __awaiter(void 0, void 0, void 0, function* () {
85
93
  const snapshot = createPersistedSession({
@@ -191,7 +199,7 @@ Suite('scheduleSave() is a no-op when persistence is disabled', () => __awaiter(
191
199
  yield tick();
192
200
  assert.is(f.saved.length, 0);
193
201
  }));
194
- Suite('freeze: a non-resumable agent stamps the PRIOR snapshot, never an empty one', () => __awaiter(void 0, void 0, void 0, function* () {
202
+ Suite('freeze: a non-resumable agent keeps the conversation and drops only in-flight work', () => __awaiter(void 0, void 0, void 0, function* () {
195
203
  var _a;
196
204
  // Prior persisted (resumable) snapshot with real conversation.
197
205
  const prior = createPersistedSession({
@@ -227,7 +235,67 @@ Suite('freeze: a non-resumable agent stamps the PRIOR snapshot, never an empty o
227
235
  const written = f.saved[0];
228
236
  assert.ok(written.interrupted, 'stamped with an interrupted marker');
229
237
  assert.is((_a = written.interrupted) === null || _a === void 0 ? void 0 : _a.message, 'start again');
230
- assert.is(written.messages.length, 2, 'froze at the PRIOR transcript, not the in-flight one');
238
+ // GENC-1511: the boundary is the LIVE transcript through the last user message, floored
239
+ // at whatever was already persisted — never the stored blob, which is what silently
240
+ // rolled a session back three hours. Here the last user message is index 0, so the floor
241
+ // (nothing persisted by this persister yet) leaves 1; the in-flight assistant turn is
242
+ // dropped either way, which is the property that matters.
243
+ assert.is(written.messages.length, 1, 'in-flight agent work is not persisted');
244
+ assert.is(written.messages[0].content, 'earlier', 'the conversation itself is kept');
245
+ assert.is(written.flowOwnerAgentName, null, 'flow ownership dropped');
246
+ assert.equal(written.agentSnapshots, {}, 'flow state dropped');
247
+ }));
248
+ Suite('freeze: the boundary never regresses below what was already persisted', () => __awaiter(void 0, void 0, void 0, function* () {
249
+ // A resumable save lands the full transcript; the agent then goes non-resumable WITHOUT
250
+ // a new user prompt (auto-advance, a resumed journey step). `keepThroughLastUserMessage`
251
+ // alone would rewind to message 1 and drop the completed exchange — the floor stops it.
252
+ const history = [
253
+ msg({ role: 'user', content: 'earlier' }),
254
+ msg({ role: 'assistant', content: 'ok' }),
255
+ ];
256
+ const agent = { name: 'Trade Ops', resumable: () => __awaiter(void 0, void 0, void 0, function* () { return true; }), serialize: () => ({}) };
257
+ const { p, f } = makePersister({ history, activeAgent: agent });
258
+ p.scheduleSave();
259
+ yield tick();
260
+ assert.is(f.saved.length, 1, 'the resumable save landed');
261
+ assert.is(f.saved[0].messages.length, 2, 'full transcript persisted');
262
+ agent.resumable = () => __awaiter(void 0, void 0, void 0, function* () { return false; });
263
+ history.push(msg({ role: 'assistant', content: 'in-flight work' }));
264
+ p.scheduleSave();
265
+ yield tick();
266
+ yield tick();
267
+ assert.is(f.saved.length, 2, 'the freeze wrote');
268
+ assert.is(f.saved[1].messages.length, 2, 'floored at the already-persisted length');
269
+ assert.ok(f.saved[1].interrupted, 'and stamped interrupted');
270
+ }));
271
+ Suite('freeze: an unmoved boundary skips the write, and never latches', () => __awaiter(void 0, void 0, void 0, function* () {
272
+ const history = [
273
+ msg({ role: 'user', content: 'do it' }),
274
+ msg({ role: 'assistant', content: 'working' }),
275
+ ];
276
+ const agent = {
277
+ name: 'Trade Ops',
278
+ resumable: () => __awaiter(void 0, void 0, void 0, function* () { return false; }),
279
+ serialize: () => ({}),
280
+ };
281
+ const { p, f } = makePersister({ history, activeAgent: agent });
282
+ p.scheduleSave();
283
+ yield tick();
284
+ yield tick();
285
+ assert.is(f.saved.length, 1, 'first freeze writes the boundary');
286
+ // More in-flight turns, same last user message → nothing new to freeze, no write.
287
+ history.push(msg({ role: 'assistant', content: 'still working' }));
288
+ p.scheduleSave();
289
+ yield tick();
290
+ yield tick();
291
+ assert.is(f.saved.length, 1, 'unmoved boundary does not re-write');
292
+ // The old `interrupted` latch made that state permanent. A new prompt must still land.
293
+ history.push(msg({ role: 'user', content: 'next thing' }));
294
+ p.scheduleSave();
295
+ yield tick();
296
+ yield tick();
297
+ assert.is(f.saved.length, 2, 'a moved boundary writes again — the skip is not a latch');
298
+ assert.is(f.saved[1].messages.length, 4, 'frozen through the new user message');
231
299
  }));
232
300
  Suite('clearRestoreCore() drops the snapshot', () => __awaiter(void 0, void 0, void 0, function* () {
233
301
  const { p, f } = makePersister();
@@ -376,4 +444,170 @@ Suite('flushDiagnostics() still dedups a genuine re-append of the same message a
376
444
  yield p.flushDiagnostics();
377
445
  assert.is(f.appended.length, 1, 'unchanged message → no re-append');
378
446
  }));
447
+ // GENC-1511: the save telemetry has to describe what HAPPENED, not what was attempted.
448
+ // `persistence.saved` used to be emitted before `provider.save` was awaited and the
449
+ // rejection was swallowed into `logger.error`, so a session whose writes had been
450
+ // failing for hours reported nothing but successful saves — in the debug log, in Loki
451
+ // and in Sentry alike.
452
+ Suite('a save that lands emits persistence.saved and no failure event', () => __awaiter(void 0, void 0, void 0, function* () {
453
+ const { p, f } = makePersister({ history: [msg({ content: 'hi' })] });
454
+ p.scheduleSave();
455
+ yield tick();
456
+ assert.is(f.saved.length, 1, 'the write landed');
457
+ const types = getMetaEvents(KEY).map((e) => e.type);
458
+ assert.ok(types.includes('persistence.saved'), 'success is recorded');
459
+ assert.not.ok(types.includes('persistence.save-failed'), 'no failure recorded');
460
+ }));
461
+ Suite('a save that throws emits persistence.save-failed and NOT persistence.saved', () => __awaiter(void 0, void 0, void 0, function* () {
462
+ var _a, _b;
463
+ const { p, f } = makePersister({
464
+ history: [msg({ content: 'hi' })],
465
+ saveError: Object.assign(new TypeError('Converting circular structure to JSON'), {}),
466
+ });
467
+ p.scheduleSave();
468
+ yield tick();
469
+ assert.is(f.saved.length, 0, 'nothing was written');
470
+ const events = getMetaEvents(KEY);
471
+ const types = events.map((e) => e.type);
472
+ assert.not.ok(types.includes('persistence.saved'), 'a failed write must never report itself as saved');
473
+ const failed = events.find((e) => e.type === 'persistence.save-failed');
474
+ assert.ok(failed, 'the failure is recorded');
475
+ // `name` is what separates a transport rejection from a serialisation fault: a
476
+ // TypeError means the body never reached the wire, so there is no server-side trace.
477
+ assert.is((_a = failed === null || failed === void 0 ? void 0 : failed.detail) === null || _a === void 0 ? void 0 : _a.error, 'TypeError');
478
+ assert.is((_b = failed === null || failed === void 0 ? void 0 : failed.detail) === null || _b === void 0 ? void 0 : _b.messages, 1);
479
+ }));
480
+ // GENC-1511 — the user-facing warning. The gate is `resumable`, not `busy`: a bootstrap journey
481
+ // is ONE turn (measured at 2817 s with 82 widget interactions inside it), so a turn-boundary gate
482
+ // would hold the warning for 47 minutes.
483
+ Suite('a single failed save does NOT report broken — one blip is not an outage', () => __awaiter(void 0, void 0, void 0, function* () {
484
+ const { p, f } = makePersister({
485
+ history: [msg({ content: 'hi' })],
486
+ saveError: new Error('transient'),
487
+ });
488
+ p.scheduleSave();
489
+ yield tick();
490
+ assert.is(f.brokenReports.length, 0, 'a modal about a self-healing blip trains dismissal');
491
+ }));
492
+ Suite('two consecutive failures report broken when the agent is resumable', () => __awaiter(void 0, void 0, void 0, function* () {
493
+ const history = [msg({ content: 'one' })];
494
+ const { p, f } = makePersister({ history, saveError: new Error('boom') });
495
+ p.scheduleSave();
496
+ yield tick();
497
+ history.push(msg({ content: 'two' }));
498
+ p.scheduleSave();
499
+ yield tick();
500
+ assert.is(f.brokenReports.length, 1, 'reported once, at the second failure');
501
+ assert.is(f.brokenReports[0].reason, 'boom');
502
+ assert.is(f.brokenReports[0].unsavedMessages, 2, 'counts what the store does not have');
503
+ // ...and only once per episode, however many more saves fail.
504
+ history.push(msg({ content: 'three' }));
505
+ p.scheduleSave();
506
+ yield tick();
507
+ assert.is(f.brokenReports.length, 1, 'not re-reported while still broken');
508
+ }));
509
+ Suite('a NON-resumable agent holds the report back until it is safe to reload', () => __awaiter(void 0, void 0, void 0, function* () {
510
+ const history = [msg({ content: 'one' }), msg({ content: 'two' })];
511
+ const agent = {
512
+ name: 'UI Builder',
513
+ resumable: () => __awaiter(void 0, void 0, void 0, function* () { return false; }),
514
+ serialize: () => ({}),
515
+ };
516
+ const { p, f } = makePersister({ history, activeAgent: agent, saveError: new Error('boom') });
517
+ // Unrolled deliberately: these saves must happen in ORDER, so the lint rule's suggested
518
+ // Promise.all would change what is being tested.
519
+ history.push(msg({ role: 'assistant', content: 'turn 0' }));
520
+ p.scheduleSave();
521
+ yield tick();
522
+ history.push(msg({ role: 'assistant', content: 'turn 1' }));
523
+ p.scheduleSave();
524
+ yield tick();
525
+ history.push(msg({ role: 'assistant', content: 'turn 2' }));
526
+ p.scheduleSave();
527
+ yield tick();
528
+ assert.is(f.brokenReports.length, 0, 'advising a reload mid-flow would discard the VFS buffer');
529
+ // Back at rest: now a reload is safe, so now the user is told.
530
+ agent.resumable = () => __awaiter(void 0, void 0, void 0, function* () { return true; });
531
+ history.push(msg({ content: 'next' }));
532
+ p.scheduleSave();
533
+ yield tick();
534
+ assert.is(f.brokenReports.length, 1, 'reported at the first resumable moment');
535
+ }));
536
+ Suite('a save that lands withdraws the warning', () => __awaiter(void 0, void 0, void 0, function* () {
537
+ const history = [msg({ content: 'one' })];
538
+ const fakes = { history, saveError: new Error('boom') };
539
+ const { p, f } = makePersister(fakes);
540
+ p.scheduleSave();
541
+ yield tick();
542
+ history.push(msg({ content: 'two' }));
543
+ p.scheduleSave();
544
+ yield tick();
545
+ assert.is(f.brokenReports.length, 1, 'broken');
546
+ f.saveError = undefined; // the store comes back — observed twice in the wild
547
+ history.push(msg({ content: 'three' }));
548
+ p.scheduleSave();
549
+ yield tick();
550
+ assert.is(f.recovered, 1, 'the warning is withdrawn rather than left stale');
551
+ assert.ok(f.saved.length > 0, 'and the write landed');
552
+ }));
553
+ Suite('a failed FREEZE write is reported, not swallowed', () => __awaiter(void 0, void 0, void 0, function* () {
554
+ var _a;
555
+ // This catch was the one silent failure left after save-failed was added to the resumable
556
+ // branch: a session breaking while an agent was mid-flow reported nothing at all.
557
+ const history = [msg({ content: 'do it' }), msg({ role: 'assistant', content: 'working' })];
558
+ const { p, f } = makePersister({
559
+ history,
560
+ activeAgent: { name: 'UI Builder', resumable: () => __awaiter(void 0, void 0, void 0, function* () { return false; }), serialize: () => ({}) },
561
+ saveError: new Error('freeze write failed'),
562
+ });
563
+ p.scheduleSave();
564
+ yield tick();
565
+ yield tick();
566
+ const failed = getMetaEvents(KEY).filter((e) => e.type === 'persistence.save-failed');
567
+ assert.is(failed.length, 1, 'the freeze path now emits the failure event');
568
+ assert.is((_a = failed[0].detail) === null || _a === void 0 ? void 0 : _a.frozen, true, 'marked as the freeze path');
569
+ }));
570
+ // GENC-1511 review [P1]: the failure event must stay serialisable even when the thing that broke
571
+ // the save is the agent snapshot itself. `recordMetaEvent` stores detail BY REFERENCE, so
572
+ // spreading the captured snapshot made the high-importance event circular too — and then
573
+ // `assembleDebugLog` threw while stringifying it, so `downloadDebugLog` could not produce the log
574
+ // the blocking modal tells the user to save. Reproduced by Matt; regression-guarded here.
575
+ Suite('the save-failed event stays serialisable when the agent snapshot is circular', () => __awaiter(void 0, void 0, void 0, function* () {
576
+ var _a;
577
+ const circular = { machine: 'generating' };
578
+ circular.self = circular;
579
+ const { p } = makePersister({
580
+ history: [msg({ content: 'hi' })],
581
+ activeAgent: { name: 'UI Builder', serialize: () => circular },
582
+ saveError: new TypeError('Converting circular structure to JSON'),
583
+ });
584
+ p.scheduleSave();
585
+ yield tick();
586
+ const events = getMetaEvents(KEY);
587
+ const failed = events.find((e) => e.type === 'persistence.save-failed');
588
+ assert.ok(failed, 'the failure is recorded');
589
+ assert.not.ok((_a = failed === null || failed === void 0 ? void 0 : failed.detail) === null || _a === void 0 ? void 0 : _a.agentSnapshots, 'the raw snapshot must NOT be in the failure detail');
590
+ // The whole point: the debug log must still be producible.
591
+ JSON.stringify(events);
592
+ }));
593
+ Suite('a non-resumable agent is NEVER told to reload, however long it stays broken', () => __awaiter(void 0, void 0, void 0, function* () {
594
+ // GENC-1511 review [P1]: an earlier draft bypassed the gate after 90 s. The modal's only action
595
+ // is a reload, so firing it here forces the user to discard the out-of-band in-flight work the
596
+ // gate exists to protect.
597
+ const history = [msg({ content: 'do it' })];
598
+ const { p, f } = makePersister({
599
+ history,
600
+ activeAgent: { name: 'UI Builder', resumable: () => __awaiter(void 0, void 0, void 0, function* () { return false; }), serialize: () => ({}) },
601
+ saveError: new Error('boom'),
602
+ });
603
+ history.push(msg({ role: 'assistant', content: 'a' }));
604
+ p.scheduleSave();
605
+ yield tick();
606
+ yield tick();
607
+ history.push(msg({ role: 'assistant', content: 'b' }));
608
+ p.scheduleSave();
609
+ yield tick();
610
+ yield tick();
611
+ assert.is(f.brokenReports.length, 0, 'no unsafe reload prompt, no matter how many failures');
612
+ }));
379
613
  Suite.run();
@@ -1 +1 @@
1
- {"root":["../src/chat-driver-node.ts","../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/align-event-globals.ts","../src/components/chat-driver/chat-driver.compact.test.ts","../src/components/chat-driver/chat-driver.invocation-scope.test.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.thinking-policy.test.ts","../src/components/chat-driver/chat-driver.trace-capture.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/chat-driver.turn-usage.test.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.budget.test.ts","../src/components/orchestrating-driver/orchestrating-driver.pin.test.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/components/settings-modal/index.ts","../src/components/settings-modal/settings-modal.styles.test.ts","../src/components/settings-modal/settings-modal.styles.ts","../src/components/settings-modal/settings-modal.template.test.ts","../src/components/settings-modal/settings-modal.template.ts","../src/config/config.ts","../src/config/define-stateful-agent.test.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/blocked-state.test.ts","../src/main/budget-meter.test.ts","../src/main/cost-session-banking.test.ts","../src/main/index.ts","../src/main/main.styles.test.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/main/popout-interaction-gate.test.ts","../src/provider/ai-provider-switcher.ts","../src/provider/assistant-app-settings.ts","../src/state/ai-assistant-slice.test.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.test.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.test.ts","../src/state/driver-registry.ts","../src/state/interaction-context.test.ts","../src/state/interaction-context.ts","../src/state/session-store.ts","../src/state/persistence/build-timeline-entries.ts","../src/state/persistence/diagnostics-cursors.test.ts","../src/state/persistence/diagnostics-cursors.ts","../src/state/persistence/diagnostics.test.ts","../src/state/persistence/diagnostics.ts","../src/state/persistence/index.ts","../src/state/persistence/persister-registry.ts","../src/state/persistence/session-persistence-provider.test.ts","../src/state/persistence/session-persistence-provider.ts","../src/state/persistence/session-persistence.integration.test.ts","../src/state/persistence/session-persister.test.ts","../src/state/persistence/session-persister.ts","../src/state/persistence/session-snapshot.test.ts","../src/state/persistence/session-snapshot.ts","../src/state/persistence/stateful-restore.e2e.test.ts","../src/styles/ai-colours.ts","../src/styles/settings-section.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/types/interaction-context.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/banked-usage-baselines.ts","../src/utils/collect-session-models.test.ts","../src/utils/collect-session-models.ts","../src/utils/condense-history.test.ts","../src/utils/condense-history.ts","../src/utils/cost-session-history.test.ts","../src/utils/cost-session-history.ts","../src/utils/derive-cost-session-title.test.ts","../src/utils/derive-cost-session-title.ts","../src/utils/flatten-sub-agent-messages.test.ts","../src/utils/flatten-sub-agent-messages.ts","../src/utils/format-usd.ts","../src/utils/history-transform.test.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/resolve-cost-history-config.test.ts","../src/utils/resolve-cost-history-config.ts","../src/utils/resolve-preference-baseline.test.ts","../src/utils/resolve-preference-baseline.ts","../src/utils/strip-agent-handlers.test.ts","../src/utils/strip-agent-handlers.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/sum-tokens.test.ts","../src/utils/sum-tokens.ts","../src/utils/sum-usage.test.ts","../src/utils/sum-usage.ts","../src/utils/tool-fold.ts","../src/utils/usage-rows.test.ts","../src/utils/usage-rows.ts","../src/utils/with-timeout.ts"],"version":"5.9.2"}
1
+ {"root":["../src/chat-driver-node.ts","../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/align-event-globals.ts","../src/components/chat-driver/chat-driver.compact.test.ts","../src/components/chat-driver/chat-driver.invocation-scope.test.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.thinking-policy.test.ts","../src/components/chat-driver/chat-driver.trace-capture.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/chat-driver.turn-usage.test.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.budget.test.ts","../src/components/orchestrating-driver/orchestrating-driver.pin.test.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/components/settings-modal/index.ts","../src/components/settings-modal/settings-modal.styles.test.ts","../src/components/settings-modal/settings-modal.styles.ts","../src/components/settings-modal/settings-modal.template.test.ts","../src/components/settings-modal/settings-modal.template.ts","../src/config/config.ts","../src/config/define-stateful-agent.test.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/blocked-state.test.ts","../src/main/budget-meter.test.ts","../src/main/cost-session-banking.test.ts","../src/main/index.ts","../src/main/main.styles.test.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/main/persistence-broken-sources.test.ts","../src/main/popout-interaction-gate.test.ts","../src/provider/ai-provider-switcher.ts","../src/provider/assistant-app-settings.ts","../src/state/ai-assistant-slice.test.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.test.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.test.ts","../src/state/driver-registry.ts","../src/state/interaction-context.test.ts","../src/state/interaction-context.ts","../src/state/session-store.ts","../src/state/persistence/build-timeline-entries.ts","../src/state/persistence/diagnostics-cursors.test.ts","../src/state/persistence/diagnostics-cursors.ts","../src/state/persistence/diagnostics.test.ts","../src/state/persistence/diagnostics.ts","../src/state/persistence/index.ts","../src/state/persistence/persister-registry.ts","../src/state/persistence/session-persistence-provider.test.ts","../src/state/persistence/session-persistence-provider.ts","../src/state/persistence/session-persistence.integration.test.ts","../src/state/persistence/session-persister.test.ts","../src/state/persistence/session-persister.ts","../src/state/persistence/session-snapshot.test.ts","../src/state/persistence/session-snapshot.ts","../src/state/persistence/stateful-restore.e2e.test.ts","../src/styles/ai-colours.ts","../src/styles/settings-section.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/types/interaction-context.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/banked-usage-baselines.ts","../src/utils/collect-session-models.test.ts","../src/utils/collect-session-models.ts","../src/utils/condense-history.test.ts","../src/utils/condense-history.ts","../src/utils/cost-session-history.test.ts","../src/utils/cost-session-history.ts","../src/utils/derive-cost-session-title.test.ts","../src/utils/derive-cost-session-title.ts","../src/utils/flatten-sub-agent-messages.test.ts","../src/utils/flatten-sub-agent-messages.ts","../src/utils/format-usd.ts","../src/utils/history-transform.test.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/resolve-cost-history-config.test.ts","../src/utils/resolve-cost-history-config.ts","../src/utils/resolve-preference-baseline.test.ts","../src/utils/resolve-preference-baseline.ts","../src/utils/strip-agent-handlers.test.ts","../src/utils/strip-agent-handlers.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/sum-tokens.test.ts","../src/utils/sum-tokens.ts","../src/utils/sum-usage.test.ts","../src/utils/sum-usage.ts","../src/utils/tool-fold.ts","../src/utils/usage-rows.test.ts","../src/utils/usage-rows.ts","../src/utils/with-timeout.ts"],"version":"5.9.2"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "15.15.1",
4
+ "version": "15.15.2",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -73,26 +73,26 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@genesislcap/foundation-testing": "15.15.1",
77
- "@genesislcap/genx": "15.15.1",
78
- "@genesislcap/rollup-builder": "15.15.1",
79
- "@genesislcap/ts-builder": "15.15.1",
80
- "@genesislcap/uvu-playwright-builder": "15.15.1",
81
- "@genesislcap/vite-builder": "15.15.1",
82
- "@genesislcap/webpack-builder": "15.15.1",
76
+ "@genesislcap/foundation-testing": "15.15.2",
77
+ "@genesislcap/genx": "15.15.2",
78
+ "@genesislcap/rollup-builder": "15.15.2",
79
+ "@genesislcap/ts-builder": "15.15.2",
80
+ "@genesislcap/uvu-playwright-builder": "15.15.2",
81
+ "@genesislcap/vite-builder": "15.15.2",
82
+ "@genesislcap/webpack-builder": "15.15.2",
83
83
  "@types/dompurify": "^3.0.5",
84
84
  "@types/marked": "^5.0.2",
85
85
  "esbuild": "0.25.12"
86
86
  },
87
87
  "dependencies": {
88
- "@genesislcap/foundation-ai": "15.15.1",
89
- "@genesislcap/foundation-logger": "15.15.1",
90
- "@genesislcap/foundation-notifications": "15.15.1",
91
- "@genesislcap/foundation-redux": "15.15.1",
92
- "@genesislcap/foundation-ui": "15.15.1",
93
- "@genesislcap/foundation-utils": "15.15.1",
94
- "@genesislcap/rapid-design-system": "15.15.1",
95
- "@genesislcap/web-core": "15.15.1",
88
+ "@genesislcap/foundation-ai": "15.15.2",
89
+ "@genesislcap/foundation-logger": "15.15.2",
90
+ "@genesislcap/foundation-notifications": "15.15.2",
91
+ "@genesislcap/foundation-redux": "15.15.2",
92
+ "@genesislcap/foundation-ui": "15.15.2",
93
+ "@genesislcap/foundation-utils": "15.15.2",
94
+ "@genesislcap/rapid-design-system": "15.15.2",
95
+ "@genesislcap/web-core": "15.15.2",
96
96
  "dompurify": "^3.3.1",
97
97
  "marked": "^17.0.3"
98
98
  },
@@ -105,5 +105,5 @@
105
105
  "access": "public"
106
106
  },
107
107
  "customElements": "dist/custom-elements.json",
108
- "gitHead": "773a578497ada0a9cee047399defa317c2982940"
108
+ "gitHead": "93190f6fb9fbb494a02b6a3fae68bdd1538797ff"
109
109
  }
@@ -407,6 +407,106 @@ const blockedBannerTemplate = html<FoundationAiAssistant>`
407
407
  // ─── Public factory ───────────────────────────────────────────────────────────
408
408
 
409
409
  /** @internal */
410
+
411
+ /**
412
+ * GENC-1511 — blocking "not saving" modal.
413
+ *
414
+ * Deliberately has NO way out but Reload. The failure it reports is invisible by nature — a
415
+ * prod session wrote nothing for 3h17m with a normal-looking UI throughout — and anything the
416
+ * user can dismiss is an invitation to carry on producing work that will be lost. So:
417
+ * `show-close-icon` off, our own `bottom` slot replacing the default close button, and the
418
+ * native dialog's Escape route cancelled on `mainElement` in `persistenceBrokenChanged`.
419
+ *
420
+ * It is only ever shown at a point where reloading is SAFE — the persister gates on the active
421
+ * agent being resumable — so the advice cannot cost the user in-flight work it is unable to carry.
422
+ *
423
+ * ⚠ `showCloseIcon` is set as a PROPERTY (`:showCloseIcon`), not via the `?show-close-icon`
424
+ * boolean attribute. It is an `@attr({ mode: 'boolean' })` whose FIELD default is `true`, so the
425
+ * property reflects the attribute ON at construction and the boolean binding's "absent means
426
+ * false" never wins. Measured live 2026-08-20: the close icon rendered anyway
427
+ * (`showCloseIcon=true`, `hasAttribute=true`) until the property was set directly. Do not
428
+ * "simplify" it back to `?show-close-icon` — this modal must not be dismissible.
429
+ */
430
+ const persistenceBrokenModalTemplate = (
431
+ designSystemPrefix: string,
432
+ ): ViewTemplate<FoundationAiAssistant> => {
433
+ const modalTag = `${designSystemPrefix}-modal`;
434
+ const buttonTag = `${designSystemPrefix}-button`;
435
+ const iconTag = `${designSystemPrefix}-icon`;
436
+ return html<FoundationAiAssistant>`
437
+ <${modalTag}
438
+ class="persistence-broken-modal"
439
+ part="persistence-broken-modal"
440
+ ${ref('persistenceModal')}
441
+ initial-width="480"
442
+ :showCloseIcon=${() => false}
443
+ >
444
+ <div slot="top" class="persistence-broken-header" part="persistence-broken-header">
445
+ <h2 class="persistence-broken-title">
446
+ <${iconTag} name="triangle-exclamation"></${iconTag}> Your work isn't being saved
447
+ </h2>
448
+ </div>
449
+ <div class="persistence-broken-content" part="persistence-broken-content">
450
+ <p>
451
+ ${(x) =>
452
+ x.persistenceBrokenMessage ??
453
+ "We've encountered a problem and this session has stopped saving to the server. " +
454
+ 'Anything you do from here will be lost, so please reload before carrying on.'}
455
+ </p>
456
+ <!--
457
+ Quantified where the reporter can manage it, because "some work" is not actionable.
458
+ Composed by the CALLER, not here: the assistant's persister counts unsaved messages, a
459
+ host counting something else has its own sentence, and a template that knew about
460
+ "messages" could only ever serve the first. Omitted entirely when nobody supplied one.
461
+ -->
462
+ ${when(
463
+ (x) => !!x.persistenceBrokenDetail,
464
+ html<FoundationAiAssistant>`
465
+ <p class="persistence-broken-detail">${(x) => x.persistenceBrokenDetail}</p>
466
+ `,
467
+ )}
468
+ <p class="persistence-broken-detail">
469
+ Reloading restores the last saved point.${when(
470
+ (x) => x.chatConfig.ui?.allowDebugDownload === true,
471
+ html<FoundationAiAssistant>`
472
+ You can download a record of this conversation first.
473
+ `,
474
+ )}
475
+ </p>
476
+ </div>
477
+ <div slot="bottom" class="persistence-broken-actions" part="persistence-broken-actions">
478
+ <!--
479
+ The download lives HERE, not in Settings. The copy used to point at the Settings panel,
480
+ which this modal makes unreachable by design — offering a record the user cannot get to.
481
+ Gated on the host's own allowDebugDownload flag, same as the Settings entry, so a host
482
+ that turned it off does not get it smuggled back in through the failure path; the
483
+ sentence above is gated on the same flag so the copy never promises a missing button.
484
+ -->
485
+ ${when(
486
+ (x) => x.chatConfig.ui?.allowDebugDownload === true,
487
+ html<FoundationAiAssistant>`
488
+ <${buttonTag}
489
+ appearance="outline"
490
+ part="persistence-broken-download"
491
+ ?disabled=${(x) => x.downloadingDebugLog}
492
+ @click=${(x) => x.downloadDebugLog()}
493
+ >
494
+ ${(x) => (x.downloadingDebugLog ? 'Preparing…' : 'Download agent log')}
495
+ </${buttonTag}>
496
+ `,
497
+ )}
498
+ <${buttonTag}
499
+ appearance="accent"
500
+ part="persistence-broken-reload"
501
+ @click=${(x) => x.reloadForPersistence()}
502
+ >
503
+ Reload the page
504
+ </${buttonTag}>
505
+ </div>
506
+ </${modalTag}>
507
+ `;
508
+ };
509
+
410
510
  export const FoundationAiAssistantTemplate = (
411
511
  designSystemPrefix: string,
412
512
  ): ViewTemplate<FoundationAiAssistant> => {
@@ -944,5 +1044,6 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
944
1044
  ></ai-halo-overlay>
945
1045
  </div>
946
1046
  ${SettingsModalTemplate(designSystemPrefix)}
1047
+ ${persistenceBrokenModalTemplate(designSystemPrefix)}
947
1048
  `;
948
1049
  };