@clawmem-ai/clawmem 0.1.11 → 0.1.13
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 +4 -2
- package/openclaw.plugin.json +10 -1
- package/package.json +1 -1
- package/skills/clawmem/SKILL.md +10 -3
- package/skills/clawmem/references/collaboration.md +2 -0
- package/skills/clawmem/references/communication.md +22 -39
- package/skills/clawmem/references/manual-ops.md +8 -4
- package/skills/clawmem/references/repair.md +2 -1
- package/skills/clawmem/references/schema.md +6 -0
- package/src/collaboration.test.ts +71 -0
- package/src/collaboration.ts +109 -0
- package/src/config.test.ts +1 -0
- package/src/config.ts +4 -3
- package/src/github-client.ts +4 -4
- package/src/memory.test.ts +17 -8
- package/src/memory.ts +32 -8
- package/src/service.ts +152 -58
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -137,7 +137,8 @@ Full config with all options:
|
|
|
137
137
|
},
|
|
138
138
|
turnCommentDelayMs: 1000,
|
|
139
139
|
summaryWaitTimeoutMs: 120000,
|
|
140
|
-
memoryRecallLimit: 5
|
|
140
|
+
memoryRecallLimit: 5,
|
|
141
|
+
memoryAutoRecallLimit: 5
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
}
|
|
@@ -152,7 +153,7 @@ Full config with all options:
|
|
|
152
153
|
- Conversation comments exclude tool calls, tool results, system messages, and heartbeat noise.
|
|
153
154
|
- Summary failures do not block finalization; the `summary` field is written as `failed: ...`.
|
|
154
155
|
- Memory search and auto-injection only return open `type:memory` issues. Closed memory issues are treated as stale.
|
|
155
|
-
- `memory_recall` now prefers the backend `/api/v3/search/issues` endpoint scoped to the current repo plus `label:"type:memory"
|
|
156
|
+
- `memory_recall` now prefers the backend `/api/v3/search/issues` endpoint scoped to the current repo plus `label:"type:memory"`, with a simple local lexical fallback when backend search is unavailable or returns no matches.
|
|
156
157
|
- Durable memories are extracted best-effort during later request-scoped maintenance, not by background subagent work after a request has already ended.
|
|
157
158
|
- 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.
|
|
158
159
|
- Route resolution is now: agent identity supplies credentials, `defaultRepo` is the fallback memory space, and explicit tool calls may override repo per operation.
|
|
@@ -160,4 +161,5 @@ Full config with all options:
|
|
|
160
161
|
- Memory issues no longer use `session:*` labels. Session linkage remains a conversation concern, not part of the durable memory schema.
|
|
161
162
|
- `memory_update` updates one existing memory issue in place; use it for evolving canonical facts or active tasks instead of creating a duplicate node.
|
|
162
163
|
- 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).
|
|
164
|
+
- Memory extraction now prefers one atomic fact per memory item instead of bundling whole sessions into a single node.
|
|
163
165
|
- 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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "clawmem",
|
|
3
3
|
"name": "ClawMem",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.13",
|
|
5
5
|
"description": "Mirror OpenClaw sessions into GitHub-compatible issues and comments.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"skills": [
|
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"type": "integer",
|
|
77
77
|
"minimum": 1,
|
|
78
78
|
"maximum": 20
|
|
79
|
+
},
|
|
80
|
+
"memoryAutoRecallLimit": {
|
|
81
|
+
"type": "integer",
|
|
82
|
+
"minimum": 1,
|
|
83
|
+
"maximum": 20
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
},
|
|
@@ -118,6 +123,10 @@
|
|
|
118
123
|
},
|
|
119
124
|
"memoryRecallLimit": {
|
|
120
125
|
"label": "Memory Recall Limit",
|
|
126
|
+
"help": "Default maximum number of active memories returned by memory recall tools."
|
|
127
|
+
},
|
|
128
|
+
"memoryAutoRecallLimit": {
|
|
129
|
+
"label": "Auto Recall Limit",
|
|
121
130
|
"help": "Maximum number of active memories injected into context before an agent starts."
|
|
122
131
|
}
|
|
123
132
|
}
|
package/package.json
CHANGED
package/skills/clawmem/SKILL.md
CHANGED
|
@@ -46,12 +46,16 @@ On every user turn, run this loop:
|
|
|
46
46
|
- Never treat a `memory_recall` miss by itself as proof that no relevant memory exists.
|
|
47
47
|
2. After answering, ask: did this turn create durable knowledge?
|
|
48
48
|
- Default to yes for corrections, preferences, decisions, workflows, lessons, and status changes.
|
|
49
|
+
- Prefer one durable fact per memory. If a turn contains several independent facts, save them separately instead of bundling them into one summary memory.
|
|
49
50
|
- Use `memory_update` when the same canonical fact or ongoing task should keep evolving as one node.
|
|
51
|
+
- When updating an existing memory, preserve that node's current language unless the user explicitly asks for a rewrite.
|
|
50
52
|
- Use `memory_store` when this is a genuinely new memory.
|
|
53
|
+
- For new memories, write the memory title and body in the user's current language by default.
|
|
51
54
|
- Use `memory_forget` when a memory is stale, superseded, or harmful if reused.
|
|
52
55
|
3. Keep the user posted.
|
|
53
|
-
- If a retrieved memory materially shaped the answer,
|
|
54
|
-
-
|
|
56
|
+
- If a retrieved memory materially shaped the answer, including automatic session-start recall, briefly surface that fact in the user's current language.
|
|
57
|
+
- Include the memory id and title only when they help with debugging, traceability, or an explicit user request.
|
|
58
|
+
- After creating or updating a memory, give a short confirmation in the user's current language instead of forcing fixed English phrasing.
|
|
55
59
|
|
|
56
60
|
Bias toward retrieving and saving. A missed search or missed memory is worse than an extra search.
|
|
57
61
|
|
|
@@ -63,11 +67,14 @@ Bias toward retrieving and saving. A missed search or missed memory is worse tha
|
|
|
63
67
|
- Private personal memory usually belongs in the agent's `defaultRepo`.
|
|
64
68
|
- Project memory belongs in the relevant project repo.
|
|
65
69
|
- Shared or team knowledge belongs in the shared repo for that group.
|
|
70
|
+
- Memory titles and bodies default to the user's current language for new memories.
|
|
71
|
+
- When updating an existing memory, keep that node in its current language unless the user explicitly asks to rewrite it.
|
|
72
|
+
- Keep schema labels and machine-oriented fields stable. Do not translate `type:*`, `kind:*`, `topic:*`, or other structural identifiers.
|
|
66
73
|
- If the user is asking about collaboration, organizations, teams, invitations, collaborators, shared repo access, or why someone can or cannot access a memory repo, switch from normal memory reasoning to the collaboration workflow in `references/collaboration.md`.
|
|
67
74
|
|
|
68
75
|
## Read the right reference
|
|
69
76
|
|
|
70
|
-
- For user-facing messaging,
|
|
77
|
+
- For user-facing runtime messaging, memory console links, and post-save confirmations, read [references/communication.md](references/communication.md).
|
|
71
78
|
- For activation repair, route verification, tool-path verification, and compatibility-file reminders after install, read [references/repair.md](references/repair.md).
|
|
72
79
|
- For shared repos, team memory, organizations, teams, invitations, collaborators, and collaboration routing, read [references/collaboration.md](references/collaboration.md).
|
|
73
80
|
- For memory kinds, labels, curated versus plugin-managed nodes, and when to use each shape, read [references/schema.md](references/schema.md).
|
|
@@ -96,7 +96,9 @@ Reason with these rules before every collaboration action:
|
|
|
96
96
|
- Teams are org-scoped authorization groups, not social groups.
|
|
97
97
|
- Effective repo access is `max(org base permission, direct collaborator grant, team grant)` after owner or admin shortcuts.
|
|
98
98
|
- Runtime permissions are only `none`, `read`, `write`, and `admin`.
|
|
99
|
+
- Organization invitation roles are `member` and `owner`.
|
|
99
100
|
- `memory_repos` only shows repos that are already accessible now; it does not prove there are no pending invitations.
|
|
101
|
+
- The repo collaborators API includes the repository owner row; reason about direct collaborators as explicit non-owner shares.
|
|
100
102
|
- A repo collaborator grant may create a pending repository invitation instead of immediate access when the target user is not already a collaborator.
|
|
101
103
|
- Accepting a repository invitation is what turns a pending share into visible repo access for the invitee.
|
|
102
104
|
- Outside collaborators are non-members who still have direct collaborator access to at least one org-owned repo.
|
|
@@ -1,60 +1,44 @@
|
|
|
1
|
-
# ClawMem Communication
|
|
1
|
+
# ClawMem Runtime Communication
|
|
2
2
|
|
|
3
|
-
Use this reference when
|
|
3
|
+
Use this reference when memory shaped the answer, a memory was saved or updated, or the user needs a console link to inspect the memory graph.
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
7
7
|
- Keep the user posted
|
|
8
|
-
- Restart notice protocol
|
|
9
|
-
- First-run message
|
|
10
8
|
- Memory visualization console
|
|
11
9
|
|
|
12
10
|
## Keep the user posted
|
|
13
11
|
|
|
14
12
|
Nothing interesting should happen silently. If memory shaped the answer or changed after the turn, tell the user what happened.
|
|
15
13
|
|
|
14
|
+
When a recalled or auto-injected memory materially shaped the answer, add a brief user-visible note in the user's current language. Keep it short, natural, and easy to skip.
|
|
15
|
+
|
|
16
16
|
Preferred retrieval transparency:
|
|
17
|
-
-
|
|
17
|
+
- say that you recalled or confirmed something from prior memory
|
|
18
|
+
- mention the remembered fact itself
|
|
19
|
+
- include the memory id and title only when they genuinely help the user follow along or when the user is debugging memory behavior
|
|
18
20
|
|
|
19
21
|
Use a miss note only when the user would reasonably expect that you checked:
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
Preferred confirmation:
|
|
23
|
-
- `Locked memory #<id>: <title>`
|
|
24
|
-
|
|
25
|
-
Friendlier variants are fine as long as they stay truthful and short.
|
|
26
|
-
|
|
27
|
-
Examples:
|
|
28
|
-
- `Memory hit #14: Team demo moved to Wednesday`
|
|
29
|
-
- `Locked memory #10: API rate limiting uses a sliding window policy`
|
|
30
|
-
- `Locked memory #27: Client meeting is Thursday at 2pm`
|
|
22
|
+
- explain in the user's current language that no relevant prior memory was found
|
|
31
23
|
|
|
32
|
-
|
|
24
|
+
When a memory is created or updated successfully, add a brief confirmation in the user's current language.
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
> ClawMem is installed and selected as the active memory plugin. Restarting the OpenClaw gateway to load it now.
|
|
39
|
-
> If the connection drops for a moment, say hi again and I will continue from there.
|
|
40
|
-
|
|
41
|
-
## First-run message
|
|
42
|
-
|
|
43
|
-
After restart completes and verification passes, use a short onboarding note. Keep it high-signal and avoid dumping the full skill into chat.
|
|
26
|
+
Preferred confirmation:
|
|
27
|
+
- say that you remembered, saved, or updated it
|
|
28
|
+
- include the memory id and title only when they help with debugging, traceability, or explicit user requests
|
|
44
29
|
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
- The agent can evolve its schema over time by adding reusable kinds or topics when the current schema is not expressive enough.
|
|
50
|
-
- The user can inspect the memory graph in the console link below.
|
|
30
|
+
Storage language defaults:
|
|
31
|
+
- For new memories, store the human-readable title and body in the user's current language.
|
|
32
|
+
- For updates, preserve the memory node's current language unless the user explicitly asks for a rewrite.
|
|
33
|
+
- Keep labels and structural markers such as `type:*`, `kind:*`, and `topic:*` in their fixed machine-readable form.
|
|
51
34
|
|
|
52
|
-
|
|
35
|
+
Do not force English markers like `Memory hit` or `Locked memory` in non-English conversations. Those are examples, not required phrasing.
|
|
53
36
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
37
|
+
Examples:
|
|
38
|
+
- `我从之前的记忆里确认到:你最近在看《Legal High》。`
|
|
39
|
+
- `这条我记住了,之后我会按这个偏好来推荐。`
|
|
40
|
+
- `I found a relevant prior memory: the team demo moved to Wednesday.`
|
|
41
|
+
- `Saved that preference. I’ll use it in later recommendations.`
|
|
58
42
|
|
|
59
43
|
## Memory visualization console
|
|
60
44
|
|
|
@@ -72,7 +56,6 @@ Read `CLAWMEM_TOKEN` from the current route, substitute it into the URL, and sho
|
|
|
72
56
|
|
|
73
57
|
### When to show the console link
|
|
74
58
|
|
|
75
|
-
- During onboarding after a successful install
|
|
76
59
|
- When the user asks to view memories, the graph, or a dashboard
|
|
77
60
|
- After significant memory operations, such as bulk saves
|
|
78
61
|
- When a visual overview would clearly help the user
|
|
@@ -75,6 +75,10 @@ Do not export `GH_HOST` or `GH_ENTERPRISE_TOKEN` globally for unrelated github.c
|
|
|
75
75
|
|
|
76
76
|
Use the tool path first. If raw issue control is required:
|
|
77
77
|
|
|
78
|
+
- For new memories, write the issue title and body in the user's current language.
|
|
79
|
+
- When manually updating an existing memory, preserve that memory's current language unless the user explicitly asks for a rewrite.
|
|
80
|
+
- Keep labels and schema markers such as `type:*`, `kind:*`, and `topic:*` in their fixed machine-readable form.
|
|
81
|
+
|
|
78
82
|
### With `gh`
|
|
79
83
|
|
|
80
84
|
```sh
|
|
@@ -85,8 +89,8 @@ done
|
|
|
85
89
|
|
|
86
90
|
GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
|
|
87
91
|
gh issue create --repo "$CLAWMEM_REPO" \
|
|
88
|
-
--title "
|
|
89
|
-
--body "<durable detail in
|
|
92
|
+
--title "<concise title in the user's current language>" \
|
|
93
|
+
--body "<durable detail in the user's current language>" \
|
|
90
94
|
--label "type:memory" \
|
|
91
95
|
--label "kind:lesson"
|
|
92
96
|
```
|
|
@@ -105,8 +109,8 @@ curl -sf -X POST -H "Authorization: token $CLAWMEM_TOKEN" \
|
|
|
105
109
|
-H "Content-Type: application/json" \
|
|
106
110
|
"$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues" \
|
|
107
111
|
-d "{
|
|
108
|
-
\"title\": \"
|
|
109
|
-
\"body\": \"<durable detail in
|
|
112
|
+
\"title\": \"<concise title in the user's current language>\",
|
|
113
|
+
\"body\": \"<durable detail in the user's current language>\",
|
|
110
114
|
\"labels\": [\"type:memory\", \"kind:lesson\"]
|
|
111
115
|
}" | jq '{number, title, url: .html_url}'
|
|
112
116
|
```
|
|
@@ -98,7 +98,8 @@ When prior context may help, I search ClawMem before answering.
|
|
|
98
98
|
|
|
99
99
|
```markdown
|
|
100
100
|
Before ending every response, ask: "Did I learn anything durable this turn?"
|
|
101
|
-
If yes or unsure, save
|
|
101
|
+
If yes or unsure, save new memory content to ClawMem in the user's current language.
|
|
102
|
+
When updating an existing memory, keep that node in its current language unless the user asks to rewrite it.
|
|
102
103
|
```
|
|
103
104
|
|
|
104
105
|
### Optional TOOLS.md reminder
|
|
@@ -58,6 +58,12 @@ If you create a curated memory manually, include:
|
|
|
58
58
|
- New labels should be short, general, and likely to apply again across future memories or agents.
|
|
59
59
|
- Do not invent random label prefixes. Schema evolution must stay within `kind:*` and `topic:*`.
|
|
60
60
|
|
|
61
|
+
## Storage language
|
|
62
|
+
|
|
63
|
+
- For new memory nodes, write the human-readable title and body in the user's current language by default.
|
|
64
|
+
- When updating an existing memory node, preserve that node's current language unless the user explicitly asks for a rewrite.
|
|
65
|
+
- Do not translate schema or routing markers such as `type:*`, `kind:*`, `topic:*`, or other machine-oriented field names.
|
|
66
|
+
|
|
61
67
|
## Storage rule
|
|
62
68
|
|
|
63
69
|
If you are writing something so the agent remembers it later, store it in ClawMem. If you are writing something for a tool or human to read directly, write a file instead.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
filterDirectCollaborators,
|
|
3
|
+
listRepoAccessTeams,
|
|
4
|
+
repoSummaryFullName,
|
|
5
|
+
resolveOrgInvitationRole,
|
|
6
|
+
} from "./collaboration.js";
|
|
7
|
+
|
|
8
|
+
function assert(condition: unknown, message: string): void {
|
|
9
|
+
if (!condition) throw new Error(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function testOrgInvitationRoleValidation(): void {
|
|
13
|
+
const fallback = resolveOrgInvitationRole(undefined, "member");
|
|
14
|
+
assert("role" in fallback && fallback.role === "member", "expected undefined role to fall back to member");
|
|
15
|
+
|
|
16
|
+
const owner = resolveOrgInvitationRole("owner", "member");
|
|
17
|
+
assert("role" in owner && owner.role === "owner", "expected owner role to pass through");
|
|
18
|
+
|
|
19
|
+
const invalid = resolveOrgInvitationRole("admin", "member");
|
|
20
|
+
assert("error" in invalid, "expected admin to be rejected because backend expects owner");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function testDirectCollaboratorFiltering(): void {
|
|
24
|
+
const collaborators = filterDirectCollaborators([
|
|
25
|
+
{ login: "Acme" },
|
|
26
|
+
{ login: "alice", outside_collaborator: true },
|
|
27
|
+
{ login: "bob", organization_member: true },
|
|
28
|
+
], "acme");
|
|
29
|
+
|
|
30
|
+
assert(collaborators.length === 2, "expected owner row to be excluded from explicit collaborators");
|
|
31
|
+
assert(collaborators[0]?.login === "alice", "expected alice to remain after owner filtering");
|
|
32
|
+
assert(collaborators[1]?.login === "bob", "expected bob to remain after owner filtering");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function testRepoSummaryFallback(): void {
|
|
36
|
+
assert(repoSummaryFullName({ full_name: "acme/project" }) === "acme/project", "expected explicit full_name to win");
|
|
37
|
+
assert(repoSummaryFullName({ owner: { login: "acme" }, name: "project" }) === "acme/project", "expected owner/name fallback to work");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function testRepoAccessTeamsDerivation(): Promise<void> {
|
|
41
|
+
const result = await listRepoAccessTeams({
|
|
42
|
+
async listOrgTeams() {
|
|
43
|
+
return [
|
|
44
|
+
{ slug: "admins", name: "admins" },
|
|
45
|
+
{ slug: "writers", name: "writers" },
|
|
46
|
+
{ slug: "broken", name: "broken" },
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
async listTeamRepos(_org: string, teamSlug: string) {
|
|
50
|
+
if (teamSlug === "admins") return [{ full_name: "acme/project", role_name: "admin" }];
|
|
51
|
+
if (teamSlug === "writers") return [{ owner: { login: "acme" }, name: "project", role_name: "write" }];
|
|
52
|
+
if (teamSlug === "broken") throw new Error("boom");
|
|
53
|
+
return [];
|
|
54
|
+
},
|
|
55
|
+
}, "acme", "acme/project");
|
|
56
|
+
|
|
57
|
+
assert(result.teams.length === 2, "expected two teams with access to be discovered via org->team->repo traversal");
|
|
58
|
+
assert(result.teams[0]?.slug === "admins", "expected admins team to be included");
|
|
59
|
+
assert(result.teams[1]?.slug === "writers", "expected writers team to be included");
|
|
60
|
+
assert(result.notes.length === 1 && result.notes[0]?.includes("broken"), "expected per-team lookup failures to be recorded as notes");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main(): Promise<void> {
|
|
64
|
+
testOrgInvitationRoleValidation();
|
|
65
|
+
testDirectCollaboratorFiltering();
|
|
66
|
+
testRepoSummaryFallback();
|
|
67
|
+
await testRepoAccessTeamsDerivation();
|
|
68
|
+
console.log("collaboration tests passed");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await main();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export type CollaborationPermission = "read" | "write" | "admin";
|
|
2
|
+
export type CollaborationOrgInvitationRole = "member" | "owner";
|
|
3
|
+
|
|
4
|
+
type PermissionMap = Record<string, boolean | undefined>;
|
|
5
|
+
|
|
6
|
+
export type CollaborationRepoSummary = {
|
|
7
|
+
full_name?: string;
|
|
8
|
+
owner?: { login?: string };
|
|
9
|
+
name?: string;
|
|
10
|
+
permissions?: PermissionMap;
|
|
11
|
+
role_name?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type CollaborationTeamSummary = {
|
|
15
|
+
id?: number;
|
|
16
|
+
slug?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
privacy?: string;
|
|
20
|
+
permission?: string;
|
|
21
|
+
role_name?: string;
|
|
22
|
+
permissions?: PermissionMap;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type CollaborationCollaboratorSummary = {
|
|
26
|
+
id?: number;
|
|
27
|
+
login?: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
permissions?: PermissionMap;
|
|
30
|
+
role_name?: string;
|
|
31
|
+
organization_member?: boolean;
|
|
32
|
+
outside_collaborator?: boolean;
|
|
33
|
+
type?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type RepoAccessTeamClient = {
|
|
37
|
+
listOrgTeams(org: string): Promise<CollaborationTeamSummary[]>;
|
|
38
|
+
listTeamRepos(org: string, teamSlug: string): Promise<CollaborationRepoSummary[]>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function normalizePermissionAlias(value: unknown): "none" | CollaborationPermission | undefined {
|
|
42
|
+
if (typeof value !== "string") return undefined;
|
|
43
|
+
const normalized = value.trim().toLowerCase();
|
|
44
|
+
if (!normalized) return undefined;
|
|
45
|
+
if (normalized === "none") return "none";
|
|
46
|
+
if (normalized === "read" || normalized === "pull" || normalized === "triage") return "read";
|
|
47
|
+
if (normalized === "write" || normalized === "push" || normalized === "maintain") return "write";
|
|
48
|
+
if (normalized === "admin") return "admin";
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveOrgInvitationRole(
|
|
53
|
+
value: unknown,
|
|
54
|
+
fallback: CollaborationOrgInvitationRole,
|
|
55
|
+
): { role: CollaborationOrgInvitationRole } | { error: string } {
|
|
56
|
+
if (value === undefined || value === null || value === "") return { role: fallback };
|
|
57
|
+
if (typeof value !== "string") return { error: "role must be member or owner." };
|
|
58
|
+
const normalized = value.trim().toLowerCase();
|
|
59
|
+
if (normalized === "member" || normalized === "owner") return { role: normalized };
|
|
60
|
+
return { error: `Unsupported role "${value}". Use member or owner.` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function repoSummaryFullName(repo?: CollaborationRepoSummary): string | undefined {
|
|
64
|
+
const fullName = repo?.full_name?.trim();
|
|
65
|
+
if (fullName) return fullName;
|
|
66
|
+
const owner = repo?.owner?.login?.trim();
|
|
67
|
+
const name = repo?.name?.trim();
|
|
68
|
+
if (owner && name) return `${owner}/${name}`;
|
|
69
|
+
return name || undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function filterDirectCollaborators(
|
|
73
|
+
collaborators: CollaborationCollaboratorSummary[],
|
|
74
|
+
ownerLogin: string,
|
|
75
|
+
): CollaborationCollaboratorSummary[] {
|
|
76
|
+
const owner = ownerLogin.trim().toLowerCase();
|
|
77
|
+
if (!owner) return collaborators;
|
|
78
|
+
return collaborators.filter((collaborator) => (collaborator.login?.trim().toLowerCase() || "") !== owner);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function listRepoAccessTeams(
|
|
82
|
+
client: RepoAccessTeamClient,
|
|
83
|
+
org: string,
|
|
84
|
+
fullName: string,
|
|
85
|
+
): Promise<{ teams: CollaborationTeamSummary[]; notes: string[] }> {
|
|
86
|
+
const notes: string[] = [];
|
|
87
|
+
const teams = await client.listOrgTeams(org);
|
|
88
|
+
const withAccess: CollaborationTeamSummary[] = [];
|
|
89
|
+
for (const team of teams) {
|
|
90
|
+
const teamSlug = team.slug?.trim() || team.name?.trim();
|
|
91
|
+
if (!teamSlug) {
|
|
92
|
+
notes.push(`Skipped a team in org "${org}" because it had no slug or name.`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const repos = await client.listTeamRepos(org, teamSlug);
|
|
97
|
+
const matchingRepo = repos.find((repo) => repoSummaryFullName(repo) === fullName);
|
|
98
|
+
if (!matchingRepo) continue;
|
|
99
|
+
withAccess.push({
|
|
100
|
+
...team,
|
|
101
|
+
...(matchingRepo.permissions ? { permissions: matchingRepo.permissions } : {}),
|
|
102
|
+
...(matchingRepo.role_name ? { role_name: matchingRepo.role_name } : {}),
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
notes.push(`Team repo lookup failed for ${org}/${teamSlug}: ${String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { teams: withAccess, notes };
|
|
109
|
+
}
|
package/src/config.test.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -12,13 +12,14 @@ export const LABEL_CLOSED = "status:closed";
|
|
|
12
12
|
export const LABEL_MEMORY_ACTIVE = "memory-status:active";
|
|
13
13
|
export const LABEL_MEMORY_STALE = "memory-status:stale";
|
|
14
14
|
|
|
15
|
-
const MANAGED_PREFIXES = ["type:", "kind:", "session:", "date:", "topic:", "agent:"
|
|
15
|
+
const MANAGED_PREFIXES = ["type:", "kind:", "session:", "date:", "topic:", "agent:"];
|
|
16
16
|
const MANAGED_EXACT = new Set([LABEL_ACTIVE, LABEL_CLOSED, LABEL_MEMORY_ACTIVE, LABEL_MEMORY_STALE]);
|
|
17
17
|
|
|
18
18
|
export function resolvePluginConfig(api: OpenClawPluginApi): ClawMemPluginConfig {
|
|
19
19
|
const raw = (api.pluginConfig ?? {}) as Record<string, unknown>;
|
|
20
20
|
const str = (v: unknown) => typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
21
21
|
const num = (v: unknown, d: number) => typeof v === "number" && Number.isFinite(v) ? Math.floor(v) : d;
|
|
22
|
+
const float = (v: unknown, d: number) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
22
23
|
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
|
|
23
24
|
const baseUrl = (str(raw.baseUrl) ?? "https://git.clawmem.ai").replace(/\/+$/, "");
|
|
24
25
|
const rawAgents = raw.agents && typeof raw.agents === "object" && !Array.isArray(raw.agents)
|
|
@@ -45,6 +46,7 @@ export function resolvePluginConfig(api: OpenClawPluginApi): ClawMemPluginConfig
|
|
|
45
46
|
authScheme: raw.authScheme === "bearer" ? "bearer" : "token",
|
|
46
47
|
agents,
|
|
47
48
|
memoryRecallLimit: clamp(num(raw.memoryRecallLimit, 5), 1, 20),
|
|
49
|
+
memoryAutoRecallLimit: clamp(num(raw.memoryAutoRecallLimit, num(raw.memoryRecallLimit, 5)), 1, 20),
|
|
48
50
|
turnCommentDelayMs: num(raw.turnCommentDelayMs, 1000),
|
|
49
51
|
summaryWaitTimeoutMs: clamp(num(raw.summaryWaitTimeoutMs, 120000), 1000, 600000),
|
|
50
52
|
};
|
|
@@ -83,14 +85,13 @@ export function resolveLabelColor(label: string): string {
|
|
|
83
85
|
if (label.startsWith("topic:")) return "fbca04";
|
|
84
86
|
if (label.startsWith("session:")) return "bfdadc";
|
|
85
87
|
if (label.startsWith("agent:")) return "1d76db";
|
|
86
|
-
if (label.startsWith("source:")) return "0e8a16";
|
|
87
88
|
return "0e8a16";
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
export function labelDescription(label: string): string {
|
|
91
92
|
for (const [pfx, d] of [["type:", "Issue type"], ["kind:", "Memory kind"], ["memory-status:", "Memory lifecycle status"],
|
|
92
93
|
["status:", "Conversation lifecycle status"], ["session:", "Session association"],
|
|
93
|
-
["date:", "Date"], ["topic:", "Topic"], ["agent:", "Agent"]
|
|
94
|
+
["date:", "Date"], ["topic:", "Topic"], ["agent:", "Agent"]] as const)
|
|
94
95
|
if (label.startsWith(pfx)) return `${d} label managed by clawmem.`;
|
|
95
96
|
return "Label managed by clawmem.";
|
|
96
97
|
}
|
package/src/github-client.ts
CHANGED
|
@@ -39,6 +39,9 @@ type CollaboratorResponse = {
|
|
|
39
39
|
name?: string;
|
|
40
40
|
permissions?: PermissionMap;
|
|
41
41
|
role_name?: string;
|
|
42
|
+
organization_member?: boolean;
|
|
43
|
+
outside_collaborator?: boolean;
|
|
44
|
+
type?: string;
|
|
42
45
|
};
|
|
43
46
|
type RepositoryInvitationResponse = {
|
|
44
47
|
id?: number;
|
|
@@ -209,9 +212,6 @@ export class GitHubIssueClient {
|
|
|
209
212
|
async getRepo(owner: string, repo: string): Promise<RepoResponse> {
|
|
210
213
|
return this.req<RepoResponse>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "GET" });
|
|
211
214
|
}
|
|
212
|
-
async listRepoTeams(owner: string, repo: string): Promise<TeamResponse[]> {
|
|
213
|
-
return this.req<TeamResponse[]>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/teams`, { method: "GET" });
|
|
214
|
-
}
|
|
215
215
|
async listUserRepoInvitations(): Promise<RepositoryInvitationResponse[]> {
|
|
216
216
|
return this.req<RepositoryInvitationResponse[]>("user/repository_invitations", { method: "GET" });
|
|
217
217
|
}
|
|
@@ -226,7 +226,7 @@ export class GitHubIssueClient {
|
|
|
226
226
|
}
|
|
227
227
|
async createOrgInvitation(
|
|
228
228
|
org: string,
|
|
229
|
-
params: { inviteeLogin: string; role?: "member" | "
|
|
229
|
+
params: { inviteeLogin: string; role?: "member" | "owner"; teamIds?: number[]; expiresInDays?: number },
|
|
230
230
|
): Promise<InvitationResponse> {
|
|
231
231
|
return this.req<InvitationResponse>(`orgs/${encodeURIComponent(org)}/invitations`, {
|
|
232
232
|
method: "POST",
|
package/src/memory.test.ts
CHANGED
|
@@ -41,6 +41,15 @@ function assert(condition: unknown, message: string): void {
|
|
|
41
41
|
if (!condition) throw new Error(message);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function testConfig(): never {
|
|
45
|
+
return {
|
|
46
|
+
memoryRecallLimit: 5,
|
|
47
|
+
memoryAutoRecallLimit: 5,
|
|
48
|
+
turnCommentDelayMs: 1000,
|
|
49
|
+
summaryWaitTimeoutMs: 120000,
|
|
50
|
+
} as never;
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
async function testSearchRanking(): Promise<void> {
|
|
45
54
|
const issues = [
|
|
46
55
|
issueFromMemory(memory({
|
|
@@ -60,7 +69,7 @@ async function testSearchRanking(): Promise<void> {
|
|
|
60
69
|
const client = {
|
|
61
70
|
listIssues: async () => issues,
|
|
62
71
|
};
|
|
63
|
-
const store = new MemoryStore(client as never, {} as never,
|
|
72
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
64
73
|
const found = await store.search("redis rate limiting", 5);
|
|
65
74
|
assert(found.length === 2, "expected both memories to match");
|
|
66
75
|
assert(found[0]?.issueNumber === 1, "expected the more specific Redis rate limiting memory to rank first");
|
|
@@ -93,7 +102,7 @@ async function testBackendSearchPreferredForRecall(): Promise<void> {
|
|
|
93
102
|
return searched;
|
|
94
103
|
},
|
|
95
104
|
};
|
|
96
|
-
const store = new MemoryStore(client as never, {} as never,
|
|
105
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
97
106
|
const found = await store.search("redis rate limiting", 5);
|
|
98
107
|
|
|
99
108
|
assert(queries.length === 1, "expected backend search to be called once");
|
|
@@ -117,7 +126,7 @@ async function testBackendSearchFallsBackToLocalLexical(): Promise<void> {
|
|
|
117
126
|
listIssues: async () => issues,
|
|
118
127
|
searchIssues: async () => { throw new Error("search unavailable"); },
|
|
119
128
|
};
|
|
120
|
-
const store = new MemoryStore(client as never, { logger: { warn: () => {} } } as never,
|
|
129
|
+
const store = new MemoryStore(client as never, { logger: { warn: () => {} } } as never, testConfig());
|
|
121
130
|
const found = await store.search("redis rate limiting", 5);
|
|
122
131
|
|
|
123
132
|
assert(found.length === 1 && found[0]?.issueNumber === 3, "expected lexical fallback when backend search fails");
|
|
@@ -155,7 +164,7 @@ async function testStructuredStoreAndSchema(): Promise<void> {
|
|
|
155
164
|
return { number: 99, title: payload.title };
|
|
156
165
|
},
|
|
157
166
|
};
|
|
158
|
-
const store = new MemoryStore(client as never, {} as never,
|
|
167
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
159
168
|
const result = await store.store({ detail: "Redis Lua scripts are required for atomic rate limiting.", kind: "Lesson", topics: ["Redis Ops", "rate_limit"] });
|
|
160
169
|
const schema = await store.listSchema();
|
|
161
170
|
|
|
@@ -211,7 +220,7 @@ async function testGetAndListMemories(): Promise<void> {
|
|
|
211
220
|
});
|
|
212
221
|
},
|
|
213
222
|
};
|
|
214
|
-
const store = new MemoryStore(client as never, {} as never,
|
|
223
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
215
224
|
const exact = await store.get("4");
|
|
216
225
|
const activeFacts = await store.listMemories({ status: "active", kind: "core-fact", limit: 10 });
|
|
217
226
|
const sports = await store.listMemories({ status: "all", topic: "sports", limit: 10 });
|
|
@@ -243,7 +252,7 @@ async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
|
|
|
243
252
|
});
|
|
244
253
|
},
|
|
245
254
|
};
|
|
246
|
-
const store = new MemoryStore(client as never, {} as never,
|
|
255
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
247
256
|
const exact = await store.get("4");
|
|
248
257
|
const recalled = await store.search("F1 Dota 2", 5);
|
|
249
258
|
|
|
@@ -292,7 +301,7 @@ async function testUpdateMemoryInPlace(): Promise<void> {
|
|
|
292
301
|
issue.labels = labels;
|
|
293
302
|
},
|
|
294
303
|
};
|
|
295
|
-
const store = new MemoryStore(client as never, {} as never,
|
|
304
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
296
305
|
const updated = await store.update("4", {
|
|
297
306
|
detail: "xiangz likes F1, watches Dota 2 as a viewer, and recently follows tennis.",
|
|
298
307
|
topics: ["preferences", "sports"],
|
|
@@ -344,7 +353,7 @@ async function testForgetClosesMemoryIssue(): Promise<void> {
|
|
|
344
353
|
return issue;
|
|
345
354
|
},
|
|
346
355
|
};
|
|
347
|
-
const store = new MemoryStore(client as never, {} as never,
|
|
356
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
348
357
|
const forgotten = await store.forget("12");
|
|
349
358
|
|
|
350
359
|
assert(forgotten?.status === "stale", "expected forgotten memory to be returned as stale");
|
package/src/memory.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
|
3
3
|
import { LABEL_MEMORY_STALE, MEMORY_TITLE_PREFIX, extractLabelNames, labelVal } from "./config.js";
|
|
4
4
|
import type { GitHubIssueClient } from "./github-client.js";
|
|
5
5
|
import { normalizeMessages } from "./transcript.js";
|
|
6
|
-
import type { ClawMemPluginConfig, MemoryDraft, MemoryListOptions, MemorySchema,
|
|
6
|
+
import type { ClawMemPluginConfig, MemoryDraft, MemoryListOptions, MemorySchema, ParsedMemoryIssue, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
7
7
|
import { fmtTranscript, localDate, sha256, subKey } from "./utils.js";
|
|
8
8
|
import { parseFlatYaml, stringifyFlatYaml } from "./yaml.js";
|
|
9
9
|
|
|
@@ -206,7 +206,8 @@ export class MemoryStore {
|
|
|
206
206
|
const status = issue.state === "closed" || labels.includes(LABEL_MEMORY_STALE) ? "stale" : "active";
|
|
207
207
|
if (!detail) return null;
|
|
208
208
|
return {
|
|
209
|
-
issueNumber: issue.number,
|
|
209
|
+
issueNumber: issue.number,
|
|
210
|
+
title: issue.title?.trim() || "",
|
|
210
211
|
memoryId: body.memory_id?.trim() || String(issue.number),
|
|
211
212
|
memoryHash: body.memory_hash?.trim() || undefined,
|
|
212
213
|
date: body.date?.trim() || "1970-01-01",
|
|
@@ -280,6 +281,7 @@ export class MemoryStore {
|
|
|
280
281
|
const message = [
|
|
281
282
|
"Extract durable memories from the conversation below.",
|
|
282
283
|
'Return JSON only in the form {"save":[{"detail":"...","kind":"...","topics":["..."]}],"stale":["memory-id"]}.',
|
|
284
|
+
"Each save item must contain one durable fact. If a turn contains several independent facts, save them separately instead of bundling them into one summary memory.",
|
|
283
285
|
"Use save for stable, reusable facts, preferences, decisions, constraints, workflows, and ongoing context worth remembering later.",
|
|
284
286
|
"Use stale for existing memory IDs only when the conversation clearly supersedes or invalidates them.",
|
|
285
287
|
"Infer kind and topics when they would help future retrieval. Reuse existing kinds and topics when possible.",
|
|
@@ -293,9 +295,12 @@ export class MemoryStore {
|
|
|
293
295
|
].join("\n");
|
|
294
296
|
try {
|
|
295
297
|
const run = await subagent.run({
|
|
296
|
-
sessionKey,
|
|
298
|
+
sessionKey,
|
|
299
|
+
message,
|
|
300
|
+
deliver: false,
|
|
301
|
+
lane: "clawmem-memory",
|
|
297
302
|
idempotencyKey: sha256(`${session.sessionId}:${snapshot.messages.length}:memory-decision`),
|
|
298
|
-
extraSystemPrompt: "You extract durable memory updates from OpenClaw conversations. Output JSON only with save objects containing detail, optional kind, and optional topics, plus stale string ids.",
|
|
303
|
+
extraSystemPrompt: "You extract durable memory updates from OpenClaw conversations. Output JSON only with save objects containing detail, optional kind, and optional topics, plus stale string ids. Keep each save item to one durable fact.",
|
|
299
304
|
});
|
|
300
305
|
const wait = await subagent.waitForRun({ runId: run.runId, timeoutMs: this.config.summaryWaitTimeoutMs });
|
|
301
306
|
if (wait.status === "timeout") throw new Error("memory decision subagent timed out");
|
|
@@ -304,7 +309,9 @@ export class MemoryStore {
|
|
|
304
309
|
const text = [...msgs].reverse().find((e) => e.role === "assistant" && e.text.trim())?.text;
|
|
305
310
|
if (!text) throw new Error("memory decision subagent returned no assistant text");
|
|
306
311
|
return parseDecision(text);
|
|
307
|
-
} finally {
|
|
312
|
+
} finally {
|
|
313
|
+
subagent.deleteSession({ sessionKey, deleteTranscript: true }).catch(() => {});
|
|
314
|
+
}
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
private async mergeSchema(memory: ParsedMemoryIssue, draft: MemoryDraft): Promise<ParsedMemoryIssue> {
|
|
@@ -333,11 +340,13 @@ function memLabels(kind?: string, topics?: string[]): string[] {
|
|
|
333
340
|
...((topics ?? []).map((topic) => `topic:${topic}`)),
|
|
334
341
|
];
|
|
335
342
|
}
|
|
343
|
+
|
|
336
344
|
function norm(v: string): string { return v.replace(/\s+/g, " ").trim(); }
|
|
337
345
|
function trunc(v: string, max: number): string { const s = norm(v); return s.length <= max ? s : `${s.slice(0, max - 1).trimEnd()}…`; }
|
|
338
346
|
function normalizeSearch(v: string): string {
|
|
339
347
|
return v.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
|
|
340
348
|
}
|
|
349
|
+
|
|
341
350
|
function buildSearchIndex(memory: ParsedMemoryIssue): SearchIndex {
|
|
342
351
|
return {
|
|
343
352
|
title: normalizeSearch(memory.title),
|
|
@@ -346,6 +355,7 @@ function buildSearchIndex(memory: ParsedMemoryIssue): SearchIndex {
|
|
|
346
355
|
topics: (memory.topics ?? []).map(normalizeSearch).filter(Boolean),
|
|
347
356
|
};
|
|
348
357
|
}
|
|
358
|
+
|
|
349
359
|
function searchTokens(v: string): string[] {
|
|
350
360
|
const seen = new Set<string>();
|
|
351
361
|
for (const token of v.split(/[^0-9\p{L}]+/u)) {
|
|
@@ -359,6 +369,7 @@ function searchTokens(v: string): string[] {
|
|
|
359
369
|
}
|
|
360
370
|
return [...seen];
|
|
361
371
|
}
|
|
372
|
+
|
|
362
373
|
function charBigrams(v: string): Set<string> {
|
|
363
374
|
const compact = v.replace(/\s+/g, "");
|
|
364
375
|
if (compact.length < 2) return new Set(compact ? [compact] : []);
|
|
@@ -366,16 +377,19 @@ function charBigrams(v: string): Set<string> {
|
|
|
366
377
|
for (let i = 0; i < compact.length - 1; i++) out.add(compact.slice(i, i + 2));
|
|
367
378
|
return out;
|
|
368
379
|
}
|
|
380
|
+
|
|
369
381
|
function overlapRatio(left: Set<string>, right: Set<string>): number {
|
|
370
382
|
if (left.size === 0 || right.size === 0) return 0;
|
|
371
383
|
let hits = 0;
|
|
372
384
|
for (const token of left) if (right.has(token)) hits++;
|
|
373
385
|
return hits / Math.max(left.size, right.size);
|
|
374
386
|
}
|
|
387
|
+
|
|
375
388
|
function buildMemorySearchQuery(query: string, repo: string): string {
|
|
376
389
|
const parts = [query.trim(), `repo:${repo}`, "is:issue", "state:open", 'label:"type:memory"'].filter(Boolean);
|
|
377
390
|
return parts.join(" ");
|
|
378
391
|
}
|
|
392
|
+
|
|
379
393
|
export function scoreMemoryMatch(memory: ParsedMemoryIssue, rawQuery: string): number {
|
|
380
394
|
const query = normalizeSearch(rawQuery);
|
|
381
395
|
if (!query) return 0;
|
|
@@ -411,6 +425,7 @@ export function scoreMemoryMatch(memory: ParsedMemoryIssue, rawQuery: string): n
|
|
|
411
425
|
|
|
412
426
|
return score;
|
|
413
427
|
}
|
|
428
|
+
|
|
414
429
|
function normalizeDraft(input: MemoryDraft): MemoryDraft {
|
|
415
430
|
const detail = norm(input.detail);
|
|
416
431
|
if (!detail) throw new Error("memory detail is empty");
|
|
@@ -422,6 +437,7 @@ function normalizeDraft(input: MemoryDraft): MemoryDraft {
|
|
|
422
437
|
...(topics.length > 0 ? { topics } : {}),
|
|
423
438
|
};
|
|
424
439
|
}
|
|
440
|
+
|
|
425
441
|
function normalizeLabelValue(value: string | undefined, prefix: string): string | undefined {
|
|
426
442
|
if (!value) return undefined;
|
|
427
443
|
const raw = value.trim().replace(new RegExp(`^${prefix}`, "i"), "");
|
|
@@ -433,6 +449,7 @@ function normalizeLabelValue(value: string | undefined, prefix: string): string
|
|
|
433
449
|
.replace(/^-+|-+$/g, "");
|
|
434
450
|
return normalized || undefined;
|
|
435
451
|
}
|
|
452
|
+
|
|
436
453
|
function normalizeOptionalLabelValue(value: string | undefined, prefix: string): string | undefined {
|
|
437
454
|
try {
|
|
438
455
|
return normalizeLabelValue(value, prefix);
|
|
@@ -440,16 +457,22 @@ function normalizeOptionalLabelValue(value: string | undefined, prefix: string):
|
|
|
440
457
|
return undefined;
|
|
441
458
|
}
|
|
442
459
|
}
|
|
460
|
+
|
|
443
461
|
function uniqueNormalized(values: string[]): string[] {
|
|
444
462
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort();
|
|
445
463
|
}
|
|
464
|
+
|
|
446
465
|
function parseDecision(raw: string): MemoryDecision {
|
|
447
466
|
const tryParse = (s: string): MemoryDecision | null => {
|
|
448
467
|
try {
|
|
449
468
|
const p = JSON.parse(s) as Record<string, unknown>;
|
|
450
|
-
return {
|
|
451
|
-
|
|
452
|
-
|
|
469
|
+
return {
|
|
470
|
+
save: Array.isArray(p.save) ? p.save.map(parseSaveItem).filter((v): v is MemoryDraft => Boolean(v)) : [],
|
|
471
|
+
stale: Array.isArray(p.stale) ? p.stale.filter((v): v is string => typeof v === "string") : [],
|
|
472
|
+
};
|
|
473
|
+
} catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
453
476
|
};
|
|
454
477
|
const t = raw.trim();
|
|
455
478
|
return tryParse(t) ?? (() => {
|
|
@@ -459,6 +482,7 @@ function parseDecision(raw: string): MemoryDecision {
|
|
|
459
482
|
throw new Error("memory decision subagent returned invalid JSON");
|
|
460
483
|
})();
|
|
461
484
|
}
|
|
485
|
+
|
|
462
486
|
function parseSaveItem(value: unknown): MemoryDraft | null {
|
|
463
487
|
if (typeof value === "string") {
|
|
464
488
|
const detail = norm(value);
|
package/src/service.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Thin orchestrator: wires conversation mirroring, memory store, and plugin lifecycle.
|
|
2
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { hasDefaultRepo, isAgentConfigured, resolveAgentRoute, resolvePluginConfig } from "./config.js";
|
|
4
|
+
import { filterDirectCollaborators, listRepoAccessTeams, resolveOrgInvitationRole } from "./collaboration.js";
|
|
4
5
|
import { ConversationMirror } from "./conversation.js";
|
|
5
6
|
import { GitHubIssueClient } from "./github-client.js";
|
|
6
7
|
import { KeyedAsyncQueue } from "./keyed-async-queue.js";
|
|
@@ -13,15 +14,17 @@ import { buildAgentBootstrapRegistration, inferAgentIdFromTranscriptPath, normal
|
|
|
13
14
|
type TurnPayload = { sessionId?: string; sessionKey?: string; agentId?: string; messages: unknown[] };
|
|
14
15
|
type FinalizePayload = { sessionId?: string; sessionKey?: string; sessionFile?: string; agentId?: string; reason?: string; messages?: unknown[] };
|
|
15
16
|
type CollaborationPermission = "read" | "write" | "admin";
|
|
16
|
-
type CollaborationOrgRole = "member" | "admin";
|
|
17
17
|
type CollaborationTeamRole = "member" | "maintainer";
|
|
18
18
|
|
|
19
|
+
const SESSION_MAINTENANCE_RETRY_DELAYS_MS = [5000, 30000, 120000] as const;
|
|
20
|
+
|
|
19
21
|
class ClawMemService {
|
|
20
22
|
private readonly config: ClawMemPluginConfig;
|
|
21
23
|
private readonly queue = new KeyedAsyncQueue();
|
|
22
24
|
private readonly stateQueue = new KeyedAsyncQueue();
|
|
23
25
|
private readonly pending = new Set<Promise<unknown>>();
|
|
24
26
|
private readonly syncTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
27
|
+
private readonly maintenanceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
25
28
|
private statePath = "";
|
|
26
29
|
private state: PluginState = { version: 2, sessions: {} };
|
|
27
30
|
private unsubTranscript?: () => void;
|
|
@@ -62,6 +65,8 @@ class ClawMemService {
|
|
|
62
65
|
this.unsubTranscript?.();
|
|
63
66
|
for (const t of this.syncTimers.values()) clearTimeout(t);
|
|
64
67
|
this.syncTimers.clear();
|
|
68
|
+
for (const t of this.maintenanceTimers.values()) clearTimeout(t);
|
|
69
|
+
this.maintenanceTimers.clear();
|
|
65
70
|
await Promise.allSettled([...this.pending]);
|
|
66
71
|
},
|
|
67
72
|
});
|
|
@@ -224,7 +229,7 @@ class ClawMemService {
|
|
|
224
229
|
|
|
225
230
|
this.api.registerTool({
|
|
226
231
|
name: "memory_recall",
|
|
227
|
-
description: "Search ClawMem active memories for relevant prior facts, decisions, conventions, and lessons.",
|
|
232
|
+
description: "Search ClawMem active memories for relevant prior facts, decisions, conventions, and lessons. Use this before answering questions about prior conversations, earlier assistant responses, user preferences, or historical project context.",
|
|
228
233
|
required: true,
|
|
229
234
|
parameters: {
|
|
230
235
|
type: "object",
|
|
@@ -287,7 +292,7 @@ class ClawMemService {
|
|
|
287
292
|
|
|
288
293
|
this.api.registerTool({
|
|
289
294
|
name: "memory_store",
|
|
290
|
-
description: "Store
|
|
295
|
+
description: "Store one atomic durable ClawMem memory immediately instead of waiting for session finalization. Keep each write to a single fact, preference, decision, or timeline update.",
|
|
291
296
|
required: true,
|
|
292
297
|
parameters: {
|
|
293
298
|
type: "object",
|
|
@@ -316,7 +321,11 @@ class ClawMemService {
|
|
|
316
321
|
if ("error" in resolved) return toolText(resolved.error);
|
|
317
322
|
const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
|
|
318
323
|
const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
|
|
319
|
-
const result = await resolved.mem.store({
|
|
324
|
+
const result = await resolved.mem.store({
|
|
325
|
+
detail,
|
|
326
|
+
...(kind ? { kind } : {}),
|
|
327
|
+
...(topics && topics.length > 0 ? { topics } : {}),
|
|
328
|
+
});
|
|
320
329
|
if (!result.created) return toolText(`Memory already exists in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
|
|
321
330
|
return toolText(`Stored memory in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
|
|
322
331
|
},
|
|
@@ -1006,7 +1015,7 @@ class ClawMemService {
|
|
|
1006
1015
|
properties: {
|
|
1007
1016
|
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
1008
1017
|
inviteeLogin: { type: "string", minLength: 1, description: "Username to invite." },
|
|
1009
|
-
role: { type: "string", enum: ["member", "
|
|
1018
|
+
role: { type: "string", enum: ["member", "owner"], description: "Org role for the invitation. Defaults to member." },
|
|
1010
1019
|
teamIds: {
|
|
1011
1020
|
type: "array",
|
|
1012
1021
|
description: "Optional numeric team ids to pre-assign on acceptance.",
|
|
@@ -1027,7 +1036,8 @@ class ClawMemService {
|
|
|
1027
1036
|
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
1028
1037
|
const inviteeLogin = typeof p.inviteeLogin === "string" ? p.inviteeLogin.trim() : "";
|
|
1029
1038
|
if (!org || !inviteeLogin) return toolText("org and inviteeLogin are required.");
|
|
1030
|
-
const role
|
|
1039
|
+
const role = resolveOrgInvitationRole(p.role, "member");
|
|
1040
|
+
if ("error" in role) return toolText(role.error);
|
|
1031
1041
|
const teamIds = Array.isArray(p.teamIds)
|
|
1032
1042
|
? p.teamIds.filter((value): value is number => typeof value === "number" && Number.isInteger(value) && value > 0)
|
|
1033
1043
|
: undefined;
|
|
@@ -1039,7 +1049,7 @@ class ClawMemService {
|
|
|
1039
1049
|
try {
|
|
1040
1050
|
const invitation = await resolved.client.createOrgInvitation(org, {
|
|
1041
1051
|
inviteeLogin,
|
|
1042
|
-
role,
|
|
1052
|
+
role: role.role,
|
|
1043
1053
|
...(teamIds && teamIds.length > 0 ? { teamIds } : {}),
|
|
1044
1054
|
...(expiresInDays ? { expiresInDays } : {}),
|
|
1045
1055
|
});
|
|
@@ -1228,9 +1238,9 @@ class ClawMemService {
|
|
|
1228
1238
|
}
|
|
1229
1239
|
|
|
1230
1240
|
try {
|
|
1231
|
-
const collaborators = await target.client.listRepoCollaborators(target.owner, target.repo);
|
|
1241
|
+
const collaborators = filterDirectCollaborators(await target.client.listRepoCollaborators(target.owner, target.repo), target.owner);
|
|
1232
1242
|
lines.push("");
|
|
1233
|
-
lines.push("
|
|
1243
|
+
lines.push("Explicit collaborators (excluding owner):");
|
|
1234
1244
|
if (collaborators.length === 0) lines.push("- None visible");
|
|
1235
1245
|
else lines.push(...collaborators.map((collaborator) => `- ${renderCollaboratorLine(collaborator)}`));
|
|
1236
1246
|
} catch (error) {
|
|
@@ -1247,14 +1257,17 @@ class ClawMemService {
|
|
|
1247
1257
|
notes.push(`Repo invitation lookup failed: ${String(error)}`);
|
|
1248
1258
|
}
|
|
1249
1259
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1260
|
+
if (orgName) {
|
|
1261
|
+
try {
|
|
1262
|
+
const teamAccess = await listRepoAccessTeams(target.client, orgName, target.fullName);
|
|
1263
|
+
lines.push("");
|
|
1264
|
+
lines.push("Teams with repo access:");
|
|
1265
|
+
if (teamAccess.teams.length === 0) lines.push("- None visible");
|
|
1266
|
+
else lines.push(...teamAccess.teams.map((team) => `- ${renderTeamLine(team)}`));
|
|
1267
|
+
notes.push(...teamAccess.notes);
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
notes.push(`Repo team grant lookup failed: ${String(error)}`);
|
|
1270
|
+
}
|
|
1258
1271
|
}
|
|
1259
1272
|
|
|
1260
1273
|
try {
|
|
@@ -1283,13 +1296,13 @@ class ClawMemService {
|
|
|
1283
1296
|
private async handleBeforeAgentStart(prompt: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
|
|
1284
1297
|
const routeAgentId = normalizeAgentId(agentId);
|
|
1285
1298
|
if (!(await this.ensureDefaultRepoConfigured(routeAgentId))) return;
|
|
1286
|
-
|
|
1299
|
+
this.scheduleRecentSessionMaintenance(routeAgentId);
|
|
1287
1300
|
if (typeof prompt !== "string" || prompt.trim().length < 5) return;
|
|
1288
1301
|
try {
|
|
1289
1302
|
const { mem } = this.getServices(routeAgentId);
|
|
1290
|
-
const memories = await mem.search(prompt, this.config.
|
|
1303
|
+
const memories = await mem.search(prompt, this.config.memoryAutoRecallLimit);
|
|
1291
1304
|
if (memories.length === 0) return;
|
|
1292
|
-
const text = memories.map((m) => `- ${m
|
|
1305
|
+
const text = memories.map((m) => `- ${formatInjectedMemory(m)}`).join("\n");
|
|
1293
1306
|
return { prependContext: `<relevant-memories>\nThe following active memories may be relevant to this conversation:\n${text}\n</relevant-memories>` };
|
|
1294
1307
|
} catch (error) { this.api.logger.warn(`clawmem: memory recall failed: ${String(error)}`); }
|
|
1295
1308
|
}
|
|
@@ -1357,6 +1370,7 @@ class ClawMemService {
|
|
|
1357
1370
|
private async finalize(p: FinalizePayload): Promise<void> {
|
|
1358
1371
|
if (!p.sessionId) return;
|
|
1359
1372
|
const agentId = normalizeAgentId(p.agentId);
|
|
1373
|
+
const scopeKey = sessionScopeKey(p.sessionId, agentId);
|
|
1360
1374
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1361
1375
|
const { conv } = this.getServices(agentId);
|
|
1362
1376
|
const s = this.getOrCreate(p.sessionId, agentId);
|
|
@@ -1375,6 +1389,7 @@ class ClawMemService {
|
|
|
1375
1389
|
s.summaryStatus = "pending";
|
|
1376
1390
|
if (allOk) s.finalizedAt = new Date().toISOString();
|
|
1377
1391
|
await this.persistState();
|
|
1392
|
+
this.scheduleSessionMaintenance(scopeKey, agentId, { reason: p.reason ?? "finalize" });
|
|
1378
1393
|
}
|
|
1379
1394
|
|
|
1380
1395
|
// --- Infrastructure ---
|
|
@@ -1531,55 +1546,109 @@ class ClawMemService {
|
|
|
1531
1546
|
},
|
|
1532
1547
|
});
|
|
1533
1548
|
}
|
|
1534
|
-
private
|
|
1549
|
+
private scheduleRecentSessionMaintenance(agentId: string): void {
|
|
1535
1550
|
const sessions = Object.values(this.state.sessions)
|
|
1536
1551
|
.filter((session) => normalizeAgentId(session.agentId) === agentId)
|
|
1537
1552
|
.sort((a, b) => Date.parse(b.updatedAt ?? b.createdAt ?? "") - Date.parse(a.updatedAt ?? a.createdAt ?? ""))
|
|
1538
1553
|
.slice(0, 8);
|
|
1539
|
-
|
|
1554
|
+
for (const session of sessions) {
|
|
1555
|
+
if (!this.sessionNeedsMaintenance(session)) continue;
|
|
1556
|
+
this.scheduleSessionMaintenance(sessionScopeKey(session.sessionId, session.agentId), agentId, {
|
|
1557
|
+
reason: "request-start-fallback",
|
|
1558
|
+
delayMs: 0,
|
|
1559
|
+
});
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
private scheduleSessionMaintenance(
|
|
1565
|
+
scopeKey: string,
|
|
1566
|
+
agentId: string,
|
|
1567
|
+
options: { delayMs?: number; attempt?: number; reason?: string } = {},
|
|
1568
|
+
): void {
|
|
1569
|
+
const prev = this.maintenanceTimers.get(scopeKey);
|
|
1570
|
+
if (prev) clearTimeout(prev);
|
|
1571
|
+
const delayMs = Math.max(0, options.delayMs ?? 0);
|
|
1572
|
+
const attempt = Math.max(0, options.attempt ?? 0);
|
|
1573
|
+
const reason = options.reason ?? "scheduled";
|
|
1574
|
+
const timer = setTimeout(() => {
|
|
1575
|
+
this.maintenanceTimers.delete(scopeKey);
|
|
1576
|
+
void this.track(this.enqueueSession(scopeKey, () => this.runSessionMaintenance(scopeKey, agentId, attempt, reason)))
|
|
1577
|
+
.catch((error) => this.warn(`background maintenance for ${scopeKey}`, error));
|
|
1578
|
+
}, delayMs);
|
|
1579
|
+
timer.unref?.();
|
|
1580
|
+
this.maintenanceTimers.set(scopeKey, timer);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
private async runSessionMaintenance(scopeKey: string, agentId: string, attempt: number, reason: string): Promise<void> {
|
|
1584
|
+
const session = this.state.sessions[scopeKey];
|
|
1585
|
+
if (!session || !this.sessionNeedsMaintenance(session)) return;
|
|
1586
|
+
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1540
1587
|
const { conv, mem } = this.getServices(agentId);
|
|
1588
|
+
const snap = await conv.loadSnapshot(session, []);
|
|
1589
|
+
if (!conv.shouldMirror(session.sessionId, snap.messages) || snap.messages.length === 0) return;
|
|
1541
1590
|
let changed = false;
|
|
1542
|
-
let
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
session.summaryStatus = "complete";
|
|
1557
|
-
if (result.title?.trim()) {
|
|
1558
|
-
session.issueTitle = result.title.trim();
|
|
1559
|
-
session.titleSource = "llm";
|
|
1560
|
-
}
|
|
1561
|
-
this.maybeAutoNameRepo(agentId, result.summary, result.title);
|
|
1562
|
-
changed = true;
|
|
1563
|
-
workDone++;
|
|
1564
|
-
} catch (error) {
|
|
1565
|
-
this.warn(`request-scoped summary sync for ${session.sessionId}`, error);
|
|
1591
|
+
let retryNeeded = false;
|
|
1592
|
+
if (!session.issueNumber) {
|
|
1593
|
+
await conv.ensureIssue(session, snap);
|
|
1594
|
+
changed = true;
|
|
1595
|
+
}
|
|
1596
|
+
if (session.summaryStatus === "pending") {
|
|
1597
|
+
try {
|
|
1598
|
+
const result = await conv.generateSummaryAndTitle(session, snap);
|
|
1599
|
+
await conv.syncLabels(session, snap, true);
|
|
1600
|
+
await conv.syncBody(session, snap, result.summary, true, result.title);
|
|
1601
|
+
session.summaryStatus = "complete";
|
|
1602
|
+
if (result.title?.trim()) {
|
|
1603
|
+
session.issueTitle = result.title.trim();
|
|
1604
|
+
session.titleSource = "llm";
|
|
1566
1605
|
}
|
|
1606
|
+
this.maybeAutoNameRepo(agentId, result.summary, result.title);
|
|
1607
|
+
changed = true;
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
retryNeeded = true;
|
|
1610
|
+
this.warn(`background summary sync for ${session.sessionId}`, error);
|
|
1567
1611
|
}
|
|
1568
|
-
|
|
1612
|
+
}
|
|
1613
|
+
if (session.titleSource !== "llm" && snap.messages.length >= 2) {
|
|
1614
|
+
try {
|
|
1569
1615
|
await conv.syncTitle(session, snap);
|
|
1570
1616
|
changed = true;
|
|
1571
|
-
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
retryNeeded = true;
|
|
1619
|
+
this.warn(`background title sync for ${session.sessionId}`, error);
|
|
1572
1620
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1621
|
+
}
|
|
1622
|
+
if (snap.messages.length >= 2 && snap.messages.length > (session.lastMemorySyncCount ?? 0)) {
|
|
1623
|
+
const ok = await mem.syncFromConversation(session, snap);
|
|
1624
|
+
if (ok) {
|
|
1625
|
+
session.lastMemorySyncCount = snap.messages.length;
|
|
1626
|
+
changed = true;
|
|
1627
|
+
} else {
|
|
1628
|
+
retryNeeded = true;
|
|
1580
1629
|
}
|
|
1581
1630
|
}
|
|
1582
1631
|
if (changed) await this.persistState();
|
|
1632
|
+
if (!retryNeeded || !this.sessionNeedsMaintenance(session)) return;
|
|
1633
|
+
if (attempt < SESSION_MAINTENANCE_RETRY_DELAYS_MS.length) {
|
|
1634
|
+
const delayMs = SESSION_MAINTENANCE_RETRY_DELAYS_MS[attempt];
|
|
1635
|
+
this.api.logger.warn?.(
|
|
1636
|
+
`clawmem: background maintenance incomplete for ${session.sessionId}; retrying in ${Math.round(delayMs / 1000)}s (${reason})`,
|
|
1637
|
+
);
|
|
1638
|
+
this.scheduleSessionMaintenance(scopeKey, agentId, { delayMs, attempt: attempt + 1, reason: "retry" });
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
this.api.logger.warn?.(
|
|
1642
|
+
`clawmem: background maintenance remains pending for ${session.sessionId}; it will be retried opportunistically on future requests`,
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
private sessionNeedsMaintenance(session: SessionMirrorState): boolean {
|
|
1647
|
+
if (session.summaryStatus === "pending") return true;
|
|
1648
|
+
const hasMeaningfulTranscript = Math.max(session.lastMirroredCount, session.turnCount) >= 2;
|
|
1649
|
+
if (!hasMeaningfulTranscript) return false;
|
|
1650
|
+
if (session.titleSource !== "llm") return true;
|
|
1651
|
+
return (session.lastMemorySyncCount ?? 0) < session.lastMirroredCount;
|
|
1583
1652
|
}
|
|
1584
1653
|
|
|
1585
1654
|
private getServices(agentId?: string, repo?: string): { route: ClawMemResolvedRoute; conv: ConversationMirror; mem: MemoryStore; client: GitHubIssueClient } {
|
|
@@ -1706,11 +1775,30 @@ function shouldFallbackToAnonymousBootstrap(error: unknown): boolean {
|
|
|
1706
1775
|
function toolText(text: string): { content: Array<{ type: "text"; text: string }> } {
|
|
1707
1776
|
return { content: [{ type: "text", text }] };
|
|
1708
1777
|
}
|
|
1709
|
-
function renderMemoryLine(memory: {
|
|
1710
|
-
|
|
1778
|
+
function renderMemoryLine(memory: {
|
|
1779
|
+
memoryId: string;
|
|
1780
|
+
title?: string;
|
|
1781
|
+
detail: string;
|
|
1782
|
+
kind?: string;
|
|
1783
|
+
topics?: string[];
|
|
1784
|
+
status: "active" | "stale";
|
|
1785
|
+
}): string {
|
|
1786
|
+
const schema = [
|
|
1787
|
+
memory.kind ? `kind:${memory.kind}` : "",
|
|
1788
|
+
...(memory.topics ?? []).map((topic) => `topic:${topic}`),
|
|
1789
|
+
].filter(Boolean).join(", ");
|
|
1711
1790
|
return `[${memory.memoryId}] ${memory.title || "Memory"}${schema ? ` (${schema})` : ""}${memory.status === "stale" ? " [stale]" : ""}: ${memory.detail}`;
|
|
1712
1791
|
}
|
|
1713
|
-
function renderMemoryBlock(memory: {
|
|
1792
|
+
function renderMemoryBlock(memory: {
|
|
1793
|
+
memoryId: string;
|
|
1794
|
+
issueNumber?: number;
|
|
1795
|
+
title?: string;
|
|
1796
|
+
detail: string;
|
|
1797
|
+
kind?: string;
|
|
1798
|
+
topics?: string[];
|
|
1799
|
+
status: "active" | "stale";
|
|
1800
|
+
date?: string;
|
|
1801
|
+
}): string {
|
|
1714
1802
|
const lines = [
|
|
1715
1803
|
`Memory ID: ${memory.memoryId}`,
|
|
1716
1804
|
...(typeof memory.issueNumber === "number" ? [`Issue Number: ${memory.issueNumber}`] : []),
|
|
@@ -1724,6 +1812,12 @@ function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; tit
|
|
|
1724
1812
|
return lines.join("\n");
|
|
1725
1813
|
}
|
|
1726
1814
|
|
|
1815
|
+
function formatInjectedMemory(memory: {
|
|
1816
|
+
detail: string;
|
|
1817
|
+
}): string {
|
|
1818
|
+
return memory.detail;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1727
1821
|
function renderOrgLine(org: { login?: string; name?: string; default_repository_permission?: string; description?: string }): string {
|
|
1728
1822
|
const login = org.login?.trim() || "unknown-org";
|
|
1729
1823
|
const name = org.name?.trim() ? ` (${org.name.trim()})` : "";
|
package/src/types.ts
CHANGED