@clawmem-ai/clawmem 0.1.16 → 0.1.18
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 -7
- package/openclaw.plugin.json +4 -31
- package/package.json +12 -5
- package/skills/clawmem/SKILL.md +5 -5
- package/skills/clawmem/references/collaboration.md +43 -1
- package/skills/clawmem/references/schema.md +2 -1
- package/src/config.test.ts +0 -3
- package/src/config.ts +0 -3
- package/src/conversation.test.ts +63 -13
- package/src/conversation.ts +89 -392
- package/src/github-client.test.ts +101 -0
- package/src/github-client.ts +59 -0
- package/src/memory.test.ts +131 -46
- package/src/memory.ts +81 -392
- package/src/service.test.ts +188 -0
- package/src/service.ts +804 -419
- package/src/state.test.ts +47 -16
- package/src/state.ts +87 -119
- package/src/types.ts +9 -26
- package/tsconfig.json +1 -0
package/src/service.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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";
|
|
6
6
|
import { GitHubIssueClient } from "./github-client.js";
|
|
7
7
|
import { KeyedAsyncQueue } from "./keyed-async-queue.js";
|
|
8
|
-
import { MemoryStore
|
|
8
|
+
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, SessionDerivedState, 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
14
|
import { getOpenClawAgentIdFromEnv, getOpenClawHostVersionFromEnv } from "./runtime-env.js";
|
|
15
15
|
|
|
@@ -17,25 +17,37 @@ type TurnPayload = { sessionId?: string; sessionKey?: string; agentId?: string;
|
|
|
17
17
|
type FinalizePayload = { sessionId?: string; sessionKey?: string; sessionFile?: string; agentId?: string; reason?: string; messages?: unknown[] };
|
|
18
18
|
type CollaborationPermission = "read" | "write" | "admin";
|
|
19
19
|
type CollaborationTeamRole = "member" | "maintainer";
|
|
20
|
+
type MemoryPromptBuilder = NonNullable<MemoryPluginCapability["promptBuilder"]>;
|
|
21
|
+
type MemoryPromptBuilderParams = Parameters<MemoryPromptBuilder>[0];
|
|
22
|
+
type PromptBuildInjection = { prependContext?: string; prependSystemContext?: string };
|
|
20
23
|
|
|
21
|
-
const DERIVED_WORK_RECOVERY_DELAYS_MS = [5000, 30000, 120000] as const;
|
|
22
24
|
const MODERN_PROMPT_HOOK_MIN_HOST_VERSION = "2026.3.7";
|
|
25
|
+
const MEMORY_PROMPT_REGISTRATION_MIN_HOST_VERSION = "2026.3.22";
|
|
26
|
+
const CLAWMEM_PROMPT_GUIDANCE_TOOL_NAMES = [
|
|
27
|
+
"memory_repos",
|
|
28
|
+
"memory_repo_create",
|
|
29
|
+
"memory_list",
|
|
30
|
+
"memory_labels",
|
|
31
|
+
"memory_recall",
|
|
32
|
+
"memory_get",
|
|
33
|
+
"memory_store",
|
|
34
|
+
"memory_update",
|
|
35
|
+
"memory_forget",
|
|
36
|
+
] as const;
|
|
23
37
|
type PromptHookMode = "modern" | "legacy";
|
|
24
38
|
|
|
25
39
|
class ClawMemService {
|
|
26
40
|
private readonly config: ClawMemPluginConfig;
|
|
27
41
|
private readonly ioQueue = new KeyedAsyncQueue();
|
|
28
|
-
private readonly deriveQueue = new KeyedAsyncQueue();
|
|
29
42
|
private readonly repoWriteQueue = new KeyedAsyncQueue();
|
|
30
43
|
private readonly stateQueue = new KeyedAsyncQueue();
|
|
31
44
|
private readonly pending = new Set<Promise<unknown>>();
|
|
32
|
-
private readonly syncTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
33
|
-
private readonly recoveryTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
34
45
|
private statePath = "";
|
|
35
|
-
private state: PluginState = { version:
|
|
46
|
+
private state: PluginState = { version: 4, sessions: {} };
|
|
36
47
|
private unsubTranscript?: () => void;
|
|
37
48
|
private loadPromise: Promise<void> | null = null;
|
|
38
49
|
private readonly configPromises = new Map<string, Promise<boolean>>();
|
|
50
|
+
private injectPromptGuidanceViaSystemContext = false;
|
|
39
51
|
|
|
40
52
|
constructor(private readonly api: OpenClawPluginApi) {
|
|
41
53
|
this.config = resolvePluginConfig(api);
|
|
@@ -43,14 +55,33 @@ class ClawMemService {
|
|
|
43
55
|
|
|
44
56
|
register(): void {
|
|
45
57
|
const promptHookMode = resolvePromptHookMode(this.api);
|
|
58
|
+
this.registerMemoryPromptGuidance(promptHookMode);
|
|
46
59
|
if (promptHookMode === "modern") {
|
|
47
60
|
this.api.on("before_prompt_build", async (ev, ctx) => this.handleBeforePromptBuild(ev, ctx.agentId));
|
|
48
61
|
} else {
|
|
49
62
|
this.api.on("before_agent_start", async (ev, ctx) => this.handleBeforeAgentStart(ev, ctx.agentId));
|
|
50
63
|
}
|
|
51
|
-
this.api.on("agent_end", (ev, ctx) =>
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
this.api.on("agent_end", async (ev, ctx) => {
|
|
65
|
+
try {
|
|
66
|
+
await this.handleAgentEnd({ sessionId: ctx.sessionId, sessionKey: ctx.sessionKey, agentId: ctx.agentId, messages: ev.messages });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.warn("turn sync", error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
this.api.on("before_reset", async (ev, ctx) => {
|
|
72
|
+
try {
|
|
73
|
+
await this.handleFinalize({ sessionId: ctx.sessionId, sessionKey: ctx.sessionKey, sessionFile: ev.sessionFile, agentId: ctx.agentId, reason: ev.reason, messages: ev.messages });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.warn("finalize", error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
this.api.on("session_end", async (ev, ctx) => {
|
|
79
|
+
try {
|
|
80
|
+
await this.handleFinalize({ sessionId: ev.sessionId ?? ctx.sessionId, sessionKey: ev.sessionKey ?? ctx.sessionKey, agentId: ctx.agentId, reason: "session_end" });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.warn("finalize", error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
54
85
|
this.registerTools();
|
|
55
86
|
|
|
56
87
|
this.api.registerService({
|
|
@@ -62,7 +93,6 @@ class ClawMemService {
|
|
|
62
93
|
this.unsubTranscript = this.api.runtime.events.onSessionTranscriptUpdate((u) => {
|
|
63
94
|
void this.track(this.handleTranscript(u.sessionFile)).catch((e) => this.warn("transcript update", e));
|
|
64
95
|
});
|
|
65
|
-
this.recoverDerivedWorkOnStart();
|
|
66
96
|
const configuredCount = Object.keys(this.config.agents).filter((agentId) => {
|
|
67
97
|
const route = resolveAgentRoute(this.config, agentId);
|
|
68
98
|
return isAgentConfigured(route) && hasDefaultRepo(route);
|
|
@@ -76,15 +106,74 @@ class ClawMemService {
|
|
|
76
106
|
},
|
|
77
107
|
stop: async () => {
|
|
78
108
|
this.unsubTranscript?.();
|
|
79
|
-
for (const t of this.syncTimers.values()) clearTimeout(t);
|
|
80
|
-
this.syncTimers.clear();
|
|
81
|
-
for (const t of this.recoveryTimers.values()) clearTimeout(t);
|
|
82
|
-
this.recoveryTimers.clear();
|
|
83
109
|
await Promise.allSettled([...this.pending]);
|
|
84
110
|
},
|
|
85
111
|
});
|
|
86
112
|
}
|
|
87
113
|
|
|
114
|
+
private registerMemoryPromptGuidance(promptHookMode: PromptHookMode): void {
|
|
115
|
+
if (!this.isSelectedMemoryPlugin()) return;
|
|
116
|
+
|
|
117
|
+
const api = this.api as OpenClawPluginApi & {
|
|
118
|
+
registerMemoryCapability?: OpenClawPluginApi["registerMemoryCapability"];
|
|
119
|
+
registerMemoryPromptSection?: OpenClawPluginApi["registerMemoryPromptSection"];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (typeof api.registerMemoryCapability === "function") {
|
|
123
|
+
api.registerMemoryCapability({ promptBuilder: buildClawMemPromptSection });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof api.registerMemoryPromptSection === "function") {
|
|
128
|
+
api.registerMemoryPromptSection(buildClawMemPromptSection);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const hostVersion = resolveOpenClawHostVersion(this.api);
|
|
133
|
+
const comparison = hostVersion ? compareOpenClawVersions(hostVersion, MEMORY_PROMPT_REGISTRATION_MIN_HOST_VERSION) : null;
|
|
134
|
+
if (promptHookMode === "modern") {
|
|
135
|
+
this.injectPromptGuidanceViaSystemContext = true;
|
|
136
|
+
if (comparison !== null && comparison < 0) {
|
|
137
|
+
this.api.logger.info?.(
|
|
138
|
+
`clawmem: OpenClaw ${hostVersion} predates memory prompt registration (requires ${MEMORY_PROMPT_REGISTRATION_MIN_HOST_VERSION}+); falling back to before_prompt_build prependSystemContext for always-on prompt guidance`,
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.api.logger.warn?.(
|
|
144
|
+
hostVersion
|
|
145
|
+
? `clawmem: OpenClaw ${hostVersion} does not expose memory prompt registration; falling back to before_prompt_build prependSystemContext for always-on prompt guidance`
|
|
146
|
+
: "clawmem: host does not expose memory prompt registration; falling back to before_prompt_build prependSystemContext for always-on prompt guidance",
|
|
147
|
+
);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (comparison !== null && comparison < 0) {
|
|
152
|
+
this.api.logger.info?.(
|
|
153
|
+
`clawmem: OpenClaw ${hostVersion} predates memory prompt registration and prompt-level system-context fallback; always-on prompt guidance is unavailable on this host`,
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.api.logger.warn?.(
|
|
159
|
+
hostVersion
|
|
160
|
+
? `clawmem: OpenClaw ${hostVersion} does not expose memory prompt registration; always-on prompt guidance is disabled`
|
|
161
|
+
: "clawmem: host does not expose memory prompt registration; always-on prompt guidance is disabled",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private isSelectedMemoryPlugin(): boolean {
|
|
166
|
+
try {
|
|
167
|
+
const root = this.api.runtime.config.loadConfig();
|
|
168
|
+
const plugins = asRecord(root.plugins);
|
|
169
|
+
const slots = asRecord(plugins.slots);
|
|
170
|
+
const slot = typeof slots.memory === "string" ? String(slots.memory).trim() : "";
|
|
171
|
+
return slot === this.api.id;
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
88
177
|
private registerTools(): void {
|
|
89
178
|
this.api.registerTool({
|
|
90
179
|
name: "memory_repos",
|
|
@@ -430,6 +519,7 @@ class ClawMemService {
|
|
|
430
519
|
return toolText(`Marked memory [${forgotten.memoryId}] stale in ${resolved.route.repo}: ${forgotten.detail}`);
|
|
431
520
|
},
|
|
432
521
|
});
|
|
522
|
+
|
|
433
523
|
this.registerCollaborationTools();
|
|
434
524
|
}
|
|
435
525
|
|
|
@@ -507,6 +597,147 @@ class ClawMemService {
|
|
|
507
597
|
},
|
|
508
598
|
});
|
|
509
599
|
|
|
600
|
+
this.api.registerTool({
|
|
601
|
+
name: "collaboration_org_members",
|
|
602
|
+
description: "List visible members in an organization, optionally filtering to admins only.",
|
|
603
|
+
required: true,
|
|
604
|
+
parameters: {
|
|
605
|
+
type: "object",
|
|
606
|
+
additionalProperties: false,
|
|
607
|
+
properties: {
|
|
608
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
609
|
+
role: { type: "string", enum: ["admin"], description: "Optional role filter. Use admin to show org owners only." },
|
|
610
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
611
|
+
},
|
|
612
|
+
required: ["org"],
|
|
613
|
+
},
|
|
614
|
+
execute: async (_id: string, params: unknown) => {
|
|
615
|
+
const p = asRecord(params);
|
|
616
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
617
|
+
if (!org) return toolText("org is empty.");
|
|
618
|
+
const role = p.role === "admin" ? "admin" : undefined;
|
|
619
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
620
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
621
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
622
|
+
try {
|
|
623
|
+
const members = await resolved.client.listOrgMembers(org, role);
|
|
624
|
+
if (members.length === 0) {
|
|
625
|
+
return toolText(role === "admin"
|
|
626
|
+
? `No org admins are visible in "${org}".`
|
|
627
|
+
: `No org members are visible in "${org}".`);
|
|
628
|
+
}
|
|
629
|
+
return toolText([
|
|
630
|
+
role === "admin" ? `Org admins in "${org}":` : `Org members in "${org}":`,
|
|
631
|
+
...members.map((member) => `- ${renderCollaboratorLine(member)}`),
|
|
632
|
+
].join("\n"));
|
|
633
|
+
} catch (error) {
|
|
634
|
+
return toolText(`Unable to list members for org "${org}": ${String(error)}`);
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
this.api.registerTool({
|
|
640
|
+
name: "collaboration_org_membership",
|
|
641
|
+
description: "Inspect one user's organization membership state, including active versus pending invitation state.",
|
|
642
|
+
required: true,
|
|
643
|
+
parameters: {
|
|
644
|
+
type: "object",
|
|
645
|
+
additionalProperties: false,
|
|
646
|
+
properties: {
|
|
647
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
648
|
+
username: { type: "string", minLength: 1, description: "Username to inspect." },
|
|
649
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
650
|
+
},
|
|
651
|
+
required: ["org", "username"],
|
|
652
|
+
},
|
|
653
|
+
execute: async (_id: string, params: unknown) => {
|
|
654
|
+
const p = asRecord(params);
|
|
655
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
656
|
+
const username = typeof p.username === "string" ? p.username.trim() : "";
|
|
657
|
+
if (!org || !username) return toolText("org and username are required.");
|
|
658
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
659
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
660
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
661
|
+
try {
|
|
662
|
+
const membership = await resolved.client.getOrgMembership(org, username);
|
|
663
|
+
return toolText(`Organization membership in "${org}":\n- ${renderOrganizationMembershipLine(membership)}`);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if (isHttpStatusError(error, 404)) {
|
|
666
|
+
return toolText(`No active or pending organization membership was found for ${username} in "${org}".`);
|
|
667
|
+
}
|
|
668
|
+
return toolText(`Unable to inspect organization membership for ${username} in "${org}": ${String(error)}`);
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
this.api.registerTool({
|
|
674
|
+
name: "collaboration_org_member_remove",
|
|
675
|
+
description: "Remove an active organization member. Requires confirmed=true after explicit user approval.",
|
|
676
|
+
required: true,
|
|
677
|
+
parameters: {
|
|
678
|
+
type: "object",
|
|
679
|
+
additionalProperties: false,
|
|
680
|
+
properties: {
|
|
681
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
682
|
+
username: { type: "string", minLength: 1, description: "Active org member to remove." },
|
|
683
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
684
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
685
|
+
},
|
|
686
|
+
required: ["org", "username"],
|
|
687
|
+
},
|
|
688
|
+
execute: async (_id: string, params: unknown) => {
|
|
689
|
+
const p = asRecord(params);
|
|
690
|
+
const blocked = this.requireMutationConfirmation(p, "remove an organization member");
|
|
691
|
+
if (blocked) return toolText(blocked);
|
|
692
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
693
|
+
const username = typeof p.username === "string" ? p.username.trim() : "";
|
|
694
|
+
if (!org || !username) return toolText("org and username are required.");
|
|
695
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
696
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
697
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
698
|
+
try {
|
|
699
|
+
await resolved.client.removeOrgMember(org, username);
|
|
700
|
+
return toolText(`Removed active organization member ${username} from "${org}". Server-side org-scoped team memberships were cleaned up as part of the removal.`);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
return toolText(`Unable to remove ${username} from org "${org}": ${String(error)}`);
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
this.api.registerTool({
|
|
708
|
+
name: "collaboration_org_membership_remove",
|
|
709
|
+
description: "Remove an active organization membership or revoke a pending org invitation for that user. Requires confirmed=true after explicit user approval.",
|
|
710
|
+
required: true,
|
|
711
|
+
parameters: {
|
|
712
|
+
type: "object",
|
|
713
|
+
additionalProperties: false,
|
|
714
|
+
properties: {
|
|
715
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
716
|
+
username: { type: "string", minLength: 1, description: "Username whose active membership or pending invite should be removed." },
|
|
717
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
718
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
719
|
+
},
|
|
720
|
+
required: ["org", "username"],
|
|
721
|
+
},
|
|
722
|
+
execute: async (_id: string, params: unknown) => {
|
|
723
|
+
const p = asRecord(params);
|
|
724
|
+
const blocked = this.requireMutationConfirmation(p, "remove an organization membership");
|
|
725
|
+
if (blocked) return toolText(blocked);
|
|
726
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
727
|
+
const username = typeof p.username === "string" ? p.username.trim() : "";
|
|
728
|
+
if (!org || !username) return toolText("org and username are required.");
|
|
729
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
730
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
731
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
732
|
+
try {
|
|
733
|
+
await resolved.client.removeOrgMembership(org, username);
|
|
734
|
+
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.`);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
return toolText(`Unable to remove organization membership state for ${username} in "${org}": ${String(error)}`);
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
|
|
510
741
|
this.api.registerTool({
|
|
511
742
|
name: "collaboration_teams",
|
|
512
743
|
description: "List teams in an organization before granting repo access or managing membership.",
|
|
@@ -540,6 +771,37 @@ class ClawMemService {
|
|
|
540
771
|
},
|
|
541
772
|
});
|
|
542
773
|
|
|
774
|
+
this.api.registerTool({
|
|
775
|
+
name: "collaboration_team",
|
|
776
|
+
description: "Inspect one organization team by slug.",
|
|
777
|
+
required: true,
|
|
778
|
+
parameters: {
|
|
779
|
+
type: "object",
|
|
780
|
+
additionalProperties: false,
|
|
781
|
+
properties: {
|
|
782
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
783
|
+
teamSlug: { type: "string", minLength: 1, description: "Team slug." },
|
|
784
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
785
|
+
},
|
|
786
|
+
required: ["org", "teamSlug"],
|
|
787
|
+
},
|
|
788
|
+
execute: async (_id: string, params: unknown) => {
|
|
789
|
+
const p = asRecord(params);
|
|
790
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
791
|
+
const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
|
|
792
|
+
if (!org || !teamSlug) return toolText("org and teamSlug are required.");
|
|
793
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
794
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
795
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
796
|
+
try {
|
|
797
|
+
const team = await resolved.client.getTeam(org, teamSlug);
|
|
798
|
+
return toolText(`Team in "${org}":\n- ${renderTeamLine(team)}`);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
return toolText(`Unable to inspect team ${org}/${teamSlug}: ${String(error)}`);
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
|
|
543
805
|
this.api.registerTool({
|
|
544
806
|
name: "collaboration_team_create",
|
|
545
807
|
description: "Create a team inside an organization. Requires confirmed=true after explicit user approval.",
|
|
@@ -581,6 +843,122 @@ class ClawMemService {
|
|
|
581
843
|
},
|
|
582
844
|
});
|
|
583
845
|
|
|
846
|
+
this.api.registerTool({
|
|
847
|
+
name: "collaboration_team_update",
|
|
848
|
+
description: "Update a team's name, description, or privacy. Requires confirmed=true after explicit user approval.",
|
|
849
|
+
required: true,
|
|
850
|
+
parameters: {
|
|
851
|
+
type: "object",
|
|
852
|
+
additionalProperties: false,
|
|
853
|
+
properties: {
|
|
854
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
855
|
+
teamSlug: { type: "string", minLength: 1, description: "Current team slug." },
|
|
856
|
+
name: { type: "string", minLength: 1, description: "Optional new team display name." },
|
|
857
|
+
description: { type: "string", minLength: 1, description: "Optional new team description." },
|
|
858
|
+
privacy: { type: "string", enum: ["closed", "secret"], description: "Optional team privacy." },
|
|
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, "update 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 name = typeof p.name === "string" && p.name.trim() ? p.name.trim() : undefined;
|
|
872
|
+
const description = typeof p.description === "string" && p.description.trim() ? p.description.trim() : undefined;
|
|
873
|
+
const privacy = p.privacy === "secret" ? "secret" : p.privacy === "closed" ? "closed" : undefined;
|
|
874
|
+
if (!name && !description && !privacy) {
|
|
875
|
+
return toolText("Provide at least one of name, description, or privacy when updating a team.");
|
|
876
|
+
}
|
|
877
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
878
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
879
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
880
|
+
try {
|
|
881
|
+
const updated = await resolved.client.updateTeam(org, teamSlug, {
|
|
882
|
+
...(name ? { name } : {}),
|
|
883
|
+
...(description ? { description } : {}),
|
|
884
|
+
...(privacy ? { privacy } : {}),
|
|
885
|
+
});
|
|
886
|
+
return toolText(`Updated team in "${org}": ${renderTeamLine(updated)}.`);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
return toolText(`Unable to update team ${org}/${teamSlug}: ${String(error)}`);
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
this.api.registerTool({
|
|
894
|
+
name: "collaboration_team_delete",
|
|
895
|
+
description: "Delete a team. Requires confirmed=true after explicit user approval.",
|
|
896
|
+
required: true,
|
|
897
|
+
parameters: {
|
|
898
|
+
type: "object",
|
|
899
|
+
additionalProperties: false,
|
|
900
|
+
properties: {
|
|
901
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
902
|
+
teamSlug: { type: "string", minLength: 1, description: "Team slug." },
|
|
903
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
904
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
905
|
+
},
|
|
906
|
+
required: ["org", "teamSlug"],
|
|
907
|
+
},
|
|
908
|
+
execute: async (_id: string, params: unknown) => {
|
|
909
|
+
const p = asRecord(params);
|
|
910
|
+
const blocked = this.requireMutationConfirmation(p, "delete a team");
|
|
911
|
+
if (blocked) return toolText(blocked);
|
|
912
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
913
|
+
const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
|
|
914
|
+
if (!org || !teamSlug) return toolText("org and teamSlug are required.");
|
|
915
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
916
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
917
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
918
|
+
try {
|
|
919
|
+
await resolved.client.deleteTeam(org, teamSlug);
|
|
920
|
+
return toolText(`Deleted team ${org}/${teamSlug}.`);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
return toolText(`Unable to delete team ${org}/${teamSlug}: ${String(error)}`);
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
this.api.registerTool({
|
|
928
|
+
name: "collaboration_team_members",
|
|
929
|
+
description: "List members of an organization team.",
|
|
930
|
+
required: true,
|
|
931
|
+
parameters: {
|
|
932
|
+
type: "object",
|
|
933
|
+
additionalProperties: false,
|
|
934
|
+
properties: {
|
|
935
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
936
|
+
teamSlug: { type: "string", minLength: 1, description: "Team slug." },
|
|
937
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
938
|
+
},
|
|
939
|
+
required: ["org", "teamSlug"],
|
|
940
|
+
},
|
|
941
|
+
execute: async (_id: string, params: unknown) => {
|
|
942
|
+
const p = asRecord(params);
|
|
943
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
944
|
+
const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
|
|
945
|
+
if (!org || !teamSlug) return toolText("org and teamSlug are required.");
|
|
946
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
947
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
948
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
949
|
+
try {
|
|
950
|
+
const members = await resolved.client.listTeamMembers(org, teamSlug);
|
|
951
|
+
if (members.length === 0) return toolText(`No members found in ${org}/${teamSlug}.`);
|
|
952
|
+
return toolText([
|
|
953
|
+
`Members of ${org}/${teamSlug}:`,
|
|
954
|
+
...members.map((member) => `- ${renderCollaboratorLine(member)}`),
|
|
955
|
+
].join("\n"));
|
|
956
|
+
} catch (error) {
|
|
957
|
+
return toolText(`Unable to list members for ${org}/${teamSlug}: ${String(error)}`);
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
|
|
584
962
|
this.api.registerTool({
|
|
585
963
|
name: "collaboration_team_membership_set",
|
|
586
964
|
description: "Add or update a user's membership in an organization team. Requires confirmed=true after explicit user approval.",
|
|
@@ -763,6 +1141,40 @@ class ClawMemService {
|
|
|
763
1141
|
},
|
|
764
1142
|
});
|
|
765
1143
|
|
|
1144
|
+
this.api.registerTool({
|
|
1145
|
+
name: "collaboration_repo_transfer",
|
|
1146
|
+
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.",
|
|
1147
|
+
required: true,
|
|
1148
|
+
parameters: {
|
|
1149
|
+
type: "object",
|
|
1150
|
+
additionalProperties: false,
|
|
1151
|
+
properties: {
|
|
1152
|
+
repo: { type: "string", minLength: 3, description: "Optional source repo in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
1153
|
+
newOwner: { type: "string", minLength: 1, description: "Destination owner login, often an organization login." },
|
|
1154
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
1155
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
1156
|
+
},
|
|
1157
|
+
required: ["newOwner"],
|
|
1158
|
+
},
|
|
1159
|
+
execute: async (_id: string, params: unknown) => {
|
|
1160
|
+
const p = asRecord(params);
|
|
1161
|
+
const blocked = this.requireMutationConfirmation(p, "transfer a repository");
|
|
1162
|
+
if (blocked) return toolText(blocked);
|
|
1163
|
+
const newOwner = typeof p.newOwner === "string" ? p.newOwner.trim() : "";
|
|
1164
|
+
if (!newOwner) return toolText("newOwner is empty.");
|
|
1165
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
1166
|
+
const target = await this.requireCollaborationRepo(agentId, p.repo);
|
|
1167
|
+
if ("error" in target) return toolText(target.error);
|
|
1168
|
+
try {
|
|
1169
|
+
const transferred = await target.client.transferRepo(target.owner, target.repo, newOwner);
|
|
1170
|
+
const nextFullName = repoSummaryFullName(transferred) || `${newOwner}/${target.repo}`;
|
|
1171
|
+
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.`);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
return toolText(`Unable to transfer ${target.fullName} to ${newOwner}: ${String(error)}`);
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
|
|
766
1178
|
this.api.registerTool({
|
|
767
1179
|
name: "collaboration_repo_collaborators",
|
|
768
1180
|
description: "List direct collaborators on a repo before changing repository-level access.",
|
|
@@ -1088,6 +1500,41 @@ class ClawMemService {
|
|
|
1088
1500
|
},
|
|
1089
1501
|
});
|
|
1090
1502
|
|
|
1503
|
+
this.api.registerTool({
|
|
1504
|
+
name: "collaboration_org_invitation_revoke",
|
|
1505
|
+
description: "Revoke a pending organization invitation from the org side. Requires confirmed=true after explicit user approval.",
|
|
1506
|
+
required: true,
|
|
1507
|
+
parameters: {
|
|
1508
|
+
type: "object",
|
|
1509
|
+
additionalProperties: false,
|
|
1510
|
+
properties: {
|
|
1511
|
+
org: { type: "string", minLength: 1, description: "Organization login." },
|
|
1512
|
+
invitationId: { type: "integer", minimum: 1, description: "Pending organization invitation id." },
|
|
1513
|
+
confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
|
|
1514
|
+
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
1515
|
+
},
|
|
1516
|
+
required: ["org", "invitationId"],
|
|
1517
|
+
},
|
|
1518
|
+
execute: async (_id: string, params: unknown) => {
|
|
1519
|
+
const p = asRecord(params);
|
|
1520
|
+
const blocked = this.requireMutationConfirmation(p, "revoke an organization invitation");
|
|
1521
|
+
if (blocked) return toolText(blocked);
|
|
1522
|
+
const org = typeof p.org === "string" ? p.org.trim() : "";
|
|
1523
|
+
if (!org) return toolText("org is empty.");
|
|
1524
|
+
const invitationId = this.resolvePositiveInteger(p.invitationId, "invitationId");
|
|
1525
|
+
if ("error" in invitationId) return toolText(invitationId.error);
|
|
1526
|
+
const agentId = this.resolveToolAgentId(p.agentId);
|
|
1527
|
+
const resolved = await this.requireToolIdentity(agentId);
|
|
1528
|
+
if ("error" in resolved) return toolText(resolved.error);
|
|
1529
|
+
try {
|
|
1530
|
+
await resolved.client.revokeOrgInvitation(org, invitationId.value);
|
|
1531
|
+
return toolText(`Revoked organization invitation ${invitationId.value} in "${org}".`);
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
return toolText(`Unable to revoke organization invitation ${invitationId.value} in "${org}": ${String(error)}`);
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1091
1538
|
this.api.registerTool({
|
|
1092
1539
|
name: "collaboration_user_org_invitations",
|
|
1093
1540
|
description: "List pending organization invitations for the current ClawMem identity before concluding that no shared org access is available.",
|
|
@@ -1234,6 +1681,7 @@ class ClawMemService {
|
|
|
1234
1681
|
additionalProperties: false,
|
|
1235
1682
|
properties: {
|
|
1236
1683
|
repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
|
|
1684
|
+
username: { type: "string", minLength: 1, description: "Optional username to inspect for org-level base permission and membership state on org-owned repos." },
|
|
1237
1685
|
agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
|
|
1238
1686
|
},
|
|
1239
1687
|
},
|
|
@@ -1246,7 +1694,10 @@ class ClawMemService {
|
|
|
1246
1694
|
try {
|
|
1247
1695
|
const lines = [`Repo access inspection for ${target.fullName}:`];
|
|
1248
1696
|
const notes: string[] = [];
|
|
1697
|
+
const username = typeof p.username === "string" ? p.username.trim() : "";
|
|
1249
1698
|
let orgName: string | undefined;
|
|
1699
|
+
let orgDefaultPermission: "none" | CollaborationPermission | undefined;
|
|
1700
|
+
let orgContextAvailable = false;
|
|
1250
1701
|
|
|
1251
1702
|
try {
|
|
1252
1703
|
const repo = await target.client.getRepo(target.owner, target.repo);
|
|
@@ -1259,12 +1710,49 @@ class ClawMemService {
|
|
|
1259
1710
|
}
|
|
1260
1711
|
|
|
1261
1712
|
try {
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1713
|
+
const ownerOrg = orgName || target.owner;
|
|
1714
|
+
const org = await target.client.getOrg(ownerOrg);
|
|
1715
|
+
orgContextAvailable = true;
|
|
1716
|
+
orgDefaultPermission = normalizePermissionAlias(org.default_repository_permission);
|
|
1717
|
+
lines.push(`- Org default repository permission: ${orgDefaultPermission || "unknown"}`);
|
|
1264
1718
|
} catch (error) {
|
|
1265
1719
|
notes.push(`Org metadata unavailable for "${orgName}": ${String(error)}`);
|
|
1266
1720
|
}
|
|
1267
1721
|
|
|
1722
|
+
if (username) {
|
|
1723
|
+
lines.push("");
|
|
1724
|
+
lines.push(`Org membership for "${username}" in "${orgName}":`);
|
|
1725
|
+
if (!orgName || !orgContextAvailable) {
|
|
1726
|
+
lines.push("- Not applicable because the owner org could not be resolved.");
|
|
1727
|
+
} else {
|
|
1728
|
+
try {
|
|
1729
|
+
const membership = await target.client.getOrgMembership(orgName, username);
|
|
1730
|
+
lines.push(`- ${renderOrganizationMembershipLine(membership)}`);
|
|
1731
|
+
if (membership.state === "active") {
|
|
1732
|
+
if (orgDefaultPermission && orgDefaultPermission !== "none") {
|
|
1733
|
+
lines.push(`- Org base repo access is active via default permission "${orgDefaultPermission}".`);
|
|
1734
|
+
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.`);
|
|
1735
|
+
} else {
|
|
1736
|
+
lines.push("- No org base repo access is visible because the org default permission is none.");
|
|
1737
|
+
}
|
|
1738
|
+
} else {
|
|
1739
|
+
lines.push("- Org base repo access is not active yet because the org membership is still pending.");
|
|
1740
|
+
}
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
if (isHttpStatusError(error, 404)) {
|
|
1743
|
+
lines.push("- No active or pending org membership was found.");
|
|
1744
|
+
if (orgDefaultPermission && orgDefaultPermission !== "none") {
|
|
1745
|
+
lines.push("- Org base repo access does not apply unless the user becomes an org member.");
|
|
1746
|
+
}
|
|
1747
|
+
} else {
|
|
1748
|
+
notes.push(`Org membership lookup failed for "${username}" in "${orgName}": ${String(error)}`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
} else if (orgDefaultPermission && orgDefaultPermission !== "none") {
|
|
1753
|
+
notes.push(`Any active org member can still inherit ${orgDefaultPermission} access from "${orgName}" even after direct collaborator or team grants are removed.`);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1268
1756
|
try {
|
|
1269
1757
|
const collaborators = filterDirectCollaborators(await target.client.listRepoCollaborators(target.owner, target.repo), target.owner);
|
|
1270
1758
|
lines.push("");
|
|
@@ -1299,7 +1787,8 @@ class ClawMemService {
|
|
|
1299
1787
|
}
|
|
1300
1788
|
|
|
1301
1789
|
try {
|
|
1302
|
-
const
|
|
1790
|
+
const ownerOrg = orgName || target.owner;
|
|
1791
|
+
const outside = await target.client.listOrgOutsideCollaborators(ownerOrg);
|
|
1303
1792
|
lines.push("");
|
|
1304
1793
|
lines.push(`Outside collaborators in owner org "${orgName}":`);
|
|
1305
1794
|
if (outside.length === 0) lines.push("- None visible");
|
|
@@ -1321,9 +1810,17 @@ class ClawMemService {
|
|
|
1321
1810
|
});
|
|
1322
1811
|
}
|
|
1323
1812
|
|
|
1324
|
-
private async handleBeforePromptBuild(event: unknown, agentId?: string): Promise<
|
|
1813
|
+
private async handleBeforePromptBuild(event: unknown, agentId?: string): Promise<PromptBuildInjection | void> {
|
|
1325
1814
|
const context = await this.collectAutoRecallContext(event, agentId);
|
|
1326
|
-
|
|
1815
|
+
const systemContext = this.injectPromptGuidanceViaSystemContext ? buildFallbackPromptGuidanceText(event) : undefined;
|
|
1816
|
+
// Auto-recall is per-turn dynamic context, so keep it out of the system prompt.
|
|
1817
|
+
// OpenClaw documents dynamic context on `prependContext`: https://github.com/maweibin/openclaw/blob/d9a2869ad69db9449336a2e2846bd9de0e647ac6/docs/concepts/agent-loop.md?plain=1#L85
|
|
1818
|
+
// Changing the system prompt can defeat provider prefix caching.
|
|
1819
|
+
if (!context && !systemContext) return undefined;
|
|
1820
|
+
return {
|
|
1821
|
+
...(systemContext ? { prependSystemContext: systemContext } : {}),
|
|
1822
|
+
...(context ? { prependContext: context } : {}),
|
|
1823
|
+
};
|
|
1327
1824
|
}
|
|
1328
1825
|
|
|
1329
1826
|
private async handleBeforeAgentStart(event: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
|
|
@@ -1331,6 +1828,16 @@ class ClawMemService {
|
|
|
1331
1828
|
return context ? { prependContext: context } : undefined;
|
|
1332
1829
|
}
|
|
1333
1830
|
|
|
1831
|
+
private async handleAgentEnd(payload: TurnPayload): Promise<void> {
|
|
1832
|
+
if (!payload.sessionId) return;
|
|
1833
|
+
await this.enqueueSessionIo(sessionScopeKey(payload.sessionId, payload.agentId), () => this.syncTurn(payload));
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
private async handleFinalize(payload: FinalizePayload): Promise<void> {
|
|
1837
|
+
if (!payload.sessionId) return;
|
|
1838
|
+
await this.enqueueSessionIo(sessionScopeKey(payload.sessionId, payload.agentId), () => this.finalize(payload));
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1334
1841
|
private async collectAutoRecallContext(event: unknown, agentId?: string): Promise<string | undefined> {
|
|
1335
1842
|
const routeAgentId = normalizeAgentId(agentId);
|
|
1336
1843
|
if (!(await this.ensureDefaultRepoConfigured(routeAgentId))) return undefined;
|
|
@@ -1369,26 +1876,13 @@ class ClawMemService {
|
|
|
1369
1876
|
});
|
|
1370
1877
|
}
|
|
1371
1878
|
|
|
1372
|
-
private scheduleTurn(p: TurnPayload): void {
|
|
1373
|
-
if (!p.sessionId) return;
|
|
1374
|
-
const scopeKey = sessionScopeKey(p.sessionId, p.agentId);
|
|
1375
|
-
const prev = this.syncTimers.get(scopeKey);
|
|
1376
|
-
if (prev) clearTimeout(prev);
|
|
1377
|
-
const timer = setTimeout(() => {
|
|
1378
|
-
this.syncTimers.delete(scopeKey);
|
|
1379
|
-
void this.track(this.enqueueSessionIo(scopeKey, () => this.syncTurn(p))).catch((e) => this.warn("turn sync", e));
|
|
1380
|
-
}, this.config.turnCommentDelayMs);
|
|
1381
|
-
timer.unref?.();
|
|
1382
|
-
this.syncTimers.set(scopeKey, timer);
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
1879
|
private async syncTurn(p: TurnPayload): Promise<void> {
|
|
1386
1880
|
if (!p.sessionId) return;
|
|
1387
1881
|
const agentId = normalizeAgentId(p.agentId);
|
|
1388
|
-
const scopeKey = sessionScopeKey(p.sessionId, agentId);
|
|
1389
1882
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1390
1883
|
const { conv } = this.getServices(agentId);
|
|
1391
1884
|
const s = this.getOrCreate(p.sessionId, agentId);
|
|
1885
|
+
if (s.finalizedAt) return;
|
|
1392
1886
|
s.sessionKey = p.sessionKey ?? s.sessionKey; s.agentId = agentId; s.updatedAt = new Date().toISOString();
|
|
1393
1887
|
const snap = await conv.loadSnapshot(s, p.messages);
|
|
1394
1888
|
if (!conv.shouldMirror(s.sessionId, snap.messages) || snap.messages.length === 0) { await this.persistState(); return; }
|
|
@@ -1396,43 +1890,183 @@ class ClawMemService {
|
|
|
1396
1890
|
await conv.syncLabels(s, snap, false);
|
|
1397
1891
|
const next = snap.messages.slice(s.lastMirroredCount);
|
|
1398
1892
|
if (next.length > 0) { const n = await conv.appendComments(s.issueNumber!, next); s.lastMirroredCount += n; s.turnCount += n; }
|
|
1399
|
-
this.markPostMirrorTasks(s);
|
|
1400
1893
|
await this.persistState();
|
|
1401
|
-
this.kickDerivedWork(scopeKey, agentId, "turn");
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
private enqueueFinalize(p: FinalizePayload): void {
|
|
1405
|
-
if (!p.sessionId) return;
|
|
1406
|
-
const scopeKey = sessionScopeKey(p.sessionId, p.agentId);
|
|
1407
|
-
const prev = this.syncTimers.get(scopeKey);
|
|
1408
|
-
if (prev) { clearTimeout(prev); this.syncTimers.delete(scopeKey); }
|
|
1409
|
-
void this.track(this.enqueueSessionIo(scopeKey, () => this.finalize(p))).catch((e) => this.warn("finalize", e));
|
|
1410
1894
|
}
|
|
1411
1895
|
|
|
1412
1896
|
private async finalize(p: FinalizePayload): Promise<void> {
|
|
1413
1897
|
if (!p.sessionId) return;
|
|
1414
1898
|
const agentId = normalizeAgentId(p.agentId);
|
|
1415
|
-
const scopeKey = sessionScopeKey(p.sessionId, agentId);
|
|
1416
1899
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1417
|
-
const { conv } = this.getServices(agentId);
|
|
1900
|
+
const { conv, mem, route } = this.getServices(agentId);
|
|
1418
1901
|
const s = this.getOrCreate(p.sessionId, agentId);
|
|
1419
|
-
if (s.finalizedAt) return;
|
|
1420
1902
|
s.sessionKey = p.sessionKey ?? s.sessionKey; s.sessionFile = p.sessionFile ?? s.sessionFile;
|
|
1421
1903
|
s.agentId = agentId; s.updatedAt = new Date().toISOString();
|
|
1422
1904
|
const snap = await conv.loadSnapshot(s, p.messages ?? []);
|
|
1423
1905
|
if (!conv.shouldMirror(s.sessionId, snap.messages)) { await this.persistState(); return; }
|
|
1424
1906
|
if (snap.messages.length === 0 && !s.issueNumber) { await this.persistState(); return; }
|
|
1425
|
-
await
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1907
|
+
await this.captureSessionFinalState(s, snap, conv, mem, route, { markFinalized: true, reason: "finalize" });
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
private async captureSessionFinalState(
|
|
1911
|
+
session: SessionMirrorState,
|
|
1912
|
+
snapshot: TranscriptSnapshot,
|
|
1913
|
+
conv: ConversationMirror,
|
|
1914
|
+
mem: MemoryStore,
|
|
1915
|
+
route: ClawMemResolvedRoute,
|
|
1916
|
+
options: { markFinalized: boolean; reason: string },
|
|
1917
|
+
): Promise<void> {
|
|
1918
|
+
await conv.ensureIssue(session, snapshot);
|
|
1919
|
+
const next = snapshot.messages.slice(session.lastMirroredCount);
|
|
1920
|
+
if (next.length > 0) {
|
|
1921
|
+
const n = await conv.appendComments(session.issueNumber!, next);
|
|
1922
|
+
session.lastMirroredCount += n;
|
|
1923
|
+
session.turnCount += n;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const derived = this.ensureDerived(session);
|
|
1927
|
+
let summaryText = derived.summary.text?.trim() || "pending";
|
|
1928
|
+
let titleOverride = derived.summary.title?.trim() || session.issueTitle;
|
|
1929
|
+
let generatedTitle = Boolean(derived.summary.title?.trim());
|
|
1930
|
+
const targetCursor = snapshot.messages.length;
|
|
1931
|
+
const meaningfulTranscript = snapshot.messages.filter((message) => message.text.trim()).length >= 2;
|
|
1932
|
+
|
|
1933
|
+
if (meaningfulTranscript) {
|
|
1934
|
+
try {
|
|
1935
|
+
const artifacts = await this.resolveFinalArtifacts(session, snapshot, conv, mem);
|
|
1936
|
+
summaryText = artifacts.summary;
|
|
1937
|
+
if (artifacts.title?.trim()) {
|
|
1938
|
+
titleOverride = artifacts.title.trim();
|
|
1939
|
+
generatedTitle = true;
|
|
1940
|
+
}
|
|
1941
|
+
const storedCount = await this.applyFinalMemoryCandidates(session, mem, route, targetCursor, artifacts.candidates);
|
|
1942
|
+
if (storedCount > 0) {
|
|
1943
|
+
this.api.logger.info?.(
|
|
1944
|
+
`clawmem: ${options.reason} stored ${storedCount} memor${storedCount === 1 ? "y" : "ies"} for ${session.sessionId}`,
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
derived.summary.status = "error";
|
|
1949
|
+
derived.summary.lastError = String(error);
|
|
1950
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1951
|
+
derived.memory.status = "error";
|
|
1952
|
+
derived.memory.lastError = String(error);
|
|
1953
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1954
|
+
this.warn(`${options.reason} derive for ${session.sessionId}`, error);
|
|
1955
|
+
}
|
|
1956
|
+
} else {
|
|
1957
|
+
derived.summary.status = "complete";
|
|
1958
|
+
derived.summary.basedOnCursor = targetCursor;
|
|
1959
|
+
derived.summary.lastError = undefined;
|
|
1960
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1961
|
+
derived.memory.capturedCursor = targetCursor;
|
|
1962
|
+
derived.memory.status = "complete";
|
|
1963
|
+
derived.memory.lastError = undefined;
|
|
1964
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
try {
|
|
1968
|
+
await conv.syncLabels(session, snapshot, true);
|
|
1969
|
+
await conv.syncBody(session, snapshot, summaryText, true, titleOverride);
|
|
1970
|
+
derived.summary.text = summaryText;
|
|
1971
|
+
derived.summary.basedOnCursor = targetCursor;
|
|
1972
|
+
derived.summary.status = "complete";
|
|
1973
|
+
derived.summary.lastError = undefined;
|
|
1974
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1975
|
+
if (titleOverride?.trim()) {
|
|
1976
|
+
derived.summary.title = titleOverride.trim();
|
|
1977
|
+
session.issueTitle = titleOverride.trim();
|
|
1978
|
+
if (generatedTitle) session.titleSource = "llm";
|
|
1979
|
+
}
|
|
1980
|
+
if (options.markFinalized && !session.finalizedAt) session.finalizedAt = new Date().toISOString();
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
derived.summary.status = "error";
|
|
1983
|
+
derived.summary.lastError = String(error);
|
|
1984
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1985
|
+
this.warn(`${options.reason} summary sync for ${session.sessionId}`, error);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
session.updatedAt = new Date().toISOString();
|
|
1434
1989
|
await this.persistState();
|
|
1435
|
-
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
private async resolveFinalArtifacts(
|
|
1993
|
+
session: SessionMirrorState,
|
|
1994
|
+
snapshot: TranscriptSnapshot,
|
|
1995
|
+
conv: ConversationMirror,
|
|
1996
|
+
mem: MemoryStore,
|
|
1997
|
+
): Promise<{ summary: string; title?: string; candidates: MemoryCandidate[] }> {
|
|
1998
|
+
const cached = getCachedFinalArtifacts(session, snapshot.messages.length);
|
|
1999
|
+
if (cached) return cached;
|
|
2000
|
+
|
|
2001
|
+
let schema;
|
|
2002
|
+
try {
|
|
2003
|
+
schema = await mem.listSchema();
|
|
2004
|
+
} catch (error) {
|
|
2005
|
+
this.warn(`finalize schema load for ${session.sessionId}`, error);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
const artifacts = await conv.generateFinalArtifacts(session, snapshot, schema);
|
|
2009
|
+
const derived = this.ensureDerived(session);
|
|
2010
|
+
const now = new Date().toISOString();
|
|
2011
|
+
derived.summary.text = artifacts.summary;
|
|
2012
|
+
derived.summary.title = artifacts.title?.trim() || undefined;
|
|
2013
|
+
derived.summary.basedOnCursor = snapshot.messages.length;
|
|
2014
|
+
derived.summary.lastError = undefined;
|
|
2015
|
+
derived.summary.updatedAt = now;
|
|
2016
|
+
derived.memory.candidates = artifacts.candidates;
|
|
2017
|
+
derived.memory.lastError = undefined;
|
|
2018
|
+
derived.memory.updatedAt = now;
|
|
2019
|
+
return artifacts;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
private async applyFinalMemoryCandidates(
|
|
2023
|
+
session: SessionMirrorState,
|
|
2024
|
+
mem: MemoryStore,
|
|
2025
|
+
route: ClawMemResolvedRoute,
|
|
2026
|
+
targetCursor: number,
|
|
2027
|
+
candidates: MemoryCandidate[],
|
|
2028
|
+
): Promise<number> {
|
|
2029
|
+
const derived = this.ensureDerived(session);
|
|
2030
|
+
if (derived.memory.capturedCursor >= targetCursor && derived.memory.status === "complete") {
|
|
2031
|
+
derived.memory.candidates = undefined;
|
|
2032
|
+
return 0;
|
|
2033
|
+
}
|
|
2034
|
+
if (candidates.length === 0) {
|
|
2035
|
+
derived.memory.candidates = undefined;
|
|
2036
|
+
derived.memory.capturedCursor = targetCursor;
|
|
2037
|
+
derived.memory.status = "complete";
|
|
2038
|
+
derived.memory.lastError = undefined;
|
|
2039
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
2040
|
+
return 0;
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
const result = await this.enqueueRepoWrite(this.repoWriteKey(route), async () => {
|
|
2044
|
+
let createdCount = 0;
|
|
2045
|
+
for (const candidate of candidates) {
|
|
2046
|
+
const stored = await mem.store({
|
|
2047
|
+
...(candidate.title ? { title: candidate.title } : {}),
|
|
2048
|
+
detail: candidate.detail,
|
|
2049
|
+
...(candidate.kind ? { kind: candidate.kind } : {}),
|
|
2050
|
+
...(candidate.topics?.length ? { topics: candidate.topics } : {}),
|
|
2051
|
+
});
|
|
2052
|
+
if (stored.created) createdCount++;
|
|
2053
|
+
}
|
|
2054
|
+
return createdCount;
|
|
2055
|
+
});
|
|
2056
|
+
derived.memory.candidates = undefined;
|
|
2057
|
+
derived.memory.capturedCursor = targetCursor;
|
|
2058
|
+
derived.memory.status = "complete";
|
|
2059
|
+
derived.memory.lastError = undefined;
|
|
2060
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
2061
|
+
return result;
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
derived.memory.candidates = candidates;
|
|
2064
|
+
derived.memory.status = "error";
|
|
2065
|
+
derived.memory.lastError = String(error);
|
|
2066
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
2067
|
+
this.warn(`finalize memory store for ${session.sessionId}`, error);
|
|
2068
|
+
return 0;
|
|
2069
|
+
}
|
|
1436
2070
|
}
|
|
1437
2071
|
|
|
1438
2072
|
// --- Infrastructure ---
|
|
@@ -1440,9 +2074,6 @@ class ClawMemService {
|
|
|
1440
2074
|
private enqueueSessionIo<T>(sessionId: string, task: () => Promise<T>): Promise<T> {
|
|
1441
2075
|
return this.ioQueue.enqueue(sessionId, async () => { await this.ensureLoaded(); return task(); });
|
|
1442
2076
|
}
|
|
1443
|
-
private enqueueSessionDerived<T>(sessionId: string, task: () => Promise<T>): Promise<T> {
|
|
1444
|
-
return this.deriveQueue.enqueue(sessionId, async () => { await this.ensureLoaded(); return task(); });
|
|
1445
|
-
}
|
|
1446
2077
|
private enqueueRepoWrite<T>(repoKey: string, task: () => Promise<T>): Promise<T> {
|
|
1447
2078
|
return this.repoWriteQueue.enqueue(repoKey, task);
|
|
1448
2079
|
}
|
|
@@ -1469,15 +2100,10 @@ class ClawMemService {
|
|
|
1469
2100
|
lastMirroredCount: 0,
|
|
1470
2101
|
turnCount: 0,
|
|
1471
2102
|
derived: {
|
|
1472
|
-
digest: { cursor: 0, status: "idle", attempt: 0 },
|
|
1473
2103
|
summary: { basedOnCursor: 0, status: "idle" },
|
|
1474
2104
|
memory: {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
extractStatus: "idle",
|
|
1478
|
-
reconcileStatus: "idle",
|
|
1479
|
-
attempt: 0,
|
|
1480
|
-
pendingCandidates: [],
|
|
2105
|
+
capturedCursor: 0,
|
|
2106
|
+
status: "idle",
|
|
1481
2107
|
},
|
|
1482
2108
|
},
|
|
1483
2109
|
createdAt: now,
|
|
@@ -1490,68 +2116,15 @@ class ClawMemService {
|
|
|
1490
2116
|
private ensureDerived(session: SessionMirrorState): SessionDerivedState {
|
|
1491
2117
|
if (!session.derived) {
|
|
1492
2118
|
session.derived = {
|
|
1493
|
-
digest: { cursor: 0, status: "idle", attempt: 0 },
|
|
1494
2119
|
summary: { basedOnCursor: 0, status: "idle" },
|
|
1495
2120
|
memory: {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
extractStatus: "idle",
|
|
1499
|
-
reconcileStatus: "idle",
|
|
1500
|
-
attempt: 0,
|
|
1501
|
-
pendingCandidates: [],
|
|
2121
|
+
capturedCursor: 0,
|
|
2122
|
+
status: "idle",
|
|
1502
2123
|
},
|
|
1503
2124
|
};
|
|
1504
2125
|
}
|
|
1505
2126
|
return session.derived;
|
|
1506
2127
|
}
|
|
1507
|
-
|
|
1508
|
-
private syncLegacyTaskFields(session: SessionMirrorState): void {
|
|
1509
|
-
const derived = this.ensureDerived(session);
|
|
1510
|
-
session.summaryStatus = derived.summary.status === "complete" ? "complete" : session.finalizedAt ? "pending" : undefined;
|
|
1511
|
-
session.lastMemorySyncCount = derived.memory.appliedCursor;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
private hasMeaningfulTranscript(session: SessionMirrorState): boolean {
|
|
1515
|
-
return Math.max(session.lastMirroredCount, session.turnCount) >= 2;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
private needsDigest(session: SessionMirrorState): boolean {
|
|
1519
|
-
if (!this.hasMeaningfulTranscript(session)) return false;
|
|
1520
|
-
const derived = this.ensureDerived(session);
|
|
1521
|
-
return derived.digest.cursor < session.lastMirroredCount;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
private needsFinalSummary(session: SessionMirrorState): boolean {
|
|
1525
|
-
if (!session.finalizedAt || !this.hasMeaningfulTranscript(session)) return false;
|
|
1526
|
-
const derived = this.ensureDerived(session);
|
|
1527
|
-
return derived.summary.status !== "complete" || derived.summary.basedOnCursor < session.lastMirroredCount;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
private needsMemoryExtract(session: SessionMirrorState): boolean {
|
|
1531
|
-
if (!this.hasMeaningfulTranscript(session)) return false;
|
|
1532
|
-
const derived = this.ensureDerived(session);
|
|
1533
|
-
return derived.memory.extractCursor < session.lastMirroredCount;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
private needsMemoryReconcile(session: SessionMirrorState): boolean {
|
|
1537
|
-
if (!this.hasMeaningfulTranscript(session)) return false;
|
|
1538
|
-
const derived = this.ensureDerived(session);
|
|
1539
|
-
return derived.memory.pendingCandidates.length > 0 || derived.memory.appliedCursor < derived.memory.extractCursor;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
private markPostMirrorTasks(session: SessionMirrorState): void {
|
|
1543
|
-
const derived = this.ensureDerived(session);
|
|
1544
|
-
if (this.needsDigest(session)) derived.digest.status = "pending";
|
|
1545
|
-
if (this.needsMemoryExtract(session)) derived.memory.extractStatus = "pending";
|
|
1546
|
-
if (this.needsMemoryReconcile(session)) derived.memory.reconcileStatus = "pending";
|
|
1547
|
-
this.syncLegacyTaskFields(session);
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
private markSummaryPending(session: SessionMirrorState): void {
|
|
1551
|
-
const derived = this.ensureDerived(session);
|
|
1552
|
-
derived.summary.status = "pending";
|
|
1553
|
-
this.syncLegacyTaskFields(session);
|
|
1554
|
-
}
|
|
1555
2128
|
private resolveTranscriptAgentId(sessionId: string, sessionFile: string): string | null {
|
|
1556
2129
|
const fromPath = inferAgentIdFromTranscriptPath(sessionFile);
|
|
1557
2130
|
if (fromPath) return fromPath;
|
|
@@ -1676,279 +2249,6 @@ class ClawMemService {
|
|
|
1676
2249
|
},
|
|
1677
2250
|
});
|
|
1678
2251
|
}
|
|
1679
|
-
private clearRecoveryTimer(scopeKey: string): void {
|
|
1680
|
-
const prev = this.recoveryTimers.get(scopeKey);
|
|
1681
|
-
if (!prev) return;
|
|
1682
|
-
clearTimeout(prev);
|
|
1683
|
-
this.recoveryTimers.delete(scopeKey);
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
private recoverDerivedWorkOnStart(): void {
|
|
1687
|
-
const recoverableSessions = Object.values(this.state.sessions)
|
|
1688
|
-
.filter((session) => this.sessionNeedsDerivedWork(session))
|
|
1689
|
-
.sort((a, b) => Date.parse(b.updatedAt ?? b.createdAt ?? "") - Date.parse(a.updatedAt ?? a.createdAt ?? ""));
|
|
1690
|
-
for (const session of recoverableSessions) {
|
|
1691
|
-
this.scheduleDerivedRecovery(sessionScopeKey(session.sessionId, session.agentId), normalizeAgentId(session.agentId), {
|
|
1692
|
-
delayMs: 0,
|
|
1693
|
-
attempt: 0,
|
|
1694
|
-
reason: "startup-recovery",
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
private kickDerivedWork(scopeKey: string, agentId: string, reason: string): void {
|
|
1700
|
-
this.clearRecoveryTimer(scopeKey);
|
|
1701
|
-
void this.track(this.enqueueSessionDerived(scopeKey, () => this.runSessionDerivedWork(scopeKey, agentId, 0, reason)))
|
|
1702
|
-
.catch((error) => this.warn(`derived work for ${scopeKey}`, error));
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
private scheduleDerivedRecovery(
|
|
1706
|
-
scopeKey: string,
|
|
1707
|
-
agentId: string,
|
|
1708
|
-
options: { delayMs?: number; attempt?: number; reason?: string } = {},
|
|
1709
|
-
): void {
|
|
1710
|
-
this.clearRecoveryTimer(scopeKey);
|
|
1711
|
-
const delayMs = Math.max(0, options.delayMs ?? 0);
|
|
1712
|
-
const attempt = Math.max(0, options.attempt ?? 0);
|
|
1713
|
-
const reason = options.reason ?? "scheduled-recovery";
|
|
1714
|
-
const timer = setTimeout(() => {
|
|
1715
|
-
this.recoveryTimers.delete(scopeKey);
|
|
1716
|
-
void this.track(this.enqueueSessionDerived(scopeKey, () => this.runSessionDerivedWork(scopeKey, agentId, attempt, reason)))
|
|
1717
|
-
.catch((error) => this.warn(`derived recovery for ${scopeKey}`, error));
|
|
1718
|
-
}, delayMs);
|
|
1719
|
-
timer.unref?.();
|
|
1720
|
-
this.recoveryTimers.set(scopeKey, timer);
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
private async runSessionDerivedWork(scopeKey: string, agentId: string, attempt: number, reason: string): Promise<void> {
|
|
1724
|
-
const session = this.state.sessions[scopeKey];
|
|
1725
|
-
if (!session || !this.sessionNeedsDerivedWork(session)) return;
|
|
1726
|
-
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1727
|
-
const { route, conv, mem, client } = this.getServices(agentId);
|
|
1728
|
-
const snap = await conv.loadSnapshot(session, []);
|
|
1729
|
-
if (!conv.shouldMirror(session.sessionId, snap.messages) || snap.messages.length === 0) return;
|
|
1730
|
-
|
|
1731
|
-
let retryNeeded = false;
|
|
1732
|
-
const derived = this.ensureDerived(session);
|
|
1733
|
-
const updateLegacyAndPersist = async (): Promise<void> => {
|
|
1734
|
-
this.syncLegacyTaskFields(session);
|
|
1735
|
-
await this.persistState();
|
|
1736
|
-
};
|
|
1737
|
-
const mirroredCount = Math.min(session.lastMirroredCount, snap.messages.length);
|
|
1738
|
-
if (mirroredCount <= 0) return;
|
|
1739
|
-
const canCombineDeltaDerivation = this.needsDigest(session)
|
|
1740
|
-
&& this.needsMemoryExtract(session)
|
|
1741
|
-
&& derived.digest.cursor === derived.memory.extractCursor;
|
|
1742
|
-
|
|
1743
|
-
if (canCombineDeltaDerivation) {
|
|
1744
|
-
const deriveTarget = mirroredCount;
|
|
1745
|
-
const deriveFromCursor = Math.min(derived.digest.cursor, deriveTarget);
|
|
1746
|
-
const deriveSnapshot: TranscriptSnapshot = { ...snap, messages: snap.messages.slice(0, deriveTarget) };
|
|
1747
|
-
const startedAt = new Date().toISOString();
|
|
1748
|
-
derived.digest.status = "running";
|
|
1749
|
-
derived.digest.updatedAt = startedAt;
|
|
1750
|
-
derived.memory.extractStatus = "running";
|
|
1751
|
-
derived.memory.updatedAt = startedAt;
|
|
1752
|
-
await updateLegacyAndPersist();
|
|
1753
|
-
try {
|
|
1754
|
-
const result = await conv.deriveDelta(session, deriveSnapshot, deriveFromCursor, derived.digest.text);
|
|
1755
|
-
derived.digest.text = result.digest.trim();
|
|
1756
|
-
derived.digest.title = result.title?.trim() || derived.digest.title;
|
|
1757
|
-
derived.digest.cursor = deriveTarget;
|
|
1758
|
-
derived.digest.status = deriveTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1759
|
-
derived.digest.attempt = 0;
|
|
1760
|
-
derived.digest.lastError = undefined;
|
|
1761
|
-
derived.digest.updatedAt = new Date().toISOString();
|
|
1762
|
-
if (result.title?.trim() && session.issueNumber) {
|
|
1763
|
-
await client.updateIssue(session.issueNumber, { title: result.title.trim() });
|
|
1764
|
-
session.issueTitle = result.title.trim();
|
|
1765
|
-
session.titleSource = "digest";
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
derived.memory.pendingCandidates = mergeMemoryCandidates(derived.memory.pendingCandidates, result.candidates);
|
|
1769
|
-
derived.memory.extractCursor = deriveTarget;
|
|
1770
|
-
derived.memory.extractStatus = deriveTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1771
|
-
derived.memory.attempt = 0;
|
|
1772
|
-
derived.memory.lastError = undefined;
|
|
1773
|
-
derived.memory.updatedAt = new Date().toISOString();
|
|
1774
|
-
if (derived.memory.pendingCandidates.length === 0) {
|
|
1775
|
-
derived.memory.appliedCursor = Math.max(derived.memory.appliedCursor, deriveTarget);
|
|
1776
|
-
derived.memory.reconcileStatus = "complete";
|
|
1777
|
-
} else {
|
|
1778
|
-
derived.memory.reconcileStatus = "pending";
|
|
1779
|
-
}
|
|
1780
|
-
} catch (error) {
|
|
1781
|
-
const failedAt = new Date().toISOString();
|
|
1782
|
-
derived.digest.status = "error";
|
|
1783
|
-
derived.digest.attempt += 1;
|
|
1784
|
-
derived.digest.lastError = String(error);
|
|
1785
|
-
derived.digest.updatedAt = failedAt;
|
|
1786
|
-
derived.memory.extractStatus = "error";
|
|
1787
|
-
derived.memory.attempt += 1;
|
|
1788
|
-
derived.memory.lastError = String(error);
|
|
1789
|
-
derived.memory.updatedAt = failedAt;
|
|
1790
|
-
retryNeeded = true;
|
|
1791
|
-
this.warn(`background derive delta for ${session.sessionId}`, error);
|
|
1792
|
-
}
|
|
1793
|
-
await updateLegacyAndPersist();
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
if (!canCombineDeltaDerivation && this.needsDigest(session)) {
|
|
1797
|
-
const digestTarget = mirroredCount;
|
|
1798
|
-
const digestFromCursor = Math.min(derived.digest.cursor, digestTarget);
|
|
1799
|
-
const digestSnapshot: TranscriptSnapshot = { ...snap, messages: snap.messages.slice(0, digestTarget) };
|
|
1800
|
-
derived.digest.status = "running";
|
|
1801
|
-
derived.digest.updatedAt = new Date().toISOString();
|
|
1802
|
-
await updateLegacyAndPersist();
|
|
1803
|
-
try {
|
|
1804
|
-
const result = await conv.generateRollingDigest(session, digestSnapshot, digestFromCursor, derived.digest.text);
|
|
1805
|
-
derived.digest.text = result.digest.trim();
|
|
1806
|
-
derived.digest.title = result.title?.trim() || derived.digest.title;
|
|
1807
|
-
derived.digest.cursor = digestTarget;
|
|
1808
|
-
derived.digest.status = digestTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1809
|
-
derived.digest.attempt = 0;
|
|
1810
|
-
derived.digest.lastError = undefined;
|
|
1811
|
-
derived.digest.updatedAt = new Date().toISOString();
|
|
1812
|
-
if (result.title?.trim() && session.issueNumber) {
|
|
1813
|
-
await client.updateIssue(session.issueNumber, { title: result.title.trim() });
|
|
1814
|
-
session.issueTitle = result.title.trim();
|
|
1815
|
-
session.titleSource = "digest";
|
|
1816
|
-
}
|
|
1817
|
-
} catch (error) {
|
|
1818
|
-
derived.digest.status = "error";
|
|
1819
|
-
derived.digest.attempt += 1;
|
|
1820
|
-
derived.digest.lastError = String(error);
|
|
1821
|
-
derived.digest.updatedAt = new Date().toISOString();
|
|
1822
|
-
retryNeeded = true;
|
|
1823
|
-
this.warn(`background digest sync for ${session.sessionId}`, error);
|
|
1824
|
-
}
|
|
1825
|
-
await updateLegacyAndPersist();
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
if (this.needsFinalSummary(session) && derived.digest.cursor >= session.lastMirroredCount) {
|
|
1829
|
-
const summaryTarget = Math.min(session.lastMirroredCount, snap.messages.length);
|
|
1830
|
-
const summarySnapshot: TranscriptSnapshot = { ...snap, messages: snap.messages.slice(0, summaryTarget) };
|
|
1831
|
-
derived.summary.status = "running";
|
|
1832
|
-
derived.summary.updatedAt = new Date().toISOString();
|
|
1833
|
-
await updateLegacyAndPersist();
|
|
1834
|
-
try {
|
|
1835
|
-
const result = await conv.generateFinalSummaryFromDigest(session, summarySnapshot, derived.digest.text ?? "");
|
|
1836
|
-
await conv.syncLabels(session, summarySnapshot, true);
|
|
1837
|
-
await conv.syncBody(session, summarySnapshot, result.summary, true, result.title);
|
|
1838
|
-
derived.summary.text = result.summary;
|
|
1839
|
-
derived.summary.status = summaryTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1840
|
-
derived.summary.basedOnCursor = summaryTarget;
|
|
1841
|
-
derived.summary.lastError = undefined;
|
|
1842
|
-
derived.summary.updatedAt = new Date().toISOString();
|
|
1843
|
-
if (result.title?.trim()) {
|
|
1844
|
-
session.issueTitle = result.title.trim();
|
|
1845
|
-
session.titleSource = "llm";
|
|
1846
|
-
}
|
|
1847
|
-
this.maybeAutoNameRepo(agentId, result.summary, result.title);
|
|
1848
|
-
} catch (error) {
|
|
1849
|
-
derived.summary.status = "error";
|
|
1850
|
-
derived.summary.lastError = String(error);
|
|
1851
|
-
derived.summary.updatedAt = new Date().toISOString();
|
|
1852
|
-
retryNeeded = true;
|
|
1853
|
-
this.warn(`background summary sync for ${session.sessionId}`, error);
|
|
1854
|
-
}
|
|
1855
|
-
await updateLegacyAndPersist();
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
if (!canCombineDeltaDerivation && this.needsMemoryExtract(session)) {
|
|
1859
|
-
const extractTarget = Math.min(session.lastMirroredCount, snap.messages.length);
|
|
1860
|
-
const extractFromCursor = Math.min(derived.memory.extractCursor, extractTarget);
|
|
1861
|
-
const extractSnapshot: TranscriptSnapshot = { ...snap, messages: snap.messages.slice(0, extractTarget) };
|
|
1862
|
-
derived.memory.extractStatus = "running";
|
|
1863
|
-
derived.memory.updatedAt = new Date().toISOString();
|
|
1864
|
-
await updateLegacyAndPersist();
|
|
1865
|
-
try {
|
|
1866
|
-
const candidates = await mem.extractCandidates(session, extractSnapshot, extractFromCursor, derived.digest.text);
|
|
1867
|
-
derived.memory.pendingCandidates = mergeMemoryCandidates(derived.memory.pendingCandidates, candidates);
|
|
1868
|
-
derived.memory.extractCursor = extractTarget;
|
|
1869
|
-
derived.memory.extractStatus = extractTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1870
|
-
derived.memory.attempt = 0;
|
|
1871
|
-
derived.memory.lastError = undefined;
|
|
1872
|
-
derived.memory.updatedAt = new Date().toISOString();
|
|
1873
|
-
if (derived.memory.pendingCandidates.length === 0) {
|
|
1874
|
-
derived.memory.appliedCursor = Math.max(derived.memory.appliedCursor, extractTarget);
|
|
1875
|
-
derived.memory.reconcileStatus = "complete";
|
|
1876
|
-
} else {
|
|
1877
|
-
derived.memory.reconcileStatus = "pending";
|
|
1878
|
-
}
|
|
1879
|
-
} catch (error) {
|
|
1880
|
-
derived.memory.extractStatus = "error";
|
|
1881
|
-
derived.memory.attempt += 1;
|
|
1882
|
-
derived.memory.lastError = String(error);
|
|
1883
|
-
derived.memory.updatedAt = new Date().toISOString();
|
|
1884
|
-
retryNeeded = true;
|
|
1885
|
-
this.warn(`background memory extract for ${session.sessionId}`, error);
|
|
1886
|
-
}
|
|
1887
|
-
await updateLegacyAndPersist();
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
if (this.needsMemoryReconcile(session)) {
|
|
1891
|
-
if (derived.memory.pendingCandidates.length === 0) {
|
|
1892
|
-
derived.memory.appliedCursor = derived.memory.extractCursor;
|
|
1893
|
-
derived.memory.reconcileStatus = "complete";
|
|
1894
|
-
derived.memory.updatedAt = new Date().toISOString();
|
|
1895
|
-
await updateLegacyAndPersist();
|
|
1896
|
-
} else {
|
|
1897
|
-
const candidates = mergeMemoryCandidates([], derived.memory.pendingCandidates);
|
|
1898
|
-
derived.memory.reconcileStatus = "running";
|
|
1899
|
-
derived.memory.updatedAt = new Date().toISOString();
|
|
1900
|
-
await updateLegacyAndPersist();
|
|
1901
|
-
try {
|
|
1902
|
-
const decision = await mem.reconcileCandidates(session, candidates);
|
|
1903
|
-
const { savedCount, staledCount } = await this.enqueueRepoWrite(
|
|
1904
|
-
this.repoWriteKey(route),
|
|
1905
|
-
() => mem.applyReconciledDecision(decision),
|
|
1906
|
-
);
|
|
1907
|
-
if (savedCount > 0 || staledCount > 0) {
|
|
1908
|
-
this.api.logger.info?.(
|
|
1909
|
-
`clawmem: synced memories for ${session.sessionId} (saved=${savedCount}, stale=${staledCount})`,
|
|
1910
|
-
);
|
|
1911
|
-
}
|
|
1912
|
-
const consumed = new Set(candidates.map((candidate) => candidate.candidateId));
|
|
1913
|
-
derived.memory.pendingCandidates = derived.memory.pendingCandidates.filter((candidate) => !consumed.has(candidate.candidateId));
|
|
1914
|
-
derived.memory.appliedCursor = derived.memory.extractCursor;
|
|
1915
|
-
derived.memory.reconcileStatus = derived.memory.pendingCandidates.length > 0 ? "pending" : "complete";
|
|
1916
|
-
derived.memory.attempt = 0;
|
|
1917
|
-
derived.memory.lastError = undefined;
|
|
1918
|
-
derived.memory.updatedAt = new Date().toISOString();
|
|
1919
|
-
} catch (error) {
|
|
1920
|
-
derived.memory.reconcileStatus = "error";
|
|
1921
|
-
derived.memory.attempt += 1;
|
|
1922
|
-
derived.memory.lastError = String(error);
|
|
1923
|
-
derived.memory.updatedAt = new Date().toISOString();
|
|
1924
|
-
retryNeeded = true;
|
|
1925
|
-
this.warn(`background memory reconcile for ${session.sessionId}`, error);
|
|
1926
|
-
}
|
|
1927
|
-
await updateLegacyAndPersist();
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
if (retryNeeded && this.sessionNeedsDerivedWork(session)) {
|
|
1932
|
-
const delayMs = DERIVED_WORK_RECOVERY_DELAYS_MS[Math.min(attempt, DERIVED_WORK_RECOVERY_DELAYS_MS.length - 1)] ?? 120000;
|
|
1933
|
-
this.api.logger.warn?.(
|
|
1934
|
-
`clawmem: derived work incomplete for ${session.sessionId}; retrying in ${Math.round(delayMs / 1000)}s (${reason})`,
|
|
1935
|
-
);
|
|
1936
|
-
this.scheduleDerivedRecovery(scopeKey, agentId, { delayMs, attempt: attempt + 1, reason: "retry" });
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
if (this.sessionNeedsDerivedWork(session)) {
|
|
1941
|
-
this.kickDerivedWork(scopeKey, agentId, "follow-up");
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
private sessionNeedsDerivedWork(session: SessionMirrorState): boolean {
|
|
1946
|
-
return this.needsDigest(session)
|
|
1947
|
-
|| this.needsFinalSummary(session)
|
|
1948
|
-
|| this.needsMemoryExtract(session)
|
|
1949
|
-
|| this.needsMemoryReconcile(session);
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
2252
|
private getServices(agentId?: string, repo?: string): { route: ClawMemResolvedRoute; conv: ConversationMirror; mem: MemoryStore; client: GitHubIssueClient } {
|
|
1953
2253
|
const route = resolveAgentRoute(this.config, agentId, repo);
|
|
1954
2254
|
const client = new GitHubIssueClient(route, this.api.logger);
|
|
@@ -1956,7 +2256,7 @@ class ClawMemService {
|
|
|
1956
2256
|
route,
|
|
1957
2257
|
client,
|
|
1958
2258
|
conv: new ConversationMirror(client, this.api, this.config),
|
|
1959
|
-
mem: new MemoryStore(client
|
|
2259
|
+
mem: new MemoryStore(client),
|
|
1960
2260
|
};
|
|
1961
2261
|
}
|
|
1962
2262
|
private resolveToolAgentId(agentId: unknown): string {
|
|
@@ -2040,28 +2340,6 @@ class ClawMemService {
|
|
|
2040
2340
|
}
|
|
2041
2341
|
return { value };
|
|
2042
2342
|
}
|
|
2043
|
-
/**
|
|
2044
|
-
* After finalization, check if the repo still has an empty/default description.
|
|
2045
|
-
* If so, use the conversation summary to suggest a meaningful name and update
|
|
2046
|
-
* the repo description automatically. Best-effort, fire-and-forget.
|
|
2047
|
-
*/
|
|
2048
|
-
private maybeAutoNameRepo(agentId: string, summary: string, title?: string): void {
|
|
2049
|
-
if (!summary || summary.startsWith("failed:") || summary === "pending") return;
|
|
2050
|
-
const snippet = title || summary.slice(0, 100);
|
|
2051
|
-
void (async () => {
|
|
2052
|
-
try {
|
|
2053
|
-
const client = new GitHubIssueClient(resolveAgentRoute(this.config, agentId), this.api.logger);
|
|
2054
|
-
const repo = await client.getRepoInfo();
|
|
2055
|
-
// Only auto-name if description is still empty or a default placeholder.
|
|
2056
|
-
if (repo.description && repo.description !== "My Memory Space" && repo.description !== "我的记忆空间" && repo.description !== "マイメモリースペース") return;
|
|
2057
|
-
// Use the conversation title or summary as a lightweight description.
|
|
2058
|
-
await client.updateRepoDescription(snippet);
|
|
2059
|
-
this.api.logger.info?.(`clawmem: auto-named repo to "${snippet}"`);
|
|
2060
|
-
} catch (e) {
|
|
2061
|
-
this.api.logger.warn(`clawmem: auto-name repo failed: ${String(e)}`);
|
|
2062
|
-
}
|
|
2063
|
-
})();
|
|
2064
|
-
}
|
|
2065
2343
|
private warn(scope: string, error: unknown): void { this.api.logger.warn(`clawmem: ${scope} failed: ${String(error)}`); }
|
|
2066
2344
|
}
|
|
2067
2345
|
|
|
@@ -2123,6 +2401,51 @@ export function buildAutoRecallContext(memories: Array<{
|
|
|
2123
2401
|
].join("\n");
|
|
2124
2402
|
}
|
|
2125
2403
|
|
|
2404
|
+
export function buildClawMemPromptSection(params: MemoryPromptBuilderParams): string[] {
|
|
2405
|
+
const hasTool = (name: string) => params.availableTools.has(name);
|
|
2406
|
+
const retrievalTools = [
|
|
2407
|
+
hasTool("memory_recall") ? "`memory_recall`" : "",
|
|
2408
|
+
hasTool("memory_list") ? "`memory_list`" : "",
|
|
2409
|
+
hasTool("memory_get") ? "`memory_get`" : "",
|
|
2410
|
+
].filter(Boolean);
|
|
2411
|
+
const routingTools = [hasTool("memory_repos") ? "`memory_repos`" : ""].filter(Boolean);
|
|
2412
|
+
const schemaTools = [hasTool("memory_labels") ? "`memory_labels`" : ""].filter(Boolean);
|
|
2413
|
+
const writeTools = [
|
|
2414
|
+
hasTool("memory_store") ? "`memory_store`" : "",
|
|
2415
|
+
hasTool("memory_update") ? "`memory_update`" : "",
|
|
2416
|
+
].filter(Boolean);
|
|
2417
|
+
const hasForgetTool = hasTool("memory_forget");
|
|
2418
|
+
|
|
2419
|
+
const lines = [
|
|
2420
|
+
"## ClawMem",
|
|
2421
|
+
"ClawMem is the active long-term memory system for this OpenClaw installation.",
|
|
2422
|
+
"Core loop:",
|
|
2423
|
+
"- 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.",
|
|
2424
|
+
`- 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)}.` : ""}`,
|
|
2425
|
+
`${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."}`,
|
|
2426
|
+
`${writeTools.length > 0 || hasForgetTool
|
|
2427
|
+
? `- 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.`
|
|
2428
|
+
: "- After answering, ask whether this turn created durable knowledge and save it immediately instead of waiting for background extraction."}`,
|
|
2429
|
+
"- Store one durable fact per memory. Skip temporary requests, tool chatter, startup boilerplate, and bundled summaries of unrelated facts.",
|
|
2430
|
+
"- 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.",
|
|
2431
|
+
`${schemaTools.length > 0
|
|
2432
|
+
? `- 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.`
|
|
2433
|
+
: "- 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."}`,
|
|
2434
|
+
"- Use the bundled `clawmem` skill for detailed routing, schema, collaboration, communication, and manual repo-backed workflows.",
|
|
2435
|
+
"",
|
|
2436
|
+
];
|
|
2437
|
+
|
|
2438
|
+
return lines;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function buildFallbackPromptGuidanceText(event: unknown): string | undefined {
|
|
2442
|
+
const record = asRecord(event);
|
|
2443
|
+
const availableTools = resolvePromptGuidanceAvailableTools(record.availableTools);
|
|
2444
|
+
const citationsMode = typeof record.citationsMode === "string" ? record.citationsMode.trim() || undefined : undefined;
|
|
2445
|
+
const text = buildClawMemPromptSection({ availableTools, ...(citationsMode ? { citationsMode } : {}) }).join("\n").trim();
|
|
2446
|
+
return text || undefined;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2126
2449
|
export function extractPromptTextForRecall(event: unknown): string | undefined {
|
|
2127
2450
|
const direct = normalizePromptText(event);
|
|
2128
2451
|
if (direct) return direct;
|
|
@@ -2143,6 +2466,32 @@ export function extractPromptTextForRecall(event: unknown): string | undefined {
|
|
|
2143
2466
|
?? promptCandidates.find((candidate) => candidate.text)?.text;
|
|
2144
2467
|
}
|
|
2145
2468
|
|
|
2469
|
+
function joinNaturalLanguageList(items: string[]): string {
|
|
2470
|
+
if (items.length === 0) return "";
|
|
2471
|
+
if (items.length === 1) return items[0]!;
|
|
2472
|
+
if (items.length === 2) return `${items[0]} and ${items[1]}`;
|
|
2473
|
+
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function resolvePromptGuidanceAvailableTools(value: unknown): Set<string> {
|
|
2477
|
+
const names = collectToolNames(value);
|
|
2478
|
+
return names.size > 0 ? names : new Set(CLAWMEM_PROMPT_GUIDANCE_TOOL_NAMES);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function collectToolNames(value: unknown): Set<string> {
|
|
2482
|
+
const names = new Set<string>();
|
|
2483
|
+
const values = value instanceof Set ? [...value] : Array.isArray(value) ? value : [];
|
|
2484
|
+
for (const entry of values) {
|
|
2485
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
2486
|
+
names.add(entry.trim());
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
const record = asRecord(entry);
|
|
2490
|
+
if (typeof record.name === "string" && record.name.trim()) names.add(record.name.trim());
|
|
2491
|
+
}
|
|
2492
|
+
return names;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2146
2495
|
function extractPromptTextFromMessages(value: unknown): string | undefined {
|
|
2147
2496
|
if (!Array.isArray(value)) return undefined;
|
|
2148
2497
|
let fallback: string | undefined;
|
|
@@ -2207,6 +2556,23 @@ function candidatePromptText(value: unknown): { text?: string; changed: boolean
|
|
|
2207
2556
|
return { changed: false };
|
|
2208
2557
|
}
|
|
2209
2558
|
|
|
2559
|
+
function getCachedFinalArtifacts(
|
|
2560
|
+
session: SessionMirrorState,
|
|
2561
|
+
targetCursor: number,
|
|
2562
|
+
): { summary: string; title?: string; candidates: MemoryCandidate[] } | null {
|
|
2563
|
+
const derived = session.derived;
|
|
2564
|
+
if (!derived) return null;
|
|
2565
|
+
const summary = derived.summary.text?.trim();
|
|
2566
|
+
if (!summary || derived.summary.basedOnCursor < targetCursor) return null;
|
|
2567
|
+
const hasCachedMemory = Array.isArray(derived.memory.candidates) || derived.memory.capturedCursor >= targetCursor;
|
|
2568
|
+
if (!hasCachedMemory) return null;
|
|
2569
|
+
return {
|
|
2570
|
+
summary,
|
|
2571
|
+
...(derived.summary.title?.trim() ? { title: derived.summary.title.trim() } : {}),
|
|
2572
|
+
candidates: Array.isArray(derived.memory.candidates) ? derived.memory.candidates : [],
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2210
2576
|
export function resolvePromptHookMode(api: Pick<OpenClawPluginApi, "runtime">): PromptHookMode {
|
|
2211
2577
|
const hostVersion = resolveOpenClawHostVersion(api);
|
|
2212
2578
|
if (!hostVersion) return "legacy";
|
|
@@ -2377,6 +2743,20 @@ function renderUserOrganizationInvitationLine(invitation: { id?: number; role?:
|
|
|
2377
2743
|
return `${org} [role:${role}${idText}${created}${expires}${teamsText}${inviter}]`;
|
|
2378
2744
|
}
|
|
2379
2745
|
|
|
2746
|
+
function renderOrganizationMembershipLine(membership: {
|
|
2747
|
+
state?: string;
|
|
2748
|
+
role?: string;
|
|
2749
|
+
organization?: { login?: string };
|
|
2750
|
+
user?: { login?: string; name?: string };
|
|
2751
|
+
}): string {
|
|
2752
|
+
const login = membership.user?.login?.trim() || membership.user?.name?.trim() || "unknown-user";
|
|
2753
|
+
const name = membership.user?.name?.trim() && membership.user?.name?.trim() !== login ? ` (${membership.user.name.trim()})` : "";
|
|
2754
|
+
const state = membership.state?.trim() || "unknown";
|
|
2755
|
+
const role = membership.role?.trim() || "unknown";
|
|
2756
|
+
const org = membership.organization?.login?.trim();
|
|
2757
|
+
return `${login}${name} [state:${state} role:${role}${org ? ` org:${org}` : ""}]`;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2380
2760
|
function canonicalPermission(permissions?: Record<string, boolean | undefined>, explicit?: string): string {
|
|
2381
2761
|
const direct = normalizePermissionAlias(explicit);
|
|
2382
2762
|
if (direct) return direct;
|
|
@@ -2398,4 +2778,9 @@ function normalizePermissionAlias(value: unknown): "none" | CollaborationPermiss
|
|
|
2398
2778
|
return undefined;
|
|
2399
2779
|
}
|
|
2400
2780
|
|
|
2781
|
+
function isHttpStatusError(error: unknown, status: number): boolean {
|
|
2782
|
+
const value = String(error);
|
|
2783
|
+
return value.includes(`HTTP ${status}:`);
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2401
2786
|
export function createClawMemPlugin(api: OpenClawPluginApi): void { new ClawMemService(api).register(); }
|