@pellux/goodvibes-sdk 0.19.6 → 0.19.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/_internal/contracts/index.d.ts +1 -0
  2. package/dist/_internal/contracts/index.d.ts.map +1 -1
  3. package/dist/_internal/contracts/index.js +2 -0
  4. package/dist/_internal/contracts/types.d.ts +4 -0
  5. package/dist/_internal/contracts/types.d.ts.map +1 -1
  6. package/dist/_internal/contracts/zod-schemas/accounts.d.ts +81 -0
  7. package/dist/_internal/contracts/zod-schemas/accounts.d.ts.map +1 -0
  8. package/dist/_internal/contracts/zod-schemas/accounts.js +47 -0
  9. package/dist/_internal/contracts/zod-schemas/auth.d.ts +42 -0
  10. package/dist/_internal/contracts/zod-schemas/auth.d.ts.map +1 -0
  11. package/dist/_internal/contracts/zod-schemas/auth.js +29 -0
  12. package/dist/_internal/contracts/zod-schemas/events.d.ts +37 -0
  13. package/dist/_internal/contracts/zod-schemas/events.d.ts.map +1 -0
  14. package/dist/_internal/contracts/zod-schemas/events.js +26 -0
  15. package/dist/_internal/contracts/zod-schemas/index.d.ts +9 -0
  16. package/dist/_internal/contracts/zod-schemas/index.d.ts.map +1 -0
  17. package/dist/_internal/contracts/zod-schemas/index.js +4 -0
  18. package/dist/_internal/contracts/zod-schemas/session.d.ts +22 -0
  19. package/dist/_internal/contracts/zod-schemas/session.d.ts.map +1 -0
  20. package/dist/_internal/contracts/zod-schemas/session.js +19 -0
  21. package/dist/_internal/daemon/api-router.d.ts.map +1 -1
  22. package/dist/_internal/daemon/api-router.js +0 -1
  23. package/dist/_internal/daemon/automation.d.ts.map +1 -1
  24. package/dist/_internal/daemon/channel-route-types.d.ts.map +1 -1
  25. package/dist/_internal/daemon/channel-routes.d.ts.map +1 -1
  26. package/dist/_internal/daemon/context.d.ts.map +1 -1
  27. package/dist/_internal/daemon/control-routes.d.ts.map +1 -1
  28. package/dist/_internal/daemon/http-policy.d.ts.map +1 -1
  29. package/dist/_internal/daemon/http-policy.js +0 -1
  30. package/dist/_internal/daemon/integration-route-types.d.ts.map +1 -1
  31. package/dist/_internal/daemon/integration-routes.d.ts.map +1 -1
  32. package/dist/_internal/daemon/knowledge-route-types.d.ts.map +1 -1
  33. package/dist/_internal/daemon/knowledge-routes.d.ts.map +1 -1
  34. package/dist/_internal/daemon/knowledge-routes.js +5 -4
  35. package/dist/_internal/daemon/media-route-types.d.ts.map +1 -1
  36. package/dist/_internal/daemon/media-routes.d.ts.map +1 -1
  37. package/dist/_internal/daemon/operator.d.ts.map +1 -1
  38. package/dist/_internal/daemon/remote-routes.d.ts.map +1 -1
  39. package/dist/_internal/daemon/remote.d.ts.map +1 -1
  40. package/dist/_internal/daemon/route-helpers.d.ts.map +1 -1
  41. package/dist/_internal/daemon/runtime-automation-routes.d.ts.map +1 -1
  42. package/dist/_internal/daemon/runtime-route-types.d.ts +14 -1
  43. package/dist/_internal/daemon/runtime-route-types.d.ts.map +1 -1
  44. package/dist/_internal/daemon/runtime-routes.d.ts.map +1 -1
  45. package/dist/_internal/daemon/runtime-session-routes.d.ts.map +1 -1
  46. package/dist/_internal/daemon/runtime-session-routes.js +0 -2
  47. package/dist/_internal/daemon/sessions.d.ts.map +1 -1
  48. package/dist/_internal/daemon/system-route-types.d.ts.map +1 -1
  49. package/dist/_internal/daemon/system-routes.d.ts.map +1 -1
  50. package/dist/_internal/daemon/tasks.d.ts.map +1 -1
  51. package/dist/_internal/daemon/telemetry-routes.d.ts.map +1 -1
  52. package/dist/_internal/errors/daemon-error-contract.d.ts.map +1 -1
  53. package/dist/_internal/errors/index.d.ts +2 -2
  54. package/dist/_internal/errors/index.js +2 -2
  55. package/dist/_internal/operator/client-core.d.ts.map +1 -1
  56. package/dist/_internal/operator/client-core.js +8 -2
  57. package/dist/_internal/operator/client.d.ts +7 -0
  58. package/dist/_internal/operator/client.d.ts.map +1 -1
  59. package/dist/_internal/operator/client.js +32 -1
  60. package/dist/_internal/peer/client-core.d.ts.map +1 -1
  61. package/dist/_internal/platform/agents/orchestrator.d.ts +7 -0
  62. package/dist/_internal/platform/agents/orchestrator.d.ts.map +1 -1
  63. package/dist/_internal/platform/agents/orchestrator.js +8 -0
  64. package/dist/_internal/platform/auth/android-keystore-token-store.d.ts +110 -0
  65. package/dist/_internal/platform/auth/android-keystore-token-store.d.ts.map +1 -0
  66. package/dist/_internal/platform/auth/android-keystore-token-store.js +164 -0
  67. package/dist/_internal/platform/auth/auto-refresh-middleware.d.ts +46 -0
  68. package/dist/_internal/platform/auth/auto-refresh-middleware.d.ts.map +1 -0
  69. package/dist/_internal/platform/auth/auto-refresh-middleware.js +155 -0
  70. package/dist/_internal/platform/auth/auto-refresh.d.ts +123 -0
  71. package/dist/_internal/platform/auth/auto-refresh.d.ts.map +1 -0
  72. package/dist/_internal/platform/auth/auto-refresh.js +236 -0
  73. package/dist/_internal/platform/auth/expo-secure-token-store.d.ts +82 -0
  74. package/dist/_internal/platform/auth/expo-secure-token-store.d.ts.map +1 -0
  75. package/dist/_internal/platform/auth/expo-secure-token-store.js +135 -0
  76. package/dist/_internal/platform/auth/index.d.ts +3 -0
  77. package/dist/_internal/platform/auth/index.d.ts.map +1 -1
  78. package/dist/_internal/platform/auth/index.js +2 -0
  79. package/dist/_internal/platform/auth/ios-keychain-token-store.d.ts +88 -0
  80. package/dist/_internal/platform/auth/ios-keychain-token-store.d.ts.map +1 -0
  81. package/dist/_internal/platform/auth/ios-keychain-token-store.js +147 -0
  82. package/dist/_internal/platform/auth/session-manager.d.ts +2 -0
  83. package/dist/_internal/platform/auth/session-manager.d.ts.map +1 -1
  84. package/dist/_internal/platform/auth/session-manager.js +9 -1
  85. package/dist/_internal/platform/auth/token-store.d.ts +13 -0
  86. package/dist/_internal/platform/auth/token-store.d.ts.map +1 -1
  87. package/dist/_internal/platform/auth/token-store.js +23 -0
  88. package/dist/_internal/platform/companion/companion-chat-manager.d.ts +64 -11
  89. package/dist/_internal/platform/companion/companion-chat-manager.d.ts.map +1 -1
  90. package/dist/_internal/platform/companion/companion-chat-manager.js +158 -12
  91. package/dist/_internal/platform/companion/companion-chat-persistence.d.ts +33 -0
  92. package/dist/_internal/platform/companion/companion-chat-persistence.d.ts.map +1 -0
  93. package/dist/_internal/platform/companion/companion-chat-persistence.js +115 -0
  94. package/dist/_internal/platform/companion/companion-chat-rate-limiter.d.ts +47 -0
  95. package/dist/_internal/platform/companion/companion-chat-rate-limiter.d.ts.map +1 -0
  96. package/dist/_internal/platform/companion/companion-chat-rate-limiter.js +117 -0
  97. package/dist/_internal/platform/companion/companion-chat-types.d.ts +2 -4
  98. package/dist/_internal/platform/companion/companion-chat-types.d.ts.map +1 -1
  99. package/dist/_internal/platform/companion/companion-chat-types.js +2 -4
  100. package/dist/_internal/platform/companion/index.d.ts +4 -0
  101. package/dist/_internal/platform/companion/index.d.ts.map +1 -1
  102. package/dist/_internal/platform/companion/index.js +2 -0
  103. package/dist/_internal/platform/daemon/facade-composition.d.ts.map +1 -1
  104. package/dist/_internal/platform/daemon/facade-composition.js +5 -0
  105. package/dist/_internal/platform/daemon/facade.d.ts.map +1 -1
  106. package/dist/_internal/platform/daemon/facade.js +3 -0
  107. package/dist/_internal/platform/daemon/http/runtime-route-types.d.ts +0 -7
  108. package/dist/_internal/platform/daemon/http/runtime-route-types.d.ts.map +1 -1
  109. package/dist/_internal/platform/state/db.d.ts.map +1 -1
  110. package/dist/_internal/platform/state/db.js +0 -1
  111. package/dist/_internal/platform/state/sqlite-store.d.ts.map +1 -1
  112. package/dist/_internal/platform/state/sqlite-store.js +0 -1
  113. package/dist/_internal/platform/version.js +1 -1
  114. package/dist/_internal/transport-core/client-transport.d.ts.map +1 -1
  115. package/dist/_internal/transport-core/event-envelope.d.ts.map +1 -1
  116. package/dist/_internal/transport-core/event-feeds.d.ts.map +1 -1
  117. package/dist/_internal/transport-core/index.d.ts +5 -0
  118. package/dist/_internal/transport-core/index.d.ts.map +1 -1
  119. package/dist/_internal/transport-core/index.js +3 -0
  120. package/dist/_internal/transport-core/middleware.d.ts +76 -0
  121. package/dist/_internal/transport-core/middleware.d.ts.map +1 -0
  122. package/dist/_internal/transport-core/middleware.js +67 -0
  123. package/dist/_internal/transport-core/observer.d.ts +53 -0
  124. package/dist/_internal/transport-core/observer.d.ts.map +1 -0
  125. package/dist/_internal/transport-core/observer.js +26 -0
  126. package/dist/_internal/transport-core/otel.d.ts +64 -0
  127. package/dist/_internal/transport-core/otel.d.ts.map +1 -0
  128. package/dist/_internal/transport-core/otel.js +149 -0
  129. package/dist/_internal/transport-direct/index.d.ts.map +1 -1
  130. package/dist/_internal/transport-direct/index.js +0 -1
  131. package/dist/_internal/transport-http/contract-client.d.ts +11 -1
  132. package/dist/_internal/transport-http/contract-client.d.ts.map +1 -1
  133. package/dist/_internal/transport-http/contract-client.js +18 -4
  134. package/dist/_internal/transport-http/http-core.d.ts +27 -1
  135. package/dist/_internal/transport-http/http-core.d.ts.map +1 -1
  136. package/dist/_internal/transport-http/http-core.js +180 -12
  137. package/dist/_internal/transport-http/http.d.ts +3 -3
  138. package/dist/_internal/transport-http/http.d.ts.map +1 -1
  139. package/dist/_internal/transport-http/http.js +2 -2
  140. package/dist/_internal/transport-http/index.d.ts +4 -2
  141. package/dist/_internal/transport-http/index.d.ts.map +1 -1
  142. package/dist/_internal/transport-http/index.js +2 -1
  143. package/dist/_internal/transport-http/paths.js +1 -1
  144. package/dist/_internal/transport-http/reconnect.d.ts +2 -0
  145. package/dist/_internal/transport-http/reconnect.d.ts.map +1 -1
  146. package/dist/_internal/transport-http/reconnect.js +4 -2
  147. package/dist/_internal/transport-http/retry.d.ts +15 -0
  148. package/dist/_internal/transport-http/retry.d.ts.map +1 -1
  149. package/dist/_internal/transport-http/retry.js +19 -0
  150. package/dist/_internal/transport-realtime/domain-events.d.ts.map +1 -1
  151. package/dist/_internal/transport-realtime/runtime-events.d.ts +10 -3
  152. package/dist/_internal/transport-realtime/runtime-events.d.ts.map +1 -1
  153. package/dist/_internal/transport-realtime/runtime-events.js +73 -8
  154. package/dist/auth.d.ts +38 -3
  155. package/dist/auth.d.ts.map +1 -1
  156. package/dist/auth.js +68 -3
  157. package/dist/client.d.ts +61 -2
  158. package/dist/client.d.ts.map +1 -1
  159. package/dist/client.js +64 -3
  160. package/dist/expo.d.ts +1 -0
  161. package/dist/expo.d.ts.map +1 -1
  162. package/dist/expo.js +1 -0
  163. package/dist/observer/index.d.ts +16 -25
  164. package/dist/observer/index.d.ts.map +1 -1
  165. package/dist/platform/runtime/transports/http.js +1 -1
  166. package/dist/react-native.d.ts +2 -0
  167. package/dist/react-native.d.ts.map +1 -1
  168. package/dist/react-native.js +2 -0
  169. package/package.json +16 -3
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ios-keychain-token-store.ts
3
+ *
4
+ * Token store backed by `react-native-keychain` for bare React Native on iOS.
5
+ *
6
+ * Uses `Keychain.setGenericPassword` / `getGenericPassword` /
7
+ * `resetGenericPassword` to persist tokens in the iOS Keychain.
8
+ *
9
+ * `react-native-keychain` is an **optional peer dependency** — this module
10
+ * does NOT import it at the top level.
11
+ *
12
+ * ## Installation
13
+ *
14
+ * ```sh
15
+ * npm install react-native-keychain
16
+ * npx pod-install # iOS CocoaPods link
17
+ * ```
18
+ *
19
+ * Wave 6 three-part error messages: [what happened] · [why] · [what to do]
20
+ */
21
+ import { GoodVibesSdkError } from '../../errors/index.js';
22
+ let _mod = null;
23
+ async function loadKeychain() {
24
+ if (_mod !== null)
25
+ return _mod;
26
+ try {
27
+ _mod = await import('react-native-keychain');
28
+ return _mod;
29
+ }
30
+ catch {
31
+ throw new GoodVibesSdkError('react-native-keychain is not installed — the iOS Keychain token store cannot be initialised. ' +
32
+ 'This optional peer dependency is required to persist tokens in the iOS Keychain. ' +
33
+ 'Run `npm install react-native-keychain && npx pod-install` and rebuild your app.', {
34
+ code: 'RN_KEYCHAIN_NOT_INSTALLED',
35
+ category: 'config',
36
+ source: 'config',
37
+ recoverable: false,
38
+ });
39
+ }
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Fixed username slot
43
+ // ---------------------------------------------------------------------------
44
+ const USERNAME_SLOT = 'goodvibes-sdk';
45
+ // ---------------------------------------------------------------------------
46
+ // Factory
47
+ // ---------------------------------------------------------------------------
48
+ /**
49
+ * Create a `GoodVibesTokenStore` backed by the iOS Keychain via
50
+ * `react-native-keychain`.
51
+ *
52
+ * Suitable for **bare React Native** iOS apps. For Expo-managed workflow, use
53
+ * `createExpoSecureTokenStore` instead.
54
+ *
55
+ * Both the `token` and `expiresAt` values are serialised as a single JSON
56
+ * blob in the keychain password slot. The username slot is fixed to
57
+ * `'goodvibes-sdk'`.
58
+ *
59
+ * `react-native-keychain` is an **optional peer dependency** — install it with:
60
+ *
61
+ * ```sh
62
+ * npm install react-native-keychain
63
+ * npx pod-install
64
+ * ```
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * import { createIOSKeychainTokenStore, createReactNativeGoodVibesSdk } from '@pellux/goodvibes-sdk/react-native';
69
+ *
70
+ * const tokenStore = createIOSKeychainTokenStore({ service: 'com.myapp.gv' });
71
+ * const sdk = createReactNativeGoodVibesSdk({ baseUrl: 'https://daemon.example.com', tokenStore });
72
+ * ```
73
+ */
74
+ export function createIOSKeychainTokenStore(options = {}, __loadModule) {
75
+ const service = options.service ?? 'com.pellux.goodvibes-sdk';
76
+ const accessible = options.accessible ?? 'WHEN_UNLOCKED_THIS_DEVICE_ONLY';
77
+ const accessGroup = options.accessGroup;
78
+ async function resolveModule() {
79
+ if (__loadModule !== undefined) {
80
+ return __loadModule();
81
+ }
82
+ return loadKeychain();
83
+ }
84
+ function buildOptions(mod) {
85
+ const opts = { service };
86
+ const accessibleValue = mod.ACCESSIBLE[accessible];
87
+ if (accessibleValue !== undefined) {
88
+ opts['accessible'] = accessibleValue;
89
+ }
90
+ else if (options.accessible !== undefined) {
91
+ console.warn(`[pellux/goodvibes-sdk] react-native-keychain does not expose ACCESSIBLE.${accessible}; falling back to default`);
92
+ }
93
+ if (accessGroup !== undefined) {
94
+ opts['accessGroup'] = accessGroup;
95
+ }
96
+ return opts;
97
+ }
98
+ async function readPayload() {
99
+ const mod = await resolveModule();
100
+ const result = await mod.getGenericPassword(buildOptions(mod));
101
+ if (result === false)
102
+ return null;
103
+ try {
104
+ return JSON.parse(result.password);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ async function writePayload(payload) {
111
+ const mod = await resolveModule();
112
+ if (payload === null) {
113
+ await mod.resetGenericPassword(buildOptions(mod));
114
+ return;
115
+ }
116
+ await mod.setGenericPassword(USERNAME_SLOT, JSON.stringify(payload), buildOptions(mod));
117
+ }
118
+ return {
119
+ async getToken() {
120
+ const payload = await readPayload();
121
+ return payload?.token ?? null;
122
+ },
123
+ async setToken(token) {
124
+ if (token === null) {
125
+ await writePayload(null);
126
+ return;
127
+ }
128
+ await writePayload({ token, expiresAt: null });
129
+ },
130
+ async clearToken() {
131
+ await writePayload(null);
132
+ },
133
+ async setTokenEntry(token, expiresAt) {
134
+ if (token === null) {
135
+ await writePayload(null);
136
+ return;
137
+ }
138
+ await writePayload({ token, expiresAt: expiresAt ?? null });
139
+ },
140
+ async getTokenEntry() {
141
+ const payload = await readPayload();
142
+ if (payload === null)
143
+ return { token: null };
144
+ return { token: payload.token, expiresAt: payload.expiresAt ?? undefined };
145
+ },
146
+ };
147
+ }
@@ -18,6 +18,8 @@ export declare class SessionManager {
18
18
  /**
19
19
  * Perform a login and, when `persistToken` is not false, automatically
20
20
  * persist the returned token into the configured token store.
21
+ * The `expiresAt` from the login response is also persisted when the store
22
+ * supports `setTokenEntry`.
21
23
  */
22
24
  login(input: GoodVibesLoginInput, options?: GoodVibesAuthLoginOptions): Promise<GoodVibesLoginOutput>;
23
25
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/auth/session-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EACV,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,qBAAa,cAAc;;gBAIb,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAKhE;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAI9C;;;OAGG;IACG,KAAK,CACT,KAAK,EAAE,mBAAmB,EAC1B,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,oBAAoB,CAAC;IAQhC;;;OAGG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,+EAA+E;IAC/E,IAAI,UAAU,IAAI,UAAU,GAAG,IAAI,CAElC;CACF"}
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/auth/session-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EACV,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,qBAAa,cAAc;;gBAIb,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAKhE;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAI9C;;;;;OAKG;IACG,KAAK,CACT,KAAK,EAAE,mBAAmB,EAC1B,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,oBAAoB,CAAC;IAahC;;;OAGG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,+EAA+E;IAC/E,IAAI,UAAU,IAAI,UAAU,GAAG,IAAI,CAElC;CACF"}
@@ -22,11 +22,19 @@ export class SessionManager {
22
22
  /**
23
23
  * Perform a login and, when `persistToken` is not false, automatically
24
24
  * persist the returned token into the configured token store.
25
+ * The `expiresAt` from the login response is also persisted when the store
26
+ * supports `setTokenEntry`.
25
27
  */
26
28
  async login(input, options = {}) {
27
29
  const result = await this.#operator.control.auth.login(input);
28
30
  if ((options.persistToken ?? true) && this.#tokenStore) {
29
- await this.#tokenStore.setToken(result.token);
31
+ // Prefer setTokenEntry to persist expiry alongside the token.
32
+ if (result.expiresAt) {
33
+ await this.#tokenStore.setTokenEntry(result.token, result.expiresAt);
34
+ }
35
+ else {
36
+ await this.#tokenStore.setToken(result.token);
37
+ }
30
38
  }
31
39
  return result;
32
40
  }
@@ -13,6 +13,19 @@ export declare class TokenStore {
13
13
  getToken(): Promise<string | null>;
14
14
  /** Persist a new token, or clear storage when null. */
15
15
  setToken(token: string | null): Promise<void>;
16
+ /**
17
+ * Persist a new token alongside its expiry timestamp (unix ms).
18
+ * Falls back to `setToken` when the store does not implement `setTokenEntry`.
19
+ */
20
+ setTokenEntry(token: string | null, expiresAt?: number): Promise<void>;
21
+ /**
22
+ * Return the current token entry, including optional expiry.
23
+ * Falls back to token-only when the store does not implement `getTokenEntry`.
24
+ */
25
+ getTokenEntry(): Promise<{
26
+ token: string | null;
27
+ expiresAt?: number;
28
+ }>;
16
29
  /** Clear the stored token. */
17
30
  clearToken(): Promise<void>;
18
31
  /** Return true when a non-empty token is currently stored. */
@@ -1 +1 @@
1
- {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/auth/token-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,qBAAa,UAAU;;gBAGT,KAAK,EAAE,mBAAmB;IAItC,2DAA2D;IACrD,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIxC,uDAAuD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,8BAA8B;IACxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC,8DAA8D;IACxD,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAKlC,uEAAuE;IACvE,IAAI,KAAK,IAAI,mBAAmB,CAE/B;CACF"}
1
+ {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/auth/token-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,qBAAa,UAAU;;gBAGT,KAAK,EAAE,mBAAmB;IAItC,2DAA2D;IACrD,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIxC,uDAAuD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD;;;OAGG;IACG,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5E;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAW5E,8BAA8B;IACxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC,8DAA8D;IACxD,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAKlC,uEAAuE;IACvE,IAAI,KAAK,IAAI,mBAAmB,CAE/B;CACF"}
@@ -18,6 +18,29 @@ export class TokenStore {
18
18
  async setToken(token) {
19
19
  return this.#store.setToken(token);
20
20
  }
21
+ /**
22
+ * Persist a new token alongside its expiry timestamp (unix ms).
23
+ * Falls back to `setToken` when the store does not implement `setTokenEntry`.
24
+ */
25
+ async setTokenEntry(token, expiresAt) {
26
+ const store = this.#store;
27
+ if (typeof store.setTokenEntry === 'function') {
28
+ return store.setTokenEntry(token, expiresAt);
29
+ }
30
+ return this.#store.setToken(token);
31
+ }
32
+ /**
33
+ * Return the current token entry, including optional expiry.
34
+ * Falls back to token-only when the store does not implement `getTokenEntry`.
35
+ */
36
+ async getTokenEntry() {
37
+ const store = this.#store;
38
+ if (typeof store.getTokenEntry === 'function') {
39
+ return store.getTokenEntry();
40
+ }
41
+ const token = await this.#store.getToken();
42
+ return { token };
43
+ }
21
44
  /** Clear the stored token. */
22
45
  async clearToken() {
23
46
  return this.#store.clearToken();
@@ -1,26 +1,27 @@
1
1
  /**
2
2
  * companion-chat-manager.ts
3
3
  *
4
- * In-memory manager for companion-app chat-mode sessions.
4
+ * Disk-backed manager for companion-app chat-mode sessions.
5
5
  *
6
6
  * Design:
7
7
  * - Each session owns a ConversationManager (isolated message history).
8
+ * - Sessions survive daemon restart via CompanionChatPersistence (atomic JSON files).
9
+ * - Inbound messages are rate-limited per session and per client via
10
+ * CompanionChatRateLimiter (token-bucket, 30 msgs/min per client,
11
+ * 10 msgs/min per session by default, configurable).
8
12
  * - When a user message is posted, the manager appends it to the conversation
9
13
  * and runs a lightweight LLM turn using the provider registry.
14
+ * - Tool calls emitted by the LLM are executed via the injected ToolRegistry
15
+ * (if provided); results are fed back into the stream and published as
16
+ * turn.tool_result events.
10
17
  * - Streaming chunks are fanned out via ControlPlaneGateway.publishEvent
11
18
  * with a per-session clientId filter, so they only reach the subscriber
12
19
  * for that specific session — never the global TUI event feed.
13
20
  * - A GC sweep closes sessions that have been idle beyond the TTL.
14
- *
15
- * TODO (follow-up): persist sessions across daemon restart.
16
- * TODO (follow-up): rate-limiting per session / per client.
17
- * TODO (follow-up): tool-call execution requires ToolRegistry injection;
18
- * currently tools are passed through the Orchestrator which needs the
19
- * full TUI context. For v1, we provide a no-op tool registry so tool
20
- * calls degrade gracefully. Proper tool support requires the daemon to
21
- * inject its ToolRegistry into CompanionChatManager.
22
21
  */
23
22
  import type { CompanionChatMessage, CompanionChatSession, CreateCompanionChatSessionInput } from './companion-chat-types.js';
23
+ import type { CompanionChatRateLimiterOptions } from './companion-chat-rate-limiter.js';
24
+ import type { ToolRegistry } from '../tools/registry.js';
24
25
  export interface CompanionProviderMessage {
25
26
  readonly role: 'user' | 'assistant';
26
27
  readonly content: string;
@@ -52,6 +53,24 @@ export interface CompanionChatEventPublisher {
52
53
  export interface CompanionChatManagerConfig {
53
54
  readonly provider: CompanionLLMProvider;
54
55
  readonly eventPublisher: CompanionChatEventPublisher;
56
+ /**
57
+ * ToolRegistry to use for executing tool calls emitted by the LLM.
58
+ * When omitted, tool_call chunks are published as events but not executed;
59
+ * the LLM receives no tool result and must degrade gracefully.
60
+ */
61
+ readonly toolRegistry?: ToolRegistry;
62
+ /**
63
+ * Directory under which session JSON files are persisted.
64
+ * Default: ~/.goodvibes/companion-chat/sessions/
65
+ */
66
+ readonly sessionsDir?: string;
67
+ /**
68
+ * Pass `false` to disable disk persistence entirely (useful in tests).
69
+ * Default: true
70
+ */
71
+ readonly persist?: boolean;
72
+ /** Rate-limiting options. Defaults: 30 msgs/min per client, 10/min per session. */
73
+ readonly rateLimiter?: CompanionChatRateLimiterOptions | false;
55
74
  /** Override for tests */
56
75
  readonly idleActiveMs?: number;
57
76
  /** Override for tests */
@@ -63,10 +82,27 @@ export declare class CompanionChatManager {
63
82
  private readonly sessions;
64
83
  private readonly provider;
65
84
  private readonly eventPublisher;
85
+ private readonly toolRegistry;
86
+ private readonly persistence;
87
+ private readonly rateLimiter;
66
88
  private readonly idleActiveMs;
67
89
  private readonly idleEmptyMs;
68
90
  private gcTimer;
91
+ /** Tracks whether the async init() has completed. */
92
+ private initCompleted;
93
+ /**
94
+ * Serializes persistence writes per session to prevent write-after-write
95
+ * races where two concurrent saves could result in an older snapshot
96
+ * overwriting a newer one.
97
+ */
98
+ private readonly _pendingSaves;
69
99
  constructor(config: CompanionChatManagerConfig);
100
+ /**
101
+ * Load sessions persisted from a previous daemon run.
102
+ * Should be called once after construction before accepting requests.
103
+ * Safe to call multiple times (idempotent after first call).
104
+ */
105
+ init(): Promise<void>;
70
106
  createSession(input?: CreateCompanionChatSessionInput): CompanionChatSession;
71
107
  getSession(sessionId: string): CompanionChatSession | null;
72
108
  getMessages(sessionId: string): CompanionChatMessage[];
@@ -83,12 +119,29 @@ export declare class CompanionChatManager {
83
119
  closeSession(sessionId: string): CompanionChatSession | null;
84
120
  /**
85
121
  * Post a user message and start an async LLM turn. Returns the messageId.
86
- * Throws if the session is closed.
122
+ *
123
+ * Rate-limited per session and per client (throws GoodVibesSdkError{kind:'rate-limit'}
124
+ * if limits are exceeded).
125
+ *
126
+ * Throws if the session is closed or not found.
127
+ *
128
+ * @param sessionId - The session to post to.
129
+ * @param content - The message text.
130
+ * @param clientId - The SSE/HTTP client identity for per-client rate limiting.
131
+ * Pass '' to skip client-level rate limiting.
87
132
  */
88
- postMessage(sessionId: string, content: string): Promise<string>;
133
+ postMessage(sessionId: string, content: string, clientId?: string): Promise<string>;
89
134
  dispose(): void;
90
135
  private _runTurn;
91
136
  _gcSweep(): void;
92
137
  private _updateMeta;
138
+ /**
139
+ * Schedule a persistence save for the given session.
140
+ * Saves are serialized per-session: each new save waits for the prior one to
141
+ * complete before writing. The save always reads the CURRENT session state,
142
+ * so rapid create→update→close sequences correctly persist the final state.
143
+ */
144
+ private _persist;
145
+ private _doSave;
93
146
  }
94
147
  //# sourceMappingURL=companion-chat-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"companion-chat-manager.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/companion/companion-chat-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EAIpB,+BAA+B,EAChC,MAAM,2BAA2B,CAAC;AAMnC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,GAAG,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;IAC7E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,UAAU,CACR,QAAQ,EAAE,wBAAwB,EAAE,EACpC,OAAO,EAAE;QACP,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;KACpC,GACA,aAAa,CAAC,sBAAsB,CAAC,CAAC;CAC1C;AAMD,MAAM,WAAW,2BAA2B;IAC1C,YAAY,CACV,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAC7B,IAAI,CAAC;CACT;AAgCD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,QAAQ,CAAC,cAAc,EAAE,2BAA2B,CAAC;IACrD,yBAAyB;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,yBAAyB;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,yBAAyB;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8B;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,OAAO,CAA+C;gBAElD,MAAM,EAAE,0BAA0B;IAgB9C,aAAa,CAAC,KAAK,GAAE,+BAAoC,GAAG,oBAAoB;IA+BhF,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI;IAI1D,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAItD;;;;OAIG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQ7D;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI;IAW5D;;;OAGG;IACG,WAAW,CACf,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC;IAkClB,OAAO,IAAI,IAAI;YAgBD,QAAQ;IA0HtB,QAAQ,IAAI,IAAI;IA2BhB,OAAO,CAAC,WAAW;CAQpB"}
1
+ {"version":3,"file":"companion-chat-manager.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/companion/companion-chat-manager.ts"],"names":[],"mappings":"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"}
@@ -1,27 +1,28 @@
1
1
  /**
2
2
  * companion-chat-manager.ts
3
3
  *
4
- * In-memory manager for companion-app chat-mode sessions.
4
+ * Disk-backed manager for companion-app chat-mode sessions.
5
5
  *
6
6
  * Design:
7
7
  * - Each session owns a ConversationManager (isolated message history).
8
+ * - Sessions survive daemon restart via CompanionChatPersistence (atomic JSON files).
9
+ * - Inbound messages are rate-limited per session and per client via
10
+ * CompanionChatRateLimiter (token-bucket, 30 msgs/min per client,
11
+ * 10 msgs/min per session by default, configurable).
8
12
  * - When a user message is posted, the manager appends it to the conversation
9
13
  * and runs a lightweight LLM turn using the provider registry.
14
+ * - Tool calls emitted by the LLM are executed via the injected ToolRegistry
15
+ * (if provided); results are fed back into the stream and published as
16
+ * turn.tool_result events.
10
17
  * - Streaming chunks are fanned out via ControlPlaneGateway.publishEvent
11
18
  * with a per-session clientId filter, so they only reach the subscriber
12
19
  * for that specific session — never the global TUI event feed.
13
20
  * - A GC sweep closes sessions that have been idle beyond the TTL.
14
- *
15
- * TODO (follow-up): persist sessions across daemon restart.
16
- * TODO (follow-up): rate-limiting per session / per client.
17
- * TODO (follow-up): tool-call execution requires ToolRegistry injection;
18
- * currently tools are passed through the Orchestrator which needs the
19
- * full TUI context. For v1, we provide a no-op tool registry so tool
20
- * calls degrade gracefully. Proper tool support requires the daemon to
21
- * inject its ToolRegistry into CompanionChatManager.
22
21
  */
23
22
  import { randomUUID } from 'node:crypto';
24
23
  import { ConversationManager } from '../core/conversation.js';
24
+ import { CompanionChatPersistence, defaultSessionsDir, } from './companion-chat-persistence.js';
25
+ import { CompanionChatRateLimiter } from './companion-chat-rate-limiter.js';
25
26
  // ---------------------------------------------------------------------------
26
27
  // Idle GC constants (customisable for tests)
27
28
  // ---------------------------------------------------------------------------
@@ -32,20 +33,88 @@ export class CompanionChatManager {
32
33
  sessions = new Map();
33
34
  provider;
34
35
  eventPublisher;
36
+ toolRegistry;
37
+ persistence;
38
+ rateLimiter;
35
39
  idleActiveMs;
36
40
  idleEmptyMs;
37
41
  gcTimer = null;
42
+ /** Tracks whether the async init() has completed. */
43
+ initCompleted = false;
44
+ /**
45
+ * Serializes persistence writes per session to prevent write-after-write
46
+ * races where two concurrent saves could result in an older snapshot
47
+ * overwriting a newer one.
48
+ */
49
+ _pendingSaves = new Map();
38
50
  constructor(config) {
39
51
  this.provider = config.provider;
40
52
  this.eventPublisher = config.eventPublisher;
53
+ this.toolRegistry = config.toolRegistry ?? null;
41
54
  this.idleActiveMs = config.idleActiveMs ?? DEFAULT_IDLE_ACTIVE_MS;
42
55
  this.idleEmptyMs = config.idleEmptyMs ?? DEFAULT_IDLE_EMPTY_MS;
56
+ // Persistence
57
+ // Default is false — most callers (tests, downstream consumers) get the
58
+ // safe no-write default. The daemon opts into persistence explicitly via
59
+ // persist: true in facade-composition.
60
+ const persist = config.persist === true;
61
+ this.persistence = persist
62
+ ? new CompanionChatPersistence(config.sessionsDir ?? defaultSessionsDir())
63
+ : null;
64
+ // Rate limiter
65
+ this.rateLimiter =
66
+ config.rateLimiter === false
67
+ ? null
68
+ : new CompanionChatRateLimiter(config.rateLimiter ?? {});
43
69
  const gcIntervalMs = config.gcIntervalMs ?? GC_INTERVAL_MS;
44
- this.gcTimer = setInterval(() => this._gcSweep(), gcIntervalMs);
70
+ this.gcTimer = setInterval(() => {
71
+ this._gcSweep();
72
+ this.rateLimiter?.cleanup();
73
+ }, gcIntervalMs);
45
74
  // Don't block node process on this timer
46
75
  this.gcTimer.unref?.();
47
76
  }
48
77
  // ---------------------------------------------------------------------------
78
+ // Async initialisation — load persisted sessions from disk
79
+ // ---------------------------------------------------------------------------
80
+ /**
81
+ * Load sessions persisted from a previous daemon run.
82
+ * Should be called once after construction before accepting requests.
83
+ * Safe to call multiple times (idempotent after first call).
84
+ */
85
+ async init() {
86
+ if (this.initCompleted || !this.persistence) {
87
+ this.initCompleted = true;
88
+ return;
89
+ }
90
+ const stored = await this.persistence.loadAll();
91
+ for (const { meta, messages } of stored) {
92
+ // Skip sessions that were already closed before the restart — they are
93
+ // in a terminal state and don't need to be in memory.
94
+ if (meta.status === 'closed')
95
+ continue;
96
+ const conversation = new ConversationManager();
97
+ // Replay messages into the conversation to restore LLM context
98
+ for (const msg of messages) {
99
+ if (msg.role === 'user') {
100
+ conversation.addUserMessage(msg.content);
101
+ }
102
+ else {
103
+ conversation.addAssistantMessage(msg.content);
104
+ }
105
+ }
106
+ this.sessions.set(meta.id, {
107
+ meta,
108
+ conversation,
109
+ messages: [...messages],
110
+ abortController: new AbortController(),
111
+ lastActivityAt: meta.updatedAt,
112
+ subscriberClientId: null,
113
+ });
114
+ }
115
+ this.initCompleted = true;
116
+ }
117
+ // ---------------------------------------------------------------------------
49
118
  // Session lifecycle
50
119
  // ---------------------------------------------------------------------------
51
120
  createSession(input = {}) {
@@ -73,6 +142,8 @@ export class CompanionChatManager {
73
142
  lastActivityAt: now,
74
143
  subscriberClientId: null,
75
144
  });
145
+ // Persist async (non-blocking)
146
+ void this._persist(id);
76
147
  return meta;
77
148
  }
78
149
  getSession(sessionId) {
@@ -106,13 +177,24 @@ export class CompanionChatManager {
106
177
  session.abortController.abort();
107
178
  const now = Date.now();
108
179
  const updated = this._updateMeta(session, { status: 'closed', closedAt: now, updatedAt: now });
180
+ // Persist the closed state async (non-blocking)
181
+ void this._persist(sessionId);
109
182
  return updated;
110
183
  }
111
184
  /**
112
185
  * Post a user message and start an async LLM turn. Returns the messageId.
113
- * Throws if the session is closed.
186
+ *
187
+ * Rate-limited per session and per client (throws GoodVibesSdkError{kind:'rate-limit'}
188
+ * if limits are exceeded).
189
+ *
190
+ * Throws if the session is closed or not found.
191
+ *
192
+ * @param sessionId - The session to post to.
193
+ * @param content - The message text.
194
+ * @param clientId - The SSE/HTTP client identity for per-client rate limiting.
195
+ * Pass '' to skip client-level rate limiting.
114
196
  */
115
- async postMessage(sessionId, content) {
197
+ async postMessage(sessionId, content, clientId = '') {
116
198
  const session = this.sessions.get(sessionId);
117
199
  if (!session) {
118
200
  throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: 'SESSION_NOT_FOUND', status: 404 });
@@ -120,6 +202,8 @@ export class CompanionChatManager {
120
202
  if (session.meta.status === 'closed') {
121
203
  throw Object.assign(new Error(`Session is closed: ${sessionId}`), { code: 'SESSION_CLOSED', status: 409 });
122
204
  }
205
+ // Rate-limit check (throws GoodVibesSdkError on violation)
206
+ this.rateLimiter?.check(sessionId, clientId);
123
207
  const messageId = randomUUID();
124
208
  const now = Date.now();
125
209
  const userMsg = {
@@ -136,6 +220,8 @@ export class CompanionChatManager {
136
220
  messageCount: session.messages.length,
137
221
  updatedAt: now,
138
222
  });
223
+ // Persist async (non-blocking)
224
+ void this._persist(sessionId);
139
225
  // Fire-and-forget: run the turn without blocking the HTTP response
140
226
  void this._runTurn(session, messageId);
141
227
  return messageId;
@@ -205,6 +291,36 @@ export class CompanionChatManager {
205
291
  toolName: chunk.toolName ?? '',
206
292
  toolInput: chunk.toolInput ?? null,
207
293
  });
294
+ // Execute via ToolRegistry if available
295
+ if (this.toolRegistry && chunk.toolName && chunk.toolCallId) {
296
+ const toolCallId = chunk.toolCallId;
297
+ const toolName = chunk.toolName;
298
+ const toolInput = (chunk.toolInput ?? {});
299
+ try {
300
+ const toolResult = await this.toolRegistry.execute(toolCallId, toolName, toolInput);
301
+ publish({
302
+ type: 'turn.tool_result',
303
+ sessionId,
304
+ turnId,
305
+ toolCallId,
306
+ toolName,
307
+ result: toolResult.output ?? null,
308
+ isError: !toolResult.success,
309
+ });
310
+ }
311
+ catch (toolErr) {
312
+ const errMsg = toolErr instanceof Error ? toolErr.message : String(toolErr);
313
+ publish({
314
+ type: 'turn.tool_result',
315
+ sessionId,
316
+ turnId,
317
+ toolCallId,
318
+ toolName,
319
+ result: errMsg,
320
+ isError: true,
321
+ });
322
+ }
323
+ }
208
324
  break;
209
325
  }
210
326
  case 'tool_result': {
@@ -241,6 +357,8 @@ export class CompanionChatManager {
241
357
  session.messages.push(assistantMsg);
242
358
  session.lastActivityAt = now;
243
359
  this._updateMeta(session, { messageCount: session.messages.length, updatedAt: now });
360
+ // Persist assistant reply
361
+ void this._persist(sessionId);
244
362
  const completedEnvelope = {
245
363
  sessionId,
246
364
  messageId: assistantMessageId,
@@ -266,6 +384,7 @@ export class CompanionChatManager {
266
384
  if (session.meta.status === 'closed') {
267
385
  // Remove already-closed sessions after a short grace period (5 min)
268
386
  if (now - (session.meta.closedAt ?? now) > 5 * 60_000) {
387
+ void this.persistence?.delete(id);
269
388
  this.sessions.delete(id);
270
389
  }
271
390
  continue;
@@ -277,6 +396,7 @@ export class CompanionChatManager {
277
396
  // Close via GC
278
397
  session.abortController.abort();
279
398
  this._updateMeta(session, { status: 'closed', closedAt: now, updatedAt: now });
399
+ void this._persist(id);
280
400
  }
281
401
  }
282
402
  }
@@ -288,4 +408,30 @@ export class CompanionChatManager {
288
408
  session.meta = updated;
289
409
  return updated;
290
410
  }
411
+ /**
412
+ * Schedule a persistence save for the given session.
413
+ * Saves are serialized per-session: each new save waits for the prior one to
414
+ * complete before writing. The save always reads the CURRENT session state,
415
+ * so rapid create→update→close sequences correctly persist the final state.
416
+ */
417
+ _persist(sessionId) {
418
+ if (!this.persistence)
419
+ return;
420
+ const prior = this._pendingSaves.get(sessionId) ?? Promise.resolve();
421
+ const next = prior.then(() => this._doSave(sessionId));
422
+ this._pendingSaves.set(sessionId, next.finally(() => {
423
+ // Clean up the slot once this save is the settled head.
424
+ if (this._pendingSaves.get(sessionId) === next) {
425
+ this._pendingSaves.delete(sessionId);
426
+ }
427
+ }));
428
+ }
429
+ async _doSave(sessionId) {
430
+ if (!this.persistence)
431
+ return;
432
+ const session = this.sessions.get(sessionId);
433
+ if (!session)
434
+ return;
435
+ await this.persistence.save({ meta: session.meta, messages: session.messages });
436
+ }
291
437
  }