@pellux/goodvibes-sdk 0.19.6 → 0.19.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/contracts/index.d.ts +1 -0
- package/dist/_internal/contracts/index.d.ts.map +1 -1
- package/dist/_internal/contracts/index.js +2 -0
- package/dist/_internal/contracts/types.d.ts +4 -0
- package/dist/_internal/contracts/types.d.ts.map +1 -1
- package/dist/_internal/contracts/zod-schemas/accounts.d.ts +81 -0
- package/dist/_internal/contracts/zod-schemas/accounts.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/accounts.js +47 -0
- package/dist/_internal/contracts/zod-schemas/auth.d.ts +42 -0
- package/dist/_internal/contracts/zod-schemas/auth.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/auth.js +29 -0
- package/dist/_internal/contracts/zod-schemas/events.d.ts +37 -0
- package/dist/_internal/contracts/zod-schemas/events.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/events.js +26 -0
- package/dist/_internal/contracts/zod-schemas/index.d.ts +9 -0
- package/dist/_internal/contracts/zod-schemas/index.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/index.js +4 -0
- package/dist/_internal/contracts/zod-schemas/session.d.ts +22 -0
- package/dist/_internal/contracts/zod-schemas/session.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/session.js +19 -0
- package/dist/_internal/daemon/api-router.d.ts.map +1 -1
- package/dist/_internal/daemon/api-router.js +0 -1
- package/dist/_internal/daemon/automation.d.ts.map +1 -1
- package/dist/_internal/daemon/channel-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/channel-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/context.d.ts.map +1 -1
- package/dist/_internal/daemon/control-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/http-policy.d.ts.map +1 -1
- package/dist/_internal/daemon/http-policy.js +0 -1
- package/dist/_internal/daemon/integration-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/integration-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/knowledge-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/knowledge-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/knowledge-routes.js +5 -4
- package/dist/_internal/daemon/media-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/media-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/operator.d.ts.map +1 -1
- package/dist/_internal/daemon/remote-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/remote.d.ts.map +1 -1
- package/dist/_internal/daemon/route-helpers.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-automation-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-route-types.d.ts +14 -1
- package/dist/_internal/daemon/runtime-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-session-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-session-routes.js +0 -2
- package/dist/_internal/daemon/sessions.d.ts.map +1 -1
- package/dist/_internal/daemon/system-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/system-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/tasks.d.ts.map +1 -1
- package/dist/_internal/daemon/telemetry-routes.d.ts.map +1 -1
- package/dist/_internal/errors/daemon-error-contract.d.ts.map +1 -1
- package/dist/_internal/errors/index.d.ts +2 -2
- package/dist/_internal/errors/index.js +2 -2
- package/dist/_internal/operator/client-core.d.ts.map +1 -1
- package/dist/_internal/operator/client-core.js +8 -2
- package/dist/_internal/operator/client.d.ts +7 -0
- package/dist/_internal/operator/client.d.ts.map +1 -1
- package/dist/_internal/operator/client.js +32 -1
- package/dist/_internal/peer/client-core.d.ts.map +1 -1
- package/dist/_internal/platform/agents/orchestrator.d.ts +7 -0
- package/dist/_internal/platform/agents/orchestrator.d.ts.map +1 -1
- package/dist/_internal/platform/agents/orchestrator.js +8 -0
- package/dist/_internal/platform/auth/android-keystore-token-store.d.ts +110 -0
- package/dist/_internal/platform/auth/android-keystore-token-store.d.ts.map +1 -0
- package/dist/_internal/platform/auth/android-keystore-token-store.js +164 -0
- package/dist/_internal/platform/auth/auto-refresh-middleware.d.ts +46 -0
- package/dist/_internal/platform/auth/auto-refresh-middleware.d.ts.map +1 -0
- package/dist/_internal/platform/auth/auto-refresh-middleware.js +155 -0
- package/dist/_internal/platform/auth/auto-refresh.d.ts +123 -0
- package/dist/_internal/platform/auth/auto-refresh.d.ts.map +1 -0
- package/dist/_internal/platform/auth/auto-refresh.js +236 -0
- package/dist/_internal/platform/auth/expo-secure-token-store.d.ts +82 -0
- package/dist/_internal/platform/auth/expo-secure-token-store.d.ts.map +1 -0
- package/dist/_internal/platform/auth/expo-secure-token-store.js +135 -0
- package/dist/_internal/platform/auth/index.d.ts +3 -0
- package/dist/_internal/platform/auth/index.d.ts.map +1 -1
- package/dist/_internal/platform/auth/index.js +2 -0
- package/dist/_internal/platform/auth/ios-keychain-token-store.d.ts +88 -0
- package/dist/_internal/platform/auth/ios-keychain-token-store.d.ts.map +1 -0
- package/dist/_internal/platform/auth/ios-keychain-token-store.js +147 -0
- package/dist/_internal/platform/auth/session-manager.d.ts +2 -0
- package/dist/_internal/platform/auth/session-manager.d.ts.map +1 -1
- package/dist/_internal/platform/auth/session-manager.js +9 -1
- package/dist/_internal/platform/auth/token-store.d.ts +13 -0
- package/dist/_internal/platform/auth/token-store.d.ts.map +1 -1
- package/dist/_internal/platform/auth/token-store.js +23 -0
- package/dist/_internal/platform/companion/companion-chat-manager.d.ts +64 -11
- package/dist/_internal/platform/companion/companion-chat-manager.d.ts.map +1 -1
- package/dist/_internal/platform/companion/companion-chat-manager.js +158 -12
- package/dist/_internal/platform/companion/companion-chat-persistence.d.ts +33 -0
- package/dist/_internal/platform/companion/companion-chat-persistence.d.ts.map +1 -0
- package/dist/_internal/platform/companion/companion-chat-persistence.js +115 -0
- package/dist/_internal/platform/companion/companion-chat-rate-limiter.d.ts +47 -0
- package/dist/_internal/platform/companion/companion-chat-rate-limiter.d.ts.map +1 -0
- package/dist/_internal/platform/companion/companion-chat-rate-limiter.js +117 -0
- package/dist/_internal/platform/companion/companion-chat-types.d.ts +2 -4
- package/dist/_internal/platform/companion/companion-chat-types.d.ts.map +1 -1
- package/dist/_internal/platform/companion/companion-chat-types.js +2 -4
- package/dist/_internal/platform/companion/index.d.ts +4 -0
- package/dist/_internal/platform/companion/index.d.ts.map +1 -1
- package/dist/_internal/platform/companion/index.js +2 -0
- package/dist/_internal/platform/daemon/facade-composition.d.ts.map +1 -1
- package/dist/_internal/platform/daemon/facade-composition.js +5 -0
- package/dist/_internal/platform/daemon/facade.d.ts.map +1 -1
- package/dist/_internal/platform/daemon/facade.js +3 -0
- package/dist/_internal/platform/daemon/http/runtime-route-types.d.ts +0 -7
- package/dist/_internal/platform/daemon/http/runtime-route-types.d.ts.map +1 -1
- package/dist/_internal/platform/state/db.d.ts.map +1 -1
- package/dist/_internal/platform/state/db.js +0 -1
- package/dist/_internal/platform/state/sqlite-store.d.ts.map +1 -1
- package/dist/_internal/platform/state/sqlite-store.js +0 -1
- package/dist/_internal/platform/version.js +1 -1
- package/dist/_internal/transport-core/client-transport.d.ts.map +1 -1
- package/dist/_internal/transport-core/event-envelope.d.ts.map +1 -1
- package/dist/_internal/transport-core/event-feeds.d.ts.map +1 -1
- package/dist/_internal/transport-core/index.d.ts +5 -0
- package/dist/_internal/transport-core/index.d.ts.map +1 -1
- package/dist/_internal/transport-core/index.js +3 -0
- package/dist/_internal/transport-core/middleware.d.ts +76 -0
- package/dist/_internal/transport-core/middleware.d.ts.map +1 -0
- package/dist/_internal/transport-core/middleware.js +67 -0
- package/dist/_internal/transport-core/observer.d.ts +53 -0
- package/dist/_internal/transport-core/observer.d.ts.map +1 -0
- package/dist/_internal/transport-core/observer.js +26 -0
- package/dist/_internal/transport-core/otel.d.ts +64 -0
- package/dist/_internal/transport-core/otel.d.ts.map +1 -0
- package/dist/_internal/transport-core/otel.js +149 -0
- package/dist/_internal/transport-direct/index.d.ts.map +1 -1
- package/dist/_internal/transport-direct/index.js +0 -1
- package/dist/_internal/transport-http/contract-client.d.ts +11 -1
- package/dist/_internal/transport-http/contract-client.d.ts.map +1 -1
- package/dist/_internal/transport-http/contract-client.js +18 -4
- package/dist/_internal/transport-http/http-core.d.ts +27 -1
- package/dist/_internal/transport-http/http-core.d.ts.map +1 -1
- package/dist/_internal/transport-http/http-core.js +180 -12
- package/dist/_internal/transport-http/http.d.ts +3 -3
- package/dist/_internal/transport-http/http.d.ts.map +1 -1
- package/dist/_internal/transport-http/http.js +2 -2
- package/dist/_internal/transport-http/index.d.ts +4 -2
- package/dist/_internal/transport-http/index.d.ts.map +1 -1
- package/dist/_internal/transport-http/index.js +2 -1
- package/dist/_internal/transport-http/paths.js +1 -1
- package/dist/_internal/transport-http/reconnect.d.ts +2 -0
- package/dist/_internal/transport-http/reconnect.d.ts.map +1 -1
- package/dist/_internal/transport-http/reconnect.js +4 -2
- package/dist/_internal/transport-http/retry.d.ts +15 -0
- package/dist/_internal/transport-http/retry.d.ts.map +1 -1
- package/dist/_internal/transport-http/retry.js +19 -0
- package/dist/_internal/transport-realtime/domain-events.d.ts.map +1 -1
- package/dist/_internal/transport-realtime/runtime-events.d.ts +10 -3
- package/dist/_internal/transport-realtime/runtime-events.d.ts.map +1 -1
- package/dist/_internal/transport-realtime/runtime-events.js +73 -8
- package/dist/auth.d.ts +38 -3
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +68 -3
- package/dist/client.d.ts +61 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +64 -3
- package/dist/expo.d.ts +1 -0
- package/dist/expo.d.ts.map +1 -1
- package/dist/expo.js +1 -0
- package/dist/observer/index.d.ts +16 -25
- package/dist/observer/index.d.ts.map +1 -1
- package/dist/platform/runtime/transports/http.js +1 -1
- package/dist/react-native.d.ts +2 -0
- package/dist/react-native.d.ts.map +1 -1
- package/dist/react-native.js +2 -0
- 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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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(() =>
|
|
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
|
-
*
|
|
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
|
}
|