@clawmem-ai/clawmem 0.1.9 → 0.1.11

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.
@@ -0,0 +1,63 @@
1
+ # ClawMem Memory Schema
2
+
3
+ Use this reference when deciding how to label or shape a memory, or when you need to explain the ClawMem graph model to another agent or user.
4
+
5
+ ## The memory graph
6
+
7
+ Issues are nodes. Labels are schema. `#ID` references are edges.
8
+
9
+ When one memory depends on, refines, supersedes, or generalizes another memory, mention the related `#ID` in the issue body so the relationship stays explicit in the graph.
10
+
11
+ There are two valid memory shapes:
12
+ - Plugin-managed structured memories: created through `memory_store` or `memory_update`; the plugin manages core labels and may also persist agent-selected `kind:*` and `topic:*` labels
13
+ - Curated graph memories: created manually through `gh` or `curl` when you explicitly need raw issue control
14
+
15
+ ## Labels
16
+
17
+ Plugin-managed memories always include:
18
+ - `type:memory`
19
+
20
+ Plugin-managed memories may also include:
21
+ - one `kind:*` label
22
+ - optional `topic:*` labels
23
+
24
+ Lifecycle is carried by native issue state:
25
+ - open issue = active memory
26
+ - closed issue = stale or superseded memory
27
+
28
+ If you create a curated memory manually, include:
29
+ - `type:memory`
30
+ - one `kind:*` label
31
+ - optional `topic:*` labels, usually no more than two or three
32
+
33
+ ## Kinds
34
+
35
+ | Kind | Label | Use it for |
36
+ |---|---|---|
37
+ | Core fact | `kind:core-fact` | Stable truths that should update in place as reality changes |
38
+ | Convention | `kind:convention` | Agreed rules or policies |
39
+ | Lesson learned | `kind:lesson` | Corrections, postmortems, or mistakes worth preserving |
40
+ | Skill blueprint | `kind:skill` | Repeatable workflows or playbooks |
41
+ | Active task | `kind:task` | Ongoing work that should stay visible and update over time |
42
+
43
+ ## When to create which kind
44
+
45
+ | Trigger | Kind |
46
+ |---|---|
47
+ | User corrects a wrong assumption | `kind:lesson` |
48
+ | You and the user agree on a rule | `kind:convention` |
49
+ | A stable fact about the user or project | `kind:core-fact` |
50
+ | A repeatable workflow you figured out | `kind:skill` |
51
+ | Ongoing work to track | `kind:task` |
52
+
53
+ ## Disciplined self-evolution
54
+
55
+ - Before inventing a new `kind` or `topic`, call `memory_labels`.
56
+ - Reuse current schema when it already fits.
57
+ - If the current schema does not fit and a new label would help future retrieval, coordination, or reuse, create it deliberately.
58
+ - New labels should be short, general, and likely to apply again across future memories or agents.
59
+ - Do not invent random label prefixes. Schema evolution must stay within `kind:*` and `topic:*`.
60
+
61
+ ## Storage rule
62
+
63
+ 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,54 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ import os
5
+ import shlex
6
+ import subprocess
7
+ import sys
8
+
9
+
10
+ def normalize_base_url(raw: str) -> str:
11
+ value = (raw or "https://git.clawmem.ai/api/v3").rstrip("/")
12
+ if not value.endswith("/api/v3"):
13
+ value = f"{value}/api/v3"
14
+ return value
15
+
16
+
17
+ def main() -> int:
18
+ agent_id = (sys.argv[1].strip() if len(sys.argv) > 1 and sys.argv[1].strip() else os.environ.get("OPENCLAW_AGENT_ID", "main"))
19
+ repo_override = sys.argv[2].strip() if len(sys.argv) > 2 else ""
20
+
21
+ try:
22
+ cfg_path = subprocess.check_output(["openclaw", "config", "file"], text=True).strip()
23
+ except FileNotFoundError:
24
+ print("clawmem_exports.py: openclaw CLI was not found in PATH", file=sys.stderr)
25
+ return 1
26
+ with open(os.path.expanduser(cfg_path), "r", encoding="utf-8") as handle:
27
+ root = json.load(handle)
28
+
29
+ cfg = (((root.get("plugins") or {}).get("entries") or {}).get("clawmem") or {}).get("config") or {}
30
+ agents = cfg.get("agents") or {}
31
+ route = agents.get(agent_id) or {}
32
+
33
+ base_url = normalize_base_url(route.get("baseUrl") or cfg.get("baseUrl") or "")
34
+ default_repo = route.get("defaultRepo") or route.get("repo") or cfg.get("defaultRepo") or cfg.get("repo") or ""
35
+ repo = repo_override or default_repo
36
+ token = route.get("token") or ""
37
+ host = base_url.removesuffix("/api/v3").replace("https://", "").replace("http://", "")
38
+
39
+ pairs = {
40
+ "CLAWMEM_AGENT_ID": agent_id,
41
+ "CLAWMEM_BASE_URL": base_url,
42
+ "CLAWMEM_HOST": host,
43
+ "CLAWMEM_DEFAULT_REPO": default_repo,
44
+ "CLAWMEM_REPO": repo,
45
+ "CLAWMEM_TOKEN": token,
46
+ }
47
+
48
+ for key, value in pairs.items():
49
+ print(f"export {key}={shlex.quote(value)}")
50
+ return 0
51
+
52
+
53
+ if __name__ == "__main__":
54
+ raise SystemExit(main())
@@ -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[] = ["source:openclaw"];
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
- repo: agent.repo?.trim() || undefined,
57
- token: agent.token?.trim() || undefined,
58
- authScheme: agent.authScheme === "bearer" ? "bearer" : config.authScheme,
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.repo && route.token);
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
+ }
@@ -1,11 +1,67 @@
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 PermissionMap = Record<string, boolean | undefined>;
10
+ type RepoResponse = {
11
+ name?: string;
12
+ full_name?: string;
13
+ description?: string;
14
+ private?: boolean;
15
+ owner?: { login?: string };
16
+ permissions?: PermissionMap;
17
+ role_name?: string;
18
+ };
19
+ type OrgResponse = {
20
+ id?: number;
21
+ login?: string;
22
+ name?: string;
23
+ description?: string;
24
+ default_repository_permission?: string;
25
+ };
26
+ type TeamResponse = {
27
+ id?: number;
28
+ slug?: string;
29
+ name?: string;
30
+ description?: string;
31
+ privacy?: string;
32
+ permission?: string;
33
+ role_name?: string;
34
+ permissions?: PermissionMap;
35
+ };
36
+ type CollaboratorResponse = {
37
+ id?: number;
38
+ login?: string;
39
+ name?: string;
40
+ permissions?: PermissionMap;
41
+ role_name?: string;
42
+ };
43
+ type RepositoryInvitationResponse = {
44
+ id?: number;
45
+ created_at?: string;
46
+ permissions?: string;
47
+ repository?: RepoResponse;
48
+ invitee?: { login?: string; name?: string };
49
+ inviter?: { login?: string; name?: string };
50
+ };
51
+ type TeamMembershipResponse = { state?: string; role?: string };
52
+ type InvitationResponse = {
53
+ id?: number;
54
+ role?: string;
55
+ created_at?: string;
56
+ expires_at?: string | null;
57
+ email?: string;
58
+ login?: string;
59
+ organization?: OrgResponse;
60
+ invitee?: { login?: string };
61
+ inviter?: { login?: string };
62
+ team_ids?: number[];
63
+ teams?: TeamResponse[];
64
+ };
9
65
  type ReqOpts = { allowNotFound?: boolean; allowValidationError?: boolean; omitAuth?: boolean };
