@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.
Files changed (37) hide show
  1. package/README.md +359 -195
  2. package/index.ts +10 -10
  3. package/openclaw.plugin.json +4 -1
  4. package/package.json +9 -2
  5. package/src/agent/agent.test.ts +127 -0
  6. package/src/{agent.ts → agent/agent.ts} +84 -7
  7. package/src/agent/watchdog.test.ts +266 -0
  8. package/src/agent/watchdog.ts +176 -0
  9. package/src/{cli.ts → infra/cli.ts} +32 -5
  10. package/src/{codex-worktree.ts → infra/codex-worktree.ts} +1 -1
  11. package/src/infra/doctor.test.ts +399 -0
  12. package/src/infra/doctor.ts +781 -0
  13. package/src/infra/notify.test.ts +169 -0
  14. package/src/{notify.ts → infra/notify.ts} +6 -1
  15. package/src/pipeline/active-session.test.ts +154 -0
  16. package/src/pipeline/artifacts.test.ts +383 -0
  17. package/src/{artifacts.ts → pipeline/artifacts.ts} +9 -1
  18. package/src/{dispatch-service.ts → pipeline/dispatch-service.ts} +1 -1
  19. package/src/pipeline/dispatch-state.test.ts +382 -0
  20. package/src/pipeline/pipeline.test.ts +226 -0
  21. package/src/{pipeline.ts → pipeline/pipeline.ts} +61 -7
  22. package/src/{tier-assess.ts → pipeline/tier-assess.ts} +1 -1
  23. package/src/{webhook.test.ts → pipeline/webhook.test.ts} +1 -1
  24. package/src/{webhook.ts → pipeline/webhook.ts} +8 -8
  25. package/src/{claude-tool.ts → tools/claude-tool.ts} +31 -5
  26. package/src/{cli-shared.ts → tools/cli-shared.ts} +5 -4
  27. package/src/{code-tool.ts → tools/code-tool.ts} +2 -2
  28. package/src/{codex-tool.ts → tools/codex-tool.ts} +31 -5
  29. package/src/{gemini-tool.ts → tools/gemini-tool.ts} +31 -5
  30. package/src/{orchestration-tools.ts → tools/orchestration-tools.ts} +1 -1
  31. package/src/client.ts +0 -94
  32. /package/src/{auth.ts → api/auth.ts} +0 -0
  33. /package/src/{linear-api.ts → api/linear-api.ts} +0 -0
  34. /package/src/{oauth-callback.ts → api/oauth-callback.ts} +0 -0
  35. /package/src/{active-session.ts → pipeline/active-session.ts} +0 -0
  36. /package/src/{dispatch-state.ts → pipeline/dispatch-state.ts} +0 -0
  37. /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 "./linear-api.js";
20
- import { runAgent } from "./agent.js";
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 "./notify.js";
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
- timeoutMs: 5 * 60_000,
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
- timeoutMs: (pluginConfig?.codexTimeoutMs as number) ?? 10 * 60_000,
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: (pluginConfig?.defaultAgentId as string) ?? "default",
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("./agent.js");
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("./linear-api.js", () => ({
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 "./linear-api.js";
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 "./notify.js";
9
+ import { createDiscordNotifier, createNoopNotifier, type NotifyFn } from "../infra/notify.js";
10
10
  import { assessTier } from "./tier-assess.js";
11
- import { createWorktree, prepareWorkspace } from "./codex-worktree.js";
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("./agent.js");
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("./agent.js");
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("./agent.js");
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("./agent.js");
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("./agent.js");
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 "./linear-api.js";
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 timeout = timeoutMs ?? (pluginConfig?.claudeTimeoutMs as number) ?? DEFAULT_TIMEOUT_MS;
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
- api.logger.warn(`Claude timed out after ${timeout}ms`);
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: `Claude timed out after ${Math.round(timeout / 1000)}s. Partial output:\n${output}`,
249
- error: "timeout",
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 "./linear-api.js";
3
- import { resolveLinearToken, LinearAgentApi as LinearAgentApiClass } from "./linear-api.js";
4
- import { getCurrentSession, getActiveSessionByIdentifier } from "./active-session.js";
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 "./active-session.js";
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 "./linear-api.js";
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 timeout = timeoutMs ?? (pluginConfig?.codexTimeoutMs as number) ?? DEFAULT_TIMEOUT_MS;
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
- api.logger.warn(`Codex timed out after ${timeout}ms`);
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: `Codex timed out after ${Math.round(timeout / 1000)}s. Partial output:\n${output}`,
209
- error: "timeout",
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 "./linear-api.js";
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 timeout = timeoutMs ?? (pluginConfig?.geminiTimeoutMs as number) ?? DEFAULT_TIMEOUT_MS;
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
- api.logger.warn(`Gemini timed out after ${timeout}ms`);
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: `Gemini timed out after ${Math.round(timeout / 1000)}s. Partial output:\n${output}`,
207
- error: "timeout",
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 "./agent.js";
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