@clawmem-ai/clawmem 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,9 @@
4
4
 
5
5
  **What it does:**
6
6
  - Creates one `type:conversation` issue per session, mirrors the full transcript as comments.
7
- - On session end: auto-extracts durable memories and stores each as a `type:memory` issue.
7
+ - During request-scoped hooks: best-effort extracts durable memories and stores each as a `type:memory` issue.
8
8
  - On session start: searches active memories by relevance and injects them into context.
9
+ - Lets agents inspect memory indexes and schema, fetch exact memories, update canonical facts in place, and write structured memories with `kind:*` and `topic:*` labels through plugin tools.
9
10
 
10
11
  ---
11
12
 
@@ -33,10 +34,13 @@ Finally: your `IDENTITY.md` is not something you fill out on day one. It fills i
33
34
 
34
35
  ```bash
35
36
  openclaw plugins install @clawmem-ai/clawmem
37
+ openclaw plugins enable clawmem
38
+ openclaw config set plugins.slots.memory clawmem
39
+ openclaw config validate
36
40
  openclaw gateway restart
37
41
  ```
38
42
 
39
- After restart, clawmem provisions per-agent memory repos on `git.clawmem.ai` as each agent is first used, then writes that agent's `token` + `repo` back into your config under `plugins.entries.clawmem.config.agents.<agentId>`. Memories start accumulating from that agent's next session.
43
+ After restart, confirm OpenClaw shows ClawMem as the active memory plugin. On first use, clawmem bootstraps each agent identity by calling `POST /api/v3/agents` on `git.clawmem.ai`, then writes the returned `token` plus `repo_full_name` back into your config under `plugins.entries.clawmem.config.agents.<agentId>` as that agent's `defaultRepo`. Automatic flows use that `defaultRepo`, while explicit memory tool calls may target other repos. When talking to an older backend that does not expose `POST /api/v3/agents`, the plugin falls back to the deprecated anonymous bootstrap path.
40
44
 
41
45
  ---
42
46
 
@@ -75,7 +79,7 @@ Without it, every session starts from zero. With it, what you learn persists. On
75
79
  After installing clawmem, you have two memory systems running in parallel:
76
80
 
77
81
  1. **Workspace files** (`MEMORY.md`, `SOUL.md`, etc.) — always injected into every session via system prompt
78
- 2. **clawmem** — auto-extracts memories at session end; selectively injects by relevance at session start
82
+ 2. **clawmem** — auto-extracts memories during request-scoped maintenance; selectively injects by relevance at session start
79
83
 
80
84
  These two systems do not sync. Without explicit rules, the agent defaults to the familiar path (workspace files), and the two systems start to contradict each other.
81
85
 
@@ -113,7 +117,7 @@ clawmem is my brain. One system per job — no exceptions:
113
117
  | What | Where |
114
118
  |---|---|
115
119
  | Credentials / API keys | `knowledge/MEMORY.md` (always injected) |
116
- | Everything I learn, conclude, or figure out | clawmem only — auto-captured at session end |
120
+ | Everything I learn, conclude, or figure out | clawmem only — auto-captured during request-scoped maintenance |
117
121
  | Files | Only when a tool or another human needs to read them |
118
122
  | Workspace versioning | clawmem git service (not github.com) |
119
123
 
@@ -144,7 +148,7 @@ Strip everything except credentials. Add a header comment:
144
148
 
145
149
  ## Locking Key Insights Manually
146
150
 
147
- clawmem auto-extracts memories at session end — but important insights deserve to be locked immediately, not left to chance extraction.
151
+ clawmem auto-extracts memories during later request-scoped maintenance — but important insights deserve to be locked immediately, not left to chance extraction.
148
152
 
149
153
  After any significant realization, create a memory issue directly:
150
154
 
@@ -158,7 +162,7 @@ curl -X POST "https://git.clawmem.ai/api/v3/repos/$CLAWMEM_REPO/issues" \
158
162
  -d '{
159
163
  "title": "Memory: <concise title>",
