fraim-hub 2.0.265 → 2.0.267
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/catalog.js +76 -3
- package/dist/src/ai-hub/conversation-store-lock.js +4 -2
- package/dist/src/ai-hub/conversation-store.js +1 -0
- package/dist/src/ai-hub/host-session-state.js +2 -0
- package/dist/src/ai-hub/hosts.js +11 -0
- package/dist/src/ai-hub/server.js +299 -49
- package/dist/src/config/persona-capability-bundles.js +5 -4
- package/dist/src/local-mcp-server/learning-context-builder.js +44 -11
- package/package.json +2 -2
- package/public/ai-hub/index.html +0 -13
- package/public/ai-hub/script.js +80 -54
- package/public/ai-hub/styles.css +2 -1
|
@@ -62,8 +62,24 @@ const KNOWN_LABEL_OVERRIDES = {
|
|
|
62
62
|
// shouldn't be split on dashes by the default humanizer.
|
|
63
63
|
'1-1': '1:1',
|
|
64
64
|
};
|
|
65
|
+
// Issue #1090: this reads JOB-LEVEL `## Intent` / `## Outcome` only.
|
|
66
|
+
//
|
|
67
|
+
// The previous expression was unanchored (`## ${heading}`), which had two
|
|
68
|
+
// consequences on a personalized override that customizes phases:
|
|
69
|
+
//
|
|
70
|
+
// 1. `#### Intent` CONTAINS the substring `## Intent`, so a phase-level heading
|
|
71
|
+
// satisfied a job-level query and the first phase's Intent was promoted to
|
|
72
|
+
// job-level metadata.
|
|
73
|
+
// 2. The terminator `\r?\n## ` requires a space as the 4th character, so it
|
|
74
|
+
// could not stop at `#### Steps` and Outcome swallowed the Steps and Skills
|
|
75
|
+
// blocks until `---` or end of file.
|
|
76
|
+
//
|
|
77
|
+
// The start anchor is written as `(?:^|\r?\n)` rather than using the `m` flag,
|
|
78
|
+
// because under `m` the trailing `$` would match end-of-LINE and truncate a
|
|
79
|
+
// multi-line Outcome to its first bullet. Terminating on any heading level
|
|
80
|
+
// `#{1,6} ` fixes (2).
|
|
65
81
|
const sectionValue = (content, heading) => {
|
|
66
|
-
const match = content.match(new RegExp(
|
|
82
|
+
const match = content.match(new RegExp(`(?:^|\\r?\\n)## ${heading}[ \\t]*\\r?\\n([\\s\\S]*?)(?:\\r?\\n#{1,6} |\\r?\\n---|$)`));
|
|
67
83
|
if (!match)
|
|
68
84
|
return [];
|
|
69
85
|
return match[1]
|
|
@@ -72,6 +88,62 @@ const sectionValue = (content, heading) => {
|
|
|
72
88
|
.filter(Boolean)
|
|
73
89
|
.map((line) => line.replace(/^[-*]\s*/, '').trim());
|
|
74
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Issue #1090: resolve the baseline stub that an `extends`-only override inherits
|
|
93
|
+
* from, so job-level Intent/Outcome can be inherited rather than scavenged from
|
|
94
|
+
* the override's first phase.
|
|
95
|
+
*
|
|
96
|
+
* `extends` is written as `<category>/<job-name>`. Only NON-personalized layers
|
|
97
|
+
* are searched: an override extending another override is not a supported shape,
|
|
98
|
+
* and following one would risk a cycle.
|
|
99
|
+
*/
|
|
100
|
+
function resolveExtendedStubPath(projectPath, extendsValue) {
|
|
101
|
+
const relative = toPosix(extendsValue).replace(/\.md$/i, '');
|
|
102
|
+
if (!relative || relative.includes('..'))
|
|
103
|
+
return null;
|
|
104
|
+
const candidateRoots = [
|
|
105
|
+
path_1.default.join(projectPath, 'fraim', 'ai-employee', 'jobs'),
|
|
106
|
+
path_1.default.join(projectPath, 'fraim', 'ai-manager', 'jobs'),
|
|
107
|
+
path_1.default.join(projectPath, 'registry', 'jobs', 'ai-employee'),
|
|
108
|
+
path_1.default.join(projectPath, 'registry', 'jobs', 'ai-manager'),
|
|
109
|
+
path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-employee', 'jobs'),
|
|
110
|
+
];
|
|
111
|
+
for (const root of candidateRoots) {
|
|
112
|
+
const candidate = path_1.default.join(root, `${relative}.md`);
|
|
113
|
+
if (fs_1.default.existsSync(candidate))
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Job-level Intent/Outcome for a stub, following `extends` when the stub itself
|
|
120
|
+
* declares none. Returns empty arrays rather than throwing when nothing resolves,
|
|
121
|
+
* so a broken `extends` degrades to the existing "No intent summary available."
|
|
122
|
+
* behaviour instead of breaking catalog discovery.
|
|
123
|
+
*/
|
|
124
|
+
function jobLevelSections(content, filePath, projectPath) {
|
|
125
|
+
const intent = sectionValue(content, 'Intent');
|
|
126
|
+
const outcome = sectionValue(content, 'Outcome');
|
|
127
|
+
if (intent.length > 0 || outcome.length > 0)
|
|
128
|
+
return { intent, outcome };
|
|
129
|
+
const frontmatter = readJobFrontmatter(filePath);
|
|
130
|
+
const extendsValue = typeof frontmatter?.extends === 'string' ? frontmatter.extends.trim() : '';
|
|
131
|
+
if (!extendsValue)
|
|
132
|
+
return { intent, outcome };
|
|
133
|
+
const basePath = resolveExtendedStubPath(projectPath, extendsValue);
|
|
134
|
+
if (!basePath)
|
|
135
|
+
return { intent, outcome };
|
|
136
|
+
try {
|
|
137
|
+
const baseContent = fs_1.default.readFileSync(basePath, 'utf8');
|
|
138
|
+
return {
|
|
139
|
+
intent: sectionValue(baseContent, 'Intent'),
|
|
140
|
+
outcome: sectionValue(baseContent, 'Outcome'),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return { intent, outcome };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
75
147
|
const humanizeName = (raw) => {
|
|
76
148
|
const lower = raw.toLowerCase();
|
|
77
149
|
if (KNOWN_LABEL_OVERRIDES[lower])
|
|
@@ -122,8 +194,9 @@ function parseJobStub(filePath, categoryId, categoryLabel, projectPath, personal
|
|
|
122
194
|
const fileName = path_1.default.basename(filePath, '.md');
|
|
123
195
|
const frontmatter = readJobFrontmatter(filePath);
|
|
124
196
|
const title = frontmatter?.displayName?.trim() || humanizeName(fileName);
|
|
125
|
-
const
|
|
126
|
-
const
|
|
197
|
+
const sections = jobLevelSections(content, filePath, projectPath);
|
|
198
|
+
const intent = sections.intent[0] || 'No intent summary available.';
|
|
199
|
+
const outcome = sections.outcome;
|
|
127
200
|
return {
|
|
128
201
|
id: fileName,
|
|
129
202
|
title,
|
|
@@ -51,9 +51,11 @@ function withBucketLock(lockPath, fn, opts = {}) {
|
|
|
51
51
|
fd = fs_1.default.openSync(lockPath, 'wx'); // O_CREAT | O_EXCL: fails if the lock already exists
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
|
-
|
|
54
|
+
const code = error.code;
|
|
55
|
+
const retryableContention = code === 'EEXIST' || code === 'EPERM' || code === 'EACCES';
|
|
56
|
+
if (!retryableContention)
|
|
55
57
|
throw error;
|
|
56
|
-
if (isLockStale(lockPath, staleMs)) {
|
|
58
|
+
if (fs_1.default.existsSync(lockPath) && isLockStale(lockPath, staleMs)) {
|
|
57
59
|
try {
|
|
58
60
|
fs_1.default.unlinkSync(lockPath);
|
|
59
61
|
}
|
|
@@ -89,6 +89,7 @@ class HostSessionState {
|
|
|
89
89
|
if (conversation.sessionId === normalizedSessionId) {
|
|
90
90
|
const replacement = this.resolve({ ...conversation, sessionId: null }, owner);
|
|
91
91
|
conversation.sessionId = replacement?.sessionId || null;
|
|
92
|
+
conversation.resumeCommand = null;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
markInvalidRun(run, owner, sessionId, reason, at = new Date().toISOString()) {
|
|
@@ -110,6 +111,7 @@ class HostSessionState {
|
|
|
110
111
|
};
|
|
111
112
|
if (run.sessionId === normalizedSessionId) {
|
|
112
113
|
run.sessionId = undefined;
|
|
114
|
+
run.resumeCommand = null;
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
hasInvalidSession(conversation, owner, sessionId) {
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.parseUsageSignal = parseUsageSignal;
|
|
|
10
10
|
exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
|
|
11
11
|
exports.__setAgentAvailabilityPathForTests = __setAgentAvailabilityPathForTests;
|
|
12
12
|
exports.invalidateEmployeeDetectionCache = invalidateEmployeeDetectionCache;
|
|
13
|
+
exports.__clearEmployeeDetectionMemoryCacheForTests = __clearEmployeeDetectionMemoryCacheForTests;
|
|
13
14
|
exports.__setEmployeeDetectionTtlForTests = __setEmployeeDetectionTtlForTests;
|
|
14
15
|
exports.__getEmployeeProbeRoundsForTests = __getEmployeeProbeRoundsForTests;
|
|
15
16
|
exports.__resetEmployeeProbeRoundsForTests = __resetEmployeeProbeRoundsForTests;
|
|
@@ -940,6 +941,13 @@ function invalidateEmployeeDetectionCache() {
|
|
|
940
941
|
}
|
|
941
942
|
catch { /* best effort */ }
|
|
942
943
|
}
|
|
944
|
+
/** Test seam only. Simulates a process restart while preserving persisted last-known data. */
|
|
945
|
+
function __clearEmployeeDetectionMemoryCacheForTests() {
|
|
946
|
+
cachedEmployees = null;
|
|
947
|
+
cachedEmployeesAtMs = 0;
|
|
948
|
+
cachedEmployeesContext = null;
|
|
949
|
+
inFlightDetection = null;
|
|
950
|
+
}
|
|
943
951
|
/** Test seam only. Mirrors __resetLatestVersionCache in hub-latest-version.ts. */
|
|
944
952
|
function __setEmployeeDetectionTtlForTests(ttlMs) {
|
|
945
953
|
employeeDetectionTtlMs = ttlMs ?? EMPLOYEE_DETECTION_TTL_MS;
|
|
@@ -1493,6 +1501,9 @@ function parseHostLine(hostId, line) {
|
|
|
1493
1501
|
if (hostId === 'codex') {
|
|
1494
1502
|
try {
|
|
1495
1503
|
const parsed = JSON.parse(trimmed);
|
|
1504
|
+
if (parsed.type === 'system' && parsed.subtype === 'status' && parsed.status === 'compacting') {
|
|
1505
|
+
return withSignal({ raw: trimmed, hostLifecycle: { status: 'compacting', source: hostId } });
|
|
1506
|
+
}
|
|
1496
1507
|
if (parsed.type === 'thread.started' && parsed.thread_id) {
|
|
1497
1508
|
return withSignal({ sessionId: parsed.thread_id, raw: trimmed });
|
|
1498
1509
|
}
|