fraim-hub 2.0.236 → 2.0.237
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/src/ai-hub/server.js +30 -11
- package/package.json +2 -2
- package/public/ai-hub/script.js +47 -0
|
@@ -1848,12 +1848,18 @@ class AiHubServer {
|
|
|
1848
1848
|
console.warn('[ai-hub] manager-team lookup failed:', err?.message || err);
|
|
1849
1849
|
return [];
|
|
1850
1850
|
});
|
|
1851
|
-
const personaProjectionPromise = this.computePersonas(resolvedApiKey, managerTeamPromise);
|
|
1852
|
-
const fallbackPersonaProjection = this.fallbackPersonaProjection(resolvedApiKey);
|
|
1851
|
+
const personaProjectionPromise = this.computePersonas(resolvedApiKey, managerTeamPromise, normalizedProjectPath);
|
|
1852
|
+
const fallbackPersonaProjection = this.fallbackPersonaProjection(resolvedApiKey, normalizedProjectPath);
|
|
1853
1853
|
const personaProjection = await this.withFirstPaintBudget(personaProjectionPromise, fallbackPersonaProjection, 'persona projection');
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1854
|
+
// Issue #1005: identity — not persona contents — is what distinguishes a first-paint
|
|
1855
|
+
// placeholder from an answer. withFirstPaintBudget resolves to this exact object only
|
|
1856
|
+
// on timeout; computePersonas returns a fresh `{ ...fallbackProjection, userKey }` even
|
|
1857
|
+
// when the authority definitively locks everything (401 / feature-off). So a real
|
|
1858
|
+
// sign-out is still `resolved`, and only a budget miss is `unresolved`.
|
|
1859
|
+
const personasResolved = personaProjection !== fallbackPersonaProjection;
|
|
1860
|
+
const managerTeam = personasResolved
|
|
1861
|
+
? await this.withFirstPaintBudget(managerTeamPromise, [], 'manager team')
|
|
1862
|
+
: [];
|
|
1857
1863
|
const { personas, subscriptionActive, workspaceId, userKey } = personaProjection;
|
|
1858
1864
|
void personaProjectionPromise.catch(() => undefined);
|
|
1859
1865
|
void managerTeamPromise.catch(() => undefined);
|
|
@@ -1888,6 +1894,9 @@ class AiHubServer {
|
|
|
1888
1894
|
employees,
|
|
1889
1895
|
configuredAgents,
|
|
1890
1896
|
personas,
|
|
1897
|
+
// Issue #1005: lets the client tell a resolved projection from the first-paint
|
|
1898
|
+
// placeholder, so a late unresolved bootstrap cannot wipe a resolved roster.
|
|
1899
|
+
personasResolved,
|
|
1891
1900
|
subscriptionActive,
|
|
1892
1901
|
activeRun,
|
|
1893
1902
|
projects,
|
|
@@ -2597,9 +2606,15 @@ class AiHubServer {
|
|
|
2597
2606
|
const message = (invocationForm ?? (0, manager_turns_1.buildSameJobContinueMessage)(userText)) + (0, manager_turns_1.buildCommunicationStyleNote)();
|
|
2598
2607
|
return { message, display };
|
|
2599
2608
|
}
|
|
2600
|
-
async computePersonas(apiKey, managerTeamPromise
|
|
2609
|
+
async computePersonas(apiKey, managerTeamPromise,
|
|
2610
|
+
// Issue #1005: the project whose custom employees belong in this projection. Custom
|
|
2611
|
+
// employees are per-project (`fraim/personalized-employee/employees/`), so reading
|
|
2612
|
+
// them from `this.projectPath` (the directory the Hub launched from) leaked one
|
|
2613
|
+
// project's employees into every other project's roster. Defaults to the launch
|
|
2614
|
+
// project only so callers that genuinely have no project context keep working.
|
|
2615
|
+
projectPath = this.projectPath) {
|
|
2601
2616
|
const allBundles = listHubPersonaBundles();
|
|
2602
|
-
const fallbackProjection = this.fallbackPersonaProjection(apiKey);
|
|
2617
|
+
const fallbackProjection = this.fallbackPersonaProjection(apiKey, projectPath);
|
|
2603
2618
|
// Issue #701: persona state comes from the hosted server (GET /api/personas/me)
|
|
2604
2619
|
// via the user's API key — never a local MongoDB connection. Issue #925: resolve
|
|
2605
2620
|
// it in a transport-aware way so a transient outage does not collapse to the same
|
|
@@ -2616,7 +2631,7 @@ class AiHubServer {
|
|
|
2616
2631
|
// (feature-off, legacy bypass, per-entitlement gating) lives solely in
|
|
2617
2632
|
// persona-entitlement-service.resolvePersonaAccessStatuses — the Hub does not
|
|
2618
2633
|
// re-derive it.
|
|
2619
|
-
const customPersonas = (0, custom_employees_1.readCustomEmployees)(
|
|
2634
|
+
const customPersonas = (0, custom_employees_1.readCustomEmployees)(projectPath).map(custom_employees_1.buildCustomEmployeePersona);
|
|
2620
2635
|
if (!state) {
|
|
2621
2636
|
return { ...fallbackProjection, userKey };
|
|
2622
2637
|
}
|
|
@@ -2655,7 +2670,9 @@ class AiHubServer {
|
|
|
2655
2670
|
return { ...fallbackProjection, userKey };
|
|
2656
2671
|
}
|
|
2657
2672
|
}
|
|
2658
|
-
fallbackPersonaProjection(apiKey
|
|
2673
|
+
fallbackPersonaProjection(apiKey,
|
|
2674
|
+
// Issue #1005: see computePersonas — custom employees are per-project.
|
|
2675
|
+
projectPath = this.projectPath) {
|
|
2659
2676
|
const fallbackPersonas = listHubPersonaBundles().map((bundle) => ({
|
|
2660
2677
|
key: bundle.personaKey,
|
|
2661
2678
|
displayName: bundle.catalogMetadata.displayName,
|
|
@@ -2668,7 +2685,7 @@ class AiHubServer {
|
|
|
2668
2685
|
seatsInUse: 0,
|
|
2669
2686
|
origin: 'catalog',
|
|
2670
2687
|
}));
|
|
2671
|
-
const customPersonas = (0, custom_employees_1.readCustomEmployees)(
|
|
2688
|
+
const customPersonas = (0, custom_employees_1.readCustomEmployees)(projectPath).map(custom_employees_1.buildCustomEmployeePersona);
|
|
2672
2689
|
return {
|
|
2673
2690
|
personas: [...fallbackPersonas, ...customPersonas],
|
|
2674
2691
|
subscriptionActive: false,
|
|
@@ -2836,7 +2853,9 @@ class AiHubServer {
|
|
|
2836
2853
|
console.warn('[ai-hub] manager-team lookup failed:', err?.message || err);
|
|
2837
2854
|
return [];
|
|
2838
2855
|
});
|
|
2839
|
-
|
|
2856
|
+
// Issue #1005: pass the requested project so custom employees are scoped to it,
|
|
2857
|
+
// not to the directory the Hub launched from.
|
|
2858
|
+
const { personas, subscriptionActive, workspaceId, userKey } = await this.computePersonas(apiKey, managerTeamPromise, projectPath);
|
|
2840
2859
|
const managerTeam = await managerTeamPromise;
|
|
2841
2860
|
const jobCount = (0, catalog_1.discoverEmployeeJobs)(projectPath, { includeRegistry: true })
|
|
2842
2861
|
.filter((job) => !FRAIM_INTERNAL_JOB_IDS.has(job.id))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.237",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fraim-hub": "bin/fraim-hub.js"
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"electron": "^41.2.2",
|
|
159
159
|
"electron-updater": "^6.8.9",
|
|
160
160
|
"express": "^5.2.1",
|
|
161
|
-
"fraim": "2.0.
|
|
161
|
+
"fraim": "2.0.237",
|
|
162
162
|
"mongodb": "^7.0.0",
|
|
163
163
|
"node-cron": "4.2.1",
|
|
164
164
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/script.js
CHANGED
|
@@ -210,7 +210,42 @@ function cacheBootstrapPayload(bootstrap, docUrl) {
|
|
|
210
210
|
bootstrapCache.set(bootstrapCacheKey(bootstrap.project.path, docUrl), bootstrap);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// Issue #1005: `GET /api/ai-hub/bootstrap` time-boxes the hosted persona lookup to a
|
|
214
|
+
// 250ms first-paint budget (#975). On a miss it returns `personasResolved: false` plus an
|
|
215
|
+
// all-locked placeholder projection. That payload is NOT an authority statement, so
|
|
216
|
+
// adopting it wholesale is what erased a resolved roster: the ~4s background bootstrap
|
|
217
|
+
// refresh landed after the ~0.2s /api/ai-hub/personas refresh and `state.bootstrap =
|
|
218
|
+
// bootstrap` overwrote every hired persona, collapsing the employee rail to nothing.
|
|
219
|
+
//
|
|
220
|
+
// Contract, mirrored by tests/ui/test-1005-persona-preserved-across-bootstrap.spec.ts:
|
|
221
|
+
// personasResolved !== false -> adopt verbatim. A definitive not-entitled answer is a
|
|
222
|
+
// real answer, so a genuine sign-out still clears.
|
|
223
|
+
// personasResolved === false -> keep the last RESOLVED projection's catalog half.
|
|
224
|
+
//
|
|
225
|
+
// Only the catalog half is preserved. Custom employees are a per-project local disk read
|
|
226
|
+
// (`fraim/personalized-employee/employees/`) that never depends on the hosted authority,
|
|
227
|
+
// so the incoming payload's custom personas are always correct for the incoming project —
|
|
228
|
+
// carrying the previous project's across would re-create the cross-project leak.
|
|
229
|
+
const PERSONA_AUTHORITY_FIELDS = ['subscriptionActive', 'workspaceId', 'userEmail', 'managerTeam'];
|
|
230
|
+
|
|
231
|
+
function mergePersonaProjection(incoming, previous) {
|
|
232
|
+
if (!incoming || incoming.personasResolved !== false) return incoming;
|
|
233
|
+
// Nothing trustworthy to fall back on (cold start, or the previous payload was itself a
|
|
234
|
+
// placeholder). Adopt it and let tfRefreshAfterBootstrap schedule the real lookup.
|
|
235
|
+
if (!previous || previous.personasResolved === false) return incoming;
|
|
236
|
+
const previousCatalog = (previous.personas || []).filter((p) => p && p.origin !== 'custom');
|
|
237
|
+
if (!previousCatalog.length) return incoming;
|
|
238
|
+
const incomingCustom = (incoming.personas || []).filter((p) => p && p.origin === 'custom');
|
|
239
|
+
// Catalog-then-custom matches the server's own union order in computePersonas.
|
|
240
|
+
const merged = { ...incoming, personas: [...previousCatalog, ...incomingCustom], personasResolved: true };
|
|
241
|
+
for (const field of PERSONA_AUTHORITY_FIELDS) {
|
|
242
|
+
if (previous[field] !== undefined) merged[field] = previous[field];
|
|
243
|
+
}
|
|
244
|
+
return merged;
|
|
245
|
+
}
|
|
246
|
+
|
|
213
247
|
function applyBootstrap(bootstrap, docUrl) {
|
|
248
|
+
bootstrap = mergePersonaProjection(bootstrap, state.bootstrap);
|
|
214
249
|
state.bootstrap = bootstrap;
|
|
215
250
|
state.projectPath = bootstrap.project.path;
|
|
216
251
|
state.remoteBaseUrl = bootstrap.remoteBaseUrl || null;
|
|
@@ -319,6 +354,11 @@ async function tfRefreshPersonasFromServerInBackground() {
|
|
|
319
354
|
state.bootstrap = {
|
|
320
355
|
...state.bootstrap,
|
|
321
356
|
personas: Array.isArray(payload.personas) ? payload.personas : state.bootstrap.personas,
|
|
357
|
+
// Issue #1005: GET /api/ai-hub/personas awaits the authority with no first-paint
|
|
358
|
+
// budget, so a successful response IS the resolved projection. Marking it lets
|
|
359
|
+
// applyBootstrap preserve it against a later unresolved bootstrap, and stops
|
|
360
|
+
// tfRefreshAfterBootstrap from re-requesting a lookup that already succeeded.
|
|
361
|
+
personasResolved: true,
|
|
322
362
|
subscriptionActive: Boolean(payload.subscriptionActive),
|
|
323
363
|
workspaceId: payload.workspaceId ?? null,
|
|
324
364
|
userEmail: payload.userEmail ?? null,
|
|
@@ -12984,6 +13024,13 @@ function tfRefreshAfterBootstrap() {
|
|
|
12984
13024
|
else if (tf.area === 'brain') tfRenderBrain();
|
|
12985
13025
|
if (tf.projectView === 'overview') tfRenderOverview();
|
|
12986
13026
|
else tfRenderTree();
|
|
13027
|
+
// Issue #1005: the bootstrap we just applied missed the first-paint persona budget and
|
|
13028
|
+
// mergePersonaProjection had no resolved projection to fall back on (cold start, or the
|
|
13029
|
+
// shell's initial refresh failed). Schedule the authoritative lookup so the roster fills
|
|
13030
|
+
// in instead of waiting for some later bootstrap to happen to win the race.
|
|
13031
|
+
if (state.bootstrap && state.bootstrap.personasResolved === false) {
|
|
13032
|
+
void tfRefreshPersonasFromServerInBackground();
|
|
13033
|
+
}
|
|
12987
13034
|
}
|
|
12988
13035
|
|
|
12989
13036
|
// Kick off the shell once the initial bootstrap has settled. The IIFE init at
|