@clawmem-ai/clawmem 0.1.5 → 0.1.6

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
@@ -53,7 +53,7 @@ Before the workflow can publish successfully, configure the package on npmjs.com
53
53
  Release flow:
54
54
 
55
55
  1. Bump `package.json` to the version you want to ship.
56
- 2. Create and push a matching tag such as `0.1.5`.
56
+ 2. Create and push a matching tag such as `0.1.6`.
57
57
  3. GitHub Actions runs `.github/workflows/release.yml` and publishes with OIDC. No long-lived `NPM_TOKEN` secret is required.
58
58
 
59
59
  The workflow intentionally publishes from a tag push instead of `workflow_dispatch`, because npm validates the workflow filename exactly when using trusted publishing.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "clawmem",
3
3
  "name": "ClawMem",
4
- "version": "0.1.5",
4
+ "version": "0.1.6",
5
5
  "description": "Mirror OpenClaw sessions into GitHub-compatible issues and comments.",
6
6
  "kind": "memory",
7
7
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawmem-ai/clawmem",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Mirror OpenClaw sessions into GitHub-compatible issues and comments.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
5
- import { AGENT_LABEL_PREFIX, DEFAULT_LABELS, LABEL_ACTIVE, LABEL_CLOSED, SESSION_TITLE_PREFIX } from "./config.js";
5
+ import { AGENT_LABEL_PREFIX, DEFAULT_LABELS, LABEL_ACTIVE, LABEL_CLOSED, SESSION_TITLE_PREFIX, extractLabelNames } from "./config.js";
6
6
  import type { GitHubIssueClient } from "./github-client.js";
7
7
  import { normalizeMessages, readTranscriptSnapshot } from "./transcript.js";
8
8
  import type { ClawMemPluginConfig, NormalizedMessage, SessionMirrorState, TranscriptSnapshot } from "./types.js";
@@ -36,7 +36,17 @@ export class ConversationMirror {
36
36
  }
37
37
 
38
38
  async ensureIssue(session: SessionMirrorState, snapshot: TranscriptSnapshot): Promise<void> {
39
- if (session.issueNumber) return;
39
+ if (session.issueNumber) {
40
+ const existing = await this.lookupBoundIssue(session);
41
+ if (existing && this.isBoundIssue(session, existing)) {
42
+ session.issueTitle = existing.title?.trim() || session.issueTitle;
43
+ return;
44
+ }
45
+ this.api.logger.warn(
46
+ `clawmem: issue binding for ${session.sessionId} is stale or mismatched (${session.issueNumber}); recreating`,
47
+ );
48
+ this.resetIssueBinding(session);
49
+ }
40
50
  const title = `${SESSION_TITLE_PREFIX}${session.sessionId}`;
41
51
  const labels = this.buildLabels(session, snapshot, false);
42
52
  const body = this.renderBody(session, snapshot, "pending", false);
@@ -143,9 +153,37 @@ export class ConversationMirror {
143
153
  } catch { /* directory unreadable */ }
144
154
  return null;
145
155
  }
156
+
157
+ private async lookupBoundIssue(session: SessionMirrorState): Promise<{ number: number; title?: string; labels?: Array<{ name?: string } | string> } | null> {
158
+ if (!session.issueNumber) return null;
159
+ try {
160
+ return await this.client.getIssue(session.issueNumber);
161
+ } catch (error) {
162
+ if (isNotFoundError(error)) return null;
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ private isBoundIssue(session: SessionMirrorState, issue: { title?: string; labels?: Array<{ name?: string } | string> }): boolean {
168
+ const labels = extractLabelNames(issue.labels);
169
+ return labels.includes("type:conversation") && labels.includes(`session:${session.sessionId}`);
170
+ }
171
+
172
+ private resetIssueBinding(session: SessionMirrorState): void {
173
+ session.issueNumber = undefined;
174
+ session.issueTitle = undefined;
175
+ session.lastSummaryHash = undefined;
176
+ session.lastMirroredCount = 0;
177
+ session.turnCount = 0;
178
+ session.finalizedAt = undefined;
179
+ }
146
180
  }
147
181
 
148
182
  async function fexists(p: string): Promise<boolean> { try { return (await fs.promises.stat(p)).isFile(); } catch { return false; } }
183
+ function isNotFoundError(error: unknown): boolean {
184
+ const text = String(error);
185
+ return text.includes("HTTP 404");
186
+ }
149
187
  function parseSummary(raw: string): string {
150
188
  const tryParse = (s: string): string | null => {
151
189
  try { const p = JSON.parse(s) as { summary?: unknown }; return typeof p?.summary === "string" && p.summary.trim() ? p.summary.trim() : null; }
package/src/service.ts CHANGED
@@ -146,7 +146,14 @@ class ClawMemService {
146
146
  return this.queue.enqueue(sessionId, async () => { await this.ensureLoaded(); return task(); });
147
147
  }
148
148
  private track<T>(promise: Promise<T>): Promise<T> {
149
- this.pending.add(promise); void promise.finally(() => this.pending.delete(promise)); return promise;
149
+ this.pending.add(promise);
150
+ // Avoid creating a second rejecting promise via finally(); OpenClaw treats
151
+ // unhandled rejections as fatal and exits the gateway process.
152
+ void promise.then(
153
+ () => this.pending.delete(promise),
154
+ () => this.pending.delete(promise),
155
+ );
156
+ return promise;
150
157
  }
151
158
  private getOrCreate(sessionId: string): SessionMirrorState {
152
159
  if (this.state.sessions[sessionId]) return this.state.sessions[sessionId];