@remnic/core 9.63.10 → 9.63.12
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/access-admin-ops-surface.d.ts +2 -1
- package/dist/access-admin-ops-surface.js +1 -1
- package/dist/access-authorization-probe.d.ts +2 -1
- package/dist/access-authorization-probe.js +2 -2
- package/dist/access-boundary.d.ts +2 -1
- package/dist/access-boundary.js +1 -1
- package/dist/access-cli.js +14 -14
- package/dist/access-extraction-force-flush.d.ts +2 -1
- package/dist/access-extraction-force-flush.js +1 -1
- package/dist/access-http-lcm-compaction.d.ts +1 -1
- package/dist/access-http-lifecycle-flush.d.ts +1 -1
- package/dist/access-http-offline-stream.d.ts +2 -1
- package/dist/access-http-query.js +1 -1
- package/dist/access-http.d.ts +3 -2
- package/dist/access-http.js +11 -11
- package/dist/access-identity-continuity-surface.js +1 -1
- package/dist/access-lcm-surface.d.ts +2 -1
- package/dist/access-lcm-surface.js +1 -1
- package/dist/access-mcp.d.ts +2 -1
- package/dist/access-mcp.js +7 -7
- package/dist/access-namespace-preflight.js +1 -1
- package/dist/access-observe-write-surface.d.ts +2 -1
- package/dist/access-observe-write-surface.js +1 -1
- package/dist/access-operations-batch.js +3 -2
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +6 -6
- package/dist/access-recall-concurrency.d.ts +2 -1
- package/dist/access-recall-concurrency.js +1 -1
- package/dist/access-recall-response.d.ts +2 -1
- package/dist/access-recall-response.js +1 -1
- package/dist/access-recall-surface.d.ts +2 -1
- package/dist/access-recall-surface.js +1 -1
- package/dist/access-schema.d.ts +92 -92
- package/dist/access-schema.js +1 -1
- package/dist/{access-service-C0AE0jLs.d.ts → access-service-U6jiERic.d.ts} +80 -155
- package/dist/access-service-helpers.d.ts +2 -1
- package/dist/access-service.d.ts +2 -1
- package/dist/access-service.js +1 -1
- package/dist/access-surface-catalog.d.ts +2 -1
- package/dist/access-surface-catalog.js +3 -3
- package/dist/{chunk-R7FQQQ23.js → chunk-36XS2ASP.js} +2 -2
- package/dist/{chunk-EPRBTGLM.js → chunk-57WMVU4D.js} +50 -41
- package/dist/chunk-57WMVU4D.js.map +1 -0
- package/dist/{chunk-34E5NBQI.js → chunk-BYFKJXIR.js} +2 -2
- package/dist/{chunk-LQNBQEMC.js → chunk-DIJCMHUE.js} +2 -2
- package/dist/{chunk-MNI7KB4Q.js → chunk-HP6B732E.js} +4 -4
- package/dist/{chunk-XFJUPJD7.js → chunk-IRXMJLTW.js} +11 -2
- package/dist/{chunk-XFJUPJD7.js.map → chunk-IRXMJLTW.js.map} +1 -1
- package/dist/{chunk-SAWGVTCE.js → chunk-IUSV4XJC.js} +2 -2
- package/dist/{chunk-K477BTF4.js → chunk-JASFZQ76.js} +4 -4
- package/dist/{chunk-SILX74CP.js → chunk-NWHKHBL4.js} +43 -63
- package/dist/chunk-NWHKHBL4.js.map +1 -0
- package/dist/{chunk-JJ45UTNT.js → chunk-R5AWIILH.js} +22 -27
- package/dist/chunk-R5AWIILH.js.map +1 -0
- package/dist/{chunk-IOE75NA4.js → chunk-V2YH7T6T.js} +2 -2
- package/dist/{chunk-H5VDBZLZ.js → chunk-VCEE4YNA.js} +2 -2
- package/dist/{chunk-U5XC6DIS.js → chunk-XREXYQ5U.js} +4 -4
- package/dist/{chunk-MIM7QT6H.js → chunk-YI25J2JD.js} +1 -1
- package/dist/chunk-YI25J2JD.js.map +1 -0
- package/dist/{cli-xoJTOdMQ.d.ts → cli-BsaYHAH9.d.ts} +1 -1
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +12 -12
- package/dist/external-wiki-access.d.ts +2 -1
- package/dist/external-wiki-access.js +2 -2
- package/dist/external-wiki-mcp-tools.d.ts +2 -1
- package/dist/index.d.ts +680 -680
- package/dist/index.js +21 -21
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/orchestrator.js +14 -14
- package/dist/{public-http-DR3sHfwk.d.ts → public-http-jMlmLny4.d.ts} +1 -1
- package/dist/schemas.d.ts +108 -108
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/support-passport/index.d.ts +5 -4
- package/dist/support-passport/index.js +4 -4
- package/dist/transfer/types.d.ts +66 -66
- package/package.json +2 -2
- package/src/access-http.ts +47 -60
- package/src/access-mcp.test.ts +6 -10
- package/src/access-mcp.ts +21 -22
- package/src/access-operations-batch.ts +66 -18
- package/src/access-schema.ts +12 -1
- package/src/access-service.ts +18 -91
- package/src/recall-disclosure.test.ts +1 -1
- package/dist/chunk-EPRBTGLM.js.map +0 -1
- package/dist/chunk-JJ45UTNT.js.map +0 -1
- package/dist/chunk-MIM7QT6H.js.map +0 -1
- package/dist/chunk-SILX74CP.js.map +0 -1
- /package/dist/{chunk-R7FQQQ23.js.map → chunk-36XS2ASP.js.map} +0 -0
- /package/dist/{chunk-34E5NBQI.js.map → chunk-BYFKJXIR.js.map} +0 -0
- /package/dist/{chunk-LQNBQEMC.js.map → chunk-DIJCMHUE.js.map} +0 -0
- /package/dist/{chunk-MNI7KB4Q.js.map → chunk-HP6B732E.js.map} +0 -0
- /package/dist/{chunk-SAWGVTCE.js.map → chunk-IUSV4XJC.js.map} +0 -0
- /package/dist/{chunk-K477BTF4.js.map → chunk-JASFZQ76.js.map} +0 -0
- /package/dist/{chunk-IOE75NA4.js.map → chunk-V2YH7T6T.js.map} +0 -0
- /package/dist/{chunk-H5VDBZLZ.js.map → chunk-VCEE4YNA.js.map} +0 -0
- /package/dist/{chunk-U5XC6DIS.js.map → chunk-XREXYQ5U.js.map} +0 -0
package/src/access-http.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type WearablesMeetingsScope,
|
|
17
17
|
type EngramAccessMemoryResponse,
|
|
18
18
|
type EngramAccessWriteResponse,
|
|
19
|
+
type EngramAccessRecallResponse,
|
|
19
20
|
} from "./access-service.js";
|
|
20
21
|
import { maybeHandleLifecycleFlush, type LifecycleFlushHttpDeps } from "./access-http-lifecycle-flush.js";
|
|
21
22
|
import {
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
33
34
|
OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
|
|
34
35
|
} from "./offline-sync.js";
|
|
35
|
-
import type { RecallDisclosure
|
|
36
|
+
import type { RecallDisclosure } from "./types.js";
|
|
36
37
|
import { isRecallDisclosure } from "./types.js";
|
|
37
38
|
import { isTrustZoneName, type TrustZoneName, type TrustZoneRecordKind, type TrustZoneSourceClass } from "./trust-zones.js";
|
|
38
39
|
import { AdapterRegistry, type ResolvedIdentity } from "./adapters/index.js";
|
|
@@ -1020,13 +1021,8 @@ export class EngramAccessHttpServer extends SupportPassportAccessHttpBase {
|
|
|
1020
1021
|
if (req.method === "POST" && pathname === "/engram/v1/recall") {
|
|
1021
1022
|
this.enforceTokenOp("recall"); // boundary dispatch (issue #1525)
|
|
1022
1023
|
const body = await this.readValidatedBody(req, "recall");
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
// (untouched). The previous `?? undefined` collapsed both into
|
|
1026
|
-
// undefined, so callers lost the ability to clear the session's
|
|
1027
|
-
// attached context through the recall endpoint.
|
|
1028
|
-
const codingContext =
|
|
1029
|
-
"codingContext" in body ? body.codingContext : undefined;
|
|
1024
|
+
// `codingContext` presence (null = explicit clear vs absent = untouched)
|
|
1025
|
+
// is preserved by spreading the parsed body into the boundary envelope.
|
|
1030
1026
|
// Disclosure resolution (issue #677 PR 2/4): accept the value from
|
|
1031
1027
|
// the validated body OR the `?disclosure=` query parameter, with
|
|
1032
1028
|
// the body taking precedence so an explicit JSON payload is never
|
|
@@ -1109,31 +1105,31 @@ export class EngramAccessHttpServer extends SupportPassportAccessHttpBase {
|
|
|
1109
1105
|
bodyIncludeLowConfidence === true ||
|
|
1110
1106
|
(bodyIncludeLowConfidence === undefined &&
|
|
1111
1107
|
queryIncludeLowConfidence === "true");
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1136
|
-
this.respondJson(res, 200,
|
|
1108
|
+
// Boundary dispatch (issue #2482): same registry path as memory_store —
|
|
1109
|
+
// the canonical recallRequestSchema re-validates the envelope the
|
|
1110
|
+
// transport assembled (resolved namespace + query-param overlays).
|
|
1111
|
+
const op = getOperation("recall");
|
|
1112
|
+
if (!op) {
|
|
1113
|
+
throw new EngramAccessInputError("access-boundary: operation not registered: recall");
|
|
1114
|
+
}
|
|
1115
|
+
const output = (await op.run(
|
|
1116
|
+
{
|
|
1117
|
+
...body,
|
|
1118
|
+
namespace: this.resolveNamespace(req, body.namespace),
|
|
1119
|
+
...(disclosure !== undefined ? { disclosure } : {}),
|
|
1120
|
+
...(asOf !== undefined ? { asOf } : {}),
|
|
1121
|
+
...(tags !== undefined ? { tags } : {}),
|
|
1122
|
+
...(tagMatch !== undefined ? { tagMatch } : {}),
|
|
1123
|
+
...(includeLowConfidence ? { includeLowConfidence: true } : {}),
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
service: this.service,
|
|
1127
|
+
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
1128
|
+
sourceConnector: this.resolveConnector(req),
|
|
1129
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
1130
|
+
},
|
|
1131
|
+
)) as { result: EngramAccessRecallResponse };
|
|
1132
|
+
this.respondJson(res, 200, output.result);
|
|
1137
1133
|
return;
|
|
1138
1134
|
}
|
|
1139
1135
|
|
|
@@ -1745,43 +1741,34 @@ export class EngramAccessHttpServer extends SupportPassportAccessHttpBase {
|
|
|
1745
1741
|
if (req.method === "POST" && pathname === "/engram/v1/observe") {
|
|
1746
1742
|
this.enforceTokenOp("observe"); // boundary dispatch (issue #1525)
|
|
1747
1743
|
const body = await this.readValidatedBody(req, "observe");
|
|
1748
|
-
|
|
1744
|
+
// Boundary dispatch (issue #2482): same registry path as memory_store.
|
|
1745
|
+
// The write-quota hook is forwarded via ctx.hooks so it fires INSIDE
|
|
1746
|
+
// the service's idempotency lock (beforeExecute, only on a real miss).
|
|
1747
|
+
// A retried observe that hits the replay path must NOT be rejected with
|
|
1748
|
+
// 429 — the original attempt may have consumed the last quota slot; the
|
|
1749
|
+
// retry returns the cached response without requiring another (#1434
|
|
1750
|
+
// invariant, preserved by the boundary migration).
|
|
1751
|
+
const op = getOperation("observe");
|
|
1752
|
+
if (!op) {
|
|
1753
|
+
throw new EngramAccessInputError("access-boundary: operation not registered: observe");
|
|
1754
|
+
}
|
|
1755
|
+
const output = (await op.run(
|
|
1756
|
+
{ ...body, namespace: this.resolveNamespace(req, body.namespace) },
|
|
1749
1757
|
{
|
|
1750
|
-
|
|
1751
|
-
messages: body.messages.map((message) => ({
|
|
1752
|
-
role: message.role,
|
|
1753
|
-
content: message.content,
|
|
1754
|
-
sourceFormat: message.sourceFormat ?? undefined,
|
|
1755
|
-
rawContent: message.rawContent ?? undefined,
|
|
1756
|
-
parts: message.parts ?? undefined,
|
|
1757
|
-
})),
|
|
1758
|
-
namespace: this.resolveNamespace(req, body.namespace),
|
|
1758
|
+
service: this.service,
|
|
1759
1759
|
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
1760
|
-
|
|
1761
|
-
// Issue #1649: optional server-side dedup key for retried POSTs.
|
|
1762
|
-
idempotencyKey: body.idempotencyKey,
|
|
1763
|
-
// Forward cwd/projectTag for auto git-context resolution (issue #569).
|
|
1764
|
-
cwd: body.cwd,
|
|
1765
|
-
projectTag: body.projectTag,
|
|
1760
|
+
hooks: { enforceWriteQuota: () => this.ensureWriteRateLimitAvailable(req) },
|
|
1766
1761
|
// Phase 1 provenance: server-resolved connector identity from the
|
|
1767
1762
|
// bearer token (REST path, mirroring the MCP tools/call dispatch).
|
|
1768
1763
|
sourceConnector: this.resolveConnector(req),
|
|
1769
1764
|
},
|
|
1770
|
-
|
|
1771
|
-
// (beforeExecute, only on a real miss). A retried observe that hits the
|
|
1772
|
-
// replay path must NOT be rejected with 429 — that is exactly the
|
|
1773
|
-
// response-lost-after-process case the dedup exists for. The original
|
|
1774
|
-
// attempt may have consumed the last quota slot; the retry returns the
|
|
1775
|
-
// cached response without requiring another. Matches memory_store's
|
|
1776
|
-
// hook-in-the-lock pattern (#1434 invariant).
|
|
1777
|
-
{ enforceWriteQuota: () => this.ensureWriteRateLimitAvailable(req) },
|
|
1778
|
-
);
|
|
1765
|
+
)) as { result: { dryRun?: boolean; idempotencyReplay?: boolean } };
|
|
1779
1766
|
// A replayed (deduplicated) observe must not consume a second write-quota
|
|
1780
1767
|
// slot — same invariant as memory_store/suggestion_submit (#1434).
|
|
1781
|
-
if (this.shouldCountWriteRateLimit(
|
|
1768
|
+
if (this.shouldCountWriteRateLimit(output.result)) {
|
|
1782
1769
|
this.recordWriteRateLimitHit(req);
|
|
1783
1770
|
}
|
|
1784
|
-
this.respondJson(res, 202,
|
|
1771
|
+
this.respondJson(res, 202, output.result);
|
|
1785
1772
|
return;
|
|
1786
1773
|
}
|
|
1787
1774
|
|
package/src/access-mcp.test.ts
CHANGED
|
@@ -472,16 +472,12 @@ test("MCP session override is injected only into tools that accept sessionKey",
|
|
|
472
472
|
namespace: undefined,
|
|
473
473
|
principal: "adapter-agent",
|
|
474
474
|
});
|
|
475
|
-
assert.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
idempotencyKey: undefined,
|
|
482
|
-
cwd: undefined,
|
|
483
|
-
projectTag: undefined,
|
|
484
|
-
});
|
|
475
|
+
assert.equal(observeArgs?.sessionKey, "adapter-session");
|
|
476
|
+
assert.equal(observeArgs?.skipExtraction, false);
|
|
477
|
+
const observeMessages = observeArgs?.messages as Array<{ role: string; content: string }>;
|
|
478
|
+
assert.equal(observeMessages?.length, 1);
|
|
479
|
+
assert.equal(observeMessages?.[0]?.role, "user");
|
|
480
|
+
assert.equal(observeMessages?.[0]?.content, "hello");
|
|
485
481
|
assert.equal((capsuleResponse as Record<string, unknown> & { result?: { isError?: boolean } }).result?.isError, false);
|
|
486
482
|
assert.equal((observeResponse as Record<string, unknown> & { result?: { isError?: boolean } }).result?.isError, false);
|
|
487
483
|
});
|
package/src/access-mcp.ts
CHANGED
|
@@ -326,6 +326,24 @@ const STRICT_MCP_SCHEMA_KEYS: Partial<Record<SchemaName, readonly string[]>> = {
|
|
|
326
326
|
capsuleList: ["namespace", "sessionKey", "cwd", "projectTag"],
|
|
327
327
|
};
|
|
328
328
|
|
|
329
|
+
// Tools whose envelope parses through the canonical zod wire schema (issue
|
|
330
|
+
// #2482) — the SAME validation the HTTP transport runs. Every other migrated
|
|
331
|
+
// tool passes raw args; its boundary op schema validates them.
|
|
332
|
+
const MCP_WIRE_SCHEMAS: Partial<Record<string, SchemaName>> = {
|
|
333
|
+
memory_store: "memoryStore",
|
|
334
|
+
suggestion_submit: "suggestionSubmit",
|
|
335
|
+
action_confidence: "actionConfidence",
|
|
336
|
+
day_summary: "daySummary",
|
|
337
|
+
capsule_export: "capsuleExport",
|
|
338
|
+
capsule_import: "capsuleImport",
|
|
339
|
+
capsule_list: "capsuleList",
|
|
340
|
+
observe: "observe",
|
|
341
|
+
recall: "recall",
|
|
342
|
+
lcm_compaction_flush: "lcmCompactionFlush",
|
|
343
|
+
extraction_force_flush: "extractionForceFlush",
|
|
344
|
+
lcm_compaction_record: "lcmCompactionRecord",
|
|
345
|
+
};
|
|
346
|
+
|
|
329
347
|
// Shared JSON-schema fragments for the client-injected git/project context
|
|
330
348
|
// fields (#1434). Declared once to avoid drift across tool definitions.
|
|
331
349
|
// `_SCOPED` is for write tools that resolve a project namespace from these
|
|
@@ -2669,28 +2687,9 @@ export class EngramMcpServer {
|
|
|
2669
2687
|
throw new EngramAccessInputError(`access-boundary: operation not registered: ${migrated}`);
|
|
2670
2688
|
}
|
|
2671
2689
|
let envelope: Record<string, unknown>;
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
envelope = parseMcpRequest("suggestionSubmit", args);
|
|
2676
|
-
} else if (migrated === "action_confidence") {
|
|
2677
|
-
envelope = parseMcpRequest("actionConfidence", args);
|
|
2678
|
-
} else if (migrated === "day_summary") {
|
|
2679
|
-
envelope = parseMcpRequest("daySummary", args);
|
|
2680
|
-
} else if (migrated === "capsule_export") {
|
|
2681
|
-
envelope = parseMcpRequest("capsuleExport", args);
|
|
2682
|
-
} else if (migrated === "capsule_import") {
|
|
2683
|
-
envelope = parseMcpRequest("capsuleImport", args);
|
|
2684
|
-
} else if (migrated === "capsule_list") {
|
|
2685
|
-
envelope = parseMcpRequest("capsuleList", args);
|
|
2686
|
-
} else if (migrated === "observe") {
|
|
2687
|
-
envelope = parseMcpRequest("observe", args);
|
|
2688
|
-
} else if (migrated === "lcm_compaction_flush") {
|
|
2689
|
-
envelope = parseMcpRequest("lcmCompactionFlush", args);
|
|
2690
|
-
} else if (migrated === "extraction_force_flush") {
|
|
2691
|
-
envelope = parseMcpRequest("extractionForceFlush", args);
|
|
2692
|
-
} else if (migrated === "lcm_compaction_record") {
|
|
2693
|
-
envelope = parseMcpRequest("lcmCompactionRecord", args);
|
|
2690
|
+
const wireSchema = MCP_WIRE_SCHEMAS[migrated];
|
|
2691
|
+
if (wireSchema) {
|
|
2692
|
+
envelope = parseMcpRequest(wireSchema, args) as Record<string, unknown>;
|
|
2694
2693
|
} else if (migrated.startsWith("codegraph_")) {
|
|
2695
2694
|
envelope = { ...args, tool: migrated.slice("codegraph_".length) };
|
|
2696
2695
|
} else if (migrated === "chat_message") {
|
|
@@ -15,6 +15,7 @@ import type { DreamsPhase, RecallDisclosure, RecallPlanMode } from "./types.js";
|
|
|
15
15
|
import type { RemnicChatGptMemoryInspectorInput } from "./mcp-memory-inspector-app.js";
|
|
16
16
|
import { defineOperation } from "./access-boundary.js";
|
|
17
17
|
import { EngramAccessForbiddenError } from "./access-errors.js";
|
|
18
|
+
import { type ObserveRequest, observeRequestSchema, type RecallRequest, recallRequestSchema } from "./access-schema.js";
|
|
18
19
|
import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
|
|
19
20
|
import { enforceNamespaceAllowList, tokenCapabilityStore } from "./access-token-capabilities.js";
|
|
20
21
|
import { projectTagProjectId } from "./coding/coding-namespace.js";
|
|
@@ -57,6 +58,24 @@ function strictSchema<T extends z.ZodRawShape>(shape: T): z.ZodType<Record<strin
|
|
|
57
58
|
return z.preprocess(stripNulls, z.object(shape).passthrough()) as unknown as z.ZodType<Record<string, unknown>>;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* stripNulls, but keys listed in `preserve` keep their null values. Used by
|
|
63
|
+
* boundary schemas whose wire contract treats an explicit `null` as a
|
|
64
|
+
* meaningful value (e.g. recall `codingContext: null` clears the session
|
|
65
|
+
* context — it must not be collapsed into "absent").
|
|
66
|
+
*/
|
|
67
|
+
function stripNullsExcept(data: unknown, preserve: readonly string[]): unknown {
|
|
68
|
+
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
|
69
|
+
const cleaned: Record<string, unknown> = {};
|
|
70
|
+
for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
|
|
71
|
+
if (value !== null || preserve.includes(key)) cleaned[key] = value;
|
|
72
|
+
}
|
|
73
|
+
return cleaned;
|
|
74
|
+
}
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
60
79
|
function optStr(v: unknown): string | undefined { return typeof v === "string" ? v : undefined; }
|
|
61
80
|
function reqStr(v: unknown, field: string): string {
|
|
62
81
|
if (typeof v !== "string" || v.length === 0) throw new EngramAccessInputError(field + " is required and must be a non-empty string");
|
|
@@ -77,24 +96,26 @@ const S = {
|
|
|
77
96
|
};
|
|
78
97
|
|
|
79
98
|
// === RECALL ===
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
// Issue #2482: the boundary validates against the SAME canonical zod schema
|
|
100
|
+
// the HTTP transport uses (recallRequestSchema). No parallel field list, no
|
|
101
|
+
// silent defaults — a missing `query` or a bad enum is a 400 at the boundary.
|
|
102
|
+
// `codingContext: null` survives the null-strip (explicit session-context
|
|
103
|
+
// clear); every other null is the MCP null-for-absent idiom.
|
|
104
|
+
defineOperation({
|
|
105
|
+
name: "recall",
|
|
106
|
+
description: "Semantic recall.",
|
|
107
|
+
schema: z.preprocess((data) => stripNullsExcept(data, ["codingContext"]), recallRequestSchema) as unknown as z.ZodType<RecallRequest>,
|
|
108
|
+
handler: async (input, ctx) => ({
|
|
109
|
+
result: await ctx.service.recall({
|
|
110
|
+
...input,
|
|
111
|
+
authenticatedPrincipal: ctx.authenticatedPrincipal,
|
|
112
|
+
...(ctx.sourceConnector ? { sourceConnector: ctx.sourceConnector } : {}),
|
|
113
|
+
...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
94
116
|
});
|
|
95
117
|
defineOperation({ name: "recall_explain", description: "Explain recall plan.", schema: strictSchema({ sessionKey: S.str, namespace: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.recallExplain({ sessionKey: optStr(input.sessionKey), namespace: optStr(input.namespace), authenticatedPrincipal: ctx.authenticatedPrincipal }) }) });
|
|
96
|
-
defineOperation({ name: "set_coding_context", description: "Set coding context.", schema: z.preprocess((data) => {
|
|
97
|
-
if (data !== null && typeof data === "object" && !Array.isArray(data)) { const o = data as Record<string, unknown>; const c: Record<string, unknown> = {}; for (const [k, v] of Object.entries(o)) { if (v !== null || k === "codingContext") c[k] = v; } return c; } return data; }, z.object({ sessionKey: S.str, codingContext: z.union([z.null(), z.object({ projectId: S.str, branch: z.union([z.string(), z.null()]).optional(), rootPath: S.str, defaultBranch: z.union([z.string(), z.null()]).optional() }).passthrough()]).optional(), projectTag: S.str }).passthrough()) as unknown as z.ZodType<Record<string, unknown>>,
|
|
118
|
+
defineOperation({ name: "set_coding_context", description: "Set coding context.", schema: z.preprocess((data) => stripNullsExcept(data, ["codingContext"]), z.object({ sessionKey: S.str, codingContext: z.union([z.null(), z.object({ projectId: S.str, branch: z.union([z.string(), z.null()]).optional(), rootPath: S.str, defaultBranch: z.union([z.string(), z.null()]).optional() }).passthrough()]).optional(), projectTag: S.str }).passthrough()) as unknown as z.ZodType<Record<string, unknown>>, // codingContext: null = clear context (preserved).
|
|
98
119
|
handler: async (input, ctx) => {
|
|
99
120
|
const sessionKey = defStr(input.sessionKey, "");
|
|
100
121
|
const projectTag = optStr(input.projectTag);
|
|
@@ -190,8 +211,35 @@ defineOperation({ name: "memory_timeline", description: "Memory timeline.", sche
|
|
|
190
211
|
defineOperation({ name: "suggestion_submit", description: "Submit suggestion.", schema: strictSchema({ schemaVersion: S.num, idempotencyKey: S.str, dryRun: S.bool, sessionKey: S.str, content: S.str, category: S.str, confidence: S.num, namespace: S.str, tags: S.strArr, entityRef: S.str, ttl: S.str, sourceReason: S.str, cwd: S.str, projectTag: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.suggestionSubmit({ schemaVersion: optNum(input.schemaVersion), idempotencyKey: optStr(input.idempotencyKey), dryRun: input.dryRun === true, sessionKey: optStr(input.sessionKey), authenticatedPrincipal: ctx.authenticatedPrincipal, ...(ctx.sourceConnector ? { sourceConnector: ctx.sourceConnector } : {}), content: optStr(input.content) ?? "", category: optStr(input.category), confidence: optNum(input.confidence), namespace: optStr(input.namespace), tags: optStrArr(input.tags), entityRef: optStr(input.entityRef), ttl: optStr(input.ttl), sourceReason: optStr(input.sourceReason), cwd: optStr(input.cwd), projectTag: optStr(input.projectTag) }) }) });
|
|
191
212
|
defineOperation({ name: "entity_get", description: "Get entity.", schema: strictSchema({ name: S.str, namespace: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.entityGet(optStr(input.name) ?? "", optStr(input.namespace)) }) });
|
|
192
213
|
defineOperation({ name: "review_queue_list", description: "List review queue.", schema: strictSchema({ runId: S.str, namespace: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.reviewQueue(optStr(input.runId) ?? "", optStr(input.namespace), ctx.authenticatedPrincipal) }) });
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
// Issue #2482: observe validates through the canonical observeRequestSchema
|
|
215
|
+
// (same as HTTP/MCP wire validation). Nullable wire fields (parts /
|
|
216
|
+
// sourceFormat) map to undefined for the service's cleaned message form.
|
|
217
|
+
defineOperation({
|
|
218
|
+
name: "observe",
|
|
219
|
+
description: "Observe messages.",
|
|
220
|
+
schema: z.preprocess(stripNulls, observeRequestSchema) as unknown as z.ZodType<ObserveRequest>,
|
|
221
|
+
handler: async (input, ctx) => ({
|
|
222
|
+
result: await ctx.service.observe(
|
|
223
|
+
{
|
|
224
|
+
sessionKey: input.sessionKey,
|
|
225
|
+
messages: input.messages.map((message) => ({
|
|
226
|
+
role: message.role,
|
|
227
|
+
content: message.content,
|
|
228
|
+
...(message.parts ? { parts: message.parts } : {}),
|
|
229
|
+
...(message.rawContent !== undefined ? { rawContent: message.rawContent } : {}),
|
|
230
|
+
...(message.sourceFormat ? { sourceFormat: message.sourceFormat } : {}),
|
|
231
|
+
})),
|
|
232
|
+
skipExtraction: input.skipExtraction === true,
|
|
233
|
+
idempotencyKey: input.idempotencyKey,
|
|
234
|
+
namespace: input.namespace,
|
|
235
|
+
authenticatedPrincipal: ctx.authenticatedPrincipal,
|
|
236
|
+
cwd: input.cwd,
|
|
237
|
+
projectTag: input.projectTag,
|
|
238
|
+
...(ctx.sourceConnector ? { sourceConnector: ctx.sourceConnector } : {}),
|
|
239
|
+
},
|
|
240
|
+
ctx.hooks?.enforceWriteQuota ? { enforceWriteQuota: ctx.hooks.enforceWriteQuota } : undefined,
|
|
241
|
+
),
|
|
242
|
+
}),
|
|
195
243
|
});
|
|
196
244
|
defineOperation({ name: "lcm_search", description: "Search LCM.", schema: strictSchema({ query: S.str, sessionKey: S.str, sessionPrefix: S.str, namespace: S.str, limit: S.num }), handler: async (input, ctx) => ({ result: await ctx.service.lcmSearch({ query: defStr(input.query, ""), sessionKey: optStr(input.sessionKey), sessionPrefix: optStr(input.sessionPrefix), namespace: optStr(input.namespace), limit: optNum(input.limit), authenticatedPrincipal: ctx.authenticatedPrincipal }) }) });
|
|
197
245
|
defineOperation({ name: "lcm_compaction_flush", description: "Flush LCM compaction.", schema: strictSchema({ sessionKey: S.str, namespace: S.str, cwd: S.str, projectTag: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.lcmCompactionFlush({ sessionKey: optStr(input.sessionKey) ?? "", namespace: optStr(input.namespace), cwd: optStr(input.cwd), projectTag: optStr(input.projectTag), authenticatedPrincipal: ctx.authenticatedPrincipal }) }) });
|
package/src/access-schema.ts
CHANGED
|
@@ -706,9 +706,20 @@ export function validateRequest<T = unknown>(
|
|
|
706
706
|
},
|
|
707
707
|
};
|
|
708
708
|
}
|
|
709
|
-
const result = schema.safeParse(body);
|
|
709
|
+
const result = schema.safeParse(stripNullsExceptCodingContext(body));
|
|
710
710
|
if (result.success) {
|
|
711
711
|
return { success: true, data: result.data as T };
|
|
712
712
|
}
|
|
713
713
|
return { success: false, error: formatZodError(result.error) };
|
|
714
714
|
}
|
|
715
|
+
|
|
716
|
+
function stripNullsExceptCodingContext(data: unknown): unknown {
|
|
717
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return data;
|
|
718
|
+
const cleaned: Record<string, unknown> = {};
|
|
719
|
+
for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
|
|
720
|
+
if (value === null && key !== "codingContext") continue;
|
|
721
|
+
cleaned[key] = value;
|
|
722
|
+
}
|
|
723
|
+
return cleaned;
|
|
724
|
+
}
|
|
725
|
+
|
package/src/access-service.ts
CHANGED
|
@@ -278,6 +278,7 @@ import {
|
|
|
278
278
|
recordMemoryOutcome,
|
|
279
279
|
} from "./memory-worth-outcomes.js";
|
|
280
280
|
import type { LcmMessagePartInput, MessagePartSourceFormat } from "./message-parts/index.js";
|
|
281
|
+
import type { ObserveRequest, RecallRequest } from "./access-schema.js";
|
|
281
282
|
import { recordObjectiveStateSnapshotsFromObservedMessages } from "./objective-state-writers.js";
|
|
282
283
|
import { objectiveStateStoreOverrideForNamespace } from "./objective-state.js";
|
|
283
284
|
import { offlineSyncStorageForSnapshot } from "./offline-sync-impression-drain.js";
|
|
@@ -373,17 +374,16 @@ function normalizeTrustZoneInputError(error: unknown): EngramAccessInputError |
|
|
|
373
374
|
|
|
374
375
|
export const ENGRAM_ACCESS_WRITE_SCHEMA_VERSION = 1;
|
|
375
376
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Recall request. Wire fields derive from the canonical zod schema
|
|
379
|
+
* (`recallRequestSchema`, access-schema.ts — issue #2482) so the HTTP body,
|
|
380
|
+
* the MCP args, and the service share ONE model. The overlay below carries
|
|
381
|
+
* server-resolved transport fields no client may set.
|
|
382
|
+
*/
|
|
383
|
+
export interface EngramAccessRecallRequest extends RecallRequest {
|
|
380
384
|
authenticatedPrincipal?: string;
|
|
381
385
|
/** Trusted connector identity resolved at the transport boundary. */
|
|
382
386
|
sourceConnector?: string;
|
|
383
|
-
idempotencyKey?: string;
|
|
384
|
-
topK?: number;
|
|
385
|
-
mode?: RecallPlanMode | "auto";
|
|
386
|
-
includeDebug?: boolean;
|
|
387
387
|
abortSignal?: AbortSignal;
|
|
388
388
|
/**
|
|
389
389
|
* Internal, server-set field (issue #1906): wall-clock ms the caller
|
|
@@ -393,70 +393,6 @@ export interface EngramAccessRecallRequest {
|
|
|
393
393
|
* (same pattern as `abortSignal`).
|
|
394
394
|
*/
|
|
395
395
|
queueWaitMs?: number;
|
|
396
|
-
/**
|
|
397
|
-
* Recall disclosure depth. Omitting it preserves the `"chunk"` default.
|
|
398
|
-
* Other accepted values are `"section"` and `"raw"`.
|
|
399
|
-
*/
|
|
400
|
-
disclosure?: RecallDisclosure;
|
|
401
|
-
/**
|
|
402
|
-
* Coding-agent context (issue #569). When a connector resolves a git
|
|
403
|
-
* context for the session's cwd, it passes it here and the access service
|
|
404
|
-
* attaches it to the orchestrator before recall so project- / branch-
|
|
405
|
-
* scoped namespace overlays apply.
|
|
406
|
-
*
|
|
407
|
-
* Keyed by `sessionKey`; ignored when `sessionKey` is absent.
|
|
408
|
-
*/
|
|
409
|
-
codingContext?: {
|
|
410
|
-
projectId: string;
|
|
411
|
-
branch: string | null;
|
|
412
|
-
rootPath: string;
|
|
413
|
-
defaultBranch: string | null;
|
|
414
|
-
} | null;
|
|
415
|
-
/**
|
|
416
|
-
* Working directory of the calling agent session. When provided and no
|
|
417
|
-
* `codingContext` is already attached to the session, the service resolves
|
|
418
|
-
* git context from this path and attaches it automatically. This enables
|
|
419
|
-
* Claude Code hooks (and any connector that knows its cwd) to get
|
|
420
|
-
* project-scoped memory without explicitly constructing a `codingContext`.
|
|
421
|
-
*/
|
|
422
|
-
cwd?: string;
|
|
423
|
-
/**
|
|
424
|
-
* Arbitrary project tag for non-git-based project scoping. When provided,
|
|
425
|
-
* creates a `CodingContext` with `projectId: "tag:<projectTag>"`.
|
|
426
|
-
* Useful for OpenClaw sessions where the whole workspace is one git repo
|
|
427
|
-
* but different conversations correspond to different client projects.
|
|
428
|
-
* Takes precedence over `cwd`-based git resolution but NOT over an
|
|
429
|
-
* explicit `codingContext`.
|
|
430
|
-
*/
|
|
431
|
-
projectTag?: string;
|
|
432
|
-
/**
|
|
433
|
-
* Historical recall pin (issue #680). ISO 8601 timestamp. When set,
|
|
434
|
-
* the orchestrator filters out memories whose `valid_at` is after this
|
|
435
|
-
* instant OR whose `invalid_at` is at-or-before this instant, so the
|
|
436
|
-
* caller sees the corpus as it existed at `asOf`. Invalid values are
|
|
437
|
-
* rejected here with `EngramAccessInputError` (CLAUDE.md rule 51).
|
|
438
|
-
*/
|
|
439
|
-
asOf?: string;
|
|
440
|
-
/**
|
|
441
|
-
* Free-form recall tag filter (issue #689). When non-empty, recall results
|
|
442
|
-
* whose frontmatter `tags` do not match are removed from the response.
|
|
443
|
-
* Comparison is case-sensitive exact match against tags stored on each
|
|
444
|
-
* memory's frontmatter (see `storage.ts` and `docs/tags.md`).
|
|
445
|
-
*/
|
|
446
|
-
tags?: string[];
|
|
447
|
-
/**
|
|
448
|
-
* Match mode for `tags` (issue #689). `"any"` (default when omitted)
|
|
449
|
-
* admits results that carry at least one filter tag. `"all"` requires
|
|
450
|
-
* every filter tag to be present. Ignored when `tags` is absent or empty.
|
|
451
|
-
*/
|
|
452
|
-
tagMatch?: "any" | "all";
|
|
453
|
-
/**
|
|
454
|
-
* Issue #681 — when `true`, bypasses the configured
|
|
455
|
-
* `graphTraversalConfidenceFloor` for this recall and includes graph edges
|
|
456
|
-
* below the floor in traversal. Useful for diagnostic queries that need to
|
|
457
|
-
* surface results pruned by confidence decay. Default `false`.
|
|
458
|
-
*/
|
|
459
|
-
includeLowConfidence?: boolean;
|
|
460
396
|
}
|
|
461
397
|
|
|
462
398
|
/**
|
|
@@ -1033,27 +969,18 @@ export interface EngramAccessObserveMessage {
|
|
|
1033
969
|
sourceFormat?: MessagePartSourceFormat;
|
|
1034
970
|
}
|
|
1035
971
|
|
|
1036
|
-
|
|
1037
|
-
|
|
972
|
+
/**
|
|
973
|
+
* Observe request. Wire fields derive from the canonical zod schema
|
|
974
|
+
* (`observeRequestSchema`, access-schema.ts — issue #2482); the service
|
|
975
|
+
* consumes the transport-cleaned message form (the wire schema tolerates
|
|
976
|
+
* nullable `parts`/`sourceFormat`, the service maps them to `undefined`).
|
|
977
|
+
*/
|
|
978
|
+
export interface EngramAccessObserveRequest
|
|
979
|
+
extends Omit<ObserveRequest, "messages">,
|
|
980
|
+
SourceConnectorProvenance {
|
|
1038
981
|
messages: EngramAccessObserveMessage[];
|
|
1039
|
-
namespace?: string;
|
|
1040
982
|
authenticatedPrincipal?: string;
|
|
1041
|
-
|
|
1042
|
-
/** Optional idempotency key (issue #1649): a retried POST with the same key is deduplicated server-side; divergent reuse is a conflict. */
|
|
1043
|
-
idempotencyKey?: string;
|
|
1044
|
-
/**
|
|
1045
|
-
* Working directory of the calling agent session (issue #569 wiring).
|
|
1046
|
-
* When provided and no `codingContext` is attached for this session,
|
|
1047
|
-
* resolves git context from the path and attaches it so writes route to
|
|
1048
|
-
* the correct project namespace.
|
|
1049
|
-
*/
|
|
1050
|
-
cwd?: string;
|
|
1051
|
-
/**
|
|
1052
|
-
* Arbitrary project tag for non-git-based project scoping (issue #569).
|
|
1053
|
-
* Creates a `CodingContext` with `projectId: "tag:<projectTag>"`.
|
|
1054
|
-
*/
|
|
1055
|
-
projectTag?: string;
|
|
1056
|
-
readonly suppressQuarantine?: boolean; // #1888: mirrors EngramAccessWriteEnvelope.suppressQuarantine (replay re-submit skips dead-lettering).
|
|
983
|
+
readonly suppressQuarantine?: boolean; // #1888: replay re-submit skips dead-lettering.
|
|
1057
984
|
}
|
|
1058
985
|
|
|
1059
986
|
/**
|
|
@@ -191,7 +191,7 @@ test("MCP engram.recall rejects non-string disclosure with a structured error",
|
|
|
191
191
|
const result = (response as { result?: { isError?: boolean; content?: Array<{ text?: string }> } } | undefined)?.result;
|
|
192
192
|
assert.strictEqual(result?.isError, true);
|
|
193
193
|
const text = result?.content?.[0]?.text ?? "";
|
|
194
|
-
assert.match(text, /disclosure
|
|
194
|
+
assert.match(text, /disclosure/i);
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
test("MCP engram.recall treats explicit `null` disclosure as absent (service applies default)", async () => {
|