bopodev-api 0.1.12 → 0.1.13
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/package.json +6 -4
- package/src/app.ts +2 -0
- package/src/lib/instance-paths.ts +12 -0
- package/src/lib/opencode-model.ts +35 -0
- package/src/lib/workspace-policy.ts +5 -0
- package/src/realtime/heartbeat-runs.ts +78 -0
- package/src/realtime/hub.ts +37 -1
- package/src/realtime/office-space.ts +10 -1
- package/src/routes/agents.ts +89 -2
- package/src/routes/companies.ts +2 -0
- package/src/routes/governance.ts +9 -2
- package/src/routes/heartbeats.ts +2 -1
- package/src/routes/issues.ts +321 -0
- package/src/routes/observability.ts +546 -18
- package/src/routes/plugins.ts +257 -0
- package/src/scripts/onboard-seed.ts +57 -12
- package/src/server.ts +62 -3
- package/src/services/governance-service.ts +97 -23
- package/src/services/heartbeat-service.ts +633 -31
- package/src/services/memory-file-service.ts +249 -0
- package/src/services/plugin-manifest-loader.ts +65 -0
- package/src/services/plugin-runtime.ts +580 -0
- package/src/services/plugin-webhook-executor.ts +94 -0
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getHeartbeatRun,
|
|
4
|
+
listAgents,
|
|
5
|
+
listAuditEvents,
|
|
6
|
+
listCostEntries,
|
|
7
|
+
listHeartbeatRunMessages,
|
|
8
|
+
listHeartbeatRuns,
|
|
9
|
+
listPluginRuns
|
|
10
|
+
} from "bopodev-db";
|
|
3
11
|
import type { AppContext } from "../context";
|
|
4
|
-
import { sendOk } from "../http";
|
|
12
|
+
import { sendError, sendOk } from "../http";
|
|
5
13
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
14
|
+
import { listAgentMemoryFiles, readAgentMemoryFile } from "../services/memory-file-service";
|
|
6
15
|
|
|
7
16
|
export function createObservabilityRouter(ctx: AppContext) {
|
|
8
17
|
const router = Router();
|
|
@@ -32,27 +41,189 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
32
41
|
|
|
33
42
|
router.get("/heartbeats", async (req, res) => {
|
|
34
43
|
const companyId = req.companyId!;
|
|
44
|
+
const rawLimit = Number(req.query.limit ?? 100);
|
|
45
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), 500) : 100;
|
|
46
|
+
const statusFilter = typeof req.query.status === "string" && req.query.status.trim().length > 0 ? req.query.status.trim() : null;
|
|
47
|
+
const agentFilter = typeof req.query.agentId === "string" && req.query.agentId.trim().length > 0 ? req.query.agentId.trim() : null;
|
|
35
48
|
const [runs, auditRows] = await Promise.all([
|
|
36
|
-
listHeartbeatRuns(ctx.db, companyId),
|
|
49
|
+
listHeartbeatRuns(ctx.db, companyId, limit),
|
|
37
50
|
listAuditEvents(ctx.db, companyId)
|
|
38
51
|
]);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
const runDetailsByRunId = buildRunDetailsMap(auditRows);
|
|
53
|
+
return sendOk(
|
|
54
|
+
res,
|
|
55
|
+
runs
|
|
56
|
+
.filter((run) => (statusFilter ? run.status === statusFilter : true))
|
|
57
|
+
.filter((run) => (agentFilter ? run.agentId === agentFilter : true))
|
|
58
|
+
.map((run) => {
|
|
59
|
+
const details = runDetailsByRunId.get(run.id);
|
|
60
|
+
const outcome = details?.outcome ?? null;
|
|
61
|
+
return {
|
|
62
|
+
...serializeRunRow(run, outcome),
|
|
63
|
+
outcome
|
|
64
|
+
};
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
router.get("/heartbeats/:runId", async (req, res) => {
|
|
70
|
+
const companyId = req.companyId!;
|
|
71
|
+
const runId = req.params.runId;
|
|
72
|
+
const [run, auditRows, transcriptResult] = await Promise.all([
|
|
73
|
+
getHeartbeatRun(ctx.db, companyId, runId),
|
|
74
|
+
listAuditEvents(ctx.db, companyId, 500),
|
|
75
|
+
listHeartbeatRunMessages(ctx.db, { companyId, runId, limit: 20 })
|
|
76
|
+
]);
|
|
77
|
+
if (!run) {
|
|
78
|
+
return sendError(res, "Run not found", 404);
|
|
79
|
+
}
|
|
80
|
+
const runDetailsByRunId = buildRunDetailsMap(auditRows);
|
|
81
|
+
const details = runDetailsByRunId.get(runId) ?? null;
|
|
82
|
+
const trace = toRecord(details?.trace);
|
|
83
|
+
const traceTranscript = Array.isArray(trace?.transcript) ? trace.transcript : [];
|
|
84
|
+
return sendOk(res, {
|
|
85
|
+
run: serializeRunRow(run, details?.outcome ?? null),
|
|
86
|
+
details,
|
|
87
|
+
transcript: {
|
|
88
|
+
hasPersistedMessages: transcriptResult.items.length > 0,
|
|
89
|
+
fallbackFromTrace: transcriptResult.items.length === 0 && traceTranscript.length > 0,
|
|
90
|
+
truncated: traceTranscript.length >= 120
|
|
49
91
|
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
router.get("/heartbeats/:runId/messages", async (req, res) => {
|
|
96
|
+
const companyId = req.companyId!;
|
|
97
|
+
const runId = req.params.runId;
|
|
98
|
+
const rawLimit = Number(req.query.limit ?? 200);
|
|
99
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), 500) : 200;
|
|
100
|
+
const afterRaw = typeof req.query.cursor === "string" ? Number(req.query.cursor) : NaN;
|
|
101
|
+
const afterSequence = Number.isFinite(afterRaw) ? Math.floor(afterRaw) : undefined;
|
|
102
|
+
const signalOnly = req.query.signalOnly !== "false";
|
|
103
|
+
const requestedKinds =
|
|
104
|
+
typeof req.query.kinds === "string"
|
|
105
|
+
? req.query.kinds
|
|
106
|
+
.split(",")
|
|
107
|
+
.map((value) => value.trim())
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
: [];
|
|
110
|
+
const allowedKinds = new Set(["system", "assistant", "thinking", "tool_call", "tool_result", "result", "stderr"]);
|
|
111
|
+
const kindFilter = requestedKinds.filter((kind) => allowedKinds.has(kind));
|
|
112
|
+
const [run, result] = await Promise.all([
|
|
113
|
+
getHeartbeatRun(ctx.db, companyId, runId),
|
|
114
|
+
listHeartbeatRunMessages(ctx.db, { companyId, runId, limit, afterSequence })
|
|
115
|
+
]);
|
|
116
|
+
if (!run) {
|
|
117
|
+
return sendError(res, "Run not found", 404);
|
|
50
118
|
}
|
|
119
|
+
const filteredItems = result.items
|
|
120
|
+
.filter((message) => (kindFilter.length > 0 ? kindFilter.includes(message.kind) : true))
|
|
121
|
+
.filter((message) => {
|
|
122
|
+
if (!signalOnly) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (message.kind === "tool_call" || message.kind === "tool_result" || message.kind === "result") {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return message.signalLevel === "high" || message.signalLevel === "medium";
|
|
129
|
+
});
|
|
130
|
+
const derivedItems = deriveRelevantMessagesFromRawTranscript(result.items, run, kindFilter, signalOnly);
|
|
131
|
+
const responseItems = [...filteredItems, ...derivedItems]
|
|
132
|
+
.sort((a, b) => a.sequence - b.sequence)
|
|
133
|
+
.filter((message, index, array) => {
|
|
134
|
+
const previous = array[index - 1];
|
|
135
|
+
if (!previous) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return !(
|
|
139
|
+
previous.kind === message.kind &&
|
|
140
|
+
previous.text === message.text &&
|
|
141
|
+
previous.label === message.label &&
|
|
142
|
+
previous.sequence === message.sequence
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
return sendOk(res, {
|
|
146
|
+
runId,
|
|
147
|
+
items: responseItems.map((message) => ({
|
|
148
|
+
id: message.id,
|
|
149
|
+
companyId: message.companyId,
|
|
150
|
+
runId: message.runId,
|
|
151
|
+
sequence: message.sequence,
|
|
152
|
+
kind: message.kind,
|
|
153
|
+
label: message.label,
|
|
154
|
+
text: message.text,
|
|
155
|
+
payload: message.payloadJson,
|
|
156
|
+
signalLevel: message.signalLevel ?? undefined,
|
|
157
|
+
groupKey: message.groupKey,
|
|
158
|
+
source: message.source ?? undefined,
|
|
159
|
+
createdAt: message.createdAt.toISOString()
|
|
160
|
+
})),
|
|
161
|
+
nextCursor: result.nextCursor
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
router.get("/memory", async (req, res) => {
|
|
166
|
+
const companyId = req.companyId!;
|
|
167
|
+
const agentIdFilter = typeof req.query.agentId === "string" && req.query.agentId.trim() ? req.query.agentId.trim() : null;
|
|
168
|
+
const rawLimit = Number(req.query.limit ?? 100);
|
|
169
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), 500) : 100;
|
|
170
|
+
const agents = await listAgents(ctx.db, companyId);
|
|
171
|
+
const targetAgents = agentIdFilter ? agents.filter((agent) => agent.id === agentIdFilter) : agents;
|
|
172
|
+
const fileRows = await Promise.all(
|
|
173
|
+
targetAgents.map(async (agent) => ({
|
|
174
|
+
agentId: agent.id,
|
|
175
|
+
files: await listAgentMemoryFiles({
|
|
176
|
+
companyId,
|
|
177
|
+
agentId: agent.id,
|
|
178
|
+
maxFiles: limit
|
|
179
|
+
})
|
|
180
|
+
}))
|
|
181
|
+
);
|
|
182
|
+
const flattened = fileRows
|
|
183
|
+
.flatMap((row) =>
|
|
184
|
+
row.files.map((file) => ({
|
|
185
|
+
agentId: row.agentId,
|
|
186
|
+
relativePath: file.relativePath,
|
|
187
|
+
path: file.path
|
|
188
|
+
}))
|
|
189
|
+
)
|
|
190
|
+
.slice(0, limit);
|
|
191
|
+
return sendOk(res, {
|
|
192
|
+
items: flattened
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
router.get("/memory/:agentId/file", async (req, res) => {
|
|
197
|
+
const companyId = req.companyId!;
|
|
198
|
+
const agentId = req.params.agentId;
|
|
199
|
+
const relativePath = typeof req.query.path === "string" ? req.query.path.trim() : "";
|
|
200
|
+
if (!relativePath) {
|
|
201
|
+
return sendError(res, "Query parameter 'path' is required.", 422);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const file = await readAgentMemoryFile({
|
|
205
|
+
companyId,
|
|
206
|
+
agentId,
|
|
207
|
+
relativePath
|
|
208
|
+
});
|
|
209
|
+
return sendOk(res, file);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return sendError(res, String(error), 422);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
router.get("/plugins/runs", async (req, res) => {
|
|
216
|
+
const companyId = req.companyId!;
|
|
217
|
+
const pluginId = typeof req.query.pluginId === "string" && req.query.pluginId.trim() ? req.query.pluginId.trim() : undefined;
|
|
218
|
+
const runId = typeof req.query.runId === "string" && req.query.runId.trim() ? req.query.runId.trim() : undefined;
|
|
219
|
+
const rawLimit = Number(req.query.limit ?? 200);
|
|
220
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), 1000) : 200;
|
|
221
|
+
const rows = await listPluginRuns(ctx.db, { companyId, pluginId, runId, limit });
|
|
51
222
|
return sendOk(
|
|
52
223
|
res,
|
|
53
|
-
|
|
54
|
-
...
|
|
55
|
-
|
|
224
|
+
rows.map((row) => ({
|
|
225
|
+
...row,
|
|
226
|
+
diagnostics: parsePayload(row.diagnosticsJson)
|
|
56
227
|
}))
|
|
57
228
|
);
|
|
58
229
|
});
|
|
@@ -60,11 +231,368 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
60
231
|
return router;
|
|
61
232
|
}
|
|
62
233
|
|
|
63
|
-
function parsePayload(payloadJson: string) {
|
|
234
|
+
function parsePayload(payloadJson: string): Record<string, unknown> {
|
|
64
235
|
try {
|
|
65
236
|
const parsed = JSON.parse(payloadJson) as unknown;
|
|
66
|
-
return
|
|
237
|
+
return toRecord(parsed) ?? {};
|
|
67
238
|
} catch {
|
|
68
239
|
return {};
|
|
69
240
|
}
|
|
70
241
|
}
|
|
242
|
+
|
|
243
|
+
function toRecord(value: unknown) {
|
|
244
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function serializeRunRow(
|
|
248
|
+
run: {
|
|
249
|
+
id: string;
|
|
250
|
+
companyId: string;
|
|
251
|
+
agentId: string;
|
|
252
|
+
status: string;
|
|
253
|
+
startedAt: Date;
|
|
254
|
+
finishedAt: Date | null;
|
|
255
|
+
message: string | null;
|
|
256
|
+
},
|
|
257
|
+
outcome: unknown
|
|
258
|
+
) {
|
|
259
|
+
const runType = resolveRunType(run, outcome);
|
|
260
|
+
return {
|
|
261
|
+
id: run.id,
|
|
262
|
+
companyId: run.companyId,
|
|
263
|
+
agentId: run.agentId,
|
|
264
|
+
status: run.status,
|
|
265
|
+
startedAt: run.startedAt.toISOString(),
|
|
266
|
+
finishedAt: run.finishedAt?.toISOString() ?? null,
|
|
267
|
+
message: run.message ?? null,
|
|
268
|
+
runType
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function resolveRunType(
|
|
273
|
+
run: {
|
|
274
|
+
status: string;
|
|
275
|
+
message: string | null;
|
|
276
|
+
},
|
|
277
|
+
outcome: unknown
|
|
278
|
+
): "work" | "no_assigned_work" | "budget_skip" | "overlap_skip" | "other_skip" | "failed" | "running" {
|
|
279
|
+
if (run.status === "started") {
|
|
280
|
+
return "running";
|
|
281
|
+
}
|
|
282
|
+
if (run.status === "failed") {
|
|
283
|
+
return "failed";
|
|
284
|
+
}
|
|
285
|
+
const normalizedMessage = (run.message ?? "").toLowerCase();
|
|
286
|
+
if (normalizedMessage.includes("already in progress")) {
|
|
287
|
+
return "overlap_skip";
|
|
288
|
+
}
|
|
289
|
+
if (normalizedMessage.includes("budget hard-stop")) {
|
|
290
|
+
return "budget_skip";
|
|
291
|
+
}
|
|
292
|
+
if (isNoAssignedWorkMessage(run.message)) {
|
|
293
|
+
return "no_assigned_work";
|
|
294
|
+
}
|
|
295
|
+
if (isNoAssignedWorkOutcome(outcome)) {
|
|
296
|
+
return "no_assigned_work";
|
|
297
|
+
}
|
|
298
|
+
if (run.status === "skipped") {
|
|
299
|
+
return "other_skip";
|
|
300
|
+
}
|
|
301
|
+
return "work";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isNoAssignedWorkMessage(message: string | null) {
|
|
305
|
+
return /\bno assigned work found\b/i.test(message ?? "");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isNoAssignedWorkOutcome(outcome: unknown) {
|
|
309
|
+
const record = toRecord(outcome);
|
|
310
|
+
if (!record) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if (record.kind !== "skipped") {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
const issueIdsTouched = Array.isArray(record.issueIdsTouched)
|
|
317
|
+
? record.issueIdsTouched.filter((value) => typeof value === "string")
|
|
318
|
+
: [];
|
|
319
|
+
if (issueIdsTouched.length === 0) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
const actions = Array.isArray(record.actions)
|
|
323
|
+
? record.actions.filter((value) => typeof value === "object" && value !== null)
|
|
324
|
+
: [];
|
|
325
|
+
return actions.some((action) => {
|
|
326
|
+
const parsed = action as Record<string, unknown>;
|
|
327
|
+
return parsed.type === "heartbeat.skip";
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildRunDetailsMap(
|
|
332
|
+
auditRows: Array<{
|
|
333
|
+
entityType: string;
|
|
334
|
+
eventType: string;
|
|
335
|
+
entityId: string;
|
|
336
|
+
payloadJson: string;
|
|
337
|
+
createdAt: Date;
|
|
338
|
+
}>
|
|
339
|
+
) {
|
|
340
|
+
const detailsByRunId = new Map<string, Record<string, unknown>>();
|
|
341
|
+
const relevantRows = auditRows
|
|
342
|
+
.filter(
|
|
343
|
+
(row) =>
|
|
344
|
+
row.entityType === "heartbeat_run" &&
|
|
345
|
+
(row.eventType === "heartbeat.completed" || row.eventType === "heartbeat.failed")
|
|
346
|
+
)
|
|
347
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
348
|
+
for (const row of relevantRows) {
|
|
349
|
+
if (detailsByRunId.has(row.entityId)) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
detailsByRunId.set(row.entityId, parsePayload(row.payloadJson));
|
|
353
|
+
}
|
|
354
|
+
return detailsByRunId;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function deriveRelevantMessagesFromRawTranscript(
|
|
358
|
+
items: Array<{
|
|
359
|
+
id: string;
|
|
360
|
+
companyId: string;
|
|
361
|
+
runId: string;
|
|
362
|
+
sequence: number;
|
|
363
|
+
kind: string;
|
|
364
|
+
label: string | null;
|
|
365
|
+
text: string | null;
|
|
366
|
+
payloadJson: string | null;
|
|
367
|
+
signalLevel: string | null;
|
|
368
|
+
groupKey: string | null;
|
|
369
|
+
source: string | null;
|
|
370
|
+
createdAt: Date;
|
|
371
|
+
}>,
|
|
372
|
+
run: {
|
|
373
|
+
id: string;
|
|
374
|
+
companyId: string;
|
|
375
|
+
status: string;
|
|
376
|
+
message: string | null;
|
|
377
|
+
finishedAt: Date | null;
|
|
378
|
+
},
|
|
379
|
+
kindFilter: string[],
|
|
380
|
+
signalOnly: boolean
|
|
381
|
+
) {
|
|
382
|
+
const derived: Array<{
|
|
383
|
+
id: string;
|
|
384
|
+
companyId: string;
|
|
385
|
+
runId: string;
|
|
386
|
+
sequence: number;
|
|
387
|
+
kind: "assistant" | "tool_call" | "tool_result" | "result";
|
|
388
|
+
label: string | null;
|
|
389
|
+
text: string | null;
|
|
390
|
+
payloadJson: string | null;
|
|
391
|
+
signalLevel: "high" | "medium";
|
|
392
|
+
groupKey: string | null;
|
|
393
|
+
source: "stderr";
|
|
394
|
+
createdAt: Date;
|
|
395
|
+
}> = [];
|
|
396
|
+
|
|
397
|
+
let inPromptBlock = true;
|
|
398
|
+
let assistantAfterCodex = false;
|
|
399
|
+
let pendingTool:
|
|
400
|
+
| {
|
|
401
|
+
sequence: number;
|
|
402
|
+
createdAt: Date;
|
|
403
|
+
command: string;
|
|
404
|
+
}
|
|
405
|
+
| undefined;
|
|
406
|
+
let pendingResult:
|
|
407
|
+
| {
|
|
408
|
+
sequence: number;
|
|
409
|
+
createdAt: Date;
|
|
410
|
+
command: string;
|
|
411
|
+
statusLine: string;
|
|
412
|
+
output: string[];
|
|
413
|
+
}
|
|
414
|
+
| undefined;
|
|
415
|
+
|
|
416
|
+
const flushPendingResult = () => {
|
|
417
|
+
if (!pendingResult) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const body = [pendingResult.statusLine, ...(pendingResult.output.length > 0 ? ["", ...pendingResult.output] : [])]
|
|
421
|
+
.join("\n")
|
|
422
|
+
.trim();
|
|
423
|
+
derived.push({
|
|
424
|
+
id: `derived-${run.id}-${pendingResult.sequence}-result`,
|
|
425
|
+
companyId: run.companyId,
|
|
426
|
+
runId: run.id,
|
|
427
|
+
sequence: pendingResult.sequence * 10 + 1,
|
|
428
|
+
kind: "tool_result",
|
|
429
|
+
label: pendingResult.command,
|
|
430
|
+
text: body || pendingResult.command,
|
|
431
|
+
payloadJson: null,
|
|
432
|
+
signalLevel: "high",
|
|
433
|
+
groupKey: `tool:${pendingResult.command}`,
|
|
434
|
+
source: "stderr",
|
|
435
|
+
createdAt: pendingResult.createdAt
|
|
436
|
+
});
|
|
437
|
+
pendingResult = undefined;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
for (const item of items) {
|
|
441
|
+
const text = (item.text ?? "").trim();
|
|
442
|
+
if (!text) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (inPromptBlock) {
|
|
446
|
+
if (text === "mcp startup: no servers" || text === "codex") {
|
|
447
|
+
inPromptBlock = false;
|
|
448
|
+
} else {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (text === "codex") {
|
|
454
|
+
flushPendingResult();
|
|
455
|
+
assistantAfterCodex = true;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (text === "exec") {
|
|
459
|
+
flushPendingResult();
|
|
460
|
+
assistantAfterCodex = false;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const inlineCommandMatch = /^(\/bin\/.+?) in .+? (succeeded|failed|exited .+?):$/i.exec(text);
|
|
465
|
+
if (inlineCommandMatch) {
|
|
466
|
+
const command = inlineCommandMatch[1]!.trim();
|
|
467
|
+
derived.push({
|
|
468
|
+
id: `derived-${run.id}-${item.sequence}-call`,
|
|
469
|
+
companyId: run.companyId,
|
|
470
|
+
runId: run.id,
|
|
471
|
+
sequence: item.sequence * 10,
|
|
472
|
+
kind: "tool_call",
|
|
473
|
+
label: "command_execution",
|
|
474
|
+
text: command,
|
|
475
|
+
payloadJson: JSON.stringify({ command }),
|
|
476
|
+
signalLevel: "high",
|
|
477
|
+
groupKey: `tool:${command}`,
|
|
478
|
+
source: "stderr",
|
|
479
|
+
createdAt: item.createdAt
|
|
480
|
+
});
|
|
481
|
+
pendingResult = {
|
|
482
|
+
sequence: item.sequence,
|
|
483
|
+
createdAt: item.createdAt,
|
|
484
|
+
command,
|
|
485
|
+
statusLine: text.slice(command.length).trim(),
|
|
486
|
+
output: []
|
|
487
|
+
};
|
|
488
|
+
assistantAfterCodex = false;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (text.startsWith("/bin/")) {
|
|
493
|
+
flushPendingResult();
|
|
494
|
+
pendingTool = {
|
|
495
|
+
sequence: item.sequence,
|
|
496
|
+
createdAt: item.createdAt,
|
|
497
|
+
command: text
|
|
498
|
+
};
|
|
499
|
+
derived.push({
|
|
500
|
+
id: `derived-${run.id}-${item.sequence}-call`,
|
|
501
|
+
companyId: run.companyId,
|
|
502
|
+
runId: run.id,
|
|
503
|
+
sequence: item.sequence * 10,
|
|
504
|
+
kind: "tool_call",
|
|
505
|
+
label: "command_execution",
|
|
506
|
+
text,
|
|
507
|
+
payloadJson: JSON.stringify({ command: text }),
|
|
508
|
+
signalLevel: "high",
|
|
509
|
+
groupKey: `tool:${text}`,
|
|
510
|
+
source: "stderr",
|
|
511
|
+
createdAt: item.createdAt
|
|
512
|
+
});
|
|
513
|
+
assistantAfterCodex = false;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (/^(succeeded in \d+ms:|failed in \d+ms:|exited \d+ in \d+ms:)$/i.test(text) && pendingTool) {
|
|
518
|
+
pendingResult = {
|
|
519
|
+
sequence: pendingTool.sequence,
|
|
520
|
+
createdAt: pendingTool.createdAt,
|
|
521
|
+
command: pendingTool.command,
|
|
522
|
+
statusLine: text,
|
|
523
|
+
output: []
|
|
524
|
+
};
|
|
525
|
+
pendingTool = undefined;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (pendingResult) {
|
|
530
|
+
if (text === "---" || text === "--------") {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
pendingResult.output.push(text);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (assistantAfterCodex && looksLikeUsefulAssistantText(text)) {
|
|
538
|
+
derived.push({
|
|
539
|
+
id: `derived-${run.id}-${item.sequence}-assistant`,
|
|
540
|
+
companyId: run.companyId,
|
|
541
|
+
runId: run.id,
|
|
542
|
+
sequence: item.sequence * 10,
|
|
543
|
+
kind: "assistant",
|
|
544
|
+
label: null,
|
|
545
|
+
text,
|
|
546
|
+
payloadJson: null,
|
|
547
|
+
signalLevel: "medium",
|
|
548
|
+
groupKey: "assistant",
|
|
549
|
+
source: "stderr",
|
|
550
|
+
createdAt: item.createdAt
|
|
551
|
+
});
|
|
552
|
+
assistantAfterCodex = false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
flushPendingResult();
|
|
557
|
+
|
|
558
|
+
const runMessage = run.message?.trim();
|
|
559
|
+
const shouldAppendRunSummary = Boolean(runMessage) && run.status !== "started";
|
|
560
|
+
if (!derived.some((item) => item.kind === "result") && shouldAppendRunSummary && runMessage) {
|
|
561
|
+
derived.push({
|
|
562
|
+
id: `derived-${run.id}-final-result`,
|
|
563
|
+
companyId: run.companyId,
|
|
564
|
+
runId: run.id,
|
|
565
|
+
sequence: (items[items.length - 1]?.sequence ?? 0) * 10 + 9,
|
|
566
|
+
kind: "result",
|
|
567
|
+
label: null,
|
|
568
|
+
text: runMessage,
|
|
569
|
+
payloadJson: null,
|
|
570
|
+
signalLevel: "high",
|
|
571
|
+
groupKey: "result",
|
|
572
|
+
source: "stderr",
|
|
573
|
+
createdAt: run.finishedAt ?? items[items.length - 1]?.createdAt ?? new Date()
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return derived
|
|
578
|
+
.filter((item) => (kindFilter.length > 0 ? kindFilter.includes(item.kind) : true))
|
|
579
|
+
.filter(() => (signalOnly ? true : true));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function looksLikeUsefulAssistantText(text: string) {
|
|
583
|
+
const normalized = text.toLowerCase();
|
|
584
|
+
if (normalized.length > 320) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
return (
|
|
588
|
+
normalized.includes("i’m ") ||
|
|
589
|
+
normalized.includes("i'm ") ||
|
|
590
|
+
normalized.includes("next i") ||
|
|
591
|
+
normalized.includes("using `") ||
|
|
592
|
+
normalized.includes("switching to") ||
|
|
593
|
+
normalized.includes("the workspace already") ||
|
|
594
|
+
normalized.includes("i have the") ||
|
|
595
|
+
normalized.includes("i still need") ||
|
|
596
|
+
normalized.includes("restoring")
|
|
597
|
+
);
|
|
598
|
+
}
|