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.
- package/dist/src/ai-hub/hosts.js +90 -17
- package/dist/src/ai-hub/server.js +90 -62
- package/dist/src/ai-hub/ui-runtime.js +8 -0
- package/dist/src/api/pricing/get-config.js +25 -0
- package/dist/src/cli/doctor/checks/agent-cli-health-checks.js +165 -0
- package/dist/src/cli/mcp/command-resolution.js +43 -43
- package/dist/src/cli/utils/managed-agent-install.js +55 -0
- package/dist/src/cli/utils/managed-agent-paths.js +55 -2
- package/dist/src/core/job-visualization.js +161 -0
- package/dist/src/core/utils/inheritance-parser.js +293 -0
- package/dist/src/core/utils/job-parser.js +179 -0
- package/dist/src/core/utils/local-registry-resolver.js +820 -0
- package/dist/src/local-mcp-server/learning-usage-projection.js +79 -0
- package/package.json +10 -2
- package/public/ai-hub/script.js +50 -16
- package/public/ai-hub/styles.css +7 -0
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -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
|
|
839
|
+
return resolvedPlan;
|
|
825
840
|
}
|
|
826
|
-
const [command, ...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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
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
|
-
|
|
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
|
+
}
|