fraim-framework 2.0.185 → 2.0.187
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 +97 -6
- package/dist/src/ai-hub/hosts.js +127 -15
- package/dist/src/ai-hub/openclaw-bridge.js +17 -6
- package/dist/src/ai-hub/server.js +215 -38
- package/dist/src/cli/commands/add-ide.js +3 -2
- package/dist/src/cli/commands/add-provider.js +7 -1
- package/dist/src/cli/commands/sync.js +2 -1
- package/dist/src/cli/mcp/ide-formats.js +36 -1
- package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
- package/dist/src/cli/setup/ide-detector.js +29 -1
- package/dist/src/cli/setup/ide-global-integration.js +1 -1
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +8 -2
- package/dist/src/first-run/types.js +1 -1
- package/package.json +1 -1
- package/public/ai-hub/script.js +484 -28
- package/public/ai-hub/styles.css +30 -0
|
@@ -3,10 +3,27 @@ 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.AiHubConversationStore = void 0;
|
|
6
|
+
exports.AiHubConversationStore = exports.COMPANY_SCOPE_KEY = exports.MANAGER_SCOPE_KEY = void 0;
|
|
7
|
+
exports.conversationScopeKey = conversationScopeKey;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
11
|
+
// Issue #708: reserved, project-independent bucket keys for non-project scopes.
|
|
12
|
+
// They are NOT filesystem paths and must never be passed through path.resolve.
|
|
13
|
+
exports.MANAGER_SCOPE_KEY = '@manager';
|
|
14
|
+
exports.COMPANY_SCOPE_KEY = '@company';
|
|
15
|
+
/**
|
|
16
|
+
* Issue #708: resolve the conversation store bucket key for a given scope.
|
|
17
|
+
* - 'manager'/'company' → a stable sentinel key (project-independent home).
|
|
18
|
+
* - 'project' (or undefined) → the resolved project path (existing behavior).
|
|
19
|
+
*/
|
|
20
|
+
function conversationScopeKey(scope, projectPath) {
|
|
21
|
+
if (scope === 'manager')
|
|
22
|
+
return exports.MANAGER_SCOPE_KEY;
|
|
23
|
+
if (scope === 'company')
|
|
24
|
+
return exports.COMPANY_SCOPE_KEY;
|
|
25
|
+
return normalizeConversationKey(projectPath);
|
|
26
|
+
}
|
|
10
27
|
const emptyProjectState = () => ({
|
|
11
28
|
activeId: null,
|
|
12
29
|
conversations: [],
|
|
@@ -23,8 +40,58 @@ function timestampValue(value) {
|
|
|
23
40
|
}
|
|
24
41
|
return 0;
|
|
25
42
|
}
|
|
43
|
+
// Issue #708: sentinel scope keys (e.g. '@manager') are stable bucket keys, not paths —
|
|
44
|
+
// pass them through untouched; resolve everything else as a real project path.
|
|
45
|
+
function normalizeConversationKey(key) {
|
|
46
|
+
if (typeof key === 'string' && (key === exports.MANAGER_SCOPE_KEY || key === exports.COMPANY_SCOPE_KEY)) {
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
return path_1.default.resolve(key || process.cwd());
|
|
50
|
+
}
|
|
51
|
+
// Retained name for project-path stamping; now sentinel-aware so manager/company
|
|
52
|
+
// records keep their scope key rather than being resolved into cwd/@manager.
|
|
26
53
|
function normalizeProjectPath(projectPath) {
|
|
27
|
-
return
|
|
54
|
+
return normalizeConversationKey(projectPath);
|
|
55
|
+
}
|
|
56
|
+
// Issue #708: one-time, idempotent migration. Legacy manager/company-invoked runs were
|
|
57
|
+
// stored inside project buckets and distinguished only by the client-side `invokedArea`
|
|
58
|
+
// field. Fold them into the reserved scope buckets (stamping `scope`) so they have a
|
|
59
|
+
// stable, project-independent home. Runs already in a scope bucket are left untouched.
|
|
60
|
+
function migrateScopeBuckets(store) {
|
|
61
|
+
const projects = {};
|
|
62
|
+
const ensure = (key) => {
|
|
63
|
+
if (!projects[key])
|
|
64
|
+
projects[key] = { activeId: null, conversations: [] };
|
|
65
|
+
return projects[key];
|
|
66
|
+
};
|
|
67
|
+
// Seed existing scope buckets first so their activeId is preserved.
|
|
68
|
+
for (const [key, state] of Object.entries(store.projects || {})) {
|
|
69
|
+
if (key === exports.MANAGER_SCOPE_KEY || key === exports.COMPANY_SCOPE_KEY) {
|
|
70
|
+
ensure(key).conversations.push(...(state.conversations || []));
|
|
71
|
+
projects[key].activeId = state.activeId ?? null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const [key, state] of Object.entries(store.projects || {})) {
|
|
75
|
+
if (key === exports.MANAGER_SCOPE_KEY || key === exports.COMPANY_SCOPE_KEY)
|
|
76
|
+
continue;
|
|
77
|
+
const stay = [];
|
|
78
|
+
for (const conv of (state.conversations || [])) {
|
|
79
|
+
const scope = conv.scope
|
|
80
|
+
?? conv.invokedArea;
|
|
81
|
+
if (scope === 'manager' || scope === 'company') {
|
|
82
|
+
const bucketKey = scope === 'manager' ? exports.MANAGER_SCOPE_KEY : exports.COMPANY_SCOPE_KEY;
|
|
83
|
+
const bucket = ensure(bucketKey);
|
|
84
|
+
if (!bucket.conversations.some((c) => c && c.id === conv.id)) {
|
|
85
|
+
bucket.conversations.push({ ...conv, scope: scope });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
stay.push(conv);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
projects[key] = { activeId: state.activeId ?? null, conversations: stay };
|
|
93
|
+
}
|
|
94
|
+
return { version: 1, projects };
|
|
28
95
|
}
|
|
29
96
|
function normalizeConversation(projectPath, raw) {
|
|
30
97
|
if (!raw || typeof raw !== 'object')
|
|
@@ -77,6 +144,32 @@ class AiHubConversationStore {
|
|
|
77
144
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
78
145
|
return normalizeProjectState(normalizedProjectPath, state.projects[normalizedProjectPath]);
|
|
79
146
|
}
|
|
147
|
+
// Issue #719: delete a project's KEY from the store entirely. Writing an empty
|
|
148
|
+
// conversation list is not enough — knownProjects re-derives a project from
|
|
149
|
+
// every stored key (listProjectPaths), so removal must drop the key itself.
|
|
150
|
+
// Sentinel scope buckets (@manager/@company) are not projects and never removable.
|
|
151
|
+
removeProject(projectPath) {
|
|
152
|
+
if (projectPath === exports.MANAGER_SCOPE_KEY || projectPath === exports.COMPANY_SCOPE_KEY)
|
|
153
|
+
return false;
|
|
154
|
+
const canonicalKey = (value) => {
|
|
155
|
+
const resolved = path_1.default.resolve(value);
|
|
156
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
157
|
+
};
|
|
158
|
+
const target = canonicalKey(projectPath);
|
|
159
|
+
const state = this.readStore();
|
|
160
|
+
let removed = false;
|
|
161
|
+
for (const key of Object.keys(state.projects)) {
|
|
162
|
+
if (key === exports.MANAGER_SCOPE_KEY || key === exports.COMPANY_SCOPE_KEY)
|
|
163
|
+
continue;
|
|
164
|
+
if (canonicalKey(key) === target) {
|
|
165
|
+
delete state.projects[key];
|
|
166
|
+
removed = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (removed)
|
|
170
|
+
this.writeStore(state);
|
|
171
|
+
return removed;
|
|
172
|
+
}
|
|
80
173
|
replaceProject(projectPath, next) {
|
|
81
174
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
82
175
|
const normalized = normalizeProjectState(normalizedProjectPath, next);
|
|
@@ -149,10 +242,8 @@ class AiHubConversationStore {
|
|
|
149
242
|
if (raw.version !== 1 || !raw.projects || typeof raw.projects !== 'object') {
|
|
150
243
|
return { version: 1, projects: {} };
|
|
151
244
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
projects: raw.projects,
|
|
155
|
-
};
|
|
245
|
+
// Issue #708: fold legacy manager/company-invoked runs into scope buckets on read.
|
|
246
|
+
return migrateScopeBuckets({ version: 1, projects: raw.projects });
|
|
156
247
|
}
|
|
157
248
|
catch {
|
|
158
249
|
return { version: 1, projects: {} };
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createHubEvent = exports.createHubMessage = exports.ScriptedHostRuntime = exports.FakeHostRuntime = exports.CliHostRuntime = void 0;
|
|
7
7
|
exports.parseSeekMentoringSignal = parseSeekMentoringSignal;
|
|
8
|
+
exports.parseFraimJobLoadSignal = parseFraimJobLoadSignal;
|
|
8
9
|
exports.parseUsageSignal = parseUsageSignal;
|
|
9
10
|
exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
|
|
10
11
|
exports.detectEmployees = detectEmployees;
|
|
@@ -91,6 +92,58 @@ function parseSeekMentoringSignal(line) {
|
|
|
91
92
|
}
|
|
92
93
|
return null;
|
|
93
94
|
}
|
|
95
|
+
// Issue #710: extract the job name from a get_fraim_job tool call. This
|
|
96
|
+
// is structured known-job evidence; the server still validates the job id
|
|
97
|
+
// against the catalog before promoting any run.
|
|
98
|
+
function parseFraimJobLoadSignal(line) {
|
|
99
|
+
if (!line.includes('get_fraim_job'))
|
|
100
|
+
return null;
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(line);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
109
|
+
return null;
|
|
110
|
+
const obj = parsed;
|
|
111
|
+
// Codex shape: { type: 'item.started' | 'item.completed', item:
|
|
112
|
+
// { type: 'mcp_tool_call', tool: 'get_fraim_job', arguments: { job } } }.
|
|
113
|
+
if ((obj.type === 'item.started' || obj.type === 'item.completed') &&
|
|
114
|
+
typeof obj.item === 'object' && obj.item !== null) {
|
|
115
|
+
const item = obj.item;
|
|
116
|
+
const tool = typeof item.tool === 'string' ? item.tool : '';
|
|
117
|
+
if (item.type === 'mcp_tool_call' && isGetFraimJobTool(tool)) {
|
|
118
|
+
const sig = readFraimJobFromArgs(item.arguments);
|
|
119
|
+
if (sig)
|
|
120
|
+
return sig;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Claude Code shape: tool_use blocks live inside message.content with
|
|
124
|
+
// an MCP-prefixed name such as mcp__fraim__get_fraim_job.
|
|
125
|
+
const candidates = [obj];
|
|
126
|
+
if (Array.isArray(obj.content))
|
|
127
|
+
candidates.push(...obj.content);
|
|
128
|
+
if (typeof obj.message === 'object' && obj.message !== null) {
|
|
129
|
+
const msg = obj.message;
|
|
130
|
+
if (Array.isArray(msg.content))
|
|
131
|
+
candidates.push(...msg.content);
|
|
132
|
+
}
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
135
|
+
continue;
|
|
136
|
+
const c = candidate;
|
|
137
|
+
const isToolUse = c.type === 'tool_use' || c.type === 'function_call';
|
|
138
|
+
const nameField = typeof c.name === 'string' ? c.name : (typeof c.tool_name === 'string' ? c.tool_name : '');
|
|
139
|
+
if (!isToolUse || !isGetFraimJobTool(nameField))
|
|
140
|
+
continue;
|
|
141
|
+
const sig = readFraimJobFromArgs(c.input || c.arguments || c.parameters);
|
|
142
|
+
if (sig)
|
|
143
|
+
return sig;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
94
147
|
// Issue #347 — extract per-turn usage from the host's JSON stream.
|
|
95
148
|
// Codex: `{"type":"turn.completed","usage":{input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens}}`.
|
|
96
149
|
// Claude Code: `{"type":"result", ..., "usage":{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}, "total_cost_usd": ...}`.
|
|
@@ -256,6 +309,41 @@ function readAgentFromArgs(args) {
|
|
|
256
309
|
return null;
|
|
257
310
|
return { agentName, agentModel };
|
|
258
311
|
}
|
|
312
|
+
function isGetFraimJobTool(toolName) {
|
|
313
|
+
return toolName === 'get_fraim_job' ||
|
|
314
|
+
toolName === 'mcp__fraim__get_fraim_job' ||
|
|
315
|
+
toolName.endsWith('get_fraim_job');
|
|
316
|
+
}
|
|
317
|
+
function readFraimJobFromArgs(rawArgs) {
|
|
318
|
+
const args = normalizeToolArgs(rawArgs);
|
|
319
|
+
if (!args)
|
|
320
|
+
return null;
|
|
321
|
+
const rawJob = stringValue(args.job) ||
|
|
322
|
+
stringValue(args.jobName) ||
|
|
323
|
+
stringValue(args.job_id) ||
|
|
324
|
+
stringValue(args.name);
|
|
325
|
+
const jobId = rawJob.trim().toLowerCase();
|
|
326
|
+
if (!jobId)
|
|
327
|
+
return null;
|
|
328
|
+
return { jobId, source: 'get_fraim_job' };
|
|
329
|
+
}
|
|
330
|
+
function normalizeToolArgs(rawArgs) {
|
|
331
|
+
if (rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)) {
|
|
332
|
+
return rawArgs;
|
|
333
|
+
}
|
|
334
|
+
if (typeof rawArgs !== 'string' || rawArgs.trim().length === 0)
|
|
335
|
+
return null;
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(rawArgs);
|
|
338
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
339
|
+
return parsed;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
259
347
|
function numberOrNull(v) {
|
|
260
348
|
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
261
349
|
}
|
|
@@ -987,17 +1075,24 @@ function parseHostLine(hostId, line) {
|
|
|
987
1075
|
const trimmed = line.trim();
|
|
988
1076
|
if (!trimmed)
|
|
989
1077
|
return {};
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
1078
|
+
// Scan every line for structured signals the Hub UI cares about:
|
|
1079
|
+
// seekMentoring (tracker), get_fraim_job (job identity promotion),
|
|
1080
|
+
// turn-level usage (totals), and fraim_connect agent identity
|
|
1081
|
+
// (cost lookup when the host doesn't emit it).
|
|
994
1082
|
const seekMentoring = parseSeekMentoringSignal(trimmed);
|
|
1083
|
+
const fraimJob = parseFraimJobLoadSignal(trimmed);
|
|
995
1084
|
const usage = parseUsageSignal(trimmed);
|
|
996
1085
|
const agentIdentity = parseAgentIdentitySignal(trimmed);
|
|
997
1086
|
const withSignal = (event) => {
|
|
998
|
-
if (!seekMentoring && !usage && !agentIdentity)
|
|
1087
|
+
if (!seekMentoring && !fraimJob && !usage && !agentIdentity)
|
|
999
1088
|
return event;
|
|
1000
|
-
return {
|
|
1089
|
+
return {
|
|
1090
|
+
...event,
|
|
1091
|
+
...(seekMentoring ? { seekMentoring } : {}),
|
|
1092
|
+
...(fraimJob ? { fraimJob } : {}),
|
|
1093
|
+
...(usage ? { usage } : {}),
|
|
1094
|
+
...(agentIdentity ? { agentIdentity } : {}),
|
|
1095
|
+
};
|
|
1001
1096
|
};
|
|
1002
1097
|
if (hostId === 'codex') {
|
|
1003
1098
|
try {
|
|
@@ -1339,9 +1434,10 @@ class ScriptedHostRuntime {
|
|
|
1339
1434
|
{ id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Scripted test double.', supportsRaw: true },
|
|
1340
1435
|
{ id: 'copilot', label: 'GitHub Copilot CLI', available: true, detail: 'Scripted test double.', supportsRaw: true },
|
|
1341
1436
|
];
|
|
1342
|
-
// Track each active run so the test can emit signals at it.
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1437
|
+
// Track each active run so the test can emit signals at it. The Hub
|
|
1438
|
+
// passes the run id as the requested start session id in test/demo paths;
|
|
1439
|
+
// real resume flows still use the host session id. Resolving by the
|
|
1440
|
+
// supplied id first lets concurrent scripted runs receive targeted events.
|
|
1345
1441
|
this.handlersBySession = new Map();
|
|
1346
1442
|
// Tracks runDiscriminant per sessionId so consecutive emitPhase calls
|
|
1347
1443
|
// can resolve onSuccess routing without each test having to repeat it.
|
|
@@ -1352,8 +1448,8 @@ class ScriptedHostRuntime {
|
|
|
1352
1448
|
}
|
|
1353
1449
|
startRun(_hostId, _projectPath, _message, handlers, requestedSessionId) {
|
|
1354
1450
|
const sessionId = requestedSessionId || (0, crypto_1.randomUUID)();
|
|
1355
|
-
handlers.onEvent({ sessionId, raw: 'scripted-session-start' }, 'system');
|
|
1356
1451
|
this.handlersBySession.set(sessionId, handlers);
|
|
1452
|
+
handlers.onEvent({ sessionId, raw: 'scripted-session-start' }, 'system');
|
|
1357
1453
|
return this.spawnDouble();
|
|
1358
1454
|
}
|
|
1359
1455
|
continueRun(_hostId, _projectPath, sessionId, _message, handlers) {
|
|
@@ -1363,6 +1459,7 @@ class ScriptedHostRuntime {
|
|
|
1363
1459
|
}
|
|
1364
1460
|
startDirectRun(_hostId, _message, _projectPath, handlers, requestedSessionId) {
|
|
1365
1461
|
const sessionId = requestedSessionId || (0, crypto_1.randomUUID)();
|
|
1462
|
+
this.handlersBySession.set(sessionId, handlers);
|
|
1366
1463
|
handlers.onEvent({ sessionId, raw: 'scripted-direct-session-start' }, 'system');
|
|
1367
1464
|
return this.spawnDouble();
|
|
1368
1465
|
}
|
|
@@ -1420,6 +1517,18 @@ class ScriptedHostRuntime {
|
|
|
1420
1517
|
},
|
|
1421
1518
|
}, 'stdout');
|
|
1422
1519
|
}
|
|
1520
|
+
// Test API — emit the structured job-load signal that get_fraim_job
|
|
1521
|
+
// produces before the first seekMentoring phase call.
|
|
1522
|
+
emitFraimJobLoad(runId, jobId) {
|
|
1523
|
+
const target = this.resolveSession(runId);
|
|
1524
|
+
if (!target)
|
|
1525
|
+
return;
|
|
1526
|
+
target.handlers.onEvent({
|
|
1527
|
+
sessionId: target.sessionId,
|
|
1528
|
+
raw: `scripted-get_fraim_job:${jobId}`,
|
|
1529
|
+
fraimJob: { jobId, source: 'get_fraim_job' },
|
|
1530
|
+
}, 'stdout');
|
|
1531
|
+
}
|
|
1423
1532
|
// Test API — emit a per-turn usage signal in the normalized shape
|
|
1424
1533
|
// the server expects. Mirrors what parseUsageSignal would produce
|
|
1425
1534
|
// from a real host's stream.
|
|
@@ -1464,13 +1573,16 @@ class ScriptedHostRuntime {
|
|
|
1464
1573
|
this.handlersBySession.clear();
|
|
1465
1574
|
this.discriminantBySession.clear();
|
|
1466
1575
|
}
|
|
1467
|
-
// The Hub
|
|
1468
|
-
//
|
|
1469
|
-
//
|
|
1470
|
-
//
|
|
1471
|
-
resolveSession(
|
|
1576
|
+
// The Hub passes run.id as the requested session id for scripted starts,
|
|
1577
|
+
// so tests/demo controls can target a specific run even when multiple runs
|
|
1578
|
+
// are active. Fall back to the latest handler for older callers that do not
|
|
1579
|
+
// provide a known id.
|
|
1580
|
+
resolveSession(runId) {
|
|
1472
1581
|
if (this.handlersBySession.size === 0)
|
|
1473
1582
|
return null;
|
|
1583
|
+
const direct = this.handlersBySession.get(runId);
|
|
1584
|
+
if (direct)
|
|
1585
|
+
return { sessionId: runId, handlers: direct };
|
|
1474
1586
|
const entries = [...this.handlersBySession.entries()];
|
|
1475
1587
|
const [sessionId, handlers] = entries[entries.length - 1];
|
|
1476
1588
|
return { sessionId, handlers };
|
|
@@ -134,6 +134,7 @@ class OpenClawAiHubBridge {
|
|
|
134
134
|
jobId: run.jobId,
|
|
135
135
|
projectPath,
|
|
136
136
|
lastSeenAt: new Date().toISOString(),
|
|
137
|
+
managerTexts: [inbound.text],
|
|
137
138
|
};
|
|
138
139
|
this.threads.set(key, state);
|
|
139
140
|
this.persistThreadState();
|
|
@@ -147,6 +148,7 @@ class OpenClawAiHubBridge {
|
|
|
147
148
|
}
|
|
148
149
|
run = continued.data;
|
|
149
150
|
state.lastSeenAt = new Date().toISOString();
|
|
151
|
+
state.managerTexts = [...(state.managerTexts ?? []), inbound.text];
|
|
150
152
|
this.persistThreadState();
|
|
151
153
|
}
|
|
152
154
|
const settled = await this.waitForSettlement(run.id);
|
|
@@ -193,6 +195,19 @@ class OpenClawAiHubBridge {
|
|
|
193
195
|
return this.fetchRun(runId);
|
|
194
196
|
}
|
|
195
197
|
presentThread(state, run) {
|
|
198
|
+
// The Hub records the invocation it sent as each manager bubble (#696). For a channel
|
|
199
|
+
// transcript we substitute the user's own words (state.managerTexts, in turn order)
|
|
200
|
+
// for manager-role messages, so the thread reflects what the user actually typed.
|
|
201
|
+
const managerTexts = state.managerTexts ?? [];
|
|
202
|
+
let managerIdx = 0;
|
|
203
|
+
const transcript = run.messages.map((message) => ({
|
|
204
|
+
role: message.role,
|
|
205
|
+
text: message.role === 'manager' && managerIdx < managerTexts.length
|
|
206
|
+
? managerTexts[managerIdx++]
|
|
207
|
+
: message.text,
|
|
208
|
+
createdAt: message.createdAt,
|
|
209
|
+
}));
|
|
210
|
+
const latestManagerMessage = [...transcript].reverse().find((m) => m.role === 'manager')?.text || null;
|
|
196
211
|
return {
|
|
197
212
|
channelId: state.channelId,
|
|
198
213
|
threadId: state.threadId,
|
|
@@ -204,13 +219,9 @@ class OpenClawAiHubBridge {
|
|
|
204
219
|
currentPhase: run.currentPhase || null,
|
|
205
220
|
stages: run.stages || [],
|
|
206
221
|
totals: run.totals || null,
|
|
207
|
-
latestManagerMessage
|
|
222
|
+
latestManagerMessage,
|
|
208
223
|
latestEmployeeMessage: lastMessage(run.messages, 'employee')?.text || null,
|
|
209
|
-
transcript
|
|
210
|
-
role: message.role,
|
|
211
|
-
text: message.text,
|
|
212
|
-
createdAt: message.createdAt,
|
|
213
|
-
})),
|
|
224
|
+
transcript,
|
|
214
225
|
};
|
|
215
226
|
}
|
|
216
227
|
threadKey(channelId, threadId) {
|