@genesislcap/ai-assistant 15.19.0 → 15.19.1
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/package.json +17 -18
- package/api-extractor.json +0 -11
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
- package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
- package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
- package/dist/dts/config/validate-providers.test.d.ts +0 -2
- package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
- package/dist/dts/main/blocked-state.test.d.ts +0 -2
- package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
- package/dist/dts/main/budget-meter.test.d.ts +0 -2
- package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
- package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
- package/dist/dts/main/file-attachments.test.d.ts +0 -2
- package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
- package/dist/dts/main/interaction-cost.test.d.ts +0 -2
- package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
- package/dist/dts/main/main.styles.test.d.ts +0 -2
- package/dist/dts/main/main.styles.test.d.ts.map +0 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
- package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
- package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
- package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
- package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
- package/dist/dts/state/debug-event-log.test.d.ts +0 -2
- package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
- package/dist/dts/state/driver-registry.test.d.ts +0 -2
- package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
- package/dist/dts/state/interaction-context.test.d.ts +0 -2
- package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
- package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
- package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
- package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
- package/dist/dts/utils/condense-history.test.d.ts +0 -2
- package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
- package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
- package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
- package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
- package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
- package/dist/dts/utils/history-transform.test.d.ts +0 -2
- package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
- package/dist/dts/utils/message-partition.test.d.ts +0 -2
- package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-costs.test.d.ts +0 -2
- package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
- package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-usage.test.d.ts +0 -2
- package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
- package/dist/dts/utils/usage-rows.test.d.ts +0 -2
- package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
- package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
- package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
- package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
- package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
- package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
- package/dist/esm/config/define-stateful-agent.test.js +0 -53
- package/dist/esm/config/validate-providers.test.js +0 -100
- package/dist/esm/main/blocked-state.test.js +0 -1065
- package/dist/esm/main/budget-meter.test.js +0 -359
- package/dist/esm/main/cost-session-banking.test.js +0 -538
- package/dist/esm/main/file-attachments.test.js +0 -164
- package/dist/esm/main/interaction-cost.test.js +0 -107
- package/dist/esm/main/main.styles.test.js +0 -161
- package/dist/esm/main/persistence-broken-sources.test.js +0 -180
- package/dist/esm/main/popout-interaction-gate.test.js +0 -54
- package/dist/esm/state/ai-assistant-slice.test.js +0 -229
- package/dist/esm/state/debug-event-log.test.js +0 -117
- package/dist/esm/state/driver-registry.test.js +0 -55
- package/dist/esm/state/interaction-context.test.js +0 -78
- package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
- package/dist/esm/state/persistence/diagnostics.test.js +0 -280
- package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
- package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
- package/dist/esm/state/persistence/session-persister.test.js +0 -613
- package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
- package/dist/esm/utils/animation-exclusivity.test.js +0 -52
- package/dist/esm/utils/collect-session-models.test.js +0 -53
- package/dist/esm/utils/condense-history.test.js +0 -547
- package/dist/esm/utils/cost-session-history.test.js +0 -174
- package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
- package/dist/esm/utils/history-transform.test.js +0 -158
- package/dist/esm/utils/message-partition.test.js +0 -90
- package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
- package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
- package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
- package/dist/esm/utils/sum-costs.test.js +0 -114
- package/dist/esm/utils/sum-tokens.test.js +0 -51
- package/dist/esm/utils/sum-usage.test.js +0 -164
- package/dist/esm/utils/usage-rows.test.js +0 -189
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/license.txt +0 -46
- package/sandbox/tsconfig.json +0 -8
- package/src/channel/ai-activity-bus.ts +0 -209
- package/src/channel/ai-activity-channel.ts +0 -146
- package/src/chat-driver-node.ts +0 -159
- package/src/components/activity-halo/activity-halo.ts +0 -106
- package/src/components/agent-picker/agent-picker.constants.ts +0 -2
- package/src/components/agent-picker/agent-picker.styles.ts +0 -24
- package/src/components/agent-picker/agent-picker.template.ts +0 -54
- package/src/components/agent-picker/agent-picker.ts +0 -99
- package/src/components/agent-picker/index.ts +0 -1
- package/src/components/ai-driver/ai-driver.ts +0 -166
- package/src/components/ai-driver/index.ts +0 -1
- package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
- package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
- package/src/components/chat-bubble/chat-bubble.ts +0 -436
- package/src/components/chat-bubble/index.ts +0 -1
- package/src/components/chat-driver/align-event-globals.ts +0 -23
- package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
- package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
- package/src/components/chat-driver/chat-driver.test.ts +0 -3679
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
- package/src/components/chat-driver/chat-driver.ts +0 -3925
- package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
- package/src/components/chat-driver/index.ts +0 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
- package/src/components/chat-interaction-wrapper/index.ts +0 -3
- package/src/components/chat-markdown/chat-markdown.ts +0 -89
- package/src/components/chat-markdown/index.ts +0 -1
- package/src/components/flowing-waves-indicator.ts +0 -286
- package/src/components/halo-overlay.ts +0 -184
- package/src/components/orchestrating-driver/index.ts +0 -1
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
- package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
- package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
- package/src/components/plasma-orb-indicator.ts +0 -281
- package/src/components/popout-manager/index.ts +0 -1
- package/src/components/popout-manager/popout-manager.ts +0 -212
- package/src/components/settings-modal/index.ts +0 -2
- package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
- package/src/components/settings-modal/settings-modal.styles.ts +0 -853
- package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
- package/src/components/settings-modal/settings-modal.template.ts +0 -943
- package/src/components/waves-indicator.ts +0 -241
- package/src/config/config.ts +0 -608
- package/src/config/define-stateful-agent.test.ts +0 -64
- package/src/config/define-stateful-agent.ts +0 -642
- package/src/config/fallback-agents.ts +0 -29
- package/src/config/index.ts +0 -2
- package/src/config/validate-providers.test.ts +0 -148
- package/src/config/validate-providers.ts +0 -58
- package/src/index.ts +0 -29
- package/src/main/blocked-state.test.ts +0 -1444
- package/src/main/budget-meter.test.ts +0 -477
- package/src/main/cost-session-banking.test.ts +0 -679
- package/src/main/file-attachments.test.ts +0 -215
- package/src/main/index.ts +0 -3
- package/src/main/interaction-cost.test.ts +0 -140
- package/src/main/main.styles.test.ts +0 -216
- package/src/main/main.styles.ts +0 -995
- package/src/main/main.template.ts +0 -1119
- package/src/main/main.ts +0 -5432
- package/src/main/main.types.ts +0 -225
- package/src/main/persistence-broken-sources.test.ts +0 -219
- package/src/main/popout-interaction-gate.test.ts +0 -87
- package/src/provider/ai-provider-switcher.ts +0 -40
- package/src/provider/assistant-app-settings.ts +0 -84
- package/src/state/ai-assistant-slice.test.ts +0 -336
- package/src/state/ai-assistant-slice.ts +0 -604
- package/src/state/debug-event-log.test.ts +0 -154
- package/src/state/debug-event-log.ts +0 -358
- package/src/state/driver-registry.test.ts +0 -70
- package/src/state/driver-registry.ts +0 -79
- package/src/state/interaction-context.test.ts +0 -90
- package/src/state/interaction-context.ts +0 -101
- package/src/state/persistence/build-timeline-entries.ts +0 -66
- package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
- package/src/state/persistence/diagnostics-cursors.ts +0 -80
- package/src/state/persistence/diagnostics.test.ts +0 -346
- package/src/state/persistence/diagnostics.ts +0 -220
- package/src/state/persistence/index.ts +0 -10
- package/src/state/persistence/persister-registry.ts +0 -55
- package/src/state/persistence/session-persistence-provider.test.ts +0 -136
- package/src/state/persistence/session-persistence-provider.ts +0 -226
- package/src/state/persistence/session-persistence.integration.test.ts +0 -141
- package/src/state/persistence/session-persister.test.ts +0 -787
- package/src/state/persistence/session-persister.ts +0 -917
- package/src/state/persistence/session-snapshot.test.ts +0 -352
- package/src/state/persistence/session-snapshot.ts +0 -344
- package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
- package/src/state/session-store.ts +0 -56
- package/src/styles/ai-colours.ts +0 -30
- package/src/styles/menu.ts +0 -187
- package/src/styles/settings-section.ts +0 -40
- package/src/suggestions/chat-suggestions.ts +0 -186
- package/src/tags/index.ts +0 -1
- package/src/types/ai-chat-widget.ts +0 -67
- package/src/types/interaction-context.ts +0 -82
- package/src/utils/animated-panel-toggle.ts +0 -62
- package/src/utils/animation-exclusivity.test.ts +0 -72
- package/src/utils/animation-exclusivity.ts +0 -40
- package/src/utils/banked-usage-baselines.ts +0 -90
- package/src/utils/collect-session-models.test.ts +0 -82
- package/src/utils/collect-session-models.ts +0 -65
- package/src/utils/condense-history.test.ts +0 -746
- package/src/utils/condense-history.ts +0 -448
- package/src/utils/cost-session-history.test.ts +0 -226
- package/src/utils/cost-session-history.ts +0 -218
- package/src/utils/derive-cost-session-title.test.ts +0 -63
- package/src/utils/derive-cost-session-title.ts +0 -76
- package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
- package/src/utils/flatten-sub-agent-messages.ts +0 -88
- package/src/utils/format-usd.ts +0 -26
- package/src/utils/history-transform.test.ts +0 -228
- package/src/utils/history-transform.ts +0 -163
- package/src/utils/index.ts +0 -3
- package/src/utils/logger.ts +0 -3
- package/src/utils/message-partition.test.ts +0 -129
- package/src/utils/message-partition.ts +0 -115
- package/src/utils/resolve-cost-history-config.test.ts +0 -38
- package/src/utils/resolve-cost-history-config.ts +0 -38
- package/src/utils/resolve-preference-baseline.test.ts +0 -84
- package/src/utils/resolve-preference-baseline.ts +0 -80
- package/src/utils/strip-agent-handlers.test.ts +0 -99
- package/src/utils/strip-agent-handlers.ts +0 -53
- package/src/utils/sum-costs.test.ts +0 -136
- package/src/utils/sum-costs.ts +0 -18
- package/src/utils/sum-tokens.test.ts +0 -93
- package/src/utils/sum-tokens.ts +0 -68
- package/src/utils/sum-usage.test.ts +0 -191
- package/src/utils/sum-usage.ts +0 -208
- package/src/utils/tool-fold.ts +0 -181
- package/src/utils/usage-rows.test.ts +0 -237
- package/src/utils/usage-rows.ts +0 -187
- package/src/utils/with-timeout.ts +0 -24
- package/tsconfig.json +0 -9
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import {
|
|
4
|
-
PERSISTED_SESSION_VERSION,
|
|
5
|
-
createPersistedSession,
|
|
6
|
-
isRestorableSession,
|
|
7
|
-
type PersistedSession,
|
|
8
|
-
reconcileAgentSnapshots,
|
|
9
|
-
resolveNonResumableNotice,
|
|
10
|
-
toLoadSessionPayload,
|
|
11
|
-
keepThroughLastUserMessage,
|
|
12
|
-
trimToRestorableBoundary,
|
|
13
|
-
unknownAgentSnapshots,
|
|
14
|
-
} from './session-snapshot';
|
|
15
|
-
|
|
16
|
-
const asMsg = (o: Record<string, unknown>): ChatMessage => o as unknown as ChatMessage;
|
|
17
|
-
|
|
18
|
-
// GENC-1351 §5.1: the PersistedSession schema + its pure (de)serialize helpers.
|
|
19
|
-
// These have no DOM/storage deps, so they run straight in Node.
|
|
20
|
-
|
|
21
|
-
const Suite = createLogicSuite('persisted-session-schema');
|
|
22
|
-
|
|
23
|
-
const base = () =>
|
|
24
|
-
createPersistedSession({
|
|
25
|
-
sessionKey: 'k',
|
|
26
|
-
host: 'app.example',
|
|
27
|
-
messages: [],
|
|
28
|
-
pinnedAgentName: 'A',
|
|
29
|
-
flowOwnerAgentName: null,
|
|
30
|
-
sessionCostUsd: 2,
|
|
31
|
-
contextTokens: 10,
|
|
32
|
-
contextLimit: 100,
|
|
33
|
-
activeModel: 'm',
|
|
34
|
-
activeProviderName: 'p',
|
|
35
|
-
activeAgentName: 'A',
|
|
36
|
-
agentSnapshots: { A: { machine: { state: 'x', context: {} } } },
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
Suite('createPersistedSession stamps version + savedAt and keeps agentSnapshots', () => {
|
|
40
|
-
const s = base();
|
|
41
|
-
assert.is(s.version, PERSISTED_SESSION_VERSION);
|
|
42
|
-
assert.is(typeof s.savedAt, 'string');
|
|
43
|
-
assert.ok(s.savedAt.length > 0);
|
|
44
|
-
assert.equal(s.agentSnapshots, { A: { machine: { state: 'x', context: {} } } });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// GENC-1464 — the blocked latch is deliberately NOT persisted. The snapshot is
|
|
48
|
-
// per-stateKey and long-lived, so a persisted latch would survive an out-of-band
|
|
49
|
-
// budget raise with no in-element way to clear it: the user would return to a
|
|
50
|
-
// permanently dead composer against a budget that has headroom. Re-deriving the
|
|
51
|
-
// state (host pre-flight on mount, or the next 402) is strictly safer than
|
|
52
|
-
// remembering it. This test exists so a future "helpful" addition trips here and
|
|
53
|
-
// has to read that reasoning first — see `FoundationAiAssistant.blocked`'s
|
|
54
|
-
// Lifetime paragraph and docs/migration-GENC-1464.md §Scope.
|
|
55
|
-
Suite('the blocked latch is deliberately not persisted', () => {
|
|
56
|
-
const s = base() as unknown as Record<string, unknown>;
|
|
57
|
-
assert.not.ok('blocked' in s, 'blocked must not enter the persisted snapshot');
|
|
58
|
-
assert.not.ok('blockedReason' in s, 'blockedReason must not enter the persisted snapshot');
|
|
59
|
-
// The per-vendor latches inherit the same lifetime, so they inherit the same
|
|
60
|
-
// rule — a persisted per-vendor wall is exactly as stale as a persisted global
|
|
61
|
-
// one, just harder to notice because only one vendor looks broken.
|
|
62
|
-
assert.not.ok('blockedVendors' in s, 'blockedVendors must not enter the persisted snapshot');
|
|
63
|
-
assert.not.ok(
|
|
64
|
-
'blockedVendorReasons' in s,
|
|
65
|
-
'blockedVendorReasons must not enter the persisted snapshot',
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
Suite('createPersistedSession defaults agentSnapshots to {}', () => {
|
|
70
|
-
const s = createPersistedSession({
|
|
71
|
-
sessionKey: 'k',
|
|
72
|
-
host: 'h',
|
|
73
|
-
messages: [],
|
|
74
|
-
pinnedAgentName: null,
|
|
75
|
-
flowOwnerAgentName: null,
|
|
76
|
-
sessionCostUsd: 0,
|
|
77
|
-
});
|
|
78
|
-
assert.equal(s.agentSnapshots, {});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
Suite('isRestorableSession accepts a matching version + host', () => {
|
|
82
|
-
assert.ok(isRestorableSession(base(), { host: 'app.example' }));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
Suite('isRestorableSession rejects version mismatch, cross-host, and malformed input', () => {
|
|
86
|
-
assert.ok(!isRestorableSession({ ...base(), version: 999 }, { host: 'app.example' }));
|
|
87
|
-
assert.ok(!isRestorableSession(base(), { host: 'other.example' }));
|
|
88
|
-
assert.ok(!isRestorableSession(null, { host: 'h' }));
|
|
89
|
-
assert.ok(
|
|
90
|
-
!isRestorableSession({ version: PERSISTED_SESSION_VERSION, host: 'h' }, { host: 'h' }),
|
|
91
|
-
'missing messages array → not restorable',
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
Suite('toLoadSessionPayload projects only the redux-restorable fields', () => {
|
|
96
|
-
const p = toLoadSessionPayload(base());
|
|
97
|
-
assert.is(p.pinnedAgentName, 'A');
|
|
98
|
-
assert.is(p.flowOwnerAgentName, null);
|
|
99
|
-
// Cost is deliberately NOT projected — the slice re-derives it (and the token
|
|
100
|
-
// buckets) from `messages`, so a stored counter would be a second, divergeable
|
|
101
|
-
// source of truth.
|
|
102
|
-
assert.is((p as { sessionCostUsd?: number }).sessionCostUsd, undefined);
|
|
103
|
-
assert.is(p.contextTokens, 10);
|
|
104
|
-
assert.is(p.contextLimit, 100);
|
|
105
|
-
assert.is(p.activeModel, 'm');
|
|
106
|
-
assert.is(p.activeProviderName, 'p');
|
|
107
|
-
assert.equal(p.messages, [] as ChatMessage[]);
|
|
108
|
-
// agentSnapshots + activeAgentName are applied by the host, not via the slice.
|
|
109
|
-
assert.ok(!('agentSnapshots' in p));
|
|
110
|
-
assert.ok(!('activeAgentName' in p));
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
Suite('trimToRestorableBoundary returns a clean transcript unchanged (same ref)', () => {
|
|
114
|
-
const clean = [
|
|
115
|
-
asMsg({ role: 'user', content: 'hi' }),
|
|
116
|
-
asMsg({ role: 'assistant', content: 'hello' }),
|
|
117
|
-
];
|
|
118
|
-
assert.is(trimToRestorableBoundary(clean), clean);
|
|
119
|
-
assert.is(trimToRestorableBoundary([]).length, 0);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
Suite(
|
|
123
|
-
'trimToRestorableBoundary drops a trailing unresolved interaction, keeps a resolved one',
|
|
124
|
-
() => {
|
|
125
|
-
const user = asMsg({ role: 'user', content: 'hi' });
|
|
126
|
-
const pending = asMsg({
|
|
127
|
-
role: 'assistant',
|
|
128
|
-
content: '',
|
|
129
|
-
interaction: { interactionId: '2', componentName: 'x', data: {} },
|
|
130
|
-
});
|
|
131
|
-
const resolved = asMsg({
|
|
132
|
-
role: 'assistant',
|
|
133
|
-
content: '',
|
|
134
|
-
interaction: {
|
|
135
|
-
interactionId: '1',
|
|
136
|
-
componentName: 'x',
|
|
137
|
-
data: {},
|
|
138
|
-
resolved: { status: 'approved' },
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
assert.is(trimToRestorableBoundary([user, pending]).length, 1);
|
|
142
|
-
assert.is(trimToRestorableBoundary([user, resolved]).length, 2);
|
|
143
|
-
},
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
Suite('trimToRestorableBoundary drops dangling + partially-answered tool calls', () => {
|
|
147
|
-
const user = asMsg({ role: 'user', content: 'do x' });
|
|
148
|
-
const callAB = asMsg({
|
|
149
|
-
role: 'assistant',
|
|
150
|
-
content: '',
|
|
151
|
-
toolCalls: [
|
|
152
|
-
{ id: 'a', name: 'f', args: {} },
|
|
153
|
-
{ id: 'b', name: 'g', args: {} },
|
|
154
|
-
],
|
|
155
|
-
});
|
|
156
|
-
const resultA = asMsg({
|
|
157
|
-
role: 'tool',
|
|
158
|
-
content: '',
|
|
159
|
-
toolResult: { toolCallId: 'a', content: 'ra' },
|
|
160
|
-
});
|
|
161
|
-
const resultB = asMsg({
|
|
162
|
-
role: 'tool',
|
|
163
|
-
content: '',
|
|
164
|
-
toolResult: { toolCallId: 'b', content: 'rb' },
|
|
165
|
-
});
|
|
166
|
-
// No results → drop the whole round back to [user].
|
|
167
|
-
assert.is(trimToRestorableBoundary([user, callAB]).length, 1);
|
|
168
|
-
// Only A answered → B orphaned → drop the round back to [user].
|
|
169
|
-
assert.is(trimToRestorableBoundary([user, callAB, resultA]).length, 1);
|
|
170
|
-
// Both answered → kept (ends on a balanced tool result).
|
|
171
|
-
assert.is(trimToRestorableBoundary([user, callAB, resultA, resultB]).length, 4);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
Suite('trimToRestorableBoundary drops a trailing empty assistant placeholder', () => {
|
|
175
|
-
const user = asMsg({ role: 'user', content: 'hi' });
|
|
176
|
-
const placeholder = asMsg({ role: 'assistant', content: '' });
|
|
177
|
-
assert.is(trimToRestorableBoundary([user, placeholder]).length, 1);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// GENC-1351: orphaned agent snapshots (dynamic agent arrays — e.g. genesis-create).
|
|
181
|
-
Suite('unknownAgentSnapshots flags saved agents missing from the configured set', () => {
|
|
182
|
-
const snap = {
|
|
183
|
-
agentSnapshots: { 'Guided Booking': {}, Bootstrap: {} },
|
|
184
|
-
} as unknown as PersistedSession;
|
|
185
|
-
// Bootstrap gone from the current set → flagged; Guided Booking is known.
|
|
186
|
-
assert.equal(unknownAgentSnapshots(snap, ['Guided Booking', 'Trades']), ['Bootstrap']);
|
|
187
|
-
// All known → none flagged.
|
|
188
|
-
assert.equal(unknownAgentSnapshots(snap, ['Guided Booking', 'Bootstrap']), []);
|
|
189
|
-
// Empty known set → can't classify yet → none (avoids false positives mid-init).
|
|
190
|
-
assert.equal(unknownAgentSnapshots(snap, []), []);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// GENC-1351 §6 — `reconcileAgentSnapshots`: the save-time guard against a
|
|
194
|
-
// post-restore autosave wiping a mid-flow machine. The bug (regression #1): a
|
|
195
|
-
// restore reads a good snapshot, then the restore's own history change fires an
|
|
196
|
-
// autosave BEFORE the agent re-activates, so the live capture is empty and, left
|
|
197
|
-
// unguarded, `{}` overwrites the machine → the flow resets on the next reload.
|
|
198
|
-
const MACHINE = { machine: { value: 'pickInstrument', context: { counterparty: { id: 'c1' } } } };
|
|
199
|
-
|
|
200
|
-
Suite(
|
|
201
|
-
'reconcileAgentSnapshots: REGRESSION — carries the flow-owner forward when live capture is empty',
|
|
202
|
-
() => {
|
|
203
|
-
// Post-restore, pre-activation: getActiveAgent() is undefined → live is {}, but
|
|
204
|
-
// a flow is owned and the good image sits in the cache (seeded on restore).
|
|
205
|
-
const { snapshots, cache } = reconcileAgentSnapshots({}, 'Guided Booking', {
|
|
206
|
-
'Guided Booking': MACHINE,
|
|
207
|
-
});
|
|
208
|
-
assert.equal(
|
|
209
|
-
snapshots['Guided Booking'],
|
|
210
|
-
MACHINE,
|
|
211
|
-
'the flow-owner machine is preserved, NOT wiped to {}',
|
|
212
|
-
);
|
|
213
|
-
assert.equal(cache['Guided Booking'], MACHINE, 'cache keeps the good image');
|
|
214
|
-
},
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
Suite('reconcileAgentSnapshots: a live capture wins and advances the cache', () => {
|
|
218
|
-
const FRESH = { machine: { value: 'enterDetails', context: {} } };
|
|
219
|
-
const { snapshots, cache } = reconcileAgentSnapshots(
|
|
220
|
-
{ 'Guided Booking': FRESH },
|
|
221
|
-
'Guided Booking',
|
|
222
|
-
{
|
|
223
|
-
'Guided Booking': MACHINE, // stale cached value
|
|
224
|
-
},
|
|
225
|
-
);
|
|
226
|
-
assert.equal(snapshots['Guided Booking'], FRESH, 'live image is used (never the stale cache)');
|
|
227
|
-
assert.equal(cache['Guided Booking'], FRESH, 'cache advances to the freshly-captured image');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
Suite('reconcileAgentSnapshots: no flow owner → empty stays empty (no stale carry-forward)', () => {
|
|
231
|
-
// Idle / stateless save: nothing to carry, even if the cache holds an old image.
|
|
232
|
-
const { snapshots, cache } = reconcileAgentSnapshots({}, null, { 'Guided Booking': MACHINE });
|
|
233
|
-
assert.equal(snapshots, {}, 'nothing carried forward without a flow owner');
|
|
234
|
-
assert.equal(cache['Guided Booking'], MACHINE, 'an empty save never erases the cache');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
Suite('reconcileAgentSnapshots: flow owner set but nothing cached → cannot fabricate state', () => {
|
|
238
|
-
const { snapshots } = reconcileAgentSnapshots({}, 'Guided Booking', {});
|
|
239
|
-
assert.is(
|
|
240
|
-
snapshots['Guided Booking'],
|
|
241
|
-
undefined,
|
|
242
|
-
'no image to carry → left absent, not invented',
|
|
243
|
-
);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
Suite(
|
|
247
|
-
'reconcileAgentSnapshots: carries the flow owner while keeping a co-captured other agent',
|
|
248
|
-
() => {
|
|
249
|
-
const OTHER = { machine: { value: 'x' } };
|
|
250
|
-
const { snapshots } = reconcileAgentSnapshots({ Other: OTHER }, 'Guided Booking', {
|
|
251
|
-
'Guided Booking': MACHINE,
|
|
252
|
-
});
|
|
253
|
-
assert.equal(snapshots.Other, OTHER, 'the live non-owner image is kept');
|
|
254
|
-
assert.equal(snapshots['Guided Booking'], MACHINE, 'the missing flow-owner is carried forward');
|
|
255
|
-
},
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
Suite('reconcileAgentSnapshots: does not mutate its inputs', () => {
|
|
259
|
-
const live = {};
|
|
260
|
-
const cacheIn = { 'Guided Booking': MACHINE };
|
|
261
|
-
reconcileAgentSnapshots(live, 'Guided Booking', cacheIn);
|
|
262
|
-
assert.equal(live, {}, 'live input untouched');
|
|
263
|
-
assert.equal(Object.keys(cacheIn), ['Guided Booking'], 'cache input untouched');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// GENC-1351 §5.9: the pure notice-resolution helper (message + pre-fill prompt).
|
|
267
|
-
// The freeze/marker mechanics live on the element (integration), not here.
|
|
268
|
-
|
|
269
|
-
Suite('resolveNonResumableNotice: custom message wins, blank falls back to default', () => {
|
|
270
|
-
const msgs = [
|
|
271
|
-
asMsg({ role: 'user', content: 'first' }),
|
|
272
|
-
asMsg({ role: 'assistant', content: 'x' }),
|
|
273
|
-
];
|
|
274
|
-
assert.is(
|
|
275
|
-
resolveNonResumableNotice({
|
|
276
|
-
notResumableMessage: 'custom',
|
|
277
|
-
messages: msgs,
|
|
278
|
-
defaultMessage: 'D',
|
|
279
|
-
}).resumeMessage,
|
|
280
|
-
'custom',
|
|
281
|
-
);
|
|
282
|
-
assert.is(
|
|
283
|
-
resolveNonResumableNotice({ notResumableMessage: ' ', messages: msgs, defaultMessage: 'D' })
|
|
284
|
-
.resumeMessage,
|
|
285
|
-
'D',
|
|
286
|
-
'a blank custom message falls back to the default',
|
|
287
|
-
);
|
|
288
|
-
assert.is(
|
|
289
|
-
resolveNonResumableNotice({ messages: msgs, defaultMessage: 'D' }).resumeMessage,
|
|
290
|
-
'D',
|
|
291
|
-
'no custom message → default',
|
|
292
|
-
);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
Suite(
|
|
296
|
-
'resolveNonResumableNotice: prompt prefers the captured flow prompt, else last user msg',
|
|
297
|
-
() => {
|
|
298
|
-
const msgs = [
|
|
299
|
-
asMsg({ role: 'user', content: 'earlier' }),
|
|
300
|
-
asMsg({ role: 'assistant', content: 'x' }),
|
|
301
|
-
asMsg({ role: 'user', content: 'latest user' }),
|
|
302
|
-
];
|
|
303
|
-
assert.is(
|
|
304
|
-
resolveNonResumableNotice({
|
|
305
|
-
flowActivationPrompt: 'the flow prompt',
|
|
306
|
-
messages: msgs,
|
|
307
|
-
defaultMessage: 'D',
|
|
308
|
-
}).resumePrompt,
|
|
309
|
-
'the flow prompt',
|
|
310
|
-
'captured flow-activating prompt takes precedence',
|
|
311
|
-
);
|
|
312
|
-
assert.is(
|
|
313
|
-
resolveNonResumableNotice({ flowActivationPrompt: null, messages: msgs, defaultMessage: 'D' })
|
|
314
|
-
.resumePrompt,
|
|
315
|
-
'latest user',
|
|
316
|
-
'no flow prompt → falls back to the last user message',
|
|
317
|
-
);
|
|
318
|
-
assert.is(
|
|
319
|
-
resolveNonResumableNotice({ messages: [], defaultMessage: 'D' }).resumePrompt,
|
|
320
|
-
undefined,
|
|
321
|
-
'no flow prompt and no user message → undefined',
|
|
322
|
-
);
|
|
323
|
-
},
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
// GENC-1351 §5.9: the freeze fallback keeps the transcript through the last user
|
|
327
|
-
// message, dropping the agent's in-flight turns (issue #3).
|
|
328
|
-
Suite(
|
|
329
|
-
'keepThroughLastUserMessage drops trailing agent turns, keeps through the last user msg',
|
|
330
|
-
() => {
|
|
331
|
-
const u1 = asMsg({ role: 'user', content: 'book a flight' });
|
|
332
|
-
const a1 = asMsg({ role: 'assistant', content: 'ok' });
|
|
333
|
-
const u2 = asMsg({ role: 'user', content: 'show fills for T1' });
|
|
334
|
-
const plan = asMsg({
|
|
335
|
-
role: 'assistant',
|
|
336
|
-
content: '',
|
|
337
|
-
toolCalls: [{ id: 'a', name: 'f', args: {} }],
|
|
338
|
-
});
|
|
339
|
-
const res = asMsg({ role: 'tool', content: '' });
|
|
340
|
-
// Trailing agent turns after the last user message → dropped.
|
|
341
|
-
assert.equal(keepThroughLastUserMessage([u1, a1, u2, plan, res]), [u1, a1, u2]);
|
|
342
|
-
// Already ends on a user message → unchanged (same ref).
|
|
343
|
-
const endsUser = [u1, a1, u2];
|
|
344
|
-
assert.is(keepThroughLastUserMessage(endsUser), endsUser);
|
|
345
|
-
// No user message at all → unchanged (nothing to trim to).
|
|
346
|
-
const noUser = [a1, res];
|
|
347
|
-
assert.is(keepThroughLastUserMessage(noUser), noUser);
|
|
348
|
-
assert.equal(keepThroughLastUserMessage([]).length, 0);
|
|
349
|
-
},
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
Suite.run();
|
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
import type { LoadSessionPayload } from '../ai-assistant-slice';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Schema version of the persisted session shape. Bump on an incompatible
|
|
6
|
-
* change; a snapshot whose `version` doesn't match is discarded on load rather
|
|
7
|
-
* than risk feeding a stale shape into restore.
|
|
8
|
-
*
|
|
9
|
-
* @public
|
|
10
|
-
*/
|
|
11
|
-
export const PERSISTED_SESSION_VERSION = 1;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A durable, restorable snapshot of one chat session — the *restorable core* of
|
|
15
|
-
* the debug log (GENC-1351 §5.1/D1), captured losslessly and independently of
|
|
16
|
-
* the diagnostic timeline. Written by a `SessionPersistenceProvider` and read
|
|
17
|
-
* back on page-reload restore.
|
|
18
|
-
*
|
|
19
|
-
* @public
|
|
20
|
-
*/
|
|
21
|
-
export interface PersistedSession {
|
|
22
|
-
/** Schema version — see {@link PERSISTED_SESSION_VERSION}. */
|
|
23
|
-
version: number;
|
|
24
|
-
/** ISO timestamp the snapshot was written. */
|
|
25
|
-
savedAt: string;
|
|
26
|
-
/** `window.location.host` at save time — restore refuses a cross-host snapshot. */
|
|
27
|
-
host: string;
|
|
28
|
-
/** The session this snapshot belongs to (the assistant's state key). */
|
|
29
|
-
sessionKey: string;
|
|
30
|
-
/** Full conversation transcript (`driver.getRawHistory()`) — the source of truth. */
|
|
31
|
-
messages: ChatMessage[];
|
|
32
|
-
/** Name of the agent active at save time, if any. */
|
|
33
|
-
activeAgentName?: string;
|
|
34
|
-
/** User's pinned agent (sticky picker choice); `null` = Auto. */
|
|
35
|
-
pinnedAgentName: string | null;
|
|
36
|
-
/** Agent that owned a guided flow at save time; `null` = none. */
|
|
37
|
-
flowOwnerAgentName: string | null;
|
|
38
|
-
/** Aggregated USD cost across the session. */
|
|
39
|
-
sessionCostUsd: number;
|
|
40
|
-
/** Provider-reported context-window usage on the last turn. */
|
|
41
|
-
contextTokens?: number;
|
|
42
|
-
/** Context-window limit for the active model. */
|
|
43
|
-
contextLimit?: number;
|
|
44
|
-
/** Active model id (e.g. `claude-sonnet-4-6`). */
|
|
45
|
-
activeModel?: string;
|
|
46
|
-
/** Name of the provider used on the last turn. */
|
|
47
|
-
activeProviderName?: string;
|
|
48
|
-
/**
|
|
49
|
-
* Per-stateful-agent serialized state images (each from an agent's
|
|
50
|
-
* `serialize()`), keyed by agent name. Handed back to the agent's
|
|
51
|
-
* `init({ restored })` on restore. Typically holds at most the active
|
|
52
|
-
* flow-owner — other stateful agents are disposed when switched away. The
|
|
53
|
-
* value shape is the agent's own, hence `unknown`.
|
|
54
|
-
*/
|
|
55
|
-
agentSnapshots: Record<string, unknown>;
|
|
56
|
-
/**
|
|
57
|
-
* Interrupted-flow marker (GENC-1351 §5.9). Present when the session was saved
|
|
58
|
-
* while an agent declared its current state non-resumable (`resumable === false`).
|
|
59
|
-
* The **conversation is still persisted and restored in full** — only the running
|
|
60
|
-
* agent's flow-state is dropped (`agentSnapshots` empty, `flowOwnerAgentName`
|
|
61
|
-
* null), so restore does not rehydrate the mid-flight flow. On restore the
|
|
62
|
-
* assistant appends `message` as a normal assistant bubble and pre-fills the
|
|
63
|
-
* composer with `prompt` so the user can re-run it.
|
|
64
|
-
*/
|
|
65
|
-
interrupted?: {
|
|
66
|
-
/** Shown as an assistant message on return. */
|
|
67
|
-
message: string;
|
|
68
|
-
/** The prompt that started the flow, pre-filled into the composer on return. */
|
|
69
|
-
prompt?: string;
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Everything needed to assemble a {@link PersistedSession} except the stamped
|
|
75
|
-
* metadata (`version`, `savedAt`), which `createPersistedSession` fills.
|
|
76
|
-
*
|
|
77
|
-
* @internal
|
|
78
|
-
*/
|
|
79
|
-
export interface PersistedSessionInput {
|
|
80
|
-
sessionKey: string;
|
|
81
|
-
host: string;
|
|
82
|
-
messages: ChatMessage[];
|
|
83
|
-
activeAgentName?: string;
|
|
84
|
-
pinnedAgentName: string | null;
|
|
85
|
-
flowOwnerAgentName: string | null;
|
|
86
|
-
sessionCostUsd: number;
|
|
87
|
-
contextTokens?: number;
|
|
88
|
-
contextLimit?: number;
|
|
89
|
-
activeModel?: string;
|
|
90
|
-
activeProviderName?: string;
|
|
91
|
-
agentSnapshots?: Record<string, unknown>;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Assemble a versioned {@link PersistedSession}, stamping `version` and
|
|
96
|
-
* `savedAt`. Pure but for the timestamp (reads the host clock at call time).
|
|
97
|
-
*
|
|
98
|
-
* @internal
|
|
99
|
-
*/
|
|
100
|
-
export function createPersistedSession(input: PersistedSessionInput): PersistedSession {
|
|
101
|
-
return {
|
|
102
|
-
version: PERSISTED_SESSION_VERSION,
|
|
103
|
-
savedAt: new Date().toISOString(),
|
|
104
|
-
host: input.host,
|
|
105
|
-
sessionKey: input.sessionKey,
|
|
106
|
-
messages: input.messages,
|
|
107
|
-
activeAgentName: input.activeAgentName,
|
|
108
|
-
pinnedAgentName: input.pinnedAgentName,
|
|
109
|
-
flowOwnerAgentName: input.flowOwnerAgentName,
|
|
110
|
-
sessionCostUsd: input.sessionCostUsd,
|
|
111
|
-
contextTokens: input.contextTokens,
|
|
112
|
-
contextLimit: input.contextLimit,
|
|
113
|
-
activeModel: input.activeModel,
|
|
114
|
-
activeProviderName: input.activeProviderName,
|
|
115
|
-
agentSnapshots: input.agentSnapshots ?? {},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Resolve the notice text + pre-fill prompt for an interrupted (non-resumable) flow
|
|
121
|
-
* (GENC-1351 §5.9). Pure — the decision half of the persist gate, split out so it's
|
|
122
|
-
* unit-testable independent of the save path:
|
|
123
|
-
* - `resumeMessage`: the agent's `notResumableMessage` when non-blank, else the
|
|
124
|
-
* passed default.
|
|
125
|
-
* - `resumePrompt`: the captured flow-activating prompt, falling back to the last
|
|
126
|
-
* user message in the transcript (covers a stateless agent that declines to
|
|
127
|
-
* persist without owning a flow); `undefined` when neither exists.
|
|
128
|
-
*
|
|
129
|
-
* @internal
|
|
130
|
-
*/
|
|
131
|
-
export function resolveNonResumableNotice(input: {
|
|
132
|
-
notResumableMessage?: string;
|
|
133
|
-
flowActivationPrompt?: string | null;
|
|
134
|
-
messages: ReadonlyArray<ChatMessage>;
|
|
135
|
-
defaultMessage: string;
|
|
136
|
-
}): { resumeMessage: string; resumePrompt?: string } {
|
|
137
|
-
const custom = input.notResumableMessage;
|
|
138
|
-
const resumeMessage = custom && custom.trim() ? custom : input.defaultMessage;
|
|
139
|
-
const resumePrompt =
|
|
140
|
-
input.flowActivationPrompt ?? input.messages[lastUserMessageIndex(input.messages)]?.content;
|
|
141
|
-
return { resumeMessage, resumePrompt };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Index of the last `user`-role message in `messages`, or `-1` when there is none.
|
|
146
|
-
* The single backward scan behind "what did the user last say" — used to slice the
|
|
147
|
-
* transcript to a clean boundary (`keepThroughLastUserMessage`) and to recover a
|
|
148
|
-
* flow-activation prompt. Allocation-free (no reversed copy).
|
|
149
|
-
*
|
|
150
|
-
* @internal
|
|
151
|
-
*/
|
|
152
|
-
export function lastUserMessageIndex(messages: ReadonlyArray<ChatMessage>): number {
|
|
153
|
-
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
154
|
-
if (messages[i]?.role === 'user') return i;
|
|
155
|
-
}
|
|
156
|
-
return -1;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Type guard: is `value` a {@link PersistedSession} we can safely restore?
|
|
161
|
-
* Gates on matching schema version, same host, and a messages array — the
|
|
162
|
-
* minimum needed to avoid feeding a stale or cross-app blob into restore.
|
|
163
|
-
*
|
|
164
|
-
* @internal
|
|
165
|
-
*/
|
|
166
|
-
export function isRestorableSession(
|
|
167
|
-
value: unknown,
|
|
168
|
-
opts: { host: string; version?: number },
|
|
169
|
-
): value is PersistedSession {
|
|
170
|
-
if (!value || typeof value !== 'object') return false;
|
|
171
|
-
const s = value as Partial<PersistedSession>;
|
|
172
|
-
const expectedVersion = opts.version ?? PERSISTED_SESSION_VERSION;
|
|
173
|
-
return (
|
|
174
|
-
s.version === expectedVersion &&
|
|
175
|
-
typeof s.host === 'string' &&
|
|
176
|
-
s.host === opts.host &&
|
|
177
|
-
Array.isArray(s.messages)
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Project a {@link PersistedSession} onto the Redux `loadSession` payload (the
|
|
183
|
-
* fields restored verbatim into the slice). Agent identity + machine snapshots
|
|
184
|
-
* are applied by the host separately.
|
|
185
|
-
*
|
|
186
|
-
* @internal
|
|
187
|
-
*/
|
|
188
|
-
export function toLoadSessionPayload(s: PersistedSession): LoadSessionPayload {
|
|
189
|
-
return {
|
|
190
|
-
messages: s.messages,
|
|
191
|
-
pinnedAgentName: s.pinnedAgentName ?? null,
|
|
192
|
-
flowOwnerAgentName: s.flowOwnerAgentName ?? null,
|
|
193
|
-
// `sessionCostUsd` is deliberately NOT projected: the slice re-derives cost and
|
|
194
|
-
// token totals from `messages` on load. The snapshot field is still written (its
|
|
195
|
-
// shape is public, and it is a useful cross-check in a saved blob), but it is no
|
|
196
|
-
// longer authoritative — a snapshot saved mid-reset could hold a zeroed total
|
|
197
|
-
// while its transcript still shows the spend.
|
|
198
|
-
contextTokens: s.contextTokens,
|
|
199
|
-
contextLimit: s.contextLimit,
|
|
200
|
-
activeModel: s.activeModel,
|
|
201
|
-
activeProviderName: s.activeProviderName,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Trim a restored transcript back to the last point a provider will accept.
|
|
207
|
-
*
|
|
208
|
-
* Autosave fires on every transcript change (GENC-1351 §5.3) so it can land
|
|
209
|
-
* mid-turn — meaning a snapshot can end on an *incomplete* turn:
|
|
210
|
-
* • an assistant `tool_use` whose `tool_result` hasn't arrived (or only some
|
|
211
|
-
* of its calls are answered) — an orphaned call makes the next turn invalid;
|
|
212
|
-
* • an empty streaming/`thinking` placeholder assistant message;
|
|
213
|
-
* • a pending (unresolved) interaction — a mid-`requestInteraction` turn that
|
|
214
|
-
* can't be resumed (explicitly unsupported).
|
|
215
|
-
*
|
|
216
|
-
* Walks back from the end, dropping such trailing messages until the transcript
|
|
217
|
-
* is balanced — every assistant tool call has its matching result — and ends on
|
|
218
|
-
* a clean boundary. Returns the input unchanged (same reference) when already
|
|
219
|
-
* clean. Turns a mid-turn reload into "recover to the last complete exchange"
|
|
220
|
-
* rather than a hard drop.
|
|
221
|
-
*
|
|
222
|
-
* @internal
|
|
223
|
-
*/
|
|
224
|
-
export function trimToRestorableBoundary(messages: ChatMessage[]): ChatMessage[] {
|
|
225
|
-
let end = messages.length;
|
|
226
|
-
while (end > 0) {
|
|
227
|
-
const slice = messages.slice(0, end);
|
|
228
|
-
const last = slice[end - 1];
|
|
229
|
-
// Trailing unresolved interaction — can't be resumed.
|
|
230
|
-
if (last.interaction && !last.interaction.resolved) {
|
|
231
|
-
end -= 1;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
// Trailing empty assistant placeholder (streaming/thinking stub with no
|
|
235
|
-
// content, tool calls, or interaction yet).
|
|
236
|
-
if (
|
|
237
|
-
last.role === 'assistant' &&
|
|
238
|
-
!last.content &&
|
|
239
|
-
!last.toolCalls?.length &&
|
|
240
|
-
!last.interaction
|
|
241
|
-
) {
|
|
242
|
-
end -= 1;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
// Any assistant tool call without its matching result — orphaned `tool_use`.
|
|
246
|
-
if (hasUnansweredToolCall(slice)) {
|
|
247
|
-
end -= 1;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
return end === messages.length ? messages : messages.slice(0, end);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Keep the transcript through the **last user message** (inclusive), dropping any
|
|
257
|
-
* trailing assistant/tool messages after it (GENC-1351 §5.9). Used by the freeze
|
|
258
|
-
* fallback: when a flow goes non-resumable and there's no prior resumable snapshot
|
|
259
|
-
* to freeze at, we keep the conversation + the prompt that triggered the flow but
|
|
260
|
-
* discard the agent's in-flight work — so a reload shows the user's request, not the
|
|
261
|
-
* half-done agent turns. Returns the input unchanged (same ref) when it already ends
|
|
262
|
-
* on a user message, or when there is no user message at all (nothing to trim to).
|
|
263
|
-
*
|
|
264
|
-
* @internal
|
|
265
|
-
*/
|
|
266
|
-
export function keepThroughLastUserMessage(messages: ChatMessage[]): ChatMessage[] {
|
|
267
|
-
const lastUser = lastUserMessageIndex(messages);
|
|
268
|
-
if (lastUser === -1 || lastUser === messages.length - 1) return messages;
|
|
269
|
-
return messages.slice(0, lastUser + 1);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Names of agents the restored session holds machine/state snapshots for but
|
|
274
|
-
* which are **not** in the currently-configured agent set — their flow cannot
|
|
275
|
-
* resume. Common when the host's agent array is dynamic (agents gated by feature
|
|
276
|
-
* flags, project state, or permissions; e.g. genesis-create). Callers should
|
|
277
|
-
* surface these; the restore is otherwise non-fatal (the transcript and any
|
|
278
|
-
* known agents still restore). Returns `[]` when `knownAgentNames` is empty —
|
|
279
|
-
* the agent set isn't known yet, so we can't classify without false positives.
|
|
280
|
-
*
|
|
281
|
-
* @beta
|
|
282
|
-
*/
|
|
283
|
-
export function unknownAgentSnapshots(
|
|
284
|
-
snapshot: PersistedSession,
|
|
285
|
-
knownAgentNames: Iterable<string>,
|
|
286
|
-
): string[] {
|
|
287
|
-
const known = new Set(knownAgentNames);
|
|
288
|
-
if (known.size === 0) return [];
|
|
289
|
-
return Object.keys(snapshot.agentSnapshots ?? {}).filter((name) => !known.has(name));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Reconcile the per-agent `serialize()` images to persist on a session save,
|
|
294
|
-
* guarding against a post-restore autosave wiping a mid-flow machine (GENC-1351).
|
|
295
|
-
*
|
|
296
|
-
* A restore reads a good snapshot, but the restore's own history change fires an
|
|
297
|
-
* autosave **before** the pinned stateful agent re-activates — at which point the
|
|
298
|
-
* live capture (`getActiveAgent()?.serialize()`) is empty even though a flow is
|
|
299
|
-
* mid-stream. Writing that empty map would overwrite the stored machine snapshot,
|
|
300
|
-
* resetting the flow to its initial step on the *next* reload. So when a flow is
|
|
301
|
-
* owned (`flowOwnerName`) but its live image is missing, carry the last-known-good
|
|
302
|
-
* image forward instead of dropping it.
|
|
303
|
-
*
|
|
304
|
-
* Pure: the caller owns the `lastKnownGood` cache and its lifecycle (seeded on
|
|
305
|
-
* restore, reset on clear). This decides only what to persist now and what the
|
|
306
|
-
* cache should become — advanced only when non-empty, so an empty capture can
|
|
307
|
-
* never erase a good cache.
|
|
308
|
-
*
|
|
309
|
-
* @param live - Agent images captured live this save, keyed by agent name.
|
|
310
|
-
* @param flowOwnerName - The agent owning the in-flight flow, or `null`.
|
|
311
|
-
* @param lastKnownGood - The last non-empty images persisted or restored.
|
|
312
|
-
* @returns `snapshots` to persist, and the `cache` to remember for next time.
|
|
313
|
-
*
|
|
314
|
-
* @internal
|
|
315
|
-
*/
|
|
316
|
-
export function reconcileAgentSnapshots(
|
|
317
|
-
live: Record<string, unknown>,
|
|
318
|
-
flowOwnerName: string | null,
|
|
319
|
-
lastKnownGood: Record<string, unknown>,
|
|
320
|
-
): { snapshots: Record<string, unknown>; cache: Record<string, unknown> } {
|
|
321
|
-
const snapshots = { ...live };
|
|
322
|
-
if (flowOwnerName && snapshots[flowOwnerName] === undefined) {
|
|
323
|
-
const carried = lastKnownGood[flowOwnerName];
|
|
324
|
-
if (carried !== undefined) snapshots[flowOwnerName] = carried;
|
|
325
|
-
}
|
|
326
|
-
const cache = Object.keys(snapshots).length ? snapshots : lastKnownGood;
|
|
327
|
-
return { snapshots, cache };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** True if any assistant tool call in `messages` lacks a matching tool result. */
|
|
331
|
-
function hasUnansweredToolCall(messages: ChatMessage[]): boolean {
|
|
332
|
-
const answered = new Set<string>();
|
|
333
|
-
for (const m of messages) {
|
|
334
|
-
if (m.toolResult) answered.add(m.toolResult.toolCallId);
|
|
335
|
-
}
|
|
336
|
-
for (const m of messages) {
|
|
337
|
-
if (m.toolCalls) {
|
|
338
|
-
for (const tc of m.toolCalls) {
|
|
339
|
-
if (!answered.has(tc.id)) return true;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return false;
|
|
344
|
-
}
|