fraim-hub 2.0.291 → 2.0.293
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.
|
@@ -78,6 +78,13 @@ function normalizeConversationKey(key) {
|
|
|
78
78
|
function normalizeProjectPath(projectPath) {
|
|
79
79
|
return normalizeConversationKey(projectPath);
|
|
80
80
|
}
|
|
81
|
+
function scopeForBucket(projectPath) {
|
|
82
|
+
if (projectPath === exports.MANAGER_SCOPE_KEY)
|
|
83
|
+
return 'manager';
|
|
84
|
+
if (projectPath === exports.COMPANY_SCOPE_KEY)
|
|
85
|
+
return 'company';
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
81
88
|
// Issue #708: fold legacy manager/company-invoked runs into the reserved scope buckets on read.
|
|
82
89
|
function migrateScopeBuckets(store) {
|
|
83
90
|
const projects = {};
|
|
@@ -297,9 +304,11 @@ function normalizeConversation(projectPath, raw) {
|
|
|
297
304
|
return null;
|
|
298
305
|
if (value.status !== 'running' && value.status !== 'completed' && value.status !== 'failed')
|
|
299
306
|
return null;
|
|
307
|
+
const repairedScope = value.scope ?? scopeForBucket(projectPath);
|
|
300
308
|
return {
|
|
301
309
|
...value,
|
|
302
310
|
id: value.id,
|
|
311
|
+
...(repairedScope !== undefined && { scope: repairedScope }),
|
|
303
312
|
projectPath: normalizeProjectPath(typeof value.projectPath === 'string' ? value.projectPath : projectPath),
|
|
304
313
|
title: value.title,
|
|
305
314
|
jobId: value.jobId,
|
|
@@ -65,6 +65,20 @@ function parseArgs(argv) {
|
|
|
65
65
|
}
|
|
66
66
|
return { projectPath, preferredPort, runtimeId };
|
|
67
67
|
}
|
|
68
|
+
// Issue #1415 (follow-up): the orphan sweep inside reconcileRunningHub hits real system ports
|
|
69
|
+
// directly (not scoped to FRAIM_USER_DIR), so a test proving it fires must not point it at the
|
|
70
|
+
// real 43091-43200 range - that could kill whatever Hub is genuinely running on the machine the
|
|
71
|
+
// test happens to execute on. Not used in any real launch path; mirrors the existing
|
|
72
|
+
// FRAIM_HUB_SKIP_MATERIALIZE / FRAIM_INSTALLER_LIFECYCLE_TEST kill-switch pattern.
|
|
73
|
+
function resolveOrphanScanPortRangeOverride() {
|
|
74
|
+
const raw = process.env.FRAIM_HUB_ORPHAN_SCAN_PORT_RANGE;
|
|
75
|
+
if (!raw)
|
|
76
|
+
return hub_instance_reconciliation_1.DEFAULT_HUB_ORPHAN_SCAN_PORT_RANGE;
|
|
77
|
+
const [start, end] = raw.split('-').map(Number);
|
|
78
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end)
|
|
79
|
+
return hub_instance_reconciliation_1.DEFAULT_HUB_ORPHAN_SCAN_PORT_RANGE;
|
|
80
|
+
return { start, end };
|
|
81
|
+
}
|
|
68
82
|
function applyUserDataOverride() {
|
|
69
83
|
const userDataDir = process.env.FRAIM_AI_HUB_USER_DATA_DIR;
|
|
70
84
|
if (!userDataDir)
|
|
@@ -570,7 +584,7 @@ async function bootstrap() {
|
|
|
570
584
|
// regardless of FRAIM_USER_DIR isolation, so a test double (FAKE_HOST=1) skips it for the same
|
|
571
585
|
// reason it skips the lock — it must never disturb a real Hub already running on this machine.
|
|
572
586
|
if (!skipSingleInstance) {
|
|
573
|
-
await (0, hub_instance_reconciliation_1.reconcileRunningHub)({ restart: true, keepRunning: false }, options.runtimeId);
|
|
587
|
+
await (0, hub_instance_reconciliation_1.reconcileRunningHub)({ restart: true, keepRunning: false }, options.runtimeId, resolveOrphanScanPortRangeOverride());
|
|
574
588
|
}
|
|
575
589
|
// macOS reads the dock icon from the app bundle once packaged, but an
|
|
576
590
|
// unpackaged `npm run hub:desktop` / `npx fraim-hub` run shows Electron's
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_HUB_ORPHAN_SCAN_PORT_RANGE = void 0;
|
|
6
7
|
exports.killPid = killPid;
|
|
7
8
|
exports.isPortFree = isPortFree;
|
|
8
9
|
exports.waitForPortFree = waitForPortFree;
|
|
@@ -109,10 +110,13 @@ function fetchHubPid(port) {
|
|
|
109
110
|
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
exports.DEFAULT_HUB_ORPHAN_SCAN_PORT_RANGE = { start: 43091, end: 43200 };
|
|
114
|
+
// `portRange` is injectable (default: the real Hub port range) so a test can point this at an
|
|
115
|
+
// isolated range instead - the scan hits real system ports directly, independent of any
|
|
116
|
+
// FRAIM_USER_DIR isolation, so exercising it against the real range in a test would risk killing
|
|
117
|
+
// whatever Hub is genuinely running on this machine at the time.
|
|
118
|
+
async function scanAndKillOrphanHubs(excludePid, portRange = exports.DEFAULT_HUB_ORPHAN_SCAN_PORT_RANGE) {
|
|
119
|
+
for (let port = portRange.start; port <= portRange.end; port++) {
|
|
116
120
|
const version = await fetchRunningHubVersion(port);
|
|
117
121
|
if (!version)
|
|
118
122
|
continue; // port not a Hub
|
|
@@ -127,8 +131,8 @@ async function scanAndKillOrphanHubs(excludePid) {
|
|
|
127
131
|
// The one entry point both launch paths call before starting their own server: reads
|
|
128
132
|
// hub-runtime.json for `runtimeId`, confirms the recorded pid is actually alive and answering
|
|
129
133
|
// (not just present in a stale file), and - when the decision is `replace` - kills it, waits for
|
|
130
|
-
// its port to free
|
|
131
|
-
async function reconcileRunningHub(flags, runtimeId = 'hub') {
|
|
134
|
+
// its port to free. The orphan sweep (#921) then runs independently of that decision - see below.
|
|
135
|
+
async function reconcileRunningHub(flags, runtimeId = 'hub', orphanScanPortRange = exports.DEFAULT_HUB_ORPHAN_SCAN_PORT_RANGE) {
|
|
132
136
|
const running = (0, hub_runtime_file_1.readHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)(), runtimeId);
|
|
133
137
|
const confirmedVersion = running ? await fetchRunningHubVersion(running.port) : null;
|
|
134
138
|
const live = !!(running && confirmedVersion && (0, process_liveness_1.isPidAlive)(running.pid));
|
|
@@ -140,17 +144,27 @@ async function reconcileRunningHub(flags, runtimeId = 'hub') {
|
|
|
140
144
|
restart: flags.restart,
|
|
141
145
|
noRestart: flags.keepRunning,
|
|
142
146
|
});
|
|
147
|
+
let replacedPid = null;
|
|
143
148
|
if (decision.action === 'replace' && effective) {
|
|
144
149
|
console.log(`Replacing running Hub (v${effective.version}, pid ${effective.pid}) - ${decision.reason}`);
|
|
145
150
|
killPid(effective.pid);
|
|
146
151
|
await waitForPortFree(effective.port);
|
|
147
|
-
|
|
148
|
-
// never registered in hub-runtime.json (e.g. older versions started via bare npx).
|
|
149
|
-
if (runtimeId === 'hub') {
|
|
150
|
-
await scanAndKillOrphanHubs(effective.pid);
|
|
151
|
-
}
|
|
152
|
+
replacedPid = effective.pid;
|
|
152
153
|
}
|
|
153
154
|
else if (decision.action === 'focus-existing' && effective) {
|
|
154
155
|
console.log(`A Hub (v${effective.version}) is already running - focusing it. Use --restart to replace it.`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Issue #1415 (follow-up): confirmed live - a Hub started via a different launch path (so it
|
|
159
|
+
// was never registered in hub-runtime.json at all, e.g. an older `npx fraim-hub` build) is only
|
|
160
|
+
// ever caught here. The bug this closes: the *registered* Hub can perfectly well have already
|
|
161
|
+
// quit on its own (a clean shutdown, or - observed live - electron-updater's own native update
|
|
162
|
+
// silently restarting it) by the time this reconciliation runs. That collapses the decision to
|
|
163
|
+
// 'launch-fresh', and the sweep used to live *inside* the 'replace' branch above, so a real,
|
|
164
|
+
// still-running orphan on the standard port range was never even looked for. The sweep must run
|
|
165
|
+
// whenever there is no live-and-focusable Hub for this runtimeId, not only when one was found
|
|
166
|
+
// and replaced - "no other launch path registered a Hub" is not evidence "no Hub is running."
|
|
167
|
+
if (runtimeId === 'hub') {
|
|
168
|
+
await scanAndKillOrphanHubs(replacedPid, orphanScanPortRange);
|
|
155
169
|
}
|
|
156
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.293",
|
|
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.293",
|
|
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
|
@@ -1405,6 +1405,12 @@ function persistConversations(options) {
|
|
|
1405
1405
|
if (opts.disk !== false) scheduleConversationDiskPersist();
|
|
1406
1406
|
}
|
|
1407
1407
|
|
|
1408
|
+
function renderActiveScopedArea() {
|
|
1409
|
+
if (typeof tf === 'undefined') return;
|
|
1410
|
+
if (tf.area === 'manager' && typeof tfRenderManager === 'function') tfRenderManager();
|
|
1411
|
+
else if (tf.area === 'company' && typeof tfRenderCompany === 'function') tfRenderCompany();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1408
1414
|
async function hydrateConversationsFromServer() {
|
|
1409
1415
|
// Issue #708: load the project-independent scope buckets (manager/company) first, so
|
|
1410
1416
|
// manager runs are present regardless of whether a project is selected.
|
|
@@ -1425,7 +1431,7 @@ async function hydrateConversationsFromServer() {
|
|
|
1425
1431
|
console.warn('Could not hydrate manager/company conversations:', error);
|
|
1426
1432
|
}
|
|
1427
1433
|
if (!state.projectPath) {
|
|
1428
|
-
|
|
1434
|
+
renderActiveScopedArea();
|
|
1429
1435
|
return;
|
|
1430
1436
|
}
|
|
1431
1437
|
const projectPath = state.projectPath;
|
|
@@ -1454,6 +1460,7 @@ async function hydrateConversationsFromServer() {
|
|
|
1454
1460
|
renderRail();
|
|
1455
1461
|
renderActive();
|
|
1456
1462
|
await hydrateConversationBody(projectPath, state.activeId);
|
|
1463
|
+
renderActiveScopedArea();
|
|
1457
1464
|
syncConversationRefreshPolling();
|
|
1458
1465
|
} catch (error) {
|
|
1459
1466
|
if (state.conversationBodiesPending === projectPath) state.conversationBodiesPending = null;
|
|
@@ -6280,8 +6287,9 @@ function renderNextJobRecommendations(conv) {
|
|
|
6280
6287
|
const instructions = issueNumber
|
|
6281
6288
|
? `Continue from issue #${issueNumber}: ${summary}`
|
|
6282
6289
|
: `Continue from ${conv.jobTitle || 'the previous job'}: ${summary}`;
|
|
6283
|
-
const
|
|
6284
|
-
|
|
6290
|
+
const scope = convScope(conv);
|
|
6291
|
+
const area = scope === 'manager' ? 'manager'
|
|
6292
|
+
: scope === 'company' ? 'company'
|
|
6285
6293
|
: 'projects';
|
|
6286
6294
|
const employeeId = conversationAgentName(conv) || (state.selectedEmployeeId) || 'claude';
|
|
6287
6295
|
// Issue #989 R1-R6: open step-2 with job pre-selected and instructions pre-filled
|
|
@@ -8053,7 +8061,7 @@ async function startRun(job, instructions, employeeId, preassignedConvId, invoke
|
|
|
8053
8061
|
projectPath: state.projectPath,
|
|
8054
8062
|
// Issue #892: send the run scope so project-independent runs (Company/Manager
|
|
8055
8063
|
// onboarding) can start with no project selected.
|
|
8056
|
-
scope: conv
|
|
8064
|
+
scope: convScope(conv),
|
|
8057
8065
|
hostId: baseHostIdForAgent(employeeId),
|
|
8058
8066
|
configuredAgentId: employeeId,
|
|
8059
8067
|
jobId: job.id,
|
|
@@ -8105,13 +8113,14 @@ async function restartConvWithAgent(conv, newAgentId, text) {
|
|
|
8105
8113
|
renderRail();
|
|
8106
8114
|
renderActive();
|
|
8107
8115
|
try {
|
|
8116
|
+
const scope = convScope(conv);
|
|
8108
8117
|
const result = await requestJson(`/api/ai-hub/conversations/${encodeURIComponent(conv.id)}/switch-agent`, {
|
|
8109
8118
|
method: 'POST',
|
|
8110
8119
|
headers: { 'Content-Type': 'application/json' },
|
|
8111
8120
|
body: JSON.stringify({
|
|
8112
8121
|
projectPath: state.projectPath,
|
|
8113
8122
|
configuredAgentId: newAgentId,
|
|
8114
|
-
scope:
|
|
8123
|
+
scope: scope === 'manager' || scope === 'company' ? scope : undefined,
|
|
8115
8124
|
instructions: text,
|
|
8116
8125
|
}),
|
|
8117
8126
|
});
|
|
@@ -11599,6 +11608,12 @@ function kbMakeCard(conv) {
|
|
|
11599
11608
|
// Kanban/Cards overview is read-only. Clicking anywhere on the card navigates to the workspace.
|
|
11600
11609
|
const navigate = () => {
|
|
11601
11610
|
if (!conv.id) return;
|
|
11611
|
+
const scope = convScope(conv);
|
|
11612
|
+
if (scope === 'manager' || scope === 'company') {
|
|
11613
|
+
tfShowArea(scope);
|
|
11614
|
+
switchToConversation(conv.id);
|
|
11615
|
+
return;
|
|
11616
|
+
}
|
|
11602
11617
|
const _proj = conv.projectPath ? tfProjectForPath(conv.projectPath) : null;
|
|
11603
11618
|
tfSelectProjectView('workspace', _proj ? _proj.id : tf.activeProjectId);
|
|
11604
11619
|
switchToConversation(conv.id);
|