@calltelemetry/openclaw-linear 0.5.2 → 0.6.1
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} +32 -5
- package/src/{codex-worktree.ts → infra/codex-worktree.ts} +1 -1
- package/src/infra/doctor.test.ts +399 -0
- package/src/infra/doctor.ts +781 -0
- 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
|
@@ -16,8 +16,8 @@ import { join, dirname } from "node:path";
|
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { parse as parseYaml } from "yaml";
|
|
18
18
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
19
|
-
import type { LinearAgentApi, ActivityContent } from "
|
|
20
|
-
import { runAgent } from "
|
|
19
|
+
import type { LinearAgentApi, ActivityContent } from "../api/linear-api.js";
|
|
20
|
+
import { runAgent } from "../agent/agent.js";
|
|
21
21
|
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
22
22
|
import {
|
|
23
23
|
type Tier,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
readDispatchState,
|
|
34
34
|
getActiveDispatch,
|
|
35
35
|
} from "./dispatch-state.js";
|
|
36
|
-
import { type NotifyFn } from "
|
|
36
|
+
import { type NotifyFn } from "../infra/notify.js";
|
|
37
37
|
import {
|
|
38
38
|
saveWorkerOutput,
|
|
39
39
|
saveAuditVerdict,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
writeDispatchMemory,
|
|
45
45
|
resolveOrchestratorWorkspace,
|
|
46
46
|
} from "./artifacts.js";
|
|
47
|
+
import { resolveWatchdogConfig } from "../agent/watchdog.js";
|
|
47
48
|
|
|
48
49
|
// ---------------------------------------------------------------------------
|
|
49
50
|
// Prompt loading
|
|
@@ -86,7 +87,7 @@ export function loadPrompts(pluginConfig?: Record<string, unknown>): PromptTempl
|
|
|
86
87
|
raw = readFileSync(resolved, "utf-8");
|
|
87
88
|
} else {
|
|
88
89
|
// Load from plugin directory (sidecar file)
|
|
89
|
-
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "
|
|
90
|
+
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
90
91
|
raw = readFileSync(join(pluginRoot, "prompts.yaml"), "utf-8");
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -326,7 +327,9 @@ export async function triggerAudit(
|
|
|
326
327
|
agentId: (pluginConfig?.defaultAgentId as string) ?? "default",
|
|
327
328
|
sessionId: auditSessionId,
|
|
328
329
|
message: `${auditPrompt.system}\n\n${auditPrompt.task}`,
|
|
329
|
-
|
|
330
|
+
streaming: dispatch.agentSessionId
|
|
331
|
+
? { linearApi, agentSessionId: dispatch.agentSessionId }
|
|
332
|
+
: undefined,
|
|
330
333
|
});
|
|
331
334
|
|
|
332
335
|
// runAgent returns inline (embedded runner) — process verdict directly.
|
|
@@ -670,22 +673,73 @@ export async function spawnWorker(
|
|
|
670
673
|
agentId: (pluginConfig?.defaultAgentId as string) ?? "default",
|
|
671
674
|
sessionId: workerSessionId,
|
|
672
675
|
message: `${workerPrompt.system}\n\n${workerPrompt.task}`,
|
|
673
|
-
|
|
676
|
+
streaming: dispatch.agentSessionId
|
|
677
|
+
? { linearApi, agentSessionId: dispatch.agentSessionId }
|
|
678
|
+
: undefined,
|
|
674
679
|
});
|
|
675
680
|
|
|
676
681
|
// Save worker output to .claw/
|
|
677
682
|
const workerElapsed = Date.now() - workerStartTime;
|
|
683
|
+
const agentId = (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
678
684
|
try { saveWorkerOutput(dispatch.worktreePath, dispatch.attempt, result.output); } catch {}
|
|
679
685
|
try {
|
|
680
686
|
appendLog(dispatch.worktreePath, {
|
|
681
687
|
ts: new Date().toISOString(), phase: "worker", attempt: dispatch.attempt,
|
|
682
|
-
agent:
|
|
688
|
+
agent: agentId,
|
|
683
689
|
prompt: workerPrompt.task.slice(0, 200),
|
|
684
690
|
outputPreview: result.output.slice(0, 500),
|
|
685
691
|
success: result.success, durationMs: workerElapsed,
|
|
686
692
|
});
|
|
687
693
|
} catch {}
|
|
688
694
|
|
|
695
|
+
// Handle watchdog kill (runAgent already retried once — both attempts failed)
|
|
696
|
+
if (result.watchdogKilled) {
|
|
697
|
+
const wdConfig = resolveWatchdogConfig(agentId, pluginConfig ?? undefined);
|
|
698
|
+
const thresholdSec = Math.round(wdConfig.inactivityMs / 1000);
|
|
699
|
+
|
|
700
|
+
api.logger.warn(`${TAG} worker killed by inactivity watchdog 2x — escalating to stuck`);
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
appendLog(dispatch.worktreePath, {
|
|
704
|
+
ts: new Date().toISOString(), phase: "watchdog", attempt: dispatch.attempt,
|
|
705
|
+
agent: agentId, prompt: "(watchdog kill)",
|
|
706
|
+
outputPreview: result.output.slice(0, 500), success: false,
|
|
707
|
+
durationMs: workerElapsed,
|
|
708
|
+
watchdog: { reason: "inactivity", silenceSec: thresholdSec, thresholdSec, retried: true },
|
|
709
|
+
});
|
|
710
|
+
} catch {}
|
|
711
|
+
|
|
712
|
+
try { updateManifest(dispatch.worktreePath, { status: "stuck", attempts: dispatch.attempt + 1 }); } catch {}
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
await transitionDispatch(
|
|
716
|
+
dispatch.issueIdentifier, "working", "stuck",
|
|
717
|
+
{ stuckReason: "watchdog_kill_2x" }, configPath,
|
|
718
|
+
);
|
|
719
|
+
} catch (err) {
|
|
720
|
+
if (err instanceof TransitionError) {
|
|
721
|
+
api.logger.warn(`${TAG} CAS failed for watchdog stuck transition: ${err.message}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
await linearApi.createComment(
|
|
726
|
+
dispatch.issueId,
|
|
727
|
+
`## Watchdog Kill\n\nAgent killed by inactivity watchdog (no I/O for ${thresholdSec}s). ` +
|
|
728
|
+
`Automatic retry also failed.\n\n---\n*Needs human review. Artifacts: \`${dispatch.worktreePath}/.claw/\`*`,
|
|
729
|
+
).catch(() => {});
|
|
730
|
+
|
|
731
|
+
await hookCtx.notify("watchdog_kill", {
|
|
732
|
+
identifier: dispatch.issueIdentifier,
|
|
733
|
+
title: issue.title,
|
|
734
|
+
status: "stuck",
|
|
735
|
+
attempt: dispatch.attempt,
|
|
736
|
+
reason: `no I/O for ${thresholdSec}s`,
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
clearActiveSession(dispatch.issueId);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
689
743
|
// runAgent returns inline — trigger audit directly.
|
|
690
744
|
// Re-read dispatch state since it may have changed during worker run.
|
|
691
745
|
const freshState = await readDispatchState(configPath);
|
|
@@ -75,7 +75,7 @@ export async function assessTier(
|
|
|
75
75
|
const message = `${ASSESS_PROMPT}\n\n${issueText}`;
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
|
-
const { runAgent } = await import("
|
|
78
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
79
79
|
const result = await runAgent({
|
|
80
80
|
api,
|
|
81
81
|
agentId: agentId ?? resolveDefaultAgent(api),
|
|
@@ -17,7 +17,7 @@ vi.mock("./pipeline.js", () => ({
|
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
19
|
// Mock the linear-api module
|
|
20
|
-
vi.mock("
|
|
20
|
+
vi.mock("../api/linear-api.js", () => ({
|
|
21
21
|
LinearAgentApi: class MockLinearAgentApi {
|
|
22
22
|
emitActivity = vi.fn().mockResolvedValue(undefined);
|
|
23
23
|
createComment = vi.fn().mockResolvedValue("comment-id");
|
|
@@ -2,13 +2,13 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
|
-
import { LinearAgentApi, resolveLinearToken } from "
|
|
5
|
+
import { LinearAgentApi, resolveLinearToken } from "../api/linear-api.js";
|
|
6
6
|
import { spawnWorker, type HookContext } from "./pipeline.js";
|
|
7
7
|
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
8
8
|
import { readDispatchState, getActiveDispatch, registerDispatch, updateDispatchStatus, completeDispatch, removeActiveDispatch } from "./dispatch-state.js";
|
|
9
|
-
import { createDiscordNotifier, createNoopNotifier, type NotifyFn } from "
|
|
9
|
+
import { createDiscordNotifier, createNoopNotifier, type NotifyFn } from "../infra/notify.js";
|
|
10
10
|
import { assessTier } from "./tier-assess.js";
|
|
11
|
-
import { createWorktree, prepareWorkspace } from "
|
|
11
|
+
import { createWorktree, prepareWorkspace } from "../infra/codex-worktree.js";
|
|
12
12
|
import { ensureClawDir, writeManifest } from "./artifacts.js";
|
|
13
13
|
|
|
14
14
|
// ── Agent profiles (loaded from config, no hardcoded names) ───────
|
|
@@ -245,7 +245,7 @@ export async function handleLinearWebhook(
|
|
|
245
245
|
|
|
246
246
|
// 3. Run agent with streaming
|
|
247
247
|
const sessionId = `linear-notif-${notification?.type ?? "unknown"}-${Date.now()}`;
|
|
248
|
-
const { runAgent } = await import("
|
|
248
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
249
249
|
const result = await runAgent({
|
|
250
250
|
api,
|
|
251
251
|
agentId,
|
|
@@ -418,7 +418,7 @@ export async function handleLinearWebhook(
|
|
|
418
418
|
|
|
419
419
|
// Run agent with streaming to Linear
|
|
420
420
|
const sessionId = `linear-session-${session.id}`;
|
|
421
|
-
const { runAgent } = await import("
|
|
421
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
422
422
|
const result = await runAgent({
|
|
423
423
|
api,
|
|
424
424
|
agentId,
|
|
@@ -596,7 +596,7 @@ export async function handleLinearWebhook(
|
|
|
596
596
|
}).catch(() => {});
|
|
597
597
|
|
|
598
598
|
const sessionId = `linear-session-${session.id}`;
|
|
599
|
-
const { runAgent } = await import("
|
|
599
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
600
600
|
const result = await runAgent({
|
|
601
601
|
api,
|
|
602
602
|
agentId,
|
|
@@ -798,7 +798,7 @@ export async function handleLinearWebhook(
|
|
|
798
798
|
|
|
799
799
|
// 3. Run agent subprocess with streaming
|
|
800
800
|
const sessionId = `linear-comment-${comment.id ?? Date.now()}`;
|
|
801
|
-
const { runAgent } = await import("
|
|
801
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
802
802
|
const result = await runAgent({
|
|
803
803
|
api,
|
|
804
804
|
agentId: mentionedAgent,
|
|
@@ -1036,7 +1036,7 @@ export async function handleLinearWebhook(
|
|
|
1036
1036
|
].filter(Boolean).join("\n");
|
|
1037
1037
|
|
|
1038
1038
|
const sessionId = `linear-triage-${issue.id}-${Date.now()}`;
|
|
1039
|
-
const { runAgent } = await import("
|
|
1039
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
1040
1040
|
const result = await runAgent({
|
|
1041
1041
|
api,
|
|
1042
1042
|
agentId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
4
|
-
import type { ActivityContent } from "
|
|
4
|
+
import type { ActivityContent } from "../api/linear-api.js";
|
|
5
5
|
import {
|
|
6
6
|
buildLinearApi,
|
|
7
7
|
resolveSession,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type CliToolParams,
|
|
12
12
|
type CliResult,
|
|
13
13
|
} from "./cli-shared.js";
|
|
14
|
+
import { InactivityWatchdog, resolveWatchdogConfig } from "../agent/watchdog.js";
|
|
14
15
|
|
|
15
16
|
const CLAUDE_BIN = "/home/claw/.npm-global/bin/claude";
|
|
16
17
|
|
|
@@ -116,7 +117,9 @@ export async function runClaude(
|
|
|
116
117
|
|
|
117
118
|
api.logger.info(`claude_run: session=${agentSessionId ?? "none"}, issue=${issueIdentifier ?? "none"}`);
|
|
118
119
|
|
|
119
|
-
const
|
|
120
|
+
const agentId = (params as any).agentId ?? (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
121
|
+
const wdConfig = resolveWatchdogConfig(agentId, pluginConfig ?? undefined);
|
|
122
|
+
const timeout = timeoutMs ?? (pluginConfig?.claudeTimeoutMs as number) ?? wdConfig.toolTimeoutMs;
|
|
120
123
|
const workingDir = params.workingDir ?? (pluginConfig?.claudeBaseRepo as string) ?? DEFAULT_BASE_REPO;
|
|
121
124
|
|
|
122
125
|
const linearApi = buildLinearApi(api, agentSessionId);
|
|
@@ -155,12 +158,27 @@ export async function runClaude(
|
|
|
155
158
|
});
|
|
156
159
|
|
|
157
160
|
let killed = false;
|
|
161
|
+
let killedByWatchdog = false;
|
|
158
162
|
const timer = setTimeout(() => {
|
|
159
163
|
killed = true;
|
|
160
164
|
child.kill("SIGTERM");
|
|
161
165
|
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
162
166
|
}, timeout);
|
|
163
167
|
|
|
168
|
+
const watchdog = new InactivityWatchdog({
|
|
169
|
+
inactivityMs: wdConfig.inactivityMs,
|
|
170
|
+
label: `claude:${agentSessionId ?? "unknown"}`,
|
|
171
|
+
logger: api.logger,
|
|
172
|
+
onKill: () => {
|
|
173
|
+
killedByWatchdog = true;
|
|
174
|
+
killed = true;
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
child.kill("SIGTERM");
|
|
177
|
+
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
watchdog.start();
|
|
181
|
+
|
|
164
182
|
const collectedMessages: string[] = [];
|
|
165
183
|
const collectedCommands: string[] = [];
|
|
166
184
|
let stderrOutput = "";
|
|
@@ -169,6 +187,7 @@ export async function runClaude(
|
|
|
169
187
|
const rl = createInterface({ input: child.stdout! });
|
|
170
188
|
rl.on("line", (line) => {
|
|
171
189
|
if (!line.trim()) return;
|
|
190
|
+
watchdog.tick();
|
|
172
191
|
|
|
173
192
|
let event: any;
|
|
174
193
|
try {
|
|
@@ -229,11 +248,13 @@ export async function runClaude(
|
|
|
229
248
|
});
|
|
230
249
|
|
|
231
250
|
child.stderr?.on("data", (chunk) => {
|
|
251
|
+
watchdog.tick();
|
|
232
252
|
stderrOutput += chunk.toString();
|
|
233
253
|
});
|
|
234
254
|
|
|
235
255
|
child.on("close", (code) => {
|
|
236
256
|
clearTimeout(timer);
|
|
257
|
+
watchdog.stop();
|
|
237
258
|
rl.close();
|
|
238
259
|
|
|
239
260
|
const parts: string[] = [];
|
|
@@ -242,11 +263,15 @@ export async function runClaude(
|
|
|
242
263
|
const output = parts.join("\n\n") || stderrOutput || "(no output)";
|
|
243
264
|
|
|
244
265
|
if (killed) {
|
|
245
|
-
|
|
266
|
+
const errorType = killedByWatchdog ? "inactivity_timeout" : "timeout";
|
|
267
|
+
const reason = killedByWatchdog
|
|
268
|
+
? `Claude killed by inactivity watchdog (no I/O for ${Math.round(wdConfig.inactivityMs / 1000)}s)`
|
|
269
|
+
: `Claude timed out after ${Math.round(timeout / 1000)}s`;
|
|
270
|
+
api.logger.warn(reason);
|
|
246
271
|
resolve({
|
|
247
272
|
success: false,
|
|
248
|
-
output:
|
|
249
|
-
error:
|
|
273
|
+
output: `${reason}. Partial output:\n${output}`,
|
|
274
|
+
error: errorType,
|
|
250
275
|
});
|
|
251
276
|
return;
|
|
252
277
|
}
|
|
@@ -267,6 +292,7 @@ export async function runClaude(
|
|
|
267
292
|
|
|
268
293
|
child.on("error", (err) => {
|
|
269
294
|
clearTimeout(timer);
|
|
295
|
+
watchdog.stop();
|
|
270
296
|
rl.close();
|
|
271
297
|
api.logger.error(`Claude spawn error: ${err}`);
|
|
272
298
|
resolve({
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { LinearAgentApi } from "
|
|
3
|
-
import { resolveLinearToken, LinearAgentApi as LinearAgentApiClass } from "
|
|
4
|
-
import { getCurrentSession, getActiveSessionByIdentifier } from "
|
|
2
|
+
import type { LinearAgentApi } from "../api/linear-api.js";
|
|
3
|
+
import { resolveLinearToken, LinearAgentApi as LinearAgentApiClass } from "../api/linear-api.js";
|
|
4
|
+
import { getCurrentSession, getActiveSessionByIdentifier } from "../pipeline/active-session.js";
|
|
5
5
|
|
|
6
|
-
export const DEFAULT_TIMEOUT_MS = 10 * 60_000; // 10 minutes
|
|
6
|
+
export const DEFAULT_TIMEOUT_MS = 10 * 60_000; // 10 minutes (legacy — prefer watchdog config)
|
|
7
|
+
export { DEFAULT_INACTIVITY_SEC, DEFAULT_MAX_TOTAL_SEC, DEFAULT_TOOL_TIMEOUT_SEC } from "../agent/watchdog.js";
|
|
7
8
|
export const DEFAULT_BASE_REPO = "/home/claw/ai-workspace";
|
|
8
9
|
|
|
9
10
|
export interface CliToolParams {
|
|
@@ -3,7 +3,7 @@ import { join, dirname } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
5
|
import { jsonResult } from "openclaw/plugin-sdk";
|
|
6
|
-
import { getCurrentSession } from "
|
|
6
|
+
import { getCurrentSession } from "../pipeline/active-session.js";
|
|
7
7
|
import { runCodex } from "./codex-tool.js";
|
|
8
8
|
import { runClaude } from "./claude-tool.js";
|
|
9
9
|
import { runGemini } from "./gemini-tool.js";
|
|
@@ -43,7 +43,7 @@ export interface CodingToolsConfig {
|
|
|
43
43
|
export function loadCodingConfig(): CodingToolsConfig {
|
|
44
44
|
try {
|
|
45
45
|
// Resolve relative to the plugin root (one level up from src/)
|
|
46
|
-
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "
|
|
46
|
+
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
47
47
|
const raw = readFileSync(join(pluginRoot, "coding-tools.json"), "utf8");
|
|
48
48
|
return JSON.parse(raw) as CodingToolsConfig;
|
|
49
49
|
} catch {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
4
|
-
import type { ActivityContent } from "
|
|
4
|
+
import type { ActivityContent } from "../api/linear-api.js";
|
|
5
5
|
import {
|
|
6
6
|
buildLinearApi,
|
|
7
7
|
resolveSession,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type CliToolParams,
|
|
12
12
|
type CliResult,
|
|
13
13
|
} from "./cli-shared.js";
|
|
14
|
+
import { InactivityWatchdog, resolveWatchdogConfig } from "../agent/watchdog.js";
|
|
14
15
|
|
|
15
16
|
const CODEX_BIN = "/home/claw/.npm-global/bin/codex";
|
|
16
17
|
|
|
@@ -103,7 +104,9 @@ export async function runCodex(
|
|
|
103
104
|
|
|
104
105
|
api.logger.info(`codex_run: session=${agentSessionId ?? "none"}, issue=${issueIdentifier ?? "none"}`);
|
|
105
106
|
|
|
106
|
-
const
|
|
107
|
+
const agentId = (params as any).agentId ?? (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
108
|
+
const wdConfig = resolveWatchdogConfig(agentId, pluginConfig ?? undefined);
|
|
109
|
+
const timeout = timeoutMs ?? (pluginConfig?.codexTimeoutMs as number) ?? wdConfig.toolTimeoutMs;
|
|
107
110
|
const workingDir = params.workingDir ?? (pluginConfig?.codexBaseRepo as string) ?? DEFAULT_BASE_REPO;
|
|
108
111
|
|
|
109
112
|
// Build Linear API for activity streaming
|
|
@@ -134,12 +137,27 @@ export async function runCodex(
|
|
|
134
137
|
});
|
|
135
138
|
|
|
136
139
|
let killed = false;
|
|
140
|
+
let killedByWatchdog = false;
|
|
137
141
|
const timer = setTimeout(() => {
|
|
138
142
|
killed = true;
|
|
139
143
|
child.kill("SIGTERM");
|
|
140
144
|
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
141
145
|
}, timeout);
|
|
142
146
|
|
|
147
|
+
const watchdog = new InactivityWatchdog({
|
|
148
|
+
inactivityMs: wdConfig.inactivityMs,
|
|
149
|
+
label: `codex:${agentSessionId ?? "unknown"}`,
|
|
150
|
+
logger: api.logger,
|
|
151
|
+
onKill: () => {
|
|
152
|
+
killedByWatchdog = true;
|
|
153
|
+
killed = true;
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
child.kill("SIGTERM");
|
|
156
|
+
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
watchdog.start();
|
|
160
|
+
|
|
143
161
|
const collectedMessages: string[] = [];
|
|
144
162
|
const collectedCommands: string[] = [];
|
|
145
163
|
let stderrOutput = "";
|
|
@@ -147,6 +165,7 @@ export async function runCodex(
|
|
|
147
165
|
const rl = createInterface({ input: child.stdout! });
|
|
148
166
|
rl.on("line", (line) => {
|
|
149
167
|
if (!line.trim()) return;
|
|
168
|
+
watchdog.tick();
|
|
150
169
|
|
|
151
170
|
let event: any;
|
|
152
171
|
try {
|
|
@@ -189,11 +208,13 @@ export async function runCodex(
|
|
|
189
208
|
});
|
|
190
209
|
|
|
191
210
|
child.stderr?.on("data", (chunk) => {
|
|
211
|
+
watchdog.tick();
|
|
192
212
|
stderrOutput += chunk.toString();
|
|
193
213
|
});
|
|
194
214
|
|
|
195
215
|
child.on("close", (code) => {
|
|
196
216
|
clearTimeout(timer);
|
|
217
|
+
watchdog.stop();
|
|
197
218
|
rl.close();
|
|
198
219
|
|
|
199
220
|
const parts: string[] = [];
|
|
@@ -202,11 +223,15 @@ export async function runCodex(
|
|
|
202
223
|
const output = parts.join("\n\n") || stderrOutput || "(no output)";
|
|
203
224
|
|
|
204
225
|
if (killed) {
|
|
205
|
-
|
|
226
|
+
const errorType = killedByWatchdog ? "inactivity_timeout" : "timeout";
|
|
227
|
+
const reason = killedByWatchdog
|
|
228
|
+
? `Codex killed by inactivity watchdog (no I/O for ${Math.round(wdConfig.inactivityMs / 1000)}s)`
|
|
229
|
+
: `Codex timed out after ${Math.round(timeout / 1000)}s`;
|
|
230
|
+
api.logger.warn(reason);
|
|
206
231
|
resolve({
|
|
207
232
|
success: false,
|
|
208
|
-
output:
|
|
209
|
-
error:
|
|
233
|
+
output: `${reason}. Partial output:\n${output}`,
|
|
234
|
+
error: errorType,
|
|
210
235
|
});
|
|
211
236
|
return;
|
|
212
237
|
}
|
|
@@ -227,6 +252,7 @@ export async function runCodex(
|
|
|
227
252
|
|
|
228
253
|
child.on("error", (err) => {
|
|
229
254
|
clearTimeout(timer);
|
|
255
|
+
watchdog.stop();
|
|
230
256
|
rl.close();
|
|
231
257
|
api.logger.error(`Codex spawn error: ${err}`);
|
|
232
258
|
resolve({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
4
|
-
import type { ActivityContent } from "
|
|
4
|
+
import type { ActivityContent } from "../api/linear-api.js";
|
|
5
5
|
import {
|
|
6
6
|
buildLinearApi,
|
|
7
7
|
resolveSession,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type CliToolParams,
|
|
12
12
|
type CliResult,
|
|
13
13
|
} from "./cli-shared.js";
|
|
14
|
+
import { InactivityWatchdog, resolveWatchdogConfig } from "../agent/watchdog.js";
|
|
14
15
|
|
|
15
16
|
const GEMINI_BIN = "/home/claw/.npm-global/bin/gemini";
|
|
16
17
|
|
|
@@ -98,7 +99,9 @@ export async function runGemini(
|
|
|
98
99
|
|
|
99
100
|
api.logger.info(`gemini_run: session=${agentSessionId ?? "none"}, issue=${issueIdentifier ?? "none"}`);
|
|
100
101
|
|
|
101
|
-
const
|
|
102
|
+
const agentId = (params as any).agentId ?? (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
103
|
+
const wdConfig = resolveWatchdogConfig(agentId, pluginConfig ?? undefined);
|
|
104
|
+
const timeout = timeoutMs ?? (pluginConfig?.geminiTimeoutMs as number) ?? wdConfig.toolTimeoutMs;
|
|
102
105
|
const workingDir = params.workingDir ?? (pluginConfig?.geminiBaseRepo as string) ?? DEFAULT_BASE_REPO;
|
|
103
106
|
|
|
104
107
|
const linearApi = buildLinearApi(api, agentSessionId);
|
|
@@ -131,12 +134,27 @@ export async function runGemini(
|
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
let killed = false;
|
|
137
|
+
let killedByWatchdog = false;
|
|
134
138
|
const timer = setTimeout(() => {
|
|
135
139
|
killed = true;
|
|
136
140
|
child.kill("SIGTERM");
|
|
137
141
|
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
138
142
|
}, timeout);
|
|
139
143
|
|
|
144
|
+
const watchdog = new InactivityWatchdog({
|
|
145
|
+
inactivityMs: wdConfig.inactivityMs,
|
|
146
|
+
label: `gemini:${agentSessionId ?? "unknown"}`,
|
|
147
|
+
logger: api.logger,
|
|
148
|
+
onKill: () => {
|
|
149
|
+
killedByWatchdog = true;
|
|
150
|
+
killed = true;
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
child.kill("SIGTERM");
|
|
153
|
+
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
watchdog.start();
|
|
157
|
+
|
|
140
158
|
const collectedMessages: string[] = [];
|
|
141
159
|
const collectedCommands: string[] = [];
|
|
142
160
|
let stderrOutput = "";
|
|
@@ -144,6 +162,7 @@ export async function runGemini(
|
|
|
144
162
|
const rl = createInterface({ input: child.stdout! });
|
|
145
163
|
rl.on("line", (line) => {
|
|
146
164
|
if (!line.trim()) return;
|
|
165
|
+
watchdog.tick();
|
|
147
166
|
|
|
148
167
|
let event: any;
|
|
149
168
|
try {
|
|
@@ -187,11 +206,13 @@ export async function runGemini(
|
|
|
187
206
|
});
|
|
188
207
|
|
|
189
208
|
child.stderr?.on("data", (chunk) => {
|
|
209
|
+
watchdog.tick();
|
|
190
210
|
stderrOutput += chunk.toString();
|
|
191
211
|
});
|
|
192
212
|
|
|
193
213
|
child.on("close", (code) => {
|
|
194
214
|
clearTimeout(timer);
|
|
215
|
+
watchdog.stop();
|
|
195
216
|
rl.close();
|
|
196
217
|
|
|
197
218
|
const parts: string[] = [];
|
|
@@ -200,11 +221,15 @@ export async function runGemini(
|
|
|
200
221
|
const output = parts.join("\n\n") || stderrOutput || "(no output)";
|
|
201
222
|
|
|
202
223
|
if (killed) {
|
|
203
|
-
|
|
224
|
+
const errorType = killedByWatchdog ? "inactivity_timeout" : "timeout";
|
|
225
|
+
const reason = killedByWatchdog
|
|
226
|
+
? `Gemini killed by inactivity watchdog (no I/O for ${Math.round(wdConfig.inactivityMs / 1000)}s)`
|
|
227
|
+
: `Gemini timed out after ${Math.round(timeout / 1000)}s`;
|
|
228
|
+
api.logger.warn(reason);
|
|
204
229
|
resolve({
|
|
205
230
|
success: false,
|
|
206
|
-
output:
|
|
207
|
-
error:
|
|
231
|
+
output: `${reason}. Partial output:\n${output}`,
|
|
232
|
+
error: errorType,
|
|
208
233
|
});
|
|
209
234
|
return;
|
|
210
235
|
}
|
|
@@ -225,6 +250,7 @@ export async function runGemini(
|
|
|
225
250
|
|
|
226
251
|
child.on("error", (err) => {
|
|
227
252
|
clearTimeout(timer);
|
|
253
|
+
watchdog.stop();
|
|
228
254
|
rl.close();
|
|
229
255
|
api.logger.error(`Gemini spawn error: ${err}`);
|
|
230
256
|
resolve({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { jsonResult } from "openclaw/plugin-sdk";
|
|
3
|
-
import { runAgent } from "
|
|
3
|
+
import { runAgent } from "../agent/agent.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Create orchestration tools that let agents delegate work to other crew agents.
|
package/src/client.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
|
|
2
|
-
|
|
3
|
-
export class LinearClient {
|
|
4
|
-
constructor(private accessToken: string) {}
|
|
5
|
-
|
|
6
|
-
async request<T = any>(query: string, variables?: Record<string, any>): Promise<T> {
|
|
7
|
-
const res = await fetch(LINEAR_GRAPHQL_URL, {
|
|
8
|
-
method: "POST",
|
|
9
|
-
headers: {
|
|
10
|
-
"Content-Type": "application/json",
|
|
11
|
-
"Authorization": `Bearer ${this.accessToken}`,
|
|
12
|
-
},
|
|
13
|
-
body: JSON.stringify({ query, variables }),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
if (!res.ok) {
|
|
17
|
-
const text = await res.text();
|
|
18
|
-
throw new Error(`Linear API request failed: ${res.status} ${text}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const payload = await res.json();
|
|
22
|
-
if (payload.errors) {
|
|
23
|
-
throw new Error(`Linear API returned errors: ${JSON.stringify(payload.errors)}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return payload.data as T;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async getViewer() {
|
|
30
|
-
const query = `
|
|
31
|
-
query {
|
|
32
|
-
viewer {
|
|
33
|
-
id
|
|
34
|
-
name
|
|
35
|
-
email
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
`;
|
|
39
|
-
return this.request(query);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async listIssues(params: { limit: number; teamId?: string }) {
|
|
43
|
-
const query = `
|
|
44
|
-
query ListIssues($limit: Int, $teamId: String) {
|
|
45
|
-
issues(first: $limit, filter: { team: { id: { eq: $teamId } } }) {
|
|
46
|
-
nodes {
|
|
47
|
-
id
|
|
48
|
-
identifier
|
|
49
|
-
title
|
|
50
|
-
description
|
|
51
|
-
state {
|
|
52
|
-
name
|
|
53
|
-
}
|
|
54
|
-
assignee {
|
|
55
|
-
name
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
`;
|
|
61
|
-
return this.request(query, params);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async createIssue(params: { title: string; description?: string; teamId: string }) {
|
|
65
|
-
const query = `
|
|
66
|
-
mutation CreateIssue($title: String!, $description: String, $teamId: String!) {
|
|
67
|
-
issueCreate(input: { title: $title, description: $description, teamId: $teamId }) {
|
|
68
|
-
success
|
|
69
|
-
issue {
|
|
70
|
-
id
|
|
71
|
-
identifier
|
|
72
|
-
title
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
`;
|
|
77
|
-
return this.request(query, params);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async addComment(params: { issueId: string; body: string }) {
|
|
81
|
-
const query = `
|
|
82
|
-
mutation AddComment($issueId: String!, $body: String!) {
|
|
83
|
-
commentCreate(input: { issueId: $issueId, body: $body }) {
|
|
84
|
-
success
|
|
85
|
-
comment {
|
|
86
|
-
id
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
`;
|
|
91
|
-
return this.request(query, params);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|