@calltelemetry/openclaw-linear 0.9.3 → 0.9.5

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.
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
5
5
  import { LinearAgentApi, resolveLinearToken } from "../api/linear-api.js";
6
6
  import { spawnWorker, type HookContext } from "./pipeline.js";
7
- import { setActiveSession, clearActiveSession } from "./active-session.js";
7
+ import { setActiveSession, clearActiveSession, getIssueAffinity, _configureAffinityTtl, _resetAffinityForTesting } from "./active-session.js";
8
8
  import { readDispatchState, getActiveDispatch, registerDispatch, updateDispatchStatus, completeDispatch, removeActiveDispatch } from "./dispatch-state.js";
9
9
  import { createNotifierFromConfig, type NotifyFn } from "../infra/notify.js";
10
10
  import { assessTier } from "./tier-assess.js";
@@ -80,6 +80,7 @@ export function _resetForTesting(): void {
80
80
  _dedupTtlMs = 60_000;
81
81
  _sweepIntervalMs = 10_000;
82
82
  _resetGuidanceCacheForTesting();
83
+ _resetAffinityForTesting();
83
84
  }
84
85
 
85
86
  /** @internal — test-only; add an issue ID to the activeRuns set. */
@@ -346,6 +347,14 @@ export async function handleLinearWebhook(
346
347
  }
347
348
  }
348
349
  }
350
+ // Session affinity: if no @mention override, prefer the agent that last handled this issue
351
+ if (agentId === resolveAgentId(api) && issue?.id) {
352
+ const affinityAgent = getIssueAffinity(issue.id);
353
+ if (affinityAgent) {
354
+ api.logger.info(`AgentSession routed to ${affinityAgent} via session affinity for ${issue.identifier ?? issue.id}`);
355
+ agentId = affinityAgent;
356
+ }
357
+ }
349
358
 
350
359
  api.logger.info(`AgentSession created: ${session.id} for issue ${issue?.identifier ?? issue?.id} agent=${agentId} (comments: ${previousComments.length}, guidance: ${guidanceCtx.guidance ? "yes" : "no"})`);
351
360
 
@@ -557,6 +566,14 @@ export async function handleLinearWebhook(
557
566
  }
558
567
  }
559
568
  }
569
+ // Session affinity: if no @mention override, prefer the agent that last handled this issue
570
+ if (agentId === resolveAgentId(api) && issue?.id) {
571
+ const affinityAgent = getIssueAffinity(issue.id);
572
+ if (affinityAgent) {
573
+ api.logger.info(`AgentSession prompted: routed to ${affinityAgent} via session affinity for ${issue.identifier ?? issue.id}`);
574
+ agentId = affinityAgent;
575
+ }
576
+ }
560
577
 
561
578
  api.logger.info(`AgentSession prompted (follow-up): ${session.id} issue=${issue?.identifier ?? issue?.id} agent=${agentId} message="${userMessage.slice(0, 80)}..."`);
562
579
 
