fraim-hub 2.0.324 → 2.0.326
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/conversation-store.js +29 -27
- package/dist/src/ai-hub/hosts.js +5 -2
- package/dist/src/ai-hub/raw-event-log-store.js +130 -20
- package/dist/src/ai-hub/server.js +150 -144
- package/dist/src/ai-hub/types.js +2 -0
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +106 -6
- package/package.json +2 -2
- package/public/ai-hub/index.html +7 -3
- package/public/ai-hub/review.css +4 -0
- package/public/ai-hub/script.js +114 -80
- package/public/ai-hub/styles.css +9 -3
|
@@ -17,22 +17,19 @@ const conversation_search_projection_1 = require("./conversation-search-projecti
|
|
|
17
17
|
// They are NOT filesystem paths and must never be passed through path.resolve.
|
|
18
18
|
exports.MANAGER_SCOPE_KEY = '@manager';
|
|
19
19
|
exports.COMPANY_SCOPE_KEY = '@company';
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'_stopping',
|
|
32
|
-
'_priorEvents',
|
|
33
|
-
'_priorMessages',
|
|
20
|
+
// Issue #1765: public list metadata is opt-in. These fields feed the shared Projects,
|
|
21
|
+
// Manager and Company rail identity, ordering, status, trigger and delegation controls
|
|
22
|
+
// (buildAreaRunButton / conversationUiState in public/ai-hub/script.js). New body fields
|
|
23
|
+
// must never silently enlarge every list response or short-circuit lazy hydration.
|
|
24
|
+
const HEADER_FIELDS = [
|
|
25
|
+
'id', 'projectPath', 'scope', 'invokedArea', 'title', 'jobId', 'jobTitle',
|
|
26
|
+
'agentName', 'configuredAgentId', 'configuredAgentLabel', 'baseHostId', 'personaKey',
|
|
27
|
+
'runId', 'status', 'pauseReason', 'createdAt', 'lastUpdatedAt', 'issueNumber',
|
|
28
|
+
'sourceTrigger', 'blocked', 'stopped', 'reviewApproved', 'compareMode', 'compareRunId',
|
|
29
|
+
'managedByRunId', 'managedByPersonaKey', 'managedReviewStatus', 'delegationTaskId',
|
|
30
|
+
'humanCoachingDisabled', 'personaSnapshot', 'reviewHandoff',
|
|
34
31
|
];
|
|
35
|
-
const
|
|
32
|
+
const HEADER_FIELD_SET = new Set([...HEADER_FIELDS, 'personaSnapshot', 'delegation', 'reviewHandoff']);
|
|
36
33
|
/**
|
|
37
34
|
* Issue #708: resolve the conversation store bucket key for a given scope.
|
|
38
35
|
* - 'manager'/'company' → a stable sentinel key (project-independent home).
|
|
@@ -361,18 +358,23 @@ function newestFirst(a, b) {
|
|
|
361
358
|
return timestampValue(b.lastUpdatedAt) - timestampValue(a.lastUpdatedAt);
|
|
362
359
|
}
|
|
363
360
|
function toHeader(conv) {
|
|
364
|
-
const header =
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
delete header[field];
|
|
368
|
-
if (delegation)
|
|
369
|
-
header.delegation = delegation;
|
|
370
|
-
for (const field of Object.keys(header)) {
|
|
371
|
-
if (field.startsWith('_'))
|
|
372
|
-
delete header[field];
|
|
361
|
+
const header = pickHeaderFields(conv, HEADER_FIELDS);
|
|
362
|
+
if (conv.delegation !== undefined) {
|
|
363
|
+
header.delegation = compactDelegationForHeader(conv.delegation) ?? null;
|
|
373
364
|
}
|
|
374
365
|
return header;
|
|
375
366
|
}
|
|
367
|
+
function pickHeaderFields(raw, fields) {
|
|
368
|
+
const result = {};
|
|
369
|
+
if (!raw || typeof raw !== 'object')
|
|
370
|
+
return result;
|
|
371
|
+
const value = raw;
|
|
372
|
+
for (const field of fields) {
|
|
373
|
+
if (Object.prototype.hasOwnProperty.call(value, field))
|
|
374
|
+
result[field] = value[field];
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
376
378
|
function compactDelegationForHeader(raw) {
|
|
377
379
|
if (!raw || typeof raw !== 'object')
|
|
378
380
|
return undefined;
|
|
@@ -402,15 +404,15 @@ function compactDelegationForHeader(raw) {
|
|
|
402
404
|
};
|
|
403
405
|
}
|
|
404
406
|
function headerNeedsSanitization(header) {
|
|
405
|
-
|
|
406
|
-
return Object.keys(value).some((field) => HEADER_OMITTED_FIELD_SET.has(field) || field.startsWith('_'));
|
|
407
|
+
return Object.keys(header).some((field) => !HEADER_FIELD_SET.has(field));
|
|
407
408
|
}
|
|
408
409
|
function sanitizeBucketIndex(index) {
|
|
409
410
|
let changed = false;
|
|
410
411
|
const headers = index.headers.map((header) => {
|
|
412
|
+
const compact = toHeader(header);
|
|
411
413
|
if (headerNeedsSanitization(header))
|
|
412
414
|
changed = true;
|
|
413
|
-
return
|
|
415
|
+
return compact;
|
|
414
416
|
}).sort(newestFirst);
|
|
415
417
|
const activeId = index.activeId && headers.some((header) => header.id === index.activeId) ? index.activeId : null;
|
|
416
418
|
if (activeId !== index.activeId)
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -603,7 +603,7 @@ function stringifyMentorCallArgs(rawArgs) {
|
|
|
603
603
|
}
|
|
604
604
|
function resultTextFromValue(value) {
|
|
605
605
|
if (typeof value === 'string')
|
|
606
|
-
return value.trim()
|
|
606
|
+
return value.trim() ? value : null;
|
|
607
607
|
if (Array.isArray(value)) {
|
|
608
608
|
const parts = value
|
|
609
609
|
.map((entry) => resultTextFromValue(entry))
|
|
@@ -693,6 +693,9 @@ function parseMicroEventsSignal(hostId, line) {
|
|
|
693
693
|
}
|
|
694
694
|
else if (itemType !== 'agent_message' && itemType !== 'error') {
|
|
695
695
|
events.push(microEventFromNonMentorToolCall(item) || { kind: 'tool_call', text: itemType });
|
|
696
|
+
const output = resultTextFromValue(item.result ?? item.output ?? item.aggregated_output);
|
|
697
|
+
if (output)
|
|
698
|
+
events.push({ kind: 'tool_result', text: output });
|
|
696
699
|
}
|
|
697
700
|
}
|
|
698
701
|
if (item && item.type === 'mcp_tool_call' && isFraimTool(readToolName(item), 'seekMentoring')) {
|
|
@@ -724,7 +727,7 @@ function parseMicroEventsSignal(hostId, line) {
|
|
|
724
727
|
}
|
|
725
728
|
else if (root.type === 'tool.execution_complete') {
|
|
726
729
|
const resultText = resultTextFromValue(data.result ?? data.output ?? data.content);
|
|
727
|
-
events.push({ kind: '
|
|
730
|
+
events.push({ kind: 'tool_result', text: resultText || rootType });
|
|
728
731
|
rootTypeHandled = true;
|
|
729
732
|
}
|
|
730
733
|
}
|
|
@@ -6,16 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.AiHubRawEventLogStore = void 0;
|
|
7
7
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
10
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const types_1 = require("./types");
|
|
11
11
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
12
12
|
function defaultRawEventLogRoot() {
|
|
13
|
-
|
|
14
|
-
return node_path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-event-logs');
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return node_path_1.default.join(node_os_1.default.homedir(), '.fraim', 'ai-hub-event-logs');
|
|
18
|
-
}
|
|
13
|
+
return node_path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-event-logs');
|
|
19
14
|
}
|
|
20
15
|
function hashSegment(value, length = 24) {
|
|
21
16
|
return node_crypto_1.default.createHash('sha256').update(value || 'unknown').digest('hex').slice(0, length);
|
|
@@ -23,20 +18,39 @@ function hashSegment(value, length = 24) {
|
|
|
23
18
|
function safeRunFileName(runId) {
|
|
24
19
|
return `${hashSegment(runId, 40)}.jsonl`;
|
|
25
20
|
}
|
|
21
|
+
function parseEntry(line) {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(line);
|
|
24
|
+
if (typeof value.text !== 'string' || !['stdout', 'stderr', 'system'].includes(value.channel))
|
|
25
|
+
return null;
|
|
26
|
+
return { createdAt: typeof value.createdAt === 'string' ? value.createdAt : new Date(0).toISOString(), channel: value.channel, text: value.text,
|
|
27
|
+
...(types_1.AI_HUB_MICRO_EVENT_KINDS.includes(value.hubKind) ? { hubKind: value.hubKind } : {}),
|
|
28
|
+
...(typeof value.hostId === 'string' ? { hostId: value.hostId } : {}),
|
|
29
|
+
...(Array.isArray(value.microEvents) ? { microEvents: value.microEvents.filter((event) => event && types_1.AI_HUB_MICRO_EVENT_KINDS.includes(event.kind) && typeof event.text === 'string') } : {}) };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
26
35
|
class AiHubRawEventLogStore {
|
|
27
36
|
constructor(rootDir = defaultRawEventLogRoot()) {
|
|
28
37
|
this.rootDir = node_path_1.default.resolve(rootDir);
|
|
29
38
|
}
|
|
30
39
|
append(bucketKey, conversationId, runId, entry) {
|
|
31
40
|
const dir = node_path_1.default.join(this.rootDir, hashSegment(bucketKey), hashSegment(conversationId));
|
|
32
|
-
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
33
41
|
const logPath = node_path_1.default.join(dir, safeRunFileName(runId));
|
|
34
42
|
const metaPath = `${logPath}.meta.json`;
|
|
43
|
+
if (!this.resolveReadableLogPath(logPath) || !this.resolveReadableLogPath(metaPath))
|
|
44
|
+
throw new Error('Event archive path is outside its root');
|
|
45
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
35
46
|
const createdAt = entry.createdAt || new Date().toISOString();
|
|
36
47
|
const line = {
|
|
37
48
|
createdAt,
|
|
38
49
|
channel: entry.channel,
|
|
39
50
|
text: entry.text,
|
|
51
|
+
...(entry.hubKind ? { hubKind: entry.hubKind } : {}),
|
|
52
|
+
...(entry.hostId ? { hostId: entry.hostId } : {}),
|
|
53
|
+
...(entry.microEvents ? { microEvents: entry.microEvents } : {}),
|
|
40
54
|
};
|
|
41
55
|
node_fs_1.default.appendFileSync(logPath, `${JSON.stringify(line)}\n`, 'utf8');
|
|
42
56
|
const prior = this.readMeta(metaPath, runId, logPath);
|
|
@@ -48,6 +62,8 @@ class AiHubRawEventLogStore {
|
|
|
48
62
|
eventCount: prior.eventCount + 1,
|
|
49
63
|
createdAt: prior.createdAt || createdAt,
|
|
50
64
|
updatedAt: createdAt,
|
|
65
|
+
ownerPid: process.pid,
|
|
66
|
+
...(entry.hostId || prior.hostId ? { hostId: entry.hostId || prior.hostId } : {}),
|
|
51
67
|
...(prior.truncated ? { truncated: true } : {}),
|
|
52
68
|
};
|
|
53
69
|
node_fs_1.default.writeFileSync(metaPath, `${JSON.stringify(ref, null, 2)}\n`, 'utf8');
|
|
@@ -60,22 +76,90 @@ class AiHubRawEventLogStore {
|
|
|
60
76
|
const lines = node_fs_1.default.readFileSync(logPath, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
61
77
|
const entries = [];
|
|
62
78
|
for (const line of lines) {
|
|
79
|
+
const entry = parseEntry(line);
|
|
80
|
+
if (entry)
|
|
81
|
+
entries.push(entry);
|
|
82
|
+
}
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
markEnded(ref, endedAt) {
|
|
86
|
+
const logPath = this.resolveReadableLogPath(ref.path);
|
|
87
|
+
if (!logPath || !node_fs_1.default.existsSync(logPath))
|
|
88
|
+
return ref;
|
|
89
|
+
if (!this.resolveReadableLogPath(`${logPath}.meta.json`))
|
|
90
|
+
throw new Error('Event archive metadata is outside its root');
|
|
91
|
+
const next = { ...this.readMeta(`${logPath}.meta.json`, ref.runId, logPath), endedAt };
|
|
92
|
+
delete next.ownerPid;
|
|
93
|
+
node_fs_1.default.writeFileSync(`${logPath}.meta.json`, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
/** Read display-ready entries in chronological order without reconstructing host output. */
|
|
97
|
+
async readMicroEvents(refs) {
|
|
98
|
+
const events = [];
|
|
99
|
+
let missingArchives = 0;
|
|
100
|
+
let legacyArchives = 0;
|
|
101
|
+
for (const ref of refs) {
|
|
102
|
+
const logPath = this.resolveReadableLogPath(ref.path);
|
|
103
|
+
if (!logPath) {
|
|
104
|
+
missingArchives++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
let size;
|
|
108
|
+
try {
|
|
109
|
+
size = (await node_fs_1.default.promises.stat(logPath)).size;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === 'ENOENT') {
|
|
113
|
+
missingArchives++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
if (!size)
|
|
119
|
+
continue;
|
|
120
|
+
let hasLegacyEntries = false;
|
|
121
|
+
// Snapshot the size; only newline-terminated records are committed. The
|
|
122
|
+
// stream owns its descriptor and decodes UTF-8 across chunk boundaries.
|
|
123
|
+
const stream = node_fs_1.default.createReadStream(logPath, { encoding: 'utf8', end: size - 1 });
|
|
124
|
+
let pending = '';
|
|
125
|
+
let lineNumber = 0;
|
|
63
126
|
try {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
127
|
+
for await (const chunk of stream) {
|
|
128
|
+
const lines = (pending + chunk).split('\n');
|
|
129
|
+
pending = lines.pop();
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
const entry = parseEntry(line);
|
|
132
|
+
const id = `${ref.runId}:${lineNumber++}`;
|
|
133
|
+
if (!entry)
|
|
134
|
+
continue;
|
|
135
|
+
const displayed = entry.hubKind ? [{ kind: entry.hubKind, text: entry.text }] : entry.microEvents;
|
|
136
|
+
if (!displayed) {
|
|
137
|
+
hasLegacyEntries = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
displayed.forEach((event, index) => events.push({
|
|
141
|
+
id: `${id}:${index}`, kind: event.kind, text: event.text,
|
|
142
|
+
channel: event.kind === 'manager_input' ? 'manager' : entry.channel, createdAt: entry.createdAt,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
72
146
|
}
|
|
73
147
|
}
|
|
74
|
-
catch {
|
|
75
|
-
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code === 'ENOENT') {
|
|
150
|
+
missingArchives++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
76
154
|
}
|
|
155
|
+
finally {
|
|
156
|
+
stream.destroy();
|
|
157
|
+
}
|
|
158
|
+
if (hasLegacyEntries)
|
|
159
|
+
legacyArchives++;
|
|
77
160
|
}
|
|
78
|
-
|
|
161
|
+
events.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
162
|
+
return { events, archiveAvailable: refs.length > missingArchives, missingArchives, legacyArchives };
|
|
79
163
|
}
|
|
80
164
|
readMeta(metaPath, runId, logPath) {
|
|
81
165
|
if (!node_fs_1.default.existsSync(metaPath)) {
|
|
@@ -90,6 +174,9 @@ class AiHubRawEventLogStore {
|
|
|
90
174
|
eventCount: typeof parsed.eventCount === 'number' ? parsed.eventCount : 0,
|
|
91
175
|
createdAt: typeof parsed.createdAt === 'string' ? parsed.createdAt : '',
|
|
92
176
|
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
|
|
177
|
+
...(typeof parsed.endedAt === 'string' ? { endedAt: parsed.endedAt } : {}),
|
|
178
|
+
...(typeof parsed.ownerPid === 'number' ? { ownerPid: parsed.ownerPid } : {}),
|
|
179
|
+
...(typeof parsed.hostId === 'string' ? { hostId: parsed.hostId } : {}),
|
|
93
180
|
...(parsed.truncated ? { truncated: true } : {}),
|
|
94
181
|
};
|
|
95
182
|
}
|
|
@@ -104,7 +191,30 @@ class AiHubRawEventLogStore {
|
|
|
104
191
|
const rootPrefix = `${this.rootDir}${node_path_1.default.sep}`;
|
|
105
192
|
const comparableResolved = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
106
193
|
const comparableRootPrefix = process.platform === 'win32' ? rootPrefix.toLowerCase() : rootPrefix;
|
|
107
|
-
|
|
194
|
+
if (!comparableResolved.startsWith(comparableRootPrefix))
|
|
195
|
+
return null;
|
|
196
|
+
// A symlink inside the archive root must not grant access outside it.
|
|
197
|
+
try {
|
|
198
|
+
const real = node_fs_1.default.realpathSync(resolved);
|
|
199
|
+
const root = node_fs_1.default.realpathSync(this.rootDir) + node_path_1.default.sep;
|
|
200
|
+
return (process.platform === 'win32' ? real.toLowerCase().startsWith(root.toLowerCase()) : real.startsWith(root)) ? real : null;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error.code === 'ENOENT') {
|
|
204
|
+
// Missing leaves still inherit the confinement of their nearest real
|
|
205
|
+
// parent, including Windows junctions inside a hashed bucket directory.
|
|
206
|
+
let parent = node_path_1.default.dirname(resolved);
|
|
207
|
+
while (!node_fs_1.default.existsSync(parent) && parent !== node_path_1.default.dirname(parent))
|
|
208
|
+
parent = node_path_1.default.dirname(parent);
|
|
209
|
+
if (!node_fs_1.default.existsSync(this.rootDir))
|
|
210
|
+
return resolved;
|
|
211
|
+
const realParent = node_fs_1.default.realpathSync(parent);
|
|
212
|
+
const realRoot = node_fs_1.default.realpathSync(this.rootDir);
|
|
213
|
+
const relative = node_path_1.default.relative(realRoot, realParent);
|
|
214
|
+
return relative === '' || (!relative.startsWith('..') && !node_path_1.default.isAbsolute(relative)) ? resolved : null;
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
108
218
|
}
|
|
109
219
|
}
|
|
110
220
|
exports.AiHubRawEventLogStore = AiHubRawEventLogStore;
|