10
66
 
11
67
  export class GitHubIssueClient {
@@ -14,6 +70,9 @@ export class GitHubIssueClient {
14
70
  repo(): string | undefined {
15
71
  return this.config.repo?.trim() || undefined;
16
72
  }
73
+ defaultRepo(): string | undefined {
74
+ return this.config.defaultRepo?.trim() || undefined;
75
+ }
17
76
 
18
77
  async createIssue(params: { title: string; body: string; labels: string[] }): Promise<IssueResponse> {
19
78
  return this.req<IssueResponse>(this.repoPath("issues"), { method: "POST", body: JSON.stringify(params) });
@@ -53,6 +112,144 @@ export class GitHubIssueClient {
53
112
  q.set("per_page", String(params?.perPage ?? 100));
54
113
  return this.req<LabelResponse[]>(`${this.repoPath("labels")}?${q}`, { method: "GET" });
55
114
  }
115
+ async listUserRepos(): Promise<RepoResponse[]> {
116
+ return this.req<RepoResponse[]>("user/repos", { method: "GET" });
117
+ }
118
+ async createUserRepo(params: { name: string; description?: string; private?: boolean; autoInit?: boolean }): Promise<RepoResponse> {
119
+ return this.req<RepoResponse>("user/repos", {
120
+ method: "POST",
121
+ body: JSON.stringify({
122
+ name: params.name,
123
+ ...(params.description ? { description: params.description } : {}),
124
+ private: params.private ?? true,
125
+ auto_init: params.autoInit ?? false,
126
+ }),
127
+ });
128
+ }
129
+ async listUserOrgs(): Promise<OrgResponse[]> {
130
+ return this.req<OrgResponse[]>("user/orgs", { method: "GET" });
131
+ }
132
+ async createUserOrg(params: { login: string; name?: string; defaultRepositoryPermission?: string }): Promise<OrgResponse> {
133
+ return this.req<OrgResponse>("user/orgs", {
134
+ method: "POST",
135
+ body: JSON.stringify({
136
+ login: params.login,
137
+ ...(params.name ? { name: params.name } : {}),
138
+ ...(params.defaultRepositoryPermission ? { default_repository_permission: params.defaultRepositoryPermission } : {}),
139
+ }),
140
+ });
141
+ }
142
+ async getOrg(org: string): Promise<OrgResponse> {
143
+ return this.req<OrgResponse>(`orgs/${encodeURIComponent(org)}`, { method: "GET" });
144
+ }
145
+ async listOrgTeams(org: string): Promise<TeamResponse[]> {
146
+ return this.req<TeamResponse[]>(`orgs/${encodeURIComponent(org)}/teams`, { method: "GET" });
147
+ }
148
+ async createOrgTeam(org: string, params: { name: string; description?: string; privacy?: "closed" | "secret" }): Promise<TeamResponse> {
149
+ return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams`, {
150
+ method: "POST",
151
+ body: JSON.stringify({
152
+ name: params.name,
153
+ ...(params.description ? { description: params.description } : {}),
154
+ privacy: params.privacy ?? "closed",
155
+ }),
156
+ });
157
+ }
158
+ async setTeamMembership(org: string, teamSlug: string, username: string, role: "member" | "maintainer"): Promise<TeamMembershipResponse> {
159
+ return this.req<TeamMembershipResponse>(
160
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`,
161
+ { method: "PUT", body: JSON.stringify({ role }) },
162
+ );
163
+ }
164
+ async removeTeamMembership(org: string, teamSlug: string, username: string): Promise<void> {
165
+ await this.req(
166
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`,
167
+ { method: "DELETE" },
168
+ );
169
+ }
170
+ async listTeamRepos(org: string, teamSlug: string): Promise<RepoResponse[]> {
171
+ return this.req<RepoResponse[]>(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos`, { method: "GET" });
172
+ }
173
+ async setTeamRepoAccess(org: string, teamSlug: string, owner: string, repo: string, permission: "read" | "write" | "admin"): Promise<void> {
174
+ await this.req(
175
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
176
+ { method: "PUT", body: JSON.stringify({ permission }) },
177
+ );
178
+ }
179
+ async removeTeamRepoAccess(org: string, teamSlug: string, owner: string, repo: string): Promise<void> {
180
+ await this.req(
181
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
182
+ { method: "DELETE" },
183
+ );
184
+ }
185
+ async listRepoCollaborators(owner: string, repo: string): Promise<CollaboratorResponse[]> {
186
+ return this.req<CollaboratorResponse[]>(
187
+ `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators`,
188
+ { method: "GET" },
189
+ );
190
+ }
191
+ async listRepoInvitations(owner: string, repo: string): Promise<RepositoryInvitationResponse[]> {
192
+ return this.req<RepositoryInvitationResponse[]>(
193
+ `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/invitations`,
194
+ { method: "GET" },
195
+ );
196
+ }
197
+ async setRepoCollaborator(owner: string, repo: string, username: string, permission: "read" | "write" | "admin"): Promise<RepositoryInvitationResponse | undefined> {
198
+ return this.req<RepositoryInvitationResponse>(
199
+ `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
200
+ { method: "PUT", body: JSON.stringify({ permission }) },
201
+ );
202
+ }
203
+ async removeRepoCollaborator(owner: string, repo: string, username: string): Promise<void> {
204
+ await this.req(
205
+ `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
206
+ { method: "DELETE" },
207
+ );
208
+ }
209
+ async getRepo(owner: string, repo: string): Promise<RepoResponse> {
210
+ return this.req<RepoResponse>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "GET" });
211
+ }
212
+ async listRepoTeams(owner: string, repo: string): Promise<TeamResponse[]> {
213
+ return this.req<TeamResponse[]>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/teams`, { method: "GET" });
214
+ }
215
+ async listUserRepoInvitations(): Promise<RepositoryInvitationResponse[]> {
216
+ return this.req<RepositoryInvitationResponse[]>("user/repository_invitations", { method: "GET" });
217
+ }
218
+ async acceptUserRepoInvitation(invitationId: number): Promise<void> {
219
+ await this.req(`user/repository_invitations/${invitationId}`, { method: "PATCH" });
220
+ }
221
+ async declineUserRepoInvitation(invitationId: number): Promise<void> {
222
+ await this.req(`user/repository_invitations/${invitationId}`, { method: "DELETE" });
223
+ }
224
+ async listOrgInvitations(org: string): Promise<InvitationResponse[]> {
225
+ return this.req<InvitationResponse[]>(`orgs/${encodeURIComponent(org)}/invitations`, { method: "GET" });
226
+ }
227
+ async createOrgInvitation(
228
+ org: string,
229
+ params: { inviteeLogin: string; role?: "member" | "admin"; teamIds?: number[]; expiresInDays?: number },
230
+ ): Promise<InvitationResponse> {
231
+ return this.req<InvitationResponse>(`orgs/${encodeURIComponent(org)}/invitations`, {
232
+ method: "POST",
233
+ body: JSON.stringify({
234
+ invitee_login: params.inviteeLogin,
235
+ role: params.role ?? "member",
236
+ ...(params.teamIds && params.teamIds.length > 0 ? { team_ids: params.teamIds } : {}),
237
+ ...(typeof params.expiresInDays === "number" ? { expires_in_days: params.expiresInDays } : {}),
238
+ }),
239
+ });
240
+ }
241
+ async listOrgOutsideCollaborators(org: string): Promise<CollaboratorResponse[]> {
242
+ return this.req<CollaboratorResponse[]>(`orgs/${encodeURIComponent(org)}/outside_collaborators`, { method: "GET" });
243
+ }
244
+ async listUserOrgInvitations(): Promise<InvitationResponse[]> {
245
+ return this.req<InvitationResponse[]>("user/organization_invitations", { method: "GET" });
246
+ }
247
+ async acceptUserOrgInvitation(invitationId: number): Promise<void> {
248
+ await this.req(`user/organization_invitations/${invitationId}`, { method: "PATCH" });
249
+ }
250
+ async declineUserOrgInvitation(invitationId: number): Promise<void> {
251
+ await this.req(`user/organization_invitations/${invitationId}`, { method: "DELETE" });
252
+ }
56
253
  async ensureLabels(labels: string[]): Promise<void> {
57
254
  for (const label of labels) {
58
255
  if (!label.trim()) continue;
@@ -71,6 +268,15 @@ export class GitHubIssueClient {
71
268
  async updateRepoDescription(description: string): Promise<void> {
72
269
  await this.req(this.repoPath("").replace(/\/$/, ""), { method: "PATCH", body: JSON.stringify({ description }) });
73
270
  }
271
+ async registerAgent(prefixLogin: string, defaultRepoName: string): Promise<AgentRegistrationResponse> {
272
+ return this.req<AgentRegistrationResponse>("agents", {
273
+ method: "POST",
274
+ body: JSON.stringify({
275
+ prefix_login: prefixLogin,
276
+ default_repo_name: defaultRepoName,
277
+ }),
278
+ }, { omitAuth: true });
279
+ }
74
280
  async createAnonymousSession(locale?: string): Promise<AnonymousSessionResponse> {
75
281
  const body = locale ? JSON.stringify({ locale }) : undefined;
76
282
  return this.req<AnonymousSessionResponse>("anonymous/session", { method: "POST", ...(body ? { body } : {}) }, { omitAuth: true });
@@ -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"] }, "manual");
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, sessionId = "manual"): Promise<{ created: boolean; memory: ParsedMemoryIssue }> {
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(sessionId, normalized.kind, normalized.topics);
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(current.sessionId, nextKind, nextTopics);
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.sessionId, mem.kind, mem.topics));
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(session.sessionId, decision);
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 sessionId = labelVal(labels, "session:"), kind = labelVal(labels, "kind:");
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(sessionId: string, decision: MemoryDecision): Promise<{ savedCount: number; staledCount: number }> {
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(sessionId, draft.kind, draft.topics);
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.sessionId, mem.kind, mem.topics));
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(memory.sessionId, nextKind, nextTopics);
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(sessionId: string, kind?: string, topics?: string[]): string[] {
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
  ];