160
164
  "body": "<the insight, in plain language>",
161
- "labels": ["type:memory", "memory-status:active"]
165
+ "labels": ["type:memory"]
162
166
  }'
163
167
  ```
164
168
 
@@ -189,7 +193,7 @@ GH_HOST=git.clawmem.ai GH_TOKEN=$CLAWMEM_TOKEN \
189
193
  gh issue create --repo <owner/team-memory> \
190
194
  --title "Memory: ..." \
191
195
  --body "..." \
192
- --label "type:memory,memory-status:active,source:team"
196
+ --label "type:memory,source:team"
193
197
  ```
194
198
 
195
199
  **Read team memories:**
@@ -197,7 +201,8 @@ GH_HOST=git.clawmem.ai GH_TOKEN=$CLAWMEM_TOKEN \
197
201
  ```bash
198
202
  GH_HOST=git.clawmem.ai GH_TOKEN=$CLAWMEM_TOKEN \
199
203
  gh issue list --repo <owner/team-memory> \
200
- --label "memory-status:active" \
204
+ --state open \
205
+ --label "type:memory" \
201
206
  --json number,title,body
202
207
  ```
203
208
 
@@ -269,25 +274,15 @@ Full config with all options:
269
274
  agents: {
270
275
  main: {
271
276
  baseUrl: "https://git.clawmem.ai/api/v3",
272
- repo: "owner/main-memory",
277
+ defaultRepo: "owner/main-memory",
273
278
  token: "<token>",
274
279
  authScheme: "token"
275
280
  },
276
281
  coder: {
277
- repo: "owner/coder-memory",
282
+ defaultRepo: "owner/coder-memory",
278
283
  token: "<token>"
279
284
  }
280
285
  },
281
- issueTitlePrefix: "Session: ",
282
- memoryTitlePrefix: "Memory: ",
283
- defaultLabels: ["source:openclaw"],
284
- agentLabelPrefix: "agent:",
285
- activeStatusLabel: "status:active",
286
- closedStatusLabel: "status:closed",
287
- memoryActiveStatusLabel: "memory-status:active",
288
- memoryStaleStatusLabel: "memory-status:stale",
289
- autoCreateLabels: true,
290
- closeIssueOnReset: true,
291
286
  turnCommentDelayMs: 1000,
292
287
  summaryWaitTimeoutMs: 120000,
293
288
  memoryRecallLimit: 5
@@ -304,6 +299,13 @@ Full config with all options:
304
299
 
305
300
  - Conversation comments exclude tool calls, tool results, system messages, and heartbeat noise.
306
301
  - Summary failures do not block finalization; the `summary` field is written as `failed: ...`.
307
- - Memory search and auto-injection only return `memory-status:active` issues.
308
- - Durable memories are auto-captured on session finalize no memory tools are injected into the agent tool list.
309
- - Memory issue bodies store only the detail text; metadata comes from labels and issue number.
302
+ - Memory search and auto-injection only return open `type:memory` issues. Closed memory issues are treated as stale.
303
+ - `memory_recall` now prefers the backend `/api/v3/search/issues` endpoint scoped to the current repo plus `label:"type:memory"`; if backend search fails, clawmem falls back to local lexical ranking.
304
+ - Durable memories are extracted best-effort during later request-scoped maintenance, not by background subagent work after a request has already ended.
305
+ - The plugin exposes `memory_repos`, `memory_repo_create`, `memory_list`, `memory_get`, `memory_labels`, `memory_recall`, `memory_store`, `memory_update`, and `memory_forget` for mid-session use.
306
+ - Route resolution is now: agent identity supplies credentials, `defaultRepo` is the fallback memory space, and explicit tool calls may override repo per operation.
307
+ - `memory_store` accepts optional schema hints such as kind and topics; the plugin normalizes them into managed `kind:*` and `topic:*` labels.
308
+ - Memory issues no longer use `session:*` labels. Session linkage remains a conversation concern, not part of the durable memory schema.
309
+ - `memory_update` updates one existing memory issue in place; use it for evolving canonical facts or active tasks instead of creating a duplicate node.
310
+ - Conversation lifecycle is stored in native issue state (`open` while live, `closed` after finalize); memory lifecycle uses native issue state too (`open` active, `closed` stale).
311
+ - Memory issue bodies store the durable detail plus flat metadata such as `memory_hash` and logical `date`; labels are reserved for schema and routing.
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "id": "clawmem",
3
3
  "name": "ClawMem",
