fraim-hub 2.0.277 → 2.0.279

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.
@@ -3,11 +3,13 @@ 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.createHubEvent = exports.createHubMessage = exports.ScriptedHostRuntime = exports.FakeHostRuntime = exports.CliHostRuntime = void 0;
6
+ exports.createHubEvent = exports.createHubMessage = exports.ScriptedHostRuntime = exports.FakeHostRuntime = exports.CliHostRuntime = exports.COMPACT_TASK_FAILURE_PATTERN = void 0;
7
7
  exports.parseSeekMentoringSignal = parseSeekMentoringSignal;
8
8
  exports.parseFraimJobLoadSignal = parseFraimJobLoadSignal;
9
9
  exports.parseUsageSignal = parseUsageSignal;
10
10
  exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
11
+ exports.escapeWindowsArg = escapeWindowsArg;
12
+ exports.resolveHostInvocation = resolveHostInvocation;
11
13
  exports.__setAgentAvailabilityPathForTests = __setAgentAvailabilityPathForTests;
12
14
  exports.invalidateEmployeeDetectionCache = invalidateEmployeeDetectionCache;
13
15
  exports.__clearEmployeeDetectionMemoryCacheForTests = __clearEmployeeDetectionMemoryCacheForTests;
@@ -25,6 +27,9 @@ exports.supportsDirectPath = supportsDirectPath;
25
27
  exports.buildDirectStartPlan = buildDirectStartPlan;
26
28
  exports.buildDirectContinuePlan = buildDirectContinuePlan;
27
29
  exports.parseHostLine = parseHostLine;
30
+ exports.detectCodexCompactTaskFailure = detectCodexCompactTaskFailure;
31
+ exports.describeSpawnError = describeSpawnError;
32
+ exports.buildHostSpawnEnv = buildHostSpawnEnv;
28
33
  exports.findGeminiSessionIdForPrompt = findGeminiSessionIdForPrompt;
29
34
  const crypto_1 = require("crypto");
30
35
  const child_process_1 = require("child_process");
@@ -34,6 +39,7 @@ const os_1 = __importDefault(require("os"));
34
39
  const path_1 = __importDefault(require("path"));
35
40
  const manager_turns_1 = require("./manager-turns");
36
41
  const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
42
+ const command_resolution_1 = require("../cli/mcp/command-resolution");
37
43
  const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
38
44
  const configured_agents_1 = require("./configured-agents");
39
45
  const pack_home_1 = require("../cli/utils/pack-home");
@@ -816,32 +822,35 @@ function quoteWindowsArg(value) {
816
822
  }
817
823
  return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
818
824
  }
