fraim-hub 2.0.241 → 2.0.243
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.
|
@@ -1092,6 +1092,13 @@ function appendHostMessage(run, hostId, event, channel) {
|
|
|
1092
1092
|
return;
|
|
1093
1093
|
run.messages.push((0, hosts_1.createHubMessage)('employee', displayMessage));
|
|
1094
1094
|
}
|
|
1095
|
+
function isCodexMissingReasoningResumeError(run) {
|
|
1096
|
+
return (run.events || []).some((event) => {
|
|
1097
|
+
const text = String(event.text || '');
|
|
1098
|
+
return text.includes("provided without its required 'reasoning' item") ||
|
|
1099
|
+
text.includes('provided without its required "reasoning" item');
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1095
1102
|
// Apply a parsed seekMentoring tool-use signal from the host stream to
|
|
1096
1103
|
// the run state. Returns the updated currentPhase.
|
|
1097
1104
|
function applySeekMentoringSignal(run, signal) {
|
|
@@ -1437,6 +1444,19 @@ function ensureDirectoryPath(projectPath) {
|
|
|
1437
1444
|
}
|
|
1438
1445
|
return resolved;
|
|
1439
1446
|
}
|
|
1447
|
+
function readRunScope(raw) {
|
|
1448
|
+
return raw === 'manager' || raw === 'company' ? raw : 'project';
|
|
1449
|
+
}
|
|
1450
|
+
// Working directory for a project-independent run when no project exists: the user-level
|
|
1451
|
+
// FRAIM home (where org/manager onboarding artifacts live), or the process cwd as a
|
|
1452
|
+
// last resort. This is an execution anchor only — it is deliberately NOT recorded as the
|
|
1453
|
+
// Hub's active project (#866: the launch/fallback dir must not masquerade as a project).
|
|
1454
|
+
function projectIndependentWorkingDir() {
|
|
1455
|
+
const userFraim = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
1456
|
+
if (directoryExists(userFraim))
|
|
1457
|
+
return path_1.default.resolve(userFraim);
|
|
1458
|
+
return process.cwd();
|
|
1459
|
+
}
|
|
1440
1460
|
function normalizedDirectoryPath(projectPath) {
|
|
1441
1461
|
const resolved = path_1.default.resolve(projectPath);
|
|
1442
1462
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
@@ -1534,6 +1554,30 @@ function recoveryBackoffMs(attempt) {
|
|
|
1534
1554
|
const base = 600 * Math.pow(2, attempt - 1); // 600, 1200, 2400ms
|
|
1535
1555
|
return base + Math.floor(0.2 * base); // +20% deterministic jitter
|
|
1536
1556
|
}
|
|
1557
|
+
function buildHubRecoveryContinueMessage(run, exitCode, attempt) {
|
|
1558
|
+
return [
|
|
1559
|
+
'[FRAIM Hub system recovery]',
|
|
1560
|
+
'The FRAIM Hub is automatically recovering this interrupted run.',
|
|
1561
|
+
'This is not a manager-authored instruction. Do not say the manager asked you to continue.',
|
|
1562
|
+
`Run id: ${run.id}`,
|
|
1563
|
+
`Session id: ${run.sessionId || 'unknown'}`,
|
|
1564
|
+
`Recovery attempt: ${attempt}/${MAX_RECOVERY_ATTEMPTS}`,
|
|
1565
|
+
`Prior exit code: ${exitCode ?? 'unknown'}`,
|
|
1566
|
+
'Resume only if the tracked FRAIM phase is non-terminal and not waiting for human review or approval.',
|
|
1567
|
+
].join('\n');
|
|
1568
|
+
}
|
|
1569
|
+
function createHubRecoveryEvent(run, exitCode, attempt) {
|
|
1570
|
+
return (0, hosts_1.createHubEvent)('system', `Hub auto-recovery attempt ${attempt}/${MAX_RECOVERY_ATTEMPTS} for run ${run.id} session ${run.sessionId || 'unknown'} after exit ${exitCode ?? 'unknown'}.`);
|
|
1571
|
+
}
|
|
1572
|
+
function isHumanActionGate(run) {
|
|
1573
|
+
if (run.stoppedByUser)
|
|
1574
|
+
return true;
|
|
1575
|
+
if (run.reviewHandoff?.reviewRequired)
|
|
1576
|
+
return true;
|
|
1577
|
+
const phaseHistory = run.phaseHistory || [];
|
|
1578
|
+
const lastEntry = phaseHistory.length > 0 ? phaseHistory[phaseHistory.length - 1] : null;
|
|
1579
|
+
return lastEntry?.latestStatus === 'incomplete' || lastEntry?.latestStatus === 'failure';
|
|
1580
|
+
}
|
|
1537
1581
|
function restartRecoveryBucketOwnershipReason(conversation, bucketKey) {
|
|
1538
1582
|
if (!bucketKey)
|
|
1539
1583
|
return null;
|
|
@@ -1588,8 +1632,11 @@ function classifyExit(run, exitCode) {
|
|
|
1588
1632
|
return { action: 'error', pauseReason: 'error' };
|
|
1589
1633
|
}
|
|
1590
1634
|
// Clean exit (code 0).
|
|
1591
|
-
if (run
|
|
1592
|
-
|
|
1635
|
+
if (isHumanActionGate(run)) {
|
|
1636
|
+
if (run.reviewHandoff?.reviewRequired) {
|
|
1637
|
+
return { action: 'park', pauseReason: 'awaiting_review' };
|
|
1638
|
+
}
|
|
1639
|
+
return { action: 'park', pauseReason: 'awaiting_user' };
|
|
1593
1640
|
}
|
|
1594
1641
|
// FRAIM job: use the phase signal the server already tracks.
|
|
1595
1642
|
const phaseHistory = run.phaseHistory || [];
|
|
@@ -1839,6 +1886,18 @@ class AiHubServer {
|
|
|
1839
1886
|
// (#866 R2). Never falls back to cwd.
|
|
1840
1887
|
return resolveInitialHubProjectPath(this.preferencesStore) || this.projectPath;
|
|
1841
1888
|
}
|
|
1889
|
+
// Issue #892: resolve the working directory for a run start/resume. Project-scoped
|
|
1890
|
+
// runs require an existing project directory (unchanged — an empty path still throws
|
|
1891
|
+
// "Project path is required."). Project-independent runs (manager/company) fall back
|
|
1892
|
+
// to the user-level FRAIM home when no project has been selected, so Company/Manager
|
|
1893
|
+
// onboarding can run before any project exists. defaultProjectPath() is left untouched.
|
|
1894
|
+
resolveRunProjectPath(bodyProjectPath, scope) {
|
|
1895
|
+
const requested = (bodyProjectPath || this.defaultProjectPath() || '').trim();
|
|
1896
|
+
if (!requested && scope !== 'project') {
|
|
1897
|
+
return projectIndependentWorkingDir();
|
|
1898
|
+
}
|
|
1899
|
+
return ensureDirectoryPath(requested);
|
|
1900
|
+
}
|
|
1842
1901
|
async start(port) {
|
|
1843
1902
|
this.httpPort = port;
|
|
1844
1903
|
await new Promise((resolve, reject) => {
|
|
@@ -2177,7 +2236,8 @@ class AiHubServer {
|
|
|
2177
2236
|
return { ok: true, continuityDecision: 'same_continuity' };
|
|
2178
2237
|
}
|
|
2179
2238
|
resolveImplicitResumeConversation(options) {
|
|
2180
|
-
const
|
|
2239
|
+
const bucketKey = (0, conversation_store_1.conversationScopeKey)(options.scope, options.projectPath);
|
|
2240
|
+
const candidates = this.conversationStore.loadProject(bucketKey).conversations.filter((conversation) => {
|
|
2181
2241
|
if (conversation.jobId !== options.jobId)
|
|
2182
2242
|
return false;
|
|
2183
2243
|
const existingAgentId = this.configuredAgentIdForConversation(conversation, options.employees);
|
|
@@ -2297,6 +2357,64 @@ class AiHubServer {
|
|
|
2297
2357
|
this.pendingConversationWrites.delete(run.id);
|
|
2298
2358
|
this.persistRunConversationNow(run, activeId);
|
|
2299
2359
|
}
|
|
2360
|
+
startFreshCodexReviewApprovalFallback(runId, managerNote) {
|
|
2361
|
+
const run = this.runRegistry.get(runId);
|
|
2362
|
+
if (!run)
|
|
2363
|
+
return;
|
|
2364
|
+
const conversation = this.conversationStore.loadConversation((0, conversation_store_1.conversationScopeKey)(run.scope, run.projectPath), run.conversationId || run.id);
|
|
2365
|
+
const handoffSummary = conversation
|
|
2366
|
+
? buildAgentSwitchHandoffSummary(conversation)
|
|
2367
|
+
: null;
|
|
2368
|
+
const freshPayload = handoffSummary
|
|
2369
|
+
? this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, renderAgentSwitchHandoffPrompt(handoffSummary, managerNote))
|
|
2370
|
+
: this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, managerNote);
|
|
2371
|
+
this.runRegistry.update(run.id, (current) => {
|
|
2372
|
+
current.status = 'running';
|
|
2373
|
+
current.sessionId = undefined;
|
|
2374
|
+
current.recoveryAttempts = 0;
|
|
2375
|
+
current.pauseReason = 'working';
|
|
2376
|
+
current.events.push((0, hosts_1.createHubEvent)('system', 'Codex resume failed because the saved session was missing a required reasoning item; starting a fresh handoff-backed turn from Hub conversation context.'));
|
|
2377
|
+
});
|
|
2378
|
+
const startedFresh = this.runRegistry.get(run.id);
|
|
2379
|
+
if (startedFresh)
|
|
2380
|
+
this.persistRunConversation(startedFresh, startedFresh.conversationId || startedFresh.id);
|
|
2381
|
+
this.runRegistry.create(run, {});
|
|
2382
|
+
const freshLaunch = this.resolveLaunchAgent(run.configuredAgentId, run.hostId);
|
|
2383
|
+
const freshChild = this.hostRuntime.startRun(run.hostId, run.projectPath, freshPayload.message, {
|
|
2384
|
+
onEvent: (event, channel) => {
|
|
2385
|
+
this.runRegistry.update(run.id, (current) => {
|
|
2386
|
+
if (event.sessionId) {
|
|
2387
|
+
current.sessionId = event.sessionId;
|
|
2388
|
+
current.resumeCommand = (0, hosts_1.buildInteractiveResumeCommand)(run.hostId, event.sessionId);
|
|
2389
|
+
}
|
|
2390
|
+
appendHostMessage(current, run.hostId, event, channel);
|
|
2391
|
+
if (event.raw) {
|
|
2392
|
+
current.events.push((0, hosts_1.createHubEvent)(channel, event.raw));
|
|
2393
|
+
applyReviewProjection(current, event.raw);
|
|
2394
|
+
}
|
|
2395
|
+
if (event.agentIdentity)
|
|
2396
|
+
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
2397
|
+
if (event.fraimJob)
|
|
2398
|
+
this.applyFraimJobSignalToRun(current, event.fraimJob);
|
|
2399
|
+
if (event.seekMentoring)
|
|
2400
|
+
this.applySeekMentoringSignalToRun(current, event.seekMentoring);
|
|
2401
|
+
if (event.usage)
|
|
2402
|
+
applyUsageSignal(current, event.usage);
|
|
2403
|
+
});
|
|
2404
|
+
const updated = this.runRegistry.get(run.id);
|
|
2405
|
+
if (updated) {
|
|
2406
|
+
this.maybeStartDelegatedChildRuns(updated);
|
|
2407
|
+
this.scheduleRunConversationPersistence(updated, updated.conversationId || updated.id);
|
|
2408
|
+
}
|
|
2409
|
+
},
|
|
2410
|
+
onExit: (exitCode) => {
|
|
2411
|
+
this.handleRunExit(run.id, exitCode, (updated) => {
|
|
2412
|
+
this.maybeStartDelegatedChildRuns(updated);
|
|
2413
|
+
});
|
|
2414
|
+
},
|
|
2415
|
+
}, startSessionSeedForHost(run.hostId, run.id), freshLaunch.launchContext);
|
|
2416
|
+
this.runRegistry.attachChildIfRunning(run.id, freshChild);
|
|
2417
|
+
}
|
|
2300
2418
|
scheduleRunConversationPersistence(run, activeId) {
|
|
2301
2419
|
this.pendingConversationWrites.set(run.id, { run, activeId });
|
|
2302
2420
|
if (this.conversationFlushTimer)
|
|
@@ -4363,15 +4481,10 @@ class AiHubServer {
|
|
|
4363
4481
|
});
|
|
4364
4482
|
this.app.post('/api/ai-hub/runs', (req, res) => {
|
|
4365
4483
|
try {
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
// so ensureDirectoryPath always receives a valid, existing path.
|
|
4371
|
-
const projectPathRaw = (!resolvedRaw && typeof rawJobId === 'string' && MACHINE_LEVEL_JOB_IDS.has(rawJobId))
|
|
4372
|
-
? os_1.default.homedir()
|
|
4373
|
-
: resolvedRaw;
|
|
4374
|
-
const projectPath = ensureDirectoryPath(projectPathRaw);
|
|
4484
|
+
// Issue #892: project-independent (manager/company) runs resolve a working dir
|
|
4485
|
+
// even with no project; project runs still require an existing project directory.
|
|
4486
|
+
const scope = readRunScope(req.body.scope);
|
|
4487
|
+
const projectPath = this.resolveRunProjectPath(req.body.projectPath, scope);
|
|
4375
4488
|
const requestedHostId = req.body.hostId;
|
|
4376
4489
|
const instructions = (req.body.instructions || '').trim();
|
|
4377
4490
|
const legacyMessage = (req.body.message || '').trim();
|
|
@@ -4435,6 +4548,10 @@ class AiHubServer {
|
|
|
4435
4548
|
totals: emptyTotals(),
|
|
4436
4549
|
lastStatusChangeAt: startTimestamp,
|
|
4437
4550
|
personaKey: jobMetadata?.personaKey ?? getHubPersonaForJob(jobId),
|
|
4551
|
+
// Issue #892: persist the invocation scope so the run is routed to the right
|
|
4552
|
+
// conversation bucket (manager/company get a project-independent home) and so
|
|
4553
|
+
// the resolved fallback working dir is never mistaken for the active project.
|
|
4554
|
+
scope,
|
|
4438
4555
|
// Issue #442: mark this as the FRAIM side of an A/B pair when applicable.
|
|
4439
4556
|
...(compareMode === 'ab' ? { runRole: 'fraim' } : {}),
|
|
4440
4557
|
// #0: trigger source — defaults to 'manager' when not provided by the caller.
|
|
@@ -4548,10 +4665,14 @@ class AiHubServer {
|
|
|
4548
4665
|
}, startSessionSeedForHost(hostId, directRun.id), launchContext);
|
|
4549
4666
|
this.runRegistry.attachChildIfRunning(directRun.id, directChild);
|
|
4550
4667
|
}
|
|
4551
|
-
|
|
4668
|
+
// Issue #892/#866: a project-independent run must NOT stamp its fallback working
|
|
4669
|
+
// dir (~/.fraim) as the Hub's active project. Keep the recorded active project
|
|
4670
|
+
// untouched for manager/company runs; still record recent-job history.
|
|
4671
|
+
const isProjectIndependentRun = scope !== 'project';
|
|
4672
|
+
const existingPreferences = this.preferencesStore.load(isProjectIndependentRun ? '' : projectPath);
|
|
4552
4673
|
this.preferencesStore.remember({
|
|
4553
4674
|
...existingPreferences,
|
|
4554
|
-
projectPath,
|
|
4675
|
+
projectPath: isProjectIndependentRun ? existingPreferences.projectPath : projectPath,
|
|
4555
4676
|
employeeId: hostId,
|
|
4556
4677
|
recentJobIds: existingPreferences.recentJobIds,
|
|
4557
4678
|
}, jobId, typeof instructions === 'string' ? instructions : undefined);
|
|
@@ -4658,6 +4779,7 @@ class AiHubServer {
|
|
|
4658
4779
|
}
|
|
4659
4780
|
// No resumable session — start fresh using a handoff prompt so the agent
|
|
4660
4781
|
// picks up from the preserved conversation state + manager coaching.
|
|
4782
|
+
const reviewApprovalSystemEventText = buildReviewApprovalSystemEventText(prepared.display || message);
|
|
4661
4783
|
if (!run.sessionId) {
|
|
4662
4784
|
if (run.humanCoachingDisabled) {
|
|
4663
4785
|
return res.status(403).json({ error: 'Human coaching is disabled for this delegated workstream. Coach the parent manager run instead.' });
|
|
@@ -4669,14 +4791,13 @@ class AiHubServer {
|
|
|
4669
4791
|
const freshPayload = handoffSummary
|
|
4670
4792
|
? this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, renderAgentSwitchHandoffPrompt(handoffSummary, prepared.display || message))
|
|
4671
4793
|
: this.prepareStartPayload(run.projectPath, run.hostId, run.jobId, prepared.display || message);
|
|
4672
|
-
const reviewApprovalSystemEventTextFresh = buildReviewApprovalSystemEventText(prepared.display || message);
|
|
4673
4794
|
this.runRegistry.update(run.id, (current) => {
|
|
4674
4795
|
current.status = 'running';
|
|
4675
4796
|
current.sessionId = undefined;
|
|
4676
4797
|
current.messages.push((0, hosts_1.createHubMessage)('manager', prepared.display || message));
|
|
4677
|
-
if (
|
|
4678
|
-
current.events.push((0, hosts_1.createHubEvent)('system',
|
|
4679
|
-
current.events.push((0, hosts_1.createHubEvent)('system', 'No resumable session
|
|
4798
|
+
if (reviewApprovalSystemEventText)
|
|
4799
|
+
current.events.push((0, hosts_1.createHubEvent)('system', reviewApprovalSystemEventText));
|
|
4800
|
+
current.events.push((0, hosts_1.createHubEvent)('system', 'No resumable session - starting a fresh agent turn from conversation context.'));
|
|
4680
4801
|
});
|
|
4681
4802
|
const startedFresh = this.runRegistry.get(run.id);
|
|
4682
4803
|
if (startedFresh)
|
|
@@ -4720,7 +4841,6 @@ class AiHubServer {
|
|
|
4720
4841
|
const refreshedFresh = this.runRegistry.get(run.id);
|
|
4721
4842
|
return res.json(refreshedFresh ? this.enrichRunForResponse(refreshedFresh) : refreshedFresh);
|
|
4722
4843
|
}
|
|
4723
|
-
const reviewApprovalSystemEventText = buildReviewApprovalSystemEventText(prepared.display || message);
|
|
4724
4844
|
this.runRegistry.update(run.id, (current) => {
|
|
4725
4845
|
current.status = 'running';
|
|
4726
4846
|
// #521: bubble shows the manager's words; the agent gets the full message.
|
|
@@ -4761,6 +4881,11 @@ class AiHubServer {
|
|
|
4761
4881
|
}
|
|
4762
4882
|
},
|
|
4763
4883
|
onExit: (exitCode) => {
|
|
4884
|
+
const exited = this.runRegistry.get(run.id);
|
|
4885
|
+
if (exitCode !== 0 && run.hostId === 'codex' && reviewApprovalSystemEventText && exited && isCodexMissingReasoningResumeError(exited)) {
|
|
4886
|
+
this.startFreshCodexReviewApprovalFallback(run.id, prepared.display || message);
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4764
4889
|
this.handleRunExit(run.id, exitCode, (updated) => {
|
|
4765
4890
|
this.maybeStartDelegatedChildRuns(updated);
|
|
4766
4891
|
});
|
|
@@ -4781,7 +4906,10 @@ class AiHubServer {
|
|
|
4781
4906
|
this.app.post('/api/ai-hub/runs/resume', (req, res) => {
|
|
4782
4907
|
try {
|
|
4783
4908
|
const body = (req.body ?? {});
|
|
4784
|
-
|
|
4909
|
+
// Issue #892: a mid-flight project-independent onboarding (manager/company) must
|
|
4910
|
+
// resume even with no project (e.g. after a Hub restart).
|
|
4911
|
+
const scope = readRunScope(body.scope);
|
|
4912
|
+
const projectPath = this.resolveRunProjectPath(body.projectPath, scope);
|
|
4785
4913
|
const requestedHostId = body.hostId;
|
|
4786
4914
|
const sessionId = (body.sessionId || '').trim();
|
|
4787
4915
|
const jobId = (body.jobId || '').trim();
|
|
@@ -4798,12 +4926,13 @@ class AiHubServer {
|
|
|
4798
4926
|
const requestedConversationId = typeof body.conversationId === 'string' && body.conversationId.trim()
|
|
4799
4927
|
? body.conversationId.trim()
|
|
4800
4928
|
: undefined;
|
|
4929
|
+
const conversationBucketKey = (0, conversation_store_1.conversationScopeKey)(scope, projectPath);
|
|
4801
4930
|
const inferredConversation = requestedConversationId
|
|
4802
4931
|
? null
|
|
4803
|
-
: this.resolveImplicitResumeConversation({ projectPath, jobId, requestedAgent: configuredAgent, employees });
|
|
4932
|
+
: this.resolveImplicitResumeConversation({ projectPath, scope, jobId, requestedAgent: configuredAgent, employees });
|
|
4804
4933
|
const conversationId = requestedConversationId || inferredConversation?.id;
|
|
4805
4934
|
const persistedConversation = requestedConversationId
|
|
4806
|
-
? this.conversationStore.loadProject(
|
|
4935
|
+
? this.conversationStore.loadProject(conversationBucketKey).conversations.find((entry) => entry.id === requestedConversationId)
|
|
4807
4936
|
: inferredConversation ?? undefined;
|
|
4808
4937
|
const persistedRun = readPersistedRunProjection(persistedConversation);
|
|
4809
4938
|
const now = new Date().toISOString();
|
|
@@ -4813,6 +4942,9 @@ class AiHubServer {
|
|
|
4813
4942
|
conversationTitle: typeof body.conversationTitle === 'string' && body.conversationTitle.trim() ? body.conversationTitle.trim() : undefined,
|
|
4814
4943
|
jobTitle: typeof body.jobTitle === 'string' && body.jobTitle.trim() ? body.jobTitle.trim() : jobId,
|
|
4815
4944
|
jobId, hostId, configuredAgentId: configuredAgent.id, configuredAgentLabel: configuredAgent.label, baseHostId: configuredAgent.baseHostId, projectPath, status: 'running', sessionId,
|
|
4945
|
+
// Issue #892: keep the invocation scope so a resumed manager/company run stays
|
|
4946
|
+
// in its project-independent conversation bucket.
|
|
4947
|
+
scope,
|
|
4816
4948
|
createdAt: now, updatedAt: now, messages: [],
|
|
4817
4949
|
events: [(0, hosts_1.createHubEvent)('system', `Resuming ${configuredAgent.label} (${hostId}) session ${sessionId} in ${projectPath}`)],
|
|
4818
4950
|
// Only carry phase state forward when the prior run was interrupted mid-job
|
|
@@ -5679,7 +5811,7 @@ class AiHubServer {
|
|
|
5679
5811
|
current.recoveryAttempts = (current.recoveryAttempts ?? 0) + 1;
|
|
5680
5812
|
current.lastRecoveryAt = new Date().toISOString();
|
|
5681
5813
|
current.pauseReason = 'working';
|
|
5682
|
-
current.events.push((
|
|
5814
|
+
current.events.push(createHubRecoveryEvent(current, exitCode, current.recoveryAttempts));
|
|
5683
5815
|
});
|
|
5684
5816
|
const refreshed = this.runRegistry.get(runId);
|
|
5685
5817
|
if (!refreshed?.sessionId) {
|
|
@@ -5700,7 +5832,7 @@ class AiHubServer {
|
|
|
5700
5832
|
const current = this.runRegistry.get(runId);
|
|
5701
5833
|
if (!current || current.status !== 'running')
|
|
5702
5834
|
return;
|
|
5703
|
-
const message =
|
|
5835
|
+
const message = buildHubRecoveryContinueMessage(current, exitCode, attempt);
|
|
5704
5836
|
const child = this.hostRuntime.continueRun(current.hostId, current.projectPath, current.sessionId, message, {
|
|
5705
5837
|
onEvent: (event, channel) => {
|
|
5706
5838
|
this.runRegistry.update(runId, (r) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CODEX_SKILL_FRONTMATTER = exports.FRAIM_INVOCATION_BODY = exports.FRAIM_DEFERRED_TOOL_PRELOAD = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
3
|
+
exports.CODEX_SKILL_FRONTMATTER = exports.FRAIM_INVOCATION_BODY = exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE = exports.FRAIM_DEFERRED_TOOL_PRELOAD = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
4
4
|
exports.buildFraimInvocationBody = buildFraimInvocationBody;
|
|
5
5
|
exports.buildClaudeSkillContent = buildClaudeSkillContent;
|
|
6
6
|
exports.buildClaudeCommandShimContent = buildClaudeCommandShimContent;
|
|
@@ -25,6 +25,12 @@ exports.FRAIM_DEFERRED_TOOL_PRELOAD = [
|
|
|
25
25
|
'get_fraim_file',
|
|
26
26
|
'seekMentoring'
|
|
27
27
|
];
|
|
28
|
+
exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE = [
|
|
29
|
+
'If required FRAIM MCP tools remain unavailable after the deferred-tool preload/retry step, stop the FRAIM job.',
|
|
30
|
+
'Do not continue from memory, local stubs, cached instructions, or prior context.',
|
|
31
|
+
'In the blocker sentence, state only the work-focused blocker and what outcome is blocked; do not narrate ToolSearch/tool_search, individual tool names, server internals, or retry mechanics.',
|
|
32
|
+
'Always help troubleshoot the unavailable FRAIM MCP connection: tell the user to reconnect or enable the FRAIM MCP server in their IDE/agent, restart or refresh the agent session, retry the FRAIM job, and if it still fails run `fraim doctor --test-mcp` and share the failing check output.'
|
|
33
|
+
].join(' ');
|
|
28
34
|
function buildDeferredToolBootstrapSection(profile) {
|
|
29
35
|
if (profile === 'none') {
|
|
30
36
|
return '';
|
|
@@ -72,6 +78,7 @@ ${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
|
|
|
72
78
|
5. **Execute**:
|
|
73
79
|
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
74
80
|
- For skills, apply the skill steps directly to the user's current context.
|
|
81
|
+
- ${exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
75
82
|
`;
|
|
76
83
|
}
|
|
77
84
|
exports.FRAIM_INVOCATION_BODY = buildFraimInvocationBody();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.243",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fraim-hub": "bin/fraim-hub.js",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"electron": "^41.2.2",
|
|
162
162
|
"electron-updater": "^6.8.9",
|
|
163
163
|
"express": "^5.2.1",
|
|
164
|
-
"fraim": "2.0.
|
|
164
|
+
"fraim": "2.0.243",
|
|
165
165
|
"mongodb": "^7.0.0",
|
|
166
166
|
"node-cron": "4.2.1",
|
|
167
167
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/script.js
CHANGED
|
@@ -4249,11 +4249,14 @@ function syncSendButton() {
|
|
|
4249
4249
|
// coach mid-run (which is exactly what /api/ai-hub/runs/:id/messages
|
|
4250
4250
|
// is for; the server only requires sessionId).
|
|
4251
4251
|
const resumable = !!(conv && conv.sessionId);
|
|
4252
|
+
// A stopped/failed run with no session (agent crashed, Hub restarted) can
|
|
4253
|
+
// also accept coaching — the server will fire a fresh startRun from context.
|
|
4254
|
+
const canRestartWithCoaching = !!(conv && !conv.sessionId && conv.status !== 'running' && !isManagedDelegationChild(conv));
|
|
4252
4255
|
// A pending coaching job (set by quick-coach buttons or template picker)
|
|
4253
4256
|
// is enough to enable Send even with an empty textarea — the user just
|
|
4254
4257
|
// wants to fire that coaching directive, optionally with added context.
|
|
4255
4258
|
const hasPendingJob = !!state.pendingCoachingJobId;
|
|
4256
|
-
els['send'].disabled = !((hasText || hasPendingJob) && resumable);
|
|
4259
|
+
els['send'].disabled = !((hasText || hasPendingJob) && (resumable || canRestartWithCoaching));
|
|
4257
4260
|
}
|
|
4258
4261
|
|
|
4259
4262
|
function defaultCoachNote(conv) {
|
|
@@ -4272,6 +4275,9 @@ function defaultCoachNote(conv) {
|
|
|
4272
4275
|
if (conv && conv.status === 'running') {
|
|
4273
4276
|
return 'The employee is still working. Add coaching here to tighten the next step without losing context.';
|
|
4274
4277
|
}
|
|
4278
|
+
if (conv && !conv.sessionId && conv.status !== 'running') {
|
|
4279
|
+
return 'The session is gone (agent crashed or Hub restarted). Send coaching to restart a fresh session from the preserved conversation context.';
|
|
4280
|
+
}
|
|
4275
4281
|
return 'The employee is waiting on you. Send the next instruction to continue this run.';
|
|
4276
4282
|
}
|
|
4277
4283
|
|
|
@@ -6783,6 +6789,9 @@ async function startRun(job, instructions, employeeId, preassignedConvId, invoke
|
|
|
6783
6789
|
headers: { 'Content-Type': 'application/json' },
|
|
6784
6790
|
body: JSON.stringify({
|
|
6785
6791
|
projectPath: state.projectPath,
|
|
6792
|
+
// Issue #892: send the run scope so project-independent runs (Company/Manager
|
|
6793
|
+
// onboarding) can start with no project selected.
|
|
6794
|
+
scope: conv.scope,
|
|
6786
6795
|
hostId: baseHostIdForAgent(employeeId),
|
|
6787
6796
|
configuredAgentId: employeeId,
|
|
6788
6797
|
jobId: job.id,
|
|
@@ -6903,6 +6912,9 @@ async function continueRun(text, options) {
|
|
|
6903
6912
|
headers: { 'Content-Type': 'application/json' },
|
|
6904
6913
|
body: JSON.stringify({
|
|
6905
6914
|
projectPath: state.projectPath,
|
|
6915
|
+
// Issue #892: carry the scope so a project-independent onboarding resumes
|
|
6916
|
+
// with no project (e.g. after a Hub restart).
|
|
6917
|
+
scope: convScope(conv),
|
|
6906
6918
|
hostId: baseHostIdForAgent(conversationAgentName(conv) || state.selectedEmployeeId || 'claude'),
|
|
6907
6919
|
configuredAgentId: conversationAgentName(conv) || state.selectedEmployeeId || 'claude',
|
|
6908
6920
|
jobId: conv.jobId,
|