@clawmem-ai/clawmem 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/openclaw.plugin.json +11 -11
- package/package.json +12 -2
- package/skills/clawmem/SKILL.md +5 -3
- package/skills/clawmem/references/collaboration.md +43 -1
- package/skills/clawmem/references/schema.md +2 -1
- package/src/config.test.ts +1 -1
- package/src/config.ts +1 -1
- package/src/conversation.test.ts +63 -13
- package/src/conversation.ts +100 -188
- package/src/github-client.test.ts +101 -0
- package/src/github-client.ts +59 -0
- package/src/memory.test.ts +154 -39
- package/src/memory.ts +139 -246
- package/src/runtime-env.ts +12 -0
- package/src/service.test.ts +118 -0
- package/src/service.ts +765 -200
- package/src/state.test.ts +119 -0
- package/src/state.ts +124 -25
- package/src/types.ts +33 -6
- package/src/utils.ts +19 -0
package/src/service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Thin orchestrator: wires conversation mirroring, memory store, and plugin lifecycle.
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { MemoryPluginCapability, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { hasDefaultRepo, isAgentConfigured, resolveAgentRoute, resolvePluginConfig } from "./config.js";
|
|
4
4
|
import { filterDirectCollaborators, listRepoAccessTeams, resolveOrgInvitationRole } from "./collaboration.js";
|
|
5
5
|
import { ConversationMirror } from "./conversation.js";
|
|
@@ -9,27 +9,28 @@ import { MemoryStore } from "./memory.js";
|
|
|
9
9
|
import { sanitizeRecallQueryInput } from "./recall-sanitize.js";
|
|
10
10
|
import { loadState, resolveStatePath, saveState } from "./state.js";
|
|
11
11
|
import { readTranscriptSnapshot } from "./transcript.js";
|
|
12
|
-
import type { BootstrapIdentityResponse, ClawMemPluginConfig, ClawMemResolvedRoute, PluginState, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
12
|
+
import type { BootstrapIdentityResponse, ClawMemPluginConfig, ClawMemResolvedRoute, MemoryCandidate, PluginState, SessionDerivedState, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
13
13
|
import { buildAgentBootstrapRegistration, inferAgentIdFromTranscriptPath, normalizeAgentId, sessionScopeKey } from "./utils.js";
|
|
14
|
+
import { getOpenClawAgentIdFromEnv, getOpenClawHostVersionFromEnv } from "./runtime-env.js";
|
|
14
15
|
|
|
15
16
|
type TurnPayload = { sessionId?: string; sessionKey?: string; agentId?: string; messages: unknown[] };
|
|
16
17
|
type FinalizePayload = { sessionId?: string; sessionKey?: string; sessionFile?: string; agentId?: string; reason?: string; messages?: unknown[] };
|
|
17
18
|
type CollaborationPermission = "read" | "write" | "admin";
|
|
18
19
|
type CollaborationTeamRole = "member" | "maintainer";
|
|
20
|
+
type MemoryPromptBuilder = NonNullable<MemoryPluginCapability["promptBuilder"]>;
|
|
21
|
+
type MemoryPromptBuilderParams = Parameters<MemoryPromptBuilder>[0];
|
|
19
22
|
|
|
20
|
-
const SESSION_MAINTENANCE_RETRY_DELAYS_MS = [5000, 30000, 120000] as const;
|
|
21
23
|
const MODERN_PROMPT_HOOK_MIN_HOST_VERSION = "2026.3.7";
|
|
22
24
|
type PromptHookMode = "modern" | "legacy";
|
|
23
25
|
|
|
24
26
|
class ClawMemService {
|
|
25
27
|
private readonly config: ClawMemPluginConfig;
|
|
26
|
-
private readonly
|
|
28
|
+
private readonly ioQueue = new KeyedAsyncQueue();
|
|
29
|
+
private readonly repoWriteQueue = new KeyedAsyncQueue();
|
|
27
30
|
private readonly stateQueue = new KeyedAsyncQueue();
|
|
28
31
|
private readonly pending = new Set<Promise<unknown>>();
|
|
29
|
-
private readonly syncTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
30
|
-
private readonly maintenanceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
31
32
|
private statePath = "";
|
|
32
|
-
private state: PluginState = { version:
|
|
33
|
+
private state: PluginState = { version: 4, sessions: {} };
|
|
33
34
|
private unsubTranscript?: () => void;
|
|
34
35
|
private loadPromise: Promise<void> | null = null;
|
|
35
36
|
private readonly configPromises = new Map<string, Promise<boolean>>();
|
|
@@ -40,14 +41,33 @@ class ClawMemService {
|
|
|
40
41
|
|
|
41
42
|
register(): void {
|
|
42
43
|
const promptHookMode = resolvePromptHookMode(this.api);
|
|
44
|
+
this.registerMemoryPromptGuidance();
|
|
43
45
|
if (promptHookMode === "modern") {
|
|
44
46
|
this.api.on("before_prompt_build", async (ev, ctx) => this.handleBeforePromptBuild(ev, ctx.agentId));
|
|
45
47
|
} else {
|
|
46
48
|
this.api.on("before_agent_start", async (ev, ctx) => this.handleBeforeAgentStart(ev, ctx.agentId));
|
|
47
49
|
}
|
|
48
|
-
this.api.on("agent_end", (ev, ctx) =>
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
this.api.on("agent_end", async (ev, ctx) => {
|
|
51
|
+
try {
|
|
52
|
+
await this.handleAgentEnd({ sessionId: ctx.sessionId, sessionKey: ctx.sessionKey, agentId: ctx.agentId, messages: ev.messages });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.warn("turn sync", error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
this.api.on("before_reset", async (ev, ctx) => {
|
|
58
|
+
try {
|
|
59
|
+
await this.handleFinalize({ sessionId: ctx.sessionId, sessionKey: ctx.sessionKey, sessionFile: ev.sessionFile, agentId: ctx.agentId, reason: ev.reason, messages: ev.messages });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.warn("finalize", error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
this.api.on("session_end", async (ev, ctx) => {
|
|
65
|
+
try {
|
|
66
|
+
await this.handleFinalize({ sessionId: ev.sessionId ?? ctx.sessionId, sessionKey: ev.sessionKey ?? ctx.sessionKey, agentId: ctx.agentId, reason: "session_end" });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.warn("finalize", error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
51
71
|
this.registerTools();
|
|
52
72
|
|
|
53
73
|
this.api.registerService({
|
|
@@ -59,9 +79,6 @@ class ClawMemService {
|
|
|
59
79
|
this.unsubTranscript = this.api.runtime.events.onSessionTranscriptUpdate((u) => {
|
|
60
80
|
void this.track(this.handleTranscript(u.sessionFile)).catch((e) => this.warn("transcript update", e));
|
|
61
81
|
});
|
|
62
|
-
for (const agentId of new Set(Object.values(this.state.sessions).map((session) => normalizeAgentId(session.agentId)))) {
|
|
63
|
-
this.scheduleRecentSessionMaintenance(agentId);
|
|
64
|
-
}
|
|
65
82
|
const configuredCount = Object.keys(this.config.agents).filter((agentId) => {
|
|
66
83
|
const route = resolveAgentRoute(this.config, agentId);
|
|
67
84
|
return isAgentConfigured(route) && hasDefaultRepo(route);
|
|
@@ -75,15 +92,44 @@ class ClawMemService {
|
|
|
75
92
|
},
|
|
76
93
|
stop: async () => {
|
|
77
94
|
this.unsubTranscript?.();
|
|
78
|
-
for (const t of this.syncTimers.values()) clearTimeout(t);
|
|
79
|
-
this.syncTimers.clear();
|
|
80
|
-
for (const t of this.maintenanceTimers.values()) clearTimeout(t);
|
|
81
|
-
this.maintenanceTimers.clear();
|
|
82
95
|
await Promise.allSettled([...this.pending]);
|
|
83
96
|
},
|
|
84
97
|
});
|
|
85
98
|
}
|
|
86
99
|
|
|
100
|
+
private registerMemoryPromptGuidance(): void {
|
|
101
|
+
if (!this.isSelectedMemoryPlugin()) return;
|
|
102
|
+
|
|
103
|
+
const api = this.api as OpenClawPluginApi & {
|
|
104
|
+
registerMemoryCapability?: OpenClawPluginApi["registerMemoryCapability"];
|
|
105
|
+
registerMemoryPromptSection?: OpenClawPluginApi["registerMemoryPromptSection"];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (typeof api.registerMemoryCapability === "function") {
|
|
109
|
+
api.registerMemoryCapability({ promptBuilder: buildClawMemPromptSection });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof api.registerMemoryPromptSection === "function") {
|
|
114
|
+
api.registerMemoryPromptSection(buildClawMemPromptSection);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.api.logger.warn?.("clawmem: host does not expose memory prompt registration; always-on prompt guidance is disabled");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private isSelectedMemoryPlugin(): boolean {
|
|
122
|
+
try {
|
|
123
|
+
const root = this.api.runtime.config.loadConfig();
|
|
124
|
+
const plugins = asRecord(root.plugins);
|
|
125
|
+
const slots = asRecord(plugins.slots);
|
|
126
|
+
const slot = typeof slots.memory === "string" ? String(slots.memory).trim() : "";
|
|
127
|
+
return slot === this.api.id;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
87
133
|
private registerTools(): void {
|
|
88
134
|
this.api.registerTool({
|
|
89
135
|
name: "memory_repos",
|
|
@@ -342,12 +388,12 @@ class ClawMemService {
|
|
|
342
388
|
if ("error" in resolved) return toolText(resolved.error);
|
|
343
389
|
const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
|
|
344
390
|
const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
|
|
345
|
-
const result = await resolved.mem.store({
|
|
391
|
+
const result = await this.enqueueRepoWrite(this.repoWriteKey(resolved.route), () => resolved.mem.store({
|
|
346
392
|
...(title ? { title } : {}),
|
|
347
393
|
detail,
|
|
348
394
|
...(kind ? { kind } : {}),
|
|
349
395
|
...(topics && topics.length > 0 ? { topics } : {}),
|
|
350
|
-
});
|
|
396
|
+
}));
|
|
351
397
|
if (!result.created) return toolText(`Memory already exists in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
|
|
352
398
|
return toolText(`Stored memory in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
|
|
353
399
|
},
|
|
@@ -391,7 +437,10 @@ class ClawMemService {
|
|
|
391
437
|
if ("error" in resolved) return toolText(resolved.error);
|
|
392
438
|
let updated;
|
|
393
439
|
try {
|
|
394
|
-
updated = await
|
|
440
|
+
updated = await this.enqueueRepoWrite(
|
|
441
|
+
this.repoWriteKey(resolved.route),
|
|
442
|
+
() => resolved.mem.update(memoryId, { ...(title ? { title } : {}), ...(detail ? { detail } : {}), ...(kind !== undefined ? { kind } : {}), ...(topics !== undefined ? { topics } : {}) }),
|
|
443
|
+
);
|
|
395
444
|
} catch (error) {
|
|
396
445
|
return toolText(`Unable to update memory "${memoryId}": ${String(error)}`);
|
|
397
446
|
}
|
|
@@ -421,11 +470,12 @@ class ClawMemService {
|
|
|
421
470
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
422
471
|
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
423
472
|
if ("error" in resolved) return toolText(resolved.error);
|
|
424
|
-
const forgotten = await resolved.mem.forget(memoryId);
|
|
473
|
+
const forgotten = await this.enqueueRepoWrite(this.repoWriteKey(resolved.route), () => resolved.mem.forget(memoryId));
|
|
425
474
|
if (!forgotten) return toolText(`No active memory matched id "${memoryId}" in ${resolved.route.repo}.`);
|
|
426
475
|
return toolText(`Marked memory [${forgotten.memoryId}] stale in ${resolved.route.repo}: ${forgotten.detail}`);
|
|
427
476
|
},
|
|
428
477
|
});
|
|
478
|
+
|
|
429
479
|
this.registerCollaborationTools();
|
|
430
480
|
}
|
|
431
481
|
|
|
@@ -503,6 +553,147 @@ class ClawMemService {
|
|
|
503
553
|
},
|
|
504
554
|
});
|
|
505
555
|
|
|
556
|
+
this.api.registerTool({
|
|
557
|
+
name: "collaboration_org_members",
|
|
558
|
+
description: "List visible members in an organization, optionally filtering to admins only.",
|
|
559
|
+
required: true,
|
|
560
|
+
parameters: {
|
|
561
|
+
type: "object",
|
|
562
|
+
additionalProperties: false,
|
|
563
|
+
properties: {
|
|
564
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
565
|
+
role: { type: "string", enum: ["admin"], description: "Optional role filter. Use admin to show org owners only." },
|
|
566
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
567
|
+
},
|
|
568
|
+
required: ["org"],
|
|
569
|
+
},
|
|
570
|
+
execute: async (_id: string, params: unknown) => {
|
|
571
|
+
const p = asRecord(params);
|
|
572
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
573
|
+
if (!org) return toolText("org is empty.");
|
|
574
|
+
const role = p.role === "admin" ? "admin" : undefined;
|
|
575
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
576
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
577
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
578
|
+
try {
|
|
579
|
+
const members = await resolved.client.listOrgMembers(org, role);
|
|
580
|
+
if (members.length === 0) {
|
|
581
|
+
return toolText(role === "admin"
|
|
582
|
+
? `No org admins are visible in "${org}".`
|
|
583
|
+
: `No org members are visible in "${org}".`);
|
|
584
|
+
}
|
|
585
|
+
return toolText([
|
|
586
|
+
role === "admin" ? `Org admins in "${org}":` : `Org members in "${org}":`,
|
|
587
|
+
...members.map((member) => `- ${renderCollaboratorLine(member)}`),
|
|
588
|
+
].join("\n"));
|
|
589
|
+
} catch (error) {
|
|
590
|
+
return toolText(`Unable to list members for org "${org}": ${String(error)}`);
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
this.api.registerTool({
|
|
596
|
+
name: "collaboration_org_membership",
|
|
597
|
+
description: "Inspect one user's organization membership state, including active versus pending invitation state.",
|
|
598
|
+
required: true,
|
|
599
|
+
parameters: {
|
|
600
|
+
type: "object",
|
|
601
|
+
additionalProperties: false,
|
|
602
|
+
properties: {
|
|
603
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
604
|
+
username: { type: "string", minLength: 1, description: "Username to inspect." },
|
|
605
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
606
|
+
},
|
|
607
|
+
required: ["org", "username"],
|
|
608
|
+
},
|
|
609
|
+
execute: async (_id: string, params: unknown) => {
|
|
610
|
+
const p = asRecord(params);
|
|
611
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
612
|
+
const username = typeof p.username === "string" ? p.username.trim() : "";
|
|
613
|
+
if (!org || !username) return toolText("org and username are required.");
|
|
614
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
615
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
616
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
617
|
+
try {
|
|
618
|
+
const membership = await resolved.client.getOrgMembership(org, username);
|
|
619
|
+
return toolText(`Organization membership in "${org}":\n- ${renderOrganizationMembershipLine(membership)}`);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (isHttpStatusError(error, 404)) {
|
|
622
|
+
return toolText(`No active or pending organization membership was found for ${username} in "${org}".`);
|
|
623
|
+
}
|
|
624
|
+
return toolText(`Unable to inspect organization membership for ${username} in "${org}": ${String(error)}`);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
this.api.registerTool({
|
|
630
|
+
name: "collaboration_org_member_remove",
|
|
631
|
+
description: "Remove an active organization member. Requires confirmed=true after explicit user approval.",
|
|
632
|
+
required: true,
|
|
633
|
+
parameters: {
|
|
634
|
+
type: "object",
|
|
635
|
+
additionalProperties: false,
|
|
636
|
+
properties: {
|
|
637
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
638
|
+
username: { type: "string", minLength: 1, description: "Active org member to remove." },
|
|
639
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
640
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
641
|
+
},
|
|
642
|
+
required: ["org", "username"],
|
|
643
|
+
},
|
|
644
|
+
execute: async (_id: string, params: unknown) => {
|
|
645
|
+
const p = asRecord(params);
|
|
646
|
+
const blocked = this.requireMutationConfirmation(p, "remove an organization member");
|
|
647
|
+
if (blocked) return toolText(blocked);
|
|
648
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
649
|
+
const username = typeof p.username === "string" ? p.username.trim() : "";
|
|
650
|
+
if (!org || !username) return toolText("org and username are required.");
|
|
651
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
652
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
653
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
654
|
+
try {
|
|
655
|
+
await resolved.client.removeOrgMember(org, username);
|
|
656
|
+
return toolText(`Removed active organization member ${username} from "${org}". Server-side org-scoped team memberships were cleaned up as part of the removal.`);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
return toolText(`Unable to remove ${username} from org "${org}": ${String(error)}`);
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
this.api.registerTool({
|
|
664
|
+
name: "collaboration_org_membership_remove",
|
|
665
|
+
description: "Remove an active organization membership or revoke a pending org invitation for that user. Requires confirmed=true after explicit user approval.",
|
|
666
|
+
required: true,
|
|
667
|
+
parameters: {
|
|
668
|
+
type: "object",
|
|
669
|
+
additionalProperties: false,
|
|
670
|
+
properties: {
|
|
671
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
672
|
+
username: { type: "string", minLength: 1, description: "Username whose active membership or pending invite should be removed." },
|
|
673
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
674
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
675
|
+
},
|
|
676
|
+
required: ["org", "username"],
|
|
677
|
+
},
|
|
678
|
+
execute: async (_id: string, params: unknown) => {
|
|
679
|
+
const p = asRecord(params);
|
|
680
|
+
const blocked = this.requireMutationConfirmation(p, "remove an organization membership");
|
|
681
|
+
if (blocked) return toolText(blocked);
|
|
682
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
683
|
+
const username = typeof p.username === "string" ? p.username.trim() : "";
|
|
684
|
+
if (!org || !username) return toolText("org and username are required.");
|
|
685
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
686
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
687
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
688
|
+
try {
|
|
689
|
+
await resolved.client.removeOrgMembership(org, username);
|
|
690
|
+
return toolText(`Removed organization membership state for ${username} in "${org}". This deletes an active membership or revokes a pending org invitation, depending on current state.`);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
return toolText(`Unable to remove organization membership state for ${username} in "${org}": ${String(error)}`);
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
506
697
|
this.api.registerTool({
|
|
507
698
|
name: "collaboration_teams",
|
|
508
699
|
description: "List teams in an organization before granting repo access or managing membership.",
|
|
@@ -536,6 +727,37 @@ class ClawMemService {
|
|
|
536
727
|
},
|
|
537
728
|
});
|
|
538
729
|
|
|
730
|
+
this.api.registerTool({
|
|
731
|
+
name: "collaboration_team",
|
|
732
|
+
description: "Inspect one organization team by slug.",
|
|
733
|
+
required: true,
|
|
734
|
+
parameters: {
|
|
735
|
+
type: "object",
|
|
736
|
+
additionalProperties: false,
|
|
737
|
+
properties: {
|
|
738
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
739
|
+
teamSlug: { type: "string", minLength: 1, description: "Team slug." },
|
|
740
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
741
|
+
},
|
|
742
|
+
required: ["org", "teamSlug"],
|
|
743
|
+
},
|
|
744
|
+
execute: async (_id: string, params: unknown) => {
|
|
745
|
+
const p = asRecord(params);
|
|
746
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
747
|
+
const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
|
|
748
|
+
if (!org || !teamSlug) return toolText("org and teamSlug are required.");
|
|
749
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
750
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
751
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
752
|
+
try {
|
|
753
|
+
const team = await resolved.client.getTeam(org, teamSlug);
|
|
754
|
+
return toolText(`Team in "${org}":\n- ${renderTeamLine(team)}`);
|
|
755
|
+
} catch (error) {
|
|
756
|
+
return toolText(`Unable to inspect team ${org}/${teamSlug}: ${String(error)}`);
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
|
|
539
761
|
this.api.registerTool({
|
|
540
762
|
name: "collaboration_team_create",
|
|
541
763
|
description: "Create a team inside an organization. Requires confirmed=true after explicit user approval.",
|
|
@@ -577,6 +799,122 @@ class ClawMemService {
|
|
|
577
799
|
},
|
|
578
800
|
});
|
|
579
801
|
|
|
802
|
+
this.api.registerTool({
|
|
803
|
+
name: "collaboration_team_update",
|
|
804
|
+
description: "Update a team's name, description, or privacy. Requires confirmed=true after explicit user approval.",
|
|
805
|
+
required: true,
|
|
806
|
+
parameters: {
|
|
807
|
+
type: "object",
|
|
808
|
+
additionalProperties: false,
|
|
809
|
+
properties: {
|
|
810
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
811
|
+
teamSlug: { type: "string", minLength: 1, description: "Current team slug." },
|
|
812
|
+
name: { type: "string", minLength: 1, description: "Optional new team display name." },
|
|
813
|
+
description: { type: "string", minLength: 1, description: "Optional new team description." },
|
|
814
|
+
privacy: { type: "string", enum: ["closed", "secret"], description: "Optional team privacy." },
|
|
815
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
816
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
817
|
+
},
|
|
818
|
+
required: ["org", "teamSlug"],
|
|
819
|
+
},
|
|
820
|
+
execute: async (_id: string, params: unknown) => {
|
|
821
|
+
const p = asRecord(params);
|
|
822
|
+
const blocked = this.requireMutationConfirmation(p, "update a team");
|
|
823
|
+
if (blocked) return toolText(blocked);
|
|
824
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
825
|
+
const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
|
|
826
|
+
if (!org || !teamSlug) return toolText("org and teamSlug are required.");
|
|
827
|
+
const name = typeof p.name === "string" && p.name.trim() ? p.name.trim() : undefined;
|
|
828
|
+
const description = typeof p.description === "string" && p.description.trim() ? p.description.trim() : undefined;
|
|
829
|
+
const privacy = p.privacy === "secret" ? "secret" : p.privacy === "closed" ? "closed" : undefined;
|
|
830
|
+
if (!name && !description && !privacy) {
|
|
831
|
+
return toolText("Provide at least one of name, description, or privacy when updating a team.");
|
|
832
|
+
}
|
|
833
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
834
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
835
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
836
|
+
try {
|
|
837
|
+
const updated = await resolved.client.updateTeam(org, teamSlug, {
|
|
838
|
+
...(name ? { name } : {}),
|
|
839
|
+
...(description ? { description } : {}),
|
|
840
|
+
...(privacy ? { privacy } : {}),
|
|
841
|
+
});
|
|
842
|
+
return toolText(`Updated team in "${org}": ${renderTeamLine(updated)}.`);
|
|
843
|
+
} catch (error) {
|
|
844
|
+
return toolText(`Unable to update team ${org}/${teamSlug}: ${String(error)}`);
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
this.api.registerTool({
|
|
850
|
+
name: "collaboration_team_delete",
|
|
851
|
+
description: "Delete a team. Requires confirmed=true after explicit user approval.",
|
|
852
|
+
required: true,
|
|
853
|
+
parameters: {
|
|
854
|
+
type: "object",
|
|
855
|
+
additionalProperties: false,
|
|
856
|
+
properties: {
|
|
857
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
858
|
+
teamSlug: { type: "string", minLength: 1, description: "Team slug." },
|
|
859
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
860
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
861
|
+
},
|
|
862
|
+
required: ["org", "teamSlug"],
|
|
863
|
+
},
|
|
864
|
+
execute: async (_id: string, params: unknown) => {
|
|
865
|
+
const p = asRecord(params);
|
|
866
|
+
const blocked = this.requireMutationConfirmation(p, "delete a team");
|
|
867
|
+
if (blocked) return toolText(blocked);
|
|
868
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
869
|
+
const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
|
|
870
|
+
if (!org || !teamSlug) return toolText("org and teamSlug are required.");
|
|
871
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
872
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
873
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
874
|
+
try {
|
|
875
|
+
await resolved.client.deleteTeam(org, teamSlug);
|
|
876
|
+
return toolText(`Deleted team ${org}/${teamSlug}.`);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
return toolText(`Unable to delete team ${org}/${teamSlug}: ${String(error)}`);
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
this.api.registerTool({
|
|
884
|
+
name: "collaboration_team_members",
|
|
885
|
+
description: "List members of an organization team.",
|
|
886
|
+
required: true,
|
|
887
|
+
parameters: {
|
|
888
|
+
type: "object",
|
|
889
|
+
additionalProperties: false,
|
|
890
|
+
properties: {
|
|
891
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
892
|
+
teamSlug: { type: "string", minLength: 1, description: "Team slug." },
|
|
893
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
894
|
+
},
|
|
895
|
+
required: ["org", "teamSlug"],
|
|
896
|
+
},
|
|
897
|
+
execute: async (_id: string, params: unknown) => {
|
|
898
|
+
const p = asRecord(params);
|
|
899
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
900
|
+
const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
|
|
901
|
+
if (!org || !teamSlug) return toolText("org and teamSlug are required.");
|
|
902
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
903
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
904
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
905
|
+
try {
|
|
906
|
+
const members = await resolved.client.listTeamMembers(org, teamSlug);
|
|
907
|
+
if (members.length === 0) return toolText(`No members found in ${org}/${teamSlug}.`);
|
|
908
|
+
return toolText([
|
|
909
|
+
`Members of ${org}/${teamSlug}:`,
|
|
910
|
+
...members.map((member) => `- ${renderCollaboratorLine(member)}`),
|
|
911
|
+
].join("\n"));
|
|
912
|
+
} catch (error) {
|
|
913
|
+
return toolText(`Unable to list members for ${org}/${teamSlug}: ${String(error)}`);
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
|
|
580
918
|
this.api.registerTool({
|
|
581
919
|
name: "collaboration_team_membership_set",
|
|
582
920
|
description: "Add or update a user's membership in an organization team. Requires confirmed=true after explicit user approval.",
|
|
@@ -759,6 +1097,40 @@ class ClawMemService {
|
|
|
759
1097
|
},
|
|
760
1098
|
});
|
|
761
1099
|
|
|
1100
|
+
this.api.registerTool({
|
|
1101
|
+
name: "collaboration_repo_transfer",
|
|
1102
|
+
description: "Transfer a repository to a new owner, commonly used to move a personal memory repo into an existing org. Requires confirmed=true after explicit user approval.",
|
|
1103
|
+
required: true,
|
|
1104
|
+
parameters: {
|
|
1105
|
+
type: "object",
|
|
1106
|
+
additionalProperties: false,
|
|
1107
|
+
properties: {
|
|
1108
|
+
repo: { type: "string", minLength: 3, description: "Optional source repo in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
1109
|
+
newOwner: { type: "string", minLength: 1, description: "Destination owner login, often an organization login." },
|
|
1110
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
1111
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
1112
|
+
},
|
|
1113
|
+
required: ["newOwner"],
|
|
1114
|
+
},
|
|
1115
|
+
execute: async (_id: string, params: unknown) => {
|
|
1116
|
+
const p = asRecord(params);
|
|
1117
|
+
const blocked = this.requireMutationConfirmation(p, "transfer a repository");
|
|
1118
|
+
if (blocked) return toolText(blocked);
|
|
1119
|
+
const newOwner = typeof p.newOwner === "string" ? p.newOwner.trim() : "";
|
|
1120
|
+
if (!newOwner) return toolText("newOwner is empty.");
|
|
1121
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
1122
|
+
const target = await this.requireCollaborationRepo(agentId, p.repo);
|
|
1123
|
+
if ("error" in target) return toolText(target.error);
|
|
1124
|
+
try {
|
|
1125
|
+
const transferred = await target.client.transferRepo(target.owner, target.repo, newOwner);
|
|
1126
|
+
const nextFullName = repoSummaryFullName(transferred) || `${newOwner}/${target.repo}`;
|
|
1127
|
+
return toolText(`Transferred ${target.fullName} to ${nextFullName}. If this repo was your configured defaultRepo, retarget future memory operations to ${nextFullName} explicitly until config is updated.`);
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
return toolText(`Unable to transfer ${target.fullName} to ${newOwner}: ${String(error)}`);
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
|
|
762
1134
|
this.api.registerTool({
|
|
763
1135
|
name: "collaboration_repo_collaborators",
|
|
764
1136
|
description: "List direct collaborators on a repo before changing repository-level access.",
|
|
@@ -1084,6 +1456,41 @@ class ClawMemService {
|
|
|
1084
1456
|
},
|
|
1085
1457
|
});
|
|
1086
1458
|
|
|
1459
|
+
this.api.registerTool({
|
|
1460
|
+
name: "collaboration_org_invitation_revoke",
|
|
1461
|
+
description: "Revoke a pending organization invitation from the org side. Requires confirmed=true after explicit user approval.",
|
|
1462
|
+
required: true,
|
|
1463
|
+
parameters: {
|
|
1464
|
+
type: "object",
|
|
1465
|
+
additionalProperties: false,
|
|
1466
|
+
properties: {
|
|
1467
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
1468
|
+
invitationId: { type: "integer", minimum: 1, description: "Pending organization invitation id." },
|
|
1469
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
1470
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
1471
|
+
},
|
|
1472
|
+
required: ["org", "invitationId"],
|
|
1473
|
+
},
|
|
1474
|
+
execute: async (_id: string, params: unknown) => {
|
|
1475
|
+
const p = asRecord(params);
|
|
1476
|
+
const blocked = this.requireMutationConfirmation(p, "revoke an organization invitation");
|
|
1477
|
+
if (blocked) return toolText(blocked);
|
|
1478
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
1479
|
+
if (!org) return toolText("org is empty.");
|
|
1480
|
+
const invitationId = this.resolvePositiveInteger(p.invitationId, "invitationId");
|
|
1481
|
+
if ("error" in invitationId) return toolText(invitationId.error);
|
|
1482
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
1483
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
1484
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
1485
|
+
try {
|
|
1486
|
+
await resolved.client.revokeOrgInvitation(org, invitationId.value);
|
|
1487
|
+
return toolText(`Revoked organization invitation ${invitationId.value} in "${org}".`);
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
return toolText(`Unable to revoke organization invitation ${invitationId.value} in "${org}": ${String(error)}`);
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1087
1494
|
this.api.registerTool({
|
|
1088
1495
|
name: "collaboration_user_org_invitations",
|
|
1089
1496
|
description: "List pending organization invitations for the current ClawMem identity before concluding that no shared org access is available.",
|
|
@@ -1230,6 +1637,7 @@ class ClawMemService {
|
|
|
1230
1637
|
additionalProperties: false,
|
|
1231
1638
|
properties: {
|
|
1232
1639
|
repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
1640
|
+
username: { type: "string", minLength: 1, description: "Optional username to inspect for org-level base permission and membership state on org-owned repos." },
|
|
1233
1641
|
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
1234
1642
|
},
|
|
1235
1643
|
},
|
|
@@ -1242,7 +1650,10 @@ class ClawMemService {
|
|
|
1242
1650
|
try {
|
|
1243
1651
|
const lines = [`Repo access inspection for ${target.fullName}:`];
|
|
1244
1652
|
const notes: string[] = [];
|
|
1653
|
+
const username = typeof p.username === "string" ? p.username.trim() : "";
|
|
1245
1654
|
let orgName: string | undefined;
|
|
1655
|
+
let orgDefaultPermission: "none" | CollaborationPermission | undefined;
|
|
1656
|
+
let orgContextAvailable = false;
|
|
1246
1657
|
|
|
1247
1658
|
try {
|
|
1248
1659
|
const repo = await target.client.getRepo(target.owner, target.repo);
|
|
@@ -1255,12 +1666,49 @@ class ClawMemService {
|
|
|
1255
1666
|
}
|
|
1256
1667
|
|
|
1257
1668
|
try {
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1669
|
+
const ownerOrg = orgName || target.owner;
|
|
1670
|
+
const org = await target.client.getOrg(ownerOrg);
|
|
1671
|
+
orgContextAvailable = true;
|
|
1672
|
+
orgDefaultPermission = normalizePermissionAlias(org.default_repository_permission);
|
|
1673
|
+
lines.push(`- Org default repository permission: ${orgDefaultPermission || "unknown"}`);
|
|
1260
1674
|
} catch (error) {
|
|
1261
1675
|
notes.push(`Org metadata unavailable for "${orgName}": ${String(error)}`);
|
|
1262
1676
|
}
|
|
1263
1677
|
|
|
1678
|
+
if (username) {
|
|
1679
|
+
lines.push("");
|
|
1680
|
+
lines.push(`Org membership for "${username}" in "${orgName}":`);
|
|
1681
|
+
if (!orgName || !orgContextAvailable) {
|
|
1682
|
+
lines.push("- Not applicable because the owner org could not be resolved.");
|
|
1683
|
+
} else {
|
|
1684
|
+
try {
|
|
1685
|
+
const membership = await target.client.getOrgMembership(orgName, username);
|
|
1686
|
+
lines.push(`- ${renderOrganizationMembershipLine(membership)}`);
|
|
1687
|
+
if (membership.state === "active") {
|
|
1688
|
+
if (orgDefaultPermission && orgDefaultPermission !== "none") {
|
|
1689
|
+
lines.push(`- Org base repo access is active via default permission "${orgDefaultPermission}".`);
|
|
1690
|
+
notes.push(`Because ${username} is an active org member and "${orgName}" default repository permission is ${orgDefaultPermission}, removing direct collaborators or team grants alone may not remove repo access.`);
|
|
1691
|
+
} else {
|
|
1692
|
+
lines.push("- No org base repo access is visible because the org default permission is none.");
|
|
1693
|
+
}
|
|
1694
|
+
} else {
|
|
1695
|
+
lines.push("- Org base repo access is not active yet because the org membership is still pending.");
|
|
1696
|
+
}
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
if (isHttpStatusError(error, 404)) {
|
|
1699
|
+
lines.push("- No active or pending org membership was found.");
|
|
1700
|
+
if (orgDefaultPermission && orgDefaultPermission !== "none") {
|
|
1701
|
+
lines.push("- Org base repo access does not apply unless the user becomes an org member.");
|
|
1702
|
+
}
|
|
1703
|
+
} else {
|
|
1704
|
+
notes.push(`Org membership lookup failed for "${username}" in "${orgName}": ${String(error)}`);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
} else if (orgDefaultPermission && orgDefaultPermission !== "none") {
|
|
1709
|
+
notes.push(`Any active org member can still inherit ${orgDefaultPermission} access from "${orgName}" even after direct collaborator or team grants are removed.`);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1264
1712
|
try {
|
|
1265
1713
|
const collaborators = filterDirectCollaborators(await target.client.listRepoCollaborators(target.owner, target.repo), target.owner);
|
|
1266
1714
|
lines.push("");
|
|
@@ -1295,7 +1743,8 @@ class ClawMemService {
|
|
|
1295
1743
|
}
|
|
1296
1744
|
|
|
1297
1745
|
try {
|
|
1298
|
-
const
|
|
1746
|
+
const ownerOrg = orgName || target.owner;
|
|
1747
|
+
const outside = await target.client.listOrgOutsideCollaborators(ownerOrg);
|
|
1299
1748
|
lines.push("");
|
|
1300
1749
|
lines.push(`Outside collaborators in owner org "${orgName}":`);
|
|
1301
1750
|
if (outside.length === 0) lines.push("- None visible");
|
|
@@ -1317,9 +1766,12 @@ class ClawMemService {
|
|
|
1317
1766
|
});
|
|
1318
1767
|
}
|
|
1319
1768
|
|
|
1320
|
-
private async handleBeforePromptBuild(event: unknown, agentId?: string): Promise<{
|
|
1769
|
+
private async handleBeforePromptBuild(event: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
|
|
1321
1770
|
const context = await this.collectAutoRecallContext(event, agentId);
|
|
1322
|
-
|
|
1771
|
+
// Auto-recall is per-turn dynamic context, so keep it out of the system prompt.
|
|
1772
|
+
// OpenClaw documents dynamic context on `prependContext`: https://github.com/maweibin/openclaw/blob/d9a2869ad69db9449336a2e2846bd9de0e647ac6/docs/concepts/agent-loop.md?plain=1#L85
|
|
1773
|
+
// Changing the system prompt can defeat provider prefix caching.
|
|
1774
|
+
return context ? { prependContext: context } : undefined;
|
|
1323
1775
|
}
|
|
1324
1776
|
|
|
1325
1777
|
private async handleBeforeAgentStart(event: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
|
|
@@ -1327,10 +1779,19 @@ class ClawMemService {
|
|
|
1327
1779
|
return context ? { prependContext: context } : undefined;
|
|
1328
1780
|
}
|
|
1329
1781
|
|
|
1782
|
+
private async handleAgentEnd(payload: TurnPayload): Promise<void> {
|
|
1783
|
+
if (!payload.sessionId) return;
|
|
1784
|
+
await this.enqueueSessionIo(sessionScopeKey(payload.sessionId, payload.agentId), () => this.syncTurn(payload));
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
private async handleFinalize(payload: FinalizePayload): Promise<void> {
|
|
1788
|
+
if (!payload.sessionId) return;
|
|
1789
|
+
await this.enqueueSessionIo(sessionScopeKey(payload.sessionId, payload.agentId), () => this.finalize(payload));
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1330
1792
|
private async collectAutoRecallContext(event: unknown, agentId?: string): Promise<string | undefined> {
|
|
1331
1793
|
const routeAgentId = normalizeAgentId(agentId);
|
|
1332
1794
|
if (!(await this.ensureDefaultRepoConfigured(routeAgentId))) return undefined;
|
|
1333
|
-
this.scheduleRecentSessionMaintenance(routeAgentId);
|
|
1334
1795
|
const prompt = extractPromptTextForRecall(event);
|
|
1335
1796
|
if (typeof prompt !== "string" || prompt.trim().length < 5) return undefined;
|
|
1336
1797
|
try {
|
|
@@ -1357,7 +1818,7 @@ class ClawMemService {
|
|
|
1357
1818
|
const { conv } = this.getServices(agentId);
|
|
1358
1819
|
if (!conv.shouldMirror(snap.sessionId, snap.messages)) return;
|
|
1359
1820
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1360
|
-
await this.
|
|
1821
|
+
await this.enqueueSessionIo(sessionScopeKey(snap.sessionId, agentId), async () => {
|
|
1361
1822
|
const s = this.getOrCreate(snap.sessionId!, agentId);
|
|
1362
1823
|
s.sessionFile = sessionFile;
|
|
1363
1824
|
s.updatedAt = new Date().toISOString();
|
|
@@ -1366,25 +1827,13 @@ class ClawMemService {
|
|
|
1366
1827
|
});
|
|
1367
1828
|
}
|
|
1368
1829
|
|
|
1369
|
-
private scheduleTurn(p: TurnPayload): void {
|
|
1370
|
-
if (!p.sessionId) return;
|
|
1371
|
-
const scopeKey = sessionScopeKey(p.sessionId, p.agentId);
|
|
1372
|
-
const prev = this.syncTimers.get(scopeKey);
|
|
1373
|
-
if (prev) clearTimeout(prev);
|
|
1374
|
-
const timer = setTimeout(() => {
|
|
1375
|
-
this.syncTimers.delete(scopeKey);
|
|
1376
|
-
void this.track(this.enqueueSession(scopeKey, () => this.syncTurn(p))).catch((e) => this.warn("turn sync", e));
|
|
1377
|
-
}, this.config.turnCommentDelayMs);
|
|
1378
|
-
timer.unref?.();
|
|
1379
|
-
this.syncTimers.set(scopeKey, timer);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
1830
|
private async syncTurn(p: TurnPayload): Promise<void> {
|
|
1383
1831
|
if (!p.sessionId) return;
|
|
1384
1832
|
const agentId = normalizeAgentId(p.agentId);
|
|
1385
1833
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1386
1834
|
const { conv } = this.getServices(agentId);
|
|
1387
1835
|
const s = this.getOrCreate(p.sessionId, agentId);
|
|
1836
|
+
if (s.finalizedAt) return;
|
|
1388
1837
|
s.sessionKey = p.sessionKey ?? s.sessionKey; s.agentId = agentId; s.updatedAt = new Date().toISOString();
|
|
1389
1838
|
const snap = await conv.loadSnapshot(s, p.messages);
|
|
1390
1839
|
if (!conv.shouldMirror(s.sessionId, snap.messages) || snap.messages.length === 0) { await this.persistState(); return; }
|
|
@@ -1393,46 +1842,194 @@ class ClawMemService {
|
|
|
1393
1842
|
const next = snap.messages.slice(s.lastMirroredCount);
|
|
1394
1843
|
if (next.length > 0) { const n = await conv.appendComments(s.issueNumber!, next); s.lastMirroredCount += n; s.turnCount += n; }
|
|
1395
1844
|
await this.persistState();
|
|
1396
|
-
this.scheduleRecentSessionMaintenance(agentId);
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
private enqueueFinalize(p: FinalizePayload): void {
|
|
1400
|
-
if (!p.sessionId) return;
|
|
1401
|
-
const scopeKey = sessionScopeKey(p.sessionId, p.agentId);
|
|
1402
|
-
const prev = this.syncTimers.get(scopeKey);
|
|
1403
|
-
if (prev) { clearTimeout(prev); this.syncTimers.delete(scopeKey); }
|
|
1404
|
-
void this.track(this.enqueueSession(scopeKey, () => this.finalize(p))).catch((e) => this.warn("finalize", e));
|
|
1405
1845
|
}
|
|
1406
1846
|
|
|
1407
1847
|
private async finalize(p: FinalizePayload): Promise<void> {
|
|
1408
1848
|
if (!p.sessionId) return;
|
|
1409
1849
|
const agentId = normalizeAgentId(p.agentId);
|
|
1410
|
-
const scopeKey = sessionScopeKey(p.sessionId, agentId);
|
|
1411
1850
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1412
|
-
const { conv } = this.getServices(agentId);
|
|
1851
|
+
const { conv, mem, route } = this.getServices(agentId);
|
|
1413
1852
|
const s = this.getOrCreate(p.sessionId, agentId);
|
|
1414
|
-
if (s.finalizedAt) return;
|
|
1415
1853
|
s.sessionKey = p.sessionKey ?? s.sessionKey; s.sessionFile = p.sessionFile ?? s.sessionFile;
|
|
1416
1854
|
s.agentId = agentId; s.updatedAt = new Date().toISOString();
|
|
1417
1855
|
const snap = await conv.loadSnapshot(s, p.messages ?? []);
|
|
1418
1856
|
if (!conv.shouldMirror(s.sessionId, snap.messages)) { await this.persistState(); return; }
|
|
1419
1857
|
if (snap.messages.length === 0 && !s.issueNumber) { await this.persistState(); return; }
|
|
1420
|
-
await
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1858
|
+
await this.captureSessionFinalState(s, snap, conv, mem, route, { markFinalized: true, reason: "finalize" });
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
private async captureSessionFinalState(
|
|
1862
|
+
session: SessionMirrorState,
|
|
1863
|
+
snapshot: TranscriptSnapshot,
|
|
1864
|
+
conv: ConversationMirror,
|
|
1865
|
+
mem: MemoryStore,
|
|
1866
|
+
route: ClawMemResolvedRoute,
|
|
1867
|
+
options: { markFinalized: boolean; reason: string },
|
|
1868
|
+
): Promise<void> {
|
|
1869
|
+
await conv.ensureIssue(session, snapshot);
|
|
1870
|
+
const next = snapshot.messages.slice(session.lastMirroredCount);
|
|
1871
|
+
if (next.length > 0) {
|
|
1872
|
+
const n = await conv.appendComments(session.issueNumber!, next);
|
|
1873
|
+
session.lastMirroredCount += n;
|
|
1874
|
+
session.turnCount += n;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
const derived = this.ensureDerived(session);
|
|
1878
|
+
let summaryText = derived.summary.text?.trim() || "pending";
|
|
1879
|
+
let titleOverride = derived.summary.title?.trim() || session.issueTitle;
|
|
1880
|
+
let generatedTitle = Boolean(derived.summary.title?.trim());
|
|
1881
|
+
const targetCursor = snapshot.messages.length;
|
|
1882
|
+
const meaningfulTranscript = snapshot.messages.filter((message) => message.text.trim()).length >= 2;
|
|
1883
|
+
|
|
1884
|
+
if (meaningfulTranscript) {
|
|
1885
|
+
try {
|
|
1886
|
+
const artifacts = await this.resolveFinalArtifacts(session, snapshot, conv, mem);
|
|
1887
|
+
summaryText = artifacts.summary;
|
|
1888
|
+
if (artifacts.title?.trim()) {
|
|
1889
|
+
titleOverride = artifacts.title.trim();
|
|
1890
|
+
generatedTitle = true;
|
|
1891
|
+
}
|
|
1892
|
+
const storedCount = await this.applyFinalMemoryCandidates(session, mem, route, targetCursor, artifacts.candidates);
|
|
1893
|
+
if (storedCount > 0) {
|
|
1894
|
+
this.api.logger.info?.(
|
|
1895
|
+
`clawmem: ${options.reason} stored ${storedCount} memor${storedCount === 1 ? "y" : "ies"} for ${session.sessionId}`,
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
} catch (error) {
|
|
1899
|
+
derived.summary.status = "error";
|
|
1900
|
+
derived.summary.lastError = String(error);
|
|
1901
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1902
|
+
derived.memory.status = "error";
|
|
1903
|
+
derived.memory.lastError = String(error);
|
|
1904
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1905
|
+
this.warn(`${options.reason} derive for ${session.sessionId}`, error);
|
|
1906
|
+
}
|
|
1907
|
+
} else {
|
|
1908
|
+
derived.summary.status = "complete";
|
|
1909
|
+
derived.summary.basedOnCursor = targetCursor;
|
|
1910
|
+
derived.summary.lastError = undefined;
|
|
1911
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1912
|
+
derived.memory.capturedCursor = targetCursor;
|
|
1913
|
+
derived.memory.status = "complete";
|
|
1914
|
+
derived.memory.lastError = undefined;
|
|
1915
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
try {
|
|
1919
|
+
await conv.syncLabels(session, snapshot, true);
|
|
1920
|
+
await conv.syncBody(session, snapshot, summaryText, true, titleOverride);
|
|
1921
|
+
derived.summary.text = summaryText;
|
|
1922
|
+
derived.summary.basedOnCursor = targetCursor;
|
|
1923
|
+
derived.summary.status = "complete";
|
|
1924
|
+
derived.summary.lastError = undefined;
|
|
1925
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1926
|
+
if (titleOverride?.trim()) {
|
|
1927
|
+
derived.summary.title = titleOverride.trim();
|
|
1928
|
+
session.issueTitle = titleOverride.trim();
|
|
1929
|
+
if (generatedTitle) session.titleSource = "llm";
|
|
1930
|
+
}
|
|
1931
|
+
if (options.markFinalized && !session.finalizedAt) session.finalizedAt = new Date().toISOString();
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
derived.summary.status = "error";
|
|
1934
|
+
derived.summary.lastError = String(error);
|
|
1935
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1936
|
+
this.warn(`${options.reason} summary sync for ${session.sessionId}`, error);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
session.updatedAt = new Date().toISOString();
|
|
1428
1940
|
await this.persistState();
|
|
1429
|
-
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
private async resolveFinalArtifacts(
|
|
1944
|
+
session: SessionMirrorState,
|
|
1945
|
+
snapshot: TranscriptSnapshot,
|
|
1946
|
+
conv: ConversationMirror,
|
|
1947
|
+
mem: MemoryStore,
|
|
1948
|
+
): Promise<{ summary: string; title?: string; candidates: MemoryCandidate[] }> {
|
|
1949
|
+
const cached = getCachedFinalArtifacts(session, snapshot.messages.length);
|
|
1950
|
+
if (cached) return cached;
|
|
1951
|
+
|
|
1952
|
+
let schema;
|
|
1953
|
+
try {
|
|
1954
|
+
schema = await mem.listSchema();
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
this.warn(`finalize schema load for ${session.sessionId}`, error);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const artifacts = await conv.generateFinalArtifacts(session, snapshot, schema);
|
|
1960
|
+
const derived = this.ensureDerived(session);
|
|
1961
|
+
const now = new Date().toISOString();
|
|
1962
|
+
derived.summary.text = artifacts.summary;
|
|
1963
|
+
derived.summary.title = artifacts.title?.trim() || undefined;
|
|
1964
|
+
derived.summary.basedOnCursor = snapshot.messages.length;
|
|
1965
|
+
derived.summary.lastError = undefined;
|
|
1966
|
+
derived.summary.updatedAt = now;
|
|
1967
|
+
derived.memory.candidates = artifacts.candidates;
|
|
1968
|
+
derived.memory.lastError = undefined;
|
|
1969
|
+
derived.memory.updatedAt = now;
|
|
1970
|
+
return artifacts;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
private async applyFinalMemoryCandidates(
|
|
1974
|
+
session: SessionMirrorState,
|
|
1975
|
+
mem: MemoryStore,
|
|
1976
|
+
route: ClawMemResolvedRoute,
|
|
1977
|
+
targetCursor: number,
|
|
1978
|
+
candidates: MemoryCandidate[],
|
|
1979
|
+
): Promise<number> {
|
|
1980
|
+
const derived = this.ensureDerived(session);
|
|
1981
|
+
if (derived.memory.capturedCursor >= targetCursor && derived.memory.status === "complete") {
|
|
1982
|
+
derived.memory.candidates = undefined;
|
|
1983
|
+
return 0;
|
|
1984
|
+
}
|
|
1985
|
+
if (candidates.length === 0) {
|
|
1986
|
+
derived.memory.candidates = undefined;
|
|
1987
|
+
derived.memory.capturedCursor = targetCursor;
|
|
1988
|
+
derived.memory.status = "complete";
|
|
1989
|
+
derived.memory.lastError = undefined;
|
|
1990
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1991
|
+
return 0;
|
|
1992
|
+
}
|
|
1993
|
+
try {
|
|
1994
|
+
const result = await this.enqueueRepoWrite(this.repoWriteKey(route), async () => {
|
|
1995
|
+
let createdCount = 0;
|
|
1996
|
+
for (const candidate of candidates) {
|
|
1997
|
+
const stored = await mem.store({
|
|
1998
|
+
...(candidate.title ? { title: candidate.title } : {}),
|
|
1999
|
+
detail: candidate.detail,
|
|
2000
|
+
...(candidate.kind ? { kind: candidate.kind } : {}),
|
|
2001
|
+
...(candidate.topics?.length ? { topics: candidate.topics } : {}),
|
|
2002
|
+
});
|
|
2003
|
+
if (stored.created) createdCount++;
|
|
2004
|
+
}
|
|
2005
|
+
return createdCount;
|
|
2006
|
+
});
|
|
2007
|
+
derived.memory.candidates = undefined;
|
|
2008
|
+
derived.memory.capturedCursor = targetCursor;
|
|
2009
|
+
derived.memory.status = "complete";
|
|
2010
|
+
derived.memory.lastError = undefined;
|
|
2011
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
2012
|
+
return result;
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
derived.memory.candidates = candidates;
|
|
2015
|
+
derived.memory.status = "error";
|
|
2016
|
+
derived.memory.lastError = String(error);
|
|
2017
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
2018
|
+
this.warn(`finalize memory store for ${session.sessionId}`, error);
|
|
2019
|
+
return 0;
|
|
2020
|
+
}
|
|
1430
2021
|
}
|
|
1431
2022
|
|
|
1432
2023
|
// --- Infrastructure ---
|
|
1433
2024
|
|
|
1434
|
-
private
|
|
1435
|
-
return this.
|
|
2025
|
+
private enqueueSessionIo<T>(sessionId: string, task: () => Promise<T>): Promise<T> {
|
|
2026
|
+
return this.ioQueue.enqueue(sessionId, async () => { await this.ensureLoaded(); return task(); });
|
|
2027
|
+
}
|
|
2028
|
+
private enqueueRepoWrite<T>(repoKey: string, task: () => Promise<T>): Promise<T> {
|
|
2029
|
+
return this.repoWriteQueue.enqueue(repoKey, task);
|
|
2030
|
+
}
|
|
2031
|
+
private repoWriteKey(route: ClawMemResolvedRoute): string {
|
|
2032
|
+
return route.repo || route.defaultRepo || route.agentId;
|
|
1436
2033
|
}
|
|
1437
2034
|
private track<T>(promise: Promise<T>): Promise<T> {
|
|
1438
2035
|
this.pending.add(promise);
|
|
@@ -1453,12 +2050,32 @@ class ClawMemService {
|
|
|
1453
2050
|
agentId: normalizeAgentId(agentId),
|
|
1454
2051
|
lastMirroredCount: 0,
|
|
1455
2052
|
turnCount: 0,
|
|
2053
|
+
derived: {
|
|
2054
|
+
summary: { basedOnCursor: 0, status: "idle" },
|
|
2055
|
+
memory: {
|
|
2056
|
+
capturedCursor: 0,
|
|
2057
|
+
status: "idle",
|
|
2058
|
+
},
|
|
2059
|
+
},
|
|
1456
2060
|
createdAt: now,
|
|
1457
2061
|
updatedAt: now,
|
|
1458
2062
|
};
|
|
1459
2063
|
this.state.sessions[scopeKey] = s;
|
|
1460
2064
|
return s;
|
|
1461
2065
|
}
|
|
2066
|
+
|
|
2067
|
+
private ensureDerived(session: SessionMirrorState): SessionDerivedState {
|
|
2068
|
+
if (!session.derived) {
|
|
2069
|
+
session.derived = {
|
|
2070
|
+
summary: { basedOnCursor: 0, status: "idle" },
|
|
2071
|
+
memory: {
|
|
2072
|
+
capturedCursor: 0,
|
|
2073
|
+
status: "idle",
|
|
2074
|
+
},
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
return session.derived;
|
|
2078
|
+
}
|
|
1462
2079
|
private resolveTranscriptAgentId(sessionId: string, sessionFile: string): string | null {
|
|
1463
2080
|
const fromPath = inferAgentIdFromTranscriptPath(sessionFile);
|
|
1464
2081
|
if (fromPath) return fromPath;
|
|
@@ -1583,111 +2200,6 @@ class ClawMemService {
|
|
|
1583
2200
|
},
|
|
1584
2201
|
});
|
|
1585
2202
|
}
|
|
1586
|
-
private scheduleRecentSessionMaintenance(agentId: string): void {
|
|
1587
|
-
const sessions = Object.values(this.state.sessions)
|
|
1588
|
-
.filter((session) => normalizeAgentId(session.agentId) === agentId)
|
|
1589
|
-
.sort((a, b) => Date.parse(b.updatedAt ?? b.createdAt ?? "") - Date.parse(a.updatedAt ?? a.createdAt ?? ""))
|
|
1590
|
-
.slice(0, 8);
|
|
1591
|
-
for (const session of sessions) {
|
|
1592
|
-
if (!this.sessionNeedsMaintenance(session)) continue;
|
|
1593
|
-
this.scheduleSessionMaintenance(sessionScopeKey(session.sessionId, session.agentId), agentId, {
|
|
1594
|
-
reason: "request-start-fallback",
|
|
1595
|
-
delayMs: 0,
|
|
1596
|
-
});
|
|
1597
|
-
break;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
private scheduleSessionMaintenance(
|
|
1602
|
-
scopeKey: string,
|
|
1603
|
-
agentId: string,
|
|
1604
|
-
options: { delayMs?: number; attempt?: number; reason?: string } = {},
|
|
1605
|
-
): void {
|
|
1606
|
-
const prev = this.maintenanceTimers.get(scopeKey);
|
|
1607
|
-
if (prev) clearTimeout(prev);
|
|
1608
|
-
const delayMs = Math.max(0, options.delayMs ?? 0);
|
|
1609
|
-
const attempt = Math.max(0, options.attempt ?? 0);
|
|
1610
|
-
const reason = options.reason ?? "scheduled";
|
|
1611
|
-
const timer = setTimeout(() => {
|
|
1612
|
-
this.maintenanceTimers.delete(scopeKey);
|
|
1613
|
-
void this.track(this.enqueueSession(scopeKey, () => this.runSessionMaintenance(scopeKey, agentId, attempt, reason)))
|
|
1614
|
-
.catch((error) => this.warn(`background maintenance for ${scopeKey}`, error));
|
|
1615
|
-
}, delayMs);
|
|
1616
|
-
timer.unref?.();
|
|
1617
|
-
this.maintenanceTimers.set(scopeKey, timer);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
private async runSessionMaintenance(scopeKey: string, agentId: string, attempt: number, reason: string): Promise<void> {
|
|
1621
|
-
const session = this.state.sessions[scopeKey];
|
|
1622
|
-
if (!session || !this.sessionNeedsMaintenance(session)) return;
|
|
1623
|
-
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1624
|
-
const { conv, mem } = this.getServices(agentId);
|
|
1625
|
-
const snap = await conv.loadSnapshot(session, []);
|
|
1626
|
-
if (!conv.shouldMirror(session.sessionId, snap.messages) || snap.messages.length === 0) return;
|
|
1627
|
-
let changed = false;
|
|
1628
|
-
let retryNeeded = false;
|
|
1629
|
-
if (!session.issueNumber) {
|
|
1630
|
-
await conv.ensureIssue(session, snap);
|
|
1631
|
-
changed = true;
|
|
1632
|
-
}
|
|
1633
|
-
if (session.summaryStatus === "pending") {
|
|
1634
|
-
try {
|
|
1635
|
-
const result = await conv.generateSummaryAndTitle(session, snap);
|
|
1636
|
-
await conv.syncLabels(session, snap, true);
|
|
1637
|
-
await conv.syncBody(session, snap, result.summary, true, result.title);
|
|
1638
|
-
session.summaryStatus = "complete";
|
|
1639
|
-
if (result.title?.trim()) {
|
|
1640
|
-
session.issueTitle = result.title.trim();
|
|
1641
|
-
session.titleSource = "llm";
|
|
1642
|
-
}
|
|
1643
|
-
this.maybeAutoNameRepo(agentId, result.summary, result.title);
|
|
1644
|
-
changed = true;
|
|
1645
|
-
} catch (error) {
|
|
1646
|
-
retryNeeded = true;
|
|
1647
|
-
this.warn(`background summary sync for ${session.sessionId}`, error);
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
if (session.titleSource !== "llm" && snap.messages.length >= 2) {
|
|
1651
|
-
try {
|
|
1652
|
-
await conv.syncTitle(session, snap);
|
|
1653
|
-
changed = true;
|
|
1654
|
-
} catch (error) {
|
|
1655
|
-
retryNeeded = true;
|
|
1656
|
-
this.warn(`background title sync for ${session.sessionId}`, error);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
if (snap.messages.length >= 2 && snap.messages.length > (session.lastMemorySyncCount ?? 0)) {
|
|
1660
|
-
const ok = await mem.syncFromConversation(session, snap);
|
|
1661
|
-
if (ok) {
|
|
1662
|
-
session.lastMemorySyncCount = snap.messages.length;
|
|
1663
|
-
changed = true;
|
|
1664
|
-
} else {
|
|
1665
|
-
retryNeeded = true;
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
if (changed) await this.persistState();
|
|
1669
|
-
if (!retryNeeded || !this.sessionNeedsMaintenance(session)) return;
|
|
1670
|
-
if (attempt < SESSION_MAINTENANCE_RETRY_DELAYS_MS.length) {
|
|
1671
|
-
const delayMs = SESSION_MAINTENANCE_RETRY_DELAYS_MS[attempt];
|
|
1672
|
-
this.api.logger.warn?.(
|
|
1673
|
-
`clawmem: background maintenance incomplete for ${session.sessionId}; retrying in ${Math.round(delayMs / 1000)}s (${reason})`,
|
|
1674
|
-
);
|
|
1675
|
-
this.scheduleSessionMaintenance(scopeKey, agentId, { delayMs, attempt: attempt + 1, reason: "retry" });
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
this.api.logger.warn?.(
|
|
1679
|
-
`clawmem: background maintenance remains pending for ${session.sessionId}; it will be retried opportunistically on future requests`,
|
|
1680
|
-
);
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
private sessionNeedsMaintenance(session: SessionMirrorState): boolean {
|
|
1684
|
-
if (session.summaryStatus === "pending") return true;
|
|
1685
|
-
const hasMeaningfulTranscript = Math.max(session.lastMirroredCount, session.turnCount) >= 2;
|
|
1686
|
-
if (!hasMeaningfulTranscript) return false;
|
|
1687
|
-
if (session.titleSource !== "llm") return true;
|
|
1688
|
-
return (session.lastMemorySyncCount ?? 0) < session.lastMirroredCount;
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
2203
|
private getServices(agentId?: string, repo?: string): { route: ClawMemResolvedRoute; conv: ConversationMirror; mem: MemoryStore; client: GitHubIssueClient } {
|
|
1692
2204
|
const route = resolveAgentRoute(this.config, agentId, repo);
|
|
1693
2205
|
const client = new GitHubIssueClient(route, this.api.logger);
|
|
@@ -1695,11 +2207,11 @@ class ClawMemService {
|
|
|
1695
2207
|
route,
|
|
1696
2208
|
client,
|
|
1697
2209
|
conv: new ConversationMirror(client, this.api, this.config),
|
|
1698
|
-
mem: new MemoryStore(client
|
|
2210
|
+
mem: new MemoryStore(client),
|
|
1699
2211
|
};
|
|
1700
2212
|
}
|
|
1701
2213
|
private resolveToolAgentId(agentId: unknown): string {
|
|
1702
|
-
return normalizeAgentId(typeof agentId === "string" && agentId.trim() ? agentId :
|
|
2214
|
+
return normalizeAgentId(typeof agentId === "string" && agentId.trim() ? agentId : getOpenClawAgentIdFromEnv());
|
|
1703
2215
|
}
|
|
1704
2216
|
private resolveToolRepo(repo: unknown): { repo?: string; error?: string } {
|
|
1705
2217
|
if (repo === undefined || repo === null || repo === "") return {};
|
|
@@ -1779,28 +2291,6 @@ class ClawMemService {
|
|
|
1779
2291
|
}
|
|
1780
2292
|
return { value };
|
|
1781
2293
|
}
|
|
1782
|
-
/**
|
|
1783
|
-
* After finalization, check if the repo still has an empty/default description.
|
|
1784
|
-
* If so, use the conversation summary to suggest a meaningful name and update
|
|
1785
|
-
* the repo description automatically. Best-effort, fire-and-forget.
|
|
1786
|
-
*/
|
|
1787
|
-
private maybeAutoNameRepo(agentId: string, summary: string, title?: string): void {
|
|
1788
|
-
if (!summary || summary.startsWith("failed:") || summary === "pending") return;
|
|
1789
|
-
const snippet = title || summary.slice(0, 100);
|
|
1790
|
-
void (async () => {
|
|
1791
|
-
try {
|
|
1792
|
-
const client = new GitHubIssueClient(resolveAgentRoute(this.config, agentId), this.api.logger);
|
|
1793
|
-
const repo = await client.getRepoInfo();
|
|
1794
|
-
// Only auto-name if description is still empty or a default placeholder.
|
|
1795
|
-
if (repo.description && repo.description !== "My Memory Space" && repo.description !== "我的记忆空间" && repo.description !== "マイメモリースペース") return;
|
|
1796
|
-
// Use the conversation title or summary as a lightweight description.
|
|
1797
|
-
await client.updateRepoDescription(snippet);
|
|
1798
|
-
this.api.logger.info?.(`clawmem: auto-named repo to "${snippet}"`);
|
|
1799
|
-
} catch (e) {
|
|
1800
|
-
this.api.logger.warn(`clawmem: auto-name repo failed: ${String(e)}`);
|
|
1801
|
-
}
|
|
1802
|
-
})();
|
|
1803
|
-
}
|
|
1804
2294
|
private warn(scope: string, error: unknown): void { this.api.logger.warn(`clawmem: ${scope} failed: ${String(error)}`); }
|
|
1805
2295
|
}
|
|
1806
2296
|
|
|
@@ -1862,6 +2352,43 @@ export function buildAutoRecallContext(memories: Array<{
|
|
|
1862
2352
|
].join("\n");
|
|
1863
2353
|
}
|
|
1864
2354
|
|
|
2355
|
+
export function buildClawMemPromptSection(params: MemoryPromptBuilderParams): string[] {
|
|
2356
|
+
const hasTool = (name: string) => params.availableTools.has(name);
|
|
2357
|
+
const retrievalTools = [
|
|
2358
|
+
hasTool("memory_recall") ? "`memory_recall`" : "",
|
|
2359
|
+
hasTool("memory_list") ? "`memory_list`" : "",
|
|
2360
|
+
hasTool("memory_get") ? "`memory_get`" : "",
|
|
2361
|
+
].filter(Boolean);
|
|
2362
|
+
const routingTools = [hasTool("memory_repos") ? "`memory_repos`" : ""].filter(Boolean);
|
|
2363
|
+
const schemaTools = [hasTool("memory_labels") ? "`memory_labels`" : ""].filter(Boolean);
|
|
2364
|
+
const writeTools = [
|
|
2365
|
+
hasTool("memory_store") ? "`memory_store`" : "",
|
|
2366
|
+
hasTool("memory_update") ? "`memory_update`" : "",
|
|
2367
|
+
].filter(Boolean);
|
|
2368
|
+
const hasForgetTool = hasTool("memory_forget");
|
|
2369
|
+
|
|
2370
|
+
const lines = [
|
|
2371
|
+
"## ClawMem",
|
|
2372
|
+
"ClawMem is the active long-term memory system for this OpenClaw installation.",
|
|
2373
|
+
"Core loop:",
|
|
2374
|
+
"- Before answering, ask whether prior memory could improve the answer. Default to yes for preferences, project history, decisions, lessons, workflows, conventions, recurring problems, and active tasks.",
|
|
2375
|
+
`- Treat auto-injected ClawMem context as a hint, not proof of absence.${retrievalTools.length > 0 ? ` If relevant memory may exist and the hint is weak or missing, retrieve explicitly with ${joinNaturalLanguageList(retrievalTools)}.` : ""}`,
|
|
2376
|
+
`${routingTools.length > 0 ? `- Before explicit memory work, choose the right repo. If unclear, inspect ${joinNaturalLanguageList(routingTools)} and then fall back to the agent's default repo.` : "- Before explicit memory work, choose the right repo instead of assuming every memory belongs in the default repo."}`,
|
|
2377
|
+
`${writeTools.length > 0 || hasForgetTool
|
|
2378
|
+
? `- After answering, ask whether this turn created durable knowledge. If yes or unsure, write it now${writeTools.length > 0 ? ` with ${joinNaturalLanguageList(writeTools)}` : ""}${hasForgetTool ? `${writeTools.length > 0 ? "; " : " "}use \`memory_forget\` for stale or superseded memories` : ""} instead of waiting for background extraction.`
|
|
2379
|
+
: "- After answering, ask whether this turn created durable knowledge and save it immediately instead of waiting for background extraction."}`,
|
|
2380
|
+
"- Store one durable fact per memory. Skip temporary requests, tool chatter, startup boilerplate, and bundled summaries of unrelated facts.",
|
|
2381
|
+
"- Prefer an explicit short `title` plus a fuller `detail`. Write new human-readable memory text in the user's current language and keep structural labels machine-readable.",
|
|
2382
|
+
`${schemaTools.length > 0
|
|
2383
|
+
? `- Reuse existing \`kind\` and \`topic\` labels by checking ${joinNaturalLanguageList(schemaTools)} first. If no current label fits, create one short stable machine-readable label instead of a translated or near-duplicate variant.`
|
|
2384
|
+
: "- Reuse existing `kind` and `topic` labels before inventing new ones. If no current label fits, create one short stable machine-readable label instead of a translated or near-duplicate variant."}`,
|
|
2385
|
+
"- Use the bundled `clawmem` skill for detailed routing, schema, collaboration, communication, and manual repo-backed workflows.",
|
|
2386
|
+
"",
|
|
2387
|
+
];
|
|
2388
|
+
|
|
2389
|
+
return lines;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
1865
2392
|
export function extractPromptTextForRecall(event: unknown): string | undefined {
|
|
1866
2393
|
const direct = normalizePromptText(event);
|
|
1867
2394
|
if (direct) return direct;
|
|
@@ -1882,6 +2409,13 @@ export function extractPromptTextForRecall(event: unknown): string | undefined {
|
|
|
1882
2409
|
?? promptCandidates.find((candidate) => candidate.text)?.text;
|
|
1883
2410
|
}
|
|
1884
2411
|
|
|
2412
|
+
function joinNaturalLanguageList(items: string[]): string {
|
|
2413
|
+
if (items.length === 0) return "";
|
|
2414
|
+
if (items.length === 1) return items[0]!;
|
|
2415
|
+
if (items.length === 2) return `${items[0]} and ${items[1]}`;
|
|
2416
|
+
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
1885
2419
|
function extractPromptTextFromMessages(value: unknown): string | undefined {
|
|
1886
2420
|
if (!Array.isArray(value)) return undefined;
|
|
1887
2421
|
let fallback: string | undefined;
|
|
@@ -1946,6 +2480,23 @@ function candidatePromptText(value: unknown): { text?: string; changed: boolean
|
|
|
1946
2480
|
return { changed: false };
|
|
1947
2481
|
}
|
|
1948
2482
|
|
|
2483
|
+
function getCachedFinalArtifacts(
|
|
2484
|
+
session: SessionMirrorState,
|
|
2485
|
+
targetCursor: number,
|
|
2486
|
+
): { summary: string; title?: string; candidates: MemoryCandidate[] } | null {
|
|
2487
|
+
const derived = session.derived;
|
|
2488
|
+
if (!derived) return null;
|
|
2489
|
+
const summary = derived.summary.text?.trim();
|
|
2490
|
+
if (!summary || derived.summary.basedOnCursor < targetCursor) return null;
|
|
2491
|
+
const hasCachedMemory = Array.isArray(derived.memory.candidates) || derived.memory.capturedCursor >= targetCursor;
|
|
2492
|
+
if (!hasCachedMemory) return null;
|
|
2493
|
+
return {
|
|
2494
|
+
summary,
|
|
2495
|
+
...(derived.summary.title?.trim() ? { title: derived.summary.title.trim() } : {}),
|
|
2496
|
+
candidates: Array.isArray(derived.memory.candidates) ? derived.memory.candidates : [],
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
1949
2500
|
export function resolvePromptHookMode(api: Pick<OpenClawPluginApi, "runtime">): PromptHookMode {
|
|
1950
2501
|
const hostVersion = resolveOpenClawHostVersion(api);
|
|
1951
2502
|
if (!hostVersion) return "legacy";
|
|
@@ -1957,13 +2508,8 @@ export function resolvePromptHookMode(api: Pick<OpenClawPluginApi, "runtime">):
|
|
|
1957
2508
|
export function resolveOpenClawHostVersion(api: Pick<OpenClawPluginApi, "runtime">): string | undefined {
|
|
1958
2509
|
const runtimeVersion = typeof api.runtime?.version === "string" ? api.runtime.version.trim() : "";
|
|
1959
2510
|
if (isUsableOpenClawVersion(runtimeVersion)) return runtimeVersion;
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
process.env.OPENCLAW_SERVICE_VERSION,
|
|
1963
|
-
]) {
|
|
1964
|
-
const trimmed = candidate?.trim();
|
|
1965
|
-
if (isUsableOpenClawVersion(trimmed)) return trimmed;
|
|
1966
|
-
}
|
|
2511
|
+
const envVersion = getOpenClawHostVersionFromEnv();
|
|
2512
|
+
if (isUsableOpenClawVersion(envVersion)) return envVersion;
|
|
1967
2513
|
return undefined;
|
|
1968
2514
|
}
|
|
1969
2515
|
|
|
@@ -2121,6 +2667,20 @@ function renderUserOrganizationInvitationLine(invitation: { id?: number; role?:
|
|
|
2121
2667
|
return `${org} [role:${role}${idText}${created}${expires}${teamsText}${inviter}]`;
|
|
2122
2668
|
}
|
|
2123
2669
|
|
|
2670
|
+
function renderOrganizationMembershipLine(membership: {
|
|
2671
|
+
state?: string;
|
|
2672
|
+
role?: string;
|
|
2673
|
+
organization?: { login?: string };
|
|
2674
|
+
user?: { login?: string; name?: string };
|
|
2675
|
+
}): string {
|
|
2676
|
+
const login = membership.user?.login?.trim() || membership.user?.name?.trim() || "unknown-user";
|
|
2677
|
+
const name = membership.user?.name?.trim() && membership.user?.name?.trim() !== login ? ` (${membership.user.name.trim()})` : "";
|
|
2678
|
+
const state = membership.state?.trim() || "unknown";
|
|
2679
|
+
const role = membership.role?.trim() || "unknown";
|
|
2680
|
+
const org = membership.organization?.login?.trim();
|
|
2681
|
+
return `${login}${name} [state:${state} role:${role}${org ? ` org:${org}` : ""}]`;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2124
2684
|
function canonicalPermission(permissions?: Record<string, boolean | undefined>, explicit?: string): string {
|
|
2125
2685
|
const direct = normalizePermissionAlias(explicit);
|
|
2126
2686
|
if (direct) return direct;
|
|
@@ -2142,4 +2702,9 @@ function normalizePermissionAlias(value: unknown): "none" | CollaborationPermiss
|
|
|
2142
2702
|
return undefined;
|
|
2143
2703
|
}
|
|
2144
2704
|
|
|
2705
|
+
function isHttpStatusError(error: unknown, status: number): boolean {
|
|
2706
|
+
const value = String(error);
|
|
2707
|
+
return value.includes(`HTTP ${status}:`);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2145
2710
|
export function createClawMemPlugin(api: OpenClawPluginApi): void { new ClawMemService(api).register(); }
|