4
- "version": "0.1.8",
4
+ "version": "0.1.10",
5
5
  "description": "Mirror OpenClaw sessions into GitHub-compatible issues and comments.",
6
6
  "kind": "memory",
7
7
  "configSchema": {
8
8
  "type": "object",
9
- "additionalProperties": false,
9
+ "additionalProperties": true,
10
10
  "properties": {
11
11
  "baseUrl": {
12
12
  "type": "string",
13
13
  "minLength": 1,
14
14
  "default": "https://git.clawmem.ai"
15
15
  },
16
+ "defaultRepo": {
17
+ "type": "string",
18
+ "pattern": "^[^/]+/[^/]+$"
19
+ },
16
20
  "repo": {
17
21
  "type": "string",
18
22
  "pattern": "^[^/]+/[^/]+$"
@@ -36,6 +40,10 @@
36
40
  "type": "string",
37
41
  "minLength": 1
38
42
  },
43
+ "defaultRepo": {
44
+ "type": "string",
45
+ "pattern": "^[^/]+/[^/]+$"
46
+ },
39
47
  "repo": {
40
48
  "type": "string",
41
49
  "pattern": "^[^/]+/[^/]+$"
@@ -51,40 +59,6 @@
51
59
  }
52
60
  }
53
61
  },
54
- "issueTitlePrefix": {
55
- "type": "string"
56
- },
57
- "memoryTitlePrefix": {
58
- "type": "string"
59
- },
60
- "defaultLabels": {
61
- "type": "array",
62
- "items": {
63
- "type": "string"
64
- },
65
- "default": ["source:openclaw"]
66
- },
67
- "agentLabelPrefix": {
68
- "type": "string"
69
- },
70
- "activeStatusLabel": {
71
- "type": "string"
72
- },
73
- "closedStatusLabel": {
74
- "type": "string"
75
- },
76
- "memoryActiveStatusLabel": {
77
- "type": "string"
78
- },
79
- "memoryStaleStatusLabel": {
80
- "type": "string"
81
- },
82
- "autoCreateLabels": {
83
- "type": "boolean"
84
- },
85
- "closeIssueOnReset": {
86
- "type": "boolean"
87
- },
88
62
  "turnCommentDelayMs": {
89
63
  "type": "integer",
90
64
  "minimum": 0,
@@ -99,15 +73,6 @@
99
73
  "type": "integer",
100
74
  "minimum": 1,
101
75
  "maximum": 20
102
- },
103
- "labelColor": {
104
- "type": "string",
105
- "pattern": "^#?[0-9a-fA-F]{6}$"
106
- },
107
- "maxExcerptChars": {
108
- "type": "integer",
109
- "minimum": 120,
110
- "maximum": 4000
111
76
  }
112
77
  }
113
78
  },
@@ -117,10 +82,15 @@
117
82
  "placeholder": "https://git.clawmem.ai",
118
83
  "help": "GitHub-compatible API base URL. Root URLs are normalized to /api/v3 automatically."
119
84
  },
85
+ "defaultRepo": {
86
+ "label": "Default Repo",
87
+ "placeholder": "owner/repo",
88
+ "help": "Default memory repo for automatic flows and for tool calls that do not specify repo explicitly."
89
+ },
120
90
  "repo": {
121
91
  "label": "Repository",
122
92
  "placeholder": "owner/repo",
123
- "help": "Legacy single-route setting. New installs should use per-agent routes under agents.<agentId>."
93
+ "help": "Legacy alias for defaultRepo. New installs should use defaultRepo or per-agent defaultRepo under agents.<agentId>."
124
94
  },
