@calltelemetry/openclaw-linear 0.9.14 → 0.9.16
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 +104 -48
- package/index.ts +57 -0
- package/openclaw.plugin.json +44 -2
- package/package.json +1 -1
- package/prompts.yaml +3 -1
- package/src/__test__/fixtures/recorded-sub-issue-flow.ts +37 -50
- package/src/agent/agent.test.ts +1 -1
- package/src/agent/agent.ts +39 -6
- package/src/api/linear-api.test.ts +188 -1
- package/src/api/linear-api.ts +114 -5
- package/src/infra/multi-repo.test.ts +127 -1
- package/src/infra/multi-repo.ts +74 -6
- package/src/infra/tmux-runner.ts +599 -0
- package/src/infra/tmux.ts +158 -0
- package/src/infra/token-refresh-timer.ts +44 -0
- package/src/pipeline/active-session.ts +19 -1
- package/src/pipeline/artifacts.ts +42 -0
- package/src/pipeline/dispatch-state.ts +3 -0
- package/src/pipeline/guidance.test.ts +53 -0
- package/src/pipeline/guidance.ts +38 -0
- package/src/pipeline/memory-search.ts +40 -0
- package/src/pipeline/pipeline.ts +184 -17
- package/src/pipeline/retro.ts +231 -0
- package/src/pipeline/webhook.test.ts +8 -8
- package/src/pipeline/webhook.ts +408 -29
- package/src/tools/claude-tool.ts +68 -10
- package/src/tools/cli-shared.ts +50 -2
- package/src/tools/code-tool.ts +230 -150
- package/src/tools/codex-tool.ts +61 -9
- package/src/tools/gemini-tool.ts +61 -10
- package/src/tools/steering-tools.ts +176 -0
- package/src/tools/tools.test.ts +47 -15
- package/src/tools/tools.ts +17 -4
- package/src/__test__/smoke-linear-api.test.ts +0 -847
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if tmux is available on the system.
|
|
5
|
+
*/
|
|
6
|
+
export function isTmuxAvailable(): boolean {
|
|
7
|
+
try {
|
|
8
|
+
execFileSync("tmux", ["-V"], { encoding: "utf8", timeout: 5000 });
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a new tmux session with given name.
|
|
17
|
+
* Uses 200x50 terminal size for consistent capture-pane output.
|
|
18
|
+
*/
|
|
19
|
+
export function createSession(name: string, cwd: string): void {
|
|
20
|
+
execFileSync("tmux", [
|
|
21
|
+
"new-session", "-d", "-s", name, "-x", "200", "-y", "50",
|
|
22
|
+
], { cwd, encoding: "utf8", timeout: 10_000 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set up pipe-pane to stream terminal output to a file.
|
|
27
|
+
* Filters for JSON lines only (lines starting with "{") to extract JSONL
|
|
28
|
+
* from raw PTY output (which includes ANSI sequences, prompts, etc).
|
|
29
|
+
*/
|
|
30
|
+
export function setupPipePane(name: string, logPath: string): void {
|
|
31
|
+
execFileSync("tmux", [
|
|
32
|
+
"pipe-pane", "-t", name, "-O",
|
|
33
|
+
`grep --line-buffered "^{" >> ${shellEscapeForTmux(logPath)}`,
|
|
34
|
+
], { encoding: "utf8", timeout: 5000 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Send text to the tmux session's active pane (injects into stdin).
|
|
39
|
+
* Appends Enter key to execute the command.
|
|
40
|
+
*/
|
|
41
|
+
export function sendKeys(name: string, text: string): void {
|
|
42
|
+
execFileSync("tmux", [
|
|
43
|
+
"send-keys", "-t", name, text, "Enter",
|
|
44
|
+
], { encoding: "utf8", timeout: 5000 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Send raw text without appending Enter (for steering prompts
|
|
49
|
+
* where the Enter should be part of the text itself).
|
|
50
|
+
*/
|
|
51
|
+
export function sendKeysRaw(name: string, text: string): void {
|
|
52
|
+
execFileSync("tmux", [
|
|
53
|
+
"send-keys", "-t", name, "-l", text,
|
|
54
|
+
], { encoding: "utf8", timeout: 5000 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Capture the visible pane content (ANSI-stripped).
|
|
59
|
+
* Returns the last `lines` lines of the terminal.
|
|
60
|
+
*/
|
|
61
|
+
export function capturePane(name: string, lines = 50): string {
|
|
62
|
+
return execFileSync("tmux", [
|
|
63
|
+
"capture-pane", "-t", name, "-p", "-S", `-${lines}`,
|
|
64
|
+
], { encoding: "utf8", timeout: 5000 }).trimEnd();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a tmux session exists.
|
|
69
|
+
*/
|
|
70
|
+
export function sessionExists(name: string): boolean {
|
|
71
|
+
try {
|
|
72
|
+
execFileSync("tmux", ["has-session", "-t", name], {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
timeout: 5000,
|
|
75
|
+
});
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Kill a tmux session.
|
|
84
|
+
*/
|
|
85
|
+
export function killSession(name: string): void {
|
|
86
|
+
try {
|
|
87
|
+
execFileSync("tmux", ["kill-session", "-t", name], {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
timeout: 10_000,
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
// Session may already be dead
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List all tmux sessions matching a prefix.
|
|
98
|
+
* Returns session names.
|
|
99
|
+
*/
|
|
100
|
+
export function listSessions(prefix?: string): string[] {
|
|
101
|
+
try {
|
|
102
|
+
const output = execFileSync("tmux", [
|
|
103
|
+
"list-sessions", "-F", "#{session_name}",
|
|
104
|
+
], { encoding: "utf8", timeout: 5000 });
|
|
105
|
+
const sessions = output.trim().split("\n").filter(Boolean);
|
|
106
|
+
if (prefix) return sessions.filter(s => s.startsWith(prefix));
|
|
107
|
+
return sessions;
|
|
108
|
+
} catch {
|
|
109
|
+
return []; // tmux server not running
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Wait for a tmux session to exit (poll-based).
|
|
115
|
+
* Resolves when the session no longer exists or timeout is reached.
|
|
116
|
+
*/
|
|
117
|
+
export function waitForExit(name: string, timeoutMs: number): Promise<void> {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
const start = Date.now();
|
|
120
|
+
const check = () => {
|
|
121
|
+
if (!sessionExists(name) || Date.now() - start > timeoutMs) {
|
|
122
|
+
resolve();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setTimeout(check, 1000);
|
|
126
|
+
};
|
|
127
|
+
check();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build a tmux session name from dispatch context.
|
|
133
|
+
* Format: lnr-{issueIdentifier}-{backend}-{attempt}
|
|
134
|
+
*/
|
|
135
|
+
export function buildSessionName(
|
|
136
|
+
issueIdentifier: string,
|
|
137
|
+
backend: string,
|
|
138
|
+
attempt: number,
|
|
139
|
+
): string {
|
|
140
|
+
// Sanitize identifier for tmux (replace dots/spaces with dashes)
|
|
141
|
+
const safe = issueIdentifier.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
142
|
+
return `lnr-${safe}-${backend}-${attempt}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Escape a string for safe use in tmux pipe-pane shell commands.
|
|
147
|
+
*/
|
|
148
|
+
function shellEscapeForTmux(s: string): string {
|
|
149
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Escape a string for safe use as a shell argument in sendKeys.
|
|
154
|
+
* Wraps in single quotes and escapes internal single quotes.
|
|
155
|
+
*/
|
|
156
|
+
export function shellEscape(s: string): string {
|
|
157
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
158
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { refreshTokenProactively } from "../api/linear-api.js";
|
|
3
|
+
|
|
4
|
+
let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
|
5
|
+
|
|
6
|
+
const REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Start the proactive token refresh timer.
|
|
10
|
+
* Runs immediately on start, then every 6 hours.
|
|
11
|
+
*/
|
|
12
|
+
export function startTokenRefreshTimer(
|
|
13
|
+
api: OpenClawPluginApi,
|
|
14
|
+
pluginConfig?: Record<string, unknown>,
|
|
15
|
+
): void {
|
|
16
|
+
if (refreshInterval) return; // Already running
|
|
17
|
+
|
|
18
|
+
const doRefresh = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const result = await refreshTokenProactively(pluginConfig);
|
|
21
|
+
if (result.refreshed) {
|
|
22
|
+
api.logger.info(`Linear token refresh: ${result.reason}`);
|
|
23
|
+
} else {
|
|
24
|
+
api.logger.debug(`Linear token refresh skipped: ${result.reason}`);
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
api.logger.warn(`Linear token refresh failed: ${err}`);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Run immediately
|
|
32
|
+
void doRefresh();
|
|
33
|
+
|
|
34
|
+
// Then every 6 hours
|
|
35
|
+
refreshInterval = setInterval(doRefresh, REFRESH_INTERVAL_MS);
|
|
36
|
+
api.logger.info(`Linear token refresh timer started (every ${REFRESH_INTERVAL_MS / 3600000}h)`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function stopTokenRefreshTimer(): void {
|
|
40
|
+
if (refreshInterval) {
|
|
41
|
+
clearInterval(refreshInterval);
|
|
42
|
+
refreshInterval = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* active-session.ts — Idempotent registry of active Linear agent sessions.
|
|
3
3
|
*
|
|
4
4
|
* When the pipeline starts work on an issue, it registers the session here.
|
|
5
|
-
* Any tool (
|
|
5
|
+
* Any tool (cli_codex, cli_claude, etc.) can look up the active session for the current
|
|
6
6
|
* issue to stream activities without relying on the LLM agent to pass params.
|
|
7
7
|
*
|
|
8
8
|
* This runs in the gateway process. Tool execution also happens in the gateway,
|
|
@@ -96,6 +96,24 @@ export function getCurrentSession(): ActiveSession | null {
|
|
|
96
96
|
return null;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Look up the most recent active session for a given agent ID.
|
|
101
|
+
* When multiple sessions exist for the same agent, returns the most
|
|
102
|
+
* recently started one. This is the primary lookup for tool execution
|
|
103
|
+
* contexts where the agent ID is known but the issue isn't.
|
|
104
|
+
*/
|
|
105
|
+
export function getActiveSessionByAgentId(agentId: string): ActiveSession | null {
|
|
106
|
+
let best: ActiveSession | null = null;
|
|
107
|
+
for (const session of sessions.values()) {
|
|
108
|
+
if (session.agentId === agentId) {
|
|
109
|
+
if (!best || session.startedAt > best.startedAt) {
|
|
110
|
+
best = session;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return best;
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
/**
|
|
100
118
|
* Hydrate the in-memory session Map from dispatch-state.json.
|
|
101
119
|
* Called on startup by the dispatch service to restore sessions
|
|
@@ -307,3 +307,45 @@ export function resolveOrchestratorWorkspace(
|
|
|
307
307
|
return join(home, ".openclaw", "workspace");
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Read worker output artifacts for all attempts.
|
|
313
|
+
*/
|
|
314
|
+
export function readWorkerOutputs(worktreePath: string, maxAttempt: number): string[] {
|
|
315
|
+
const outputs: string[] = [];
|
|
316
|
+
for (let i = 0; i <= maxAttempt; i++) {
|
|
317
|
+
try {
|
|
318
|
+
outputs.push(readFileSync(join(clawDir(worktreePath), `worker-${i}.md`), "utf-8"));
|
|
319
|
+
} catch {
|
|
320
|
+
outputs.push("(not found)");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return outputs;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Read audit verdict artifacts for all attempts.
|
|
328
|
+
*/
|
|
329
|
+
export function readAuditVerdicts(worktreePath: string, maxAttempt: number): string[] {
|
|
330
|
+
const verdicts: string[] = [];
|
|
331
|
+
for (let i = 0; i <= maxAttempt; i++) {
|
|
332
|
+
try {
|
|
333
|
+
verdicts.push(readFileSync(join(clawDir(worktreePath), `audit-${i}.json`), "utf-8"));
|
|
334
|
+
} catch {
|
|
335
|
+
verdicts.push("(not found)");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return verdicts;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Read the interaction log entries.
|
|
343
|
+
*/
|
|
344
|
+
export function readLog(worktreePath: string): string[] {
|
|
345
|
+
try {
|
|
346
|
+
const raw = readFileSync(join(clawDir(worktreePath), "log.jsonl"), "utf-8");
|
|
347
|
+
return raw.trim().split("\n").filter(Boolean);
|
|
348
|
+
} catch {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -69,6 +69,8 @@ export interface CompletedDispatch {
|
|
|
69
69
|
prUrl?: string;
|
|
70
70
|
project?: string;
|
|
71
71
|
totalAttempts?: number;
|
|
72
|
+
worktreePath?: string;
|
|
73
|
+
cleanedUp?: boolean;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/** Maps session keys to dispatch context for agent_end hook lookup */
|
|
@@ -370,6 +372,7 @@ export async function completeDispatch(
|
|
|
370
372
|
prUrl: result.prUrl,
|
|
371
373
|
project: active?.project ?? result.project,
|
|
372
374
|
totalAttempts: active?.attempt ?? 0,
|
|
375
|
+
worktreePath: active?.worktreePath,
|
|
373
376
|
};
|
|
374
377
|
await writeDispatchState(filePath, data);
|
|
375
378
|
} finally {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getCachedGuidanceForTeam,
|
|
7
7
|
formatGuidanceAppendix,
|
|
8
8
|
isGuidanceEnabled,
|
|
9
|
+
resolveGuidance,
|
|
9
10
|
_resetGuidanceCacheForTesting,
|
|
10
11
|
} from "./guidance.js";
|
|
11
12
|
|
|
@@ -220,3 +221,55 @@ describe("isGuidanceEnabled", () => {
|
|
|
220
221
|
expect(isGuidanceEnabled({ enableGuidance: true }, undefined)).toBe(true);
|
|
221
222
|
});
|
|
222
223
|
});
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// resolveGuidance
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
describe("resolveGuidance", () => {
|
|
230
|
+
it("extracts guidance from webhook payload and caches it", () => {
|
|
231
|
+
const result = resolveGuidance("team-1", { guidance: "Use TypeScript." });
|
|
232
|
+
expect(result).toBe("Use TypeScript.");
|
|
233
|
+
// Should also be cached now
|
|
234
|
+
expect(getCachedGuidanceForTeam("team-1")).toBe("Use TypeScript.");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("falls back to cache when payload has no guidance", () => {
|
|
238
|
+
cacheGuidanceForTeam("team-1", "Cached guidance");
|
|
239
|
+
const result = resolveGuidance("team-1", {});
|
|
240
|
+
expect(result).toBe("Cached guidance");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("returns null when no guidance anywhere", () => {
|
|
244
|
+
const result = resolveGuidance("team-1", {});
|
|
245
|
+
expect(result).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("returns null when guidance is disabled for team", () => {
|
|
249
|
+
cacheGuidanceForTeam("team-1", "Should be ignored");
|
|
250
|
+
const config = { teamGuidanceOverrides: { "team-1": false } };
|
|
251
|
+
const result = resolveGuidance("team-1", { guidance: "Direct guidance" }, config);
|
|
252
|
+
expect(result).toBeNull();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("returns null when teamId is undefined and no payload guidance", () => {
|
|
256
|
+
const result = resolveGuidance(undefined, {});
|
|
257
|
+
expect(result).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("extracts from payload even with undefined teamId", () => {
|
|
261
|
+
const result = resolveGuidance(undefined, { guidance: "Global guidance" });
|
|
262
|
+
expect(result).toBe("Global guidance");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("returns null when payload is null and cache is empty", () => {
|
|
266
|
+
const result = resolveGuidance("team-1", null);
|
|
267
|
+
expect(result).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("uses cached guidance when payload is null", () => {
|
|
271
|
+
cacheGuidanceForTeam("team-1", "Cached");
|
|
272
|
+
const result = resolveGuidance("team-1", null);
|
|
273
|
+
expect(result).toBe("Cached");
|
|
274
|
+
});
|
|
275
|
+
});
|
package/src/pipeline/guidance.ts
CHANGED
|
@@ -127,6 +127,44 @@ export function formatGuidanceAppendix(guidance: string | null): string {
|
|
|
127
127
|
].join("\n");
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Proactive resolution (webhook → cache → null)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolve guidance for a team through all available sources.
|
|
136
|
+
* Chain: webhook payload → cache → null
|
|
137
|
+
*
|
|
138
|
+
* This replaces ad-hoc cache lookups with a single resolution function.
|
|
139
|
+
* When the cache expires and no webhook guidance is available, returns null.
|
|
140
|
+
*/
|
|
141
|
+
export function resolveGuidance(
|
|
142
|
+
teamId: string | undefined,
|
|
143
|
+
payload: Record<string, unknown> | null,
|
|
144
|
+
pluginConfig?: Record<string, unknown>,
|
|
145
|
+
): string | null {
|
|
146
|
+
// Check if guidance is enabled for this team
|
|
147
|
+
if (!isGuidanceEnabled(pluginConfig, teamId)) return null;
|
|
148
|
+
|
|
149
|
+
// 1. Try extracting from webhook payload (freshest source)
|
|
150
|
+
if (payload) {
|
|
151
|
+
const extracted = extractGuidance(payload);
|
|
152
|
+
if (extracted.guidance) {
|
|
153
|
+
// Cache for future Comment webhook paths
|
|
154
|
+
if (teamId) cacheGuidanceForTeam(teamId, extracted.guidance);
|
|
155
|
+
return extracted.guidance;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Try cache
|
|
160
|
+
if (teamId) {
|
|
161
|
+
const cached = getCachedGuidanceForTeam(teamId);
|
|
162
|
+
if (cached) return cached;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
130
168
|
// ---------------------------------------------------------------------------
|
|
131
169
|
// Config toggle
|
|
132
170
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { runAgent } from "../agent/agent.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Search agent memory by running a short read-only agent session.
|
|
6
|
+
*
|
|
7
|
+
* The agent has access to memory_search, read, glob, grep tools
|
|
8
|
+
* (readOnly mode allows these). It returns search results as text.
|
|
9
|
+
*
|
|
10
|
+
* @param api - OpenClaw plugin API
|
|
11
|
+
* @param agentId - Agent ID to use for the session
|
|
12
|
+
* @param query - Search query string
|
|
13
|
+
* @param timeoutMs - Max time for the search session (default 15s)
|
|
14
|
+
* @returns Text output from the memory search, or empty string on failure
|
|
15
|
+
*/
|
|
16
|
+
export async function searchMemoryViaAgent(
|
|
17
|
+
api: OpenClawPluginApi,
|
|
18
|
+
agentId: string,
|
|
19
|
+
query: string,
|
|
20
|
+
timeoutMs = 15_000,
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
try {
|
|
23
|
+
const result = await runAgent({
|
|
24
|
+
api,
|
|
25
|
+
agentId,
|
|
26
|
+
sessionId: `memory-search-${Date.now()}`,
|
|
27
|
+
message: [
|
|
28
|
+
`Search your memory for information relevant to: "${query}"`,
|
|
29
|
+
`Return ONLY the search results as a bulleted list, one result per line.`,
|
|
30
|
+
`Include the most relevant content snippets. No commentary or explanation.`,
|
|
31
|
+
`If no results found, return exactly: "No relevant memories found."`,
|
|
32
|
+
].join("\n"),
|
|
33
|
+
timeoutMs,
|
|
34
|
+
readOnly: true,
|
|
35
|
+
});
|
|
36
|
+
return result.success ? result.output : "";
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|