@echomem/mcp 1.4.33 → 1.4.34
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/dist/index.js +150 -16
- package/dist/package-metadata.js +9 -13
- package/dist/save-checkpoint-hook.js +2 -2
- package/dist/setup.js +4 -2
- package/dist/v1-contract.js +150 -5
- package/package.json +1 -1
- package/templates/codex-skills/echomem-save/SKILL.md +9 -9
- package/templates/echomem-recall.md +9 -9
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import axios from "axios";
|
|
6
6
|
import { ZodError } from "zod";
|
|
7
|
-
import { canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
|
|
7
|
+
import { canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, requestGroupSessionSharingSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
|
|
8
8
|
import { KeyStore } from "./keystore.js";
|
|
9
9
|
import { EventLogger, hashText } from "./events.js";
|
|
10
10
|
import { buildReportText } from "./report.js";
|
|
@@ -1013,7 +1013,12 @@ class EchoMemApiClient {
|
|
|
1013
1013
|
async recordMemoryCitations(args) {
|
|
1014
1014
|
const parsed = recordMemoryCitationsSchema.parse(args ?? {});
|
|
1015
1015
|
try {
|
|
1016
|
-
const response = await this.axios.post("/api/extension/social/memory-citations",
|
|
1016
|
+
const response = await this.axios.post("/api/extension/social/memory-citations",
|
|
1017
|
+
// A citation receipt belongs to one planned answer, not to the lifetime of the MCP stdio
|
|
1018
|
+
// process. Deriving the legacy API's sessionKey from the required per-answer receipt keeps
|
|
1019
|
+
// retries idempotent while avoiding the same process-vs-conversation coupling that broke
|
|
1020
|
+
// group sharing in long-lived Claude hosts.
|
|
1021
|
+
{ ...parsed, sessionKey: `answer:${parsed.receiptId}` });
|
|
1017
1022
|
return response.data;
|
|
1018
1023
|
}
|
|
1019
1024
|
catch (error) {
|
|
@@ -1320,6 +1325,8 @@ class EchoMemMCPServer {
|
|
|
1320
1325
|
return await this.handleGroupContext(request.params.arguments);
|
|
1321
1326
|
case canonicalToolNames.getGroupSessionSharing:
|
|
1322
1327
|
return await this.handleGetGroupSessionSharing(request.params.arguments);
|
|
1328
|
+
case canonicalToolNames.requestGroupSessionSharing:
|
|
1329
|
+
return await this.handleRequestGroupSessionSharing(request.params.arguments);
|
|
1323
1330
|
case canonicalToolNames.setGroupSessionSharing:
|
|
1324
1331
|
return await this.handleSetGroupSessionSharing(request.params.arguments);
|
|
1325
1332
|
case canonicalToolNames.createGroup:
|
|
@@ -1607,7 +1614,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1607
1614
|
syncedGroupNames.length
|
|
1608
1615
|
? `Saved privately first and synced to approved groups: ${syncedGroupNames.join(", ")}.`
|
|
1609
1616
|
: "Saved to your private memory.",
|
|
1610
|
-
`This user has multiple groups: ${availableGroups.map((group) => `${readString(group, "name") ?? "Unnamed group"} (${readString(group, "id") ?? "unknown id"})`).join(", ")}. Call
|
|
1617
|
+
`This user has multiple groups: ${availableGroups.map((group) => `${readString(group, "name") ?? "Unnamed group"} (${readString(group, "id") ?? "unknown id"})`).join(", ")}. Call request_group_session_sharing with this conversation scope and one groupId at a time so supported hosts render a separate sharing choice for each group.`,
|
|
1611
1618
|
failedSyncs.length
|
|
1612
1619
|
? `${failedSyncs.length} approved group sync(s) failed; private memories were preserved.`
|
|
1613
1620
|
: "",
|
|
@@ -1616,7 +1623,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1616
1623
|
: "",
|
|
1617
1624
|
].filter(Boolean).join(" ")
|
|
1618
1625
|
: sharing?.decision === null || sharing?.decision === undefined
|
|
1619
|
-
? `Saved to your private memory.
|
|
1626
|
+
? `Saved to your private memory. Call request_group_session_sharing with this conversation scope${readString(sharingGroup, "id") ? ` and groupId ${readString(sharingGroup, "id")}` : ""} so a supported host can show the sharing choice for ${readString(sharingGroup, "name") ?? "your current group"}. If the client cannot render it, relay that tool's fallback question. Decline, cancel, or silence leaves the state unset.`
|
|
1620
1627
|
: sharing.decision === "private"
|
|
1621
1628
|
? "Saved to your private memory."
|
|
1622
1629
|
: syncedGroupNames.length
|
|
@@ -1646,7 +1653,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1646
1653
|
`Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
|
|
1647
1654
|
receipt,
|
|
1648
1655
|
typeof groupSharingScopeId === "string" && groupSharingScopeId
|
|
1649
|
-
? `Conversation sharing scope: ${groupSharingScopeId}\nReuse this exact groupSharingScopeId only for get_group_session_sharing, set_group_session_sharing, and save_conversation calls in this conversation. Never reuse it in another conversation or save it as memory.`
|
|
1656
|
+
? `Conversation sharing scope: ${groupSharingScopeId}\nReuse this exact groupSharingScopeId only for get_group_session_sharing, request_group_session_sharing, set_group_session_sharing, and save_conversation calls in this conversation. Never reuse it in another conversation or save it as memory.`
|
|
1650
1657
|
: "",
|
|
1651
1658
|
typeof memoriesDiscarded === "number" && memoriesDiscarded > 0
|
|
1652
1659
|
? `${memoriesDiscarded} additional memories were not stored because your active-memory limit was reached.`
|
|
@@ -2090,7 +2097,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
2090
2097
|
return {
|
|
2091
2098
|
content: [{
|
|
2092
2099
|
type: "text",
|
|
2093
|
-
text: `${scopeText}\n\nThis user belongs to multiple groups: ${choices}. Reuse this same conversation scope and call
|
|
2100
|
+
text: `${scopeText}\n\nThis user belongs to multiple groups: ${choices}. Reuse this same conversation scope and call request_group_session_sharing once per groupId so supported hosts render a separate sharing choice for each group.`,
|
|
2094
2101
|
}],
|
|
2095
2102
|
};
|
|
2096
2103
|
}
|
|
@@ -2100,7 +2107,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
2100
2107
|
return {
|
|
2101
2108
|
content: [{
|
|
2102
2109
|
type: "text",
|
|
2103
|
-
text: `${scopeText}\n\nNo sharing decision exists for this conversation.
|
|
2110
|
+
text: `${scopeText}\n\nNo sharing decision exists for this conversation. Call request_group_session_sharing with the scope above${readString(group, "id") ? ` and groupId ${readString(group, "id")}` : ""}. Supported hosts will show a native Share with team / Keep private choice. If the client cannot render it, relay that tool's fallback question. Decline, cancel, or silence is not a No and leaves the state unset.`,
|
|
2104
2111
|
}],
|
|
2105
2112
|
};
|
|
2106
2113
|
}
|
|
@@ -2116,13 +2123,23 @@ Details: ${m.details || "N/A"}`;
|
|
|
2116
2123
|
}],
|
|
2117
2124
|
};
|
|
2118
2125
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2126
|
+
sharingScopeText(groupSharingScopeId) {
|
|
2127
|
+
return groupSharingScopeId
|
|
2128
|
+
? `Conversation sharing scope: ${groupSharingScopeId}\nReuse this exact groupSharingScopeId only in this conversation for later get/request/set/save calls. Never persist it as memory.`
|
|
2129
|
+
: "";
|
|
2130
|
+
}
|
|
2131
|
+
sharingFallbackText(scopeText, groupName) {
|
|
2132
|
+
return [
|
|
2133
|
+
scopeText,
|
|
2134
|
+
`This client cannot show EchoMem's sharing choice UI. Ask: “Share eligible memories saved from this conversation with ${groupName}?” Only an explicit Yes or No may be passed to set_group_session_sharing. Decline, cancel, or silence leaves the state unset.`,
|
|
2135
|
+
].filter(Boolean).join("\n\n");
|
|
2136
|
+
}
|
|
2137
|
+
formatGroupSessionSharingUpdate(share, payload, scopeText = "") {
|
|
2138
|
+
const data = isRecord(payload) ? payload : {};
|
|
2139
|
+
const sync = isRecord(data.sync) ? data.sync : null;
|
|
2123
2140
|
const protectedIds = Array.isArray(sync?.protectedMemoryIds) ? sync.protectedMemoryIds : [];
|
|
2124
|
-
const group = isRecord(
|
|
2125
|
-
const receipt =
|
|
2141
|
+
const group = isRecord(data.group) ? data.group : {};
|
|
2142
|
+
const receipt = share
|
|
2126
2143
|
? sync?.synced === false
|
|
2127
2144
|
? "Conversation sharing is enabled, but the initial group sync failed. Private memories were preserved; retry before claiming publication."
|
|
2128
2145
|
: `Conversation sharing is enabled for ${readString(group, "name") ?? "the current group"}. Existing eligible memories in this scope were synced and later saves carrying the same scope will sync automatically.`
|
|
@@ -2130,10 +2147,127 @@ Details: ${m.details || "N/A"}`;
|
|
|
2130
2147
|
return {
|
|
2131
2148
|
content: [{
|
|
2132
2149
|
type: "text",
|
|
2133
|
-
text:
|
|
2150
|
+
text: [
|
|
2151
|
+
scopeText,
|
|
2152
|
+
`${receipt}${protectedIds.length ? ` ${protectedIds.length} flagged ${protectedIds.length === 1 ? "memory was" : "memories were"} protected and kept private.` : ""}`,
|
|
2153
|
+
].filter(Boolean).join("\n\n"),
|
|
2134
2154
|
}],
|
|
2135
2155
|
};
|
|
2136
2156
|
}
|
|
2157
|
+
async handleRequestGroupSessionSharing(args) {
|
|
2158
|
+
const parsed = requestGroupSessionSharingSchema.parse(args ?? {});
|
|
2159
|
+
const payload = await this.client.getGroupSessionSharing(parsed);
|
|
2160
|
+
const groupSharingScopeId = readString(payload ?? {}, "groupSharingScopeId");
|
|
2161
|
+
const scopeText = this.sharingScopeText(groupSharingScopeId);
|
|
2162
|
+
if (!groupSharingScopeId) {
|
|
2163
|
+
return {
|
|
2164
|
+
content: [{
|
|
2165
|
+
type: "text",
|
|
2166
|
+
text: "EchoMem could not establish a conversation sharing scope, so no decision was recorded and the checkpoint remains private. Retry request_group_session_sharing before offering team sharing.",
|
|
2167
|
+
}],
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
if (payload?.hasGroup !== true) {
|
|
2171
|
+
return {
|
|
2172
|
+
content: [{
|
|
2173
|
+
type: "text",
|
|
2174
|
+
text: "No company group is configured for this user. Saves remain private; do not ask about conversation sharing.",
|
|
2175
|
+
}],
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
if (payload?.requiresGroupSelection === true) {
|
|
2179
|
+
const availableGroups = Array.isArray(payload?.availableGroups)
|
|
2180
|
+
? payload.availableGroups.filter(isRecord)
|
|
2181
|
+
: [];
|
|
2182
|
+
const choices = availableGroups
|
|
2183
|
+
.map((group) => `${readString(group, "name") ?? "Unnamed group"} (${readString(group, "id") ?? "unknown id"})`)
|
|
2184
|
+
.join(", ");
|
|
2185
|
+
return {
|
|
2186
|
+
content: [{
|
|
2187
|
+
type: "text",
|
|
2188
|
+
text: `${scopeText}\n\nChoose one group and call request_group_session_sharing again with the same scope and its groupId: ${choices}. Each group has an independent decision.`,
|
|
2189
|
+
}],
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
const group = isRecord(payload?.group) ? payload.group : {};
|
|
2193
|
+
const groupName = readString(group, "name") ?? "your group";
|
|
2194
|
+
const existingDecision = typeof payload?.decision === "string" ? payload.decision : null;
|
|
2195
|
+
if (existingDecision === "share" || existingDecision === "private") {
|
|
2196
|
+
return {
|
|
2197
|
+
content: [{
|
|
2198
|
+
type: "text",
|
|
2199
|
+
text: [
|
|
2200
|
+
scopeText,
|
|
2201
|
+
existingDecision === "share"
|
|
2202
|
+
? `This conversation is already approved for ${groupName}. Eligible memories sync automatically; flagged memories stay private.`
|
|
2203
|
+
: `This conversation is already private for ${groupName}. No sharing prompt was shown.`,
|
|
2204
|
+
].filter(Boolean).join("\n\n"),
|
|
2205
|
+
}],
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
const formElicitation = this.server.getClientCapabilities()?.elicitation?.form;
|
|
2209
|
+
if (!formElicitation) {
|
|
2210
|
+
return {
|
|
2211
|
+
content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
let elicitation;
|
|
2215
|
+
try {
|
|
2216
|
+
elicitation = await this.server.elicitInput({
|
|
2217
|
+
mode: "form",
|
|
2218
|
+
message: `EchoMem saved this checkpoint privately. Choose whether eligible memories from this conversation should also be shared with ${groupName}. Flagged memories always remain private.`,
|
|
2219
|
+
requestedSchema: {
|
|
2220
|
+
type: "object",
|
|
2221
|
+
properties: {
|
|
2222
|
+
sharingDecision: {
|
|
2223
|
+
type: "string",
|
|
2224
|
+
title: "Team sharing",
|
|
2225
|
+
description: `Choose whether eligible conversation memories may be shared with ${groupName}.`,
|
|
2226
|
+
oneOf: [
|
|
2227
|
+
{ const: "share", title: "Share with team" },
|
|
2228
|
+
{ const: "private", title: "Keep private" },
|
|
2229
|
+
],
|
|
2230
|
+
},
|
|
2231
|
+
},
|
|
2232
|
+
required: ["sharingDecision"],
|
|
2233
|
+
},
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
catch {
|
|
2237
|
+
return {
|
|
2238
|
+
content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
if (elicitation.action !== "accept") {
|
|
2242
|
+
return {
|
|
2243
|
+
content: [{
|
|
2244
|
+
type: "text",
|
|
2245
|
+
text: [
|
|
2246
|
+
scopeText,
|
|
2247
|
+
`No sharing decision was recorded for ${groupName}. The checkpoint remains private. Ask again at a later qualifying checkpoint; never interpret decline or cancel as No.`,
|
|
2248
|
+
].filter(Boolean).join("\n\n"),
|
|
2249
|
+
}],
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
const sharingDecision = readString(elicitation.content ?? {}, "sharingDecision");
|
|
2253
|
+
if (sharingDecision !== "share" && sharingDecision !== "private") {
|
|
2254
|
+
return {
|
|
2255
|
+
content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
const updatePayload = await this.client.setGroupSessionSharing({
|
|
2259
|
+
...parsed,
|
|
2260
|
+
groupSharingScopeId,
|
|
2261
|
+
share: sharingDecision === "share",
|
|
2262
|
+
confirmed: true,
|
|
2263
|
+
});
|
|
2264
|
+
return this.formatGroupSessionSharingUpdate(sharingDecision === "share", updatePayload, scopeText);
|
|
2265
|
+
}
|
|
2266
|
+
async handleSetGroupSessionSharing(args) {
|
|
2267
|
+
const parsed = setGroupSessionSharingSchema.parse(args ?? {});
|
|
2268
|
+
const payload = await this.client.setGroupSessionSharing(parsed);
|
|
2269
|
+
return this.formatGroupSessionSharingUpdate(parsed.share, payload);
|
|
2270
|
+
}
|
|
2137
2271
|
async handlePublishToGroup(args) {
|
|
2138
2272
|
const parsed = publishToGroupSchema.parse(args ?? {});
|
|
2139
2273
|
const payload = await this.client.publishMemoryToGroup(args);
|
|
@@ -2182,8 +2316,8 @@ Details: ${m.details || "N/A"}`;
|
|
|
2182
2316
|
content: [{
|
|
2183
2317
|
type: "text",
|
|
2184
2318
|
text: payload?.alreadyMember
|
|
2185
|
-
? `You are already a member of ${payload?.group?.name ?? "this group"}. No memories were published. Use prepare_group_publication to review memories and propose any missing title or responsibility fields, then call
|
|
2186
|
-
: `Joined ${payload?.group?.name ?? "the company group"}. No memories were published. Next use prepare_group_publication to review candidates, infer a proposed title and responsibility summary, and ask the user to confirm that profile together with the publication preview. Also call
|
|
2319
|
+
? `You are already a member of ${payload?.group?.name ?? "this group"}. No memories were published. Use prepare_group_publication to review memories and propose any missing title or responsibility fields, then call request_group_session_sharing so supported hosts render the native sharing choice. Decline or cancel stays unset for a later checkpoint.`
|
|
2320
|
+
: `Joined ${payload?.group?.name ?? "the company group"}. No memories were published. Next use prepare_group_publication to review candidates, infer a proposed title and responsibility summary, and ask the user to confirm that profile together with the publication preview. Also call request_group_session_sharing without a scope so supported hosts show the native team-sharing choice. Decline or cancel leaves consent unset for a later checkpoint.`,
|
|
2187
2321
|
}],
|
|
2188
2322
|
};
|
|
2189
2323
|
}
|
package/dist/package-metadata.js
CHANGED
|
@@ -27,19 +27,15 @@ export const MEMORY_CITATION_INSTRUCTION = 'If the user-facing answer materially
|
|
|
27
27
|
export const SAVED_MEMORY_RECEIPT_INSTRUCTION = 'After save_conversation succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact "EchoMem saved:" list containing every memory created by that call. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. This save receipt is separate from "EchoMem sources:" and does not imply that the newly saved memories informed the answer.';
|
|
28
28
|
export const MCP_SERVER_INSTRUCTIONS = [
|
|
29
29
|
`${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
|
|
30
|
-
`If
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"For
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
MEMORY_CITATION_INSTRUCTION,
|
|
40
|
-
SAVED_MEMORY_RECEIPT_INSTRUCTION,
|
|
41
|
-
"During a publication preview, if an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain that marking does not publish or change encryption; it means the agent will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag inferred sensitivity. For sensitive-topic flags, search and preview exact owned memories before confirmed flag_memories_for_publication_attention. Separate already-flagged candidates, state that nothing has been published yet, and offer to exclude them, review them separately, or first search for and mark similar sensitive owned memories.",
|
|
42
|
-
"Never save an inferred group profile. Manual prepared publication requires explicit preview confirmation; flagged memories still require separate exact-memory confirmation. Never store or log an echo_grp_ invite code.",
|
|
30
|
+
`If this bridge is stale, update once with \`${MCP_UPDATE_ALL_COMMAND}\` and start a new MCP session; never auto-update at startup.`,
|
|
31
|
+
"Before re-deriving prior decisions or preferences, use search_memories.",
|
|
32
|
+
"Before the final response for a durable decision, implementation, fix, commit, passing verification, release, or milestone, call save_conversation. Skip secrets and trivial work. If the encrypted vault is locked, tell the user to run echomem-mcp unlock.",
|
|
33
|
+
"After a successful save, show every memory created by that call in a compact EchoMem saved: list with canonical links; this is separate from \"EchoMem sources:\".",
|
|
34
|
+
"For company groups, call request_group_session_sharing near conversation start or after a qualifying save. It renders a native choice on clients with MCP elicitation and returns a text fallback otherwise. Omit groupSharingScopeId only on the first call, then reuse the returned scope only in this conversation. Each group needs an explicit choice; decline, cancel, or silence stays unset. Never infer consent. Flagged memories stay private.",
|
|
35
|
+
"EchoMem credential identity is authoritative over Claude profiles, host accounts, git identity, or inference. Use get_group_context before group-orientation answers and never re-filter owners returned by search_others_memories.",
|
|
36
|
+
"When a final answer materially uses teammate or friend memories, call record_memory_citations with only the exact used Memory IDs and a unique per-answer receiptId.",
|
|
37
|
+
"End memory-informed answers with a compact EchoMem sources: list. Use each memory key as the label and https://echoknows.com/memory/<memory-id> as its canonical link.",
|
|
38
|
+
"Group profiles, conversation sharing, publication, sensitive-memory flags, and deletion require explicit user confirmation. Never store or log an echo_grp_ invite code.",
|
|
43
39
|
].join(" ");
|
|
44
40
|
export function withMcpVersion(description) {
|
|
45
41
|
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${MCP_UPDATE_ALL_COMMAND}\` (or add \`--client cursor|windsurf|claude-desktop|claude-code|codex\` for a single client), then start a new MCP session. Do not run updates repeatedly or on every startup.`;
|
|
@@ -7,8 +7,8 @@ const CHECKPOINT_REASON = [
|
|
|
7
7
|
"Run EchoMem's private completion checkpoint before finishing this turn.",
|
|
8
8
|
"Review the work that just completed. If it produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone, call `save_conversation` now with a concise structured summary of the outcome, reasoning, relevant files, and constraints.",
|
|
9
9
|
"Do not save secrets or incidental chatter. If the work was not durable, do not call the tool and finish normally.",
|
|
10
|
-
"Private persistence happens first. For a user with a company group, call `
|
|
11
|
-
"On the first
|
|
10
|
+
"Private persistence happens first. For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save.",
|
|
11
|
+
"On the first request_group_session_sharing call in a conversation, omit groupSharingScopeId so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per groupId. Supported clients render a native Share with team / Keep private choice; if the tool returns a text fallback, relay its exact question and call set_group_session_sharing only after an explicit Yes/No. Decline, cancel, or silence leaves that group's state unset. A Share decision syncs eligible memories from later saves carrying the same scope to that group; saves automatically sync to every approved group. Keep private keeps them private for that group.",
|
|
12
12
|
"Flagged memories are withheld from automatic conversation sync and remain private.",
|
|
13
13
|
"If EchoMem reports that the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip a qualifying checkpoint.",
|
|
14
14
|
].join(" ");
|
package/dist/setup.js
CHANGED
|
@@ -218,13 +218,15 @@ export function knownClients() {
|
|
|
218
218
|
{ id: "codex", label: "Codex", kind: "command", detectDir: codexHome(), configPath: path.join(codexHome(), "config.toml"), note: "add to ~/.codex/config.toml under [mcp_servers.echomem]" },
|
|
219
219
|
];
|
|
220
220
|
}
|
|
221
|
-
/** A client is "present" if its config dir already exists
|
|
221
|
+
/** A client is "present" if its config dir already exists — a cheap, side-effect-free heuristic. */
|
|
222
222
|
export function detectClients() {
|
|
223
223
|
return knownClients().filter((c) => {
|
|
224
224
|
if (c.kind === "json")
|
|
225
225
|
return fs.existsSync(path.dirname(c.configPath));
|
|
226
226
|
if (c.kind === "command")
|
|
227
227
|
return fs.existsSync(c.detectDir);
|
|
228
|
+
if (c.id === "claude-code")
|
|
229
|
+
return fs.existsSync(home(".claude"));
|
|
228
230
|
return false;
|
|
229
231
|
});
|
|
230
232
|
}
|
|
@@ -392,7 +394,7 @@ function echomemGuidanceBlock() {
|
|
|
392
394
|
'- If the final user-facing answer materially relies on one or more EchoMem memories, end it with a compact `EchoMem sources:` list containing only the memories actually used. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. Do not cite memories that were merely retrieved, and omit the section when no memory informed the answer.',
|
|
393
395
|
"- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip it.",
|
|
394
396
|
'- After `save_conversation` succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact `EchoMem saved:` list containing every memory created by that call. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. This save receipt is separate from `EchoMem sources:` and does not imply the newly saved memories informed the answer.',
|
|
395
|
-
"- For a user with a company group, call `
|
|
397
|
+
"- For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per `groupId`. Supported clients render a native Share with team / Keep private choice. If the tool returns a text fallback, relay its exact question and call `set_group_session_sharing` only after an explicit Yes/No. Decline, cancel, or silence leaves that group's state unset; never infer an answer. Saves sync eligible memories to every approved group; a No keeps them private for that group.",
|
|
396
398
|
"- If the user pastes a session carryover/checkpoint: it may reference `get_checkpoint_by_context` — use it to pull the checkpoint/decision trail when you need more than the snapshot.",
|
|
397
399
|
"",
|
|
398
400
|
"### Company group memory",
|
package/dist/v1-contract.js
CHANGED
|
@@ -13,6 +13,7 @@ export const canonicalToolNames = {
|
|
|
13
13
|
recordCitations: "record_memory_citations",
|
|
14
14
|
groupContext: "get_group_context",
|
|
15
15
|
getGroupSessionSharing: "get_group_session_sharing",
|
|
16
|
+
requestGroupSessionSharing: "request_group_session_sharing",
|
|
16
17
|
setGroupSessionSharing: "set_group_session_sharing",
|
|
17
18
|
createGroup: "create_memory_group",
|
|
18
19
|
createGroupInvite: "create_group_invite",
|
|
@@ -38,6 +39,127 @@ export const legacyAliasToCanonical = {
|
|
|
38
39
|
export function resolveCanonicalToolName(toolName) {
|
|
39
40
|
return legacyAliasToCanonical[toolName] ?? toolName;
|
|
40
41
|
}
|
|
42
|
+
export const TOOL_DESCRIPTION_BYTE_LIMIT = 2_000;
|
|
43
|
+
const READ_ONLY_TOOL_NAMES = new Set([
|
|
44
|
+
canonicalToolNames.search,
|
|
45
|
+
"search_memories_by_description_semantic",
|
|
46
|
+
canonicalToolNames.timeRange,
|
|
47
|
+
"search_memories_by_time_range",
|
|
48
|
+
canonicalToolNames.keywords,
|
|
49
|
+
canonicalToolNames.friends,
|
|
50
|
+
canonicalToolNames.searchUsers,
|
|
51
|
+
canonicalToolNames.others,
|
|
52
|
+
canonicalToolNames.publicMemory,
|
|
53
|
+
canonicalToolNames.groupContext,
|
|
54
|
+
canonicalToolNames.getGroupSessionSharing,
|
|
55
|
+
canonicalToolNames.getByContext,
|
|
56
|
+
canonicalToolNames.checkpointByContext,
|
|
57
|
+
canonicalToolNames.report,
|
|
58
|
+
canonicalToolNames.updateStatus,
|
|
59
|
+
canonicalToolNames.contextHealth,
|
|
60
|
+
canonicalToolNames.recompose,
|
|
61
|
+
]);
|
|
62
|
+
const IDEMPOTENT_WRITE_TOOL_NAMES = new Set([
|
|
63
|
+
canonicalToolNames.recordCitations,
|
|
64
|
+
canonicalToolNames.requestGroupSessionSharing,
|
|
65
|
+
canonicalToolNames.setGroupSessionSharing,
|
|
66
|
+
canonicalToolNames.updateGroupProfile,
|
|
67
|
+
canonicalToolNames.completeGroupPublication,
|
|
68
|
+
canonicalToolNames.publishToGroup,
|
|
69
|
+
canonicalToolNames.publishBatchToGroup,
|
|
70
|
+
]);
|
|
71
|
+
const REQUIRES_USER_INTERACTION_TOOL_NAMES = new Set([
|
|
72
|
+
canonicalToolNames.requestGroupSessionSharing,
|
|
73
|
+
canonicalToolNames.setGroupSessionSharing,
|
|
74
|
+
canonicalToolNames.flagPublicationAttention,
|
|
75
|
+
canonicalToolNames.updateGroupProfile,
|
|
76
|
+
canonicalToolNames.completeGroupPublication,
|
|
77
|
+
canonicalToolNames.publishToGroup,
|
|
78
|
+
canonicalToolNames.publishBatchToGroup,
|
|
79
|
+
canonicalToolNames.delete,
|
|
80
|
+
]);
|
|
81
|
+
const ALWAYS_LOAD_TOOL_NAMES = new Set([
|
|
82
|
+
canonicalToolNames.search,
|
|
83
|
+
canonicalToolNames.save,
|
|
84
|
+
]);
|
|
85
|
+
const LOCAL_ONLY_TOOL_NAMES = new Set([
|
|
86
|
+
canonicalToolNames.report,
|
|
87
|
+
canonicalToolNames.contextHealth,
|
|
88
|
+
canonicalToolNames.recompose,
|
|
89
|
+
]);
|
|
90
|
+
function truncateToByteBudget(value, limit) {
|
|
91
|
+
if (Buffer.byteLength(value, "utf8") <= limit)
|
|
92
|
+
return value;
|
|
93
|
+
const ellipsis = "…";
|
|
94
|
+
const contentBudget = limit - Buffer.byteLength(ellipsis, "utf8");
|
|
95
|
+
if (contentBudget <= 0)
|
|
96
|
+
return "";
|
|
97
|
+
let low = 0;
|
|
98
|
+
let high = value.length;
|
|
99
|
+
while (low < high) {
|
|
100
|
+
const middle = Math.ceil((low + high) / 2);
|
|
101
|
+
if (Buffer.byteLength(value.slice(0, middle), "utf8") <= contentBudget)
|
|
102
|
+
low = middle;
|
|
103
|
+
else
|
|
104
|
+
high = middle - 1;
|
|
105
|
+
}
|
|
106
|
+
const safeEnd = low > 0
|
|
107
|
+
&& /[\uD800-\uDBFF]/.test(value.charAt(low - 1))
|
|
108
|
+
&& /[\uDC00-\uDFFF]/.test(value.charAt(low))
|
|
109
|
+
? low - 1
|
|
110
|
+
: low;
|
|
111
|
+
const candidate = value.slice(0, safeEnd);
|
|
112
|
+
const boundary = Math.max(candidate.lastIndexOf("\n"), candidate.lastIndexOf(" "));
|
|
113
|
+
const body = boundary >= Math.floor(candidate.length * 0.8) ? candidate.slice(0, boundary) : candidate;
|
|
114
|
+
return `${body.trimEnd()}${ellipsis}`;
|
|
115
|
+
}
|
|
116
|
+
function boundToolDescription(description) {
|
|
117
|
+
if (Buffer.byteLength(description, "utf8") <= TOOL_DESCRIPTION_BYTE_LIMIT)
|
|
118
|
+
return description;
|
|
119
|
+
const versionMarker = "\n\nEchoMem MCP bridge:";
|
|
120
|
+
const versionIndex = description.lastIndexOf(versionMarker);
|
|
121
|
+
if (versionIndex < 0) {
|
|
122
|
+
return truncateToByteBudget(description, TOOL_DESCRIPTION_BYTE_LIMIT);
|
|
123
|
+
}
|
|
124
|
+
const suffix = description.slice(versionIndex + 2);
|
|
125
|
+
const separator = "\n\n";
|
|
126
|
+
const bodyLimit = TOOL_DESCRIPTION_BYTE_LIMIT
|
|
127
|
+
- Buffer.byteLength(suffix, "utf8")
|
|
128
|
+
- Buffer.byteLength(separator, "utf8");
|
|
129
|
+
if (bodyLimit <= 0)
|
|
130
|
+
return truncateToByteBudget(suffix, TOOL_DESCRIPTION_BYTE_LIMIT);
|
|
131
|
+
return `${truncateToByteBudget(description.slice(0, versionIndex), bodyLimit)}${separator}${suffix}`;
|
|
132
|
+
}
|
|
133
|
+
function compactGroupRoster(groupMap) {
|
|
134
|
+
const labels = groupMap
|
|
135
|
+
.split("\n")
|
|
136
|
+
.map((line) => line.trim().replace(/^-\s*/, ""))
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.map((line) => line.split(/\s+—\s+/u, 1)[0] ?? line)
|
|
139
|
+
.map((label) => truncateToByteBudget(label, 48));
|
|
140
|
+
return labels.length ? `Team roster: ${labels.join("; ")}.` : "";
|
|
141
|
+
}
|
|
142
|
+
function decorateLocalToolSpec(tool) {
|
|
143
|
+
const readOnly = READ_ONLY_TOOL_NAMES.has(tool.name);
|
|
144
|
+
const decorated = {
|
|
145
|
+
...tool,
|
|
146
|
+
description: boundToolDescription(tool.description),
|
|
147
|
+
annotations: {
|
|
148
|
+
readOnlyHint: readOnly,
|
|
149
|
+
destructiveHint: tool.name === canonicalToolNames.delete,
|
|
150
|
+
idempotentHint: readOnly || IDEMPOTENT_WRITE_TOOL_NAMES.has(tool.name),
|
|
151
|
+
openWorldHint: !LOCAL_ONLY_TOOL_NAMES.has(tool.name),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const meta = {};
|
|
155
|
+
if (ALWAYS_LOAD_TOOL_NAMES.has(tool.name)) {
|
|
156
|
+
meta["anthropic/alwaysLoad"] = true;
|
|
157
|
+
}
|
|
158
|
+
if (REQUIRES_USER_INTERACTION_TOOL_NAMES.has(tool.name)) {
|
|
159
|
+
meta["anthropic/requiresUserInteraction"] = true;
|
|
160
|
+
}
|
|
161
|
+
return Object.keys(meta).length > 0 ? { ...decorated, _meta: meta } : decorated;
|
|
162
|
+
}
|
|
41
163
|
const triggerMetadataSchema = {
|
|
42
164
|
triggerMessage: z.string().optional(),
|
|
43
165
|
triggerMessageRole: z.string().optional(),
|
|
@@ -129,6 +251,7 @@ export const getGroupSessionSharingSchema = z.object({
|
|
|
129
251
|
groupSharingScopeId: z.string().uuid().optional(),
|
|
130
252
|
groupId: z.string().uuid().optional(),
|
|
131
253
|
});
|
|
254
|
+
export const requestGroupSessionSharingSchema = getGroupSessionSharingSchema;
|
|
132
255
|
export const setGroupSessionSharingSchema = z.object({
|
|
133
256
|
...triggerMetadataSchema,
|
|
134
257
|
groupSharingScopeId: z.string().uuid(),
|
|
@@ -224,9 +347,9 @@ export function listToolSpecs(opts = {}) {
|
|
|
224
347
|
// Same device as the personal map, aimed at the group surface: the agent judges whether teammates
|
|
225
348
|
// have covered the topic before searching, instead of never calling the group tools at all.
|
|
226
349
|
const groupMapSection = groupMap
|
|
227
|
-
? `\n\nThis user's company group currently shares work in these areas (a relevance guide — search the group when the task relates to one of these people or topics):\n${groupMap}\n`
|
|
350
|
+
? `\n\nThis user's company group currently shares work in these areas (a relevance guide — search the group when the task relates to one of these people or topics):\n${compactGroupRoster(groupMap)}\n${groupMap}\n`
|
|
228
351
|
: "";
|
|
229
|
-
|
|
352
|
+
const tools = [
|
|
230
353
|
{
|
|
231
354
|
name: canonicalToolNames.search,
|
|
232
355
|
description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
|
|
@@ -265,7 +388,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
265
388
|
},
|
|
266
389
|
{
|
|
267
390
|
name: canonicalToolNames.save,
|
|
268
|
-
description: `Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to run \`echomem-mcp unlock\` and never silently skip a qualifying checkpoint. Private persistence happens first. For group sharing, reuse the exact groupSharingScopeId returned by get_group_session_sharing or an earlier save in this conversation. Never reuse it in another conversation or save it as memory. Each group has an independent decision under the same conversation scope; eligible memories sync automatically to every approved group, while flagged memories stay private. If a selected group has no decision yet,
|
|
391
|
+
description: `Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to run \`echomem-mcp unlock\` and never silently skip a qualifying checkpoint. Private persistence happens first. For group sharing, reuse the exact groupSharingScopeId returned by get_group_session_sharing, request_group_session_sharing, or an earlier save in this conversation. Never reuse it in another conversation or save it as memory. Each group has an independent decision under the same conversation scope; eligible memories sync automatically to every approved group, while flagged memories stay private. If a selected group has no decision yet, call request_group_session_sharing so supported hosts render a choice UI; its fallback tells you when a text Yes/No prompt is required. Silence leaves consent unset; never infer the answer. New extraction input uses the plan's weekly processing allowance; if the limit is reached, nothing is saved. passthrough=true stores the text verbatim as a session capsule. ${SAVED_MEMORY_RECEIPT_INSTRUCTION}`,
|
|
269
392
|
inputSchema: {
|
|
270
393
|
type: "object",
|
|
271
394
|
properties: {
|
|
@@ -513,7 +636,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
513
636
|
},
|
|
514
637
|
{
|
|
515
638
|
name: canonicalToolNames.getGroupSessionSharing,
|
|
516
|
-
description: "Read the confirmed sharing decision for one group in this exact conversation. On the first call, omit groupSharingScopeId; EchoMem returns a new opaque scope. Reuse that scope only in this conversation. If the user belongs to multiple groups and groupId is omitted, EchoMem returns the available groups
|
|
639
|
+
description: "Read the confirmed sharing decision for one group in this exact conversation. On the first call, omit groupSharingScopeId; EchoMem returns a new opaque scope. Reuse that scope only in this conversation. If the user belongs to multiple groups and groupId is omitted, EchoMem returns the available groups. If a decision is unset, call request_group_session_sharing with this scope and the selected groupId so supported hosts render a choice UI. Silence leaves that group's state unset; never infer the answer.",
|
|
517
640
|
inputSchema: {
|
|
518
641
|
type: "object",
|
|
519
642
|
properties: {
|
|
@@ -532,6 +655,27 @@ export function listToolSpecs(opts = {}) {
|
|
|
532
655
|
},
|
|
533
656
|
},
|
|
534
657
|
},
|
|
658
|
+
{
|
|
659
|
+
name: canonicalToolNames.requestGroupSessionSharing,
|
|
660
|
+
description: "Check one group's sharing decision for this exact conversation and, only when it is unset, ask the user through the MCP client's native choice UI. Omit groupSharingScopeId only on the first call so EchoMem can mint a fresh scope; reuse the returned scope only in this conversation. When multiple groups are returned, call again with the same scope and one groupId at a time. An accepted Share choice backfills eligible private memories and syncs later saves; Keep private records No. Decline or cancel records nothing. If the client cannot render MCP elicitation, relay the returned fallback question and call set_group_session_sharing only after the user explicitly answers.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
groupSharingScopeId: {
|
|
665
|
+
type: "string",
|
|
666
|
+
format: "uuid",
|
|
667
|
+
description: "Reuse the opaque scope returned in this conversation. Omit only on the first sharing check.",
|
|
668
|
+
},
|
|
669
|
+
groupId: {
|
|
670
|
+
type: "string",
|
|
671
|
+
format: "uuid",
|
|
672
|
+
description: "Group to confirm. Required when the user belongs to multiple groups.",
|
|
673
|
+
},
|
|
674
|
+
triggerMessage: { type: "string" },
|
|
675
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
},
|
|
535
679
|
{
|
|
536
680
|
name: canonicalToolNames.setGroupSessionSharing,
|
|
537
681
|
description: "Store the user's explicit Yes/No sharing decision for one group in this exact conversation scope. Pass the exact groupSharingScopeId returned in this conversation and the selected groupId when the user belongs to multiple groups. EchoMem validates membership server-side. Decisions are independent per group: share=true backfills that group and later saves sync to every approved group; share=false keeps this conversation private for that group. Flagged memories remain private.",
|
|
@@ -574,7 +718,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
574
718
|
},
|
|
575
719
|
{
|
|
576
720
|
name: canonicalToolNames.joinGroup,
|
|
577
|
-
description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories. Next call prepare_group_publication, infer a proposed title and responsibility summary from the user's own candidate memories, and ask the user to confirm the profile together with the publication preview. Also call
|
|
721
|
+
description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories. Next call prepare_group_publication, infer a proposed title and responsibility summary from the user's own candidate memories, and ask the user to confirm the profile together with the publication preview. Also call request_group_session_sharing without a scope so supported hosts render a native sharing choice. Decline or cancel stays unset and should be prompted again at a later qualifying checkpoint.",
|
|
578
722
|
inputSchema: {
|
|
579
723
|
type: "object",
|
|
580
724
|
properties: {
|
|
@@ -841,4 +985,5 @@ export function listToolSpecs(opts = {}) {
|
|
|
841
985
|
},
|
|
842
986
|
},
|
|
843
987
|
];
|
|
988
|
+
return tools.map(decorateLocalToolSpec);
|
|
844
989
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.34",
|
|
4
4
|
"description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -23,12 +23,12 @@ Use the `save_conversation` tool from the `echomem` MCP server.
|
|
|
23
23
|
read-only work and incidental chatter.
|
|
24
24
|
7. If the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip a
|
|
25
25
|
qualifying checkpoint.
|
|
26
|
-
8. For a user with a company group, call `
|
|
27
|
-
qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so
|
|
28
|
-
mints a fresh opaque scope. Reuse that exact scope only in this conversation for later
|
|
29
|
-
calls, and never persist it as memory. If multiple groups are returned, reuse
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
memories remain private.
|
|
26
|
+
8. For a user with a company group, call `request_group_session_sharing` near conversation start or
|
|
27
|
+
after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so
|
|
28
|
+
EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later
|
|
29
|
+
get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse
|
|
30
|
+
the same scope and call once per `groupId`. Supported clients render a native Share with team / Keep
|
|
31
|
+
private choice. If the tool returns a text fallback, relay its exact question and call
|
|
32
|
+
`set_group_session_sharing` only after an explicit Yes/No. Decline, cancel, or silence leaves that
|
|
33
|
+
group's state unset; never infer an answer. Saves sync eligible memories to every approved group;
|
|
34
|
+
Keep private keeps them private for that group; flagged memories remain private.
|
|
@@ -23,12 +23,12 @@ This project has EchoMem connected — the user's long-term memory across all th
|
|
|
23
23
|
created by that call. Link each memory key to its canonical
|
|
24
24
|
`https://echoknows.com/memory/<memory-id>` URL. This save receipt is separate from
|
|
25
25
|
`EchoMem sources:` and does not imply that the newly saved memories informed the answer.
|
|
26
|
-
- For a user with a company group, call `
|
|
27
|
-
qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so
|
|
28
|
-
mints a fresh opaque scope. Reuse that exact scope only in this conversation for later
|
|
29
|
-
calls, and never persist it as memory. If multiple groups are returned, reuse
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
memories stay private.
|
|
26
|
+
- For a user with a company group, call `request_group_session_sharing` near conversation start or
|
|
27
|
+
after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so
|
|
28
|
+
EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later
|
|
29
|
+
get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse
|
|
30
|
+
the same scope and call once per `groupId`. Supported clients render a native Share with team / Keep
|
|
31
|
+
private choice. If the tool returns a text fallback, relay its exact question and call
|
|
32
|
+
`set_group_session_sharing` only after an explicit Yes/No. Decline, cancel, or silence leaves that
|
|
33
|
+
group's state unset; never infer an answer. Saves sync eligible memories to every approved group;
|
|
34
|
+
Keep private keeps them private for that group; flagged memories stay private.
|