@calltelemetry/openclaw-linear 0.9.16 → 0.9.17
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/package.json +1 -1
- package/src/infra/tmux-runner.ts +157 -531
- package/src/infra/tmux.ts +18 -132
- package/src/tools/planner-tools.ts +1 -0
- package/src/tools/steering-tools.ts +56 -91
- package/src/infra/token-refresh-timer.ts +0 -44
- package/src/pipeline/memory-search.ts +0 -40
- package/src/pipeline/retro.ts +0 -231
package/src/infra/tmux.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Check if tmux is available on the system.
|
|
5
5
|
*/
|
|
6
6
|
export function isTmuxAvailable(): boolean {
|
|
7
7
|
try {
|
|
8
|
-
|
|
8
|
+
execSync("tmux -V", { stdio: "ignore" });
|
|
9
9
|
return true;
|
|
10
10
|
} catch {
|
|
11
11
|
return false;
|
|
@@ -13,146 +13,32 @@ export function isTmuxAvailable(): boolean {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* Uses 200x50 terminal size for consistent capture-pane output.
|
|
16
|
+
* Build a deterministic tmux session name from issue identifier, backend, and index.
|
|
18
17
|
*/
|
|
19
|
-
export function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
export function buildSessionName(identifier: string, backend: string, index: number): string {
|
|
19
|
+
// tmux session names can't contain dots or colons — sanitize
|
|
20
|
+
const safe = `${identifier}-${backend}-${index}`.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
21
|
+
return `claw-${safe}`;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
|
-
*
|
|
27
|
-
* Filters for JSON lines only (lines starting with "{") to extract JSONL
|
|
28
|
-
* from raw PTY output (which includes ANSI sequences, prompts, etc).
|
|
25
|
+
* Escape a string for safe shell interpolation (single-quote wrapping).
|
|
29
26
|
*/
|
|
30
|
-
export function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`grep --line-buffered "^{" >> ${shellEscapeForTmux(logPath)}`,
|
|
34
|
-
], { encoding: "utf8", timeout: 5000 });
|
|
27
|
+
export function shellEscape(value: string): string {
|
|
28
|
+
// Wrap in single quotes, escaping any embedded single quotes
|
|
29
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
32
|
/**
|
|
38
|
-
*
|
|
39
|
-
* Appends Enter key to execute the command.
|
|
33
|
+
* Capture the last N lines from a tmux session pane.
|
|
40
34
|
*/
|
|
41
|
-
export function
|
|
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[] {
|
|
35
|
+
export function capturePane(sessionName: string, lines: number): string {
|
|
101
36
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (prefix) return sessions.filter(s => s.startsWith(prefix));
|
|
107
|
-
return sessions;
|
|
37
|
+
return execSync(
|
|
38
|
+
`tmux capture-pane -t ${shellEscape(sessionName)} -p -S -${lines}`,
|
|
39
|
+
{ encoding: "utf8", timeout: 5_000 },
|
|
40
|
+
).trimEnd();
|
|
108
41
|
} catch {
|
|
109
|
-
return
|
|
42
|
+
return "";
|
|
110
43
|
}
|
|
111
44
|
}
|
|
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
|
-
}
|
|
@@ -408,6 +408,7 @@ export function createPlannerTools(): AnyAgentTool[] {
|
|
|
408
408
|
priority: { type: "number", description: "New priority: 1=Urgent, 2=High, 3=Medium, 4=Low" },
|
|
409
409
|
labelIds: {
|
|
410
410
|
type: "array",
|
|
411
|
+
items: { type: "string" },
|
|
411
412
|
description: "Label IDs to set",
|
|
412
413
|
},
|
|
413
414
|
},
|
|
@@ -1,176 +1,141 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
3
|
import { jsonResult } from "openclaw/plugin-sdk";
|
|
3
|
-
import { getActiveTmuxSession
|
|
4
|
-
import {
|
|
4
|
+
import { getActiveTmuxSession } from "../infra/tmux-runner.js";
|
|
5
|
+
import { capturePane, shellEscape } from "../infra/tmux.js";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Create steering tools for interacting with active tmux agent sessions.
|
|
9
|
+
*
|
|
10
|
+
* - steer_agent: Send input to an active agent session via tmux send-keys
|
|
11
|
+
* - capture_agent_output: Capture recent output from an agent's tmux pane
|
|
12
|
+
* - abort_agent: Kill an active agent's tmux session
|
|
13
|
+
*/
|
|
6
14
|
export function createSteeringTools(
|
|
7
15
|
api: OpenClawPluginApi,
|
|
8
16
|
_ctx: Record<string, unknown>,
|
|
9
17
|
): AnyAgentTool[] {
|
|
10
18
|
return [
|
|
11
|
-
// Tool 1: steer_agent
|
|
12
19
|
{
|
|
13
20
|
name: "steer_agent",
|
|
14
21
|
label: "Steer Agent",
|
|
15
22
|
description:
|
|
16
|
-
"Send a message to
|
|
17
|
-
"
|
|
18
|
-
"Use this to inject precise, actionable instructions — do NOT forward raw user text.",
|
|
23
|
+
"Send a message to an active coding agent running in a tmux session. " +
|
|
24
|
+
"Use this to provide information, answer questions, or redirect the agent's approach.",
|
|
19
25
|
parameters: {
|
|
20
26
|
type: "object",
|
|
21
27
|
properties: {
|
|
22
|
-
|
|
28
|
+
issueId: {
|
|
23
29
|
type: "string",
|
|
24
|
-
description: "The
|
|
30
|
+
description: "The Linear issue ID of the active agent session.",
|
|
25
31
|
},
|
|
26
|
-
|
|
32
|
+
message: {
|
|
27
33
|
type: "string",
|
|
28
|
-
description: "
|
|
34
|
+
description: "The message to send to the agent.",
|
|
29
35
|
},
|
|
30
36
|
},
|
|
31
|
-
required: ["
|
|
37
|
+
required: ["issueId", "message"],
|
|
32
38
|
},
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const session = getActiveTmuxSession(issueId);
|
|
39
|
+
async execute(params: { issueId: string; message: string }) {
|
|
40
|
+
const session = getActiveTmuxSession(params.issueId);
|
|
36
41
|
if (!session) {
|
|
37
|
-
return jsonResult({
|
|
38
|
-
success: false,
|
|
39
|
-
error: "no_active_session",
|
|
40
|
-
message: `No active tmux session found for issue ${issueId}. The agent may have already completed.`,
|
|
41
|
-
});
|
|
42
|
+
return jsonResult({ error: `No active tmux session for issue ${params.issueId}` });
|
|
42
43
|
}
|
|
44
|
+
|
|
43
45
|
if (session.steeringMode === "one-shot") {
|
|
44
46
|
return jsonResult({
|
|
45
|
-
|
|
46
|
-
error: "one_shot_mode",
|
|
47
|
-
message: `Cannot steer ${session.backend} — it runs in one-shot mode (Codex exec). ` +
|
|
48
|
-
`You can abort and re-dispatch with updated instructions, or wait for it to complete.`,
|
|
49
|
-
backend: session.backend,
|
|
47
|
+
error: `Agent ${session.backend} is in one-shot mode — steering via stdin is not supported.`,
|
|
50
48
|
});
|
|
51
49
|
}
|
|
50
|
+
|
|
52
51
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
execSync(
|
|
53
|
+
`tmux send-keys -t ${shellEscape(session.sessionName)} ${shellEscape(params.message)} Enter`,
|
|
54
|
+
{ stdio: "ignore", timeout: 5_000 },
|
|
55
|
+
);
|
|
55
56
|
return jsonResult({
|
|
56
57
|
success: true,
|
|
57
|
-
message: `Sent steering input to ${session.backend} agent (${session.issueIdentifier}).`,
|
|
58
|
-
backend: session.backend,
|
|
59
58
|
sessionName: session.sessionName,
|
|
59
|
+
backend: session.backend,
|
|
60
60
|
});
|
|
61
61
|
} catch (err) {
|
|
62
|
-
|
|
63
|
-
return jsonResult({
|
|
64
|
-
success: false,
|
|
65
|
-
error: "send_failed",
|
|
66
|
-
message: `Failed to send keys to tmux session: ${err}`,
|
|
67
|
-
});
|
|
62
|
+
return jsonResult({ error: `Failed to steer agent: ${err}` });
|
|
68
63
|
}
|
|
69
64
|
},
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Tool 2: capture_agent_output
|
|
65
|
+
},
|
|
73
66
|
{
|
|
74
67
|
name: "capture_agent_output",
|
|
75
68
|
label: "Capture Agent Output",
|
|
76
69
|
description:
|
|
77
|
-
"Capture the last
|
|
78
|
-
"Use this to
|
|
70
|
+
"Capture the last 50 lines of output from an active coding agent's tmux session. " +
|
|
71
|
+
"Use this to see what the agent is currently doing before deciding how to steer it.",
|
|
79
72
|
parameters: {
|
|
80
73
|
type: "object",
|
|
81
74
|
properties: {
|
|
82
75
|
issueId: {
|
|
83
76
|
type: "string",
|
|
84
|
-
description: "Linear issue
|
|
77
|
+
description: "The Linear issue ID of the active agent session.",
|
|
85
78
|
},
|
|
86
79
|
lines: {
|
|
87
80
|
type: "number",
|
|
88
|
-
description: "Number of lines to capture (default: 50
|
|
81
|
+
description: "Number of lines to capture (default: 50).",
|
|
89
82
|
},
|
|
90
83
|
},
|
|
91
84
|
required: ["issueId"],
|
|
92
85
|
},
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const session = getActiveTmuxSession(issueId);
|
|
86
|
+
async execute(params: { issueId: string; lines?: number }) {
|
|
87
|
+
const session = getActiveTmuxSession(params.issueId);
|
|
96
88
|
if (!session) {
|
|
97
|
-
return jsonResult({
|
|
98
|
-
success: false,
|
|
99
|
-
error: "no_active_session",
|
|
100
|
-
message: `No active tmux session found for issue ${issueId}.`,
|
|
101
|
-
});
|
|
89
|
+
return jsonResult({ error: `No active tmux session for issue ${params.issueId}` });
|
|
102
90
|
}
|
|
91
|
+
|
|
103
92
|
try {
|
|
104
|
-
const
|
|
105
|
-
const output = capturePane(session.sessionName, lineCount);
|
|
93
|
+
const output = capturePane(session.sessionName, params.lines ?? 50);
|
|
106
94
|
return jsonResult({
|
|
107
|
-
success: true,
|
|
108
|
-
backend: session.backend,
|
|
109
|
-
issueIdentifier: session.issueIdentifier,
|
|
110
95
|
sessionName: session.sessionName,
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
backend: session.backend,
|
|
97
|
+
output: output || "(empty pane)",
|
|
113
98
|
});
|
|
114
99
|
} catch (err) {
|
|
115
|
-
|
|
116
|
-
return jsonResult({
|
|
117
|
-
success: false,
|
|
118
|
-
error: "capture_failed",
|
|
119
|
-
message: `Failed to capture pane output: ${err}`,
|
|
120
|
-
});
|
|
100
|
+
return jsonResult({ error: `Failed to capture output: ${err}` });
|
|
121
101
|
}
|
|
122
102
|
},
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Tool 3: abort_agent
|
|
103
|
+
},
|
|
126
104
|
{
|
|
127
105
|
name: "abort_agent",
|
|
128
106
|
label: "Abort Agent",
|
|
129
107
|
description:
|
|
130
|
-
"Kill
|
|
131
|
-
"or when the agent is stuck. Works for all backends (Claude, Codex, Gemini).",
|
|
108
|
+
"Kill an active coding agent's tmux session. Use this when the user wants to stop a running agent.",
|
|
132
109
|
parameters: {
|
|
133
110
|
type: "object",
|
|
134
111
|
properties: {
|
|
135
112
|
issueId: {
|
|
136
113
|
type: "string",
|
|
137
|
-
description: "Linear issue
|
|
114
|
+
description: "The Linear issue ID of the active agent session.",
|
|
138
115
|
},
|
|
139
116
|
},
|
|
140
117
|
required: ["issueId"],
|
|
141
118
|
},
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
const session = getActiveTmuxSession(issueId);
|
|
119
|
+
async execute(params: { issueId: string }) {
|
|
120
|
+
const session = getActiveTmuxSession(params.issueId);
|
|
145
121
|
if (!session) {
|
|
146
|
-
return jsonResult({
|
|
147
|
-
success: false,
|
|
148
|
-
error: "no_active_session",
|
|
149
|
-
message: `No active tmux session found for issue ${issueId}. It may have already completed.`,
|
|
150
|
-
});
|
|
122
|
+
return jsonResult({ error: `No active tmux session for issue ${params.issueId}` });
|
|
151
123
|
}
|
|
124
|
+
|
|
152
125
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
126
|
+
execSync(
|
|
127
|
+
`tmux kill-session -t ${shellEscape(session.sessionName)}`,
|
|
128
|
+
{ stdio: "ignore", timeout: 5_000 },
|
|
129
|
+
);
|
|
156
130
|
return jsonResult({
|
|
157
131
|
success: true,
|
|
158
|
-
|
|
159
|
-
`The session has been terminated. You can re-dispatch with updated instructions.`,
|
|
132
|
+
killed: session.sessionName,
|
|
160
133
|
backend: session.backend,
|
|
161
|
-
issueIdentifier: session.issueIdentifier,
|
|
162
134
|
});
|
|
163
135
|
} catch (err) {
|
|
164
|
-
|
|
165
|
-
// Still try to unregister even if kill fails
|
|
166
|
-
unregisterTmuxSession(issueId);
|
|
167
|
-
return jsonResult({
|
|
168
|
-
success: false,
|
|
169
|
-
error: "kill_failed",
|
|
170
|
-
message: `Failed to kill tmux session: ${err}. Session unregistered from registry.`,
|
|
171
|
-
});
|
|
136
|
+
return jsonResult({ error: `Failed to abort agent: ${err}` });
|
|
172
137
|
}
|
|
173
138
|
},
|
|
174
|
-
}
|
|
139
|
+
},
|
|
175
140
|
];
|
|
176
141
|
}
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
}
|