fraim-hub 2.0.297 → 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()
|
|
@@ -3515,7 +3564,7 @@ class AiHubServer {
|
|
|
3515
3564
|
this.maybeStartDelegatedChildRuns(updated);
|
|
3516
3565
|
});
|
|
3517
3566
|
},
|
|
3518
|
-
}, startSessionSeedForHost(run.hostId, run.id), freshLaunch.launchContext);
|
|
3567
|
+
}, startSessionSeedForHost(run.hostId, run.id), this.withHubRunIdEnv(freshLaunch.launchContext, run.id));
|
|
3519
3568
|
this.runRegistry.attachChildIfRunning(run.id, freshChild);
|
|
3520
3569
|
}
|
|
3521
3570
|
scheduleRunConversationPersistence(run, activeId) {
|
|
@@ -3808,7 +3857,7 @@ class AiHubServer {
|
|
|
3808
3857
|
onExit: (exitCode) => {
|
|
3809
3858
|
this.handleRunExit(currentRun.id, exitCode, options.postPark);
|
|
3810
3859
|
},
|
|
3811
|
-
}, launch.launchContext);
|
|
3860
|
+
}, this.withHubRunIdEnv(launch.launchContext, currentRun.id));
|
|
3812
3861
|
this.runRegistry.attachChildIfRunning(currentRun.id, child);
|
|
3813
3862
|
const updated = this.runRegistry.get(currentRun.id);
|
|
3814
3863
|
if (updated)
|
|
@@ -4139,7 +4188,7 @@ class AiHubServer {
|
|
|
4139
4188
|
}
|
|
4140
4189
|
this.runRegistry.dispose(childRun.id);
|
|
4141
4190
|
},
|
|
4142
|
-
}, startSessionSeedForHost(managerRun.hostId, childRun.id), childLaunch.launchContext);
|
|
4191
|
+
}, startSessionSeedForHost(managerRun.hostId, childRun.id), this.withHubRunIdEnv(childLaunch.launchContext, childRun.id));
|
|
4143
4192
|
this.runRegistry.attachChildIfRunning(childRun.id, child);
|
|
4144
4193
|
}
|
|
4145
4194
|
notifyManagerOfDelegatedChild(managerRunId, childRun) {
|
|
@@ -4296,7 +4345,7 @@ class AiHubServer {
|
|
|
4296
4345
|
if (latestManager)
|
|
4297
4346
|
this.drainPendingDelegatedReviews(latestManager);
|
|
4298
4347
|
},
|
|
4299
|
-
}, managerLaunch.launchContext);
|
|
4348
|
+
}, this.withHubRunIdEnv(managerLaunch.launchContext, managerRun.id));
|
|
4300
4349
|
this.runRegistry.attachChildIfRunning(managerRun.id, child);
|
|
4301
4350
|
}
|
|
4302
4351
|
computeFirstRun(projectPath, jobCount, personas) {
|
|
@@ -5233,8 +5282,8 @@ class AiHubServer {
|
|
|
5233
5282
|
},
|
|
5234
5283
|
};
|
|
5235
5284
|
const child = strategy === 'resume_same_host'
|
|
5236
|
-
? this.hostRuntime.continueRun(run.hostId, run.projectPath, priorSessionId, hostMessage, handlers, resolved.launchContext)
|
|
5237
|
-
: 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));
|
|
5238
5287
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
5239
5288
|
attemptedRunId = null;
|
|
5240
5289
|
console.info(`[ai-hub] hub.agent_switch.${strategy}`, {
|
|
@@ -5699,7 +5748,7 @@ class AiHubServer {
|
|
|
5699
5748
|
loginHint: `Once installed, click "Check if Ready" to verify ${option.label} is on your PATH.`,
|
|
5700
5749
|
});
|
|
5701
5750
|
}
|
|
5702
|
-
const outcome = await (
|
|
5751
|
+
const outcome = await installAgentAndRefreshDetection({ label: option.label, installPackage: option.installPackage, launchCommand: option.launchCommand }, systemPath, { runProcess: hubRunProcess, commandVersion: hubCommandVersion });
|
|
5703
5752
|
if (outcome.outcome === 'standard' && outcome.npmGlobalBinDirs.length > 0) {
|
|
5704
5753
|
process.env.PATH = (0, managed_agent_paths_1.appendBinDirsToPath)(systemPath, outcome.npmGlobalBinDirs);
|
|
5705
5754
|
}
|
|
@@ -5707,12 +5756,6 @@ class AiHubServer {
|
|
|
5707
5756
|
if (!mcp.configured) {
|
|
5708
5757
|
console.warn(`[ai-hub] install-agent: FRAIM add-ide did not run for ${option.label} (${outcome.outcome} install): ${mcp.error || 'unknown reason'}`);
|
|
5709
5758
|
}
|
|
5710
|
-
if (outcome.outcome === 'standard') {
|
|
5711
|
-
// Issue #1010: something is now installed that was not before, so the cached
|
|
5712
|
-
// agent-availability answer is stale. Drop it here rather than waiting out the
|
|
5713
|
-
// TTL, so the newly installed CLI shows as available immediately.
|
|
5714
|
-
(0, hosts_1.invalidateEmployeeDetectionCache)();
|
|
5715
|
-
}
|
|
5716
5759
|
return res.json({
|
|
5717
5760
|
ok: true,
|
|
5718
5761
|
message: `${option.label} installed successfully.`,
|
|
@@ -5838,21 +5881,35 @@ class AiHubServer {
|
|
|
5838
5881
|
});
|
|
5839
5882
|
// Issue #1345: accept executionMode forwarded from get_fraim_job response by the local proxy.
|
|
5840
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.
|
|
5841
5903
|
this.app.post('/api/ai-hub/runs/by-session/:sessionId/execution-mode', (req, res) => {
|
|
5842
5904
|
if (!this.requireTrustedHubOrigin(req, res))
|
|
5843
5905
|
return;
|
|
5844
5906
|
const { sessionId } = req.params;
|
|
5845
|
-
const { mode, completedRuns } = req.body;
|
|
5846
5907
|
if (!sessionId)
|
|
5847
5908
|
return res.status(400).json({ error: 'sessionId required' });
|
|
5848
|
-
const normalizedMode = mode === 'trusted' ? 'trusted' : 'coached';
|
|
5849
|
-
const normalizedRuns = typeof completedRuns === 'number' ? completedRuns : 0;
|
|
5850
5909
|
const run = this.runRegistry.all().find((r) => r.sessionId === sessionId);
|
|
5851
5910
|
if (!run)
|
|
5852
5911
|
return res.status(404).json({ error: 'Run not found for session.' });
|
|
5853
|
-
this.
|
|
5854
|
-
current.executionMode = { mode: normalizedMode, completedRuns: normalizedRuns };
|
|
5855
|
-
});
|
|
5912
|
+
this.applyExecutionModeToRun(run.id, req.body);
|
|
5856
5913
|
return res.json({ ok: true });
|
|
5857
5914
|
});
|
|
5858
5915
|
this.app.post('/api/ai-hub/runs', (req, res) => {
|
|
@@ -6037,7 +6094,7 @@ class AiHubServer {
|
|
|
6037
6094
|
this.maybeStartDelegatedChildRuns(updated);
|
|
6038
6095
|
});
|
|
6039
6096
|
},
|
|
6040
|
-
}, startSessionSeedForHost(hostId, run.id), launchContext);
|
|
6097
|
+
}, startSessionSeedForHost(hostId, run.id), this.withHubRunIdEnv(launchContext, run.id));
|
|
6041
6098
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
6042
6099
|
// Issue #442: spawn the Direct run via startDirectRun so CliHostRuntime
|
|
6043
6100
|
// uses buildDirectStartPlan (--strict-mcp-config, raw stdin) rather than
|
|
@@ -6256,7 +6313,7 @@ class AiHubServer {
|
|
|
6256
6313
|
this.maybeStartDelegatedChildRuns(updated);
|
|
6257
6314
|
});
|
|
6258
6315
|
},
|
|
6259
|
-
}, startSessionSeedForHost(run.hostId, run.id), freshLaunch.launchContext);
|
|
6316
|
+
}, startSessionSeedForHost(run.hostId, run.id), this.withHubRunIdEnv(freshLaunch.launchContext, run.id));
|
|
6260
6317
|
this.runRegistry.attachChildIfRunning(run.id, freshChild);
|
|
6261
6318
|
const refreshedFresh = this.runRegistry.get(run.id);
|
|
6262
6319
|
return res.json(refreshedFresh ? this.enrichRunForResponse(refreshedFresh) : refreshedFresh);
|
|
@@ -6341,7 +6398,7 @@ class AiHubServer {
|
|
|
6341
6398
|
this.maybeStartDelegatedChildRuns(updated);
|
|
6342
6399
|
});
|
|
6343
6400
|
},
|
|
6344
|
-
}, continueLaunch.launchContext, deliveryIntent);
|
|
6401
|
+
}, this.withHubRunIdEnv(continueLaunch.launchContext, run.id), deliveryIntent);
|
|
6345
6402
|
if (!startedCodexReviewApprovalFallback)
|
|
6346
6403
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
6347
6404
|
const refreshed = this.runRegistry.get(run.id);
|
|
@@ -6500,7 +6557,7 @@ class AiHubServer {
|
|
|
6500
6557
|
onExit: (exitCode) => {
|
|
6501
6558
|
this.handleRunExit(run.id, exitCode);
|
|
6502
6559
|
},
|
|
6503
|
-
}, launchContext);
|
|
6560
|
+
}, this.withHubRunIdEnv(launchContext, run.id));
|
|
6504
6561
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
6505
6562
|
res.status(201).json(this.enrichRunForResponse(this.runRegistry.get(run.id) ?? run));
|
|
6506
6563
|
}
|
|
@@ -7269,7 +7326,7 @@ class AiHubServer {
|
|
|
7269
7326
|
this.finalizeRunConversationProjection(run.id, updated.conversationId || updated.id);
|
|
7270
7327
|
this.runRegistry.dispose(run.id);
|
|
7271
7328
|
},
|
|
7272
|
-
}, startSessionSeedForHost(hostId, run.id), launchContext);
|
|
7329
|
+
}, startSessionSeedForHost(hostId, run.id), this.withHubRunIdEnv(launchContext, run.id));
|
|
7273
7330
|
// Update the registry entry with the real child process handle.
|
|
7274
7331
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
7275
7332
|
return res.json({ runId: run.id, status: 'started', employee: employeeId, job: jobName });
|
|
@@ -7429,7 +7486,7 @@ class AiHubServer {
|
|
|
7429
7486
|
this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = undefined; });
|
|
7430
7487
|
this.runRegistry.dispose(run.id);
|
|
7431
7488
|
},
|
|
7432
|
-
}, startSessionSeedForHost(hostId, run.id), launchContext);
|
|
7489
|
+
}, startSessionSeedForHost(hostId, run.id), this.withHubRunIdEnv(launchContext, run.id));
|
|
7433
7490
|
this.runRegistry.create(run, child);
|
|
7434
7491
|
return run;
|
|
7435
7492
|
}
|
|
@@ -7585,7 +7642,7 @@ class AiHubServer {
|
|
|
7585
7642
|
this.scheduleRunConversationPersistence(updated, updated.conversationId || updated.id);
|
|
7586
7643
|
},
|
|
7587
7644
|
onExit: (code) => this.handleRunExit(runId, code, postPark),
|
|
7588
|
-
}, launchContext);
|
|
7645
|
+
}, this.withHubRunIdEnv(launchContext, runId));
|
|
7589
7646
|
this.runRegistry.attachChildIfRunning(runId, child);
|
|
7590
7647
|
}, delay);
|
|
7591
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
|
@@ -11056,7 +11056,9 @@ async function tfSelectProjectView(view, projectId) {
|
|
|
11056
11056
|
if (tab.id === 'ptab-add') continue;
|
|
11057
11057
|
const isOverview = tab.id === 'ptab-overview';
|
|
11058
11058
|
const matchesProj = tab.dataset.projectId === tf.activeProjectId;
|
|
11059
|
-
|
|
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' });
|
|
11060
11062
|
}
|
|
11061
11063
|
if (view === 'overview') {
|
|
11062
11064
|
tfRenderOverview();
|
|
@@ -11193,6 +11195,8 @@ function tfRenderProjectTabs() {
|
|
|
11193
11195
|
btn.addEventListener('click', () => tfSelectProjectView('workspace', proj.id));
|
|
11194
11196
|
host.appendChild(btn);
|
|
11195
11197
|
}
|
|
11198
|
+
const activeTab = host.querySelector('.ptab.on');
|
|
11199
|
+
if (activeTab) activeTab.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' });
|
|
11196
11200
|
}
|
|
11197
11201
|
|
|
11198
11202
|
function tfRenderOverview() {
|