825
+ // Exported for reuse by server.ts's hubCommandVersion(), which builds a
826
+ // similar single cmd.exe command-line string around a resolved absolute path.
819
827
  function escapeWindowsArg(value) {
820
828
  return /[\s"]/u.test(value) ? quoteWindowsArg(value) : value;
821
829
  }
830
+ // Issue #1284/#1285 (Implementation Strategy §0): resolve the real agent CLI
831
+ // binary fresh at every launch instead of handing a bare command name to
832
+ // `spawn`/`cmd.exe` and trusting whatever PATH the process happens to have.
833
+ // Covers every call site that builds an invocation through this function:
834
+ // spawnHostProcess (real job launches) and both version-probe helpers.
835
+ // Exported for direct unit testing.
822
836
  function resolveHostInvocation(plan) {
837
+ const resolvedPlan = { ...plan, command: (0, command_resolution_1.resolveManagedCommand)(plan.command) };
823
838
  if (process.platform !== 'win32') {
824
- return plan;
839
+ return resolvedPlan;
825
840
  }
826
- const [command, ...args] = [plan.command, ...plan.args];
841
+ const [command, ...args] = [resolvedPlan.command, ...resolvedPlan.args];
842
+ // Issue #1284/#1285: `command` can now be an absolute path resolved by
843
+ // resolveManagedCommand() (e.g. a temp/user directory containing a space),
844
+ // not just a bare command name — it needs the same escaping the args
845
+ // already get before joining into the single cmd.exe command-line string.
827
846
  return {
828
847
  command: 'cmd.exe',
829
- args: ['/d', '/s', '/c', [command, ...args.map(escapeWindowsArg)].join(' ')],
848
+ args: ['/d', '/s', '/c', [command, ...args].map(escapeWindowsArg).join(' ')],
830
849
  };
831
850
  }
832
- function stripProjectLocalNodeBinDirs(basePath) {
833
- return (basePath ?? '')
834
- .split(path_1.default.delimiter)
835
- .filter(Boolean)
836
- .filter((entry) => {
837
- const normalized = path_1.default.normalize(entry).toLowerCase();
838
- return !normalized.endsWith(`${path_1.default.sep}node_modules${path_1.default.sep}.bin`);
839
- })
840
- .join(path_1.default.delimiter);
841
- }
842
851
  function buildAgentVersionProbePath(basePath) {
843
852
  const withoutManaged = (0, managed_agent_paths_1.stripManagedAgentBinDirsFromPath)(basePath);
844
- const withoutProjectBins = stripProjectLocalNodeBinDirs(withoutManaged);
853
+ const withoutProjectBins = (0, managed_agent_paths_1.stripProjectLocalNodeBinDirs)(withoutManaged);
845
854
  return (0, managed_agent_paths_1.appendBinDirsToPath)(withoutProjectBins, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
846
855
  }
847
856
  // Single source for the probe environment, shared by the sync and async probes so the two
@@ -1795,6 +1804,26 @@ function scrubGeminiAssistantLine(line) {
1795
1804
  const scrubbed = line.replace(/\[Thought:\s*true\]\s*/gi, '').trim();
1796
1805
  return scrubbed.length > 0 ? scrubbed : null;
1797
1806
  }
1807
+ // Issue #1284: Codex's own error text for a rejected remote context-compaction
1808
+ // call, e.g. "Error running remote compact task: unexpected status 404 Not
1809
+ // Found: ...". Exported so `server.ts`'s `classifyExit()` can recognize a
1810
+ // repeat of this exact signature from `run.lastHostError` without duplicating
1811
+ // the pattern.
1812
+ exports.COMPACT_TASK_FAILURE_PATTERN = /Error running remote compact task/;
1813
+ // Issue #1284 (Defect C): Codex reports a remote compact-endpoint failure as
1814
+ // plain stderr text, not one of the JSON shapes `parseHostLine` understands,
1815
+ // so it silently fell through to `{ raw }` with no `hostError`/classification
1816
+ // signal at all — the run then retried blindly against the same doomed call.
1817
+ // Surfacing it as `hostError` here lets `classifyExit()` recognize and
1818
+ // short-circuit a repeat instead of exhausting the retry budget. A pure,
1819
+ // directly-testable function, matching the shape of `parseSeekMentoringSignal`
1820
+ // et al. rather than inlining this check into `wireHostProcess`'s closure.
1821
+ function detectCodexCompactTaskFailure(line) {
1822
+ if (exports.COMPACT_TASK_FAILURE_PATTERN.test(line) && /status 4\d\d/.test(line)) {
1823
+ return { hostError: line, raw: line };
1824
+ }
1825
+ return null;
1826
+ }
1798
1827
  function wireHostProcess(hostId, child, handlers) {
1799
1828
  const wire = (buffer, channel) => {
1800
1829
  let pending = '';
@@ -1815,6 +1844,11 @@ function wireHostProcess(hostId, child, handlers) {
1815
1844
  if (suppressCodexModelRefreshStderr) {
1816
1845
  continue;
1817
1846
  }
1847
+ const compactFailure = detectCodexCompactTaskFailure(line);
1848
+ if (compactFailure) {
1849
+ handlers.onEvent(compactFailure, channel);
1850
+ continue;
1851
+ }
1818
1852
  }
1819
1853
  const parsed = parseHostLine(hostId, line);
1820
1854
  if (parsed.message || parsed.sessionId || parsed.raw) {
@@ -1833,16 +1867,55 @@ function wireHostProcess(hostId, child, handlers) {
1833
1867
  };
1834
1868
  wire(child.stdout, 'stdout');
1835
1869
  wire(child.stderr, 'stderr');
1836
- child.on('close', (code) => handlers.onExit(code));
1870
+ // Issue #1327: preserve signal information so SIGSEGV kills (code=null,
1871
+ // signal='SIGSEGV') are NOT treated as clean exits by classifyExit's
1872
+ // `exitCode ?? 0` guard. Pass -1 for any signal-terminated child so the
1873
+ // non-zero branch fires and recovery is attempted.
1874
+ child.on('close', (code, signal) => handlers.onExit(signal ? -1 : code));
1875
+ // Issue #1327: without this listener, an OS-level spawn failure (EAGAIN,
1876
+ // ENOENT, EACCES) fires an 'error' event on the ChildProcess. An unhandled
1877
+ // EventEmitter 'error' throws synchronously and crashes the Hub process.
1878
+ // Surface the failure as a human-readable hostError so the run's terminal
1879
+ // message names the cause. 'close' still fires after 'error' for spawn
1880
+ // failures, so handlers.onExit is called by the close listener above.
1881
+ child.on('error', (err) => {
1882
+ handlers.onEvent({ hostError: describeSpawnError(err) }, 'system');
1883
+ });
1837
1884
  return child;
1838
1885
  }
1886
+ function describeSpawnError(err) {
1887
+ if (err.code === 'ENOENT') {
1888
+ return `Agent binary not found${err.path ? ` (${err.path})` : ''}. Ensure the agent CLI is installed and on PATH.`;
1889
+ }
1890
+ if (err.code === 'EACCES') {
1891
+ return `Permission denied starting agent process${err.path ? ` (${err.path})` : ''}. Check executable permissions.`;
1892
+ }
1893
+ if (err.code === 'EAGAIN') {
1894
+ return `Could not start agent process: system resources temporarily unavailable (EAGAIN). The OS process limit may have been reached; try again in a moment.`;
1895
+ }
1896
+ return `Failed to start agent process: ${err.message}`;
1897
+ }
1898
+ // Issue #1284/#1285 (Defect D): the version-probe path already excludes
1899
+ // project-local `node_modules/.bin` shims via `versionProbeEnv()`; the real
1900
+ // job-spawn path built `env.PATH` straight from `process.env`/`plan.env` with
1901
+ // no such filtering, so a poisoned PATH silently ran the wrong binary on the
1902
+ // one path that actually executes jobs. Reuse the exact same construction the
1903
+ // probe uses so the two can never disagree again.
1904
+ // Exported for direct unit testing of the Defect D fix (issue #1284/#1285):
1905
+ // asserting spawnHostProcess's environment construction matches
1906
+ // versionProbeEnv()'s filtering without spawning a real child process.
1907
+ function buildHostSpawnEnv(plan) {
1908
+ const merged = plan.env ? { ...process.env, ...plan.env } : { ...process.env };
1909
+ const resolvedPath = buildAgentVersionProbePath(merged.PATH ?? merged.Path);
1910
+ return { ...merged, PATH: resolvedPath, Path: resolvedPath };
1911
+ }
1839
1912
  function spawnHostProcess(hostId, plan, projectPath, handlers) {
1840
1913
  const invocation = resolveHostInvocation(plan);
1841
1914
  const startedAtMs = Date.now();
1842
1915
  const child = (0, child_process_1.spawn)(invocation.command, invocation.args, {
1843
1916
  cwd: projectPath,
1844
1917
  stdio: ['pipe', 'pipe', 'pipe'],
1845
- env: plan.env ? { ...process.env, ...plan.env } : process.env,
1918
+ env: buildHostSpawnEnv(plan),
1846
1919
  });
1847
1920
  if (typeof plan.stdin === 'string') {
1848
1921
  child.stdin.write(plan.stdin);
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.findAvailablePortExcluding = exports.findAvailablePort = exports.AiHubServer = exports.HostConfigStore = exports.DeploymentStore = void 0;
40
40
  exports.configureFraimForHubAgent = configureFraimForHubAgent;
41
+ exports.hubCommandVersion = hubCommandVersion;
41
42
  exports.buildOpenFileInvocation = buildOpenFileInvocation;
42
43
  const express_1 = __importDefault(require("express"));
43
44
  const path_1 = __importDefault(require("path"));
@@ -55,6 +56,7 @@ const job_visualization_1 = require("../core/job-visualization");
55
56
  const custom_employees_1 = require("./custom-employees");
56
57
  const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
57
58
  const hosts_1 = require("./hosts");
59
+ const command_resolution_1 = require("../cli/mcp/command-resolution");
58
60
  const configured_agents_1 = require("./configured-agents");
59
61
  const host_session_state_1 = require("./host-session-state");
60
62
  const url_safety_1 = require("./url-safety");
@@ -69,6 +71,8 @@ const restart_recovery_policy_1 = require("./restart-recovery-policy");
69
71
  const remote_hub_gateway_1 = require("./remote-hub-gateway");
70
72
  const managed_browser_1 = require("./managed-browser");
71
73
  const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
74
+ const managed_agent_install_1 = require("../cli/utils/managed-agent-install");
75
+ const agent_cli_health_checks_1 = require("../cli/doctor/checks/agent-cli-health-checks");
72
76
  const user_config_1 = require("../cli/utils/user-config");
73
77
  const version_utils_1 = require("../cli/utils/version-utils");
74
78
  const hub_latest_version_1 = require("./hub-latest-version");
@@ -1499,17 +1503,26 @@ async function configureFraimForHubAgent(hubId) {
1499
1503
  return { configured: false, error: e instanceof Error ? e.message : String(e) };
1500
1504
  }
1501
1505
  }
1506
+ // Exported for direct unit testing of the Defect D fix (issue #1284/#1285):
1507
+ // asserting this resolves via a real system install and skips a poisoned
1508
+ // project-local node_modules/.bin entry, without spinning up a full server.
1502
1509
  function hubCommandVersion(command, extraBinDirs, basePath) {
1503
1510
  if (process.env.NODE_ENV === 'test' && process.env.FRAIM_TEST_HUB_COMMAND_VERSION_EMPTY === '1') {
1504
1511
  return null;
1505
1512
  }
1506
- const executable = process.platform === 'win32' ? 'cmd.exe' : command;
1507
- const args = process.platform === 'win32'
1508
- ? ['/d', '/s', '/c', `${command} --version`]
1509
- : ['--version'];
1510
1513
  const pathValue = extraBinDirs && extraBinDirs.length > 0
1511
1514
  ? (0, managed_agent_paths_1.appendBinDirsToPath)(basePath ?? process.env.PATH, extraBinDirs)
1512
1515
  : basePath;
1516
+ // Issue #1284/#1285 (Defect D, §0): resolve to a real absolute path — with
1517
+ // project-local node_modules/.bin stripped — instead of handing the bare
1518
+ // command name to cmd.exe's own PATH search, which does not exclude those
1519
+ // shims. Falls back to the bare command (today's behavior, resolves to
1520
+ // null below) when no system install is found on the constructed PATH.
1521
+ const resolvedCommand = (0, command_resolution_1.getSystemCommandPath)(command, pathValue ?? process.env.PATH) || command;
1522
+ const executable = process.platform === 'win32' ? 'cmd.exe' : resolvedCommand;
1523
+ const args = process.platform === 'win32'
1524
+ ? ['/d', '/s', '/c', `${(0, hosts_1.escapeWindowsArg)(resolvedCommand)} --version`]
1525
+ : ['--version'];
1513
1526
  const env = pathValue === undefined ? undefined : { ...process.env, PATH: pathValue };
1514
1527
  const result = (0, child_process_1.spawnSync)(executable, args, { encoding: 'utf8', timeout: 5000, ...(env ? { env } : {}) });
1515
1528
  if (result.status !== 0 || result.error)
@@ -1893,6 +1906,22 @@ function isHumanActionGate(run) {
1893
1906
  const lastEntry = phaseHistory.length > 0 ? phaseHistory[phaseHistory.length - 1] : null;
1894
1907
  return lastEntry?.latestStatus === 'incomplete' || lastEntry?.latestStatus === 'failure';
1895
1908
  }
1909
+ // Issue #1284 (Defect C): a 404/4xx on Codex's remote context-compaction
1910
+ // endpoint means the installed Codex build's compact request is not accepted
1911
+ // by the current backend (a version incompatibility) — not a transient
1912
+ // failure blind retry can recover from. Reads the most recent host error
1913
+ // already captured on the run (recordHostEvent sets `run.lastHostError` from
1914
+ // the stderr signal wireHostProcess raises for this exact line).
1915
+ function detectCompactFailureSignature(run) {
1916
+ const error = run.lastHostError;
1917
+ if (error && hosts_1.COMPACT_TASK_FAILURE_PATTERN.test(error) && /status 4\d\d/.test(error)) {
1918
+ return 'compact-4xx';
1919
+ }
1920
+ return null;
1921
+ }
1922
+ const COMPACT_INCOMPATIBLE_NOTE = "Codex's remote context-compaction call was rejected by the backend (404). "
1923
+ + 'This usually means the installed Codex CLI is out of date. Run `codex update`, confirm `codex --version` '
1924
+ + 'matches the latest release, and retry.';
1896
1925
  // #1275: true when the host reported an active local_bash task. These are
1897
1926
  // intentional async continuations — the agent ended its turn knowing bash was
1898
1927
  // running, intending to check results in the next turn. In -p mode Claude Code
@@ -1928,11 +1957,21 @@ function classifyExit(run, exitCode) {
1928
1957
  if (run.stoppedByUser) {
1929
1958
  return { action: 'park', pauseReason: 'stopped' };
1930
1959
  }
1931
- const code = exitCode ?? 0;
1960
+ // Issue #1327: null exit code means the child was killed by a signal (e.g.
1961
+ // SIGSEGV). wireHostProcess now passes -1 in that case, but guard here too:
1962
+ // any null that reaches classifyExit must not be treated as a clean exit.
1963
+ const code = exitCode ?? -1;
1932
1964
  if (code !== 0) {
1965
+ // Issue #1284: a repeat of the exact same classified-unrecoverable
1966
+ // signature short-circuits to 'error' immediately instead of exhausting
1967
+ // the retry budget against a call that will fail identically every time.
1968
+ const signature = detectCompactFailureSignature(run);
1969
+ if (signature && run.lastRecoverySignature === signature) {
1970
+ return { action: 'error', pauseReason: 'error', systemNote: COMPACT_INCOMPATIBLE_NOTE };
1971
+ }
1933
1972
  const attempts = (run.recoveryAttempts ?? 0);
1934
1973
  if (attempts < MAX_RECOVERY_ATTEMPTS) {
1935
- return { action: 'resume', pauseReason: 'working' };
1974
+ return { action: 'resume', pauseReason: 'working', matchedSignature: signature ?? undefined };
1936
1975
  }
1937
1976
  return { action: 'error', pauseReason: 'error' };
1938
1977
  }
@@ -2801,15 +2840,26 @@ class AiHubServer {
2801
2840
  appendHostMessage(run, hostId, event, channel);
2802
2841
  // Issue #1221: a real assistant reply proves the host recovered, so a
2803
2842
  // previously recorded error is stale and must not keep haunting the next
2804
- // exit/recovery message.
2843
+ // exit/recovery message. Issue #1284: a classified recovery signature is
2844
+ // scoped to the same failure recurring; a genuine recovery must not let
2845
+ // it bleed into an unrelated later failure in the same run.
2805
2846
  if (event.message && channel === 'stdout') {
2806
2847
  run.lastHostError = undefined;
2848
+ run.lastRecoverySignature = undefined;
2807
2849
  }
2808
2850
  if (event.raw) {
2809
2851
  applyDelegationLedgerProjection(run, event.raw);
2810
2852
  try {
2811
2853
  const ref = this.rawEventLogStore.append((0, conversation_store_1.conversationScopeKey)(run.scope, run.projectPath), run.conversationId || run.id, run.id, { channel, text: event.raw });
2812
2854
  run.eventLogRefs = upsertEventLogRef(run.eventLogRefs, ref);
2855
+ // Issue #1327: if the Hub crashes before the debounced conversation
2856
+ // flush fires, run.eventLogRefs is lost from the store and the raw
2857
+ // event log on disk becomes unreachable. Flush immediately on the
2858
+ // first event so the ref is always on disk within the same synchronous
2859
+ // turn. Subsequent events use the normal debounced path.
2860
+ if (ref.eventCount === 1) {
2861
+ this.persistRunConversationNow(run, run.conversationId || run.id);
2862
+ }
2813
2863
  }
2814
2864
  catch (error) {
2815
2865
  run.events.push((0, hosts_1.createHubEvent)('system', 'Raw host event logging failed; visible conversation state was preserved.'));
@@ -3014,6 +3064,7 @@ class AiHubServer {
3014
3064
  current.status = 'running';
3015
3065
  current.sessionId = undefined;
3016
3066
  current.recoveryAttempts = 0;
3067
+ current.lastRecoverySignature = undefined;
3017
3068
  current.pauseReason = 'working';
3018
3069
  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.'));
3019
3070
  });
@@ -5174,6 +5225,14 @@ class AiHubServer {
5174
5225
  if (!option)
5175
5226
  return res.status(400).json({ error: `Unknown agent: ${hubId}` });
5176
5227
  try {
5228
+ // Issue #1285 (Implementation Strategy §3): self-heal any shim orphaned
5229
+ // in the legacy flat managed directory before this run's version checks
5230
+ // consult FRAIM's managed PATH, so a stale build can no longer shadow
5231
+ // the current one. Idempotent; cheap enough to run on every call.
5232
+ const removedShims = (0, managed_agent_paths_1.cleanupOrphanedManagedShims)();
5233
+ if (removedShims.length > 0) {
5234
+ console.warn(`[ai-hub] install-agent: removed orphaned managed shim(s): ${removedShims.join(', ')}`);
5235
+ }
5177
5236
  const systemPath = (0, managed_agent_paths_1.stripManagedAgentBinDirsFromPath)(process.env.PATH);
5178
5237
  const existingVersion = hubCommandVersion(option.launchCommand, undefined, systemPath);
5179
5238
  if (existingVersion) {
@@ -5200,62 +5259,19 @@ class AiHubServer {
5200
5259
  loginHint: `Once installed, click "Check if Ready" to verify ${option.label} is on your PATH.`,
5201
5260
  });
5202
5261
  }
5203
- let standardInstallError = null;
5204
- try {
5205
- await hubRunProcess('npm', ['install', '-g', option.installPackage], {
5206
- PATH: systemPath,
5207
- npm_config_prefix: undefined,
5208
- NPM_CONFIG_PREFIX: undefined,
5209
- });
5210
- const standardVersion = hubCommandVersion(option.launchCommand, undefined, systemPath);
5211
- const npmGlobalBinDirs = standardVersion
5212
- ? []
5213
- : (0, managed_agent_paths_1.resolveNpmGlobalBinDirs)(systemPath, {
5214
- npm_config_prefix: undefined,
5215
- NPM_CONFIG_PREFIX: undefined,
5216
- });
5217
- const standardVersionWithNpmBin = standardVersion
5218
- || (npmGlobalBinDirs.length > 0
5219
- ? hubCommandVersion(option.launchCommand, npmGlobalBinDirs, systemPath)
5220
- : null);
5221
- if (standardVersionWithNpmBin) {
5222
- if (npmGlobalBinDirs.length > 0) {
5223
- process.env.PATH = (0, managed_agent_paths_1.appendBinDirsToPath)(systemPath, npmGlobalBinDirs);
5224
- }
5225
- const mcp = await configureFraimForHubAgent(hubId);
5226
- if (!mcp.configured) {
5227
- console.warn(`[ai-hub] install-agent: FRAIM add-ide did not run for standard ${option.label}: ${mcp.error || 'unknown reason'}`);
5228
- }
5229
- // Issue #1010: something is now installed that was not before, so the cached
5230
- // agent-availability answer is stale. Drop it here rather than waiting out the
5231
- // TTL, so the newly installed CLI shows as available immediately.
5232
- (0, hosts_1.invalidateEmployeeDetectionCache)();
5233
- return res.json({
5234
- ok: true,
5235
- message: `${option.label} installed successfully.`,
5236
- needsLogin: true,
5237
- loginCommand: option.loginCommand,
5238
- loginHint: `Sign in to ${option.label} to activate it. A terminal window will open — complete sign-in there, then click "Check if Ready".`,
5239
- fraimConfigured: mcp.configured,
5240
- });
5241
- }
5242
- standardInstallError = `${option.label} standard install completed, but the CLI is not runnable from the user PATH.`;
5243
- }
5244
- catch (error) {
5245
- standardInstallError = error instanceof Error ? error.message : 'Unknown error';
5246
- }
5247
- const prefix = (0, managed_agent_paths_1.getManagedNodeRoot)();
5248
- fs_1.default.mkdirSync(prefix, { recursive: true });
5249
- await hubRunProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix });
5250
- const ver = hubCommandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
5251
- if (!ver) {
5252
- throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH. Standard install failure: ${standardInstallError}`);
5262
+ const outcome = await (0, managed_agent_install_1.installManagedAgent)({ label: option.label, installPackage: option.installPackage, launchCommand: option.launchCommand }, systemPath, { runProcess: hubRunProcess, commandVersion: hubCommandVersion });
5263
+ if (outcome.outcome === 'standard' && outcome.npmGlobalBinDirs.length > 0) {
5264
+ process.env.PATH = (0, managed_agent_paths_1.appendBinDirsToPath)(systemPath, outcome.npmGlobalBinDirs);
5253
5265
  }
5254
- // Issue #747: run `add-ide` for the newly installed agent so the FRAIM MCP is wired in
5255
- // and its first run works (previously the agent launched with no `fraim` MCP server).
5256
5266
  const mcp = await configureFraimForHubAgent(hubId);
5257
5267
  if (!mcp.configured) {
5258
- console.warn(`[ai-hub] install-agent: FRAIM add-ide did not run for ${option.label}: ${mcp.error || 'unknown reason'}`);
5268
+ console.warn(`[ai-hub] install-agent: FRAIM add-ide did not run for ${option.label} (${outcome.outcome} install): ${mcp.error || 'unknown reason'}`);
5269
+ }
5270
+ if (outcome.outcome === 'standard') {
5271
+ // Issue #1010: something is now installed that was not before, so the cached
5272
+ // agent-availability answer is stale. Drop it here rather than waiting out the
5273
+ // TTL, so the newly installed CLI shows as available immediately.
5274
+ (0, hosts_1.invalidateEmployeeDetectionCache)();
5259
5275
  }
5260
5276
  return res.json({
5261
5277
  ok: true,
@@ -5293,16 +5309,27 @@ class AiHubServer {
5293
5309
  });
5294
5310
  }
5295
5311
  });
5296
- this.app.post('/api/ai-hub/check-agent', (req, res) => {
5312
+ this.app.post('/api/ai-hub/check-agent', async (req, res) => {
5297
5313
  const { hubId } = req.body;
5298
5314
  if (!hubId)
5299
5315
  return res.status(400).json({ error: 'hubId is required.' });
5300
5316
  const option = hubAgentOption(hubId);
5301
5317
  if (!option)
5302
5318
  return res.status(400).json({ error: `Unknown agent: ${hubId}` });
5319
+ (0, managed_agent_paths_1.cleanupOrphanedManagedShims)();
5303
5320
  const ver = hubCommandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
5304
5321
  if (ver) {
5305
- return res.json({ ok: true, ready: true, message: `${option.label} is ready.` });
5322
+ // Issue #1285 (§4): a manager who ran `codex update` and clicks "Check
5323
+ // if Ready" should see PATH drift immediately rather than a bare pass.
5324
+ const health = await (0, agent_cli_health_checks_1.checkAgentCliHealthByCommand)(option.launchCommand);
5325
+ return res.json({
5326
+ ok: true,
5327
+ ready: true,
5328
+ message: `${option.label} is ready.`,
5329
+ ...(health && health.status !== 'passed'
5330
+ ? { driftWarning: health.message, driftSuggestion: health.suggestion }
5331
+ : {}),
5332
+ });
5306
5333
  }
5307
5334
  return res.json({
5308
5335
  ok: true,
@@ -6829,6 +6856,7 @@ class AiHubServer {
6829
6856
  : createHubRecoveryEvent(current, exitCode, current.recoveryAttempts));
6830
6857
  }
6831
6858
  current.lastRecoveryAt = new Date().toISOString();
6859
+ current.lastRecoverySignature = classification.matchedSignature ?? null;
6832
6860
  current.pauseReason = 'working';
6833
6861
  });
6834
6862
  const refreshed = this.runRegistry.get(runId);
@@ -18,6 +18,7 @@ const BUNDLED_ASSETS = {
18
18
  styles: ['/ai-hub/styles.css', '/ai-hub/review.css'],
19
19
  scripts: ['/ai-hub/script.js'],
20
20
  };
21
+ const REQUIRED_REMOTE_UI_CAPABILITIES = ['jobs.visualization'];
21
22
  function createAiHubBridgeInfo(options) {
22
23
  const remoteUiEnabled = options.remoteUiEnabled ?? process.env.FRAIM_HUB_REMOTE_UI === '1';
23
24
  return {
@@ -34,6 +35,7 @@ function createAiHubBridgeInfo(options) {
34
35
  'office.wordTaskpane': true,
35
36
  'customEmployees': true,
36
37
  'ui.serverDrivenRuntime': true,
38
+ 'jobs.visualization': true,
37
39
  },
38
40
  };
39
41
  }
@@ -53,6 +55,12 @@ function evaluateHubUiManifestCompatibility(manifest, bridge) {
53
55
  if (missing) {
54
56
  return { compatible: false, updateRequired: true, reason: `missing-capability:${missing}` };
55
57
  }
58
+ const missingRequired = REQUIRED_REMOTE_UI_CAPABILITIES.find((capability) => {
59
+ return bridge.capabilities[capability] && !manifest.requiredCapabilities.includes(capability);
60
+ });
61
+ if (missingRequired) {
62
+ return { compatible: false, updateRequired: true, reason: `missing-required-capability:${missingRequired}` };
63
+ }
56
64
  return { compatible: true, updateRequired: false };
57
65
  }
58
66
  function runtimeAssetsForRelease(releaseId, manifest) {
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPricingConfig = getPricingConfig;
4
+ const pricing_1 = require("../../config/pricing");
5
+ const feature_flags_1 = require("../../config/feature-flags");
6
+ /**
7
+ * GET /api/pricing/config
8
+ * Returns pricing configuration for frontend
9
+ */
10
+ async function getPricingConfig(req, res) {
11
+ try {
12
+ res.json({
13
+ pricing: pricing_1.PRICING,
14
+ fixedFees: pricing_1.FIXED_FEES,
15
+ managedPricing: pricing_1.MANAGED_PRICING,
16
+ founderDiscountRate: pricing_1.FOUNDER_DISCOUNT_RATE,
17
+ consumerEmailDomains: pricing_1.CONSUMER_EMAIL_DOMAINS,
18
+ featureFlags: (0, feature_flags_1.getPublicFeatureFlags)(),
19
+ });
20
+ }
21
+ catch (error) {
22
+ console.error('Error getting pricing config:', error);
23
+ res.status(500).json({ error: 'Failed to get pricing configuration' });
24
+ }
25
+ }