@@ -885,8 +902,9 @@ export async function handleLinearWebhook(
885
902
  case "plan_continue": {
886
903
  if (!isPlanning || !planSession) {
887
904
  // Not in planning mode — treat as general
888
- api.logger.info("Comment intent plan_continue but not in planning mode — dispatching to default agent");
889
- void dispatchCommentToAgent(api, linearApi, profiles, resolveAgentId(api), issue, comment, commentBody, commentor, pluginConfig)
905
+ const planContinueAgent = getIssueAffinity(issue.id) ?? resolveAgentId(api);
906
+ api.logger.info(`Comment intent plan_continue but not in planning mode — dispatching to ${planContinueAgent}`);
907
+ void dispatchCommentToAgent(api, linearApi, profiles, planContinueAgent, issue, comment, commentBody, commentor, pluginConfig)
890
908
  .catch((err) => api.logger.error(`Comment dispatch error: ${err}`));
891
909
  break;
892
910
  }
@@ -908,15 +926,15 @@ export async function handleLinearWebhook(
908
926
 
909
927
  case "request_work":
910
928
  case "question": {
911
- const defaultAgent = resolveAgentId(api);
912
- api.logger.info(`Comment intent ${intentResult.intent}: routing to default agent ${defaultAgent}`);
929
+ const defaultAgent = getIssueAffinity(issue.id) ?? resolveAgentId(api);
930
+ api.logger.info(`Comment intent ${intentResult.intent}: routing to ${defaultAgent}`);
913
931
  void dispatchCommentToAgent(api, linearApi, profiles, defaultAgent, issue, comment, commentBody, commentor, pluginConfig)
914
932
  .catch((err) => api.logger.error(`Comment dispatch error: ${err}`));
915
933
  break;
916
934
  }
917
935
 
918
936
  case "close_issue": {
919
- const closeAgent = resolveAgentId(api);
937
+ const closeAgent = getIssueAffinity(issue.id) ?? resolveAgentId(api);
920
938
  api.logger.info(`Comment intent close_issue: closing ${issue.identifier ?? issue.id} via ${closeAgent}`);
921
939
  void handleCloseIssue(api, linearApi, profiles, closeAgent, issue, comment, commentBody, commentor, pluginConfig)
922
940
  .catch((err) => api.logger.error(`Close issue error: ${err}`));
@@ -13,7 +13,7 @@ import {
13
13
  } from "./cli-shared.js";
14
14
  import { InactivityWatchdog, resolveWatchdogConfig } from "../agent/watchdog.js";
15
15
 
16
- const CLAUDE_BIN = "/home/claw/.npm-global/bin/claude";
16
+ const CLAUDE_BIN = "claude";
17
17
 
18
18
  /**
19
19
  * Map a Claude Code stream-json JSONL event to a Linear activity.
@@ -4,6 +4,9 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
4
4
  vi.mock("../pipeline/active-session.js", () => ({
5
5
  getCurrentSession: vi.fn(() => null),
6
6
  getActiveSessionByIdentifier: vi.fn(() => null),
7
+ getIssueAffinity: vi.fn().mockReturnValue(null),
8
+ _configureAffinityTtl: vi.fn(),
9
+ _resetAffinityForTesting: vi.fn(),
7
10
  }));
8
11
 
9
12
  // Mock the linear-api module — LinearAgentApi must be a class (used with `new`)
@@ -1,3 +1,5 @@
1
+ import { join } from "node:path";
2
+ import { homedir } from "node:os";
1
3
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
4
  import type { LinearAgentApi } from "../api/linear-api.js";
3
5
  import { resolveLinearToken, LinearAgentApi as LinearAgentApiClass } from "../api/linear-api.js";
@@ -5,7 +7,7 @@ import { getCurrentSession, getActiveSessionByIdentifier } from "../pipeline/act
5
7
 
6
8
  export const DEFAULT_TIMEOUT_MS = 10 * 60_000; // 10 minutes (legacy — prefer watchdog config)
7
9
  export { DEFAULT_INACTIVITY_SEC, DEFAULT_MAX_TOTAL_SEC, DEFAULT_TOOL_TIMEOUT_SEC } from "../agent/watchdog.js";
8
- export const DEFAULT_BASE_REPO = "/home/claw/ai-workspace";
10
+ export const DEFAULT_BASE_REPO = join(homedir(), "ai-workspace");
9
11
 
10
12
  export interface CliToolParams {
11
13
  prompt: string;
@@ -17,6 +17,9 @@ vi.mock("./gemini-tool.js", () => ({
17
17
  }));
18
18
  vi.mock("../pipeline/active-session.js", () => ({
19
19
  getCurrentSession: vi.fn(() => null),
20
+ getIssueAffinity: vi.fn().mockReturnValue(null),
21
+ _configureAffinityTtl: vi.fn(),
22
+ _resetAffinityForTesting: vi.fn(),
20
23
  }));
21
24
  vi.mock("openclaw/plugin-sdk", () => ({
22
25
  jsonResult: vi.fn((v: unknown) => v),
@@ -164,7 +164,7 @@ export function createCodeTool(
164
164
  },
165
165
  workingDir: {
166
166
  type: "string",
167
- description: "Override working directory (default: /home/claw/ai-workspace).",
167
+ description: "Override working directory (default: ~/ai-workspace).",
168
168
  },
169
169
  model: {
170
170
  type: "string",
@@ -13,7 +13,7 @@ import {
13
13
  } from "./cli-shared.js";
14
14
  import { InactivityWatchdog, resolveWatchdogConfig } from "../agent/watchdog.js";
15
15
 
16
- const CODEX_BIN = "/home/claw/.npm-global/bin/codex";
16
+ const CODEX_BIN = "codex";
17
17
 
18
18
  /**
19
19
  * Parse a JSONL line from `codex exec --json` and map it to a Linear activity.
@@ -13,7 +13,7 @@ import {
13
13
  } from "./cli-shared.js";
14
14
  import { InactivityWatchdog, resolveWatchdogConfig } from "../agent/watchdog.js";
15
15
 
16
- const GEMINI_BIN = "/home/claw/.npm-global/bin/gemini";
16
+ const GEMINI_BIN = "gemini";
17
17
 
18
18
  /**
19
19
  * Map a Gemini CLI stream-json JSONL event to a Linear activity.