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.reviewHandoff?.reviewRequired) {
1592
- return { action: 'park', pauseReason: 'awaiting_review' };
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 candidates = this.conversationStore.loadProject(options.projectPath).conversations.filter((conversation) => {
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
- const rawJobId = req.body.jobId;
4367
- const resolvedRaw = req.body.projectPath || this.defaultProjectPath();
4368
- // Machine-level jobs (#892) operate on ~/.fraim/ and must be launchable
4369
- // even when no project has been selected yet. Fall back to the home dir
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
- const existingPreferences = this.preferencesStore.load(projectPath);
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 (reviewApprovalSystemEventTextFresh)
4678
- current.events.push((0, hosts_1.createHubEvent)('system', reviewApprovalSystemEventTextFresh));
4679
- current.events.push((0, hosts_1.createHubEvent)('system', 'No resumable session starting a fresh agent turn from conversation context.'));
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
- const projectPath = ensureDirectoryPath(body.projectPath || this.defaultProjectPath());
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(projectPath).conversations.find((entry) => entry.id === requestedConversationId)
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((0, hosts_1.createHubEvent)('system', `Run exited (code ${exitCode ?? 'unknown'}); auto-resuming (attempt ${current.recoveryAttempts}/${MAX_RECOVERY_ATTEMPTS}).`));
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 = 'Continue where you left off.';
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.241",
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.241",
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",
@@ -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,