125
95
  "token": {
126
96
  "label": "API Token",
@@ -133,37 +103,7 @@
133
103
  },
134
104
  "agents": {
135
105
  "label": "Agent Routes",
136
- "help": "Per-agent ClawMem credentials keyed by agent id. Missing repo/token are provisioned automatically on first use."
137
- },
138
- "defaultLabels": {
139
- "label": "Default Labels",
140
- "help": "Labels added to every new session issue. Good place for topic:* tags."
141
- },
142
- "memoryTitlePrefix": {
143
- "label": "Memory Title Prefix",
144
- "help": "Prefix used when creating memory issue titles."
145
- },
146
- "agentLabelPrefix": {
147
- "label": "Agent Label Prefix",
148
- "help": "Dynamic agent label prefix, for example agent:coder."
149
- },
150
- "activeStatusLabel": {
151
- "label": "Active Status Label"
152
- },
153
- "closedStatusLabel": {
154
- "label": "Closed Status Label"
155
- },
156
- "memoryActiveStatusLabel": {
157
- "label": "Memory Active Status Label"
158
- },
159
- "memoryStaleStatusLabel": {
160
- "label": "Memory Stale Status Label"
161
- },
162
- "autoCreateLabels": {
163
- "label": "Auto Create Labels"
164
- },
165
- "closeIssueOnReset": {
166
- "label": "Close On Reset"
106
+ "help": "Per-agent ClawMem identities keyed by agent id. Each identity has credentials plus an optional defaultRepo."
167
107
  },
168
108
  "turnCommentDelayMs": {
169
109
  "label": "Turn Sync Delay (ms)",
@@ -176,14 +116,6 @@
176
116
  "memoryRecallLimit": {
177
117
  "label": "Memory Recall Limit",
178
118
  "help": "Maximum number of active memories injected into context before an agent starts."
179
- },
180
- "labelColor": {
181
- "label": "Fallback Label Color",
182
- "placeholder": "0e8a16"
183
- },
184
- "maxExcerptChars": {
185
- "label": "Max Excerpt Chars",
186
- "help": "Soft cap used when rendering summaries and comment sections."
187
119
  }
188
120
  }
