@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.
- package/dist/ai-assistant.api.json +438 -8
- package/dist/ai-assistant.d.ts +120 -4
- package/dist/chat-driver.cjs +60 -9
- package/dist/chat-driver.cjs.map +2 -2
- package/dist/chat-driver.mjs +60 -9
- package/dist/chat-driver.mjs.map +2 -2
- package/dist/custom-elements.json +238 -3
- package/dist/dts/components/agent-session/agent-session.d.ts +9 -0
- package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +31 -2
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +18 -2
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +15 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +21 -0
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +6 -0
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +21 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- 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/resume-verdict.d.ts +32 -0
- package/dist/dts/state/persistence/resume-verdict.d.ts.map +1 -0
- package/dist/dts/state/persistence/session-persister.d.ts +34 -1
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-snapshot.d.ts +19 -0
- package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
- package/dist/dts/utils/message-partition.d.ts +9 -1
- package/dist/dts/utils/message-partition.d.ts.map +1 -1
- package/dist/esm/components/agent-session/agent-session.js +14 -0
- package/dist/esm/components/chat-driver/chat-driver.js +12 -5
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +34 -2
- package/dist/esm/config/define-stateful-agent.js +1 -0
- package/dist/esm/main/main.js +101 -22
- package/dist/esm/state/debug-event-log.js +4 -0
- package/dist/esm/state/persistence/resume-verdict.js +20 -0
- package/dist/esm/state/persistence/session-persister.js +55 -15
- package/dist/esm/state/persistence/session-snapshot.js +67 -0
- package/dist/esm/utils/message-partition.js +11 -2
- package/package.json +17 -17
package/dist/esm/main/main.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
(
|
|
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 = (
|
|
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((
|
|
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((
|
|
691
|
-
orchestrator.setFlowOwner((
|
|
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' &&
|
|
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.
|
|
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.
|
|
96
|
-
"@genesislcap/foundation-testing": "15.
|
|
97
|
-
"@genesislcap/genx": "15.
|
|
98
|
-
"@genesislcap/ts-builder": "15.
|
|
99
|
-
"@genesislcap/uvu-playwright-builder": "15.
|
|
100
|
-
"@genesislcap/vite-builder": "15.
|
|
101
|
-
"@genesislcap/webpack-builder": "15.
|
|
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.
|
|
108
|
-
"@genesislcap/foundation-logger": "15.
|
|
109
|
-
"@genesislcap/foundation-notifications": "15.
|
|
110
|
-
"@genesislcap/foundation-redux": "15.
|
|
111
|
-
"@genesislcap/foundation-ui": "15.
|
|
112
|
-
"@genesislcap/foundation-utils": "15.
|
|
113
|
-
"@genesislcap/rapid-design-system": "15.
|
|
114
|
-
"@genesislcap/web-core": "15.
|
|
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.
|
|
119
|
+
"@genesislcap/foundation-comms": "^15.58.0"
|
|
120
120
|
},
|
|
121
121
|
"peerDependenciesMeta": {
|
|
122
122
|
"@genesislcap/foundation-comms": {
|