fraim-hub 2.0.337 → 2.0.339
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/registry/scripts/learning/entry-format.js +27 -0
- package/dist/registry/scripts/learning/learning-domains.json +78 -0
- package/dist/src/ai-hub/managed-browser.js +130 -5
- package/dist/src/ai-hub/server.js +61 -31
- package/dist/src/cli/utils/pack-home.js +4 -2
- package/dist/src/config/learning-domains.js +10 -113
- package/dist/src/core/utils/local-registry-resolver.js +6 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +49 -8
- package/dist/src/local-mcp-server/learning-usage-events.js +261 -0
- package/dist/src/local-mcp-server/learning-usage-projection.js +25 -21
- package/dist/src/local-mcp-server/learning-usage-store.js +589 -82
- package/package.json +7 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Read the shared author's current entry format for identity and update guards.
|
|
4
|
+
function learningEntryBlocks(body) {
|
|
5
|
+
const headings = [...body.matchAll(/^#{2,6}\s+\[P-[A-Z]+\].*$/gm)];
|
|
6
|
+
return headings.map((heading, index) => {
|
|
7
|
+
const start = heading.index;
|
|
8
|
+
const next = headings[index + 1]?.index ?? body.length;
|
|
9
|
+
const ownDepth = heading[0].match(/^#+/)[0].length;
|
|
10
|
+
const afterHeading = start + heading[0].length;
|
|
11
|
+
const shallower = [...body.slice(afterHeading, next).matchAll(/^(#{1,6})\s+.*$/gm)].find(m => m[1].length <= ownDepth);
|
|
12
|
+
const limit = shallower ? afterHeading + shallower.index : next;
|
|
13
|
+
const text = body.slice(start, limit).trimEnd();
|
|
14
|
+
return { start, end: start + text.length, text, id: text.match(/^\*\*Id\*\*:\s*(L-[a-f0-9]{10})\s*$/m)?.[1] ?? null };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function learningEntryMetadata(content) {
|
|
19
|
+
const metadata = {};
|
|
20
|
+
for (const match of content.matchAll(/^\*\*(Recurrences|First recorded|First synthesized)\*\*:\s*([^\r\n]+)$/gm)) {
|
|
21
|
+
if (metadata[match[1]] !== undefined && metadata[match[1]] !== match[2]) throw new Error('entry-metadata-conflict');
|
|
22
|
+
metadata[match[1]] = match[2];
|
|
23
|
+
}
|
|
24
|
+
return metadata;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { learningEntryMetadata, learningEntryBlocks };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"mapVersion": 1,
|
|
4
|
+
"domains": [
|
|
5
|
+
"engineering",
|
|
6
|
+
"product",
|
|
7
|
+
"go-to-market",
|
|
8
|
+
"people-ops",
|
|
9
|
+
"finance",
|
|
10
|
+
"legal",
|
|
11
|
+
"compliance",
|
|
12
|
+
"customer",
|
|
13
|
+
"company-building",
|
|
14
|
+
"operations"
|
|
15
|
+
],
|
|
16
|
+
"jobDomainMap": {
|
|
17
|
+
"product-building": "engineering",
|
|
18
|
+
"dev-ops": "engineering",
|
|
19
|
+
"quality-assurance": "engineering",
|
|
20
|
+
"migration": "engineering",
|
|
21
|
+
"replicate": "engineering",
|
|
22
|
+
"reviewer": "engineering",
|
|
23
|
+
"security": "engineering",
|
|
24
|
+
"delivery-ops": "engineering",
|
|
25
|
+
"salesforce": "engineering",
|
|
26
|
+
"ai-engineering": "engineering",
|
|
27
|
+
"product-management": "product",
|
|
28
|
+
"customer-development": "product",
|
|
29
|
+
"brainstorming": "product",
|
|
30
|
+
"marketing": "go-to-market",
|
|
31
|
+
"branding": "go-to-market",
|
|
32
|
+
"sales": "go-to-market",
|
|
33
|
+
"go-to-market": "go-to-market",
|
|
34
|
+
"business-development": "go-to-market",
|
|
35
|
+
"commercial-ops": "go-to-market",
|
|
36
|
+
"people-ops": "people-ops",
|
|
37
|
+
"inclusion-ops": "people-ops",
|
|
38
|
+
"career": "people-ops",
|
|
39
|
+
"finance": "finance",
|
|
40
|
+
"invoicing": "finance",
|
|
41
|
+
"banking": "finance",
|
|
42
|
+
"legal": "legal",
|
|
43
|
+
"compliance": "compliance",
|
|
44
|
+
"customer-success": "customer",
|
|
45
|
+
"customer-communication": "customer",
|
|
46
|
+
"company-creation": "company-building",
|
|
47
|
+
"bootstrap": "company-building",
|
|
48
|
+
"fundraising": "company-building",
|
|
49
|
+
"business-ops": "operations",
|
|
50
|
+
"procurement": "operations",
|
|
51
|
+
"project-management": "operations"
|
|
52
|
+
},
|
|
53
|
+
"globalJobCategories": [
|
|
54
|
+
"manager",
|
|
55
|
+
"fraim",
|
|
56
|
+
"1-1",
|
|
57
|
+
"coaching",
|
|
58
|
+
"delegation",
|
|
59
|
+
"hiring",
|
|
60
|
+
"productivity",
|
|
61
|
+
"project-setup",
|
|
62
|
+
"setup-guardrails",
|
|
63
|
+
"team-learning",
|
|
64
|
+
"verification"
|
|
65
|
+
],
|
|
66
|
+
"domainVoice": {
|
|
67
|
+
"engineering": "Precise and technical; file paths, commands, and test names are appropriate.",
|
|
68
|
+
"product": "Product and user-outcome language; behavior and flow, not code symbols.",
|
|
69
|
+
"go-to-market": "Marketing and sales language; campaigns, funnels, messaging, audience.",
|
|
70
|
+
"people-ops": "Plain HR and recruiting language; no engineering jargon.",
|
|
71
|
+
"finance": "Finance and accounting language; figures, close, forecasts.",
|
|
72
|
+
"legal": "Contract and IP language; obligations, clauses, risk.",
|
|
73
|
+
"compliance": "Controls, evidence, and regulation language.",
|
|
74
|
+
"customer": "Customer-relationship and support language.",
|
|
75
|
+
"company-building": "Founder and operations language; setup, entity, funding.",
|
|
76
|
+
"operations": "Business-operations language; process, vendors, planning, coordination."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -110,15 +110,27 @@ function buildBrowserLaunchArgs(opts) {
|
|
|
110
110
|
* baked into any registry job or skill. Returns '' when the shared browser is not
|
|
111
111
|
* available (so the note only appears when there is something to connect to).
|
|
112
112
|
*/
|
|
113
|
-
function buildBrowserContextNote(cdpEndpoint, hubBaseUrl) {
|
|
113
|
+
function buildBrowserContextNote(cdpEndpoint, hubBaseUrl, conversationId, page) {
|
|
114
114
|
if (!cdpEndpoint || !hubBaseUrl)
|
|
115
115
|
return '';
|
|
116
|
+
const trimmedConversationId = typeof conversationId === 'string' ? conversationId.trim() : '';
|
|
117
|
+
const pageGuidance = trimmedConversationId
|
|
118
|
+
? [
|
|
119
|
+
`Browser work for this Hub conversation must use the conversation page for \`${trimmedConversationId}\`, not the globally active tab.`,
|
|
120
|
+
`Before using a browser, ensure it is running and get this conversation's page: \`POST ${hubBaseUrl}/api/ai-hub/browser/start\` with JSON body \`{"conversationId":"${trimmedConversationId}"}\` (idempotent - it reuses the running browser and this conversation's page, or relaunches/recreates them if they were closed).`,
|
|
121
|
+
page?.targetId
|
|
122
|
+
? `Current allocated page target: \`${page.targetId}\`${page.webSocketDebuggerUrl ? ` (${page.webSocketDebuggerUrl})` : ''}.`
|
|
123
|
+
: 'Use the returned `page.targetId` / `page.webSocketDebuggerUrl` from that endpoint when your browser tooling lets you choose a CDP target.',
|
|
124
|
+
]
|
|
125
|
+
: [
|
|
126
|
+
`Before using a browser, ensure it is running: \`POST ${hubBaseUrl}/api/ai-hub/browser/start\` (idempotent - it reuses the running browser, or relaunches it if it was closed).`,
|
|
127
|
+
];
|
|
116
128
|
return [
|
|
117
129
|
'',
|
|
118
|
-
'[FRAIM shared browser] A persistent shared browser is available
|
|
119
|
-
|
|
120
|
-
`Then drive the shared browser at ${cdpEndpoint}: your Playwright/browser tools are pointed at it on supported agents; if your tools would otherwise open their own browser, connect to the shared one with \`connectOverCDP("${cdpEndpoint}")\` and
|
|
121
|
-
'Never close the shared browser or sign me out. If a site needs login, open it there and ask me to log in
|
|
130
|
+
'[FRAIM shared browser] A persistent shared browser is available; use it instead of launching your own.',
|
|
131
|
+
...pageGuidance,
|
|
132
|
+
`Then drive the shared browser at ${cdpEndpoint}: your Playwright/browser tools are pointed at it on supported agents; if your tools would otherwise open their own browser, connect to the shared one with \`connectOverCDP("${cdpEndpoint}")\` and use the conversation page target instead of an arbitrary existing tab.`,
|
|
133
|
+
'Never close the shared browser or sign me out. If a site needs login, open it there and ask me to log in; the session persists to your next turn.',
|
|
122
134
|
].join('\n');
|
|
123
135
|
}
|
|
124
136
|
/** Default persistent profile directory — sandboxed from the user's daily browser. */
|
|
@@ -168,6 +180,7 @@ class ManagedBrowser {
|
|
|
168
180
|
this.child = null;
|
|
169
181
|
this.ownsProcess = false;
|
|
170
182
|
this.resolved = null;
|
|
183
|
+
this.conversationPages = new Map();
|
|
171
184
|
this.opts = opts;
|
|
172
185
|
this.channel = opts.channel || 'auto';
|
|
173
186
|
this.port = opts.port || DEFAULT_PORT;
|
|
@@ -180,6 +193,11 @@ class ManagedBrowser {
|
|
|
180
193
|
probe() {
|
|
181
194
|
return this.opts.probeFn ? this.opts.probeFn(this.cdpEndpoint()) : probeCdpEndpoint(this.cdpEndpoint());
|
|
182
195
|
}
|
|
196
|
+
async cdpRequest(requestPath, method = 'GET') {
|
|
197
|
+
if (this.opts.cdpRequestFn)
|
|
198
|
+
return this.opts.cdpRequestFn(requestPath, method);
|
|
199
|
+
return cdpJsonRequest(this.cdpEndpoint(), requestPath, method);
|
|
200
|
+
}
|
|
183
201
|
/** Live check — does a Chromium CDP endpoint answer on the port right now. */
|
|
184
202
|
isRunning() {
|
|
185
203
|
return this.probe();
|
|
@@ -232,6 +250,71 @@ class ManagedBrowser {
|
|
|
232
250
|
await this.waitForRunning();
|
|
233
251
|
return { endpoint: this.cdpEndpoint(), reused: false, executable: exe.path, channel: exe.channel };
|
|
234
252
|
}
|
|
253
|
+
async listPageTargets() {
|
|
254
|
+
const raw = await this.cdpRequest('/json/list');
|
|
255
|
+
if (!Array.isArray(raw))
|
|
256
|
+
return [];
|
|
257
|
+
return raw
|
|
258
|
+
.filter((target) => Boolean(target && typeof target === 'object' && typeof target.id === 'string'))
|
|
259
|
+
.filter((target) => !target.type || target.type === 'page');
|
|
260
|
+
}
|
|
261
|
+
pageFromTarget(conversationId, target) {
|
|
262
|
+
return {
|
|
263
|
+
conversationId,
|
|
264
|
+
targetId: target.id,
|
|
265
|
+
url: target.url || 'about:blank',
|
|
266
|
+
webSocketDebuggerUrl: target.webSocketDebuggerUrl || null,
|
|
267
|
+
endpoint: this.cdpEndpoint(),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async ensureConversationPage(conversationId, startUrl = 'about:blank') {
|
|
271
|
+
const id = conversationId.trim();
|
|
272
|
+
if (!id)
|
|
273
|
+
throw new Error('conversationId is required for browser page allocation.');
|
|
274
|
+
await this.start();
|
|
275
|
+
const existing = this.conversationPages.get(id);
|
|
276
|
+
const targets = await this.listPageTargets();
|
|
277
|
+
if (existing && targets.some((target) => target.id === existing.targetId)) {
|
|
278
|
+
try {
|
|
279
|
+
await this.cdpRequest(`/json/activate/${encodeURIComponent(existing.targetId)}`);
|
|
280
|
+
}
|
|
281
|
+
catch { /* best effort */ }
|
|
282
|
+
return existing;
|
|
283
|
+
}
|
|
284
|
+
const created = await this.cdpRequest(`/json/new?${encodeURIComponent(startUrl)}`, 'PUT');
|
|
285
|
+
if (!created || typeof created.id !== 'string') {
|
|
286
|
+
throw new Error('Browser did not return a page target for the conversation.');
|
|
287
|
+
}
|
|
288
|
+
const page = this.pageFromTarget(id, created);
|
|
289
|
+
this.conversationPages.set(id, page);
|
|
290
|
+
try {
|
|
291
|
+
await this.cdpRequest(`/json/activate/${encodeURIComponent(page.targetId)}`);
|
|
292
|
+
}
|
|
293
|
+
catch { /* best effort */ }
|
|
294
|
+
return page;
|
|
295
|
+
}
|
|
296
|
+
async releaseConversationPage(conversationId) {
|
|
297
|
+
const id = conversationId.trim();
|
|
298
|
+
if (!id)
|
|
299
|
+
return { released: false, conversationId: id, targetId: null };
|
|
300
|
+
const existing = this.conversationPages.get(id);
|
|
301
|
+
this.conversationPages.delete(id);
|
|
302
|
+
if (!existing)
|
|
303
|
+
return { released: false, conversationId: id, targetId: null };
|
|
304
|
+
if (await this.probe()) {
|
|
305
|
+
try {
|
|
306
|
+
await this.cdpRequest(`/json/close/${encodeURIComponent(existing.targetId)}`);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// The user may have closed the tab or browser manually. Dropping the
|
|
310
|
+
// mapping is the important cleanup invariant.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { released: true, conversationId: id, targetId: existing.targetId };
|
|
314
|
+
}
|
|
315
|
+
conversationPage(conversationId) {
|
|
316
|
+
return this.conversationPages.get(conversationId.trim()) || null;
|
|
317
|
+
}
|
|
235
318
|
async waitForRunning() {
|
|
236
319
|
const timeoutMs = this.opts.waitTimeoutMs ?? 15000;
|
|
237
320
|
const intervalMs = this.opts.waitIntervalMs ?? 250;
|
|
@@ -263,7 +346,49 @@ class ManagedBrowser {
|
|
|
263
346
|
channel: this.resolved?.channel || this.channel,
|
|
264
347
|
userDataDir: this.userDataDir,
|
|
265
348
|
executable: this.resolved?.path || null,
|
|
349
|
+
conversationPages: Array.from(this.conversationPages.values()).map((page) => ({
|
|
350
|
+
conversationId: page.conversationId,
|
|
351
|
+
targetId: page.targetId,
|
|
352
|
+
})),
|
|
266
353
|
};
|
|
267
354
|
}
|
|
268
355
|
}
|
|
269
356
|
exports.ManagedBrowser = ManagedBrowser;
|
|
357
|
+
function cdpJsonRequest(endpoint, requestPath, method) {
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
let url;
|
|
360
|
+
try {
|
|
361
|
+
url = new URL(requestPath, endpoint);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
reject(error);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const req = http_1.default.request(url, { method, timeout: 3000 }, (res) => {
|
|
368
|
+
let body = '';
|
|
369
|
+
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
370
|
+
res.on('end', () => {
|
|
371
|
+
if ((res.statusCode || 500) >= 400) {
|
|
372
|
+
reject(new Error(`CDP ${method} ${requestPath} failed with HTTP ${res.statusCode}`));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!body.trim()) {
|
|
376
|
+
resolve({});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
resolve(JSON.parse(body));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
resolve(body);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
req.on('error', reject);
|
|
388
|
+
req.on('timeout', () => {
|
|
389
|
+
req.destroy();
|
|
390
|
+
reject(new Error(`CDP ${method} ${requestPath} timed out.`));
|
|
391
|
+
});
|
|
392
|
+
req.end();
|
|
393
|
+
});
|
|
394
|
+
}
|
|
@@ -3705,8 +3705,24 @@ class AiHubServer {
|
|
|
3705
3705
|
// at spawn time instead, so it is available for the entire life of the
|
|
3706
3706
|
// process. Call this at every startRun/continueRun call site (Direct-mode
|
|
3707
3707
|
// spawns are exempt — they never load FRAIM, so there is nothing to report).
|
|
3708
|
-
|
|
3709
|
-
return {
|
|
3708
|
+
withBrowserConversationEnv(launchContext, runId, conversationId, page) {
|
|
3709
|
+
return {
|
|
3710
|
+
...launchContext,
|
|
3711
|
+
env: {
|
|
3712
|
+
...(launchContext.env || {}),
|
|
3713
|
+
FRAIM_HUB_RUN_ID: runId,
|
|
3714
|
+
FRAIM_BROWSER_CONVERSATION_ID: conversationId,
|
|
3715
|
+
...(page ? {
|
|
3716
|
+
FRAIM_BROWSER_PAGE_TARGET_ID: page.targetId,
|
|
3717
|
+
...(page.webSocketDebuggerUrl ? { FRAIM_BROWSER_PAGE_WS_URL: page.webSocketDebuggerUrl } : {}),
|
|
3718
|
+
} : {}),
|
|
3719
|
+
},
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
async releaseBrowserConversationPage(conversationId) {
|
|
3723
|
+
await this.managedBrowser.releaseConversationPage(conversationId).catch((error) => {
|
|
3724
|
+
console.warn('[ai-hub] shared browser page release failed:', error instanceof Error ? error.message : error);
|
|
3725
|
+
});
|
|
3710
3726
|
}
|
|
3711
3727
|
// Shared by both the runId-keyed and legacy by-session execution-mode routes.
|
|
3712
3728
|
applyExecutionModeToRun(runId, body) {
|
|
@@ -4204,7 +4220,7 @@ class AiHubServer {
|
|
|
4204
4220
|
this.maybeStartDelegatedChildRuns(updated);
|
|
4205
4221
|
});
|
|
4206
4222
|
},
|
|
4207
|
-
}, startSessionSeedForHost(run.hostId, run.id), this.
|
|
4223
|
+
}, startSessionSeedForHost(run.hostId, run.id), this.withBrowserConversationEnv(freshLaunch.launchContext, run.id, run.conversationId || run.id));
|
|
4208
4224
|
this.runRegistry.attachChildIfRunning(run.id, freshChild);
|
|
4209
4225
|
}
|
|
4210
4226
|
scheduleRunConversationPersistence(run, activeId) {
|
|
@@ -4507,7 +4523,7 @@ class AiHubServer {
|
|
|
4507
4523
|
onExit: (exitCode) => {
|
|
4508
4524
|
this.handleRunExit(currentRun.id, exitCode, options.postPark);
|
|
4509
4525
|
},
|
|
4510
|
-
}, this.
|
|
4526
|
+
}, this.withBrowserConversationEnv(launch.launchContext, currentRun.id, currentRun.conversationId || currentRun.id));
|
|
4511
4527
|
this.runRegistry.attachChildIfRunning(currentRun.id, child);
|
|
4512
4528
|
const updated = this.runRegistry.get(currentRun.id);
|
|
4513
4529
|
if (updated)
|
|
@@ -4840,7 +4856,7 @@ class AiHubServer {
|
|
|
4840
4856
|
}
|
|
4841
4857
|
this.runRegistry.dispose(childRun.id);
|
|
4842
4858
|
},
|
|
4843
|
-
}, startSessionSeedForHost(managerRun.hostId, childRun.id), this.
|
|
4859
|
+
}, startSessionSeedForHost(managerRun.hostId, childRun.id), this.withBrowserConversationEnv(childLaunch.launchContext, childRun.id, childRun.conversationId || childRun.id));
|
|
4844
4860
|
this.runRegistry.attachChildIfRunning(childRun.id, child);
|
|
4845
4861
|
}
|
|
4846
4862
|
notifyManagerOfDelegatedChild(managerRunId, childRun) {
|
|
@@ -5004,7 +5020,7 @@ class AiHubServer {
|
|
|
5004
5020
|
if (latestManager)
|
|
5005
5021
|
this.drainPendingDelegatedReviews(latestManager);
|
|
5006
5022
|
},
|
|
5007
|
-
}, this.
|
|
5023
|
+
}, this.withBrowserConversationEnv(managerLaunch.launchContext, managerRun.id, managerRun.conversationId || managerRun.id));
|
|
5008
5024
|
this.runRegistry.attachChildIfRunning(managerRun.id, child);
|
|
5009
5025
|
}
|
|
5010
5026
|
computeFirstRun(projectPath, jobCount, personas) {
|
|
@@ -5113,6 +5129,9 @@ class AiHubServer {
|
|
|
5113
5129
|
// unpromoted and then dropping its phase signals).
|
|
5114
5130
|
this.maybePromoteFreeformRunToJob(run, signal.jobName || signal.jobId);
|
|
5115
5131
|
applySeekMentoringSignal(run, signal);
|
|
5132
|
+
if (signal.phaseId === 'retrospective' && signal.phaseStatus === 'complete') {
|
|
5133
|
+
void this.releaseBrowserConversationPage(run.conversationId || run.id);
|
|
5134
|
+
}
|
|
5116
5135
|
}
|
|
5117
5136
|
applyFraimJobSignalToRun(run, signal) {
|
|
5118
5137
|
this.maybePromoteFreeformRunToJob(run, signal.jobId);
|
|
@@ -5153,7 +5172,7 @@ class AiHubServer {
|
|
|
5153
5172
|
// #521: the shared-browser guidance is injected HERE, at the Hub layer — never
|
|
5154
5173
|
// baked into the registry job/skill. It only appears when a shared browser is
|
|
5155
5174
|
// available (env published at boot).
|
|
5156
|
-
const browserNote = (0, managed_browser_1.buildBrowserContextNote)(process.env.FRAIM_BROWSER_CDP_ENDPOINT, process.env.FRAIM_HUB_BASE_URL);
|
|
5175
|
+
const browserNote = (0, managed_browser_1.buildBrowserContextNote)(process.env.FRAIM_BROWSER_CDP_ENDPOINT, process.env.FRAIM_HUB_BASE_URL, options?.browserConversationId, options?.browserPage);
|
|
5157
5176
|
const styleNote = (0, manager_turns_1.buildCommunicationStyleNote)();
|
|
5158
5177
|
const backgroundTaskNote = (0, manager_turns_1.buildBackgroundTaskPolicyNote)();
|
|
5159
5178
|
const ignoreEmbedded = options?.ignoreEmbeddedInvocation === true;
|
|
@@ -5873,7 +5892,7 @@ class AiHubServer {
|
|
|
5873
5892
|
});
|
|
5874
5893
|
// Issue #1740: interactive run deletion is a bounded one-record mutation. It requires an
|
|
5875
5894
|
// explicit record-owned partition and never falls back to the currently selected project.
|
|
5876
|
-
this.app.delete('/api/ai-hub/conversations/:conversationId', (req, res) => {
|
|
5895
|
+
this.app.delete('/api/ai-hub/conversations/:conversationId', async (req, res) => {
|
|
5877
5896
|
try {
|
|
5878
5897
|
const scope = scopeParam(req.query.scope);
|
|
5879
5898
|
if (!scope && (typeof req.query.projectPath !== 'string' || req.query.projectPath.trim().length === 0)) {
|
|
@@ -5885,6 +5904,7 @@ class AiHubServer {
|
|
|
5885
5904
|
const result = this.conversationStore.deleteConversation(key, req.params.conversationId);
|
|
5886
5905
|
if (!result.removed)
|
|
5887
5906
|
return res.status(404).json({ error: 'Conversation not found.' });
|
|
5907
|
+
await this.releaseBrowserConversationPage(req.params.conversationId);
|
|
5888
5908
|
return res.json(result);
|
|
5889
5909
|
}
|
|
5890
5910
|
catch (error) {
|
|
@@ -6017,7 +6037,7 @@ class AiHubServer {
|
|
|
6017
6037
|
attemptedRunId = run.id;
|
|
6018
6038
|
const hostMessage = strategy === 'resume_same_host'
|
|
6019
6039
|
? this.prepareContinueMessage(run, managerNote).message
|
|
6020
|
-
: this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, renderAgentSwitchHandoffPrompt(handoffSummary, managerNote)).message;
|
|
6040
|
+
: this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, renderAgentSwitchHandoffPrompt(handoffSummary, managerNote), { browserConversationId: run.conversationId || run.id }).message;
|
|
6021
6041
|
this.runRegistry.create(run, {});
|
|
6022
6042
|
this.persistRunConversation(run, run.conversationId || run.id);
|
|
6023
6043
|
const handlers = {
|
|
@@ -6063,8 +6083,8 @@ class AiHubServer {
|
|
|
6063
6083
|
};
|
|
6064
6084
|
this.recordHubGeneratedEvent(run, 'agent_input', hostMessage);
|
|
6065
6085
|
const child = strategy === 'resume_same_host'
|
|
6066
|
-
? this.hostRuntime.continueRun(run.hostId, run.projectPath, priorSessionId, hostMessage, handlers, this.
|
|
6067
|
-
: this.hostRuntime.startRun(run.hostId, run.projectPath, hostMessage, handlers, startSessionSeedForHost(run.hostId, run.id), this.
|
|
6086
|
+
? this.hostRuntime.continueRun(run.hostId, run.projectPath, priorSessionId, hostMessage, handlers, this.withBrowserConversationEnv(resolved.launchContext, run.id, run.conversationId || run.id))
|
|
6087
|
+
: this.hostRuntime.startRun(run.hostId, run.projectPath, hostMessage, handlers, startSessionSeedForHost(run.hostId, run.id), this.withBrowserConversationEnv(resolved.launchContext, run.id, run.conversationId || run.id));
|
|
6068
6088
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
6069
6089
|
attemptedRunId = null;
|
|
6070
6090
|
console.info(`[ai-hub] hub.agent_switch.${strategy}`, {
|
|
@@ -6794,7 +6814,7 @@ class AiHubServer {
|
|
|
6794
6814
|
// The proxy parses the Execution Mode Context block and POSTs here; Hub script reads from run poll.
|
|
6795
6815
|
//
|
|
6796
6816
|
// Issue #1443: the local proxy's only stable identity for "which run is this" is the
|
|
6797
|
-
// Hub's own run id (see
|
|
6817
|
+
// Hub's own run id (see FRAIM_HUB_RUN_ID) — the FRAIM/MCP session id it
|
|
6798
6818
|
// carries is unrelated to any Hub run, and the host CLI's own session id (run.sessionId)
|
|
6799
6819
|
// often isn't known yet when the first get_fraim_job call fires. Runs by id, not session.
|
|
6800
6820
|
this.app.post('/api/ai-hub/runs/:runId/execution-mode', (req, res) => {
|
|
@@ -6849,8 +6869,13 @@ class AiHubServer {
|
|
|
6849
6869
|
}
|
|
6850
6870
|
const employees = this.hostRuntime.detectEmployees();
|
|
6851
6871
|
const { hostId, agent: configuredAgent, launchContext } = this.resolveLaunchAgent(req.body.configuredAgentId, requestedHostId, employees);
|
|
6872
|
+
const runId = (0, crypto_1.randomUUID)();
|
|
6873
|
+
const requestedConversationId = typeof req.body.conversationId === 'string' && req.body.conversationId.trim()
|
|
6874
|
+
? req.body.conversationId.trim()
|
|
6875
|
+
: undefined;
|
|
6876
|
+
const browserConversationId = requestedConversationId || runId;
|
|
6852
6877
|
const prepared = instructions
|
|
6853
|
-
? this.prepareStartPayload(projectPath, hostId, req.body.jobId, instructions)
|
|
6878
|
+
? this.prepareStartPayload(projectPath, hostId, req.body.jobId, instructions, { browserConversationId })
|
|
6854
6879
|
: {
|
|
6855
6880
|
jobId: req.body.jobId,
|
|
6856
6881
|
message: legacyMessage,
|
|
@@ -6890,9 +6915,6 @@ class AiHubServer {
|
|
|
6890
6915
|
const fallbackJobTitle = typeof req.body.jobTitle === 'string' && req.body.jobTitle.trim()
|
|
6891
6916
|
? req.body.jobTitle.trim()
|
|
6892
6917
|
: jobId;
|
|
6893
|
-
const requestedConversationId = typeof req.body.conversationId === 'string' && req.body.conversationId.trim()
|
|
6894
|
-
? req.body.conversationId.trim()
|
|
6895
|
-
: undefined;
|
|
6896
6918
|
const continuity = this.validateRunConversationContinuity({
|
|
6897
6919
|
conversationId: requestedConversationId,
|
|
6898
6920
|
projectPath,
|
|
@@ -6912,7 +6934,7 @@ class AiHubServer {
|
|
|
6912
6934
|
? this.resolveMessageAttachments((0, conversation_store_1.conversationScopeKey)(scope, projectPath), requestedConversationId, req.body.attachmentIds)
|
|
6913
6935
|
: { attachments: undefined, referenceSuffix: '' };
|
|
6914
6936
|
const run = {
|
|
6915
|
-
id:
|
|
6937
|
+
id: runId,
|
|
6916
6938
|
conversationId: requestedConversationId,
|
|
6917
6939
|
conversationTitle: typeof req.body.conversationTitle === 'string' && req.body.conversationTitle.trim() ? req.body.conversationTitle.trim() : undefined,
|
|
6918
6940
|
jobTitle: jobMetadata?.title || fallbackJobTitle,
|
|
@@ -7025,7 +7047,7 @@ class AiHubServer {
|
|
|
7025
7047
|
this.maybeStartDelegatedChildRuns(updated);
|
|
7026
7048
|
});
|
|
7027
7049
|
},
|
|
7028
|
-
}, startSessionSeedForHost(hostId, run.id), this.
|
|
7050
|
+
}, startSessionSeedForHost(hostId, run.id), this.withBrowserConversationEnv(launchContext, run.id, run.conversationId || run.id));
|
|
7029
7051
|
pendingSpawnRuns.delete(run);
|
|
7030
7052
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
7031
7053
|
// Issue #442: spawn the Direct run via startDirectRun so CliHostRuntime
|
|
@@ -7103,9 +7125,14 @@ class AiHubServer {
|
|
|
7103
7125
|
const running = await this.managedBrowser.isRunning();
|
|
7104
7126
|
res.json({ running, ...this.managedBrowser.status() });
|
|
7105
7127
|
});
|
|
7106
|
-
this.app.post('/api/ai-hub/browser/start', async (
|
|
7128
|
+
this.app.post('/api/ai-hub/browser/start', async (req, res) => {
|
|
7107
7129
|
try {
|
|
7108
|
-
const
|
|
7130
|
+
const body = (req.body ?? {});
|
|
7131
|
+
const conversationId = typeof body.conversationId === 'string' && body.conversationId.trim()
|
|
7132
|
+
? body.conversationId.trim()
|
|
7133
|
+
: undefined;
|
|
7134
|
+
const url = typeof body.url === 'string' && body.url.trim() ? body.url.trim() : undefined;
|
|
7135
|
+
const result = await this.ensureManagedBrowser(conversationId, url);
|
|
7109
7136
|
res.json({
|
|
7110
7137
|
ok: true,
|
|
7111
7138
|
...result,
|
|
@@ -7203,8 +7230,8 @@ class AiHubServer {
|
|
|
7203
7230
|
? buildAgentSwitchHandoffSummary(conversation)
|
|
7204
7231
|
: null;
|
|
7205
7232
|
const freshPayload = handoffSummary
|
|
7206
|
-
? this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, renderAgentSwitchHandoffPrompt(handoffSummary, prepared.display || message))
|
|
7207
|
-
: this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, prepared.display || message);
|
|
7233
|
+
? this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, renderAgentSwitchHandoffPrompt(handoffSummary, prepared.display || message), { browserConversationId: run.conversationId || run.id })
|
|
7234
|
+
: this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, prepared.display || message, { browserConversationId: run.conversationId || run.id });
|
|
7208
7235
|
this.runRegistry.update(run.id, (current) => {
|
|
7209
7236
|
current.status = 'running';
|
|
7210
7237
|
current.pauseReason = 'working';
|
|
@@ -7269,7 +7296,7 @@ class AiHubServer {
|
|
|
7269
7296
|
this.maybeStartDelegatedChildRuns(updated);
|
|
7270
7297
|
});
|
|
7271
7298
|
},
|
|
7272
|
-
}, startSessionSeedForHost(run.hostId, run.id), this.
|
|
7299
|
+
}, startSessionSeedForHost(run.hostId, run.id), this.withBrowserConversationEnv(freshLaunch.launchContext, run.id, run.conversationId || run.id));
|
|
7273
7300
|
this.runRegistry.attachChildIfRunning(run.id, freshChild);
|
|
7274
7301
|
const refreshedFresh = this.runRegistry.get(run.id);
|
|
7275
7302
|
return res.json(refreshedFresh ? this.enrichRunForResponse(refreshedFresh) : refreshedFresh);
|
|
@@ -7392,7 +7419,7 @@ class AiHubServer {
|
|
|
7392
7419
|
if (failedInterrupt)
|
|
7393
7420
|
this.scheduleRunConversationPersistence(failedInterrupt, failedInterrupt.conversationId || failedInterrupt.id);
|
|
7394
7421
|
},
|
|
7395
|
-
}, this.
|
|
7422
|
+
}, this.withBrowserConversationEnv(continueLaunch.launchContext, run.id, run.conversationId || run.id), deliveryIntent);
|
|
7396
7423
|
if (!startedCodexReviewApprovalFallback)
|
|
7397
7424
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
7398
7425
|
const refreshed = this.runRegistry.get(run.id);
|
|
@@ -7504,7 +7531,7 @@ class AiHubServer {
|
|
|
7504
7531
|
const reviewApprovalSystemEventText = buildReviewApprovalSystemEventText(preparedResume.display || preparedResume.message);
|
|
7505
7532
|
const browserContextNote = reviewApprovalSystemEventText
|
|
7506
7533
|
? ''
|
|
7507
|
-
: (0, managed_browser_1.buildBrowserContextNote)(process.env.FRAIM_BROWSER_CDP_ENDPOINT, process.env.FRAIM_HUB_BASE_URL);
|
|
7534
|
+
: (0, managed_browser_1.buildBrowserContextNote)(process.env.FRAIM_BROWSER_CDP_ENDPOINT, process.env.FRAIM_HUB_BASE_URL, run.conversationId || run.id);
|
|
7508
7535
|
const message = preparedResume.message + browserContextNote;
|
|
7509
7536
|
// #521: bubble shows the manager's words; the agent gets the full message.
|
|
7510
7537
|
const managerMessage = (0, hosts_1.createHubMessage)('manager', preparedResume.display || message);
|
|
@@ -7559,7 +7586,7 @@ class AiHubServer {
|
|
|
7559
7586
|
onExit: (exitCode) => {
|
|
7560
7587
|
this.handleRunExit(run.id, exitCode);
|
|
7561
7588
|
},
|
|
7562
|
-
}, this.
|
|
7589
|
+
}, this.withBrowserConversationEnv(launchContext, run.id, run.conversationId || run.id));
|
|
7563
7590
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
7564
7591
|
res.status(201).json(this.enrichRunForResponse(this.runRegistry.get(run.id) ?? run));
|
|
7565
7592
|
}
|
|
@@ -8453,7 +8480,7 @@ class AiHubServer {
|
|
|
8453
8480
|
this.finalizeRunConversationProjection(run.id, updated.conversationId || updated.id);
|
|
8454
8481
|
this.runRegistry.dispose(run.id);
|
|
8455
8482
|
},
|
|
8456
|
-
}, startSessionSeedForHost(hostId, run.id), this.
|
|
8483
|
+
}, startSessionSeedForHost(hostId, run.id), this.withBrowserConversationEnv(launchContext, run.id, run.conversationId || run.id));
|
|
8457
8484
|
// Update the registry entry with the real child process handle.
|
|
8458
8485
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
8459
8486
|
return res.json({
|
|
@@ -8473,10 +8500,13 @@ class AiHubServer {
|
|
|
8473
8500
|
// #521: start (or reuse) the shared browser and publish its CDP endpoint so
|
|
8474
8501
|
// every agent spawned afterward inherits FRAIM_BROWSER_CDP_ENDPOINT and its
|
|
8475
8502
|
// browser-use skill connects to the long-lived window instead of a throwaway.
|
|
8476
|
-
async ensureManagedBrowser() {
|
|
8503
|
+
async ensureManagedBrowser(conversationId, url) {
|
|
8477
8504
|
const result = await this.managedBrowser.start();
|
|
8478
8505
|
process.env.FRAIM_BROWSER_CDP_ENDPOINT = result.endpoint;
|
|
8479
|
-
|
|
8506
|
+
const page = conversationId
|
|
8507
|
+
? await this.managedBrowser.ensureConversationPage(conversationId, url)
|
|
8508
|
+
: undefined;
|
|
8509
|
+
return { endpoint: result.endpoint, reused: result.reused, channel: result.channel, ...(page ? { page } : {}) };
|
|
8480
8510
|
}
|
|
8481
8511
|
// ─── Issue #578: Scheduled deployment helpers ─────────────────────────────
|
|
8482
8512
|
rehydrateScheduledDeployments() {
|
|
@@ -8734,7 +8764,7 @@ class AiHubServer {
|
|
|
8734
8764
|
this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = undefined; });
|
|
8735
8765
|
}),
|
|
8736
8766
|
};
|
|
8737
|
-
const childLaunchContext = this.
|
|
8767
|
+
const childLaunchContext = this.withBrowserConversationEnv(launchContext, run.id, run.conversationId || run.id);
|
|
8738
8768
|
this.recordHubGeneratedEvent(run, 'agent_input', instructions);
|
|
8739
8769
|
const child = existingHostSession
|
|
8740
8770
|
? this.hostRuntime.continueRun(hostId, effectiveDeployment.projectPath, existingHostSession.sessionId, instructions, handlers, childLaunchContext)
|
|
@@ -8917,7 +8947,7 @@ class AiHubServer {
|
|
|
8917
8947
|
this.scheduleRunConversationPersistence(updated, updated.conversationId || updated.id);
|
|
8918
8948
|
},
|
|
8919
8949
|
onExit: (code) => this.handleRunExit(runId, code, postPark),
|
|
8920
|
-
}, this.
|
|
8950
|
+
}, this.withBrowserConversationEnv(launchContext, runId, current.conversationId || current.id));
|
|
8921
8951
|
this.runRegistry.attachChildIfRunning(runId, child);
|
|
8922
8952
|
}, delay);
|
|
8923
8953
|
if (tid.unref)
|
|
@@ -113,12 +113,14 @@ function defaultCloneDir(layer) {
|
|
|
113
113
|
* Resolve where this layer's details live. Never throws and never touches the
|
|
114
114
|
* network; call `ensurePackClone` separately when the git working copy has to
|
|
115
115
|
* be present and current.
|
|
116
|
+
* Read-only inventory callers set `migrateLegacy: false` to suppress the
|
|
117
|
+
* otherwise backwards-compatible flat org layout migration.
|
|
116
118
|
*/
|
|
117
|
-
function resolvePackHome(layer) {
|
|
119
|
+
function resolvePackHome(layer, options = {}) {
|
|
118
120
|
const config = (0, user_config_1.getLayerBackend)(layer);
|
|
119
121
|
const standard = standardHome(layer);
|
|
120
122
|
const make = (root, extra) => {
|
|
121
|
-
if (layer === 'org')
|
|
123
|
+
if (layer === 'org' && options.migrateLegacy !== false)
|
|
122
124
|
migrateOrgFlatContent(root);
|
|
123
125
|
return { layer, root, contentRoot: path_1.default.join(root, 'personalized-employee'), ...extra };
|
|
124
126
|
};
|