@clawmem-ai/clawmem 0.1.9 → 0.1.10
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 +6 -10
- package/openclaw.plugin.json +17 -89
- package/package.json +1 -1
- package/src/config.test.ts +82 -0
- package/src/config.ts +23 -6
- package/src/github-client.ts +28 -1
- package/src/memory.test.ts +2 -4
- package/src/memory.ts +11 -15
- package/src/service.ts +206 -55
- package/src/types.ts +9 -2
- package/src/utils.ts +13 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ openclaw config validate
|
|
|
40
40
|
openclaw gateway restart
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
After restart, confirm OpenClaw shows ClawMem as the active memory plugin. clawmem
|
|
43
|
+
After restart, confirm OpenClaw shows ClawMem as the active memory plugin. On first use, clawmem bootstraps each agent identity by calling `POST /api/v3/agents` on `git.clawmem.ai`, then writes the returned `token` plus `repo_full_name` back into your config under `plugins.entries.clawmem.config.agents.<agentId>` as that agent's `defaultRepo`. Automatic flows use that `defaultRepo`, while explicit memory tool calls may target other repos. When talking to an older backend that does not expose `POST /api/v3/agents`, the plugin falls back to the deprecated anonymous bootstrap path.
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
@@ -274,21 +274,15 @@ Full config with all options:
|
|
|
274
274
|
agents: {
|
|
275
275
|
main: {
|
|
276
276
|
baseUrl: "https://git.clawmem.ai/api/v3",
|
|
277
|
-
|
|
277
|
+
defaultRepo: "owner/main-memory",
|
|
278
278
|
token: "<token>",
|
|
279
279
|
authScheme: "token"
|
|
280
280
|
},
|
|
281
281
|
coder: {
|
|
282
|
-
|
|
282
|
+
defaultRepo: "owner/coder-memory",
|
|
283
283
|
token: "<token>"
|
|
284
284
|
}
|
|
285
285
|
},
|
|
286
|
-
issueTitlePrefix: "Session: ",
|
|
287
|
-
memoryTitlePrefix: "Memory: ",
|
|
288
|
-
defaultLabels: ["source:openclaw"],
|
|
289
|
-
agentLabelPrefix: "agent:",
|
|
290
|
-
autoCreateLabels: true,
|
|
291
|
-
closeIssueOnReset: true,
|
|
292
286
|
turnCommentDelayMs: 1000,
|
|
293
287
|
summaryWaitTimeoutMs: 120000,
|
|
294
288
|
memoryRecallLimit: 5
|
|
@@ -308,8 +302,10 @@ Full config with all options:
|
|
|
308
302
|
- Memory search and auto-injection only return open `type:memory` issues. Closed memory issues are treated as stale.
|
|
309
303
|
- `memory_recall` now prefers the backend `/api/v3/search/issues` endpoint scoped to the current repo plus `label:"type:memory"`; if backend search fails, clawmem falls back to local lexical ranking.
|
|
310
304
|
- Durable memories are extracted best-effort during later request-scoped maintenance, not by background subagent work after a request has already ended.
|
|
311
|
-
- The plugin exposes `memory_list`, `memory_get`, `memory_labels`, `memory_recall`, `memory_store`, `memory_update`, and `memory_forget` for mid-session use.
|
|
305
|
+
- The plugin exposes `memory_repos`, `memory_repo_create`, `memory_list`, `memory_get`, `memory_labels`, `memory_recall`, `memory_store`, `memory_update`, and `memory_forget` for mid-session use.
|
|
306
|
+
- Route resolution is now: agent identity supplies credentials, `defaultRepo` is the fallback memory space, and explicit tool calls may override repo per operation.
|
|
312
307
|
- `memory_store` accepts optional schema hints such as kind and topics; the plugin normalizes them into managed `kind:*` and `topic:*` labels.
|
|
308
|
+
- Memory issues no longer use `session:*` labels. Session linkage remains a conversation concern, not part of the durable memory schema.
|
|
313
309
|
- `memory_update` updates one existing memory issue in place; use it for evolving canonical facts or active tasks instead of creating a duplicate node.
|
|
314
310
|
- Conversation lifecycle is stored in native issue state (`open` while live, `closed` after finalize); memory lifecycle uses native issue state too (`open` active, `closed` stale).
|
|
315
311
|
- Memory issue bodies store the durable detail plus flat metadata such as `memory_hash` and logical `date`; labels are reserved for schema and routing.
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "clawmem",
|
|
3
3
|
"name": "ClawMem",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.10",
|
|
5
5
|
"description": "Mirror OpenClaw sessions into GitHub-compatible issues and comments.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
|
-
"additionalProperties":
|
|
9
|
+
"additionalProperties": true,
|
|
10
10
|
"properties": {
|
|
11
11
|
"baseUrl": {
|
|
12
12
|
"type": "string",
|
|
13
13
|
"minLength": 1,
|
|
14
14
|
"default": "https://git.clawmem.ai"
|
|
15
15
|
},
|
|
16
|
+
"defaultRepo": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"pattern": "^[^/]+/[^/]+$"
|
|
19
|
+
},
|
|
16
20
|
"repo": {
|
|
17
21
|
"type": "string",
|
|
18
22
|
"pattern": "^[^/]+/[^/]+$"
|
|
@@ -36,6 +40,10 @@
|
|
|
36
40
|
"type": "string",
|
|
37
41
|
"minLength": 1
|
|
38
42
|
},
|
|
43
|
+
"defaultRepo": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"pattern": "^[^/]+/[^/]+$"
|
|
46
|
+
},
|
|
39
47
|
"repo": {
|
|
40
48
|
"type": "string",
|
|
41
49
|
"pattern": "^[^/]+/[^/]+$"
|
|
@@ -51,40 +59,6 @@
|
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
},
|
|
54
|
-
"issueTitlePrefix": {
|
|
55
|
-
"type": "string"
|
|
56
|
-
},
|
|
57
|
-
"memoryTitlePrefix": {
|
|
58
|
-
"type": "string"
|
|
59
|
-
},
|
|
60
|
-
"defaultLabels": {
|
|
61
|
-
"type": "array",
|
|
62
|
-
"items": {
|
|
63
|
-
"type": "string"
|
|
64
|
-
},
|
|
65
|
-
"default": ["source:openclaw"]
|
|
66
|
-
},
|
|
67
|
-
"agentLabelPrefix": {
|
|
68
|
-
"type": "string"
|
|
69
|
-
},
|
|
70
|
-
"activeStatusLabel": {
|
|
71
|
-
"type": "string"
|
|
72
|
-
},
|
|
73
|
-
"closedStatusLabel": {
|
|
74
|
-
"type": "string"
|
|
75
|
-
},
|
|
76
|
-
"memoryActiveStatusLabel": {
|
|
77
|
-
"type": "string"
|
|
78
|
-
},
|
|
79
|
-
"memoryStaleStatusLabel": {
|
|
80
|
-
"type": "string"
|
|
81
|
-
},
|
|
82
|
-
"autoCreateLabels": {
|
|
83
|
-
"type": "boolean"
|
|
84
|
-
},
|
|
85
|
-
"closeIssueOnReset": {
|
|
86
|
-
"type": "boolean"
|
|
87
|
-
},
|
|
88
62
|
"turnCommentDelayMs": {
|
|
89
63
|
"type": "integer",
|
|
90
64
|
"minimum": 0,
|
|
@@ -99,15 +73,6 @@
|
|
|
99
73
|
"type": "integer",
|
|
100
74
|
"minimum": 1,
|
|
101
75
|
"maximum": 20
|
|
102
|
-
},
|
|
103
|
-
"labelColor": {
|
|
104
|
-
"type": "string",
|
|
105
|
-
"pattern": "^#?[0-9a-fA-F]{6}$"
|
|
106
|
-
},
|
|
107
|
-
"maxExcerptChars": {
|
|
108
|
-
"type": "integer",
|
|
109
|
-
"minimum": 120,
|
|
110
|
-
"maximum": 4000
|
|
111
76
|
}
|
|
112
77
|
}
|
|
113
78
|
},
|
|
@@ -117,10 +82,15 @@
|
|
|
117
82
|
"placeholder": "https://git.clawmem.ai",
|
|
118
83
|
"help": "GitHub-compatible API base URL. Root URLs are normalized to /api/v3 automatically."
|
|
119
84
|
},
|
|
85
|
+
"defaultRepo": {
|
|
86
|
+
"label": "Default Repo",
|
|
87
|
+
"placeholder": "owner/repo",
|
|
88
|
+
"help": "Default memory repo for automatic flows and for tool calls that do not specify repo explicitly."
|
|
89
|
+
},
|
|
120
90
|
"repo": {
|
|
121
91
|
"label": "Repository",
|
|
122
92
|
"placeholder": "owner/repo",
|
|
123
|
-
"help": "Legacy
|
|
93
|
+
"help": "Legacy alias for defaultRepo. New installs should use defaultRepo or per-agent defaultRepo under agents.<agentId>."
|
|
124
94
|
},
|
|
125
95
|
"token": {
|
|
126
96
|
"label": "API Token",
|
|
@@ -133,41 +103,7 @@
|
|
|
133
103
|
},
|
|
134
104
|
"agents": {
|
|
135
105
|
"label": "Agent Routes",
|
|
136
|
-
"help": "Per-agent ClawMem
|
|
137
|
-
},
|
|
138
|
-
"defaultLabels": {
|
|
139
|
-
"label": "Default Labels",
|
|
140
|
-
"help": "Labels added to every new session issue. Good place for topic:* tags."
|
|
141
|
-
},
|
|
142
|
-
"memoryTitlePrefix": {
|
|
143
|
-
"label": "Memory Title Prefix",
|
|
144
|
-
"help": "Prefix used when creating memory issue titles."
|
|
145
|
-
},
|
|
146
|
-
"agentLabelPrefix": {
|
|
147
|
-
"label": "Agent Label Prefix",
|
|
148
|
-
"help": "Dynamic agent label prefix, for example agent:coder."
|
|
149
|
-
},
|
|
150
|
-
"activeStatusLabel": {
|
|
151
|
-
"label": "Active Status Label",
|
|
152
|
-
"help": "Deprecated compatibility setting. Conversation lifecycle now uses native issue state."
|
|
153
|
-
},
|
|
154
|
-
"closedStatusLabel": {
|
|
155
|
-
"label": "Closed Status Label",
|
|
156
|
-
"help": "Deprecated compatibility setting. Conversation lifecycle now uses native issue state."
|
|
157
|
-
},
|
|
158
|
-
"memoryActiveStatusLabel": {
|
|
159
|
-
"label": "Memory Active Status Label",
|
|
160
|
-
"help": "Deprecated compatibility setting. Memory lifecycle now uses native issue state."
|
|
161
|
-
},
|
|
162
|
-
"memoryStaleStatusLabel": {
|
|
163
|
-
"label": "Memory Stale Status Label",
|
|
164
|
-
"help": "Deprecated compatibility setting. Memory lifecycle now uses native issue state."
|
|
165
|
-
},
|
|
166
|
-
"autoCreateLabels": {
|
|
167
|
-
"label": "Auto Create Labels"
|
|
168
|
-
},
|
|
169
|
-
"closeIssueOnReset": {
|
|
170
|
-
"label": "Close On Reset"
|
|
106
|
+
"help": "Per-agent ClawMem identities keyed by agent id. Each identity has credentials plus an optional defaultRepo."
|
|
171
107
|
},
|
|
172
108
|
"turnCommentDelayMs": {
|
|
173
109
|
"label": "Turn Sync Delay (ms)",
|
|
@@ -180,14 +116,6 @@
|
|
|
180
116
|
"memoryRecallLimit": {
|
|
181
117
|
"label": "Memory Recall Limit",
|
|
182
118
|
"help": "Maximum number of active memories injected into context before an agent starts."
|
|
183
|
-
},
|
|
184
|
-
"labelColor": {
|
|
185
|
-
"label": "Fallback Label Color",
|
|
186
|
-
"placeholder": "0e8a16"
|
|
187
|
-
},
|
|
188
|
-
"maxExcerptChars": {
|
|
189
|
-
"label": "Max Excerpt Chars",
|
|
190
|
-
"help": "Soft cap used when rendering summaries and comment sections."
|
|
191
119
|
}
|
|
192
120
|
}
|
|
193
121
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { hasDefaultRepo, isAgentConfigured, resolveAgentRoute } from "./config.js";
|
|
2
|
+
import type { ClawMemPluginConfig } from "./types.js";
|
|
3
|
+
import { buildAgentBootstrapRegistration, DEFAULT_BOOTSTRAP_REPO_NAME } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
function assert(condition: unknown, message: string): void {
|
|
6
|
+
if (!condition) throw new Error(message);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function baseConfig(): ClawMemPluginConfig {
|
|
10
|
+
return {
|
|
11
|
+
baseUrl: "https://git.clawmem.ai/api/v3",
|
|
12
|
+
authScheme: "token",
|
|
13
|
+
token: "top-token",
|
|
14
|
+
defaultRepo: "global/default-memory",
|
|
15
|
+
repo: "global/legacy-memory",
|
|
16
|
+
agents: {
|
|
17
|
+
main: {
|
|
18
|
+
token: "agent-token",
|
|
19
|
+
defaultRepo: "main/private-memory",
|
|
20
|
+
},
|
|
21
|
+
legacy: {
|
|
22
|
+
token: "legacy-token",
|
|
23
|
+
repo: "legacy/old-memory",
|
|
24
|
+
},
|
|
25
|
+
identityOnly: {
|
|
26
|
+
token: "identity-token",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
memoryRecallLimit: 5,
|
|
30
|
+
turnCommentDelayMs: 1000,
|
|
31
|
+
summaryWaitTimeoutMs: 120000,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function testDefaultRepoResolution(): void {
|
|
36
|
+
const route = resolveAgentRoute(baseConfig(), "main");
|
|
37
|
+
assert(route.defaultRepo === "main/private-memory", "expected per-agent defaultRepo to be preferred");
|
|
38
|
+
assert(route.repo === "main/private-memory", "expected selected repo to default to defaultRepo");
|
|
39
|
+
assert(route.token === "agent-token", "expected per-agent token to be preferred");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function testRepoOverride(): void {
|
|
43
|
+
const route = resolveAgentRoute(baseConfig(), "main", "org/shared-memory");
|
|
44
|
+
assert(route.defaultRepo === "main/private-memory", "expected defaultRepo to remain unchanged");
|
|
45
|
+
assert(route.repo === "org/shared-memory", "expected explicit repo override to win");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function testLegacyRepoFallback(): void {
|
|
49
|
+
const route = resolveAgentRoute(baseConfig(), "legacy");
|
|
50
|
+
assert(route.defaultRepo === "legacy/old-memory", "expected legacy repo to act as defaultRepo fallback");
|
|
51
|
+
assert(route.repo === "legacy/old-memory", "expected selected repo to use the legacy repo fallback");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function testIdentityOnlyStillConfigured(): void {
|
|
55
|
+
const config = baseConfig();
|
|
56
|
+
delete config.defaultRepo;
|
|
57
|
+
delete config.repo;
|
|
58
|
+
const route = resolveAgentRoute(config, "identityOnly");
|
|
59
|
+
assert(isAgentConfigured(route) === true, "expected an identity with baseUrl and token to count as configured");
|
|
60
|
+
assert(hasDefaultRepo(route) === false, "expected no default repo when only credentials are present");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function testBootstrapRegistrationUsesStableDefaults(): void {
|
|
64
|
+
const registration = buildAgentBootstrapRegistration("Main_Coder");
|
|
65
|
+
assert(registration.prefixLogin === "main-coder", "expected agent bootstrap login prefix to match backend format");
|
|
66
|
+
assert(registration.defaultRepoName === DEFAULT_BOOTSTRAP_REPO_NAME, "expected bootstrap repo name to use the stable default");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function testBootstrapRegistrationTrimsLongPrefixes(): void {
|
|
70
|
+
const registration = buildAgentBootstrapRegistration("___THIS_IS_A_SUPER_LONG_AGENT_ID_THAT_SHOULD_BE_TRIMMED___");
|
|
71
|
+
assert(/^[a-z0-9][a-z0-9-]*$/.test(registration.prefixLogin), "expected bootstrap login prefix to satisfy backend validation");
|
|
72
|
+
assert(registration.prefixLogin.length <= 32, "expected bootstrap login prefix to fit backend max length");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
testDefaultRepoResolution();
|
|
76
|
+
testRepoOverride();
|
|
77
|
+
testLegacyRepoFallback();
|
|
78
|
+
testIdentityOnlyStillConfigured();
|
|
79
|
+
testBootstrapRegistrationUsesStableDefaults();
|
|
80
|
+
testBootstrapRegistrationTrimsLongPrefixes();
|
|
81
|
+
|
|
82
|
+
console.log("config tests passed");
|
package/src/config.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { normalizeAgentId } from "./utils.js";
|
|
|
5
5
|
|
|
6
6
|
export const SESSION_TITLE_PREFIX = "Session: ";
|
|
7
7
|
export const MEMORY_TITLE_PREFIX = "Memory: ";
|
|
8
|
-
export const DEFAULT_LABELS: readonly string[] = [
|
|
8
|
+
export const DEFAULT_LABELS: readonly string[] = [];
|
|
9
9
|
export const AGENT_LABEL_PREFIX = "agent:";
|
|
10
10
|
export const LABEL_ACTIVE = "status:active";
|
|
11
11
|
export const LABEL_CLOSED = "status:closed";
|
|
@@ -31,6 +31,7 @@ export function resolvePluginConfig(api: OpenClawPluginApi): ClawMemPluginConfig
|
|
|
31
31
|
const agent = rawAgentConfig as Record<string, unknown>;
|
|
32
32
|
agents[agentId] = {
|
|
33
33
|
baseUrl: str(agent.baseUrl)?.replace(/\/+$/, ""),
|
|
34
|
+
defaultRepo: normalizeRepoName(str(agent.defaultRepo) ?? str(agent.repo)),
|
|
34
35
|
repo: str(agent.repo),
|
|
35
36
|
token: str(agent.token),
|
|
36
37
|
authScheme: agent.authScheme === "bearer" ? "bearer" : agent.authScheme === "token" ? "token" : undefined,
|
|
@@ -38,6 +39,9 @@ export function resolvePluginConfig(api: OpenClawPluginApi): ClawMemPluginConfig
|
|
|
38
39
|
}
|
|
39
40
|
return {
|
|
40
41
|
baseUrl: baseUrl.endsWith("/api/v3") ? baseUrl : `${baseUrl}/api/v3`,
|
|
42
|
+
defaultRepo: normalizeRepoName(str(raw.defaultRepo) ?? str(raw.repo)),
|
|
43
|
+
repo: normalizeRepoName(str(raw.repo)),
|
|
44
|
+
token: str(raw.token),
|
|
41
45
|
authScheme: raw.authScheme === "bearer" ? "bearer" : "token",
|
|
42
46
|
agents,
|
|
43
47
|
memoryRecallLimit: clamp(num(raw.memoryRecallLimit, 5), 1, 20),
|
|
@@ -46,21 +50,28 @@ export function resolvePluginConfig(api: OpenClawPluginApi): ClawMemPluginConfig
|
|
|
46
50
|
};
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
export function resolveAgentRoute(config: ClawMemPluginConfig, agentId?: string): ClawMemResolvedRoute {
|
|
53
|
+
export function resolveAgentRoute(config: ClawMemPluginConfig, agentId?: string, repoOverride?: string): ClawMemResolvedRoute {
|
|
50
54
|
const id = normalizeAgentId(agentId);
|
|
51
55
|
const agent = config.agents[id] ?? {};
|
|
52
56
|
const baseUrl = (agent.baseUrl ?? config.baseUrl).replace(/\/+$/, "");
|
|
57
|
+
const defaultRepo = normalizeRepoName(agent.defaultRepo ?? agent.repo) ?? config.defaultRepo ?? normalizeRepoName(config.repo);
|
|
58
|
+
const repo = normalizeRepoName(repoOverride) ?? defaultRepo;
|
|
53
59
|
return {
|
|
54
60
|
agentId: id,
|
|
55
61
|
baseUrl: baseUrl.endsWith("/api/v3") ? baseUrl : `${baseUrl}/api/v3`,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
...(defaultRepo ? { defaultRepo } : {}),
|
|
63
|
+
...(repo ? { repo } : {}),
|
|
64
|
+
token: agent.token?.trim() || config.token?.trim() || undefined,
|
|
65
|
+
authScheme: agent.authScheme === "bearer" ? "bearer" : agent.authScheme === "token" ? "token" : config.authScheme,
|
|
59
66
|
};
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
export function isAgentConfigured(route: ClawMemResolvedRoute): boolean {
|
|
63
|
-
return Boolean(route.baseUrl && route.
|
|
70
|
+
return Boolean(route.baseUrl && route.token);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function hasDefaultRepo(route: ClawMemResolvedRoute): boolean {
|
|
74
|
+
return Boolean(route.defaultRepo);
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
export function resolveLabelColor(label: string): string {
|
|
@@ -97,3 +108,9 @@ export function labelVal(labels: string[], prefix: string): string | undefined {
|
|
|
97
108
|
const m = labels.find((l) => l.startsWith(prefix));
|
|
98
109
|
return m ? m.slice(prefix.length).trim() || undefined : undefined;
|
|
99
110
|
}
|
|
111
|
+
|
|
112
|
+
function normalizeRepoName(value: string | undefined): string | undefined {
|
|
113
|
+
if (!value) return undefined;
|
|
114
|
+
const trimmed = value.trim().replace(/^\/+|\/+$/g, "");
|
|
115
|
+
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : undefined;
|
|
116
|
+
}
|
package/src/github-client.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// GitHub Issues API client for clawmem. No label caching — idempotent create-if-absent.
|
|
2
2
|
import { resolveLabelColor, labelDescription, extractLabelNames, isManagedLabel } from "./config.js";
|
|
3
|
-
import type { AnonymousSessionResponse, ClawMemResolvedRoute } from "./types.js";
|
|
3
|
+
import type { AgentRegistrationResponse, AnonymousSessionResponse, ClawMemResolvedRoute } from "./types.js";
|
|
4
4
|
|
|
5
5
|
type IssueResponse = { number: number; title?: string; body?: string; state?: string; labels?: Array<{ name?: string } | string> };
|
|
6
6
|
type SearchIssuesResponse = { items?: IssueResponse[]; total_count?: number; incomplete_results?: boolean };
|
|
7
7
|
type CommentResponse = { id?: number; body?: string; created_at?: string };
|
|
8
8
|
type LabelResponse = { name?: string; color?: string; description?: string };
|
|
9
|
+
type RepoResponse = { name?: string; full_name?: string; description?: string; private?: boolean; owner?: { login?: string } };
|
|
9
10
|
type ReqOpts = { allowNotFound?: boolean; allowValidationError?: boolean; omitAuth?: boolean };
|
|
10
11
|
|
|
11
12
|
export class GitHubIssueClient {
|
|
@@ -14,6 +15,9 @@ export class GitHubIssueClient {
|
|
|
14
15
|
repo(): string | undefined {
|
|
15
16
|
return this.config.repo?.trim() || undefined;
|
|
16
17
|
}
|
|
18
|
+
defaultRepo(): string | undefined {
|
|
19
|
+
return this.config.defaultRepo?.trim() || undefined;
|
|
20
|
+
}
|
|
17
21
|
|
|
18
22
|
async createIssue(params: { title: string; body: string; labels: string[] }): Promise<IssueResponse> {
|
|
19
23
|
return this.req<IssueResponse>(this.repoPath("issues"), { method: "POST", body: JSON.stringify(params) });
|
|
@@ -53,6 +57,20 @@ export class GitHubIssueClient {
|
|
|
53
57
|
q.set("per_page", String(params?.perPage ?? 100));
|
|
54
58
|
return this.req<LabelResponse[]>(`${this.repoPath("labels")}?${q}`, { method: "GET" });
|
|
55
59
|
}
|
|
60
|
+
async listUserRepos(): Promise<RepoResponse[]> {
|
|
61
|
+
return this.req<RepoResponse[]>("user/repos", { method: "GET" });
|
|
62
|
+
}
|
|
63
|
+
async createUserRepo(params: { name: string; description?: string; private?: boolean; autoInit?: boolean }): Promise<RepoResponse> {
|
|
64
|
+
return this.req<RepoResponse>("user/repos", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
name: params.name,
|
|
68
|
+
...(params.description ? { description: params.description } : {}),
|
|
69
|
+
private: params.private ?? true,
|
|
70
|
+
auto_init: params.autoInit ?? false,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
56
74
|
async ensureLabels(labels: string[]): Promise<void> {
|
|
57
75
|
for (const label of labels) {
|
|
58
76
|
if (!label.trim()) continue;
|
|
@@ -71,6 +89,15 @@ export class GitHubIssueClient {
|
|
|
71
89
|
async updateRepoDescription(description: string): Promise<void> {
|
|
72
90
|
await this.req(this.repoPath("").replace(/\/$/, ""), { method: "PATCH", body: JSON.stringify({ description }) });
|
|
73
91
|
}
|
|
92
|
+
async registerAgent(prefixLogin: string, defaultRepoName: string): Promise<AgentRegistrationResponse> {
|
|
93
|
+
return this.req<AgentRegistrationResponse>("agents", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
prefix_login: prefixLogin,
|
|
97
|
+
default_repo_name: defaultRepoName,
|
|
98
|
+
}),
|
|
99
|
+
}, { omitAuth: true });
|
|
100
|
+
}
|
|
74
101
|
async createAnonymousSession(locale?: string): Promise<AnonymousSessionResponse> {
|
|
75
102
|
const body = locale ? JSON.stringify({ locale }) : undefined;
|
|
76
103
|
return this.req<AnonymousSessionResponse>("anonymous/session", { method: "POST", ...(body ? { body } : {}) }, { omitAuth: true });
|
package/src/memory.test.ts
CHANGED
|
@@ -7,7 +7,6 @@ function memory(overrides: Partial<ParsedMemoryIssue> = {}): ParsedMemoryIssue {
|
|
|
7
7
|
issueNumber: overrides.issueNumber ?? 1,
|
|
8
8
|
title: overrides.title ?? "Memory: Example",
|
|
9
9
|
memoryId: overrides.memoryId ?? String(overrides.issueNumber ?? 1),
|
|
10
|
-
sessionId: overrides.sessionId ?? "sess-1",
|
|
11
10
|
date: overrides.date ?? "2026-03-23",
|
|
12
11
|
detail: overrides.detail ?? "Example durable detail",
|
|
13
12
|
status: overrides.status ?? "active",
|
|
@@ -32,7 +31,6 @@ function issueFromMemory(m: ParsedMemoryIssue): IssueRecord {
|
|
|
32
31
|
state: m.status === "stale" ? "closed" : "open",
|
|
33
32
|
labels: [
|
|
34
33
|
"type:memory",
|
|
35
|
-
`session:${m.sessionId}`,
|
|
36
34
|
...(m.kind ? [`kind:${m.kind}`] : []),
|
|
37
35
|
...(m.topics ?? []).map((topic) => `topic:${topic}`),
|
|
38
36
|
],
|
|
@@ -158,7 +156,7 @@ async function testStructuredStoreAndSchema(): Promise<void> {
|
|
|
158
156
|
},
|
|
159
157
|
};
|
|
160
158
|
const store = new MemoryStore(client as never, {} as never, { memoryRecallLimit: 5, turnCommentDelayMs: 1000, summaryWaitTimeoutMs: 120000 } as never);
|
|
161
|
-
const result = await store.store({ detail: "Redis Lua scripts are required for atomic rate limiting.", kind: "Lesson", topics: ["Redis Ops", "rate_limit"] }
|
|
159
|
+
const result = await store.store({ detail: "Redis Lua scripts are required for atomic rate limiting.", kind: "Lesson", topics: ["Redis Ops", "rate_limit"] });
|
|
162
160
|
const schema = await store.listSchema();
|
|
163
161
|
|
|
164
162
|
assert(result.created === true, "expected a new structured memory to be created");
|
|
@@ -168,6 +166,7 @@ async function testStructuredStoreAndSchema(): Promise<void> {
|
|
|
168
166
|
assert(created[0]?.labels.includes("kind:lesson"), "expected created labels to include normalized kind");
|
|
169
167
|
assert(created[0]?.labels.includes("topic:redis-ops"), "expected created labels to include normalized topic");
|
|
170
168
|
assert(created[0]?.labels.includes("topic:rate-limit"), "expected created labels to include normalized topic");
|
|
169
|
+
assert(!created[0]?.labels.some((label) => label.startsWith("session:")), "expected manual memory_store writes to omit synthetic session labels");
|
|
171
170
|
assert(!created[0]?.labels.some((label) => label.startsWith("date:")), "expected new memory labels to omit date labels");
|
|
172
171
|
assert(created[0]?.body.includes(`date: ${result.memory.date}`), "expected new memory body to retain logical date metadata");
|
|
173
172
|
assert(ensured[0]?.includes("kind:lesson"), "expected ensureLabels to include kind label");
|
|
@@ -249,7 +248,6 @@ async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
|
|
|
249
248
|
const recalled = await store.search("F1 Dota 2", 5);
|
|
250
249
|
|
|
251
250
|
assert(exact?.issueNumber === 4, "expected legacy memory without session/date to be readable");
|
|
252
|
-
assert(exact?.sessionId === "legacy", "expected missing session label to fall back to legacy");
|
|
253
251
|
assert(exact?.date === "1970-01-01", "expected missing date label to fall back to a placeholder");
|
|
254
252
|
assert(recalled.some((memory) => memory.issueNumber === 4), "expected legacy memory to participate in recall");
|
|
255
253
|
}
|
package/src/memory.ts
CHANGED
|
@@ -71,7 +71,7 @@ export class MemoryStore {
|
|
|
71
71
|
.slice(0, limit);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
async store(draft: MemoryDraft
|
|
74
|
+
async store(draft: MemoryDraft): Promise<{ created: boolean; memory: ParsedMemoryIssue }> {
|
|
75
75
|
const normalized = normalizeDraft(draft);
|
|
76
76
|
const detail = norm(normalized.detail);
|
|
77
77
|
const allActive = await this.listByStatus("active");
|
|
@@ -83,7 +83,7 @@ export class MemoryStore {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const date = localDate();
|
|
86
|
-
const labels = memLabels(
|
|
86
|
+
const labels = memLabels(normalized.kind, normalized.topics);
|
|
87
87
|
const title = `${MEMORY_TITLE_PREFIX}${trunc(detail, 72)}`;
|
|
88
88
|
const body = stringifyFlatYaml([["memory_hash", hash], ["date", date], ["detail", detail]]);
|
|
89
89
|
await this.client.ensureLabels(labels);
|
|
@@ -95,7 +95,6 @@ export class MemoryStore {
|
|
|
95
95
|
title,
|
|
96
96
|
memoryId: String(issue.number),
|
|
97
97
|
memoryHash: hash,
|
|
98
|
-
sessionId,
|
|
99
98
|
date,
|
|
100
99
|
detail,
|
|
101
100
|
...(normalized.kind ? { kind: normalized.kind } : {}),
|
|
@@ -121,7 +120,7 @@ export class MemoryStore {
|
|
|
121
120
|
if (duplicate) throw new Error(`another active memory already stores this detail as [${duplicate.memoryId}]`);
|
|
122
121
|
const nextTitle = `${MEMORY_TITLE_PREFIX}${trunc(nextDetail, 72)}`;
|
|
123
122
|
const nextBody = stringifyFlatYaml([["memory_hash", nextHash], ["date", current.date], ["detail", nextDetail]]);
|
|
124
|
-
const nextLabels = memLabels(
|
|
123
|
+
const nextLabels = memLabels(nextKind, nextTopics);
|
|
125
124
|
await this.client.ensureLabels(nextLabels);
|
|
126
125
|
await this.client.updateIssue(current.issueNumber, { title: nextTitle, body: nextBody });
|
|
127
126
|
await this.client.syncManagedLabels(current.issueNumber, nextLabels);
|
|
@@ -140,7 +139,7 @@ export class MemoryStore {
|
|
|
140
139
|
if (!id) throw new Error("memoryId is empty");
|
|
141
140
|
const mem = await this.get(id, "active");
|
|
142
141
|
if (!mem) return null;
|
|
143
|
-
await this.client.syncManagedLabels(mem.issueNumber, memLabels(mem.
|
|
142
|
+
await this.client.syncManagedLabels(mem.issueNumber, memLabels(mem.kind, mem.topics));
|
|
144
143
|
await this.client.updateIssue(mem.issueNumber, { state: "closed" });
|
|
145
144
|
return { ...mem, status: "stale" };
|
|
146
145
|
}
|
|
@@ -148,7 +147,7 @@ export class MemoryStore {
|
|
|
148
147
|
async syncFromConversation(session: SessionMirrorState, snapshot: TranscriptSnapshot): Promise<boolean> {
|
|
149
148
|
try {
|
|
150
149
|
const decision = await this.generateDecision(session, snapshot);
|
|
151
|
-
const { savedCount, staledCount } = await this.applyDecision(
|
|
150
|
+
const { savedCount, staledCount } = await this.applyDecision(decision);
|
|
152
151
|
if (savedCount > 0 || staledCount > 0)
|
|
153
152
|
this.api.logger.info?.(`clawmem: synced memories for ${session.sessionId} (saved=${savedCount}, stale=${staledCount})`);
|
|
154
153
|
return true;
|
|
@@ -199,7 +198,7 @@ export class MemoryStore {
|
|
|
199
198
|
private parseIssue(issue: { number: number; title?: string; body?: string; state?: string; labels?: Array<{ name?: string } | string> }): ParsedMemoryIssue | null {
|
|
200
199
|
const labels = extractLabelNames(issue.labels);
|
|
201
200
|
if (!labels.includes("type:memory")) return null;
|
|
202
|
-
const
|
|
201
|
+
const kind = labelVal(labels, "kind:");
|
|
203
202
|
const topics = labels.filter((l) => l.startsWith("topic:")).map((l) => l.slice(6).trim()).filter(Boolean);
|
|
204
203
|
const rawBody = (issue.body ?? "").trim();
|
|
205
204
|
const body = rawBody ? parseFlatYaml(rawBody) : {};
|
|
@@ -210,7 +209,6 @@ export class MemoryStore {
|
|
|
210
209
|
issueNumber: issue.number, title: issue.title?.trim() || "",
|
|
211
210
|
memoryId: body.memory_id?.trim() || String(issue.number),
|
|
212
211
|
memoryHash: body.memory_hash?.trim() || undefined,
|
|
213
|
-
sessionId: sessionId || "legacy",
|
|
214
212
|
date: body.date?.trim() || "1970-01-01",
|
|
215
213
|
detail,
|
|
216
214
|
...(kind ? { kind } : {}),
|
|
@@ -219,7 +217,7 @@ export class MemoryStore {
|
|
|
219
217
|
};
|
|
220
218
|
}
|
|
221
219
|
|
|
222
|
-
private async applyDecision(
|
|
220
|
+
private async applyDecision(decision: MemoryDecision): Promise<{ savedCount: number; staledCount: number }> {
|
|
223
221
|
const allActive = await this.listByStatus("active");
|
|
224
222
|
const activeById = new Map(allActive.map((m) => [m.memoryId, m]));
|
|
225
223
|
const activeByHash = new Map(allActive.map((m) => [m.memoryHash || sha256(norm(m.detail)), m]));
|
|
@@ -235,7 +233,7 @@ export class MemoryStore {
|
|
|
235
233
|
activeByHash.set(hash, merged);
|
|
236
234
|
continue;
|
|
237
235
|
}
|
|
238
|
-
const labels = memLabels(
|
|
236
|
+
const labels = memLabels(draft.kind, draft.topics);
|
|
239
237
|
const date = localDate();
|
|
240
238
|
const title = `${MEMORY_TITLE_PREFIX}${trunc(detail, 72)}`;
|
|
241
239
|
const body = stringifyFlatYaml([["memory_hash", hash], ["date", date], ["detail", detail]]);
|
|
@@ -246,7 +244,6 @@ export class MemoryStore {
|
|
|
246
244
|
title,
|
|
247
245
|
memoryId: String(issue.number),
|
|
248
246
|
memoryHash: hash,
|
|
249
|
-
sessionId,
|
|
250
247
|
date,
|
|
251
248
|
detail,
|
|
252
249
|
...(draft.kind ? { kind: draft.kind } : {}),
|
|
@@ -259,7 +256,7 @@ export class MemoryStore {
|
|
|
259
256
|
for (const id of [...new Set(decision.stale.map((s) => s.trim()).filter(Boolean))]) {
|
|
260
257
|
const mem = activeById.get(id);
|
|
261
258
|
if (!mem) continue;
|
|
262
|
-
await this.client.syncManagedLabels(mem.issueNumber, memLabels(mem.
|
|
259
|
+
await this.client.syncManagedLabels(mem.issueNumber, memLabels(mem.kind, mem.topics));
|
|
263
260
|
await this.client.updateIssue(mem.issueNumber, { state: "closed" });
|
|
264
261
|
staledCount++;
|
|
265
262
|
}
|
|
@@ -318,7 +315,7 @@ export class MemoryStore {
|
|
|
318
315
|
const sameKind = (memory.kind ?? "") === (nextKind ?? "");
|
|
319
316
|
const sameTopics = JSON.stringify(currentTopics) === JSON.stringify(nextTopics);
|
|
320
317
|
if (sameKind && sameTopics) return memory;
|
|
321
|
-
const labels = memLabels(
|
|
318
|
+
const labels = memLabels(nextKind, nextTopics);
|
|
322
319
|
await this.client.ensureLabels(labels);
|
|
323
320
|
await this.client.syncManagedLabels(memory.issueNumber, labels);
|
|
324
321
|
return {
|
|
@@ -329,10 +326,9 @@ export class MemoryStore {
|
|
|
329
326
|
}
|
|
330
327
|
}
|
|
331
328
|
|
|
332
|
-
function memLabels(
|
|
329
|
+
function memLabels(kind?: string, topics?: string[]): string[] {
|
|
333
330
|
return [
|
|
334
331
|
"type:memory",
|
|
335
|
-
`session:${sessionId}`,
|
|
336
332
|
...(kind ? [`kind:${kind}`] : []),
|
|
337
333
|
...((topics ?? []).map((topic) => `topic:${topic}`)),
|
|
338
334
|
];
|
package/src/service.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Thin orchestrator: wires conversation mirroring, memory store, and plugin lifecycle.
|
|
2
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
3
|
-
import { isAgentConfigured, resolveAgentRoute, resolvePluginConfig } from "./config.js";
|
|
3
|
+
import { hasDefaultRepo, isAgentConfigured, resolveAgentRoute, resolvePluginConfig } from "./config.js";
|
|
4
4
|
import { ConversationMirror } from "./conversation.js";
|
|
5
5
|
import { GitHubIssueClient } from "./github-client.js";
|
|
6
6
|
import { KeyedAsyncQueue } from "./keyed-async-queue.js";
|
|
7
7
|
import { MemoryStore } from "./memory.js";
|
|
8
8
|
import { loadState, resolveStatePath, saveState } from "./state.js";
|
|
9
9
|
import { readTranscriptSnapshot } from "./transcript.js";
|
|
10
|
-
import type { ClawMemPluginConfig, PluginState, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
11
|
-
import { inferAgentIdFromTranscriptPath, normalizeAgentId, sessionScopeKey } from "./utils.js";
|
|
10
|
+
import type { BootstrapIdentityResponse, ClawMemPluginConfig, ClawMemResolvedRoute, PluginState, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
11
|
+
import { buildAgentBootstrapRegistration, inferAgentIdFromTranscriptPath, normalizeAgentId, sessionScopeKey } from "./utils.js";
|
|
12
12
|
|
|
13
13
|
type TurnPayload = { sessionId?: string; sessionKey?: string; agentId?: string; messages: unknown[] };
|
|
14
14
|
type FinalizePayload = { sessionId?: string; sessionKey?: string; sessionFile?: string; agentId?: string; reason?: string; messages?: unknown[] };
|
|
@@ -46,7 +46,8 @@ class ClawMemService {
|
|
|
46
46
|
void this.track(this.handleTranscript(u.sessionFile)).catch((e) => this.warn("transcript update", e));
|
|
47
47
|
});
|
|
48
48
|
const configuredCount = Object.keys(this.config.agents).filter((agentId) => {
|
|
49
|
-
|
|
49
|
+
const route = resolveAgentRoute(this.config, agentId);
|
|
50
|
+
return isAgentConfigured(route) && hasDefaultRepo(route);
|
|
50
51
|
}).length;
|
|
51
52
|
this.api.logger.info?.(
|
|
52
53
|
configuredCount > 0
|
|
@@ -64,6 +65,86 @@ class ClawMemService {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
private registerTools(): void {
|
|
68
|
+
this.api.registerTool({
|
|
69
|
+
name: "memory_repos",
|
|
70
|
+
description: "List the memory repos the current ClawMem agent identity can access so the agent can choose the right space before retrieving or storing memory.",
|
|
71
|
+
required: true,
|
|
72
|
+
parameters: {
|
|
73
|
+
type: "object",
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
properties: {
|
|
76
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
execute: async (_id: string, params: unknown) => {
|
|
80
|
+
const p = asRecord(params);
|
|
81
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
82
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
83
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
84
|
+
const repos = await resolved.client.listUserRepos();
|
|
85
|
+
if (repos.length === 0) return toolText(`Agent "${agentId}" has no accessible ClawMem repos yet.`);
|
|
86
|
+
const lines = [
|
|
87
|
+
`Accessible ClawMem repos for agent "${agentId}":`,
|
|
88
|
+
...repos
|
|
89
|
+
.map((repo) => {
|
|
90
|
+
const fullName = repo.full_name?.trim() || repo.name?.trim() || "unknown";
|
|
91
|
+
const flags = [
|
|
92
|
+
resolved.route.defaultRepo === fullName ? "default" : "",
|
|
93
|
+
repo.private ? "private" : "shared",
|
|
94
|
+
].filter(Boolean).join(", ");
|
|
95
|
+
const description = repo.description?.trim() ? ` - ${repo.description.trim()}` : "";
|
|
96
|
+
return `- ${fullName}${flags ? ` [${flags}]` : ""}${description}`;
|
|
97
|
+
}),
|
|
98
|
+
];
|
|
99
|
+
return toolText(lines.join("\n"));
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.api.registerTool({
|
|
104
|
+
name: "memory_repo_create",
|
|
105
|
+
description: "Create a new ClawMem repo under the current agent identity when the agent decides a new memory space is needed.",
|
|
106
|
+
required: true,
|
|
107
|
+
parameters: {
|
|
108
|
+
type: "object",
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
properties: {
|
|
111
|
+
name: { type: "string", minLength: 1, description: "Repository name only, without owner prefix." },
|
|
112
|
+
description: { type: "string", minLength: 1, description: "Optional repo description." },
|
|
113
|
+
private: { type: "boolean", description: "Whether the new repo should be private. Defaults to true." },
|
|
114
|
+
setDefault: { type: "boolean", description: "Whether to make the new repo this agent's default memory repo." },
|
|
115
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
116
|
+
},
|
|
117
|
+
required: ["name"],
|
|
118
|
+
},
|
|
119
|
+
execute: async (_id: string, params: unknown) => {
|
|
120
|
+
const p = asRecord(params);
|
|
121
|
+
const name = typeof p.name === "string" ? p.name.trim() : "";
|
|
122
|
+
if (!name) return toolText("name is empty.");
|
|
123
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
124
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
125
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
126
|
+
const created = await resolved.client.createUserRepo({
|
|
127
|
+
name,
|
|
128
|
+
...(typeof p.description === "string" && p.description.trim() ? { description: p.description.trim() } : {}),
|
|
129
|
+
...(typeof p.private === "boolean" ? { private: p.private } : {}),
|
|
130
|
+
});
|
|
131
|
+
const fullName = created.full_name?.trim() || created.name?.trim() || name;
|
|
132
|
+
let defaultNote = "";
|
|
133
|
+
const shouldSetDefault = p.setDefault === true || !resolved.route.defaultRepo;
|
|
134
|
+
if (shouldSetDefault && fullName.includes("/")) {
|
|
135
|
+
await this.persistAgentConfig(agentId, {
|
|
136
|
+
baseUrl: resolved.route.baseUrl,
|
|
137
|
+
authScheme: resolved.route.authScheme,
|
|
138
|
+
token: resolved.route.token!,
|
|
139
|
+
defaultRepo: fullName,
|
|
140
|
+
});
|
|
141
|
+
this.config.agents[agentId] = { ...(this.config.agents[agentId] ?? {}), defaultRepo: fullName };
|
|
142
|
+
defaultNote = resolved.route.defaultRepo ? "\nSet as default repo for this agent." : "\nSet as the first default repo for this agent.";
|
|
143
|
+
}
|
|
144
|
+
return toolText(`Created memory repo ${fullName}.${defaultNote}`);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
67
148
|
this.api.registerTool({
|
|
68
149
|
name: "memory_list",
|
|
69
150
|
description: "List ClawMem memories by status or schema so the agent can inspect the current memory index before deduping or saving.",
|
|
@@ -76,25 +157,26 @@ class ClawMemService {
|
|
|
76
157
|
kind: { type: "string", minLength: 1, description: "Optional kind filter, for example core-fact, lesson, or task." },
|
|
77
158
|
topic: { type: "string", minLength: 1, description: "Optional topic filter." },
|
|
78
159
|
limit: { type: "integer", minimum: 1, maximum: 200, description: "Maximum number of memories to return." },
|
|
160
|
+
repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
79
161
|
agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
|
|
80
162
|
},
|
|
81
163
|
},
|
|
82
164
|
execute: async (_id: string, params: unknown) => {
|
|
83
165
|
const p = asRecord(params);
|
|
84
166
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
85
|
-
|
|
86
|
-
|
|
167
|
+
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
168
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
87
169
|
const status = p.status === "stale" || p.status === "all" ? p.status : "active";
|
|
88
170
|
const limit = typeof p.limit === "number" && Number.isFinite(p.limit) ? Math.floor(p.limit) : 20;
|
|
89
171
|
const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
|
|
90
172
|
const topic = typeof p.topic === "string" && p.topic.trim() ? p.topic.trim() : undefined;
|
|
91
|
-
const memories = await mem.listMemories({ status, kind, topic, limit });
|
|
173
|
+
const memories = await resolved.mem.listMemories({ status, kind, topic, limit });
|
|
92
174
|
if (memories.length === 0) {
|
|
93
175
|
const filters = [status !== "active" ? `status=${status}` : "", kind ? `kind=${kind}` : "", topic ? `topic=${topic}` : ""].filter(Boolean).join(", ");
|
|
94
|
-
return toolText(`No memories matched${filters ? ` (${filters})` : ""}.`);
|
|
176
|
+
return toolText(`No memories matched in ${resolved.route.repo}${filters ? ` (${filters})` : ""}.`);
|
|
95
177
|
}
|
|
96
178
|
const lines = [
|
|
97
|
-
`Found ${memories.length} ${status === "all" ? "" : `${status} `}memor${memories.length === 1 ? "y" : "ies"}:`,
|
|
179
|
+
`Found ${memories.length} ${status === "all" ? "" : `${status} `}memor${memories.length === 1 ? "y" : "ies"} in ${resolved.route.repo}:`,
|
|
98
180
|
...memories.map((memory) => `- ${renderMemoryLine(memory)}`),
|
|
99
181
|
];
|
|
100
182
|
return toolText(lines.join("\n"));
|
|
@@ -109,6 +191,7 @@ class ClawMemService {
|
|
|
109
191
|
type: "object",
|
|
110
192
|
additionalProperties: false,
|
|
111
193
|
properties: {
|
|
194
|
+
repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
112
195
|
agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
|
|
113
196
|
limitTopics: { type: "integer", minimum: 1, maximum: 200, description: "Maximum number of topic labels to display." },
|
|
114
197
|
},
|
|
@@ -116,16 +199,16 @@ class ClawMemService {
|
|
|
116
199
|
execute: async (_id: string, params: unknown) => {
|
|
117
200
|
const p = asRecord(params);
|
|
118
201
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const schema = await mem.listSchema();
|
|
202
|
+
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
203
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
204
|
+
const schema = await resolved.mem.listSchema();
|
|
122
205
|
const rawLimit = typeof p.limitTopics === "number" && Number.isFinite(p.limitTopics) ? Math.floor(p.limitTopics) : 50;
|
|
123
206
|
const limitTopics = Math.min(200, Math.max(1, rawLimit));
|
|
124
207
|
const kinds = schema.kinds.length > 0 ? schema.kinds.map((kind) => `- kind:${kind}`).join("\n") : "- None";
|
|
125
208
|
const topics = schema.topics.length > 0 ? schema.topics.slice(0, limitTopics).map((topic) => `- topic:${topic}`).join("\n") : "- None";
|
|
126
209
|
const extra = schema.topics.length > limitTopics ? `\n- ...and ${schema.topics.length - limitTopics} more topics` : "";
|
|
127
210
|
return toolText([
|
|
128
|
-
|
|
211
|
+
`Current ClawMem schema labels in ${resolved.route.repo}:`,
|
|
129
212
|
"",
|
|
130
213
|
"Kinds:",
|
|
131
214
|
kinds,
|
|
@@ -146,6 +229,7 @@ class ClawMemService {
|
|
|
146
229
|
properties: {
|
|
147
230
|
query: { type: "string", minLength: 1, description: "What to recall from memory." },
|
|
148
231
|
limit: { type: "integer", minimum: 1, maximum: 20, description: "Maximum number of memories to return." },
|
|
232
|
+
repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
149
233
|
agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
|
|
150
234
|
},
|
|
151
235
|
required: ["query"],
|
|
@@ -155,14 +239,14 @@ class ClawMemService {
|
|
|
155
239
|
const query = typeof p.query === "string" ? p.query.trim() : "";
|
|
156
240
|
if (!query) return toolText("Query is empty.");
|
|
157
241
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
158
|
-
|
|
159
|
-
|
|
242
|
+
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
243
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
160
244
|
const rawLimit = typeof p.limit === "number" && Number.isFinite(p.limit) ? Math.floor(p.limit) : this.config.memoryRecallLimit;
|
|
161
245
|
const limit = Math.min(20, Math.max(1, rawLimit));
|
|
162
|
-
const memories = await mem.search(query, limit);
|
|
163
|
-
if (memories.length === 0) return toolText(`No active memories matched "${query}".`);
|
|
246
|
+
const memories = await resolved.mem.search(query, limit);
|
|
247
|
+
if (memories.length === 0) return toolText(`No active memories matched "${query}" in ${resolved.route.repo}.`);
|
|
164
248
|
const text = [
|
|
165
|
-
`Found ${memories.length} active memor${memories.length === 1 ? "y" : "ies"} for "${query}":`,
|
|
249
|
+
`Found ${memories.length} active memor${memories.length === 1 ? "y" : "ies"} for "${query}" in ${resolved.route.repo}:`,
|
|
166
250
|
...memories.map((memory) => `- ${renderMemoryLine(memory)}`),
|
|
167
251
|
].join("\n");
|
|
168
252
|
return toolText(text);
|
|
@@ -179,6 +263,7 @@ class ClawMemService {
|
|
|
179
263
|
properties: {
|
|
180
264
|
memoryId: { type: "string", minLength: 1, description: "The memory id or issue number to retrieve." },
|
|
181
265
|
status: { type: "string", enum: ["active", "stale", "all"], description: "Which status bucket to search. Defaults to all." },
|
|
266
|
+
repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
182
267
|
agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
|
|
183
268
|
},
|
|
184
269
|
required: ["memoryId"],
|
|
@@ -188,12 +273,12 @@ class ClawMemService {
|
|
|
188
273
|
const memoryId = typeof p.memoryId === "string" ? p.memoryId.trim() : "";
|
|
189
274
|
if (!memoryId) return toolText("memoryId is empty.");
|
|
190
275
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
191
|
-
|
|
192
|
-
|
|
276
|
+
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
277
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
193
278
|
const status = p.status === "active" || p.status === "stale" ? p.status : "all";
|
|
194
|
-
const memory = await mem.get(memoryId, status);
|
|
195
|
-
if (!memory) return toolText(`No ${status === "all" ? "" : `${status} `}memory matched id "${memoryId}".`);
|
|
196
|
-
return toolText(renderMemoryBlock(memory));
|
|
279
|
+
const memory = await resolved.mem.get(memoryId, status);
|
|
280
|
+
if (!memory) return toolText(`No ${status === "all" ? "" : `${status} `}memory matched id "${memoryId}" in ${resolved.route.repo}.`);
|
|
281
|
+
return toolText(`Repo: ${resolved.route.repo}\n${renderMemoryBlock(memory)}`);
|
|
197
282
|
},
|
|
198
283
|
});
|
|
199
284
|
|
|
@@ -214,7 +299,7 @@ class ClawMemService {
|
|
|
214
299
|
minItems: 1,
|
|
215
300
|
maxItems: 10,
|
|
216
301
|
},
|
|
217
|
-
|
|
302
|
+
repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
218
303
|
agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
|
|
219
304
|
},
|
|
220
305
|
required: ["detail"],
|
|
@@ -224,14 +309,13 @@ class ClawMemService {
|
|
|
224
309
|
const detail = typeof p.detail === "string" ? p.detail.trim() : "";
|
|
225
310
|
if (!detail) return toolText("Detail is empty.");
|
|
226
311
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const sessionId = typeof p.sessionId === "string" && p.sessionId.trim() ? p.sessionId.trim() : "manual";
|
|
312
|
+
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
313
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
230
314
|
const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
|
|
231
315
|
const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
|
|
232
|
-
const result = await mem.store({ detail, ...(kind ? { kind } : {}), ...(topics && topics.length > 0 ? { topics } : {}) }
|
|
233
|
-
if (!result.created) return toolText(`Memory already exists.\n${renderMemoryBlock(result.memory)}`);
|
|
234
|
-
return toolText(`Stored memory.\n${renderMemoryBlock(result.memory)}`);
|
|
316
|
+
const result = await resolved.mem.store({ detail, ...(kind ? { kind } : {}), ...(topics && topics.length > 0 ? { topics } : {}) });
|
|
317
|
+
if (!result.created) return toolText(`Memory already exists in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
|
|
318
|
+
return toolText(`Stored memory in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
|
|
235
319
|
},
|
|
236
320
|
});
|
|
237
321
|
|
|
@@ -253,6 +337,7 @@ class ClawMemService {
|
|
|
253
337
|
minItems: 1,
|
|
254
338
|
maxItems: 10,
|
|
255
339
|
},
|
|
340
|
+
repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
256
341
|
agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
|
|
257
342
|
},
|
|
258
343
|
required: ["memoryId"],
|
|
@@ -266,16 +351,16 @@ class ClawMemService {
|
|
|
266
351
|
const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
|
|
267
352
|
if (!detail && kind === undefined && topics === undefined) return toolText("Provide at least one of detail, kind, or topics.");
|
|
268
353
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
269
|
-
|
|
270
|
-
|
|
354
|
+
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
355
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
271
356
|
let updated;
|
|
272
357
|
try {
|
|
273
|
-
updated = await mem.update(memoryId, { ...(detail ? { detail } : {}), ...(kind !== undefined ? { kind } : {}), ...(topics !== undefined ? { topics } : {}) });
|
|
358
|
+
updated = await resolved.mem.update(memoryId, { ...(detail ? { detail } : {}), ...(kind !== undefined ? { kind } : {}), ...(topics !== undefined ? { topics } : {}) });
|
|
274
359
|
} catch (error) {
|
|
275
360
|
return toolText(`Unable to update memory "${memoryId}": ${String(error)}`);
|
|
276
361
|
}
|
|
277
|
-
if (!updated) return toolText(`No memory matched id "${memoryId}".`);
|
|
278
|
-
return toolText(`Updated memory.\n${renderMemoryBlock(updated)}`);
|
|
362
|
+
if (!updated) return toolText(`No memory matched id "${memoryId}" in ${resolved.route.repo}.`);
|
|
363
|
+
return toolText(`Updated memory in ${resolved.route.repo}.\n${renderMemoryBlock(updated)}`);
|
|
279
364
|
},
|
|
280
365
|
});
|
|
281
366
|
|
|
@@ -288,6 +373,7 @@ class ClawMemService {
|
|
|
288
373
|
additionalProperties: false,
|
|
289
374
|
properties: {
|
|
290
375
|
memoryId: { type: "string", minLength: 1, description: "The memory id or issue number to mark stale." },
|
|
376
|
+
repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
291
377
|
agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
|
|
292
378
|
},
|
|
293
379
|
required: ["memoryId"],
|
|
@@ -297,18 +383,18 @@ class ClawMemService {
|
|
|
297
383
|
const memoryId = typeof p.memoryId === "string" ? p.memoryId.trim() : "";
|
|
298
384
|
if (!memoryId) return toolText("memoryId is empty.");
|
|
299
385
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const forgotten = await mem.forget(memoryId);
|
|
303
|
-
if (!forgotten) return toolText(`No active memory matched id "${memoryId}".`);
|
|
304
|
-
return toolText(`Marked memory [${forgotten.memoryId}] stale: ${forgotten.detail}`);
|
|
386
|
+
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
387
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
388
|
+
const forgotten = await resolved.mem.forget(memoryId);
|
|
389
|
+
if (!forgotten) return toolText(`No active memory matched id "${memoryId}" in ${resolved.route.repo}.`);
|
|
390
|
+
return toolText(`Marked memory [${forgotten.memoryId}] stale in ${resolved.route.repo}: ${forgotten.detail}`);
|
|
305
391
|
},
|
|
306
392
|
});
|
|
307
393
|
}
|
|
308
394
|
|
|
309
395
|
private async handleBeforeAgentStart(prompt: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
|
|
310
396
|
const routeAgentId = normalizeAgentId(agentId);
|
|
311
|
-
if (!(await this.
|
|
397
|
+
if (!(await this.ensureDefaultRepoConfigured(routeAgentId))) return;
|
|
312
398
|
await this.runRequestMaintenance(routeAgentId);
|
|
313
399
|
if (typeof prompt !== "string" || prompt.trim().length < 5) return;
|
|
314
400
|
try {
|
|
@@ -333,7 +419,7 @@ class ClawMemService {
|
|
|
333
419
|
}
|
|
334
420
|
const { conv } = this.getServices(agentId);
|
|
335
421
|
if (!conv.shouldMirror(snap.sessionId, snap.messages)) return;
|
|
336
|
-
if (!(await this.
|
|
422
|
+
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
337
423
|
await this.enqueueSession(sessionScopeKey(snap.sessionId, agentId), async () => {
|
|
338
424
|
const s = this.getOrCreate(snap.sessionId!, agentId);
|
|
339
425
|
s.sessionFile = sessionFile;
|
|
@@ -359,7 +445,7 @@ class ClawMemService {
|
|
|
359
445
|
private async syncTurn(p: TurnPayload): Promise<void> {
|
|
360
446
|
if (!p.sessionId) return;
|
|
361
447
|
const agentId = normalizeAgentId(p.agentId);
|
|
362
|
-
if (!(await this.
|
|
448
|
+
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
363
449
|
const { conv } = this.getServices(agentId);
|
|
364
450
|
const s = this.getOrCreate(p.sessionId, agentId);
|
|
365
451
|
s.sessionKey = p.sessionKey ?? s.sessionKey; s.agentId = agentId; s.updatedAt = new Date().toISOString();
|
|
@@ -383,7 +469,7 @@ class ClawMemService {
|
|
|
383
469
|
private async finalize(p: FinalizePayload): Promise<void> {
|
|
384
470
|
if (!p.sessionId) return;
|
|
385
471
|
const agentId = normalizeAgentId(p.agentId);
|
|
386
|
-
if (!(await this.
|
|
472
|
+
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
387
473
|
const { conv } = this.getServices(agentId);
|
|
388
474
|
const s = this.getOrCreate(p.sessionId, agentId);
|
|
389
475
|
if (s.finalizedAt) return;
|
|
@@ -456,7 +542,7 @@ class ClawMemService {
|
|
|
456
542
|
})();
|
|
457
543
|
return this.loadPromise;
|
|
458
544
|
}
|
|
459
|
-
private async
|
|
545
|
+
private async ensureIdentityConfigured(agentId?: string): Promise<boolean> {
|
|
460
546
|
const id = normalizeAgentId(agentId);
|
|
461
547
|
if (isAgentConfigured(resolveAgentRoute(this.config, id))) return true;
|
|
462
548
|
const pending = this.configPromises.get(id);
|
|
@@ -465,19 +551,50 @@ class ClawMemService {
|
|
|
465
551
|
this.configPromises.set(id, p);
|
|
466
552
|
try { return await p; } finally { if (this.configPromises.get(id) === p) this.configPromises.delete(id); }
|
|
467
553
|
}
|
|
554
|
+
private async ensureDefaultRepoConfigured(agentId?: string): Promise<boolean> {
|
|
555
|
+
const id = normalizeAgentId(agentId);
|
|
556
|
+
if (!(await this.ensureIdentityConfigured(id))) return false;
|
|
557
|
+
return hasDefaultRepo(resolveAgentRoute(this.config, id));
|
|
558
|
+
}
|
|
468
559
|
private async bootstrap(agentId: string): Promise<boolean> {
|
|
469
560
|
const route = resolveAgentRoute(this.config, agentId);
|
|
470
561
|
if (!route.baseUrl) { this.api.logger.warn(`clawmem: cannot provision Git credentials for ${agentId} without a baseUrl`); return false; }
|
|
471
562
|
try {
|
|
472
563
|
const client = new GitHubIssueClient(route, this.api.logger);
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
564
|
+
const bootstrap = await this.provisionAgentIdentity(client, agentId);
|
|
565
|
+
await this.persistAgentConfig(agentId, {
|
|
566
|
+
baseUrl: route.baseUrl,
|
|
567
|
+
authScheme: "token",
|
|
568
|
+
token: bootstrap.identity.token,
|
|
569
|
+
defaultRepo: bootstrap.identity.repo_full_name,
|
|
570
|
+
});
|
|
571
|
+
this.config.agents[agentId] = {
|
|
572
|
+
...(this.config.agents[agentId] ?? {}),
|
|
573
|
+
baseUrl: route.baseUrl,
|
|
574
|
+
authScheme: "token",
|
|
575
|
+
token: bootstrap.identity.token,
|
|
576
|
+
defaultRepo: bootstrap.identity.repo_full_name,
|
|
577
|
+
};
|
|
578
|
+
this.api.logger.info?.(
|
|
579
|
+
`clawmem: provisioned Git credentials for agent ${agentId} with default repo ${bootstrap.identity.repo_full_name} via ${route.baseUrl} (${bootstrap.method})`,
|
|
580
|
+
);
|
|
478
581
|
return true;
|
|
479
582
|
} catch (error) { this.api.logger.warn(`clawmem: failed to provision Git credentials for agent ${agentId} via ${route.baseUrl}: ${String(error)}`); return false; }
|
|
480
583
|
}
|
|
584
|
+
private async provisionAgentIdentity(client: GitHubIssueClient, agentId: string): Promise<{ identity: BootstrapIdentityResponse; method: string }> {
|
|
585
|
+
const registration = buildAgentBootstrapRegistration(agentId);
|
|
586
|
+
try {
|
|
587
|
+
const identity = await client.registerAgent(registration.prefixLogin, registration.defaultRepoName);
|
|
588
|
+
return { identity, method: "/api/v3/agents" };
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (!shouldFallbackToAnonymousBootstrap(error)) throw error;
|
|
591
|
+
this.api.logger.warn?.(`clawmem: /api/v3/agents is unavailable for agent ${agentId}; falling back to deprecated anonymous bootstrap`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const locale = Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.locale ?? "";
|
|
595
|
+
const identity = await client.createAnonymousSession(locale);
|
|
596
|
+
return { identity, method: "/api/v3/anonymous/session" };
|
|
597
|
+
}
|
|
481
598
|
private warnIfInactiveMemorySlot(): void {
|
|
482
599
|
try {
|
|
483
600
|
const root = this.api.runtime.config.loadConfig();
|
|
@@ -499,7 +616,7 @@ class ClawMemService {
|
|
|
499
616
|
this.api.logger.warn(`clawmem: memory slot check failed: ${String(error)}`);
|
|
500
617
|
}
|
|
501
618
|
}
|
|
502
|
-
private async persistAgentConfig(agentId: string, values: { baseUrl: string; authScheme: "token" | "bearer"; token: string;
|
|
619
|
+
private async persistAgentConfig(agentId: string, values: { baseUrl: string; authScheme: "token" | "bearer"; token: string; defaultRepo: string }): Promise<void> {
|
|
503
620
|
const root = this.api.runtime.config.loadConfig();
|
|
504
621
|
const plugins = root.plugins;
|
|
505
622
|
const entries = plugins?.entries && typeof plugins.entries === "object" && !Array.isArray(plugins.entries) ? (plugins.entries as Record<string, unknown>) : {};
|
|
@@ -577,9 +694,12 @@ class ClawMemService {
|
|
|
577
694
|
if (changed) await this.persistState();
|
|
578
695
|
}
|
|
579
696
|
|
|
580
|
-
private getServices(agentId?: string): { conv: ConversationMirror; mem: MemoryStore } {
|
|
581
|
-
const
|
|
697
|
+
private getServices(agentId?: string, repo?: string): { route: ClawMemResolvedRoute; conv: ConversationMirror; mem: MemoryStore; client: GitHubIssueClient } {
|
|
698
|
+
const route = resolveAgentRoute(this.config, agentId, repo);
|
|
699
|
+
const client = new GitHubIssueClient(route, this.api.logger);
|
|
582
700
|
return {
|
|
701
|
+
route,
|
|
702
|
+
client,
|
|
583
703
|
conv: new ConversationMirror(client, this.api, this.config),
|
|
584
704
|
mem: new MemoryStore(client, this.api, this.config),
|
|
585
705
|
};
|
|
@@ -587,6 +707,34 @@ class ClawMemService {
|
|
|
587
707
|
private resolveToolAgentId(agentId: unknown): string {
|
|
588
708
|
return normalizeAgentId(typeof agentId === "string" && agentId.trim() ? agentId : process.env.OPENCLAW_AGENT_ID);
|
|
589
709
|
}
|
|
710
|
+
private resolveToolRepo(repo: unknown): { repo?: string; error?: string } {
|
|
711
|
+
if (repo === undefined || repo === null || repo === "") return {};
|
|
712
|
+
if (typeof repo !== "string") return { error: "repo must be a string like owner/repo." };
|
|
713
|
+
const trimmed = repo.trim().replace(/^\/+|\/+$/g, "");
|
|
714
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(trimmed)) return { error: `Invalid repo "${repo}". Expected owner/repo.` };
|
|
715
|
+
return { repo: trimmed };
|
|
716
|
+
}
|
|
717
|
+
private async requireToolIdentity(agentId: string): Promise<{ route: ClawMemResolvedRoute; client: GitHubIssueClient } | { error: string }> {
|
|
718
|
+
if (!(await this.ensureIdentityConfigured(agentId))) {
|
|
719
|
+
return { error: `ClawMem identity for agent "${agentId}" is not configured.` };
|
|
720
|
+
}
|
|
721
|
+
const { route, client } = this.getServices(agentId);
|
|
722
|
+
return { route, client };
|
|
723
|
+
}
|
|
724
|
+
private async requireToolRoute(agentId: string, repo: unknown): Promise<{ route: ClawMemResolvedRoute; conv: ConversationMirror; mem: MemoryStore; client: GitHubIssueClient } | { error: string }> {
|
|
725
|
+
const parsed = this.resolveToolRepo(repo);
|
|
726
|
+
if (parsed.error) return { error: parsed.error };
|
|
727
|
+
if (!(await this.ensureIdentityConfigured(agentId))) {
|
|
728
|
+
return { error: `ClawMem identity for agent "${agentId}" is not configured.` };
|
|
729
|
+
}
|
|
730
|
+
const services = this.getServices(agentId, parsed.repo);
|
|
731
|
+
if (!services.route.repo) {
|
|
732
|
+
return {
|
|
733
|
+
error: `No memory repo selected for agent "${agentId}". Provide repo explicitly or configure agents.${agentId}.defaultRepo.`,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return services;
|
|
737
|
+
}
|
|
590
738
|
/**
|
|
591
739
|
* After finalization, check if the repo still has an empty/default description.
|
|
592
740
|
* If so, use the conversation summary to suggest a meaningful name and update
|
|
@@ -613,6 +761,10 @@ class ClawMemService {
|
|
|
613
761
|
}
|
|
614
762
|
|
|
615
763
|
function asRecord(v: unknown): Record<string, unknown> { return v && typeof v === "object" ? (v as Record<string, unknown>) : {}; }
|
|
764
|
+
function shouldFallbackToAnonymousBootstrap(error: unknown): boolean {
|
|
765
|
+
const msg = String(error);
|
|
766
|
+
return /^Error:\s*HTTP (404|405|501):/i.test(msg) || /^HTTP (404|405|501):/i.test(msg);
|
|
767
|
+
}
|
|
616
768
|
function toolText(text: string): { content: Array<{ type: "text"; text: string }> } {
|
|
617
769
|
return { content: [{ type: "text", text }] };
|
|
618
770
|
}
|
|
@@ -620,7 +772,7 @@ function renderMemoryLine(memory: { memoryId: string; title?: string; detail: st
|
|
|
620
772
|
const schema = [memory.kind ? `kind:${memory.kind}` : "", ...(memory.topics ?? []).map((topic) => `topic:${topic}`)].filter(Boolean).join(", ");
|
|
621
773
|
return `[${memory.memoryId}] ${memory.title || "Memory"}${schema ? ` (${schema})` : ""}${memory.status === "stale" ? " [stale]" : ""}: ${memory.detail}`;
|
|
622
774
|
}
|
|
623
|
-
function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; title?: string; detail: string; kind?: string; topics?: string[]; status: "active" | "stale";
|
|
775
|
+
function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; title?: string; detail: string; kind?: string; topics?: string[]; status: "active" | "stale"; date?: string }): string {
|
|
624
776
|
const lines = [
|
|
625
777
|
`Memory ID: ${memory.memoryId}`,
|
|
626
778
|
...(typeof memory.issueNumber === "number" ? [`Issue Number: ${memory.issueNumber}`] : []),
|
|
@@ -628,7 +780,6 @@ function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; tit
|
|
|
628
780
|
`Title: ${memory.title || "Memory"}`,
|
|
629
781
|
...(memory.kind ? [`Kind: ${memory.kind}`] : []),
|
|
630
782
|
...(memory.topics && memory.topics.length > 0 ? [`Topics: ${memory.topics.join(", ")}`] : []),
|
|
631
|
-
...(memory.sessionId ? [`Session: ${memory.sessionId}`] : []),
|
|
632
783
|
...(memory.date ? [`Date: ${memory.date}`] : []),
|
|
633
784
|
`Detail: ${memory.detail}`,
|
|
634
785
|
];
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Shared types for the clawmem plugin.
|
|
2
2
|
export type ClawMemAgentConfig = {
|
|
3
3
|
baseUrl?: string;
|
|
4
|
+
defaultRepo?: string;
|
|
4
5
|
repo?: string;
|
|
5
6
|
token?: string;
|
|
6
7
|
authScheme?: "token" | "bearer";
|
|
@@ -8,6 +9,9 @@ export type ClawMemAgentConfig = {
|
|
|
8
9
|
|
|
9
10
|
export type ClawMemPluginConfig = {
|
|
10
11
|
baseUrl: string;
|
|
12
|
+
defaultRepo?: string;
|
|
13
|
+
repo?: string;
|
|
14
|
+
token?: string;
|
|
11
15
|
authScheme: "token" | "bearer";
|
|
12
16
|
agents: Record<string, ClawMemAgentConfig>;
|
|
13
17
|
memoryRecallLimit: number;
|
|
@@ -18,12 +22,15 @@ export type ClawMemPluginConfig = {
|
|
|
18
22
|
export type ClawMemResolvedRoute = {
|
|
19
23
|
agentId: string;
|
|
20
24
|
baseUrl: string;
|
|
25
|
+
defaultRepo?: string;
|
|
21
26
|
repo?: string;
|
|
22
27
|
token?: string;
|
|
23
28
|
authScheme: "token" | "bearer";
|
|
24
29
|
};
|
|
25
30
|
|
|
26
|
-
export type
|
|
31
|
+
export type BootstrapIdentityResponse = { token: string; repo_full_name: string };
|
|
32
|
+
export type AgentRegistrationResponse = BootstrapIdentityResponse & { login: string };
|
|
33
|
+
export type AnonymousSessionResponse = BootstrapIdentityResponse & { owner_login: string; repo_name: string };
|
|
27
34
|
export type SessionMirrorState = {
|
|
28
35
|
sessionId: string; sessionKey?: string; sessionFile?: string; agentId?: string;
|
|
29
36
|
issueNumber?: number; issueTitle?: string; titleSource?: "placeholder" | "llm";
|
|
@@ -46,6 +53,6 @@ export type MemoryListOptions = {
|
|
|
46
53
|
};
|
|
47
54
|
export type ParsedMemoryIssue = {
|
|
48
55
|
issueNumber: number; title: string; memoryId: string; memoryHash?: string;
|
|
49
|
-
|
|
56
|
+
date: string; detail: string;
|
|
50
57
|
kind?: string; topics?: string[]; status: "active" | "stale";
|
|
51
58
|
};
|
package/src/utils.ts
CHANGED
|
@@ -4,6 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import type { NormalizedMessage } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_AGENT_ID = "main";
|
|
7
|
+
export const DEFAULT_BOOTSTRAP_REPO_NAME = "memory";
|
|
8
|
+
|
|
9
|
+
const MAX_AGENT_LOGIN_PREFIX_LEN = 32;
|
|
7
10
|
|
|
8
11
|
export function sha256(v: string): string { return crypto.createHash("sha256").update(v).digest("hex"); }
|
|
9
12
|
|
|
@@ -13,6 +16,16 @@ export function normalizeAgentId(value: string | undefined | null): string {
|
|
|
13
16
|
return trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || DEFAULT_AGENT_ID;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
export function buildAgentBootstrapRegistration(agentId: string): { prefixLogin: string; defaultRepoName: string } {
|
|
20
|
+
const prefixLogin = normalizeAgentId(agentId)
|
|
21
|
+
.replace(/_/g, "-")
|
|
22
|
+
.replace(/-+/g, "-")
|
|
23
|
+
.replace(/^-+|-+$/g, "")
|
|
24
|
+
.slice(0, MAX_AGENT_LOGIN_PREFIX_LEN)
|
|
25
|
+
.replace(/-+$/g, "") || DEFAULT_AGENT_ID;
|
|
26
|
+
return { prefixLogin, defaultRepoName: DEFAULT_BOOTSTRAP_REPO_NAME };
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
export function sessionScopeKey(sessionId: string, agentId?: string): string {
|
|
17
30
|
return `${normalizeAgentId(agentId)}:${sessionId.trim()}`;
|
|
18
31
|
}
|