@genesislcap/ai-assistant 15.56.0 → 15.58.0

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 (42) hide show
  1. package/dist/ai-assistant.api.json +438 -8
  2. package/dist/ai-assistant.d.ts +120 -4
  3. package/dist/chat-driver.cjs +60 -9
  4. package/dist/chat-driver.cjs.map +2 -2
  5. package/dist/chat-driver.mjs +60 -9
  6. package/dist/chat-driver.mjs.map +2 -2
  7. package/dist/custom-elements.json +238 -3
  8. package/dist/dts/components/agent-session/agent-session.d.ts +9 -0
  9. package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
  10. package/dist/dts/components/ai-driver/ai-driver.d.ts +31 -2
  11. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  12. package/dist/dts/components/chat-driver/chat-driver.d.ts +18 -2
  13. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  14. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +15 -2
  15. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  16. package/dist/dts/config/config.d.ts +21 -0
  17. package/dist/dts/config/config.d.ts.map +1 -1
  18. package/dist/dts/config/define-stateful-agent.d.ts +6 -0
  19. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
  20. package/dist/dts/main/main.d.ts +21 -0
  21. package/dist/dts/main/main.d.ts.map +1 -1
  22. package/dist/dts/state/debug-event-log.d.ts +1 -1
  23. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  24. package/dist/dts/state/persistence/resume-verdict.d.ts +32 -0
  25. package/dist/dts/state/persistence/resume-verdict.d.ts.map +1 -0
  26. package/dist/dts/state/persistence/session-persister.d.ts +34 -1
  27. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  28. package/dist/dts/state/persistence/session-snapshot.d.ts +19 -0
  29. package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
  30. package/dist/dts/utils/message-partition.d.ts +9 -1
  31. package/dist/dts/utils/message-partition.d.ts.map +1 -1
  32. package/dist/esm/components/agent-session/agent-session.js +14 -0
  33. package/dist/esm/components/chat-driver/chat-driver.js +12 -5
  34. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +34 -2
  35. package/dist/esm/config/define-stateful-agent.js +1 -0
  36. package/dist/esm/main/main.js +101 -22
  37. package/dist/esm/state/debug-event-log.js +4 -0
  38. package/dist/esm/state/persistence/resume-verdict.js +20 -0
  39. package/dist/esm/state/persistence/session-persister.js +55 -15
  40. package/dist/esm/state/persistence/session-snapshot.js +67 -0
  41. package/dist/esm/utils/message-partition.js +11 -2
  42. package/package.json +17 -17
@@ -47,6 +47,7 @@ import { buildTimelineEntries } from '../state/persistence/build-timeline-entrie
47
47
  import { assembleDebugLog, withFreshMetaSnapshot } from '../state/persistence/diagnostics';
48
48
  import { deleteDiagnosticsCursorsFor, resetDiagnosticsCursorsFor, } from '../state/persistence/diagnostics-cursors';
49
49
  import { getOrCreatePersister, getPersister, deletePersister, } from '../state/persistence/persister-registry';
50
+ import { resumeVerdict } from '../state/persistence/resume-verdict';
50
51
  import { SessionPersister, } from '../state/persistence/session-persister';
51
52
  import { lastUserMessageIndex } from '../state/persistence/session-snapshot';
52
53
  import { getSessionStore, hasSessionStore, deleteSessionStore, } from '../state/session-store';
@@ -3982,7 +3983,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3982
3983
  // the clobber this guards. A write through the captured ref after a swap/disconnect
3983
3984
  // is a harmless no-op on the now-detached store (the pattern `send()` uses).
3984
3985
  const ref = this._sessionRef;
3986
+ // Captured with `ref`, for the same reason (GENC-1662): the restored flow must be resumed on
3987
+ // THIS session's driver, and only while this session is still the one on screen.
3988
+ const driver = this.driver;
3985
3989
  ref === null || ref === void 0 ? void 0 : ref.actions.aiAssistant.setRestoring(true);
