fraim-hub 2.0.296 → 2.0.298
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.
|
@@ -630,12 +630,24 @@ async function bootstrap() {
|
|
|
630
630
|
});
|
|
631
631
|
await launchDesktopShell(options);
|
|
632
632
|
}
|
|
633
|
-
|
|
633
|
+
function isDirectElectronEntry() {
|
|
634
|
+
if (require.main === module)
|
|
635
|
+
return true;
|
|
636
|
+
const currentFile = path_1.default.resolve(__filename).toLowerCase();
|
|
637
|
+
return process.argv.some((arg) => {
|
|
638
|
+
try {
|
|
639
|
+
return path_1.default.resolve(arg).toLowerCase() === currentFile;
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
// Issue #1415: self-execution is gated on a direct Electron entry check in addition to the
|
|
634
647
|
// existing Electron-main-process check, so `desktop-launcher.ts` requiring this file (bundled or
|
|
635
|
-
// materialized) does not double-run bootstrap
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
if (require.main === module && process.versions.electron && process.type !== 'renderer') {
|
|
648
|
+
// materialized) does not double-run bootstrap. Electron's loader can own `require.main`, so
|
|
649
|
+
// direct test launches also match the resolved script path in argv.
|
|
650
|
+
if (isDirectElectronEntry() && process.versions.electron && process.type !== 'renderer') {
|
|
639
651
|
bootstrap().catch(async (error) => {
|
|
640
652
|
console.error(error instanceof Error ? error.message : error);
|
|
641
653
|
await stopServerOnce();
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -2257,13 +2257,15 @@ class FakeHostRuntime {
|
|
|
2257
2257
|
detectEmployees() {
|
|
2258
2258
|
return this.employees;
|
|
2259
2259
|
}
|
|
2260
|
-
startRun(hostId, _projectPath, message, handlers, _sessionId) {
|
|
2260
|
+
startRun(hostId, _projectPath, message, handlers, _sessionId, launchContext) {
|
|
2261
2261
|
this.lastStartMessage = message;
|
|
2262
|
+
this.lastLaunchContext = launchContext;
|
|
2262
2263
|
const reply = this.startReply ?? this.fakeEmployeeReply('start', message);
|
|
2263
2264
|
return this.fakeProcess(hostId, reply, handlers);
|
|
2264
2265
|
}
|
|
2265
|
-
continueRun(hostId, _projectPath, sessionId, message, handlers) {
|
|
2266
|
+
continueRun(hostId, _projectPath, sessionId, message, handlers, launchContext) {
|
|
2266
2267
|
this.lastContinueMessage = message;
|
|
2268
|
+
this.lastLaunchContext = launchContext;
|
|
2267
2269
|
return this.fakeProcess(hostId, this.fakeEmployeeReply('continue', message), handlers);
|
|
2268
2270
|
}
|
|
2269
2271
|
startDirectRun(hostId, _message, _projectPath, handlers, _sessionId) {
|
|
@@ -50,7 +50,7 @@ function buildCommunicationStyleNote() {
|
|
|
50
50
|
function buildBackgroundTaskPolicyNote() {
|
|
51
51
|
return [
|
|
52
52
|
'',
|
|
53
|
-
'[Background task policy] You are running in Hub headless mode. Never end your turn while background tasks (bash commands started with run_in_background=true, or sub-agents) are still active. Instead: monitor them, poll for results, and provide periodic progress commentary to the manager. Only end your turn when all background tasks have finished, or when you genuinely need a user decision or input to continue. If you exit while a background task is active, the Hub loses the link to that task permanently.',
|
|
53
|
+
'[Background task policy] You are running in Hub headless mode. Never end your turn while background tasks (bash commands started with run_in_background=true, or sub-agents) are still active. Instead: monitor them, poll for results, and provide periodic progress commentary to the manager. Only end your turn when all background tasks have finished, or when you genuinely need a user decision or input to continue. If you exit while a background task is active, the Hub loses the link to that task permanently. Background tasks and Monitor watchers cannot survive this turn ending: the next turn is a new process with no link to the old one (Windows: hard-killed; Unix: orphaned). The two things that reliably work: run the command synchronously in the foreground within this turn, or supervise it with `npx tsx ~/.fraim/scripts/exec-with-timeout.ts --supervise <name> --timeout <n> -- <command>` and poll `--status`/`--wait` (or `--stop` to cancel) without ending your turn.',
|
|
54
54
|
].join('\n');
|
|
55
55
|
}
|
|
56
56
|
// Issue #732: a plain continue of the SAME active job must not re-load the job.
|
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.findAvailablePortExcluding = exports.findAvailablePort = exports.AiHubServer = exports.HubConnectorStatusStore = exports.HostConfigStore = exports.DeploymentStore = void 0;
|
|
40
40
|
exports.selfHealBrokenManagedAgents = selfHealBrokenManagedAgents;
|
|
41
41
|
exports.configureFraimForHubAgent = configureFraimForHubAgent;
|
|
42
|
+
exports.installAgentAndRefreshDetection = installAgentAndRefreshDetection;
|
|
42
43
|
exports.hubCommandVersion = hubCommandVersion;
|
|
43
44
|
exports.buildOpenFileInvocation = buildOpenFileInvocation;
|
|
44
45
|
const express_1 = __importDefault(require("express"));
|
|
@@ -1760,6 +1761,30 @@ async function configureFraimForHubAgent(hubId) {
|
|
|
1760
1761
|
return { configured: false, error: e instanceof Error ? e.message : String(e) };
|
|
1761
1762
|
}
|
|
1762
1763
|
}
|
|
1764
|
+
// Issue #1429: extracted from the install-agent route so the post-install cache-invalidation
|
|
1765
|
+
// policy is directly unit-testable with fake deps (no real npm install, no running HTTP
|
|
1766
|
+
// server) rather than only reachable by exercising the whole route.
|
|
1767
|
+
//
|
|
1768
|
+
// Root cause of #1429: this used to invalidate only when `outcome.outcome === 'standard'`.
|
|
1769
|
+
// When the CLI is installed via FRAIM's own managed-Node fallback (`outcome === 'managed'` -
|
|
1770
|
+
// exactly what happens when a CLI is reachable only through FRAIM's managed install
|
|
1771
|
+
// directory, not the Hub process's inherited system PATH), the cache was never invalidated,
|
|
1772
|
+
// so `detectEmployees()` (which backs `configured-agents`/`reconfigure-surfaces`/the employee
|
|
1773
|
+
// roster) kept serving its pre-install "unavailable" answer for up to
|
|
1774
|
+
// `EMPLOYEE_DETECTION_TTL_MS` (5 minutes) while `check-agent`'s uncached `hubCommandVersion()`
|
|
1775
|
+
// found the freshly-installed managed copy immediately - the exact divergence reported.
|
|
1776
|
+
// `selfHealBrokenManagedAgents()` already establishes the correct pattern (invalidate
|
|
1777
|
+
// unconditionally after any successful install); this now matches it.
|
|
1778
|
+
async function installAgentAndRefreshDetection(option, systemPath, deps) {
|
|
1779
|
+
const outcome = await (0, managed_agent_install_1.installManagedAgent)(option, systemPath, deps);
|
|
1780
|
+
// Issue #1010/#1429: something is now installed that was not before - via a standard
|
|
1781
|
+
// system-PATH install OR FRAIM's own managed-Node fallback - so the cached agent-
|
|
1782
|
+
// availability answer is stale either way. Drop it here rather than waiting out the TTL,
|
|
1783
|
+
// so the newly installed CLI shows as available immediately regardless of which install
|
|
1784
|
+
// path produced it.
|
|
1785
|
+
(0, hosts_1.invalidateEmployeeDetectionCache)();
|
|
1786
|
+
return outcome;
|
|
1787
|
+
}
|
|
1763
1788
|
// Exported for direct unit testing of the Defect D fix (issue #1284/#1285):
|
|
1764
1789
|
// asserting this resolves via a real system install and skips a poisoned
|
|
1765
1790
|
// project-local node_modules/.bin entry, without spinning up a full server.
|
|
@@ -2199,6 +2224,7 @@ function buildHubBackgroundTaskContinueMessage(run) {
|
|
|
2199
2224
|
`Session id: ${run.sessionId || 'unknown'}`,
|
|
2200
2225
|
'The background task may still be running as an orphan process. Check whether its results are available on disk and continue your work.',
|
|
2201
2226
|
'If results are not yet available, wait briefly or check again. Once you have the results, continue with your original plan.',
|
|
2227
|
+
'Going forward, do not end your turn while this kind of task is active: either run it synchronously in the foreground, or supervise it with `npx tsx ~/.fraim/scripts/exec-with-timeout.ts --supervise <name> --timeout <n> -- <command>` and poll `--status`/`--wait` without ending your turn — ending the turn is what put this task at risk the first time.',
|
|
2202
2228
|
].join('\n');
|
|
2203
2229
|
}
|
|
2204
2230
|
// #1419 (Change 3): auto-resume when the only unresolved condition is a
|
|
@@ -2224,6 +2250,7 @@ function buildHubBackgroundTaskLostContinueMessage(run, killedDescription) {
|
|
|
2224
2250
|
`A background task was killed when a turn ended before it reached a terminal state: ${killedDescription || describeKilledBackgroundTasks(run)}.`,
|
|
2225
2251
|
'Its result is unknown, not passing or failing.',
|
|
2226
2252
|
'Check for partial output first; if none exists, re-run the work and say plainly that this is a re-run because the prior attempt\'s result was never observed.',
|
|
2253
|
+
'When you re-run it, do not repeat the mistake: run it synchronously in the foreground, or supervise it with `npx tsx ~/.fraim/scripts/exec-with-timeout.ts --supervise <name> --timeout <n> -- <command>` and poll `--status`/`--wait` without ending your turn — ending the turn is what killed it the first time.',
|
|
2227
2254
|
].join('\n');
|
|
2228
2255
|
}
|
|
2229
2256
|
function isHumanActionGate(run) {
|
|
@@ -3130,6 +3157,28 @@ class AiHubServer {
|
|
|
3130
3157
|
launchContext: { agent: resolved.agent, env },
|
|
3131
3158
|
};
|
|
3132
3159
|
}
|
|
3160
|
+
// Issue #1443: stamp the Hub's own run id into the spawned host process's
|
|
3161
|
+
// env so the local FRAIM MCP proxy (a grandchild of this spawn, wired to
|
|
3162
|
+
// the host CLI over stdio) can report run-scoped signals like executionMode
|
|
3163
|
+
// back to the correct run. The proxy has no other way to know it: the FRAIM
|
|
3164
|
+
// workflow/MCP session id it carries is minted independently of any Hub run,
|
|
3165
|
+
// and the host CLI's own session id (`run.sessionId`, set via
|
|
3166
|
+
// applyRunHostSessionSignal) is not known until the host reports it — often
|
|
3167
|
+
// well after the FRAIM MCP tools have already fired. FRAIM_HUB_RUN_ID is set
|
|
3168
|
+
// at spawn time instead, so it is available for the entire life of the
|
|
3169
|
+
// process. Call this at every startRun/continueRun call site (Direct-mode
|
|
3170
|
+
// spawns are exempt — they never load FRAIM, so there is nothing to report).
|
|
3171
|
+
withHubRunIdEnv(launchContext, runId) {
|
|
3172
|
+
return { ...launchContext, env: { ...(launchContext.env || {}), FRAIM_HUB_RUN_ID: runId } };
|
|
3173
|
+
}
|
|
3174
|
+
// Shared by both the runId-keyed and legacy by-session execution-mode routes.
|
|
3175
|
+
applyExecutionModeToRun(runId, body) {
|
|
3176
|
+
const normalizedMode = body.mode === 'trusted' ? 'trusted' : 'coached';
|
|
3177
|
+
const normalizedRuns = typeof body.completedRuns === 'number' ? body.completedRuns : 0;
|
|
3178
|
+
this.runRegistry.update(runId, (current) => {
|
|
3179
|
+
current.executionMode = { mode: normalizedMode, completedRuns: normalizedRuns };
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3133
3182
|
configuredAgentIdForConversation(conversation, employees) {
|
|
3134
3183
|
const existingConfiguredAgentId = typeof conversation.configuredAgentId === 'string'
|
|
3135
3184
|
? conversation.configuredAgentId.trim()
|
|
@@ -3328,9 +3377,17 @@ class AiHubServer {
|
|
|
3328
3377
|
conversationRecordFromRun(run) {
|
|
3329
3378
|
const lastUpdatedAt = run.updatedAt || new Date().toISOString();
|
|
3330
3379
|
const stages = deriveStages(run, run.projectPath);
|
|
3380
|
+
const rawScope = run.scope
|
|
3381
|
+
?? run.invokedArea;
|
|
3382
|
+
const recordScope = rawScope === 'manager' || rawScope === 'company' ? rawScope : 'project';
|
|
3383
|
+
const recordProjectPath = recordScope === 'manager'
|
|
3384
|
+
? conversation_store_1.MANAGER_SCOPE_KEY
|
|
3385
|
+
: recordScope === 'company'
|
|
3386
|
+
? conversation_store_1.COMPANY_SCOPE_KEY
|
|
3387
|
+
: path_1.default.resolve(run.projectPath);
|
|
3331
3388
|
const record = {
|
|
3332
3389
|
id: run.conversationId || run.id,
|
|
3333
|
-
projectPath:
|
|
3390
|
+
projectPath: recordProjectPath,
|
|
3334
3391
|
title: run.conversationTitle || run.jobTitle || run.jobId,
|
|
3335
3392
|
jobId: run.jobId,
|
|
3336
3393
|
jobTitle: run.jobTitle || run.jobId,
|
|
@@ -3392,9 +3449,7 @@ class AiHubServer {
|
|
|
3392
3449
|
sourceTrigger: run.sourceTrigger,
|
|
3393
3450
|
// Issue #708: carry the invocation scope so the record lands in (and is keyed to)
|
|
3394
3451
|
// the right bucket. Falls back to the legacy client `invokedArea` when present.
|
|
3395
|
-
scope:
|
|
3396
|
-
?? run.invokedArea
|
|
3397
|
-
?? 'project',
|
|
3452
|
+
scope: recordScope,
|
|
3398
3453
|
run: {
|
|
3399
3454
|
stages,
|
|
3400
3455
|
currentPhase: run.currentPhase || null,
|
|
@@ -3509,7 +3564,7 @@ class AiHubServer {
|
|
|
3509
3564
|
this.maybeStartDelegatedChildRuns(updated);
|
|
3510
3565
|
});
|
|
3511
3566
|
},
|
|
3512
|
-
}, startSessionSeedForHost(run.hostId, run.id), freshLaunch.launchContext);
|
|
3567
|
+
}, startSessionSeedForHost(run.hostId, run.id), this.withHubRunIdEnv(freshLaunch.launchContext, run.id));
|
|
3513
3568
|
this.runRegistry.attachChildIfRunning(run.id, freshChild);
|
|
3514
3569
|
}
|
|
3515
3570
|
scheduleRunConversationPersistence(run, activeId) {
|
|
@@ -3802,7 +3857,7 @@ class AiHubServer {
|
|
|
3802
3857
|
onExit: (exitCode) => {
|
|
3803
3858
|
this.handleRunExit(currentRun.id, exitCode, options.postPark);
|
|
3804
3859
|
},
|
|
3805
|
-
}, launch.launchContext);
|
|
3860
|
+
}, this.withHubRunIdEnv(launch.launchContext, currentRun.id));
|
|
3806
3861
|
this.runRegistry.attachChildIfRunning(currentRun.id, child);
|
|
3807
3862
|
const updated = this.runRegistry.get(currentRun.id);
|
|
3808
3863
|
if (updated)
|
|
@@ -4133,7 +4188,7 @@ class AiHubServer {
|
|
|
4133
4188
|
}
|
|
4134
4189
|
this.runRegistry.dispose(childRun.id);
|
|
4135
4190
|
},
|
|
4136
|
-
}, startSessionSeedForHost(managerRun.hostId, childRun.id), childLaunch.launchContext);
|
|
4191
|
+
}, startSessionSeedForHost(managerRun.hostId, childRun.id), this.withHubRunIdEnv(childLaunch.launchContext, childRun.id));
|
|
4137
4192
|
this.runRegistry.attachChildIfRunning(childRun.id, child);
|
|
4138
4193
|
}
|
|
4139
4194
|
notifyManagerOfDelegatedChild(managerRunId, childRun) {
|
|
@@ -4290,7 +4345,7 @@ class AiHubServer {
|
|
|
4290
4345
|
if (latestManager)
|
|
4291
4346
|
this.drainPendingDelegatedReviews(latestManager);
|
|
4292
4347
|
},
|
|
4293
|
-
}, managerLaunch.launchContext);
|
|
4348
|
+
}, this.withHubRunIdEnv(managerLaunch.launchContext, managerRun.id));
|
|
4294
4349
|
this.runRegistry.attachChildIfRunning(managerRun.id, child);
|
|
4295
4350
|
}
|
|
4296
4351
|
computeFirstRun(projectPath, jobCount, personas) {
|
|
@@ -5227,8 +5282,8 @@ class AiHubServer {
|
|
|
5227
5282
|
},
|
|
5228
5283
|
};
|
|
5229
5284
|
const child = strategy === 'resume_same_host'
|
|
5230
|
-
? this.hostRuntime.continueRun(run.hostId, run.projectPath, priorSessionId, hostMessage, handlers, resolved.launchContext)
|
|
5231
|
-
: this.hostRuntime.startRun(run.hostId, run.projectPath, hostMessage, handlers, startSessionSeedForHost(run.hostId, run.id), resolved.launchContext);
|
|
5285
|
+
? this.hostRuntime.continueRun(run.hostId, run.projectPath, priorSessionId, hostMessage, handlers, this.withHubRunIdEnv(resolved.launchContext, run.id))
|
|
5286
|
+
: this.hostRuntime.startRun(run.hostId, run.projectPath, hostMessage, handlers, startSessionSeedForHost(run.hostId, run.id), this.withHubRunIdEnv(resolved.launchContext, run.id));
|
|
5232
5287
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
5233
5288
|
attemptedRunId = null;
|
|
5234
5289
|
console.info(`[ai-hub] hub.agent_switch.${strategy}`, {
|
|
@@ -5693,7 +5748,7 @@ class AiHubServer {
|
|
|
5693
5748
|
loginHint: `Once installed, click "Check if Ready" to verify ${option.label} is on your PATH.`,
|
|
5694
5749
|
});
|
|
5695
5750
|
}
|
|
5696
|
-
const outcome = await (
|
|
5751
|
+
const outcome = await installAgentAndRefreshDetection({ label: option.label, installPackage: option.installPackage, launchCommand: option.launchCommand }, systemPath, { runProcess: hubRunProcess, commandVersion: hubCommandVersion });
|
|
5697
5752
|
if (outcome.outcome === 'standard' && outcome.npmGlobalBinDirs.length > 0) {
|
|
5698
5753
|
process.env.PATH = (0, managed_agent_paths_1.appendBinDirsToPath)(systemPath, outcome.npmGlobalBinDirs);
|
|
5699
5754
|
}
|
|
@@ -5701,12 +5756,6 @@ class AiHubServer {
|
|
|
5701
5756
|
if (!mcp.configured) {
|
|
5702
5757
|
console.warn(`[ai-hub] install-agent: FRAIM add-ide did not run for ${option.label} (${outcome.outcome} install): ${mcp.error || 'unknown reason'}`);
|
|
5703
5758
|
}
|
|
5704
|
-
if (outcome.outcome === 'standard') {
|
|
5705
|
-
// Issue #1010: something is now installed that was not before, so the cached
|
|
5706
|
-
// agent-availability answer is stale. Drop it here rather than waiting out the
|
|
5707
|
-
// TTL, so the newly installed CLI shows as available immediately.
|
|
5708
|
-
(0, hosts_1.invalidateEmployeeDetectionCache)();
|
|
5709
|
-
}
|
|
5710
5759
|
return res.json({
|
|
5711
5760
|
ok: true,
|
|
5712
5761
|
message: `${option.label} installed successfully.`,
|
|
@@ -5832,21 +5881,35 @@ class AiHubServer {
|
|
|
5832
5881
|
});
|
|
5833
5882
|
// Issue #1345: accept executionMode forwarded from get_fraim_job response by the local proxy.
|
|
5834
5883
|
// The proxy parses the Execution Mode Context block and POSTs here; Hub script reads from run poll.
|
|
5884
|
+
//
|
|
5885
|
+
// Issue #1443: the local proxy's only stable identity for "which run is this" is the
|
|
5886
|
+
// Hub's own run id (see withHubRunIdEnv / FRAIM_HUB_RUN_ID) — the FRAIM/MCP session id it
|
|
5887
|
+
// carries is unrelated to any Hub run, and the host CLI's own session id (run.sessionId)
|
|
5888
|
+
// often isn't known yet when the first get_fraim_job call fires. Runs by id, not session.
|
|
5889
|
+
this.app.post('/api/ai-hub/runs/:runId/execution-mode', (req, res) => {
|
|
5890
|
+
if (!this.requireTrustedHubOrigin(req, res))
|
|
5891
|
+
return;
|
|
5892
|
+
const run = this.runRegistry.get(req.params.runId);
|
|
5893
|
+
if (!run)
|
|
5894
|
+
return res.status(404).json({ error: 'Run not found.' });
|
|
5895
|
+
this.applyExecutionModeToRun(run.id, req.body);
|
|
5896
|
+
return res.json({ ok: true });
|
|
5897
|
+
});
|
|
5898
|
+
// Legacy fallback for a local proxy build that predates #1443 and has not yet been
|
|
5899
|
+
// updated to send FRAIM_HUB_RUN_ID. Session-id correlation is unreliable by construction
|
|
5900
|
+
// (see the comment above), so this only succeeds when a run happens to already carry a
|
|
5901
|
+
// matching sessionId; it exists to avoid a hard 404 during a mixed-version rollout, not as
|
|
5902
|
+
// a supported correlation path.
|
|
5835
5903
|
this.app.post('/api/ai-hub/runs/by-session/:sessionId/execution-mode', (req, res) => {
|
|
5836
5904
|
if (!this.requireTrustedHubOrigin(req, res))
|
|
5837
5905
|
return;
|
|
5838
5906
|
const { sessionId } = req.params;
|
|
5839
|
-
const { mode, completedRuns } = req.body;
|
|
5840
5907
|
if (!sessionId)
|
|
5841
5908
|
return res.status(400).json({ error: 'sessionId required' });
|
|
5842
|
-
const normalizedMode = mode === 'trusted' ? 'trusted' : 'coached';
|
|
5843
|
-
const normalizedRuns = typeof completedRuns === 'number' ? completedRuns : 0;
|
|
5844
5909
|
const run = this.runRegistry.all().find((r) => r.sessionId === sessionId);
|
|
5845
5910
|
if (!run)
|
|
5846
5911
|
return res.status(404).json({ error: 'Run not found for session.' });
|
|
5847
|
-
this.
|
|
5848
|
-
current.executionMode = { mode: normalizedMode, completedRuns: normalizedRuns };
|
|
5849
|
-
});
|
|
5912
|
+
this.applyExecutionModeToRun(run.id, req.body);
|
|
5850
5913
|
return res.json({ ok: true });
|
|
5851
5914
|
});
|
|
5852
5915
|
this.app.post('/api/ai-hub/runs', (req, res) => {
|
|
@@ -6031,7 +6094,7 @@ class AiHubServer {
|
|
|
6031
6094
|
this.maybeStartDelegatedChildRuns(updated);
|
|
6032
6095
|
});
|
|
6033
6096
|
},
|
|
6034
|
-
}, startSessionSeedForHost(hostId, run.id), launchContext);
|
|
6097
|
+
}, startSessionSeedForHost(hostId, run.id), this.withHubRunIdEnv(launchContext, run.id));
|
|
6035
6098
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
6036
6099
|
// Issue #442: spawn the Direct run via startDirectRun so CliHostRuntime
|
|
6037
6100
|
// uses buildDirectStartPlan (--strict-mcp-config, raw stdin) rather than
|
|
@@ -6250,7 +6313,7 @@ class AiHubServer {
|
|
|
6250
6313
|
this.maybeStartDelegatedChildRuns(updated);
|
|
6251
6314
|
});
|
|
6252
6315
|
},
|
|
6253
|
-
}, startSessionSeedForHost(run.hostId, run.id), freshLaunch.launchContext);
|
|
6316
|
+
}, startSessionSeedForHost(run.hostId, run.id), this.withHubRunIdEnv(freshLaunch.launchContext, run.id));
|
|
6254
6317
|
this.runRegistry.attachChildIfRunning(run.id, freshChild);
|
|
6255
6318
|
const refreshedFresh = this.runRegistry.get(run.id);
|
|
6256
6319
|
return res.json(refreshedFresh ? this.enrichRunForResponse(refreshedFresh) : refreshedFresh);
|
|
@@ -6335,7 +6398,7 @@ class AiHubServer {
|
|
|
6335
6398
|
this.maybeStartDelegatedChildRuns(updated);
|
|
6336
6399
|
});
|
|
6337
6400
|
},
|
|
6338
|
-
}, continueLaunch.launchContext, deliveryIntent);
|
|
6401
|
+
}, this.withHubRunIdEnv(continueLaunch.launchContext, run.id), deliveryIntent);
|
|
6339
6402
|
if (!startedCodexReviewApprovalFallback)
|
|
6340
6403
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
6341
6404
|
const refreshed = this.runRegistry.get(run.id);
|
|
@@ -6494,7 +6557,7 @@ class AiHubServer {
|
|
|
6494
6557
|
onExit: (exitCode) => {
|
|
6495
6558
|
this.handleRunExit(run.id, exitCode);
|
|
6496
6559
|
},
|
|
6497
|
-
}, launchContext);
|
|
6560
|
+
}, this.withHubRunIdEnv(launchContext, run.id));
|
|
6498
6561
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
6499
6562
|
res.status(201).json(this.enrichRunForResponse(this.runRegistry.get(run.id) ?? run));
|
|
6500
6563
|
}
|
|
@@ -7263,7 +7326,7 @@ class AiHubServer {
|
|
|
7263
7326
|
this.finalizeRunConversationProjection(run.id, updated.conversationId || updated.id);
|
|
7264
7327
|
this.runRegistry.dispose(run.id);
|
|
7265
7328
|
},
|
|
7266
|
-
}, startSessionSeedForHost(hostId, run.id), launchContext);
|
|
7329
|
+
}, startSessionSeedForHost(hostId, run.id), this.withHubRunIdEnv(launchContext, run.id));
|
|
7267
7330
|
// Update the registry entry with the real child process handle.
|
|
7268
7331
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
7269
7332
|
return res.json({ runId: run.id, status: 'started', employee: employeeId, job: jobName });
|
|
@@ -7423,7 +7486,7 @@ class AiHubServer {
|
|
|
7423
7486
|
this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = undefined; });
|
|
7424
7487
|
this.runRegistry.dispose(run.id);
|
|
7425
7488
|
},
|
|
7426
|
-
}, startSessionSeedForHost(hostId, run.id), launchContext);
|
|
7489
|
+
}, startSessionSeedForHost(hostId, run.id), this.withHubRunIdEnv(launchContext, run.id));
|
|
7427
7490
|
this.runRegistry.create(run, child);
|
|
7428
7491
|
return run;
|
|
7429
7492
|
}
|
|
@@ -7579,7 +7642,7 @@ class AiHubServer {
|
|
|
7579
7642
|
this.scheduleRunConversationPersistence(updated, updated.conversationId || updated.id);
|
|
7580
7643
|
},
|
|
7581
7644
|
onExit: (code) => this.handleRunExit(runId, code, postPark),
|
|
7582
|
-
}, launchContext);
|
|
7645
|
+
}, this.withHubRunIdEnv(launchContext, runId));
|
|
7583
7646
|
this.runRegistry.attachChildIfRunning(runId, child);
|
|
7584
7647
|
}, delay);
|
|
7585
7648
|
if (tid.unref)
|
|
@@ -172,6 +172,8 @@ class FraimDbService {
|
|
|
172
172
|
this.orgAuditCollection = this.db.collection('fraim_org_audit');
|
|
173
173
|
// Issue #1345 — job execution modes (Coached/Trusted).
|
|
174
174
|
this.jobExecutionModesCollection = this.db.collection('fraim_job_execution_modes');
|
|
175
|
+
// Issue #1431 — per-user referral codes.
|
|
176
|
+
this.referralCodesCollection = this.db.collection('fraim_referral_codes');
|
|
175
177
|
}
|
|
176
178
|
async initializeIndexes() {
|
|
177
179
|
if (!this.db)
|
|
@@ -252,6 +254,9 @@ class FraimDbService {
|
|
|
252
254
|
await this.auditLogCollection.createIndex({ sequence: 1 }).catch(() => { });
|
|
253
255
|
await this.auditLogCollection.createIndex({ userId: 1, ts: -1 }).catch(() => { });
|
|
254
256
|
await this.auditLogCollection.createIndex({ ts: -1 }).catch(() => { });
|
|
257
|
+
// Issue #1431 — referral codes: one code per user, globally unique codes.
|
|
258
|
+
await this.referralCodesCollection.createIndex({ userId: 1 }, { unique: true }).catch(() => { });
|
|
259
|
+
await this.referralCodesCollection.createIndex({ code: 1 }, { unique: true }).catch(() => { });
|
|
255
260
|
}
|
|
256
261
|
async createSession(session) {
|
|
257
262
|
if (!this.sessionsCollection)
|
|
@@ -613,6 +618,65 @@ class FraimDbService {
|
|
|
613
618
|
throw new Error('DB not connected');
|
|
614
619
|
return await this.signupsCollection.findOne({ email });
|
|
615
620
|
}
|
|
621
|
+
async setSignupAttribution(email, data) {
|
|
622
|
+
if (!this.signupsCollection)
|
|
623
|
+
throw new Error('DB not connected');
|
|
624
|
+
const now = new Date();
|
|
625
|
+
await this.signupsCollection.updateOne({ email }, {
|
|
626
|
+
$setOnInsert: {
|
|
627
|
+
email,
|
|
628
|
+
name: '',
|
|
629
|
+
company: '',
|
|
630
|
+
source: data.source,
|
|
631
|
+
timestamp: now,
|
|
632
|
+
ipAddress: data.ipAddress,
|
|
633
|
+
userAgent: data.userAgent,
|
|
634
|
+
...(data.referrerName ? { referrerName: data.referrerName } : {}),
|
|
635
|
+
...(data.referralCodeUsed ? { referralCodeUsed: data.referralCodeUsed } : {}),
|
|
636
|
+
},
|
|
637
|
+
}, { upsert: true });
|
|
638
|
+
}
|
|
639
|
+
generateReferralCodeString() {
|
|
640
|
+
const crypto = require('crypto');
|
|
641
|
+
const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
642
|
+
const bytes = crypto.randomBytes(12);
|
|
643
|
+
return Array.from(bytes).map((b) => ALPHABET[b % ALPHABET.length]).join('');
|
|
644
|
+
}
|
|
645
|
+
async getReferralCode(userId) {
|
|
646
|
+
if (!this.referralCodesCollection)
|
|
647
|
+
throw new Error('DB not connected');
|
|
648
|
+
return await this.referralCodesCollection.findOne({ userId });
|
|
649
|
+
}
|
|
650
|
+
async getOrCreateReferralCode(userId) {
|
|
651
|
+
if (!this.referralCodesCollection)
|
|
652
|
+
throw new Error('DB not connected');
|
|
653
|
+
const existing = await this.referralCodesCollection.findOne({ userId });
|
|
654
|
+
if (existing)
|
|
655
|
+
return existing.code;
|
|
656
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
657
|
+
const code = this.generateReferralCodeString();
|
|
658
|
+
try {
|
|
659
|
+
await this.referralCodesCollection.insertOne({ userId, code, createdAt: new Date() });
|
|
660
|
+
return code;
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
if (err?.code === 11000) {
|
|
664
|
+
const row = await this.referralCodesCollection.findOne({ userId });
|
|
665
|
+
if (row)
|
|
666
|
+
return row.code;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
throw err;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
throw new Error('Failed to generate unique referral code after retries');
|
|
673
|
+
}
|
|
674
|
+
async validateReferralCode(code) {
|
|
675
|
+
if (!this.referralCodesCollection)
|
|
676
|
+
throw new Error('DB not connected');
|
|
677
|
+
const exists = await this.referralCodesCollection.findOne({ code });
|
|
678
|
+
return exists !== null;
|
|
679
|
+
}
|
|
616
680
|
/** Get existing API key by userId (email for self-serve) */
|
|
617
681
|
async getApiKeyByUserId(userId, activeOnly = true) {
|
|
618
682
|
if (!this.db)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.298",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"author": "Sid Mathur <sid.mathur@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/mathursrus/FRAIM#readme",
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
"electron-updater": "^6.8.9",
|
|
212
212
|
"express": "^5.2.1",
|
|
213
213
|
"extract-zip": "^2.0.1",
|
|
214
|
-
"fraim": "2.0.
|
|
214
|
+
"fraim": "2.0.298",
|
|
215
215
|
"mongodb": "^7.0.0",
|
|
216
216
|
"node-cron": "4.2.1",
|
|
217
217
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/script.js
CHANGED
|
@@ -8515,6 +8515,12 @@ function convNeedsPolling(conv) {
|
|
|
8515
8515
|
}
|
|
8516
8516
|
|
|
8517
8517
|
function switchToConversation(id) {
|
|
8518
|
+
const conv = findConversation(id);
|
|
8519
|
+
const scope = convScope(conv);
|
|
8520
|
+
const targetArea = scope === 'manager' || scope === 'company' ? scope : 'projects';
|
|
8521
|
+
if (conv && tf.area !== targetArea) {
|
|
8522
|
+
tfShowArea(targetArea);
|
|
8523
|
+
}
|
|
8518
8524
|
state.activeId = id;
|
|
8519
8525
|
persistActiveConversationSelection(id);
|
|
8520
8526
|
// Issue #1065: a find session belongs to one thread. srchOpenResult re-opens it with the search
|
|
@@ -8522,23 +8528,23 @@ function switchToConversation(id) {
|
|
|
8522
8528
|
if (typeof convFindClose === 'function') convFindClose();
|
|
8523
8529
|
// For Company/Manager: move the shared .page into this area's conv-host and
|
|
8524
8530
|
// switch from the info view to the full coaching panel.
|
|
8525
|
-
if (
|
|
8526
|
-
tfEnsurePageInArea(
|
|
8527
|
-
const host = document.getElementById(
|
|
8528
|
-
const info = document.getElementById(
|
|
8531
|
+
if (targetArea === 'company' || targetArea === 'manager') {
|
|
8532
|
+
tfEnsurePageInArea(targetArea);
|
|
8533
|
+
const host = document.getElementById(targetArea + '-conv-host');
|
|
8534
|
+
const info = document.getElementById(targetArea + '-info-view');
|
|
8529
8535
|
if (host) host.hidden = false;
|
|
8530
8536
|
if (info) info.hidden = true;
|
|
8531
8537
|
// #1397: an area selection represents every explicit destination, not only
|
|
8532
8538
|
// the Info view. Poll-driven renders must preserve this conversation until
|
|
8533
8539
|
// the user chooses another destination or the conversation disappears.
|
|
8534
|
-
if (tf.areaSelection) tf.areaSelection[
|
|
8540
|
+
if (tf.areaSelection) tf.areaSelection[targetArea] = id;
|
|
8535
8541
|
}
|
|
8536
8542
|
renderRail();
|
|
8537
8543
|
renderActive();
|
|
8538
|
-
hydrateConversationBody(state.projectPath, id).catch((error) =>
|
|
8544
|
+
hydrateConversationBody(conv ? convBucketKey(conv) : state.projectPath, id).catch((error) =>
|
|
8539
8545
|
console.warn('Could not hydrate switched conversation body:', error));
|
|
8540
|
-
const
|
|
8541
|
-
if (convNeedsPolling(
|
|
8546
|
+
const active = activeConversation();
|
|
8547
|
+
if (convNeedsPolling(active)) {
|
|
8542
8548
|
startPolling();
|
|
8543
8549
|
} else if (state.pollHandle) {
|
|
8544
8550
|
window.clearInterval(state.pollHandle);
|
|
@@ -11050,7 +11056,9 @@ async function tfSelectProjectView(view, projectId) {
|
|
|
11050
11056
|
if (tab.id === 'ptab-add') continue;
|
|
11051
11057
|
const isOverview = tab.id === 'ptab-overview';
|
|
11052
11058
|
const matchesProj = tab.dataset.projectId === tf.activeProjectId;
|
|
11053
|
-
|
|
11059
|
+
const isOn = view === 'overview' ? isOverview : matchesProj;
|
|
11060
|
+
tab.classList.toggle('on', isOn);
|
|
11061
|
+
if (isOn && matchesProj) tab.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' });
|
|
11054
11062
|
}
|
|
11055
11063
|
if (view === 'overview') {
|
|
11056
11064
|
tfRenderOverview();
|
|
@@ -11187,6 +11195,8 @@ function tfRenderProjectTabs() {
|
|
|
11187
11195
|
btn.addEventListener('click', () => tfSelectProjectView('workspace', proj.id));
|
|
11188
11196
|
host.appendChild(btn);
|
|
11189
11197
|
}
|
|
11198
|
+
const activeTab = host.querySelector('.ptab.on');
|
|
11199
|
+
if (activeTab) activeTab.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' });
|
|
11190
11200
|
}
|
|
11191
11201
|
|
|
11192
11202
|
function tfRenderOverview() {
|