fraim-hub 2.0.345 → 2.0.348
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/registry/scripts/learning/completion-state.js +123 -4
- package/dist/registry/scripts/learning/corpus-audit.js +205 -0
- package/dist/registry/scripts/learning/entry-format.js +10 -1
- package/dist/registry/scripts/learning/outcome-types.js +92 -0
- package/dist/registry/scripts/learning/owner-identity.js +148 -0
- package/dist/registry/scripts/learning/proposal-queue.js +81 -0
- package/dist/src/ai-hub/hosts.js +35 -1
- package/dist/src/ai-hub/hub-app-materializer.js +20 -0
- package/dist/src/ai-hub/server.js +10 -3
- package/dist/src/local-mcp-server/learning-context-builder.js +141 -477
- package/dist/src/local-mcp-server/learning-entry-format.js +25 -0
- package/dist/src/local-mcp-server/learning-usage-eligibility.js +8 -14
- package/dist/src/local-mcp-server/learning-usage-projection.js +5 -22
- package/dist/src/local-mcp-server/learning-usage-store.js +93 -393
- package/package.json +11 -2
- package/public/ai-hub/review.css +2 -1
- package/public/ai-hub/script.js +161 -188
- package/public/ai-hub/styles.css +2 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ownerArtifactRoot = void 0;
|
|
7
|
+
exports.ownerPath = ownerPath;
|
|
8
|
+
exports.validateOwner = validateOwner;
|
|
9
|
+
exports.ownerKey = ownerKey;
|
|
10
|
+
exports.assertSameOwner = assertSameOwner;
|
|
11
|
+
exports.assertLearningTargetPath = assertLearningTargetPath;
|
|
12
|
+
exports.resolveOwnerHome = resolveOwnerHome;
|
|
13
|
+
exports.ownerArtifactPath = ownerArtifactPath;
|
|
14
|
+
exports.configuredArtifactPath = configuredArtifactPath;
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
/** A recorded home path is immutable; moving storage requires a newly reviewed run. */
|
|
18
|
+
function ownerPath(root) {
|
|
19
|
+
const absolute = node_path_1.default.resolve(root).replace(/\\/g, '/');
|
|
20
|
+
return process.platform === 'win32' ? absolute.toLowerCase() : absolute;
|
|
21
|
+
}
|
|
22
|
+
function validateOwner(owner) {
|
|
23
|
+
if (!owner || !['Project', 'Manager', 'Org'].includes(owner.level)
|
|
24
|
+
|| Object.keys(owner).some(key => !['level', 'path'].includes(key))
|
|
25
|
+
|| typeof owner.path !== 'string' || !node_path_1.default.isAbsolute(owner.path)
|
|
26
|
+
|| /[\x00-\x1f]/.test(owner.path) || owner.path !== ownerPath(owner.path)) {
|
|
27
|
+
throw new Error('owner-unresolved: a canonical absolute home path is required');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function ownerKey(owner) {
|
|
31
|
+
validateOwner(owner);
|
|
32
|
+
return JSON.stringify([owner.level, owner.path]);
|
|
33
|
+
}
|
|
34
|
+
function assertSameOwner(expected, actual) {
|
|
35
|
+
if (ownerKey(expected) !== ownerKey(actual))
|
|
36
|
+
throw new Error('owner-path-conflict: the approved home differs from the configured path');
|
|
37
|
+
}
|
|
38
|
+
/** Shared authored-family identity check. Handwritten entries can omit Domain metadata. */
|
|
39
|
+
function assertLearningTargetPath(ref, domain) {
|
|
40
|
+
const subject = ref.owner.level === 'Org' ? 'org' : ref.learningSubject;
|
|
41
|
+
if (!subject || !/^[a-zA-Z0-9@._+-]+$/.test(subject))
|
|
42
|
+
throw new Error('learning-subject-required');
|
|
43
|
+
const escaped = subject.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
|
+
const match = ref.relativePath.match(new RegExp(`^learnings/${escaped}-((?:[a-z][a-z-]*-)?)(preferences|mistake-patterns|validated-patterns|manager-coaching)(\\.cold)?\\.md$`));
|
|
45
|
+
if (!match || (domain !== undefined && match[1] !== (domain === 'global' ? '' : domain + '-')))
|
|
46
|
+
throw new Error('domain-target-mismatch: learning subject, domain and family must match its file');
|
|
47
|
+
}
|
|
48
|
+
/** Pure read: resolve existing configured homes without migration or identity configuration. */
|
|
49
|
+
function resolveOwnerHome(projectPath, level, options) {
|
|
50
|
+
const load = (relative) => options.runtimeModule(projectPath, relative);
|
|
51
|
+
const paths = load('core/utils/project-fraim-paths');
|
|
52
|
+
let home;
|
|
53
|
+
if (level === 'Project') {
|
|
54
|
+
const root = paths.getWorkspaceFraimPath(projectPath);
|
|
55
|
+
home = { root, artifactRoot: node_path_1.default.resolve(projectPath), contentRoot: node_path_1.default.join(root, 'personalized-employee'), requiresPublish: false, backend: 'project' };
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (!['Manager', 'Org'].includes(level))
|
|
59
|
+
throw new Error('owner-unresolved: invalid owner level');
|
|
60
|
+
const configFile = node_path_1.default.join(paths.getUserFraimDirPath(), 'config.json');
|
|
61
|
+
let config;
|
|
62
|
+
try {
|
|
63
|
+
config = node_fs_1.default.existsSync(configFile) ? JSON.parse(node_fs_1.default.readFileSync(configFile, 'utf8')) : {};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error('home-unavailable: user configuration is invalid');
|
|
67
|
+
}
|
|
68
|
+
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
69
|
+
throw new Error('home-unavailable: user configuration is invalid');
|
|
70
|
+
const raw = level === 'Manager' ? config.managerStorage : config.organization;
|
|
71
|
+
if (raw !== undefined && (!raw || typeof raw !== 'object' || Array.isArray(raw)
|
|
72
|
+
|| !['git', 'local-folder', 'fraim-cloud', 'single-machine'].includes(raw.backend)
|
|
73
|
+
|| (raw.backend === 'git' && (typeof raw.gitUrl !== 'string' || !raw.gitUrl.trim()))
|
|
74
|
+
|| (raw.backend === 'local-folder' && (typeof raw.localPath !== 'string' || !raw.localPath.trim())))) {
|
|
75
|
+
throw new Error('home-unavailable: invalid configured storage home');
|
|
76
|
+
}
|
|
77
|
+
const resolved = load('cli/utils/pack-home').resolvePackHome(level === 'Manager' ? 'manager' : 'org', { migrateLegacy: false });
|
|
78
|
+
home = { root: resolved.root, contentRoot: resolved.contentRoot, requiresPublish: resolved.requiresPublish, backend: resolved.backend, ...(resolved.gitUrl ? { gitUrl: resolved.gitUrl } : {}) };
|
|
79
|
+
}
|
|
80
|
+
const owner = { level, path: ownerPath(home.root) };
|
|
81
|
+
if (options.expectedOwner)
|
|
82
|
+
assertSameOwner(options.expectedOwner, owner);
|
|
83
|
+
if (!node_fs_1.default.existsSync(home.root) || !node_fs_1.default.statSync(home.root).isDirectory()
|
|
84
|
+
|| (home.backend === 'git' && !node_fs_1.default.existsSync(node_path_1.default.join(home.root, '.git')))) {
|
|
85
|
+
throw new Error('home-unavailable: configured home is not available locally');
|
|
86
|
+
}
|
|
87
|
+
return { ...home, level, owner };
|
|
88
|
+
}
|
|
89
|
+
/** Resolve a relative artifact without permitting lexical or realpath escapes, including junctions. */
|
|
90
|
+
function ownerArtifactPath(home, relativePath) {
|
|
91
|
+
assertSameOwner(home.owner, { level: home.level, path: ownerPath(home.root) });
|
|
92
|
+
if (!relativePath || node_path_1.default.isAbsolute(relativePath) || relativePath.includes('\\')
|
|
93
|
+
|| relativePath.includes(':') || relativePath.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
94
|
+
throw new Error('invalid-artifact-path');
|
|
95
|
+
}
|
|
96
|
+
// Shared project capabilities live in the repository registry, outside its personalized pack.
|
|
97
|
+
const shared = home.level === 'Project' && relativePath.startsWith('registry/');
|
|
98
|
+
const containmentRoot = shared ? node_path_1.default.dirname(home.root) : home.root;
|
|
99
|
+
// The workspace configuration has one explicit reference; arbitrary parent paths stay forbidden.
|
|
100
|
+
const workspaceConfig = home.level === 'Project' && relativePath === 'config.json';
|
|
101
|
+
const contentRoot = shared ? containmentRoot : workspaceConfig ? home.root : home.contentRoot;
|
|
102
|
+
const target = node_path_1.default.resolve(contentRoot, relativePath);
|
|
103
|
+
const existingAncestor = (file) => {
|
|
104
|
+
let current = file;
|
|
105
|
+
while (!node_fs_1.default.existsSync(current)) {
|
|
106
|
+
const parent = node_path_1.default.dirname(current);
|
|
107
|
+
if (parent === current)
|
|
108
|
+
throw new Error('home-unavailable');
|
|
109
|
+
current = parent;
|
|
110
|
+
}
|
|
111
|
+
return current;
|
|
112
|
+
};
|
|
113
|
+
const realRoot = node_fs_1.default.realpathSync(containmentRoot);
|
|
114
|
+
const ancestor = node_fs_1.default.realpathSync(existingAncestor(target));
|
|
115
|
+
const relative = node_path_1.default.relative(realRoot, ancestor);
|
|
116
|
+
if (relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative))
|
|
117
|
+
throw new Error('invalid-artifact-path: escaped owner home');
|
|
118
|
+
// Also reject escaping a content root via a symlink to a sibling in the pack.
|
|
119
|
+
if (node_fs_1.default.existsSync(contentRoot)) {
|
|
120
|
+
const contentRelative = node_path_1.default.relative(node_fs_1.default.realpathSync(contentRoot), ancestor);
|
|
121
|
+
if (contentRelative === '..' || contentRelative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(contentRelative))
|
|
122
|
+
throw new Error('invalid-artifact-path: escaped content root');
|
|
123
|
+
}
|
|
124
|
+
return target;
|
|
125
|
+
}
|
|
126
|
+
/** Explicit artifact base; never infer a project from its personalized directory. */
|
|
127
|
+
const ownerArtifactRoot = (home) => home.artifactRoot ?? home.root;
|
|
128
|
+
exports.ownerArtifactRoot = ownerArtifactRoot;
|
|
129
|
+
/** Configured artifacts may sit outside the personalized pack, but never outside their owner. */
|
|
130
|
+
function configuredArtifactPath(home, file) {
|
|
131
|
+
assertSameOwner(home.owner, { level: home.level, path: ownerPath(home.root) });
|
|
132
|
+
const root = node_path_1.default.resolve((0, exports.ownerArtifactRoot)(home));
|
|
133
|
+
const target = node_path_1.default.resolve(file);
|
|
134
|
+
const relative = node_path_1.default.relative(root, target);
|
|
135
|
+
if (!relative || relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative))
|
|
136
|
+
throw new Error('invalid-artifact-path: escaped owner home');
|
|
137
|
+
for (let current = target; ownerPath(current) !== ownerPath(root); current = node_path_1.default.dirname(current)) {
|
|
138
|
+
try {
|
|
139
|
+
if (node_fs_1.default.lstatSync(current).isSymbolicLink())
|
|
140
|
+
throw new Error('invalid-artifact-path: linked artifact');
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error.code !== 'ENOENT')
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return target;
|
|
148
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.hashContent = void 0;
|
|
7
|
+
exports.atomicWrite = atomicWrite;
|
|
8
|
+
exports.withFileLock = withFileLock;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
|
+
const node_os_1 = require("node:os");
|
|
13
|
+
const hashContent = (text) => (0, node_crypto_1.createHash)('sha256').update(text).digest('hex');
|
|
14
|
+
exports.hashContent = hashContent;
|
|
15
|
+
function atomicWrite(file, text) {
|
|
16
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
17
|
+
const temporary = file + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
|
|
18
|
+
try {
|
|
19
|
+
node_fs_1.default.writeFileSync(temporary, text, { encoding: 'utf8', flag: 'wx' });
|
|
20
|
+
const fd = node_fs_1.default.openSync(temporary, 'r+');
|
|
21
|
+
try {
|
|
22
|
+
node_fs_1.default.fsyncSync(fd);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
node_fs_1.default.closeSync(fd);
|
|
26
|
+
}
|
|
27
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (node_fs_1.default.existsSync(temporary))
|
|
31
|
+
node_fs_1.default.unlinkSync(temporary);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function withFileLock(file, fn) {
|
|
35
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
36
|
+
let fd;
|
|
37
|
+
try {
|
|
38
|
+
fd = node_fs_1.default.openSync(file + '.lock', 'wx');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Serialize recovery so two contenders cannot unlink each other's new lock.
|
|
42
|
+
let recovery;
|
|
43
|
+
try {
|
|
44
|
+
recovery = node_fs_1.default.openSync(file + '.lock.recovery', 'wx');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error('busy: lock recovery is already in progress');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const owner = JSON.parse(node_fs_1.default.readFileSync(file + '.lock', 'utf8'));
|
|
51
|
+
let stale = false;
|
|
52
|
+
if (owner.host === (0, node_os_1.hostname)() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(owner.pid, 0);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
stale = error.code === 'ESRCH';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!stale)
|
|
61
|
+
throw new Error('busy');
|
|
62
|
+
node_fs_1.default.unlinkSync(file + '.lock');
|
|
63
|
+
fd = node_fs_1.default.openSync(file + '.lock', 'wx');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error('busy: reconcile outstanding lock before retry');
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
node_fs_1.default.closeSync(recovery);
|
|
70
|
+
node_fs_1.default.unlinkSync(file + '.lock.recovery');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
node_fs_1.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, host: (0, node_os_1.hostname)(), at: new Date().toISOString() }));
|
|
74
|
+
try {
|
|
75
|
+
return fn();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
node_fs_1.default.closeSync(fd);
|
|
79
|
+
node_fs_1.default.unlinkSync(file + '.lock');
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -31,6 +31,7 @@ exports.buildDirectStartPlan = buildDirectStartPlan;
|
|
|
31
31
|
exports.buildDirectContinuePlan = buildDirectContinuePlan;
|
|
32
32
|
exports.parseHostLine = parseHostLine;
|
|
33
33
|
exports.detectCodexCompactTaskFailure = detectCodexCompactTaskFailure;
|
|
34
|
+
exports.wireHostProcess = wireHostProcess;
|
|
34
35
|
exports.describeSpawnError = describeSpawnError;
|
|
35
36
|
exports.hostDefaultUnsetEnv = hostDefaultUnsetEnv;
|
|
36
37
|
exports.buildHostSpawnEnv = buildHostSpawnEnv;
|
|
@@ -2398,6 +2399,32 @@ function detectCodexCompactTaskFailure(line) {
|
|
|
2398
2399
|
return null;
|
|
2399
2400
|
}
|
|
2400
2401
|
function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
2402
|
+
// Exec's JSON error has no auth code. Require independent provider stderr
|
|
2403
|
+
// provenance AND the same terminal protocol error; never scan tool/prose text.
|
|
2404
|
+
const codexRefreshFailure = 'Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.';
|
|
2405
|
+
let codexAuthDiagnostic = false;
|
|
2406
|
+
let codexTerminalAuthFailure = false;
|
|
2407
|
+
const observeCodexAuth = (line, channel) => {
|
|
2408
|
+
if (hostId !== 'codex')
|
|
2409
|
+
return;
|
|
2410
|
+
if (channel === 'stderr') {
|
|
2411
|
+
const clean = line.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
2412
|
+
const diagnostic = clean.match(/^(?:\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+)?ERROR\s+codex_login::auth::manager:\s+Failed to refresh token: (.+)$/);
|
|
2413
|
+
if (diagnostic?.[1] === codexRefreshFailure)
|
|
2414
|
+
codexAuthDiagnostic = true;
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
try {
|
|
2418
|
+
const event = JSON.parse(line);
|
|
2419
|
+
if (event.type === 'turn.failed' || event.type === 'error') {
|
|
2420
|
+
codexTerminalAuthFailure = (event.type === 'turn.failed' ? event.error?.message : event.message) === codexRefreshFailure;
|
|
2421
|
+
}
|
|
2422
|
+
else if (event.type === 'turn.started' || event.type === 'turn.completed') {
|
|
2423
|
+
codexTerminalAuthFailure = false;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
catch { /* Plain stdout cannot establish authentication failure. */ }
|
|
2427
|
+
};
|
|
2401
2428
|
const wire = (buffer, channel) => {
|
|
2402
2429
|
let pending = '';
|
|
2403
2430
|
let suppressCodexModelRefreshStderr = false;
|
|
@@ -2406,6 +2433,7 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
|
2406
2433
|
const lines = pending.split(/\r?\n/);
|
|
2407
2434
|
pending = lines.pop() || '';
|
|
2408
2435
|
for (const line of lines) {
|
|
2436
|
+
observeCodexAuth(line, channel);
|
|
2409
2437
|
if (hostId === 'codex' && channel === 'stderr') {
|
|
2410
2438
|
if (line.includes('codex_models_manager::manager: failed to refresh available models:')) {
|
|
2411
2439
|
suppressCodexModelRefreshStderr = true;
|
|
@@ -2431,6 +2459,7 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
|
2431
2459
|
});
|
|
2432
2460
|
buffer.on('end', () => {
|
|
2433
2461
|
if (pending.trim().length > 0) {
|
|
2462
|
+
observeCodexAuth(pending, channel);
|
|
2434
2463
|
const parsed = parseHostLine(hostId, pending);
|
|
2435
2464
|
if (parsed.message || parsed.sessionId || parsed.raw) {
|
|
2436
2465
|
handlers.onEvent(parsed, channel);
|
|
@@ -2444,7 +2473,12 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
|
2444
2473
|
// signal='SIGSEGV') are NOT treated as clean exits by classifyExit's
|
|
2445
2474
|
// `exitCode ?? 0` guard. Pass -1 for any signal-terminated child so the
|
|
2446
2475
|
// non-zero branch fires and recovery is attempted.
|
|
2447
|
-
child.on('close', (code, signal) =>
|
|
2476
|
+
child.on('close', (code, signal) => {
|
|
2477
|
+
if (!signal && code !== null && code !== 0 && codexAuthDiagnostic && codexTerminalAuthFailure) {
|
|
2478
|
+
handlers.onEvent({ hostAuthenticationRequired: true, hostError: codexRefreshFailure, raw: codexRefreshFailure }, 'system');
|
|
2479
|
+
}
|
|
2480
|
+
handlers.onExit(signal ? -1 : code);
|
|
2481
|
+
});
|
|
2448
2482
|
// Issue #1327: without this listener, an OS-level spawn failure (EAGAIN,
|
|
2449
2483
|
// ENOENT, EACCES) fires an 'error' event on the ChildProcess. An unhandled
|
|
2450
2484
|
// EventEmitter 'error' throws synchronously and crashes the Hub process.
|
|
@@ -10,6 +10,7 @@ exports.resolveHubAppCacheDir = resolveHubAppCacheDir;
|
|
|
10
10
|
exports.isHubAppCacheComplete = isHubAppCacheComplete;
|
|
11
11
|
exports.listCachedHubAppVersions = listCachedHubAppVersions;
|
|
12
12
|
exports.runNpmInstall = runNpmInstall;
|
|
13
|
+
exports.pruneHubAppCache = pruneHubAppCache;
|
|
13
14
|
exports.ensureMaterializedHubApp = ensureMaterializedHubApp;
|
|
14
15
|
exports.resolveHubAppEntry = resolveHubAppEntry;
|
|
15
16
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -209,6 +210,24 @@ const defaultInstallPackage = async (version, destDir) => {
|
|
|
209
210
|
}
|
|
210
211
|
fs_1.default.rmSync(installedRoot, { recursive: true, force: true });
|
|
211
212
|
};
|
|
213
|
+
const HUB_APP_CACHE_MAX_VERSIONS = 3;
|
|
214
|
+
/** Prune the hub-app cache to at most HUB_APP_CACHE_MAX_VERSIONS entries, always preserving keepVersion. Non-fatal. */
|
|
215
|
+
function pruneHubAppCache(fraimDir, keepVersion) {
|
|
216
|
+
try {
|
|
217
|
+
const all = listCachedHubAppVersions(fraimDir);
|
|
218
|
+
const sorted = all.map((e) => e.version).sort((a, b) => (semver_1.default.gt(a, b) ? -1 : 1));
|
|
219
|
+
const toKeep = new Set(sorted.slice(0, HUB_APP_CACHE_MAX_VERSIONS));
|
|
220
|
+
toKeep.add(keepVersion);
|
|
221
|
+
for (const v of sorted) {
|
|
222
|
+
if (!toKeep.has(v)) {
|
|
223
|
+
fs_1.default.rmSync(resolveHubAppCacheDir(v, fraimDir), { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Prune failure must never break a launch.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
212
231
|
/**
|
|
213
232
|
* Return the entry path for `version`, materializing it first if it is not already a complete
|
|
214
233
|
* cache entry. Returns immediately (no npm invocation) when a complete entry already exists.
|
|
@@ -248,6 +267,7 @@ async function ensureMaterializedHubApp(version, options = {}) {
|
|
|
248
267
|
finally {
|
|
249
268
|
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
250
269
|
}
|
|
270
|
+
pruneHubAppCache(fraimDir, version);
|
|
251
271
|
return entryPath;
|
|
252
272
|
}
|
|
253
273
|
/**
|
|
@@ -2596,7 +2596,9 @@ function summarizeHostErrorForThread(error) {
|
|
|
2596
2596
|
// noise that the system handled on its own.
|
|
2597
2597
|
function pushTerminalFailureToThread(run, exitCode) {
|
|
2598
2598
|
if (exitCode !== 0 && run.lastHostError) {
|
|
2599
|
-
|
|
2599
|
+
const detail = detectLoginFailureSignature(run, exitCode)
|
|
2600
|
+
? run.lastHostError : summarizeHostErrorForThread(run.lastHostError);
|
|
2601
|
+
run.messages.push((0, hosts_1.createHubMessage)('system', `This run stopped: ${detail}`));
|
|
2600
2602
|
}
|
|
2601
2603
|
}
|
|
2602
2604
|
function createHubRecoveryEvent(run, exitCode, attempt) {
|
|
@@ -3017,6 +3019,7 @@ class AiHubServer {
|
|
|
3017
3019
|
this.latestVersionPollTimer = null;
|
|
3018
3020
|
this.catalogSyncChild = null;
|
|
3019
3021
|
this.attachmentPolicy = (0, attachment_store_1.attachmentPolicy)();
|
|
3022
|
+
this.configureAgentSurface = options.configureAgentSurface ?? configureFraimForHubAgent;
|
|
3020
3023
|
this.openTerminal = options.openTerminal ?? hubOpenTerminal;
|
|
3021
3024
|
this.preferencesStore = options.preferencesStore || new preferences_1.AiHubPreferencesStore();
|
|
3022
3025
|
const launchResolution = options.projectPath
|
|
@@ -4254,7 +4257,10 @@ class AiHubServer {
|
|
|
4254
4257
|
const option = hubAgentOption(run.hostId);
|
|
4255
4258
|
run.status = 'failed';
|
|
4256
4259
|
run.pauseReason = 'awaiting_user';
|
|
4257
|
-
run.loginRequired = option ? {
|
|
4260
|
+
run.loginRequired = option ? {
|
|
4261
|
+
agentLabel: option.label,
|
|
4262
|
+
...(run.hostId === 'codex' && run.lastHostError ? { remediation: run.lastHostError } : {}),
|
|
4263
|
+
} : undefined;
|
|
4258
4264
|
}
|
|
4259
4265
|
if (run.status !== 'running') {
|
|
4260
4266
|
run.eventLogRefs = (run.eventLogRefs || []).map((ref) => {
|
|
@@ -5856,6 +5862,7 @@ class AiHubServer {
|
|
|
5856
5862
|
}
|
|
5857
5863
|
try {
|
|
5858
5864
|
const result = (0, learning_context_builder_1.applyLearningEntryChange)(resolveProjectPath(req), userEmail, ref, b.action, {
|
|
5865
|
+
originalId: b.originalId,
|
|
5859
5866
|
originalTitle: b.originalTitle,
|
|
5860
5867
|
severity: b.severity,
|
|
5861
5868
|
title: b.title,
|
|
@@ -7012,7 +7019,7 @@ class AiHubServer {
|
|
|
7012
7019
|
return res.json({ ok: true, configured: [], message: 'No ready CLI agents to configure.' });
|
|
7013
7020
|
}
|
|
7014
7021
|
const results = await Promise.all(readyIds.map(async (hubId) => {
|
|
7015
|
-
const result = await
|
|
7022
|
+
const result = await this.configureAgentSurface(hubId);
|
|
7016
7023
|
return { hubId, ...result };
|
|
7017
7024
|
}));
|
|
7018
7025
|
const failed = results.filter((r) => !r.configured);
|