@calltelemetry/openclaw-linear 0.5.2 → 0.6.0
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/README.md +359 -195
- package/index.ts +10 -10
- package/openclaw.plugin.json +4 -1
- package/package.json +9 -2
- package/src/agent/agent.test.ts +127 -0
- package/src/{agent.ts → agent/agent.ts} +84 -7
- package/src/agent/watchdog.test.ts +266 -0
- package/src/agent/watchdog.ts +176 -0
- package/src/{cli.ts → infra/cli.ts} +5 -5
- package/src/{codex-worktree.ts → infra/codex-worktree.ts} +1 -1
- package/src/infra/notify.test.ts +169 -0
- package/src/{notify.ts → infra/notify.ts} +6 -1
- package/src/pipeline/active-session.test.ts +154 -0
- package/src/pipeline/artifacts.test.ts +383 -0
- package/src/{artifacts.ts → pipeline/artifacts.ts} +9 -1
- package/src/{dispatch-service.ts → pipeline/dispatch-service.ts} +1 -1
- package/src/pipeline/dispatch-state.test.ts +382 -0
- package/src/pipeline/pipeline.test.ts +226 -0
- package/src/{pipeline.ts → pipeline/pipeline.ts} +61 -7
- package/src/{tier-assess.ts → pipeline/tier-assess.ts} +1 -1
- package/src/{webhook.test.ts → pipeline/webhook.test.ts} +1 -1
- package/src/{webhook.ts → pipeline/webhook.ts} +8 -8
- package/src/{claude-tool.ts → tools/claude-tool.ts} +31 -5
- package/src/{cli-shared.ts → tools/cli-shared.ts} +5 -4
- package/src/{code-tool.ts → tools/code-tool.ts} +2 -2
- package/src/{codex-tool.ts → tools/codex-tool.ts} +31 -5
- package/src/{gemini-tool.ts → tools/gemini-tool.ts} +31 -5
- package/src/{orchestration-tools.ts → tools/orchestration-tools.ts} +1 -1
- package/src/client.ts +0 -94
- /package/src/{auth.ts → api/auth.ts} +0 -0
- /package/src/{linear-api.ts → api/linear-api.ts} +0 -0
- /package/src/{oauth-callback.ts → api/oauth-callback.ts} +0 -0
- /package/src/{active-session.ts → pipeline/active-session.ts} +0 -0
- /package/src/{dispatch-state.ts → pipeline/dispatch-state.ts} +0 -0
- /package/src/{tools.ts → tools/tools.ts} +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
// Mock pipeline.js to break the import chain: artifacts → pipeline → agent → extensionAPI
|
|
7
|
+
vi.mock("./pipeline.js", () => ({}));
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ensureClawDir,
|
|
11
|
+
ensureGitignore,
|
|
12
|
+
writeManifest,
|
|
13
|
+
readManifest,
|
|
14
|
+
updateManifest,
|
|
15
|
+
saveWorkerOutput,
|
|
16
|
+
savePlan,
|
|
17
|
+
saveAuditVerdict,
|
|
18
|
+
appendLog,
|
|
19
|
+
writeSummary,
|
|
20
|
+
buildSummaryFromArtifacts,
|
|
21
|
+
writeDispatchMemory,
|
|
22
|
+
resolveOrchestratorWorkspace,
|
|
23
|
+
type ClawManifest,
|
|
24
|
+
type LogEntry,
|
|
25
|
+
} from "./artifacts.js";
|
|
26
|
+
|
|
27
|
+
function makeTmpDir(): string {
|
|
28
|
+
return mkdtempSync(join(tmpdir(), "claw-test-"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeManifest(overrides?: Partial<ClawManifest>): ClawManifest {
|
|
32
|
+
return {
|
|
33
|
+
issueIdentifier: "API-100",
|
|
34
|
+
issueTitle: "Fix login bug",
|
|
35
|
+
issueId: "id-123",
|
|
36
|
+
tier: "junior",
|
|
37
|
+
model: "test-model",
|
|
38
|
+
dispatchedAt: "2026-01-01T00:00:00Z",
|
|
39
|
+
worktreePath: "/tmp/test",
|
|
40
|
+
branch: "codex/API-100",
|
|
41
|
+
attempts: 0,
|
|
42
|
+
status: "dispatched",
|
|
43
|
+
plugin: "linear",
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// ensureClawDir
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
describe("ensureClawDir", () => {
|
|
53
|
+
it("creates .claw/ directory and returns path", () => {
|
|
54
|
+
const tmp = makeTmpDir();
|
|
55
|
+
const result = ensureClawDir(tmp);
|
|
56
|
+
expect(result).toBe(join(tmp, ".claw"));
|
|
57
|
+
expect(existsSync(result)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("is idempotent", () => {
|
|
61
|
+
const tmp = makeTmpDir();
|
|
62
|
+
const first = ensureClawDir(tmp);
|
|
63
|
+
const second = ensureClawDir(tmp);
|
|
64
|
+
expect(first).toBe(second);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// ensureGitignore
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
describe("ensureGitignore", () => {
|
|
73
|
+
it("creates .gitignore with .claw/ entry", () => {
|
|
74
|
+
const tmp = makeTmpDir();
|
|
75
|
+
ensureGitignore(tmp);
|
|
76
|
+
const content = readFileSync(join(tmp, ".gitignore"), "utf-8");
|
|
77
|
+
expect(content).toContain(".claw/");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("appends to existing .gitignore", () => {
|
|
81
|
+
const tmp = makeTmpDir();
|
|
82
|
+
writeFileSync(join(tmp, ".gitignore"), "node_modules/\n", "utf-8");
|
|
83
|
+
ensureGitignore(tmp);
|
|
84
|
+
const content = readFileSync(join(tmp, ".gitignore"), "utf-8");
|
|
85
|
+
expect(content).toContain("node_modules/");
|
|
86
|
+
expect(content).toContain(".claw/");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("does not duplicate entry", () => {
|
|
90
|
+
const tmp = makeTmpDir();
|
|
91
|
+
ensureGitignore(tmp);
|
|
92
|
+
ensureGitignore(tmp);
|
|
93
|
+
const content = readFileSync(join(tmp, ".gitignore"), "utf-8");
|
|
94
|
+
const matches = content.match(/\.claw\//g);
|
|
95
|
+
expect(matches).toHaveLength(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("handles file without trailing newline", () => {
|
|
99
|
+
const tmp = makeTmpDir();
|
|
100
|
+
writeFileSync(join(tmp, ".gitignore"), "node_modules/", "utf-8"); // no trailing \n
|
|
101
|
+
ensureGitignore(tmp);
|
|
102
|
+
const content = readFileSync(join(tmp, ".gitignore"), "utf-8");
|
|
103
|
+
expect(content).toBe("node_modules/\n.claw/\n");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Manifest CRUD
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
describe("manifest", () => {
|
|
112
|
+
it("write + read round-trip", () => {
|
|
113
|
+
const tmp = makeTmpDir();
|
|
114
|
+
const manifest = makeManifest({ worktreePath: tmp });
|
|
115
|
+
writeManifest(tmp, manifest);
|
|
116
|
+
const read = readManifest(tmp);
|
|
117
|
+
expect(read).toEqual(manifest);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("readManifest returns null when missing", () => {
|
|
121
|
+
const tmp = makeTmpDir();
|
|
122
|
+
expect(readManifest(tmp)).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("updateManifest merges partial updates", () => {
|
|
126
|
+
const tmp = makeTmpDir();
|
|
127
|
+
writeManifest(tmp, makeManifest({ worktreePath: tmp }));
|
|
128
|
+
updateManifest(tmp, { status: "working", attempts: 1 });
|
|
129
|
+
const read = readManifest(tmp);
|
|
130
|
+
expect(read!.status).toBe("working");
|
|
131
|
+
expect(read!.attempts).toBe(1);
|
|
132
|
+
expect(read!.issueIdentifier).toBe("API-100"); // preserved
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("updateManifest no-op when no manifest", () => {
|
|
136
|
+
const tmp = makeTmpDir();
|
|
137
|
+
// Should not throw
|
|
138
|
+
updateManifest(tmp, { status: "done" });
|
|
139
|
+
expect(readManifest(tmp)).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Phase artifacts
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
describe("saveWorkerOutput", () => {
|
|
148
|
+
it("writes worker-{N}.md", () => {
|
|
149
|
+
const tmp = makeTmpDir();
|
|
150
|
+
saveWorkerOutput(tmp, 0, "hello world");
|
|
151
|
+
const content = readFileSync(join(tmp, ".claw", "worker-0.md"), "utf-8");
|
|
152
|
+
expect(content).toBe("hello world");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("truncates at 8192 bytes", () => {
|
|
156
|
+
const tmp = makeTmpDir();
|
|
157
|
+
const longOutput = "x".repeat(10000);
|
|
158
|
+
saveWorkerOutput(tmp, 1, longOutput);
|
|
159
|
+
const content = readFileSync(join(tmp, ".claw", "worker-1.md"), "utf-8");
|
|
160
|
+
expect(content.length).toBeLessThan(10000);
|
|
161
|
+
expect(content).toContain("--- truncated ---");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("does not truncate under limit", () => {
|
|
165
|
+
const tmp = makeTmpDir();
|
|
166
|
+
const shortOutput = "y".repeat(100);
|
|
167
|
+
saveWorkerOutput(tmp, 2, shortOutput);
|
|
168
|
+
const content = readFileSync(join(tmp, ".claw", "worker-2.md"), "utf-8");
|
|
169
|
+
expect(content).toBe(shortOutput);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("savePlan", () => {
|
|
174
|
+
it("writes plan.md", () => {
|
|
175
|
+
const tmp = makeTmpDir();
|
|
176
|
+
savePlan(tmp, "# My Plan\n\nStep 1...");
|
|
177
|
+
const content = readFileSync(join(tmp, ".claw", "plan.md"), "utf-8");
|
|
178
|
+
expect(content).toBe("# My Plan\n\nStep 1...");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("saveAuditVerdict", () => {
|
|
183
|
+
it("writes audit-{N}.json with correct JSON", () => {
|
|
184
|
+
const tmp = makeTmpDir();
|
|
185
|
+
const verdict = { pass: true, criteria: ["tests pass"], gaps: [], testResults: "all green" };
|
|
186
|
+
saveAuditVerdict(tmp, 0, verdict);
|
|
187
|
+
const raw = readFileSync(join(tmp, ".claw", "audit-0.json"), "utf-8");
|
|
188
|
+
expect(JSON.parse(raw)).toEqual(verdict);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Interaction log
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
describe("appendLog", () => {
|
|
197
|
+
function makeEntry(overrides?: Partial<LogEntry>): LogEntry {
|
|
198
|
+
return {
|
|
199
|
+
ts: "2026-01-01T00:00:00Z",
|
|
200
|
+
phase: "worker",
|
|
201
|
+
attempt: 0,
|
|
202
|
+
agent: "zoe",
|
|
203
|
+
prompt: "do the thing",
|
|
204
|
+
outputPreview: "done",
|
|
205
|
+
success: true,
|
|
206
|
+
...overrides,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
it("creates log.jsonl and writes entry", () => {
|
|
211
|
+
const tmp = makeTmpDir();
|
|
212
|
+
appendLog(tmp, makeEntry());
|
|
213
|
+
const content = readFileSync(join(tmp, ".claw", "log.jsonl"), "utf-8");
|
|
214
|
+
const parsed = JSON.parse(content.trim());
|
|
215
|
+
expect(parsed.phase).toBe("worker");
|
|
216
|
+
expect(parsed.success).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("appends multiple entries as JSONL", () => {
|
|
220
|
+
const tmp = makeTmpDir();
|
|
221
|
+
appendLog(tmp, makeEntry({ attempt: 0 }));
|
|
222
|
+
appendLog(tmp, makeEntry({ attempt: 1, phase: "audit" }));
|
|
223
|
+
const lines = readFileSync(join(tmp, ".claw", "log.jsonl"), "utf-8")
|
|
224
|
+
.trim()
|
|
225
|
+
.split("\n");
|
|
226
|
+
expect(lines).toHaveLength(2);
|
|
227
|
+
expect(JSON.parse(lines[1]).phase).toBe("audit");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("truncates prompt to 200 chars", () => {
|
|
231
|
+
const tmp = makeTmpDir();
|
|
232
|
+
appendLog(tmp, makeEntry({ prompt: "x".repeat(500) }));
|
|
233
|
+
const content = readFileSync(join(tmp, ".claw", "log.jsonl"), "utf-8");
|
|
234
|
+
const parsed = JSON.parse(content.trim());
|
|
235
|
+
expect(parsed.prompt.length).toBe(200);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("truncates outputPreview to 500 chars", () => {
|
|
239
|
+
const tmp = makeTmpDir();
|
|
240
|
+
appendLog(tmp, makeEntry({ outputPreview: "y".repeat(1000) }));
|
|
241
|
+
const content = readFileSync(join(tmp, ".claw", "log.jsonl"), "utf-8");
|
|
242
|
+
const parsed = JSON.parse(content.trim());
|
|
243
|
+
expect(parsed.outputPreview.length).toBe(500);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("preserves watchdog detail when phase=watchdog", () => {
|
|
247
|
+
const tmp = makeTmpDir();
|
|
248
|
+
appendLog(tmp, makeEntry({
|
|
249
|
+
phase: "watchdog",
|
|
250
|
+
watchdog: { reason: "inactivity", silenceSec: 120, thresholdSec: 120, retried: true },
|
|
251
|
+
}));
|
|
252
|
+
const content = readFileSync(join(tmp, ".claw", "log.jsonl"), "utf-8");
|
|
253
|
+
const parsed = JSON.parse(content.trim());
|
|
254
|
+
expect(parsed.phase).toBe("watchdog");
|
|
255
|
+
expect(parsed.watchdog).toEqual({
|
|
256
|
+
reason: "inactivity",
|
|
257
|
+
silenceSec: 120,
|
|
258
|
+
thresholdSec: 120,
|
|
259
|
+
retried: true,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("omits watchdog field when undefined", () => {
|
|
264
|
+
const tmp = makeTmpDir();
|
|
265
|
+
appendLog(tmp, makeEntry());
|
|
266
|
+
const content = readFileSync(join(tmp, ".claw", "log.jsonl"), "utf-8");
|
|
267
|
+
const parsed = JSON.parse(content.trim());
|
|
268
|
+
expect(parsed.watchdog).toBeUndefined();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Summary
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
describe("writeSummary", () => {
|
|
277
|
+
it("writes summary.md", () => {
|
|
278
|
+
const tmp = makeTmpDir();
|
|
279
|
+
writeSummary(tmp, "# Summary\nAll done.");
|
|
280
|
+
const content = readFileSync(join(tmp, ".claw", "summary.md"), "utf-8");
|
|
281
|
+
expect(content).toBe("# Summary\nAll done.");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("buildSummaryFromArtifacts", () => {
|
|
286
|
+
it("returns null with no manifest", () => {
|
|
287
|
+
const tmp = makeTmpDir();
|
|
288
|
+
expect(buildSummaryFromArtifacts(tmp)).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("builds markdown with header", () => {
|
|
292
|
+
const tmp = makeTmpDir();
|
|
293
|
+
writeManifest(tmp, makeManifest({ worktreePath: tmp }));
|
|
294
|
+
const summary = buildSummaryFromArtifacts(tmp)!;
|
|
295
|
+
expect(summary).toContain("# Dispatch: API-100");
|
|
296
|
+
expect(summary).toContain("Fix login bug");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("includes plan section when plan exists", () => {
|
|
300
|
+
const tmp = makeTmpDir();
|
|
301
|
+
writeManifest(tmp, makeManifest({ worktreePath: tmp }));
|
|
302
|
+
savePlan(tmp, "Step 1: do stuff");
|
|
303
|
+
const summary = buildSummaryFromArtifacts(tmp)!;
|
|
304
|
+
expect(summary).toContain("## Plan");
|
|
305
|
+
expect(summary).toContain("Step 1: do stuff");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("includes worker+audit per attempt", () => {
|
|
309
|
+
const tmp = makeTmpDir();
|
|
310
|
+
writeManifest(tmp, makeManifest({ worktreePath: tmp, attempts: 2 }));
|
|
311
|
+
saveWorkerOutput(tmp, 0, "attempt 0 output");
|
|
312
|
+
saveAuditVerdict(tmp, 0, { pass: false, criteria: ["c1"], gaps: ["g1"], testResults: "fail" });
|
|
313
|
+
saveWorkerOutput(tmp, 1, "attempt 1 output");
|
|
314
|
+
saveAuditVerdict(tmp, 1, { pass: true, criteria: ["c2"], gaps: [], testResults: "pass" });
|
|
315
|
+
|
|
316
|
+
const summary = buildSummaryFromArtifacts(tmp)!;
|
|
317
|
+
expect(summary).toContain("## Attempt 0");
|
|
318
|
+
expect(summary).toContain("attempt 0 output");
|
|
319
|
+
expect(summary).toContain("FAIL");
|
|
320
|
+
expect(summary).toContain("g1");
|
|
321
|
+
expect(summary).toContain("## Attempt 1");
|
|
322
|
+
expect(summary).toContain("attempt 1 output");
|
|
323
|
+
expect(summary).toContain("PASS");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("handles missing artifact files gracefully", () => {
|
|
327
|
+
const tmp = makeTmpDir();
|
|
328
|
+
writeManifest(tmp, makeManifest({ worktreePath: tmp, attempts: 1 }));
|
|
329
|
+
// No worker or audit files — should not throw
|
|
330
|
+
const summary = buildSummaryFromArtifacts(tmp)!;
|
|
331
|
+
expect(summary).toContain("## Attempt 0");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Memory
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
describe("writeDispatchMemory", () => {
|
|
340
|
+
it("creates memory/ dir and writes file", () => {
|
|
341
|
+
const tmp = makeTmpDir();
|
|
342
|
+
writeDispatchMemory("API-100", "summary content", tmp);
|
|
343
|
+
const content = readFileSync(join(tmp, "memory", "dispatch-API-100.md"), "utf-8");
|
|
344
|
+
expect(content).toBe("summary content");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("overwrites on second call", () => {
|
|
348
|
+
const tmp = makeTmpDir();
|
|
349
|
+
writeDispatchMemory("API-100", "first", tmp);
|
|
350
|
+
writeDispatchMemory("API-100", "second", tmp);
|
|
351
|
+
const content = readFileSync(join(tmp, "memory", "dispatch-API-100.md"), "utf-8");
|
|
352
|
+
expect(content).toBe("second");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Orchestrator workspace
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
describe("resolveOrchestratorWorkspace", () => {
|
|
361
|
+
it("falls back to default on error", () => {
|
|
362
|
+
const api = { runtime: { config: { loadConfig: () => { throw new Error("no config"); } } } };
|
|
363
|
+
const result = resolveOrchestratorWorkspace(api);
|
|
364
|
+
expect(result).toContain(".openclaw");
|
|
365
|
+
expect(result).toContain("workspace");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("returns workspace from config", () => {
|
|
369
|
+
const api = {
|
|
370
|
+
runtime: {
|
|
371
|
+
config: {
|
|
372
|
+
loadConfig: () => ({
|
|
373
|
+
agents: {
|
|
374
|
+
list: [{ id: "default", workspace: "/custom/ws" }],
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
const result = resolveOrchestratorWorkspace(api);
|
|
381
|
+
expect(result).toBe("/custom/ws");
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -45,15 +45,23 @@ export interface ClawManifest {
|
|
|
45
45
|
plugin: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface WatchdogLogDetail {
|
|
49
|
+
reason: "inactivity";
|
|
50
|
+
silenceSec: number;
|
|
51
|
+
thresholdSec: number;
|
|
52
|
+
retried: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
export interface LogEntry {
|
|
49
56
|
ts: string;
|
|
50
|
-
phase: "worker" | "audit" | "verdict" | "dispatch";
|
|
57
|
+
phase: "worker" | "audit" | "verdict" | "dispatch" | "watchdog";
|
|
51
58
|
attempt: number;
|
|
52
59
|
agent: string;
|
|
53
60
|
prompt: string;
|
|
54
61
|
outputPreview: string;
|
|
55
62
|
success: boolean;
|
|
56
63
|
durationMs?: number;
|
|
64
|
+
watchdog?: WatchdogLogDetail;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
// ---------------------------------------------------------------------------
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
removeActiveDispatch,
|
|
23
23
|
pruneCompleted,
|
|
24
24
|
} from "./dispatch-state.js";
|
|
25
|
-
import { getWorktreeStatus } from "
|
|
25
|
+
import { getWorktreeStatus } from "../infra/codex-worktree.js";
|
|
26
26
|
|
|
27
27
|
const INTERVAL_MS = 5 * 60_000; // 5 minutes
|
|
28
28
|
const STALE_THRESHOLD_MS = 2 * 60 * 60_000; // 2 hours
|