@fiodos/web-core 0.1.11 → 0.1.12

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.
@@ -56,6 +56,12 @@ export interface FiodosWebAgentConfig {
56
56
  telemetry?: TelemetryAdapter;
57
57
  /** Host auth check, required by manifest actions declaring requiresAuth. */
58
58
  isAuthenticated?: () => boolean;
59
+ /**
60
+ * Host end-user identity. Conversation memory is namespaced per identity
61
+ * (hashed LOCALLY, never sent by this module): logout/account switches swap
62
+ * to that identity's own conversation instead of leaking the previous one.
63
+ */
64
+ getUserId?: () => string | null;
59
65
  messages?: {
60
66
  catalogs?: Record<string, MessageCatalog>;
61
67
  overrides?: Partial<AgentMessages>;
@@ -89,7 +89,7 @@ export declare class AgentController {
89
89
  private consentModalVisible;
90
90
  private readonly sessionId;
91
91
  private sessionStarted;
92
- private readonly conversation;
92
+ private conversation;
93
93
  private pending;
94
94
  private confirmationReasked;
95
95
  private confirmationVoiceActive;
@@ -116,6 +116,10 @@ export declare class AgentController {
116
116
  /** Persistence scope: internal orbs keep their memory under a separate key
117
117
  * so a public orb sharing the same appId can never read it. */
118
118
  private conversationScope;
119
+ /** Current identity segment (LOCAL hash of the host's getUserId; never sent
120
+ * anywhere). The conversation belongs to (app, scope, identity). */
121
+ private conversationIdentity;
122
+ private removeIdentityListeners;
119
123
  private loadingOlderExchanges;
120
124
  private loadOlderTimer;
121
125
  private readonly listeners;
@@ -123,6 +127,22 @@ export declare class AgentController {
123
127
  constructor(config: FiodosWebAgentConfig);
124
128
  private restorePersistedConversation;
125
129
  private persistConversation;
130
+ /**
131
+ * Re-evaluates the host identity and, when it changed, swaps conversations:
132
+ * the outgoing identity's session is saved under ITS private key (so a user
133
+ * who logs back in recovers it, honouring "never clear"), all in-memory
134
+ * state and UI are wiped, and the incoming identity's own session (usually
135
+ * empty) is restored. An anonymous visitor after a logout always starts
136
+ * clean — the previous user's transcript is never inherited.
137
+ */
138
+ private syncConversationIdentity;
139
+ /**
140
+ * Wipes the CURRENT identity's conversation: live memory, panel and the
141
+ * persisted blob. Hosts can call it from their logout for a hard wipe (by
142
+ * default a logout only parks the conversation under the user's private
143
+ * local key).
144
+ */
145
+ resetConversation(): void;
126
146
  getState(): AgentState;
127
147
  /** Update the REAL live activity and re-render (no-op when unchanged). */
128
148
  private setActivity;
@@ -43,6 +43,7 @@ class AgentController {
43
43
  this.consentModalVisible = false;
44
44
  this.sessionId = randomSessionId();
45
45
  this.sessionStarted = false;
46
+ // Replaced wholesale on identity swaps / explicit resets.
46
47
  this.conversation = (0, core_1.createConversationSession)();
47
48
  this.pending = null;
48
49
  this.confirmationReasked = false;
@@ -72,6 +73,7 @@ class AgentController {
72
73
  /** Persistence scope: internal orbs keep their memory under a separate key
73
74
  * so a public orb sharing the same appId can never read it. */
74
75
  this.conversationScope = 'public';
76
+ this.removeIdentityListeners = null;
75
77
  this.loadingOlderExchanges = false;
76
78
  this.loadOlderTimer = null;
77
79
  this.listeners = new Set();
@@ -131,15 +133,29 @@ class AgentController {
131
133
  });
132
134
  // Re-emit combined state whenever the speech machine advances.
133
135
  this.unsubscribeSpeech = this.speech.subscribe(() => this.emit());
136
+ this.conversationIdentity = (0, core_1.conversationIdentityKey)(config.getUserId?.());
134
137
  // Rehydrate the previous conversation (page reloads): the persisted blob
135
138
  // carries its own retention rule, so expiry is decided correctly even
136
139
  // before the published config arrives. Best-effort and non-blocking.
137
140
  void this.restorePersistedConversation();
141
+ // Logout/login usually happens on another screen: re-check identity when
142
+ // the tab regains focus or becomes visible again, besides before every turn.
143
+ if (typeof window !== 'undefined') {
144
+ const check = () => void this.syncConversationIdentity();
145
+ window.addEventListener('focus', check);
146
+ document.addEventListener('visibilitychange', check);
147
+ this.removeIdentityListeners = () => {
148
+ window.removeEventListener('focus', check);
149
+ document.removeEventListener('visibilitychange', check);
150
+ };
151
+ }
138
152
  }
139
153
  // ── Conversation persistence (survives page reloads) ───────────────────────
140
154
  async restorePersistedConversation() {
141
- const restored = await (0, core_1.restoreConversationSession)(this.config.storage, this.config.manifest.appId, Date.now(), this.conversationScope);
142
- if (!restored)
155
+ const identityAtStart = this.conversationIdentity;
156
+ const restored = await (0, core_1.restoreConversationSession)(this.config.storage, this.config.manifest.appId, Date.now(), this.conversationScope, identityAtStart);
157
+ // The identity may have changed again while the read resolved.
158
+ if (!restored || this.conversationIdentity !== identityAtStart)
143
159
  return;
144
160
  // Never clobber turns the user already made while the restore resolved.
145
161
  if (this.conversation.turns.length > 0 || this.conversation.uiArchive?.length)
@@ -151,7 +167,43 @@ class AgentController {
151
167
  this.emit();
152
168
  }
153
169
  persistConversation() {
154
- void (0, core_1.persistConversationSession)(this.config.storage, this.config.manifest.appId, this.conversation, this.conversationRetentionMinutes, this.conversationScope);
170
+ void (0, core_1.persistConversationSession)(this.config.storage, this.config.manifest.appId, this.conversation, this.conversationRetentionMinutes, this.conversationScope, this.conversationIdentity);
171
+ }
172
+ /**
173
+ * Re-evaluates the host identity and, when it changed, swaps conversations:
174
+ * the outgoing identity's session is saved under ITS private key (so a user
175
+ * who logs back in recovers it, honouring "never clear"), all in-memory
176
+ * state and UI are wiped, and the incoming identity's own session (usually
177
+ * empty) is restored. An anonymous visitor after a logout always starts
178
+ * clean — the previous user's transcript is never inherited.
179
+ */
180
+ syncConversationIdentity() {
181
+ const next = (0, core_1.conversationIdentityKey)(this.config.getUserId?.());
182
+ if (next === this.conversationIdentity)
183
+ return false;
184
+ // 1. Park the outgoing identity's session under its own key.
185
+ this.persistConversation();
186
+ // 2. Wipe every trace from memory and the panel.
187
+ this.conversation = (0, core_1.createConversationSession)();
188
+ this.conversationIdentity = next;
189
+ this.lastExchange = null;
190
+ this.emit();
191
+ // 3. Bring in the new identity's own session, if any.
192
+ void this.restorePersistedConversation();
193
+ return true;
194
+ }
195
+ /**
196
+ * Wipes the CURRENT identity's conversation: live memory, panel and the
197
+ * persisted blob. Hosts can call it from their logout for a hard wipe (by
198
+ * default a logout only parks the conversation under the user's private
199
+ * local key).
200
+ */
201
+ resetConversation() {
202
+ this.cancelAll();
203
+ this.conversation = (0, core_1.createConversationSession)();
204
+ this.lastExchange = null;
205
+ void (0, core_1.clearPersistedConversation)(this.config.storage, this.config.manifest.appId, this.conversationScope, this.conversationIdentity);
206
+ this.emit();
155
207
  }
156
208
  // ── Subscription / state ───────────────────────────────────────────────────
157
209
  getState() {
@@ -820,6 +872,9 @@ class AgentController {
820
872
  this.setPhase('idle');
821
873
  return;
822
874
  }
875
+ // Identity gate: if the host user changed since the last turn (login,
876
+ // logout, account switch), swap conversations BEFORE recording anything.
877
+ this.syncConversationIdentity();
823
878
  this.setPhase('thinking');
824
879
  // REAL activity: the request is genuinely in flight from this moment.
825
880
  this.setActivity({ kind: 'thinking' });
@@ -1112,6 +1167,8 @@ class AgentController {
1112
1167
  }
1113
1168
  dispose() {
1114
1169
  this.cancelAll();
1170
+ this.removeIdentityListeners?.();
1171
+ this.removeIdentityListeners = null;
1115
1172
  if (this.loadOlderTimer) {
1116
1173
  clearTimeout(this.loadOlderTimer);
1117
1174
  this.loadOlderTimer = null;
@@ -59,6 +59,11 @@ export interface FiodosAgentHandle {
59
59
  orb: MountedOrb | null;
60
60
  /** Resolves once the controller is ready (immediately on the manual path). */
61
61
  ready: Promise<AgentController | null>;
62
+ /**
63
+ * Wipes the current identity's conversation (live memory + persisted blob).
64
+ * Call it from the app's logout for a hard wipe; no-op until ready.
65
+ */
66
+ resetConversation(): void;
62
67
  destroy(): void;
63
68
  }
64
69
  export declare function createFiodosAgent(options: CreateFiodosAgentOptions): FiodosAgentHandle;
@@ -75,6 +75,7 @@ function assemble(options, manifest, baseUrl) {
75
75
  storage: (0, webStorageAdapter_1.createWebStorageAdapter)(),
76
76
  telemetry,
77
77
  isAuthenticated: options.isAuthenticated,
78
+ getUserId: options.getUserId,
78
79
  ttsVoice: options.ttsVoice,
79
80
  ...options.configOverrides,
80
81
  };
@@ -97,6 +98,9 @@ function createFiodosAgent(options) {
97
98
  controller,
98
99
  orb,
99
100
  ready: Promise.resolve(controller),
101
+ resetConversation() {
102
+ controller.resetConversation();
103
+ },
100
104
  destroy() {
101
105
  orb?.unmount();
102
106
  controller.dispose();
@@ -109,6 +113,9 @@ function createFiodosAgent(options) {
109
113
  controller: null,
110
114
  orb: null,
111
115
  ready: Promise.resolve(null),
116
+ resetConversation() {
117
+ handle.controller?.resetConversation();
118
+ },
112
119
  destroy() {
113
120
  cancelled = true;
114
121
  handle.orb?.unmount();
@@ -5,5 +5,5 @@
5
5
  * Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
6
6
  * by hand — change package.json `version` and re-run the sync/release script.
7
7
  */
8
- export declare const SDK_VERSION = "0.1.11";
8
+ export declare const SDK_VERSION = "0.1.12";
9
9
  export declare const SDK_PACKAGE = "@fiodos/web-core";
@@ -8,5 +8,5 @@ exports.SDK_PACKAGE = exports.SDK_VERSION = void 0;
8
8
  * Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
9
9
  * by hand — change package.json `version` and re-run the sync/release script.
10
10
  */
11
- exports.SDK_VERSION = '0.1.11';
11
+ exports.SDK_VERSION = '0.1.12';
12
12
  exports.SDK_PACKAGE = '@fiodos/web-core';
@@ -56,6 +56,12 @@ export interface FiodosWebAgentConfig {
56
56
  telemetry?: TelemetryAdapter;
57
57
  /** Host auth check, required by manifest actions declaring requiresAuth. */
58
58
  isAuthenticated?: () => boolean;
59
+ /**
60
+ * Host end-user identity. Conversation memory is namespaced per identity
61
+ * (hashed LOCALLY, never sent by this module): logout/account switches swap
62
+ * to that identity's own conversation instead of leaking the previous one.
63
+ */
64
+ getUserId?: () => string | null;
59
65
  messages?: {
60
66
  catalogs?: Record<string, MessageCatalog>;
61
67
  overrides?: Partial<AgentMessages>;
@@ -89,7 +89,7 @@ export declare class AgentController {
89
89
  private consentModalVisible;
90
90
  private readonly sessionId;
91
91
  private sessionStarted;
92
- private readonly conversation;
92
+ private conversation;
93
93
  private pending;
94
94
  private confirmationReasked;
95
95
  private confirmationVoiceActive;
@@ -116,6 +116,10 @@ export declare class AgentController {
116
116
  /** Persistence scope: internal orbs keep their memory under a separate key
117
117
  * so a public orb sharing the same appId can never read it. */
118
118
  private conversationScope;
119
+ /** Current identity segment (LOCAL hash of the host's getUserId; never sent
120
+ * anywhere). The conversation belongs to (app, scope, identity). */
121
+ private conversationIdentity;
122
+ private removeIdentityListeners;
119
123
  private loadingOlderExchanges;
120
124
  private loadOlderTimer;
121
125
  private readonly listeners;
@@ -123,6 +127,22 @@ export declare class AgentController {
123
127
  constructor(config: FiodosWebAgentConfig);
124
128
  private restorePersistedConversation;
125
129
  private persistConversation;
130
+ /**
131
+ * Re-evaluates the host identity and, when it changed, swaps conversations:
132
+ * the outgoing identity's session is saved under ITS private key (so a user
133
+ * who logs back in recovers it, honouring "never clear"), all in-memory
134
+ * state and UI are wiped, and the incoming identity's own session (usually
135
+ * empty) is restored. An anonymous visitor after a logout always starts
136
+ * clean — the previous user's transcript is never inherited.
137
+ */
138
+ private syncConversationIdentity;
139
+ /**
140
+ * Wipes the CURRENT identity's conversation: live memory, panel and the
141
+ * persisted blob. Hosts can call it from their logout for a hard wipe (by
142
+ * default a logout only parks the conversation under the user's private
143
+ * local key).
144
+ */
145
+ resetConversation(): void;
126
146
  getState(): AgentState;
127
147
  /** Update the REAL live activity and re-render (no-op when unchanged). */
128
148
  private setActivity;
@@ -15,7 +15,7 @@
15
15
  * deliberate modal tap; a loose "yes" never confirms them.
16
16
  * - The agent only ever executes actions declared in the manifest.
17
17
  */
18
- import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, buildBubbleExchangeWindow, BUBBLE_PAGE_EXCHANGES, conversationRetentionOptions, createConversationSession, persistConversationSession, restoreConversationSession, defaultRouteContextText, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, validateManifest, } from '@fiodos/core';
18
+ import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, buildBubbleExchangeWindow, BUBBLE_PAGE_EXCHANGES, clearPersistedConversation, conversationIdentityKey, conversationRetentionOptions, createConversationSession, persistConversationSession, restoreConversationSession, defaultRouteContextText, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, validateManifest, } from '@fiodos/core';
19
19
  import { AgentApiError } from '../api/errors.js';
20
20
  import { formatAgentTurnError } from '../api/formatTurnError.js';
21
21
  import { buildManifestPayload } from '../api/backendClient.js';
@@ -40,6 +40,7 @@ export class AgentController {
40
40
  this.consentModalVisible = false;
41
41
  this.sessionId = randomSessionId();
42
42
  this.sessionStarted = false;
43
+ // Replaced wholesale on identity swaps / explicit resets.
43
44
  this.conversation = createConversationSession();
44
45
  this.pending = null;
45
46
  this.confirmationReasked = false;
@@ -69,6 +70,7 @@ export class AgentController {
69
70
  /** Persistence scope: internal orbs keep their memory under a separate key
70
71
  * so a public orb sharing the same appId can never read it. */
71
72
  this.conversationScope = 'public';
73
+ this.removeIdentityListeners = null;
72
74
  this.loadingOlderExchanges = false;
73
75
  this.loadOlderTimer = null;
74
76
  this.listeners = new Set();
@@ -128,15 +130,29 @@ export class AgentController {
128
130
  });
129
131
  // Re-emit combined state whenever the speech machine advances.
130
132
  this.unsubscribeSpeech = this.speech.subscribe(() => this.emit());
133
+ this.conversationIdentity = conversationIdentityKey(config.getUserId?.());
131
134
  // Rehydrate the previous conversation (page reloads): the persisted blob
132
135
  // carries its own retention rule, so expiry is decided correctly even
133
136
  // before the published config arrives. Best-effort and non-blocking.
134
137
  void this.restorePersistedConversation();
138
+ // Logout/login usually happens on another screen: re-check identity when
139
+ // the tab regains focus or becomes visible again, besides before every turn.
140
+ if (typeof window !== 'undefined') {
141
+ const check = () => void this.syncConversationIdentity();
142
+ window.addEventListener('focus', check);
143
+ document.addEventListener('visibilitychange', check);
144
+ this.removeIdentityListeners = () => {
145
+ window.removeEventListener('focus', check);
146
+ document.removeEventListener('visibilitychange', check);
147
+ };
148
+ }
135
149
  }
136
150
  // ── Conversation persistence (survives page reloads) ───────────────────────
137
151
  async restorePersistedConversation() {
138
- const restored = await restoreConversationSession(this.config.storage, this.config.manifest.appId, Date.now(), this.conversationScope);
139
- if (!restored)
152
+ const identityAtStart = this.conversationIdentity;
153
+ const restored = await restoreConversationSession(this.config.storage, this.config.manifest.appId, Date.now(), this.conversationScope, identityAtStart);
154
+ // The identity may have changed again while the read resolved.
155
+ if (!restored || this.conversationIdentity !== identityAtStart)
140
156
  return;
141
157
  // Never clobber turns the user already made while the restore resolved.
142
158
  if (this.conversation.turns.length > 0 || this.conversation.uiArchive?.length)
@@ -148,7 +164,43 @@ export class AgentController {
148
164
  this.emit();
149
165
  }
150
166
  persistConversation() {
151
- void persistConversationSession(this.config.storage, this.config.manifest.appId, this.conversation, this.conversationRetentionMinutes, this.conversationScope);
167
+ void persistConversationSession(this.config.storage, this.config.manifest.appId, this.conversation, this.conversationRetentionMinutes, this.conversationScope, this.conversationIdentity);
168
+ }
169
+ /**
170
+ * Re-evaluates the host identity and, when it changed, swaps conversations:
171
+ * the outgoing identity's session is saved under ITS private key (so a user
172
+ * who logs back in recovers it, honouring "never clear"), all in-memory
173
+ * state and UI are wiped, and the incoming identity's own session (usually
174
+ * empty) is restored. An anonymous visitor after a logout always starts
175
+ * clean — the previous user's transcript is never inherited.
176
+ */
177
+ syncConversationIdentity() {
178
+ const next = conversationIdentityKey(this.config.getUserId?.());
179
+ if (next === this.conversationIdentity)
180
+ return false;
181
+ // 1. Park the outgoing identity's session under its own key.
182
+ this.persistConversation();
183
+ // 2. Wipe every trace from memory and the panel.
184
+ this.conversation = createConversationSession();
185
+ this.conversationIdentity = next;
186
+ this.lastExchange = null;
187
+ this.emit();
188
+ // 3. Bring in the new identity's own session, if any.
189
+ void this.restorePersistedConversation();
190
+ return true;
191
+ }
192
+ /**
193
+ * Wipes the CURRENT identity's conversation: live memory, panel and the
194
+ * persisted blob. Hosts can call it from their logout for a hard wipe (by
195
+ * default a logout only parks the conversation under the user's private
196
+ * local key).
197
+ */
198
+ resetConversation() {
199
+ this.cancelAll();
200
+ this.conversation = createConversationSession();
201
+ this.lastExchange = null;
202
+ void clearPersistedConversation(this.config.storage, this.config.manifest.appId, this.conversationScope, this.conversationIdentity);
203
+ this.emit();
152
204
  }
153
205
  // ── Subscription / state ───────────────────────────────────────────────────
154
206
  getState() {
@@ -817,6 +869,9 @@ export class AgentController {
817
869
  this.setPhase('idle');
818
870
  return;
819
871
  }
872
+ // Identity gate: if the host user changed since the last turn (login,
873
+ // logout, account switch), swap conversations BEFORE recording anything.
874
+ this.syncConversationIdentity();
820
875
  this.setPhase('thinking');
821
876
  // REAL activity: the request is genuinely in flight from this moment.
822
877
  this.setActivity({ kind: 'thinking' });
@@ -1109,6 +1164,8 @@ export class AgentController {
1109
1164
  }
1110
1165
  dispose() {
1111
1166
  this.cancelAll();
1167
+ this.removeIdentityListeners?.();
1168
+ this.removeIdentityListeners = null;
1112
1169
  if (this.loadOlderTimer) {
1113
1170
  clearTimeout(this.loadOlderTimer);
1114
1171
  this.loadOlderTimer = null;
@@ -59,6 +59,11 @@ export interface FiodosAgentHandle {
59
59
  orb: MountedOrb | null;
60
60
  /** Resolves once the controller is ready (immediately on the manual path). */
61
61
  ready: Promise<AgentController | null>;
62
+ /**
63
+ * Wipes the current identity's conversation (live memory + persisted blob).
64
+ * Call it from the app's logout for a hard wipe; no-op until ready.
65
+ */
66
+ resetConversation(): void;
62
67
  destroy(): void;
63
68
  }
64
69
  export declare function createFiodosAgent(options: CreateFiodosAgentOptions): FiodosAgentHandle;
@@ -72,6 +72,7 @@ function assemble(options, manifest, baseUrl) {
72
72
  storage: createWebStorageAdapter(),
73
73
  telemetry,
74
74
  isAuthenticated: options.isAuthenticated,
75
+ getUserId: options.getUserId,
75
76
  ttsVoice: options.ttsVoice,
76
77
  ...options.configOverrides,
77
78
  };
@@ -94,6 +95,9 @@ export function createFiodosAgent(options) {
94
95
  controller,
95
96
  orb,
96
97
  ready: Promise.resolve(controller),
98
+ resetConversation() {
99
+ controller.resetConversation();
100
+ },
97
101
  destroy() {
98
102
  orb?.unmount();
99
103
  controller.dispose();
@@ -106,6 +110,9 @@ export function createFiodosAgent(options) {
106
110
  controller: null,
107
111
  orb: null,
108
112
  ready: Promise.resolve(null),
113
+ resetConversation() {
114
+ handle.controller?.resetConversation();
115
+ },
109
116
  destroy() {
110
117
  cancelled = true;
111
118
  handle.orb?.unmount();
@@ -5,5 +5,5 @@
5
5
  * Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
6
6
  * by hand — change package.json `version` and re-run the sync/release script.
7
7
  */
8
- export declare const SDK_VERSION = "0.1.11";
8
+ export declare const SDK_VERSION = "0.1.12";
9
9
  export declare const SDK_PACKAGE = "@fiodos/web-core";
@@ -5,5 +5,5 @@
5
5
  * Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
6
6
  * by hand — change package.json `version` and re-run the sync/release script.
7
7
  */
8
- export const SDK_VERSION = '0.1.11';
8
+ export const SDK_VERSION = '0.1.12';
9
9
  export const SDK_PACKAGE = '@fiodos/web-core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiodos/web-core",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Framework-agnostic browser layer for the Fiodos agent: a vanilla AgentController (orchestrator), DOM adapters (navigation/voice/storage), HTTP client and decision engine over @fiodos/core. Shared base for @fiodos/vue, @fiodos/svelte and @fiodos/angular (no framework imports).",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": {
@@ -29,7 +29,7 @@
29
29
  "prepublishOnly": "node ../../scripts/sync-sdk-version.mjs && npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@fiodos/core": "^0.1.7"
32
+ "@fiodos/core": "^0.1.9"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^22.0.0",