3990
+ let outcome;
3986
3991
  try {
3987
3992
  try {
3988
3993
  // The persister loads + owns the stored blob; reconcile overlays it on the
@@ -3995,12 +4000,79 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3995
4000
  logger.error('Persistence init: loadPreferences failed (using defaults):', e);
3996
4001
  }
3997
4002
  if (this.persistence.enabled) {
3998
- yield ((_b = this.persister()) === null || _b === void 0 ? void 0 : _b.restore());
4003
+ outcome = yield ((_b = this.persister()) === null || _b === void 0 ? void 0 : _b.restore());
4004
+ }
4005
+ // Re-activate the flow owner INSIDE the "Restoring…" gate (GENC-1662), so the composer stays
4006
+ // disabled while its `onActivate` runs. Outside a turn nothing else marks the driver busy, and
4007
+ // a send landing mid-activation used to run `onActivate` a second time.
4008
+ if ((outcome === null || outcome === void 0 ? void 0 : outcome.flowOwnerAgentName) && !this.isRestoredSessionStale(key, driver)) {
4009
+ yield this.reactivateRestoredFlow(driver);
3999
4010
  }
4000
4011
  }
4001
4012
  finally {
4002
4013
  ref === null || ref === void 0 ? void 0 : ref.actions.aiAssistant.setRestoring(false);
4003
4014
  }
4015
+ // After the gate lifts, so the resumed turn runs like any other. Not awaited: a resumed turn
4016
+ // can run as long as any turn, and nothing here waits on it.
4017
+ if (outcome === null || outcome === void 0 ? void 0 : outcome.resumeNote)
4018
+ void this.resumeInterruptedTurn(key, driver, ref, outcome);
4019
+ });
4020
+ }
4021
+ /**
4022
+ * Whether the session restored for `key` is no longer the one on screen (GENC-1662): switched in
4023
+ * place, or the element disconnected, while the load was in flight.
4024
+ */
4025
+ isRestoredSessionStale(key, driver) {
4026
+ return this._activeStateKey !== key || !driver || this.driver !== driver;
4027
+ }
4028
+ /**
4029
+ * Re-activate a restored flow owner (GENC-1662), so a stateful agent's `init({ restored })` — and
4030
+ * anything the host hangs off it, like a progress stepper — runs straight after the reload
4031
+ * instead of on the next message.
4032
+ */
4033
+ reactivateRestoredFlow(driver) {
4034
+ return __awaiter(this, void 0, void 0, function* () {
4035
+ var _a;
4036
+ try {
4037
+ yield ((_a = driver === null || driver === void 0 ? void 0 : driver.resumeRestoredFlow) === null || _a === void 0 ? void 0 : _a.call(driver));
4038
+ }
4039
+ catch (e) {
4040
+ logger.error('Session restore: re-activating the flow owner failed:', e);
4041
+ }
4042
+ });
4043
+ }
4044
+ /**
4045
+ * Run the turn a reload interrupted (GENC-1662), shown as busy exactly as a send is — or, when it
4046
+ * cannot run, withdraw the resume the restore divider promised.
4047
+ */
4048
+ resumeInterruptedTurn(key, driver, ref, outcome) {
4049
+ return __awaiter(this, void 0, void 0, function* () {
4050
+ const verdict = resumeVerdict(outcome, {
4051
+ stale: this.isRestoredSessionStale(key, driver),
4052
+ sendBlocked: this.sendBlocked,
4053
+ });
4054
+ const persister = getPersister(key);
4055
+ if (verdict.action === 'withdraw') {
4056
+ persister === null || persister === void 0 ? void 0 : persister.withdrawResume(verdict.reason);
4057
+ return;
4058
+ }
4059
+ if (verdict.action !== 'resume' || !(driver === null || driver === void 0 ? void 0 : driver.resumeRestoredFlow))
4060
+ return;
4061
+ this.state = 'loading';
4062
+ this.startLoadingTimer();
4063
+ try {
4064
+ const result = yield driver.resumeRestoredFlow(verdict.note);
4065
+ if ((result === null || result === void 0 ? void 0 : result.reason) === 'done') {
4066
+ this.budget.latchFrom(result.failureReason, ref, result.budget);
4067
+ }
4068
+ }
4069
+ catch (e) {
4070
+ logger.error('Session restore: resuming the restored flow failed:', e);
4071
+ persister === null || persister === void 0 ? void 0 : persister.withdrawResume('error');
4072
+ }
4073
+ finally {
4074
+ this.settleAfterTurn(ref);
4075
+ }
4004
4076
  });
4005
4077
  }
