@dotsetlabs/dotclaw 2.6.0 → 2.6.2
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/config-examples/runtime.json +2 -1
- package/container/agent-runner/package-lock.json +2 -2
- package/container/agent-runner/package.json +1 -1
- package/container/agent-runner/src/index.ts +43 -28
- package/container/agent-runner/src/memory.ts +106 -6
- package/container/agent-runner/src/openrouter-followup.ts +87 -0
- package/container/agent-runner/src/openrouter-input.ts +310 -17
- package/dist/agent-context.d.ts.map +1 -1
- package/dist/agent-context.js +44 -8
- package/dist/agent-context.js.map +1 -1
- package/dist/runtime-config.d.ts +1 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +3 -1
- package/dist/runtime-config.js.map +1 -1
- package/package.json +3 -1
- package/scripts/preflight-prod-chat.js +304 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
|
|
9
|
+
function usage() {
|
|
10
|
+
console.log([
|
|
11
|
+
'Usage:',
|
|
12
|
+
' node scripts/preflight-prod-chat.js --chat <chat_jid> [options]',
|
|
13
|
+
'',
|
|
14
|
+
'Options:',
|
|
15
|
+
' --chat <jid> Required (example: discord:1469421941294108713)',
|
|
16
|
+
' --dotclaw-home <path> Defaults to DOTCLAW_HOME or ~/.dotclaw',
|
|
17
|
+
' --start-iso <iso8601> Defaults to script start time',
|
|
18
|
+
' --timeout-sec <n> Defaults to 180',
|
|
19
|
+
' --poll-ms <n> Defaults to 1000',
|
|
20
|
+
' --require-completed <n> Defaults to 1',
|
|
21
|
+
' --max-processing-age-sec <n> Defaults to 120',
|
|
22
|
+
' --allow-failed Don\'t fail on failed queue rows',
|
|
23
|
+
' --allow-error-traces Don\'t fail when trace rows contain error_code',
|
|
24
|
+
' --no-require-success-trace Don\'t require at least one successful trace row',
|
|
25
|
+
' --no-require-trace Don\'t require at least one trace row',
|
|
26
|
+
' --help Show this help'
|
|
27
|
+
].join('\n'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const parsed = {
|
|
32
|
+
chat: '',
|
|
33
|
+
dotclawHome: process.env.DOTCLAW_HOME || path.join(os.homedir(), '.dotclaw'),
|
|
34
|
+
startIso: new Date().toISOString(),
|
|
35
|
+
timeoutSec: 180,
|
|
36
|
+
pollMs: 1000,
|
|
37
|
+
requireCompleted: 1,
|
|
38
|
+
maxProcessingAgeSec: 120,
|
|
39
|
+
allowFailed: false,
|
|
40
|
+
allowErrorTraces: false,
|
|
41
|
+
requireSuccessfulTrace: true,
|
|
42
|
+
requireTrace: true
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
46
|
+
const arg = argv[i];
|
|
47
|
+
if (arg === '--help' || arg === '-h') {
|
|
48
|
+
parsed.help = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg === '--allow-failed') {
|
|
52
|
+
parsed.allowFailed = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === '--allow-error-traces') {
|
|
56
|
+
parsed.allowErrorTraces = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg === '--no-require-success-trace') {
|
|
60
|
+
parsed.requireSuccessfulTrace = false;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg === '--no-require-trace') {
|
|
64
|
+
parsed.requireTrace = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const next = argv[i + 1];
|
|
68
|
+
if (!next || next.startsWith('--')) {
|
|
69
|
+
throw new Error(`Missing value for ${arg}`);
|
|
70
|
+
}
|
|
71
|
+
if (arg === '--chat') parsed.chat = next;
|
|
72
|
+
else if (arg === '--dotclaw-home') parsed.dotclawHome = next;
|
|
73
|
+
else if (arg === '--start-iso') parsed.startIso = next;
|
|
74
|
+
else if (arg === '--timeout-sec') parsed.timeoutSec = Number(next);
|
|
75
|
+
else if (arg === '--poll-ms') parsed.pollMs = Number(next);
|
|
76
|
+
else if (arg === '--require-completed') parsed.requireCompleted = Number(next);
|
|
77
|
+
else if (arg === '--max-processing-age-sec') parsed.maxProcessingAgeSec = Number(next);
|
|
78
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
79
|
+
i += 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function clampInt(value, fallback, min) {
|
|
86
|
+
if (!Number.isFinite(value)) return fallback;
|
|
87
|
+
return Math.max(min, Math.floor(value));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseIsoOrNow(value) {
|
|
91
|
+
const ts = Date.parse(value);
|
|
92
|
+
if (!Number.isFinite(ts)) return new Date().toISOString();
|
|
93
|
+
return new Date(ts).toISOString();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listTraceFiles(traceDir, startIso) {
|
|
97
|
+
if (!fs.existsSync(traceDir)) return [];
|
|
98
|
+
const startDate = startIso.slice(0, 10);
|
|
99
|
+
return fs.readdirSync(traceDir)
|
|
100
|
+
.filter((name) => /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/.test(name))
|
|
101
|
+
.filter((name) => {
|
|
102
|
+
const datePart = name.slice(6, 16);
|
|
103
|
+
return datePart >= startDate;
|
|
104
|
+
})
|
|
105
|
+
.sort()
|
|
106
|
+
.map((name) => path.join(traceDir, name));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function summarizeTraceRows({ traceDir, chatJid, startIso }) {
|
|
110
|
+
const startMs = Date.parse(startIso);
|
|
111
|
+
const stats = {
|
|
112
|
+
total: 0,
|
|
113
|
+
success: 0,
|
|
114
|
+
error: 0,
|
|
115
|
+
latestError: ''
|
|
116
|
+
};
|
|
117
|
+
for (const filePath of listTraceFiles(traceDir, startIso)) {
|
|
118
|
+
let content = '';
|
|
119
|
+
try {
|
|
120
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
121
|
+
} catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const line of content.split('\n')) {
|
|
125
|
+
if (!line.trim()) continue;
|
|
126
|
+
try {
|
|
127
|
+
const row = JSON.parse(line);
|
|
128
|
+
if (row?.chat_id !== chatJid) continue;
|
|
129
|
+
const ts = Date.parse(String(row.timestamp || ''));
|
|
130
|
+
if (!Number.isFinite(ts) || ts < startMs) continue;
|
|
131
|
+
stats.total += 1;
|
|
132
|
+
const errorCode = typeof row.error_code === 'string' ? row.error_code.trim() : '';
|
|
133
|
+
if (errorCode) {
|
|
134
|
+
stats.error += 1;
|
|
135
|
+
stats.latestError = errorCode;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (typeof row.output_text === 'string' && row.output_text.trim()) {
|
|
139
|
+
stats.success += 1;
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore malformed trace rows
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return stats;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getQueueRows(db, chatJid, startIso) {
|
|
150
|
+
return db.prepare(`
|
|
151
|
+
SELECT id, message_id, status, created_at, started_at, completed_at, error
|
|
152
|
+
FROM message_queue
|
|
153
|
+
WHERE chat_jid = ?
|
|
154
|
+
AND created_at >= ?
|
|
155
|
+
ORDER BY id DESC
|
|
156
|
+
LIMIT 200
|
|
157
|
+
`).all(chatJid, startIso);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function summarizeRows(rows, nowMs, maxProcessingAgeSec) {
|
|
161
|
+
const counts = {
|
|
162
|
+
pending: 0,
|
|
163
|
+
processing: 0,
|
|
164
|
+
completed: 0,
|
|
165
|
+
failed: 0
|
|
166
|
+
};
|
|
167
|
+
const staleProcessing = [];
|
|
168
|
+
for (const row of rows) {
|
|
169
|
+
const status = String(row.status || '');
|
|
170
|
+
if (status === 'pending' || status === 'processing' || status === 'completed' || status === 'failed') {
|
|
171
|
+
counts[status] += 1;
|
|
172
|
+
}
|
|
173
|
+
if (status === 'processing') {
|
|
174
|
+
const startedMs = Date.parse(String(row.started_at || row.created_at || ''));
|
|
175
|
+
if (Number.isFinite(startedMs)) {
|
|
176
|
+
const ageSec = (nowMs - startedMs) / 1000;
|
|
177
|
+
if (ageSec > maxProcessingAgeSec) {
|
|
178
|
+
staleProcessing.push({ id: row.id, ageSec, messageId: row.message_id });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { counts, staleProcessing };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sleep(ms) {
|
|
187
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function main() {
|
|
191
|
+
let args;
|
|
192
|
+
try {
|
|
193
|
+
args = parseArgs(process.argv.slice(2));
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error(`Argument error: ${err instanceof Error ? err.message : String(err)}`);
|
|
196
|
+
usage();
|
|
197
|
+
process.exit(2);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (args.help) {
|
|
201
|
+
usage();
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!args.chat || !String(args.chat).trim()) {
|
|
206
|
+
console.error('Missing required argument: --chat <chat_jid>');
|
|
207
|
+
usage();
|
|
208
|
+
process.exit(2);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const chatJid = String(args.chat).trim();
|
|
212
|
+
const dotclawHome = path.resolve(String(args.dotclawHome || '.'));
|
|
213
|
+
const startIso = parseIsoOrNow(String(args.startIso || ''));
|
|
214
|
+
const timeoutSec = clampInt(args.timeoutSec, 180, 1);
|
|
215
|
+
const pollMs = clampInt(args.pollMs, 1000, 50);
|
|
216
|
+
const requireCompleted = clampInt(args.requireCompleted, 1, 1);
|
|
217
|
+
const maxProcessingAgeSec = clampInt(args.maxProcessingAgeSec, 120, 1);
|
|
218
|
+
const allowFailed = !!args.allowFailed;
|
|
219
|
+
const allowErrorTraces = !!args.allowErrorTraces;
|
|
220
|
+
const requireSuccessfulTrace = !!args.requireSuccessfulTrace;
|
|
221
|
+
const requireTrace = !!args.requireTrace;
|
|
222
|
+
|
|
223
|
+
const dbPath = path.join(dotclawHome, 'data', 'store', 'messages.db');
|
|
224
|
+
const traceDir = path.join(dotclawHome, 'traces');
|
|
225
|
+
if (!fs.existsSync(dbPath)) {
|
|
226
|
+
console.error(`messages.db not found: ${dbPath}`);
|
|
227
|
+
process.exit(2);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
231
|
+
const startedAt = Date.now();
|
|
232
|
+
const deadline = startedAt + (timeoutSec * 1000);
|
|
233
|
+
let lastSignature = '';
|
|
234
|
+
|
|
235
|
+
console.log(`[preflight] chat=${chatJid}`);
|
|
236
|
+
console.log(`[preflight] dotclawHome=${dotclawHome}`);
|
|
237
|
+
console.log(`[preflight] startIso=${startIso}`);
|
|
238
|
+
console.log(`[preflight] timeoutSec=${timeoutSec}, pollMs=${pollMs}, requireCompleted=${requireCompleted}, maxProcessingAgeSec=${maxProcessingAgeSec}, requireTrace=${requireTrace}, requireSuccessfulTrace=${requireSuccessfulTrace}, allowFailed=${allowFailed}, allowErrorTraces=${allowErrorTraces}`);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
while (Date.now() <= deadline) {
|
|
242
|
+
const nowMs = Date.now();
|
|
243
|
+
const rows = getQueueRows(db, chatJid, startIso);
|
|
244
|
+
const { counts, staleProcessing } = summarizeRows(rows, nowMs, maxProcessingAgeSec);
|
|
245
|
+
const traceStats = (requireTrace || !allowErrorTraces)
|
|
246
|
+
? summarizeTraceRows({ traceDir, chatJid, startIso })
|
|
247
|
+
: { total: 0, success: 0, error: 0, latestError: '' };
|
|
248
|
+
|
|
249
|
+
const signature = `${counts.pending}/${counts.processing}/${counts.completed}/${counts.failed}|stale:${staleProcessing.length}|trace:${traceStats.total}/${traceStats.success}/${traceStats.error}`;
|
|
250
|
+
if (signature !== lastSignature) {
|
|
251
|
+
const traceSummary = requireTrace
|
|
252
|
+
? ` traces(total/success/error)=${traceStats.total}/${traceStats.success}/${traceStats.error}`
|
|
253
|
+
: '';
|
|
254
|
+
console.log(`[preflight] queue pending=${counts.pending} processing=${counts.processing} completed=${counts.completed} failed=${counts.failed} staleProcessing=${staleProcessing.length}${traceSummary}`);
|
|
255
|
+
if (rows[0]) {
|
|
256
|
+
const latest = rows[0];
|
|
257
|
+
console.log(`[preflight] latest id=${latest.id} status=${latest.status} message_id=${latest.message_id || 'n/a'} error=${latest.error || 'none'}`);
|
|
258
|
+
}
|
|
259
|
+
lastSignature = signature;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!allowFailed && counts.failed > 0) {
|
|
263
|
+
console.error('[preflight] FAIL: detected failed queue rows.');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (staleProcessing.length > 0) {
|
|
267
|
+
console.error('[preflight] FAIL: detected stale processing rows.');
|
|
268
|
+
for (const row of staleProcessing.slice(0, 5)) {
|
|
269
|
+
console.error(`[preflight] stale id=${row.id} message_id=${row.messageId || 'n/a'} ageSec=${Math.floor(row.ageSec)}`);
|
|
270
|
+
}
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!allowErrorTraces && traceStats.error > 0) {
|
|
275
|
+
console.error('[preflight] FAIL: detected trace rows with error_code.');
|
|
276
|
+
if (traceStats.latestError) {
|
|
277
|
+
console.error(`[preflight] latest trace error: ${traceStats.latestError}`);
|
|
278
|
+
}
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const completedEnough = counts.completed >= requireCompleted;
|
|
283
|
+
const traceEnough = !requireTrace || traceStats.total > 0;
|
|
284
|
+
const successTraceEnough = !requireTrace || !requireSuccessfulTrace || traceStats.success > 0;
|
|
285
|
+
if (completedEnough && traceEnough) {
|
|
286
|
+
if (!successTraceEnough) {
|
|
287
|
+
await sleep(pollMs);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
console.log('[preflight] PASS: gate conditions satisfied.');
|
|
291
|
+
process.exit(0);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await sleep(pollMs);
|
|
295
|
+
}
|
|
296
|
+
} finally {
|
|
297
|
+
db.close();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.error('[preflight] FAIL: timed out waiting for pass conditions.');
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
void main();
|