@echomem/mcp 1.4.51 โ 1.4.53
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 +14 -0
- package/dist/index.js +80 -20
- package/dist/source-session.js +46 -8
- package/dist/v1-contract.js +27 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -133,6 +133,20 @@ Both hook entries are merged idempotently into `~/.codex/hooks.json` and
|
|
|
133
133
|
`~/.claude/settings.json`; unrelated user hooks and settings are preserved. Hooks fail open when
|
|
134
134
|
their local input is missing so they cannot prevent a conversation from starting or finishing.
|
|
135
135
|
|
|
136
|
+
### Cloud Cowork through `remote-devices`
|
|
137
|
+
|
|
138
|
+
Cloud Cowork inference cannot use the local hook or transcript-verification path. The shared local
|
|
139
|
+
MCP process also must not retain one cloud conversation as global bridge state. Cloud Cowork agents
|
|
140
|
+
therefore pass their runtime-issued `session_...` identifier as `cloudCoworkSessionId` on each
|
|
141
|
+
EchoMem tool call. EchoMem binds that request to the canonical `cowork:session_...` context with
|
|
142
|
+
`cloud_cowork_tool_argument` evidence and `remote_mcp` origin, then links or saves against that
|
|
143
|
+
context without changing shared bridge state.
|
|
144
|
+
|
|
145
|
+
This identity is intentionally reported as **provisional**: the authenticated user owns the
|
|
146
|
+
context and ticket link, but EchoMem cannot independently corroborate the cloud identifier from a
|
|
147
|
+
local transcript. Verified local Cowork remains `cowork:local_...` with trusted local evidence.
|
|
148
|
+
Never convert one form into the other or silently present a provisional cloud link as verified.
|
|
149
|
+
|
|
136
150
|
### Manual / headless (SSH, containers, CI)
|
|
137
151
|
|
|
138
152
|
No browser? Provide secrets directly โ this is the documented headless path:
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS, MEMORY_CITATION_INSTRUCTI
|
|
|
16
16
|
import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
|
|
17
17
|
import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
|
|
18
18
|
import { autoUpdateHeadlessRuntime } from "./headless-runtime.js";
|
|
19
|
-
import { resolveSourceSessionRequestContext, resolveSourceSessionFromBindingToken, } from "./source-session.js";
|
|
19
|
+
import { SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE, resolveCloudCoworkSessionFromToolArguments, resolveSourceSessionRequestContext, resolveSourceSessionFromBindingToken, } from "./source-session.js";
|
|
20
20
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
21
21
|
const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/account";
|
|
22
22
|
const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/memory").replace(/\/$/, "");
|
|
@@ -169,6 +169,17 @@ function formatReconnectRequiredResult(error) {
|
|
|
169
169
|
"No editor restart is needed. Keep all credentials out of chat.",
|
|
170
170
|
].join("\n");
|
|
171
171
|
}
|
|
172
|
+
function formatWorkspaceTicketNotFoundResult(error) {
|
|
173
|
+
if (!axios.isAxiosError(error) || error.response?.status !== 404)
|
|
174
|
+
return null;
|
|
175
|
+
if (errorCodeFrom(error.response.data) !== "WORKSPACE_TICKET_NOT_FOUND")
|
|
176
|
+
return null;
|
|
177
|
+
return [
|
|
178
|
+
"๐ซ This Echo workspace ticket no longer exists or is unavailable.",
|
|
179
|
+
"Do not retry this ticket ID.",
|
|
180
|
+
"Ask the user whether to continue without a ticket or attach this session to another ticket.",
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
172
183
|
/** Map a thrown error to the telemetry error_kind taxonomy. */
|
|
173
184
|
function classifyError(error) {
|
|
174
185
|
if (error instanceof NoTokenError)
|
|
@@ -339,6 +350,9 @@ function detectMcpHostFromEnv() {
|
|
|
339
350
|
return "cursor";
|
|
340
351
|
if (process.env.WINDSURF_WORKSPACE_ID || process.env.WINDSURF_USER_ID)
|
|
341
352
|
return "windsurf";
|
|
353
|
+
// Cowork embeds a Claude Code process, but this outer host ID is the distinguishing signal.
|
|
354
|
+
if (process.env.CLAUDE_CODE_HOST_SESSION_ID)
|
|
355
|
+
return "cowork";
|
|
342
356
|
if (process.env.CLAUDE_CODE || process.env.CLAUDECODE || process.env.ANTHROPIC_CLAUDE_CODE) {
|
|
343
357
|
return "claude_code";
|
|
344
358
|
}
|
|
@@ -697,7 +711,7 @@ class EchoMemApiClient {
|
|
|
697
711
|
const requestContext = this.requestContext.getStore();
|
|
698
712
|
return requestContext === undefined ? this.boundSourceSession : requestContext.sourceSession;
|
|
699
713
|
}
|
|
700
|
-
|
|
714
|
+
getRequestSourceSession() {
|
|
701
715
|
const requestContext = this.requestContext.getStore();
|
|
702
716
|
if (!requestContext || requestContext.lineageStatus === "unbound")
|
|
703
717
|
return null;
|
|
@@ -709,26 +723,29 @@ class EchoMemApiClient {
|
|
|
709
723
|
async withRequestContext(requestContext, operation) {
|
|
710
724
|
return this.requestContext.run(requestContext, operation);
|
|
711
725
|
}
|
|
712
|
-
async bindSourceSession(
|
|
726
|
+
async bindSourceSession(identity, persistForBridge = true) {
|
|
713
727
|
this.synchronizeAccountContext();
|
|
714
|
-
|
|
728
|
+
if (identity.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE && persistForBridge) {
|
|
729
|
+
throw new Error("A provisional Cloud Cowork identity cannot become shared bridge state");
|
|
730
|
+
}
|
|
731
|
+
const cached = this.sourceSessionsByCanonicalKey.get(identity.canonicalKey);
|
|
715
732
|
if (cached) {
|
|
716
733
|
if (persistForBridge)
|
|
717
734
|
this.boundSourceSession = cached;
|
|
718
735
|
return { ...cached, created: false };
|
|
719
736
|
}
|
|
720
737
|
const response = await this.axios.post("/api/extension/source-sessions/bind", {
|
|
721
|
-
provider:
|
|
722
|
-
providerSessionId:
|
|
723
|
-
evidence:
|
|
738
|
+
provider: identity.provider,
|
|
739
|
+
providerSessionId: identity.providerSessionId,
|
|
740
|
+
evidence: identity.evidence,
|
|
724
741
|
});
|
|
725
742
|
const contextId = readString(response.data, "contextId");
|
|
726
743
|
const canonicalKey = readString(response.data, "canonicalKey");
|
|
727
|
-
if (response.data?.success !== true || !contextId || canonicalKey !==
|
|
744
|
+
if (response.data?.success !== true || !contextId || canonicalKey !== identity.canonicalKey) {
|
|
728
745
|
throw new Error("EchoMem returned an invalid source-session binding receipt");
|
|
729
746
|
}
|
|
730
|
-
const bound = { ...
|
|
731
|
-
this.sourceSessionsByCanonicalKey.set(
|
|
747
|
+
const bound = { ...identity, contextId };
|
|
748
|
+
this.sourceSessionsByCanonicalKey.set(identity.canonicalKey, bound);
|
|
732
749
|
if (this.sourceSessionsByCanonicalKey.size > 64) {
|
|
733
750
|
const oldestKey = this.sourceSessionsByCanonicalKey.keys().next().value;
|
|
734
751
|
if (oldestKey)
|
|
@@ -745,9 +762,10 @@ class EchoMemApiClient {
|
|
|
745
762
|
const parsed = linkWorkspaceTicketSessionSchema.parse(args ?? {});
|
|
746
763
|
// A bridge-level compatibility binding can outlive a conversation in
|
|
747
764
|
// long-running hosts. Ticket links are therefore allowed to take the fast
|
|
748
|
-
// path only from identity
|
|
749
|
-
//
|
|
750
|
-
|
|
765
|
+
// path only from identity scoped to this exact request. That identity is
|
|
766
|
+
// either verified host metadata or an explicitly marked provisional Cloud
|
|
767
|
+
// Cowork claim; otherwise Desktop completes the link from local evidence.
|
|
768
|
+
const sourceSession = this.getRequestSourceSession();
|
|
751
769
|
if (!sourceSession)
|
|
752
770
|
return null;
|
|
753
771
|
const response = await this.axios.post(`/api/extension/workspace-tickets/${encodeURIComponent(parsed.ticketId)}/sessions`, {
|
|
@@ -1026,10 +1044,14 @@ class EchoMemApiClient {
|
|
|
1026
1044
|
// For an encrypted account, hand the server the key transiently in the X-Encryption-Key header
|
|
1027
1045
|
// so it encrypts at rest (mirrors the extension's write path, spec ยง3.1a). Locked โ LockedError.
|
|
1028
1046
|
const enc = await this.encState();
|
|
1047
|
+
const boundSourceSession = this.getBoundSourceSession();
|
|
1048
|
+
const originChannel = boundSourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE
|
|
1049
|
+
? "remote_mcp"
|
|
1050
|
+
: "local_mcp";
|
|
1029
1051
|
const config = {
|
|
1030
1052
|
headers: {
|
|
1031
1053
|
"X-EchoMem-Request-Id": randomUUID(),
|
|
1032
|
-
"X-EchoMem-Origin-Channel":
|
|
1054
|
+
"X-EchoMem-Origin-Channel": originChannel,
|
|
1033
1055
|
...(enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : {}),
|
|
1034
1056
|
},
|
|
1035
1057
|
};
|
|
@@ -1039,9 +1061,10 @@ class EchoMemApiClient {
|
|
|
1039
1061
|
sourceUrl: parsed.url,
|
|
1040
1062
|
source: parsed.source || "mcp_server",
|
|
1041
1063
|
title: parsed.title,
|
|
1042
|
-
// A
|
|
1064
|
+
// A source-session identity owns the memory context. Provisional Cloud Cowork identity
|
|
1065
|
+
// is explicitly marked by its evidence and origin channel. The independent opaque scope still
|
|
1043
1066
|
// governs group-sharing consent and never becomes the source-session identity.
|
|
1044
|
-
conversationKey:
|
|
1067
|
+
conversationKey: boundSourceSession?.canonicalKey ?? groupSharingScopeId,
|
|
1045
1068
|
groupSharingScopeId,
|
|
1046
1069
|
passthrough: parsed.passthrough || false,
|
|
1047
1070
|
triggerMessage: parsed.triggerMessage ||
|
|
@@ -1411,8 +1434,10 @@ class EchoMemMCPServer {
|
|
|
1411
1434
|
});
|
|
1412
1435
|
}
|
|
1413
1436
|
getMcpClientAnalytics() {
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1437
|
+
const environmentPlatform = detectMcpHostFromEnv();
|
|
1438
|
+
const hostPlatform = (environmentPlatform === "cowork" ? environmentPlatform : undefined)
|
|
1439
|
+
?? normalizeMcpHostPlatform(this.mcpClientName)
|
|
1440
|
+
?? environmentPlatform
|
|
1416
1441
|
?? "unknown";
|
|
1417
1442
|
return {
|
|
1418
1443
|
mcp_client_name: this.mcpClientName,
|
|
@@ -1486,6 +1511,23 @@ class EchoMemMCPServer {
|
|
|
1486
1511
|
catch {
|
|
1487
1512
|
// Malformed host metadata is not model input. Ignore it and retain the explicit fallback.
|
|
1488
1513
|
}
|
|
1514
|
+
let provisionalCloudSession = null;
|
|
1515
|
+
try {
|
|
1516
|
+
provisionalCloudSession = resolveCloudCoworkSessionFromToolArguments(toolArgs);
|
|
1517
|
+
}
|
|
1518
|
+
catch (error) {
|
|
1519
|
+
throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : "Invalid Cloud Cowork session identity");
|
|
1520
|
+
}
|
|
1521
|
+
if (provisionalCloudSession && sourceResolution.sourceSession) {
|
|
1522
|
+
throw new McpError(ErrorCode.InvalidParams, "This request already has a verified local source session. Omit cloudCoworkSessionId rather than mixing identities.");
|
|
1523
|
+
}
|
|
1524
|
+
if (provisionalCloudSession) {
|
|
1525
|
+
sourceResolution = {
|
|
1526
|
+
sourceSession: provisionalCloudSession,
|
|
1527
|
+
isSubagent: false,
|
|
1528
|
+
lineageStatus: "not_applicable",
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1489
1531
|
// A child request starts fail-closed. Resolved children bind to the originating root; unresolved
|
|
1490
1532
|
// children carry an explicit null so AsyncLocalStorage cannot fall through to a global binding.
|
|
1491
1533
|
let requestSourceSession = sourceResolution.isSubagent
|
|
@@ -1735,6 +1777,13 @@ class EchoMemMCPServer {
|
|
|
1735
1777
|
isError: true,
|
|
1736
1778
|
};
|
|
1737
1779
|
}
|
|
1780
|
+
const missingWorkspaceTicket = formatWorkspaceTicketNotFoundResult(error);
|
|
1781
|
+
if (missingWorkspaceTicket) {
|
|
1782
|
+
return {
|
|
1783
|
+
content: [{ type: "text", text: missingWorkspaceTicket }],
|
|
1784
|
+
isError: true,
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1738
1787
|
const upgradeRequired = formatUpgradeRequiredResult(error);
|
|
1739
1788
|
if (upgradeRequired) {
|
|
1740
1789
|
return {
|
|
@@ -2028,15 +2077,26 @@ Details: ${m.details || "N/A"}`)
|
|
|
2028
2077
|
}
|
|
2029
2078
|
const ticket = isRecord(result.ticket) ? result.ticket : {};
|
|
2030
2079
|
const workspaceId = readString(ticket, "workspaceId") ?? parsed.workspaceId;
|
|
2080
|
+
const sourceSession = this.client.getBoundSourceSession();
|
|
2081
|
+
const provisional = sourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE;
|
|
2031
2082
|
return {
|
|
2032
2083
|
content: [{
|
|
2033
2084
|
type: "text",
|
|
2034
2085
|
text: [
|
|
2035
2086
|
result.changed === true
|
|
2036
|
-
?
|
|
2037
|
-
|
|
2087
|
+
? provisional
|
|
2088
|
+
? "Linked this provisional Cloud Cowork session to the Echo workspace ticket."
|
|
2089
|
+
: "Linked this verified source session to the Echo workspace ticket."
|
|
2090
|
+
: provisional
|
|
2091
|
+
? "This provisional Cloud Cowork session was already linked to the Echo workspace ticket."
|
|
2092
|
+
: "This verified source session was already linked to the Echo workspace ticket.",
|
|
2038
2093
|
`Ticket: ${parsed.ticketId}`,
|
|
2039
2094
|
workspaceId ? `Workspace: ${workspaceId}` : "",
|
|
2095
|
+
sourceSession ? `Source session: ${sourceSession.canonicalKey}` : "",
|
|
2096
|
+
sourceSession ? `Context: ${sourceSession.contextId}` : "",
|
|
2097
|
+
provisional
|
|
2098
|
+
? "Identity trust: provisional (reported by Cloud Cowork in the tool call; not verified by a local transcript). Repeat cloudCoworkSessionId on later EchoMem calls from this conversation."
|
|
2099
|
+
: "",
|
|
2040
2100
|
"Retries are safe and do not create duplicate links or history events.",
|
|
2041
2101
|
].filter(Boolean).join("\n"),
|
|
2042
2102
|
}],
|
package/dist/source-session.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveClaudeCoworkSessionsDir, resolveClaudeProjectsDir, resolveCodexS
|
|
|
4
4
|
export const SOURCE_SESSION_BINDING_EVIDENCE = "local_jsonl_tool_call";
|
|
5
5
|
export const SOURCE_SESSION_HOOK_EVIDENCE = "local_session_start_hook";
|
|
6
6
|
export const SOURCE_SESSION_MCP_METADATA_EVIDENCE = "local_mcp_session_metadata";
|
|
7
|
+
export const SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE = "cloud_cowork_tool_argument";
|
|
7
8
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
9
|
const UUID_IN_TEXT_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
9
10
|
const MAX_TAIL_BYTES = 512 * 1024;
|
|
@@ -20,13 +21,18 @@ function normalizedProviderSessionId(provider, value) {
|
|
|
20
21
|
if (provider === "codex" && !UUID_RE.test(normalized)) {
|
|
21
22
|
throw new Error("Codex source-session ID must be a UUID");
|
|
22
23
|
}
|
|
23
|
-
if (provider === "cowork"
|
|
24
|
-
|
|
24
|
+
if (provider === "cowork"
|
|
25
|
+
&& !/^local_[A-Za-z0-9_-]{1,194}$/.test(normalized)
|
|
26
|
+
&& !/^session_[A-Za-z0-9_-]{1,192}$/.test(normalized)) {
|
|
27
|
+
throw new Error("Cowork source-session ID must use a local_ host ID or session_ Cloud Cowork ID");
|
|
25
28
|
}
|
|
26
29
|
return UUID_RE.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
27
30
|
}
|
|
28
31
|
export function verifiedSourceSession(provider, providerSessionId, evidence) {
|
|
29
32
|
const normalized = normalizedProviderSessionId(provider, providerSessionId);
|
|
33
|
+
if (provider === "cowork" && !normalized.startsWith("local_")) {
|
|
34
|
+
throw new Error("Verified Cowork source-session ID must use the local_ host identifier");
|
|
35
|
+
}
|
|
30
36
|
return {
|
|
31
37
|
provider,
|
|
32
38
|
providerSessionId: normalized,
|
|
@@ -34,6 +40,26 @@ export function verifiedSourceSession(provider, providerSessionId, evidence) {
|
|
|
34
40
|
evidence,
|
|
35
41
|
};
|
|
36
42
|
}
|
|
43
|
+
export function provisionalCloudCoworkSession(providerSessionId) {
|
|
44
|
+
const normalized = normalizedProviderSessionId("cowork", providerSessionId);
|
|
45
|
+
if (!normalized.startsWith("session_")) {
|
|
46
|
+
throw new Error("Cloud Cowork source-session ID must use the session_ identifier from the cloud runtime");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
provider: "cowork",
|
|
50
|
+
providerSessionId: normalized,
|
|
51
|
+
canonicalKey: `cowork:${normalized}`,
|
|
52
|
+
evidence: SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function resolveCloudCoworkSessionFromToolArguments(args) {
|
|
56
|
+
if (!isRecord(args) || args.cloudCoworkSessionId === undefined)
|
|
57
|
+
return null;
|
|
58
|
+
if (typeof args.cloudCoworkSessionId !== "string") {
|
|
59
|
+
throw new Error("cloudCoworkSessionId must be the current Cloud Cowork session_ identifier");
|
|
60
|
+
}
|
|
61
|
+
return provisionalCloudCoworkSession(args.cloudCoworkSessionId);
|
|
62
|
+
}
|
|
37
63
|
function isRecord(value) {
|
|
38
64
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
65
|
}
|
|
@@ -361,14 +387,23 @@ export function resolveSourceSessionRequestContext(metadata, options = {}) {
|
|
|
361
387
|
agentDepth: lineage.agentDepth,
|
|
362
388
|
};
|
|
363
389
|
}
|
|
390
|
+
const isClaudeHost = host.includes("claude") || host.includes("cowork");
|
|
391
|
+
const coworkCandidate = env.CLAUDE_CODE_HOST_SESSION_ID?.trim() ?? "";
|
|
392
|
+
// Cowork embeds Claude Code, so its MCP handshake may still identify the nested client as
|
|
393
|
+
// "Claude Code" and expose both IDs. The outer host session is the durable Cowork conversation;
|
|
394
|
+
// it must win over the nested CLI session whenever Anthropic supplies both host-owned values.
|
|
395
|
+
if (isClaudeHost && coworkCandidate) {
|
|
396
|
+
return {
|
|
397
|
+
sourceSession: verifiedSourceSession("cowork", coworkCandidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE),
|
|
398
|
+
isSubagent: false,
|
|
399
|
+
lineageStatus: "not_applicable",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
364
402
|
if (host.includes("claude_desktop") || host === "claude" || host.includes("cowork")) {
|
|
365
|
-
const candidate = env.CLAUDE_CODE_HOST_SESSION_ID?.trim() ?? "";
|
|
366
403
|
return {
|
|
367
|
-
sourceSession:
|
|
368
|
-
? verifiedSourceSession("cowork", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
|
|
369
|
-
: null,
|
|
404
|
+
sourceSession: null,
|
|
370
405
|
isSubagent: false,
|
|
371
|
-
lineageStatus:
|
|
406
|
+
lineageStatus: "unbound",
|
|
372
407
|
};
|
|
373
408
|
}
|
|
374
409
|
if (host.includes("claude_code")) {
|
|
@@ -384,7 +419,10 @@ export function resolveSourceSessionRequestContext(metadata, options = {}) {
|
|
|
384
419
|
return { sourceSession: null, isSubagent: false, lineageStatus: "unbound" };
|
|
385
420
|
}
|
|
386
421
|
export function resolveSourceSessionFromMcpContext(metadata, options = {}) {
|
|
387
|
-
|
|
422
|
+
const sourceSession = resolveSourceSessionRequestContext(metadata, options).sourceSession;
|
|
423
|
+
return sourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE
|
|
424
|
+
? null
|
|
425
|
+
: sourceSession;
|
|
388
426
|
}
|
|
389
427
|
export function resolveSourceSessionFromBindingToken(bindingToken, options = {}) {
|
|
390
428
|
const normalizedToken = bindingToken.trim().toLowerCase();
|
package/dist/v1-contract.js
CHANGED
|
@@ -164,7 +164,32 @@ function decorateLocalToolSpec(tool) {
|
|
|
164
164
|
const triggerMetadataSchema = {
|
|
165
165
|
triggerMessage: z.string().optional(),
|
|
166
166
|
triggerMessageRole: z.string().optional(),
|
|
167
|
+
cloudCoworkSessionId: z.string().regex(/^session_[A-Za-z0-9_-]{1,192}$/).optional(),
|
|
167
168
|
};
|
|
169
|
+
const CLOUD_COWORK_SESSION_DESCRIPTION = "Cloud Cowork only: the current session_ identifier supplied by the Cowork runtime. Repeat it on each EchoMem call so the shared remote-devices bridge can attach that one request to the same provisional conversation context. Never guess or reuse an ID from another conversation. Local Cowork, Claude Code, and Codex must omit this field.";
|
|
170
|
+
const CLOUD_COWORK_SESSION_PROPERTY = {
|
|
171
|
+
type: "string",
|
|
172
|
+
pattern: "^session_[A-Za-z0-9_-]{1,192}$",
|
|
173
|
+
description: CLOUD_COWORK_SESSION_DESCRIPTION,
|
|
174
|
+
};
|
|
175
|
+
const SOURCE_CONTEXT_FREE_TOOL_NAMES = new Set([
|
|
176
|
+
canonicalToolNames.bindSourceSession,
|
|
177
|
+
canonicalToolNames.updateStatus,
|
|
178
|
+
canonicalToolNames.contextHealth,
|
|
179
|
+
canonicalToolNames.recompose,
|
|
180
|
+
]);
|
|
181
|
+
function injectCloudCoworkSession(specs) {
|
|
182
|
+
for (const spec of specs) {
|
|
183
|
+
if (SOURCE_CONTEXT_FREE_TOOL_NAMES.has(spec.name))
|
|
184
|
+
continue;
|
|
185
|
+
const existing = spec.inputSchema.properties ?? {};
|
|
186
|
+
spec.inputSchema.properties = {
|
|
187
|
+
...existing,
|
|
188
|
+
cloudCoworkSessionId: CLOUD_COWORK_SESSION_PROPERTY,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return specs;
|
|
192
|
+
}
|
|
168
193
|
// Canonical workspace selector for tools that act inside a company workspace.
|
|
169
194
|
// `workspaceId` is the current name; `groupId` is the legacy alias kept working
|
|
170
195
|
// so existing prompts keep functioning. Handlers normalize with
|
|
@@ -478,7 +503,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
478
503
|
{
|
|
479
504
|
name: canonicalToolNames.linkWorkspaceTicketSession,
|
|
480
505
|
title: "Link this agent conversation to an Echo workspace ticket",
|
|
481
|
-
description: withMcpVersion("Call when the user asks to work on, continue, attach, or bind this conversation to an Echo workspace ticket and supplies its UUID. Pass the ticketId exactly; pass workspaceId when a structured Echo ticket marker provides it. The operation is idempotent.
|
|
506
|
+
description: withMcpVersion("Call when the user asks to work on, continue, attach, or bind this conversation to an Echo workspace ticket and supplies its UUID. Pass the ticketId exactly; pass workspaceId when a structured Echo ticket marker provides it. The operation is idempotent. A verified local source session links immediately. Cloud Cowork has no local transcript, so pass its current session_ runtime identifier as cloudCoworkSessionId; EchoMem records that path explicitly as provisional and links it immediately. Otherwise Echo Desktop can recover this exact invocation from a local transcript. Never guess or ask the user for a source-session/context ID."),
|
|
482
507
|
inputSchema: {
|
|
483
508
|
type: "object",
|
|
484
509
|
properties: {
|
|
@@ -1097,5 +1122,5 @@ export function listToolSpecs(opts = {}) {
|
|
|
1097
1122
|
},
|
|
1098
1123
|
},
|
|
1099
1124
|
];
|
|
1100
|
-
return injectWorkspaceSelector(tools).map(decorateLocalToolSpec);
|
|
1125
|
+
return injectWorkspaceSelector(injectCloudCoworkSession(tools)).map(decorateLocalToolSpec);
|
|
1101
1126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.53",
|
|
4
4
|
"description": "EchoMem MCP bridge for cross-agent memory, local history import, and recall",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
|
|
34
34
|
"test:config-safety": "npm run build && node test/config-safety.test.mjs",
|
|
35
35
|
"test:mcp-control": "npm run build && node test/mcp-control.test.mjs",
|
|
36
|
-
"test": "npm run build && node test/source-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/workspace-selector.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/config-safety.test.mjs && node test/mcp-control.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs && node test/durable-entry.test.mjs && node test/durable-reexec.test.mjs && node test/doctor.test.mjs && node test/environment-matrix.test.mjs",
|
|
36
|
+
"test": "npm run build && node test/source-session.test.mjs && node test/cowork-ticket-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/workspace-selector.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/config-safety.test.mjs && node test/mcp-control.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs && node test/durable-entry.test.mjs && node test/durable-reexec.test.mjs && node test/doctor.test.mjs && node test/environment-matrix.test.mjs",
|
|
37
37
|
"prepack": "npm run build"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|