4006
4078
  triggerFileInput() {
@@ -4216,31 +4288,38 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
4216
4288
  }
4217
4289
  }
4218
4290
  finally {
4219
- this.stopLoadingTimer();
4220
4291
  this._pendingSendPrompt = undefined;
4221
- // Write directly via captured ref — this._sessionRef may be null if the element
4222
- // disconnected mid-execution. The state setter also calls syncShowHalo(); replicate
4223
- // that here for the case where this element is still connected.
4224
- const prevState = capturedSessionRef === null || capturedSessionRef === void 0 ? void 0 : capturedSessionRef.store.aiAssistant.state;
4225
- capturedSessionRef === null || capturedSessionRef === void 0 ? void 0 : capturedSessionRef.actions.aiAssistant.setState('idle');
4226
- // The setter's `state.changed` log is bypassed on this direct write, so
4227
- // emit it here — otherwise the debug timeline shows the turn entering
4228
- // loading/cancelling but never returning to idle.
4229
- if (prevState && prevState !== 'idle') {
4230
- this.logMeta('state.changed', { from: prevState, to: 'idle' });
4231
- }
4232
- this.syncShowHalo();
4233
- this.fetchSuggestions();
4234
- // setState was dispatched directly via the captured ref (not via the
4235
- // state setter) so the setter's loading→idle cleanup path doesn't fire
4236
- // here. Replicate the parts of it that matter for the post-turn settle.
4237
- this.rearmContextGate();
4238
- this.maybeAutoFocusChatInput();
4239
- this.applyDeferredSessionSwitch();
4240
- this.applyDeferredAgentsSwap();
4292
+ this.settleAfterTurn(capturedSessionRef);
4241
4293
  }
4242
4294
  });
4243
4295
  }