189
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawmem-ai/clawmem",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Mirror OpenClaw sessions into GitHub-compatible issues and comments.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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,14 +5,14 @@ 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";
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:", "session:", "date:", "topic:", "agent:", "source:"];
15
+ const MANAGED_PREFIXES = ["type:", "kind:", "session:", "date:", "topic:", "agent:", "source:"];
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 {
@@ -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,27 +50,35 @@ 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 {
67
78
  if (label.startsWith("status:")) return "b60205";
68
79
  if (label.startsWith("memory-status:")) return label.endsWith(":stale") ? "d93f0b" : "0e8a16";
69
80
  if (label.startsWith("type:")) return label === "type:memory" ? "5319e7" : "1d76db";
81
+ if (label.startsWith("kind:")) return "5319e7";
70
82
  if (label.startsWith("date:")) return "c5def5";
71
83
  if (label.startsWith("topic:")) return "fbca04";
72
84
  if (label.startsWith("session:")) return "bfdadc";
@@ -76,7 +88,7 @@ export function resolveLabelColor(label: string): string {
76
88
  }
77
89
 
78
90
  export function labelDescription(label: string): string {
79
- for (const [pfx, d] of [["type:", "Issue type"], ["memory-status:", "Memory lifecycle status"],
91
+ for (const [pfx, d] of [["type:", "Issue type"], ["kind:", "Memory kind"], ["memory-status:", "Memory lifecycle status"],
80
92
  ["status:", "Conversation lifecycle status"], ["session:", "Session association"],
81
93
  ["date:", "Date"], ["topic:", "Topic"], ["agent:", "Agent"], ["source:", "Source"]] as const)
82
94
  if (label.startsWith(pfx)) return `${d} label managed by clawmem.`;
@@ -96,3 +108,9 @@ export function labelVal(labels: string[], prefix: string): string | undefined {
96
108
  const m = labels.find((l) => l.startsWith(prefix));
97
109
  return m ? m.slice(prefix.length).trim() || undefined : undefined;
98
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
+ }
@@ -6,54 +6,48 @@ function msg(role: string, text: string): NormalizedMessage {
6
6
  return { role, text };
7
7
  }
8
8
 
9
- const tests: Array<{ name: string; messages: NormalizedMessage[]; sessionId: string; expected: string | RegExp }> = [
9
+ const tests: Array<{ name: string; messages: NormalizedMessage[]; sessionId: string; expected: string }> = [
10
10
  {
11
- name: "uses first user message",
11
+ name: "returns placeholder regardless of user message content",
12
12
  messages: [msg("user", "How do I configure Redis rate limiting?")],
13
13
  sessionId: "abc123",
14
- expected: "How do I configure Redis rate limiting?",
14
+ expected: "Session: abc123",
15
15
  },
16
16
  {
17
- name: "truncates long messages to 50 chars",
17
+ name: "returns placeholder for long messages",
18
18
  messages: [msg("user", "I need help with configuring the distributed rate limiting system for our production Redis cluster")],
19
19
  sessionId: "abc123",
20
- expected: /^I need help with configuring the distributed rate…$/,
20
+ expected: "Session: abc123",
21
21
  },
22
22
  {
23
- name: "strips markdown formatting",
23
+ name: "returns placeholder for messages with markdown",
24
24
  messages: [msg("user", "## How do I **configure** `Redis` rate limiting?")],
25
25
  sessionId: "abc123",
26
- expected: "How do I configure Redis rate limiting?",
26
+ expected: "Session: abc123",
27
27
  },
28
28
  {
29
- name: "falls back to session ID for short messages",
29
+ name: "returns placeholder for short messages",
30
30
  messages: [msg("user", "hi")],
31
31
  sessionId: "abc-def-123",
32
32
  expected: "Session: abc-def-123",
33
33
  },
34
34
  {
35
- name: "falls back to session ID when no user messages",
35
+ name: "returns placeholder when no user messages",
36
36
  messages: [msg("assistant", "Hello!")],
37
37
  sessionId: "xyz-789",
38
38
  expected: "Session: xyz-789",
39
39
  },
40
40
  {
41
- name: "falls back to session ID for empty messages",
41
+ name: "returns placeholder for empty messages",
42
42
  messages: [],
43
43
  sessionId: "empty-sess",
44
44
  expected: "Session: empty-sess",
45
45
  },
46
46
  {
47
- name: "collapses whitespace",
48
- messages: [msg("user", "How do I configure\n\nRedis?")],
49
- sessionId: "abc",
50
- expected: "How do I configure Redis?",
51
- },
52
- {
53
- name: "skips assistant messages, uses first user message",
47
+ name: "returns placeholder with session ID for any input",
54
48
  messages: [msg("assistant", "Welcome!"), msg("user", "Fix the login bug please")],
55
49
  sessionId: "abc",
56
- expected: "Fix the login bug please",
50
+ expected: "Session: abc",
57
51
  },
58
52
  ];
59
53
 
@@ -62,9 +56,9 @@ let failed = 0;
62
56
 
63
57
  for (const t of tests) {
64
58
  const got = deriveInitialTitle(t.messages, t.sessionId);
65
- const ok = t.expected instanceof RegExp ? t.expected.test(got) : got === t.expected;
59
+ const ok = got === t.expected;
66
60
  if (!ok) {
67
- console.error(`FAIL: ${t.name}\n got: ${JSON.stringify(got)}\n expected: ${t.expected instanceof RegExp ? t.expected.toString() : JSON.stringify(t.expected)}`);
61
+ console.error(`FAIL: ${t.name}\n got: ${JSON.stringify(got)}\n expected: ${JSON.stringify(t.expected)}`);
68
62
  failed++;
69
63
  } else {
70
64
  console.log(`PASS: ${t.name}`);