job-forge 2.14.37 → 2.14.38
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/.opencode/instructions.md +8 -0
- package/README.md +2 -2
- package/batch/README.md +7 -6
- package/batch/batch-runner.sh +2 -2
- package/bin/create-job-forge.mjs +4 -1
- package/bin/sync.mjs +35 -1
- package/docs/ARCHITECTURE.md +9 -8
- package/docs/CUSTOMIZATION.md +6 -6
- package/iso/instructions.opencode.md +8 -0
- package/lib/jobforge-observability.mjs +847 -0
- package/opencode.json +2 -1
- package/package.json +8 -5
- package/scripts/batch-orchestrator.mjs +158 -9
- package/scripts/check-iso-smoke.mjs +2 -0
- package/scripts/guard.mjs +114 -190
- package/scripts/telemetry.mjs +214 -450
- package/scripts/trace.mjs +103 -232
package/scripts/telemetry.mjs
CHANGED
|
@@ -1,29 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawnSync } from 'child_process';
|
|
4
3
|
import { existsSync, readdirSync, statSync } from 'fs';
|
|
5
4
|
import { join, resolve } from 'path';
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildSessionGraph,
|
|
7
|
+
clean,
|
|
8
|
+
collectDispatchCalls,
|
|
9
|
+
descendantIds,
|
|
10
|
+
discoverProjectSessions,
|
|
11
|
+
duplicateDispatchUrlCount,
|
|
12
|
+
findObservedSession,
|
|
13
|
+
hasOutcome,
|
|
14
|
+
hasProxyLeak,
|
|
15
|
+
inspectionForSession,
|
|
16
|
+
isFreeModelRoute,
|
|
17
|
+
mentionsLimitedCandidatePool,
|
|
18
|
+
mergeModelUsage,
|
|
19
|
+
messageEvents,
|
|
20
|
+
modelLabel,
|
|
21
|
+
modelUsageFromSession,
|
|
22
|
+
outcomeFromText,
|
|
23
|
+
pad,
|
|
24
|
+
providerErrorsForSession,
|
|
25
|
+
providerErrorCategory,
|
|
26
|
+
redactSecrets,
|
|
27
|
+
relativeToProject,
|
|
28
|
+
requestedJobCount,
|
|
29
|
+
shorten,
|
|
30
|
+
statusCodeFromText,
|
|
31
|
+
stringValue,
|
|
32
|
+
summarizeChildSession,
|
|
33
|
+
userRequestSummaries,
|
|
34
|
+
loadObservedSession,
|
|
35
|
+
} from '../lib/jobforge-observability.mjs';
|
|
7
36
|
import { jobForgeLedgerSummary } from '../lib/jobforge-ledger.mjs';
|
|
8
37
|
|
|
9
38
|
const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
10
39
|
const DEFAULT_SINCE = '24h';
|
|
11
40
|
|
|
12
|
-
const USAGE = `job-forge telemetry — JobForge pipeline view over local
|
|
41
|
+
const USAGE = `job-forge telemetry — JobForge pipeline view over local traces
|
|
13
42
|
|
|
14
43
|
Usage:
|
|
15
|
-
job-forge telemetry:list [--since 24h] [--cwd <dir>] [--json]
|
|
16
|
-
job-forge telemetry:status [--since 24h] [--cwd <dir>] [--json]
|
|
17
|
-
job-forge telemetry:show <id-or-prefix> [--cwd <dir>] [--json]
|
|
18
|
-
job-forge telemetry:watch [--since 24h] [--cwd <dir>] [--interval 5]
|
|
44
|
+
job-forge telemetry:list [--since 24h] [--cwd <dir>] [--harness <name>] [--json]
|
|
45
|
+
job-forge telemetry:status [--since 24h] [--cwd <dir>] [--harness <name>] [--json]
|
|
46
|
+
job-forge telemetry:show <id-or-prefix> [--cwd <dir>] [--harness <name>] [--json]
|
|
47
|
+
job-forge telemetry:watch [--since 24h] [--cwd <dir>] [--harness <name>] [--interval 5]
|
|
19
48
|
|
|
20
|
-
Telemetry is local-only and passive. It derives status from
|
|
21
|
-
plus JobForge tracker files; agents do not need to emit custom events.`;
|
|
49
|
+
Telemetry is local-only and passive. It derives status from normalized local
|
|
50
|
+
traces plus JobForge tracker files; agents do not need to emit custom events.`;
|
|
22
51
|
|
|
23
52
|
const [cmd = 'help', ...args] = process.argv.slice(2);
|
|
24
53
|
|
|
25
54
|
function parseArgs(rawArgs, { allowSession = false, allowInterval = false } = {}) {
|
|
26
|
-
const opts = { since: DEFAULT_SINCE, cwd: PROJECT_DIR, json: false, interval: 5 };
|
|
55
|
+
const opts = { since: DEFAULT_SINCE, cwd: PROJECT_DIR, harness: '', json: false, interval: 5 };
|
|
27
56
|
const positional = [];
|
|
28
57
|
|
|
29
58
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
@@ -36,6 +65,10 @@ function parseArgs(rawArgs, { allowSession = false, allowInterval = false } = {}
|
|
|
36
65
|
opts.cwd = rawArgs[++i];
|
|
37
66
|
} else if (arg.startsWith('--cwd=')) {
|
|
38
67
|
opts.cwd = arg.slice('--cwd='.length);
|
|
68
|
+
} else if (arg === '--harness') {
|
|
69
|
+
opts.harness = rawArgs[++i];
|
|
70
|
+
} else if (arg.startsWith('--harness=')) {
|
|
71
|
+
opts.harness = arg.slice('--harness='.length);
|
|
39
72
|
} else if (arg === '--json') {
|
|
40
73
|
opts.json = true;
|
|
41
74
|
} else if (allowInterval && arg === '--interval') {
|
|
@@ -58,146 +91,122 @@ function parseArgs(rawArgs, { allowSession = false, allowInterval = false } = {}
|
|
|
58
91
|
return { opts, positional };
|
|
59
92
|
}
|
|
60
93
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
encoding: 'utf8',
|
|
64
|
-
maxBuffer: 24 * 1024 * 1024,
|
|
65
|
-
});
|
|
66
|
-
if ((result.status ?? 0) !== 0) {
|
|
67
|
-
const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
|
|
68
|
-
throw new Error(`job-forge telemetry: sqlite3 query failed: ${detail}`);
|
|
69
|
-
}
|
|
70
|
-
return JSON.parse(result.stdout || '[]');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function sqlString(value) {
|
|
74
|
-
return `'${String(value).replaceAll("'", "''")}'`;
|
|
94
|
+
function rootRefs(refs, graph) {
|
|
95
|
+
return graph.roots.length > 0 ? graph.roots : refs;
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
's.time_archived is null',
|
|
87
|
-
`s.directory = ${sqlString(opts.cwd)}`,
|
|
88
|
-
];
|
|
89
|
-
const sinceMs = includeAllForShow ? undefined : parseSinceCutoff(opts.since);
|
|
90
|
-
if (sinceMs !== undefined) where.push(`s.time_created >= ${Number(sinceMs)}`);
|
|
91
|
-
|
|
92
|
-
const rows = queryOpenCodeDb(dbPath, [
|
|
93
|
-
'select',
|
|
94
|
-
' s.id,',
|
|
95
|
-
' s.parent_id,',
|
|
96
|
-
' s.title,',
|
|
97
|
-
' s.directory,',
|
|
98
|
-
' s.time_created,',
|
|
99
|
-
' s.time_updated,',
|
|
100
|
-
' (select count(*) from message m where m.session_id = s.id) as turn_count,',
|
|
101
|
-
' (',
|
|
102
|
-
' (select coalesce(sum(length(data)), 0) from message m where m.session_id = s.id) +',
|
|
103
|
-
' (select coalesce(sum(length(data)), 0) from part p where p.session_id = s.id)',
|
|
104
|
-
' ) as size_bytes',
|
|
105
|
-
'from session s',
|
|
106
|
-
`where ${where.join(' and ')}`,
|
|
107
|
-
'order by s.time_updated desc',
|
|
108
|
-
].join(' '));
|
|
109
|
-
|
|
110
|
-
return rows.map((row) => ({
|
|
111
|
-
id: row.id,
|
|
112
|
-
parentId: row.parent_id || null,
|
|
113
|
-
title: row.title || '',
|
|
114
|
-
cwd: row.directory,
|
|
115
|
-
startedAt: msToIso(row.time_created),
|
|
116
|
-
endedAt: msToIso(row.time_updated),
|
|
117
|
-
turnCount: row.turn_count ?? 0,
|
|
118
|
-
sizeBytes: row.size_bytes ?? 0,
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function loadRows(sessionId) {
|
|
123
|
-
const dbPath = defaultOpenCodeDbPath();
|
|
124
|
-
const id = sqlString(sessionId);
|
|
125
|
-
return {
|
|
126
|
-
messages: queryOpenCodeDb(dbPath, `select id, time_created, data from message where session_id = ${id} order by time_created, id`),
|
|
127
|
-
parts: queryOpenCodeDb(dbPath, `select id, message_id, time_created, data from part where session_id = ${id} order by time_created, id`),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function parseJson(raw) {
|
|
132
|
-
try {
|
|
133
|
-
return JSON.parse(raw || '{}');
|
|
134
|
-
} catch (error) {
|
|
135
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
-
return { __parseError: message, __raw: raw || '' };
|
|
98
|
+
function loadContext(refs) {
|
|
99
|
+
const sessionsById = new Map();
|
|
100
|
+
const loadErrors = new Map();
|
|
101
|
+
for (const ref of refs) {
|
|
102
|
+
try {
|
|
103
|
+
sessionsById.set(ref.id, loadObservedSession(ref));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
loadErrors.set(ref.id, error instanceof Error ? error.message : String(error));
|
|
106
|
+
}
|
|
137
107
|
}
|
|
108
|
+
const loadedRefs = refs.filter((ref) => sessionsById.has(ref.id));
|
|
109
|
+
const graph = buildSessionGraph(loadedRefs, sessionsById);
|
|
110
|
+
const refsById = new Map(refs.map((ref) => [ref.id, ref]));
|
|
111
|
+
return { refsById, sessionsById, graph, loadedRefs, loadErrors };
|
|
138
112
|
}
|
|
139
113
|
|
|
140
|
-
function analyzeSession(
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const messageById = new Map(messages.map((m) => [m.row.id, m.data]));
|
|
145
|
-
const textParts = parts.filter((p) => p.data.type === 'text');
|
|
146
|
-
const taskCalls = parts.filter((p) => p.data.type === 'tool' && p.data.tool === 'task').map(taskCallSummary);
|
|
147
|
-
const userRequests = userRequestSummaries(textParts, messageById);
|
|
114
|
+
function analyzeSession(ref, context, opts) {
|
|
115
|
+
const session = context.sessionsById.get(ref.id);
|
|
116
|
+
const inspection = inspectionForSession(session);
|
|
117
|
+
const userRequests = userRequestSummaries(session);
|
|
148
118
|
const activeRequest = userRequests.at(-1) || null;
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
119
|
+
const prompt = activeRequest?.prompt || userRequests[0]?.prompt || inspection.preview.firstUser || '';
|
|
120
|
+
const dispatchCalls = collectDispatchCalls(session);
|
|
121
|
+
const latestDispatchCalls = activeRequest
|
|
122
|
+
? dispatchCalls.filter((call) => call.atMs >= activeRequest.atMs)
|
|
123
|
+
: dispatchCalls;
|
|
124
|
+
const providerErrors = providerErrorsForSession(session);
|
|
125
|
+
const rootModels = modelUsageFromSession(session);
|
|
155
126
|
const tracker = trackerStatus(opts.cwd);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.
|
|
159
|
-
.
|
|
127
|
+
|
|
128
|
+
const childRefs = (context.graph.childrenBySession.get(ref.id) || [])
|
|
129
|
+
.map((id) => context.refsById.get(id))
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
132
|
+
const children = childRefs
|
|
133
|
+
.map((childRef) => summarizeChildSession(context.sessionsById.get(childRef.id)))
|
|
134
|
+
.filter(Boolean);
|
|
160
135
|
const latestChildren = activeRequest
|
|
161
136
|
? children.filter((child) => child.startedAtMs >= activeRequest.atMs)
|
|
162
137
|
: children;
|
|
163
138
|
const models = mergeModelUsage([rootModels, ...children.map((child) => child.models)]);
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
139
|
+
const unresolvedChildren = dispatchCalls.filter((call) => call.sessionId && !children.find((child) => child.id === call.sessionId));
|
|
140
|
+
const policyIssues = detectPolicyIssues(ref, session, providerErrors, {
|
|
141
|
+
dispatchCalls,
|
|
142
|
+
latestDispatchCalls,
|
|
167
143
|
children,
|
|
168
144
|
latestChildren,
|
|
145
|
+
unresolvedChildren,
|
|
169
146
|
activeRequest,
|
|
170
147
|
models,
|
|
148
|
+
inspection,
|
|
149
|
+
isChildSession: context.graph.childIds.has(ref.id),
|
|
171
150
|
});
|
|
151
|
+
|
|
172
152
|
const childOutcomes = children.filter((child) => child.outcome !== 'unknown').length;
|
|
173
153
|
const childProviderErrors = children.reduce((sum, child) => sum + child.providerErrors, 0);
|
|
174
|
-
const status = sessionStatus({
|
|
175
|
-
|
|
154
|
+
const status = sessionStatus({
|
|
155
|
+
dispatchCalls,
|
|
156
|
+
children,
|
|
157
|
+
unresolvedChildren,
|
|
158
|
+
childOutcomes,
|
|
159
|
+
childProviderErrors,
|
|
160
|
+
policyIssues,
|
|
161
|
+
providerErrors,
|
|
162
|
+
});
|
|
163
|
+
const recommendations = nextActions({ tracker, policyIssues, providerErrors, dispatchCalls, children });
|
|
176
164
|
|
|
177
165
|
return {
|
|
178
|
-
session
|
|
166
|
+
session: {
|
|
167
|
+
id: ref.id,
|
|
168
|
+
harness: ref.source.harness,
|
|
169
|
+
title: ref.title || session.title || '',
|
|
170
|
+
startedAt: ref.startedAt,
|
|
171
|
+
endedAt: ref.endedAt,
|
|
172
|
+
},
|
|
179
173
|
projectDir: opts.cwd,
|
|
180
174
|
status,
|
|
181
|
-
prompt
|
|
175
|
+
prompt,
|
|
182
176
|
userRequests,
|
|
183
177
|
latestRequest: activeRequest ? {
|
|
184
178
|
...activeRequest,
|
|
185
|
-
taskDispatches:
|
|
179
|
+
taskDispatches: latestDispatchCalls.filter((call) => !call.isStatusPoll).length,
|
|
186
180
|
children: latestChildren.length,
|
|
187
181
|
childOutcomes: latestChildren.filter((child) => child.outcome !== 'unknown').length,
|
|
188
182
|
} : null,
|
|
189
183
|
tasks: {
|
|
190
|
-
total:
|
|
191
|
-
statusPolls:
|
|
192
|
-
running:
|
|
193
|
-
calls:
|
|
184
|
+
total: dispatchCalls.length,
|
|
185
|
+
statusPolls: dispatchCalls.filter((call) => call.isStatusPoll).length,
|
|
186
|
+
running: children.filter((child) => child.outcome === 'unknown').length + unresolvedChildren.length,
|
|
187
|
+
calls: dispatchCalls,
|
|
194
188
|
},
|
|
195
189
|
children: {
|
|
196
|
-
total: children.length,
|
|
190
|
+
total: children.length + unresolvedChildren.length,
|
|
197
191
|
withOutcomes: childOutcomes,
|
|
198
192
|
providerErrors: childProviderErrors,
|
|
199
193
|
toolErrors: children.reduce((sum, child) => sum + child.toolErrors, 0),
|
|
200
|
-
sessions:
|
|
194
|
+
sessions: [
|
|
195
|
+
...children,
|
|
196
|
+
...unresolvedChildren.map((call) => ({
|
|
197
|
+
id: call.sessionId,
|
|
198
|
+
title: call.description || '',
|
|
199
|
+
startedAt: call.at,
|
|
200
|
+
outcome: 'unknown',
|
|
201
|
+
providerErrors: 0,
|
|
202
|
+
taskCalls: 0,
|
|
203
|
+
dispatchCalls: 0,
|
|
204
|
+
toolErrors: 0,
|
|
205
|
+
dedupeMiss: false,
|
|
206
|
+
trackerWrites: 0,
|
|
207
|
+
models: [],
|
|
208
|
+
})),
|
|
209
|
+
],
|
|
201
210
|
},
|
|
202
211
|
models,
|
|
203
212
|
providerErrors,
|
|
@@ -207,80 +216,32 @@ function analyzeSession(session, allSessions, opts) {
|
|
|
207
216
|
};
|
|
208
217
|
}
|
|
209
218
|
|
|
210
|
-
function
|
|
211
|
-
return textParts
|
|
212
|
-
.filter((part) => messageById.get(part.row.message_id)?.role === 'user')
|
|
213
|
-
.map((part) => {
|
|
214
|
-
const prompt = clean(redactSecrets(part.data.text || ''));
|
|
215
|
-
return {
|
|
216
|
-
at: msToIso(part.row.time_created),
|
|
217
|
-
atMs: Number(part.row.time_created),
|
|
218
|
-
prompt,
|
|
219
|
-
requestedJobs: requestedJobCount(prompt),
|
|
220
|
-
};
|
|
221
|
-
})
|
|
222
|
-
.filter((request) => request.prompt.length > 0);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function taskCallSummary(part) {
|
|
226
|
-
const input = objectOrEmpty(part.data.state?.input);
|
|
227
|
-
const metadata = objectOrEmpty(part.data.state?.metadata);
|
|
228
|
-
const prompt = typeof input.prompt === 'string' ? input.prompt : '';
|
|
229
|
-
const description = stringValue(input.description || metadata.description || part.data.state?.title);
|
|
230
|
-
const sessionId = stringValue(input.task_id || metadata.sessionId);
|
|
231
|
-
const subagentType = stringValue(input.subagent_type || metadata.subagent_type || metadata.agent);
|
|
232
|
-
const isStatusPoll = Boolean(input.task_id) ||
|
|
233
|
-
/\b(check|poll|status|force|abort|progress|result)\b/i.test(description) ||
|
|
234
|
-
/\b(return your final outcome now|if still working|current status|report your current status|still running)\b/i.test(prompt);
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
at: msToIso(part.row.time_created),
|
|
238
|
-
atMs: Number(part.row.time_created),
|
|
239
|
-
description,
|
|
240
|
-
subagentType,
|
|
241
|
-
sessionId,
|
|
242
|
-
status: stringValue(part.data.state?.status),
|
|
243
|
-
isStatusPoll,
|
|
244
|
-
promptBytes: Buffer.byteLength(prompt, 'utf8'),
|
|
245
|
-
proxyLeak: hasProxyLeak(prompt),
|
|
246
|
-
url: firstUrl(prompt),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function providerErrorSummary(message) {
|
|
251
|
-
const error = message.data.error;
|
|
252
|
-
if (!error) return null;
|
|
253
|
-
const rawMessage = stringValue(error.data?.message || error.message || error.name || 'unknown provider error');
|
|
254
|
-
const statusCode = error.data?.statusCode ?? statusCodeFromText(rawMessage);
|
|
255
|
-
return {
|
|
256
|
-
at: msToIso(message.row.time_created),
|
|
257
|
-
provider: stringValue(message.data.providerID),
|
|
258
|
-
model: stringValue(message.data.modelID),
|
|
259
|
-
statusCode,
|
|
260
|
-
category: providerErrorCategory(rawMessage, statusCode),
|
|
261
|
-
message: redactSecrets(rawMessage),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function detectPolicyIssues(session, parts, textParts, messageById, providerErrors, context = {}) {
|
|
219
|
+
function detectPolicyIssues(ref, session, providerErrors, context = {}) {
|
|
266
220
|
const issues = [];
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const latestTaskCalls = context.latestTaskCalls || taskCalls;
|
|
221
|
+
const dispatchCalls = context.dispatchCalls || [];
|
|
222
|
+
const latestDispatchCalls = context.latestDispatchCalls || dispatchCalls;
|
|
270
223
|
const children = context.children || [];
|
|
271
224
|
const latestChildren = context.latestChildren || children;
|
|
225
|
+
const unresolvedChildren = context.unresolvedChildren || [];
|
|
272
226
|
const activeRequest = context.activeRequest || null;
|
|
273
|
-
const
|
|
227
|
+
const assistantTexts = messageEvents(session, 'assistant');
|
|
228
|
+
const latestAssistantText = assistantTexts
|
|
229
|
+
.filter((event) => !activeRequest || event.atMs >= activeRequest.atMs)
|
|
230
|
+
.map((event) => event.text || '')
|
|
231
|
+
.join('\n');
|
|
232
|
+
const finalText = assistantTexts.slice(-5).map((event) => event.text || '').join('\n');
|
|
233
|
+
|
|
234
|
+
const statusPolls = dispatchCalls.filter((call) => call.isStatusPoll);
|
|
274
235
|
if (statusPolls.length > 0) {
|
|
275
236
|
issues.push({
|
|
276
237
|
type: 'task_status_poll',
|
|
277
238
|
severity: 'high',
|
|
278
239
|
count: statusPolls.length,
|
|
279
|
-
detail: 'A
|
|
240
|
+
detail: 'A dispatch call tried to poll/check an existing subagent session.',
|
|
280
241
|
});
|
|
281
242
|
}
|
|
282
243
|
|
|
283
|
-
const proxyLeakCount =
|
|
244
|
+
const proxyLeakCount = dispatchCalls.filter((call) => call.proxyLeak).length;
|
|
284
245
|
if (proxyLeakCount > 0) {
|
|
285
246
|
issues.push({
|
|
286
247
|
type: 'proxy_prompt_leak',
|
|
@@ -290,13 +251,12 @@ function detectPolicyIssues(session, parts, textParts, messageById, providerErro
|
|
|
290
251
|
});
|
|
291
252
|
}
|
|
292
253
|
|
|
293
|
-
|
|
294
|
-
if (childTaskCalls > 0) {
|
|
254
|
+
if (context.isChildSession && dispatchCalls.length > 0) {
|
|
295
255
|
issues.push({
|
|
296
256
|
type: 'subagent_spawned_task',
|
|
297
257
|
severity: 'high',
|
|
298
|
-
count:
|
|
299
|
-
detail: 'A child/subagent session
|
|
258
|
+
count: dispatchCalls.length,
|
|
259
|
+
detail: 'A child/subagent session spawned more child work.',
|
|
300
260
|
});
|
|
301
261
|
}
|
|
302
262
|
|
|
@@ -330,7 +290,7 @@ function detectPolicyIssues(session, parts, textParts, messageById, providerErro
|
|
|
330
290
|
});
|
|
331
291
|
}
|
|
332
292
|
|
|
333
|
-
const duplicateUrlCount =
|
|
293
|
+
const duplicateUrlCount = duplicateDispatchUrlCount(dispatchCalls);
|
|
334
294
|
if (duplicateUrlCount > 0) {
|
|
335
295
|
issues.push({
|
|
336
296
|
type: 'duplicate_task_url',
|
|
@@ -340,35 +300,22 @@ function detectPolicyIssues(session, parts, textParts, messageById, providerErro
|
|
|
340
300
|
});
|
|
341
301
|
}
|
|
342
302
|
|
|
343
|
-
|
|
344
|
-
if (runningTasks.length > 0) {
|
|
345
|
-
const consumed = runningTasks.filter((task) => {
|
|
346
|
-
if (!task.sessionId) return false;
|
|
347
|
-
const child = children.find((candidate) => candidate.id === task.sessionId);
|
|
348
|
-
return child && child.outcome !== 'unknown';
|
|
349
|
-
}).length;
|
|
303
|
+
if (unresolvedChildren.length > 0) {
|
|
350
304
|
issues.push({
|
|
351
|
-
type:
|
|
352
|
-
severity:
|
|
353
|
-
count:
|
|
354
|
-
detail:
|
|
355
|
-
? 'One or more task calls still show running even though child sessions have terminal-looking outcomes; root did not consume the final task result.'
|
|
356
|
-
: 'One or more task calls still show running and do not have terminal child outcomes.',
|
|
305
|
+
type: 'task_still_running',
|
|
306
|
+
severity: 'high',
|
|
307
|
+
count: unresolvedChildren.length,
|
|
308
|
+
detail: 'One or more dispatches reference child sessions that do not yet have a visible terminal outcome.',
|
|
357
309
|
});
|
|
358
310
|
}
|
|
359
311
|
|
|
360
|
-
const
|
|
361
|
-
.filter((part) => messageById.get(part.row.message_id)?.role === 'assistant')
|
|
362
|
-
.filter((part) => !activeRequest || Number(part.row.time_created) >= activeRequest.atMs)
|
|
363
|
-
.map((part) => part.data.text || '')
|
|
364
|
-
.join('\n');
|
|
365
|
-
const latestDispatches = latestTaskCalls.filter((task) => !task.isStatusPoll).length;
|
|
312
|
+
const latestDispatches = latestDispatchCalls.filter((call) => !call.isStatusPoll).length;
|
|
366
313
|
if (activeRequest?.requestedJobs && latestDispatches > 0 && latestDispatches < activeRequest.requestedJobs && !mentionsLimitedCandidatePool(latestAssistantText)) {
|
|
367
314
|
issues.push({
|
|
368
315
|
type: 'requested_count_not_met',
|
|
369
316
|
severity: 'high',
|
|
370
317
|
count: activeRequest.requestedJobs - latestDispatches,
|
|
371
|
-
detail: `Latest request asked for ${activeRequest.requestedJobs} jobs, but only ${latestDispatches}
|
|
318
|
+
detail: `Latest request asked for ${activeRequest.requestedJobs} jobs, but only ${latestDispatches} dispatches are visible after that prompt.`,
|
|
372
319
|
});
|
|
373
320
|
}
|
|
374
321
|
|
|
@@ -381,105 +328,32 @@ function detectPolicyIssues(session, parts, textParts, messageById, providerErro
|
|
|
381
328
|
});
|
|
382
329
|
}
|
|
383
330
|
|
|
384
|
-
const finalText = textParts
|
|
385
|
-
.filter((part) => messageById.get(part.row.message_id)?.role === 'assistant')
|
|
386
|
-
.slice(-5)
|
|
387
|
-
.map((part) => part.data.text || '')
|
|
388
|
-
.join('\n');
|
|
389
331
|
if (latestDispatches > 0 && !hasOutcome(latestAssistantText) && !/round .*in flight|still running|waiting/i.test(latestAssistantText)) {
|
|
390
332
|
issues.push({
|
|
391
333
|
type: 'latest_request_no_visible_final_outcome',
|
|
392
334
|
severity: 'high',
|
|
393
335
|
count: 1,
|
|
394
|
-
detail: 'Latest request dispatched
|
|
336
|
+
detail: 'Latest request dispatched child work but assistant text after that request has no final outcome or in-flight notice.',
|
|
395
337
|
});
|
|
396
|
-
} else if (
|
|
338
|
+
} else if (dispatchCalls.length > 0 && !hasOutcome(finalText) && !/round .*in flight|still running|waiting/i.test(finalText)) {
|
|
397
339
|
issues.push({
|
|
398
340
|
type: 'no_visible_final_outcome',
|
|
399
341
|
severity: 'medium',
|
|
400
342
|
count: 1,
|
|
401
|
-
detail: 'Session dispatched
|
|
343
|
+
detail: 'Session dispatched child work but recent assistant text has no final outcome or in-flight notice.',
|
|
402
344
|
});
|
|
403
345
|
}
|
|
404
346
|
|
|
405
347
|
return issues;
|
|
406
348
|
}
|
|
407
349
|
|
|
408
|
-
function
|
|
409
|
-
const data = part.data;
|
|
410
|
-
if (data.type === 'text' || data.type === 'reasoning') return hasProxyLeak(data.text || '');
|
|
411
|
-
if (data.type === 'tool') return hasProxyLeak(JSON.stringify(data.state?.input || {}));
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function childSummary(session) {
|
|
416
|
-
const rows = loadRows(session.id);
|
|
417
|
-
const messages = rows.messages.map((row) => ({ row, data: parseJson(row.data) }));
|
|
418
|
-
const parts = rows.parts.map((row) => ({ row, data: parseJson(row.data) }));
|
|
419
|
-
const messageById = new Map(messages.map((m) => [m.row.id, m.data]));
|
|
420
|
-
const assistantTexts = parts
|
|
421
|
-
.filter((p) => p.data.type === 'text' && messageById.get(p.row.message_id)?.role === 'assistant')
|
|
422
|
-
.map((p) => p.data.text || '');
|
|
423
|
-
const finalText = assistantTexts.slice(-5).join('\n');
|
|
424
|
-
const providerErrors = messages.map(providerErrorSummary).filter(Boolean);
|
|
425
|
-
const taskCalls = parts.filter((p) => p.data.type === 'tool' && p.data.tool === 'task').length;
|
|
426
|
-
const trackerWrites = parts.filter((p) => p.data.type === 'tool' && /batch\/tracker-additions\/.*\.tsv/.test(JSON.stringify(p.data.state?.input || {}))).length;
|
|
427
|
-
const toolErrors = parts.filter((p) => p.data.type === 'tool' && (p.data.state?.status === 'error' || p.data.state?.error)).length;
|
|
428
|
-
const dedupeMiss = /\b(DUPLICATE|already\s+\*{0,2}Applied|already applied|per \[H2\]|Hard Limit #2|No re-dispatch needed)\b/i.test(finalText) ||
|
|
429
|
-
/\bpreviously applied (on|as|under)\b/i.test(finalText);
|
|
430
|
-
|
|
431
|
-
return {
|
|
432
|
-
id: session.id,
|
|
433
|
-
title: session.title,
|
|
434
|
-
startedAt: session.startedAt,
|
|
435
|
-
startedAtMs: Date.parse(session.startedAt),
|
|
436
|
-
endedAt: session.endedAt,
|
|
437
|
-
outcome: outcomeFromText(finalText, trackerWrites),
|
|
438
|
-
providerErrors: providerErrors.length,
|
|
439
|
-
taskCalls,
|
|
440
|
-
toolErrors,
|
|
441
|
-
dedupeMiss,
|
|
442
|
-
trackerWrites,
|
|
443
|
-
models: modelUsageFromMessages(messages),
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function outcomeFromText(text, trackerWrites = 0) {
|
|
448
|
-
const explicitFailed = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*(FAILED|APPLY FAILED)\b/i.test(text) ||
|
|
449
|
-
/\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?Failed\*\*?/i.test(text);
|
|
450
|
-
const explicitSkipped = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*(SKIP|SKIPPED|DISCARDED|DISCARD)\b/i.test(text) ||
|
|
451
|
-
/\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?(SKIP|SKIPPED|Discarded|DISCARDED)\*\*?/i.test(text);
|
|
452
|
-
const explicitApplied = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*APPLIED\b/i.test(text) ||
|
|
453
|
-
/\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?Applied\*\*?/i.test(text);
|
|
454
|
-
|
|
455
|
-
if (explicitFailed) return 'Failed';
|
|
456
|
-
if (explicitSkipped) return 'Discarded';
|
|
457
|
-
if (explicitApplied) return 'Applied';
|
|
458
|
-
|
|
459
|
-
if (/\bAPPLY FAILED\b/i.test(text) || /^\s*(FAILED|Failed)\b/m.test(text)) return 'Failed';
|
|
460
|
-
if (/^\s*(SKIP|SKIPPED|DISCARDED|Discarded)\b/m.test(text) ||
|
|
461
|
-
/\b(DUPLICATE|job posting closed|role no longer available)\b/i.test(text)) return 'Discarded';
|
|
462
|
-
if (/\bwith\s+\*\*?Applied\*\*?\s+status\b/i.test(text) ||
|
|
463
|
-
/\bAPPLIED\s+https?:\/\//i.test(text) ||
|
|
464
|
-
/\b(successfully submitted|Applied via|Thank you for applying|confirmation page)\b/i.test(text)) return 'Applied';
|
|
465
|
-
if (trackerWrites > 0) return 'TSV written';
|
|
466
|
-
return 'unknown';
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function hasOutcome(text) {
|
|
470
|
-
return outcomeFromText(text) !== 'unknown' ||
|
|
471
|
-
/tracker-additions\/.*\.tsv/i.test(text) ||
|
|
472
|
-
/\bAll\s+\d+\s+jobs?\s+dispatched\b/i.test(text) ||
|
|
473
|
-
/\*\*(Applied|Skipped|Failed|Discarded)\s*\(\d+\):\*\*/i.test(text);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function sessionStatus({ taskCalls, children, childOutcomes, childProviderErrors, policyIssues, providerErrors }) {
|
|
350
|
+
function sessionStatus({ dispatchCalls, children, unresolvedChildren, childOutcomes, childProviderErrors, policyIssues, providerErrors }) {
|
|
477
351
|
if (policyIssues.some((issue) => issue.severity === 'high')) return 'attention';
|
|
478
352
|
if (providerErrors.length > 0) return 'attention';
|
|
479
353
|
if (childProviderErrors > 0) return 'attention';
|
|
480
|
-
if (
|
|
481
|
-
if (
|
|
482
|
-
if (
|
|
354
|
+
if (unresolvedChildren.length > 0) return 'in-flight-or-incomplete';
|
|
355
|
+
if (dispatchCalls.length > 0 && children.length > childOutcomes) return 'in-flight-or-incomplete';
|
|
356
|
+
if (dispatchCalls.length > 0 && children.length === childOutcomes) return 'complete';
|
|
483
357
|
return 'observed';
|
|
484
358
|
}
|
|
485
359
|
|
|
@@ -519,12 +393,11 @@ function listTsv(dir) {
|
|
|
519
393
|
function nextActions({ tracker, policyIssues, providerErrors, children }) {
|
|
520
394
|
const actions = [];
|
|
521
395
|
if (tracker.pending.length > 0) actions.push('Run `npm run merge && npm run verify` when you are ready to fold pending TSV outcomes into day files.');
|
|
522
|
-
if (policyIssues.some((issue) => issue.type === 'task_status_poll')) actions.push('Avoid resuming by spawning "check
|
|
523
|
-
if (policyIssues.some((issue) => issue.type === 'proxy_prompt_leak')) actions.push('
|
|
524
|
-
if (policyIssues.some((issue) => issue.type === 'free_model_usage')) actions.push('
|
|
396
|
+
if (policyIssues.some((issue) => issue.type === 'task_status_poll')) actions.push('Avoid resuming by spawning "check status" child sessions; inspect telemetry/trace and tracker files instead.');
|
|
397
|
+
if (policyIssues.some((issue) => issue.type === 'proxy_prompt_leak')) actions.push('Refresh the harness instructions so new sessions inherit the proxy prompt hygiene rule.');
|
|
398
|
+
if (policyIssues.some((issue) => issue.type === 'free_model_usage')) actions.push('Refresh the harness config so application tiers use the intended paid/default route.');
|
|
525
399
|
if (policyIssues.some((issue) => issue.type === 'requested_count_not_met')) actions.push('Resume the latest apply request or start a new run for the remaining requested jobs; telemetry did not see enough dispatches after the latest prompt.');
|
|
526
|
-
if (policyIssues.some((issue) => issue.type === 'latest_request_no_visible_final_outcome')) actions.push('Inspect the latest child sessions before treating the current
|
|
527
|
-
if (policyIssues.some((issue) => issue.type === 'task_result_not_consumed')) actions.push('Resume the root session only to collect final task results and summarize; do not dispatch new applications until it reconciles current children.');
|
|
400
|
+
if (policyIssues.some((issue) => issue.type === 'latest_request_no_visible_final_outcome')) actions.push('Inspect the latest child sessions before treating the current run as complete.');
|
|
528
401
|
if (policyIssues.some((issue) => issue.type === 'duplicate_task_url')) actions.push('Do not re-dispatch duplicate URLs automatically; inspect the prior child result and tracker TSV before retrying.');
|
|
529
402
|
if (policyIssues.some((issue) => issue.type === 'dedupe_preflight_missed')) actions.push('Tighten candidate preflight: grep all application day files plus pending/merged TSVs before dispatching replacements.');
|
|
530
403
|
if (providerErrors.some((err) => err.statusCode === 402)) actions.push('Provider balance errors occurred; use a non-402 fallback or add provider credits before retrying paid routes.');
|
|
@@ -535,6 +408,7 @@ function nextActions({ tracker, policyIssues, providerErrors, children }) {
|
|
|
535
408
|
function summaryForList(telemetry) {
|
|
536
409
|
return {
|
|
537
410
|
id: telemetry.session.id,
|
|
411
|
+
harness: telemetry.session.harness,
|
|
538
412
|
startedAt: telemetry.session.startedAt,
|
|
539
413
|
updatedAt: telemetry.session.endedAt,
|
|
540
414
|
status: telemetry.status,
|
|
@@ -547,112 +421,43 @@ function summaryForList(telemetry) {
|
|
|
547
421
|
};
|
|
548
422
|
}
|
|
549
423
|
|
|
550
|
-
function modelUsageFromMessages(messages) {
|
|
551
|
-
const counts = new Map();
|
|
552
|
-
for (const message of messages) {
|
|
553
|
-
const provider = stringValue(message.data.providerID);
|
|
554
|
-
const model = stringValue(message.data.modelID);
|
|
555
|
-
if (!provider && !model) continue;
|
|
556
|
-
const key = `${provider}\u0000${model}`;
|
|
557
|
-
const current = counts.get(key) || { provider, model, count: 0 };
|
|
558
|
-
current.count += 1;
|
|
559
|
-
counts.set(key, current);
|
|
560
|
-
}
|
|
561
|
-
return [...counts.values()].sort((a, b) => b.count - a.count || modelLabel(a).localeCompare(modelLabel(b)));
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function mergeModelUsage(groups) {
|
|
565
|
-
const counts = new Map();
|
|
566
|
-
for (const group of groups) {
|
|
567
|
-
for (const item of group || []) {
|
|
568
|
-
const provider = stringValue(item.provider);
|
|
569
|
-
const model = stringValue(item.model);
|
|
570
|
-
const key = `${provider}\u0000${model}`;
|
|
571
|
-
const current = counts.get(key) || { provider, model, count: 0 };
|
|
572
|
-
current.count += Number(item.count || 0);
|
|
573
|
-
counts.set(key, current);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
return [...counts.values()].sort((a, b) => b.count - a.count || modelLabel(a).localeCompare(modelLabel(b)));
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function modelLabel(model) {
|
|
580
|
-
return `${model.provider || '(unknown)'}/${model.model || '(unknown)'} x${model.count}`;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function isFreeModelRoute(provider, model) {
|
|
584
|
-
const route = `${provider}/${model}`.toLowerCase();
|
|
585
|
-
return route.includes(':free') ||
|
|
586
|
-
route.includes('/big-pickle') ||
|
|
587
|
-
route.includes('minimax-m2.5-free') ||
|
|
588
|
-
route.includes('glm-4.5-air') ||
|
|
589
|
-
route.includes('gpt-oss-20b') ||
|
|
590
|
-
route.includes('qwen3-next-80b-a3b-instruct:free');
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function requestedJobCount(prompt) {
|
|
594
|
-
const text = String(prompt || '').toLowerCase();
|
|
595
|
-
if (!/\b(job|jobs|application|applications)\b/.test(text)) return null;
|
|
596
|
-
if (!/\b(apply|applt|another|nother|more|process)\b/.test(text)) return null;
|
|
597
|
-
const match = text.match(/\b(\d{1,3})\b/);
|
|
598
|
-
return match ? Number(match[1]) : null;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function firstUrl(text) {
|
|
602
|
-
const match = String(text || '').match(/https?:\/\/[^\s)>\]]+/i);
|
|
603
|
-
return match ? match[0].replace(/[.,;]+$/, '') : '';
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function duplicateTaskUrlCount(taskCalls) {
|
|
607
|
-
const seen = new Set();
|
|
608
|
-
const duplicates = new Set();
|
|
609
|
-
for (const task of taskCalls) {
|
|
610
|
-
if (!task.url || task.isStatusPoll) continue;
|
|
611
|
-
if (seen.has(task.url)) duplicates.add(task.url);
|
|
612
|
-
seen.add(task.url);
|
|
613
|
-
}
|
|
614
|
-
return duplicates.size;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function mentionsLimitedCandidatePool(text) {
|
|
618
|
-
return /\b(only|just)\s+\d+\s+(candidate|candidates|jobs?|applications?)\b/i.test(text) ||
|
|
619
|
-
/\b(no more|not enough|ran out of|exhausted)\s+(candidate|candidates|jobs?|applications?|pipeline)\b/i.test(text);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
424
|
function printList(items) {
|
|
623
425
|
const rows = items.map((item) => [
|
|
624
426
|
item.id,
|
|
427
|
+
item.harness,
|
|
625
428
|
item.startedAt.replace('T', ' ').replace(/\.\d+Z$/, 'Z'),
|
|
626
429
|
item.status,
|
|
627
430
|
String(item.tasks),
|
|
628
431
|
`${item.outcomes}/${item.children}`,
|
|
629
432
|
String(item.issues + item.providerErrors),
|
|
630
|
-
shorten(item.prompt || '',
|
|
433
|
+
shorten(item.prompt || '', 36),
|
|
631
434
|
]);
|
|
632
|
-
const header = ['session', 'started', 'status', '
|
|
435
|
+
const header = ['session', 'harness', 'started', 'status', 'dispatches', 'outcomes', 'alerts', 'prompt'];
|
|
633
436
|
printTable(header, rows);
|
|
634
437
|
}
|
|
635
438
|
|
|
636
439
|
function printStatus(telemetry) {
|
|
637
|
-
console.log(`project:
|
|
638
|
-
console.log(`session:
|
|
639
|
-
console.log(`
|
|
640
|
-
console.log(`
|
|
641
|
-
console.log(`
|
|
440
|
+
console.log(`project: ${telemetry.projectDir}`);
|
|
441
|
+
console.log(`session: ${telemetry.session.id}`);
|
|
442
|
+
console.log(`harness: ${telemetry.session.harness}`);
|
|
443
|
+
console.log(`status: ${telemetry.status}`);
|
|
444
|
+
console.log(`started: ${telemetry.session.startedAt}`);
|
|
445
|
+
if (telemetry.session.title) console.log(`title: ${telemetry.session.title}`);
|
|
446
|
+
console.log(`prompt: ${shorten(telemetry.prompt || '', 100)}`);
|
|
642
447
|
if (telemetry.userRequests.length > 1 || telemetry.latestRequest?.requestedJobs) {
|
|
643
448
|
const latest = telemetry.latestRequest;
|
|
644
449
|
const requestDetail = latest?.requestedJobs
|
|
645
450
|
? `latest ${latest.taskDispatches}/${latest.requestedJobs} dispatches`
|
|
646
451
|
: `latest ${latest?.taskDispatches ?? 0} dispatches`;
|
|
647
|
-
console.log(`requests:
|
|
452
|
+
console.log(`requests: ${telemetry.userRequests.length} user prompt${telemetry.userRequests.length === 1 ? '' : 's'} (${requestDetail})`);
|
|
648
453
|
}
|
|
649
|
-
console.log(`
|
|
650
|
-
console.log(`children:
|
|
651
|
-
console.log(`tracker:
|
|
652
|
-
console.log(`ledger:
|
|
653
|
-
console.log(`models:
|
|
654
|
-
console.log(`errors:
|
|
655
|
-
console.log(`issues:
|
|
454
|
+
console.log(`dispatches: ${telemetry.tasks.total} (${telemetry.tasks.statusPolls} status-poll, ${telemetry.tasks.running} unresolved)`);
|
|
455
|
+
console.log(`children: ${telemetry.children.withOutcomes}/${telemetry.children.total} with outcomes`);
|
|
456
|
+
console.log(`tracker: ${telemetry.tracker.pending.length} pending TSVs, ${telemetry.tracker.mergedCount} merged TSVs`);
|
|
457
|
+
console.log(`ledger: ${telemetry.tracker.ledger.error ? `error: ${telemetry.tracker.ledger.error}` : telemetry.tracker.ledger.exists ? `${telemetry.tracker.ledger.events} events` : 'missing'}`);
|
|
458
|
+
console.log(`models: ${telemetry.models.slice(0, 3).map(modelLabel).join(', ') || 'none'}`);
|
|
459
|
+
console.log(`errors: ${telemetry.providerErrors.length} root, ${telemetry.children.providerErrors} child provider errors, ${telemetry.children.toolErrors} child tool errors`);
|
|
460
|
+
console.log(`issues: ${telemetry.policyIssues.length}`);
|
|
656
461
|
|
|
657
462
|
if (telemetry.policyIssues.length > 0) {
|
|
658
463
|
console.log('\nissues:');
|
|
@@ -690,20 +495,20 @@ function printStatus(telemetry) {
|
|
|
690
495
|
function printShow(telemetry) {
|
|
691
496
|
printStatus(telemetry);
|
|
692
497
|
if (telemetry.tasks.calls.length > 0) {
|
|
693
|
-
console.log('\
|
|
498
|
+
console.log('\ndispatches:');
|
|
694
499
|
for (const task of telemetry.tasks.calls) {
|
|
695
500
|
const flags = [
|
|
696
501
|
task.isStatusPoll ? 'status-poll' : '',
|
|
697
502
|
task.status && task.status !== 'completed' ? task.status : '',
|
|
698
503
|
task.proxyLeak ? 'proxy-values-detected' : '',
|
|
699
504
|
].filter(Boolean).join(', ');
|
|
700
|
-
console.log(` - ${task.at} ${task.description || '(no description)'} ${task.sessionId || ''} ${task.subagentType || ''}${flags ? ` [${flags}]` : ''}`);
|
|
505
|
+
console.log(` - ${task.at} ${task.name} ${task.description || '(no description)'} ${task.sessionId || ''} ${task.subagentType || ''}${flags ? ` [${flags}]` : ''}`);
|
|
701
506
|
}
|
|
702
507
|
}
|
|
703
508
|
if (telemetry.providerErrors.length > 0) {
|
|
704
509
|
console.log('\nprovider errors:');
|
|
705
510
|
for (const err of telemetry.providerErrors) {
|
|
706
|
-
console.log(` - ${err.at} ${err.provider}/${err.model} ${err.statusCode || ''} ${err.category}: ${err.message}`);
|
|
511
|
+
console.log(` - ${err.at} ${err.provider || '(unknown)'}/${err.model || '(unknown)'} ${err.statusCode || ''} ${err.category}: ${err.message}`);
|
|
707
512
|
}
|
|
708
513
|
}
|
|
709
514
|
}
|
|
@@ -715,72 +520,26 @@ function printTable(header, rows) {
|
|
|
715
520
|
for (const row of rows) console.log(row.map((cell, i) => pad(cell, widths[i])).join(' '));
|
|
716
521
|
}
|
|
717
522
|
|
|
718
|
-
function
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
function stringValue(value) {
|
|
730
|
-
return typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function statusCodeFromText(text) {
|
|
734
|
-
const match = String(text).match(/\b(40[0-9]|42[0-9]|50[0-9])\b/);
|
|
735
|
-
return match ? Number(match[1]) : undefined;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function providerErrorCategory(text, statusCode) {
|
|
739
|
-
if (statusCode === 402 || /insufficient|balance|credits|diem/i.test(text)) return 'balance';
|
|
740
|
-
if (statusCode === 429 || /rate.?limit|quota/i.test(text)) return 'rate-limit';
|
|
741
|
-
if (/overload|temporarily unavailable|timeout/i.test(text)) return 'transient';
|
|
742
|
-
return 'provider-error';
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function hasProxyLeak(text) {
|
|
746
|
-
const raw = String(text || '');
|
|
747
|
-
if (!/proxy/i.test(raw)) return false;
|
|
748
|
-
return /\b(server|username|password|bypass)["']?\s*[:=]\s*["']?[^"',\s)}]+/i.test(raw) ||
|
|
749
|
-
/brd-customer|superproxy|oxylabs|smartproxy|soax/i.test(raw);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function redactSecrets(text) {
|
|
753
|
-
return String(text || '')
|
|
754
|
-
.replace(/\b(password|username|server|bypass)["']?\s*[:=]\s*["']?[^"',\s)}]+/gi, '$1=<redacted>')
|
|
755
|
-
.replace(/brd-customer-[A-Za-z0-9_.-]+/g, '<redacted-proxy-user>');
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function relativeToProject(file, projectDir = PROJECT_DIR) {
|
|
759
|
-
return file.startsWith(`${projectDir}/`) ? file.slice(projectDir.length + 1) : file;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function clean(text) {
|
|
763
|
-
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function shorten(value, max) {
|
|
767
|
-
const text = clean(value);
|
|
768
|
-
if (text.length <= max) return text;
|
|
769
|
-
return `${text.slice(0, max - 1)}...`;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function pad(value, width) {
|
|
773
|
-
const text = String(value ?? '');
|
|
774
|
-
return text.length >= width ? text : text + ' '.repeat(width - text.length);
|
|
523
|
+
async function recentTelemetry(opts) {
|
|
524
|
+
const refs = await discoverProjectSessions(opts);
|
|
525
|
+
if (refs.length === 0) return { refs, context: null, telemetry: null };
|
|
526
|
+
const context = loadContext(refs);
|
|
527
|
+
const roots = rootRefs(context.loadedRefs, context.graph);
|
|
528
|
+
const selected = roots[0] || refs[0] || null;
|
|
529
|
+
return {
|
|
530
|
+
refs,
|
|
531
|
+
context,
|
|
532
|
+
telemetry: selected ? analyzeSession(selected, context, opts) : null,
|
|
533
|
+
};
|
|
775
534
|
}
|
|
776
535
|
|
|
777
536
|
async function runWatch(opts) {
|
|
778
537
|
while (true) {
|
|
779
538
|
console.clear();
|
|
780
539
|
console.log(new Date().toISOString());
|
|
781
|
-
const { telemetry } =
|
|
540
|
+
const { telemetry } = await recentTelemetry(opts);
|
|
782
541
|
if (!telemetry) {
|
|
783
|
-
console.log('No recent JobForge
|
|
542
|
+
console.log('No recent JobForge sessions found.');
|
|
784
543
|
} else {
|
|
785
544
|
printStatus(telemetry);
|
|
786
545
|
}
|
|
@@ -804,15 +563,15 @@ async function main() {
|
|
|
804
563
|
console.error(`job-forge telemetry:list: ${opts.error}`);
|
|
805
564
|
return 2;
|
|
806
565
|
}
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
.
|
|
810
|
-
|
|
566
|
+
const refs = await discoverProjectSessions(opts);
|
|
567
|
+
if (refs.length === 0) {
|
|
568
|
+
console.error('job-forge telemetry:list: no recent JobForge sessions found');
|
|
569
|
+
return 2;
|
|
570
|
+
}
|
|
571
|
+
const context = loadContext(refs);
|
|
572
|
+
const items = rootRefs(context.loadedRefs, context.graph).map((ref) => summaryForList(analyzeSession(ref, context, opts)));
|
|
811
573
|
if (opts.json) {
|
|
812
574
|
console.log(JSON.stringify(items, null, 2));
|
|
813
|
-
} else if (items.length === 0) {
|
|
814
|
-
console.error('job-forge telemetry:list: no recent JobForge OpenCode sessions found');
|
|
815
|
-
return 2;
|
|
816
575
|
} else {
|
|
817
576
|
printList(items);
|
|
818
577
|
}
|
|
@@ -829,9 +588,9 @@ async function main() {
|
|
|
829
588
|
console.error(`job-forge telemetry:status: ${opts.error}`);
|
|
830
589
|
return 2;
|
|
831
590
|
}
|
|
832
|
-
const { telemetry } =
|
|
591
|
+
const { telemetry } = await recentTelemetry(opts);
|
|
833
592
|
if (!telemetry) {
|
|
834
|
-
console.error('job-forge telemetry:status: no recent JobForge
|
|
593
|
+
console.error('job-forge telemetry:status: no recent JobForge sessions found');
|
|
835
594
|
return 2;
|
|
836
595
|
}
|
|
837
596
|
if (opts.json) console.log(JSON.stringify(telemetry, null, 2));
|
|
@@ -853,13 +612,18 @@ async function main() {
|
|
|
853
612
|
console.error('job-forge telemetry:show: missing <id-or-prefix>');
|
|
854
613
|
return 2;
|
|
855
614
|
}
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
if (!
|
|
615
|
+
const refs = await discoverProjectSessions({ ...opts, since: undefined });
|
|
616
|
+
const sessionRef = findObservedSession(refs, positional[0]);
|
|
617
|
+
if (!sessionRef) {
|
|
859
618
|
console.error(`job-forge telemetry:show: no session matches "${positional[0]}"`);
|
|
860
619
|
return 2;
|
|
861
620
|
}
|
|
862
|
-
const
|
|
621
|
+
const context = loadContext(refs);
|
|
622
|
+
if (!context.sessionsById.has(sessionRef.id)) {
|
|
623
|
+
console.error(`job-forge telemetry:show: could not load session "${sessionRef.id}": ${context.loadErrors.get(sessionRef.id) || 'unknown parse error'}`);
|
|
624
|
+
return 2;
|
|
625
|
+
}
|
|
626
|
+
const telemetry = analyzeSession(sessionRef, context, opts);
|
|
863
627
|
if (opts.json) console.log(JSON.stringify(telemetry, null, 2));
|
|
864
628
|
else printShow(telemetry);
|
|
865
629
|
return 0;
|