4296
+ /**
4297
+ * The post-turn settle shared by `send()` and a resumed restore turn: back to idle through
4298
+ * the session ref captured at turn start, then the cleanup the state setter would have done.
4299
+ */
4300
+ settleAfterTurn(capturedSessionRef) {
4301
+ this.stopLoadingTimer();
4302
+ // Write directly via captured ref — this._sessionRef may be null if the element
4303
+ // disconnected mid-execution. The state setter also calls syncShowHalo(); replicate
4304
+ // that here for the case where this element is still connected.
4305
+ const prevState = capturedSessionRef === null || capturedSessionRef === void 0 ? void 0 : capturedSessionRef.store.aiAssistant.state;
4306
+ capturedSessionRef === null || capturedSessionRef === void 0 ? void 0 : capturedSessionRef.actions.aiAssistant.setState('idle');
4307
+ // The setter's `state.changed` log is bypassed on this direct write, so
4308
+ // emit it here — otherwise the debug timeline shows the turn entering
4309
+ // loading/cancelling but never returning to idle.
4310
+ if (prevState && prevState !== 'idle') {
4311
+ this.logMeta('state.changed', { from: prevState, to: 'idle' });
4312
+ }
4313
+ this.syncShowHalo();
4314
+ this.fetchSuggestions();
4315
+ // setState was dispatched directly via the captured ref (not via the
4316
+ // state setter) so the setter's loading→idle cleanup path doesn't fire
4317
+ // here. Replicate the parts of it that matter for the post-turn settle.
4318
+ this.rearmContextGate();
4319
+ this.maybeAutoFocusChatInput();
4320
+ this.applyDeferredSessionSwitch();
4321
+ this.applyDeferredAgentsSwap();
4322
+ }
4244
4323
  onChatHeaderMouseDown(e) {
4245
4324
  if (this.popoutMode === 'collapse')
4246
4325
  return;
@@ -81,6 +81,10 @@ export const META_EVENT_IMPORTANCE = {
81
81
  'session.switched': 'low',
82
82
  'session.switch-deferred': 'low',
83
83
  'persistence.restored': 'normal',
84
+ // Whether a restored flow will be re-activated and resumed (GENC-1662), and how much the trim cut.
85
+ 'persistence.resume-planned': 'normal',
86
+ // A planned resume that did not run, and why (GENC-1662) — the divider was put back to plain.
87
+ 'persistence.resume-withdrawn': 'normal',
84
88
  'persistence.suppressed': 'normal',
85
89
  'persistence.not-resumable': 'normal',
86
90
  'driver.wired': 'low',
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Decide the resumed turn (GENC-1662). Pure, so the element's branching is testable on its own.
3
+ *
4
+ * - No note → nothing to resume (and nothing was promised).
5
+ * - `stale` → the session on screen is no longer the one restored (switched or disconnected while
6
+ * the load was in flight); running would put one session's turn on another's flow.
7
+ * - `sendBlocked` → a send would be refused right now (an exhausted budget, a full context), so is
8
+ * the resumed turn; the user resumes by hand once the block clears.
9
+ *
10
+ * @internal
11
+ */
12
+ export function resumeVerdict(outcome, context) {
13
+ if (!(outcome === null || outcome === void 0 ? void 0 : outcome.resumeNote))
14
+ return { action: 'none' };
15
+ if (context.stale)
16
+ return { action: 'withdraw', reason: 'session-changed' };
17
+ if (context.sendBlocked)
18
+ return { action: 'withdraw', reason: 'send-blocked' };
19
+ return { action: 'resume', note: outcome.resumeNote };
20
+ }
@@ -2,12 +2,12 @@ import { __awaiter } from "tslib";
2
2
  import { asOrchestrator } from '../../components/orchestrating-driver/orchestrating-driver';
3
3
  import { flattenSubAgentMessages } from '../../utils/flatten-sub-agent-messages';
4
4
  import { logger } from '../../utils/logger';
5
- import { isSessionNoticeMarker, SESSION_RESTORE_FAILED_LABEL, SESSION_RESTORED_LABEL, } from '../../utils/message-partition';
5
+ import { isSessionNoticeMarker, SESSION_RESTORE_FAILED_LABEL, SESSION_RESTORED_LABEL, SESSION_RESUMED_LABEL, } from '../../utils/message-partition';
6
6
  import { withTimeout } from '../../utils/with-timeout';
7
7
  import { recordMetaEvent } from '../debug-event-log';
8
8
  import { diagnosticsMessageKey } from './diagnostics';
9
9
  import { getDiagnosticsCursors } from './diagnostics-cursors';
10
- import { createPersistedSession, isRestorableSession, keepThroughLastUserMessage, reconcileAgentSnapshots, resolveNonResumableNotice, toLoadSessionPayload, trimToRestorableBoundary, unknownAgentSnapshots, } from './session-snapshot';
10
+ import { buildResumeNote, createPersistedSession, isRestorableSession, keepThroughLastUserMessage, reconcileAgentSnapshots, resolveNonResumableNotice, toLoadSessionPayload, trimToRestorableBoundary, unknownAgentSnapshots, } from './session-snapshot';
11
11
  /**
12
12
  * Backstop for a slow/hung persistence provider on startup restore (GENC-1351
13
13
  * §6). Deliberately long: silently losing a restorable session is a far worse
@@ -518,13 +518,17 @@ export class SessionPersister {
518
518
  * when disabled, absent, or version/host mismatched. An `interrupted` snapshot
519
519
  * (§5.9) restores the conversation but shows a "start again" assistant message
520
520
  * instead of rehydrating the mid-flight flow.
521
+ *
522
+ * Resolves to a `RestoreOutcome` when a session was restored, `undefined` otherwise. The
523
+ * outcome says whether the restored flow should resume (GENC-1662); acting on it is the host's
524
+ * job, after its "Restoring…" gate has lifted, so the resumed turn runs like any other.
521
525
  */
522
526
  restore() {
523
527
  return __awaiter(this, void 0, void 0, function* () {
524
- var _a, _b, _c, _d, _e;
528
+ var _a, _b, _c, _d, _e, _f, _g, _h;
525
529
  const { provider } = this.deps.getPersistence();
526
530
  if (!provider)
527
- return;
531
+ return undefined;
528
532
  const key = this.stateKey;
529
533
  const driver = this.deps.getDriver();
530
534
  // Bound a slow/hung provider so a stuck load can't brick startup. On timeout
@@ -540,25 +544,34 @@ export class SessionPersister {
540
544
  catch (e) {
541
545
  logger.warn('Session restore: load failed or timed out — starting fresh', e);
542
546
  this.showRestoreFailedNotice();
543
- return;
547
+ return undefined;
544
548
  }
545
549
  if (!snapshot)
546
- return; // genuinely nothing stored → fresh session, no notice
550
+ return undefined; // genuinely nothing stored → fresh session, no notice
547
551
  try {
548
552
  if (!isRestorableSession(snapshot, { host: window.location.host })) {
549
553
  // A saved snapshot existed but can't be used (schema bump / cross-host) —
550
554
  // tell the user rather than silently dropping their conversation.
551
555
  logger.debug('Session restore skipped: version/host mismatch', { key });
552
556
  this.showRestoreFailedNotice();
553
- return;
557
+ return undefined;
554
558
  }
555
559
  // Trim a trailing incomplete turn (orphaned tool call / placeholder / pending
556
560
  // interaction) so a snapshot saved mid-loop restores to the last
557
561
  // provider-valid boundary (§5.3 step 2).
558
- const trimmed = trimToRestorableBoundary(snapshot.messages).filter((m) => !isSessionNoticeMarker(m));
562
+ const bounded = trimToRestorableBoundary(snapshot.messages);
563
+ const trimmed = bounded.filter((m) => !isSessionNoticeMarker(m));
564
+ // GENC-1662: re-activate a flow that is still owned, was not frozen as non-resumable, and
565
+ // whose owner this driver knows; resume it as well when the reload interrupted a turn and the
566
+ // owner has not opted out. Otherwise the restore is exactly what it was.
567
+ const savedOwner = snapshot.interrupted ? null : ((_a = snapshot.flowOwnerAgentName) !== null && _a !== void 0 ? _a : null);
568
+ const flowOwner = savedOwner && ((_b = driver === null || driver === void 0 ? void 0 : driver.canRestoreFlowOwner) === null || _b === void 0 ? void 0 : _b.call(driver, savedOwner)) ? savedOwner : null;
569
+ const resumeNote = flowOwner && ((_c = driver === null || driver === void 0 ? void 0 : driver.canResumeRestoredFlow) === null || _c === void 0 ? void 0 : _c.call(driver, flowOwner))
570
+ ? buildResumeNote(trimmed, snapshot.messages.slice(bounded.length))
571
+ : undefined;
559
572
  const ref = this.deps.getStore();
560
573
  if (!ref)
561
- return;
574
+ return undefined;
562
575
  // Diagnostics watermark (§5.8): messages present at restore already live in a
563
576
  // prior load's diagnostics stream — record their identities in the shared
564
577
  // per-session cursors so this load's forward capture never re-appends them
@@ -633,21 +646,22 @@ export class SessionPersister {
633
646
  trailers = [
634
647
  {
635
648
  role: 'system-event',
636
- content: SESSION_RESTORED_LABEL,
649
+ // Says why the next question may not read exactly as it did (GENC-1662).
650
+ content: resumeNote ? SESSION_RESUMED_LABEL : SESSION_RESTORED_LABEL,
637
651
  timestamp: trailerTs,
638
652
  },
639
653
  ];
640
654
  }
641
655
  const messages = [...trimmed, ...trailers];
642
656
  ref.actions.aiAssistant.loadSession(toLoadSessionPayload(Object.assign(Object.assign({}, snapshot), { messages })));
643
- (_a = driver === null || driver === void 0 ? void 0 : driver.loadHistory) === null || _a === void 0 ? void 0 : _a.call(driver, [...messages]);
657
+ (_d = driver === null || driver === void 0 ? void 0 : driver.loadHistory) === null || _d === void 0 ? void 0 : _d.call(driver, [...messages]);
644
658
  // Re-arm the condense policies that had not fired at save time (GENC-1632) — AFTER
645
659
  // `loadHistory`, because the driver keeps only entries whose tool call is in the history it
646
660
  // now holds, which is how an entry for a call `trimToRestorableBoundary` cut away is dropped
647
661
  // rather than resurrected. A snapshot from before the field existed has none, so this is a
648
662
  // no-op and the restore is exactly what it was. Timing against the agent's re-activation is
649
663
  // the driver's problem, not ours: it defers the activation stamp to the next `installAgent`.
650
- const condenseRestore = (_b = driver === null || driver === void 0 ? void 0 : driver.restoreCondensePolicies) === null || _b === void 0 ? void 0 : _b.call(driver, snapshot.condensePolicies);
664
+ const condenseRestore = (_e = driver === null || driver === void 0 ? void 0 : driver.restoreCondensePolicies) === null || _e === void 0 ? void 0 : _e.call(driver, snapshot.condensePolicies);
651
665
  // Observability (GENC-1351, counts refined for GENC-1632 review): record exactly what the
652
666
  // restore primed, so a post-reload debug-log export shows whether a stateful agent's snapshot
653
667
  // actually came back (meta events reset on reload, so this is the only window onto the
@@ -659,7 +673,7 @@ export class SessionPersister {
659
673
  // are left OFF the object (not zeroed) when there is no driver or it predates this method, so
660
674
  // that case can't be misread as "re-armed none" either.
661
675
  this.logMeta('persistence.restored', {
662
- agents: Object.keys((_c = snapshot.agentSnapshots) !== null && _c !== void 0 ? _c : {}),
676
+ agents: Object.keys((_f = snapshot.agentSnapshots) !== null && _f !== void 0 ? _f : {}),
663
677
  condensePolicies: Object.assign({ stored: Array.isArray(snapshot.condensePolicies) ? snapshot.condensePolicies.length : 0 }, condenseRestore),
664
678
  agentSnapshots: snapshot.agentSnapshots,
665
679
  pinnedAgentName: snapshot.pinnedAgentName,
@@ -687,17 +701,43 @@ export class SessionPersister {
687
701
  // driver's routing agrees with the restored (picker-locked) store state.
688
702
  const orchestrator = asOrchestrator(driver);
689
703
  if (orchestrator) {
690
- orchestrator.setPinnedAgent((_d = snapshot.pinnedAgentName) !== null && _d !== void 0 ? _d : null);
691
- orchestrator.setFlowOwner((_e = snapshot.flowOwnerAgentName) !== null && _e !== void 0 ? _e : null);
704
+ orchestrator.setPinnedAgent((_g = snapshot.pinnedAgentName) !== null && _g !== void 0 ? _g : null);
705
+ orchestrator.setFlowOwner((_h = snapshot.flowOwnerAgentName) !== null && _h !== void 0 ? _h : null);
706
+ }
707
+ if (flowOwner) {
708
+ this.logMeta('persistence.resume-planned', {
709
+ flowOwner,
710
+ resume: !!resumeNote,
711
+ dropped: snapshot.messages.length - bounded.length,
712
+ });
692
713
  }
693
714
  logger.debug('Session restored', { key, messages: messages.length });
715
+ return { sessionKey: key, flowOwnerAgentName: flowOwner, resumeNote };
694
716
  }
695
717
  catch (e) {
696
718
  logger.error('Session restore failed:', e);
697
719
  this.showRestoreFailedNotice();
720
+ return undefined;
698
721
  }
699
722
  });
700
723
  }
724
+ /**
725
+ * Take back a resume the restore promised (GENC-1662). `restore()` picks the divider wording when
726
+ * it plans a resume, but the host decides afterwards whether the turn actually runs — a blocked
727
+ * send, a session switched away, a driver error. When it does not, this puts the plain divider
728
+ * back so the transcript does not claim a resume that never happened, and records why.
729
+ */
730
+ withdrawResume(reason) {
731
+ var _a;
732
+ const driver = this.deps.getDriver();
733
+ const history = (_a = driver === null || driver === void 0 ? void 0 : driver.getRawHistory) === null || _a === void 0 ? void 0 : _a.call(driver);
734
+ if (driver &&
735
+ (history === null || history === void 0 ? void 0 : history.some((m) => m.role === 'system-event' && m.content === SESSION_RESUMED_LABEL))) {
736
+ driver.loadHistory(history.map((m) => m.role === 'system-event' && m.content === SESSION_RESUMED_LABEL
737
+ ? Object.assign(Object.assign({}, m), { content: SESSION_RESTORED_LABEL }) : m));
738
+ }
739
+ this.logMeta('persistence.resume-withdrawn', { reason });
740
+ }
701
741
  /**
702
742
  * Surface a non-fatal "couldn't restore" notice after a failed restore (load
703
743
  * timeout/error, or an unusable snapshot) so a vanished conversation isn't a
@@ -141,6 +141,73 @@ export function trimToRestorableBoundary(messages) {
141
141
  }
142
142
  return end === messages.length ? messages : messages.slice(0, end);
143
143
  }
144
+ /** Longest argument rendering the resume note carries per call — a hint, not a replay. */
145
+ const RESUME_NOTE_ARGS_MAX = 300;
146
+ /** The driver's marker for a turn the user stopped — a turn that ended, not one a reload cut. */
147
+ const STOPPED_EVENT = 'Stopped.';
148
+ /**
149
+ * The note for the turn that resumes a flow after a reload (GENC-1662), or `undefined` when the
150
+ * reload interrupted nothing.
151
+ *
152
+ * Interrupted means the model owed a reply when the page went. Three ways that shows up:
153
+ * - `dropped` holds a tool call — `trimToRestorableBoundary` cut an unanswered call (often one
154
+ * waiting on a widget);
155
+ * - the restored transcript ends on a `tool` result — the results landed, the model call that
156
+ * would read them never finished;
157
+ * - it ends on a user message — the user's turn never got its first reply.
158
+ * An assistant reply, or the driver's `Stopped.` event, ends the transcript on a finished turn.
159
+ *
160
+ * The note gives CONTEXT, never an instruction to repeat anything: the model reads where things
161
+ * stand and decides afresh. A cut call may have asked the user something (unanswered), or may
162
+ * have partly run before the reload (a write that landed), so a blind repeat could be wrong.
163
+ *
164
+ * @internal
165
+ */
166
+ export function buildResumeNote(restored, dropped) {
167
+ const lead = '[Session restored] The page was reloaded part-way through your turn.';
168
+ const tail = 'Continue from where the conversation now stands.';
169
+ const calls = dropped.flatMap((m) => { var _a; return (_a = m.toolCalls) !== null && _a !== void 0 ? _a : []; });
170
+ if (calls.length) {
171
+ const listed = calls.map((c) => `\`${c.name}\` ${truncateArgs(c.args)}`).join('; ');
172
+ const plural = calls.length > 1;
173
+ return (`${lead} Your last ${plural ? 'calls' : 'call'} did not complete, so ${plural ? 'they were' : 'it was'} ` +
174
+ `cut from the conversation: ${listed}. If ${plural ? 'one was' : 'it was'} asking the user ` +
175
+ `something, they have not answered. Otherwise it may not have run, or may have partly run — ` +
176
+ `check the current state before repeating it. ${tail}`);
177
+ }
178
+ const last = lastTurnRow(restored);
179
+ if ((last === null || last === void 0 ? void 0 : last.role) === 'tool') {
180
+ return `${lead} Your last tool results came back, but you had not yet responded to them. ${tail}`;
181
+ }
182
+ if ((last === null || last === void 0 ? void 0 : last.role) === 'user' || (last === null || last === void 0 ? void 0 : last.role) === 'synthetic-user') {
183
+ return `${lead} You had not yet replied to the user's last message. ${tail}`;
184
+ }
185
+ return undefined;
186
+ }
187
+ /**
188
+ * The last row that says how the turn stood: skips display-only `system-event` rows (agent switch
189
+ * indicators, restore dividers), except the driver's `Stopped.` event, which is returned because it
190
+ * ends the turn.
191
+ */
192
+ function lastTurnRow(messages) {
193
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
194
+ const m = messages[i];
195
+ if ((m === null || m === void 0 ? void 0 : m.role) === 'system-event' && m.content !== STOPPED_EVENT)
196
+ continue;
197
+ return m;
198
+ }
199
+ return undefined;
200
+ }
201
+ function truncateArgs(args) {
202
+ let text;
203
+ try {
204
+ text = JSON.stringify(args !== null && args !== void 0 ? args : {});
205
+ }
206
+ catch (_a) {
207
+ return '';
208
+ }
209
+ return text.length > RESUME_NOTE_ARGS_MAX ? `${text.slice(0, RESUME_NOTE_ARGS_MAX)}…` : text;
210
+ }
144
211
  /**
145
212
  * Keep the transcript through the **last user message** (inclusive), dropping any
146
213
  * trailing assistant/tool messages after it (GENC-1351 §5.9). Used by the freeze
@@ -10,10 +10,19 @@
10
10
  * @internal
11
11
  */
12
12
  export const SESSION_RESTORED_LABEL = 'Restored previous conversation';
13
+ /**
14
+ * The restore divider when the restore also resumes the flow the reload interrupted (GENC-1662).
15
+ * Deliberately promises a place in the conversation, not an action: the resumed turn is a fresh
16
+ * model decision and may not repeat what was on screen before the reload.
17
+ *
18
+ * @internal
19
+ */
20
+ export const SESSION_RESUMED_LABEL = 'Restored previous conversation. Resuming from where we were interrupted.';
13
21
  /** @internal */
14
22
  export const SESSION_RESTORE_FAILED_LABEL = "Couldn't restore the previous conversation";
15
- /** @internal */
16
- export const isSessionRestoredMarker = (m) => m.role === 'system-event' && m.content === SESSION_RESTORED_LABEL;
23
+ /** Either restore divider — plain or resumed. Both are display-only. @internal */
24
+ export const isSessionRestoredMarker = (m) => m.role === 'system-event' &&
25
+ (m.content === SESSION_RESTORED_LABEL || m.content === SESSION_RESUMED_LABEL);
17
26
  /** @internal */
18
27
  export const isSessionRestoreFailedMarker = (m) => m.role === 'system-event' && m.content === SESSION_RESTORE_FAILED_LABEL;
19
28
  /** Either restore notice — both display-only, stripped before persist. @internal */
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.56.0",
4
+ "version": "15.58.0",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -92,31 +92,31 @@
92
92
  }
93
93
  },
94
94
  "devDependencies": {
95
- "@genesislcap/foundation-comms": "15.56.0",
96
- "@genesislcap/foundation-testing": "15.56.0",
97
- "@genesislcap/genx": "15.56.0",
98
- "@genesislcap/ts-builder": "15.56.0",
99
- "@genesislcap/uvu-playwright-builder": "15.56.0",
100
- "@genesislcap/vite-builder": "15.56.0",
101
- "@genesislcap/webpack-builder": "15.56.0",
95
+ "@genesislcap/foundation-comms": "15.58.0",
96
+ "@genesislcap/foundation-testing": "15.58.0",
97
+ "@genesislcap/genx": "15.58.0",
98
+ "@genesislcap/ts-builder": "15.58.0",
99
+ "@genesislcap/uvu-playwright-builder": "15.58.0",
100
+ "@genesislcap/vite-builder": "15.58.0",
101
+ "@genesislcap/webpack-builder": "15.58.0",
102
102
  "@types/dompurify": "^3.0.5",
103
103
  "@types/marked": "^5.0.2",
104
104
  "esbuild": "0.25.12"
105
105
  },
106
106
  "dependencies": {
107
- "@genesislcap/foundation-ai": "15.56.0",
108
- "@genesislcap/foundation-logger": "15.56.0",
109
- "@genesislcap/foundation-notifications": "15.56.0",
110
- "@genesislcap/foundation-redux": "15.56.0",
111
- "@genesislcap/foundation-ui": "15.56.0",
112
- "@genesislcap/foundation-utils": "15.56.0",
113
- "@genesislcap/rapid-design-system": "15.56.0",
114
- "@genesislcap/web-core": "15.56.0",
107
+ "@genesislcap/foundation-ai": "15.58.0",
108
+ "@genesislcap/foundation-logger": "15.58.0",
109
+ "@genesislcap/foundation-notifications": "15.58.0",
110
+ "@genesislcap/foundation-redux": "15.58.0",
111
+ "@genesislcap/foundation-ui": "15.58.0",
112
+ "@genesislcap/foundation-utils": "15.58.0",
113
+ "@genesislcap/rapid-design-system": "15.58.0",
114
+ "@genesislcap/web-core": "15.58.0",
115
115
  "dompurify": "^3.3.1",
116
116
  "marked": "^17.0.3"
117
117
  },
118
118
  "peerDependencies": {
119
- "@genesislcap/foundation-comms": "^15.56.0"
119
+ "@genesislcap/foundation-comms": "^15.58.0"
120
120
  },
121
121
  "peerDependenciesMeta": {
122
122
  "@genesislcap/foundation-comms": {