@remnic/core 9.3.614 → 9.3.616

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.
Files changed (49) hide show
  1. package/dist/access-cli.js +3 -3
  2. package/dist/access-http.d.ts +1 -1
  3. package/dist/access-http.js +5 -5
  4. package/dist/access-mcp.d.ts +1 -1
  5. package/dist/access-mcp.js +4 -4
  6. package/dist/access-schema.d.ts +48 -36
  7. package/dist/access-schema.js +1 -1
  8. package/dist/{access-service-DGG_2xPK.d.ts → access-service-CBNEKjzN.d.ts} +70 -5
  9. package/dist/access-service.d.ts +1 -1
  10. package/dist/access-service.js +2 -2
  11. package/dist/{chunk-B6FDZPCF.js → chunk-5OHHEORR.js} +50 -15
  12. package/dist/chunk-5OHHEORR.js.map +1 -0
  13. package/dist/{chunk-T5XWMMU2.js → chunk-EXUAP5LH.js} +2 -2
  14. package/dist/{chunk-EUML3N6B.js → chunk-IMA6GU4Y.js} +3 -3
  15. package/dist/chunk-IMA6GU4Y.js.map +1 -0
  16. package/dist/{chunk-7YQFWOF7.js → chunk-KGLPJROV.js} +4 -4
  17. package/dist/{chunk-VPGUMLBA.js → chunk-NM5NQYJE.js} +16 -16
  18. package/dist/chunk-NM5NQYJE.js.map +1 -0
  19. package/dist/{chunk-QEMCQFDW.js → chunk-WD2W4234.js} +8 -2
  20. package/dist/chunk-WD2W4234.js.map +1 -0
  21. package/dist/{chunk-ADNZVFXG.js → chunk-ZK32E74R.js} +142 -31
  22. package/dist/chunk-ZK32E74R.js.map +1 -0
  23. package/dist/{cli-DWeu7eTY.d.ts → cli-Cw729yLf.d.ts} +1 -1
  24. package/dist/cli.d.ts +2 -2
  25. package/dist/cli.js +6 -6
  26. package/dist/explicit-capture.d.ts +10 -0
  27. package/dist/explicit-capture.js +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.js +7 -7
  30. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  31. package/dist/orchestrator.js +2 -2
  32. package/dist/schemas.d.ts +64 -64
  33. package/dist/shared-context/manager.d.ts +2 -2
  34. package/dist/transfer/types.d.ts +12 -12
  35. package/package.json +1 -1
  36. package/src/access-http.ts +21 -10
  37. package/src/access-mcp.test.ts +109 -0
  38. package/src/access-mcp.ts +46 -2
  39. package/src/access-schema.ts +11 -0
  40. package/src/access-service-coding-write.test.ts +478 -0
  41. package/src/access-service.ts +237 -32
  42. package/src/explicit-capture.ts +19 -2
  43. package/dist/chunk-ADNZVFXG.js.map +0 -1
  44. package/dist/chunk-B6FDZPCF.js.map +0 -1
  45. package/dist/chunk-EUML3N6B.js.map +0 -1
  46. package/dist/chunk-QEMCQFDW.js.map +0 -1
  47. package/dist/chunk-VPGUMLBA.js.map +0 -1
  48. /package/dist/{chunk-T5XWMMU2.js.map → chunk-EXUAP5LH.js.map} +0 -0
  49. /package/dist/{chunk-7YQFWOF7.js.map → chunk-KGLPJROV.js.map} +0 -0
@@ -1094,12 +1094,20 @@ export class EngramAccessHttpServer {
1094
1094
  entityRef: body.entityRef,
1095
1095
  ttl: body.ttl,
1096
1096
  sourceReason: body.sourceReason,
1097
+ cwd: body.cwd,
1098
+ projectTag: body.projectTag,
1097
1099
  };
1098
- const idempotencyStatus = await this.service.peekMemoryStoreIdempotency(request);
1099
- if (idempotencyStatus === "miss" && request.dryRun !== true) {
1100
- this.ensureWriteRateLimitAvailable();
1101
- }
1102
- const response = await this.service.memoryStore(request);
1100
+ // Rate-limit enforcement is SOLELY authoritative inside memoryStore via
1101
+ // enforceWriteQuota: it runs atomically with the real idempotency-miss
1102
+ // determination (and the real resolved namespace), and is never invoked
1103
+ // for a replay. We deliberately do NOT pre-check here: the write namespace
1104
+ // is resolved from mutable session/git context, so a stale peek could
1105
+ // report "miss" for a request that is actually an idempotent replay in the
1106
+ // now-scoped namespace and hard-reject a safe replay with 429 (#1434 Codex
1107
+ // review). Letting the in-lock hook be the only hard gate avoids that.
1108
+ const response = await this.service.memoryStore(request, {
1109
+ enforceWriteQuota: () => this.ensureWriteRateLimitAvailable(),
1110
+ });
1103
1111
  if (this.shouldCountWriteRateLimit(response as { dryRun?: boolean; idempotencyReplay?: boolean })) {
1104
1112
  this.recordWriteRateLimitHit();
1105
1113
  }
@@ -1123,12 +1131,15 @@ export class EngramAccessHttpServer {
1123
1131
  entityRef: body.entityRef,
1124
1132
  ttl: body.ttl,
1125
1133
  sourceReason: body.sourceReason,
1134
+ cwd: body.cwd,
1135
+ projectTag: body.projectTag,
1126
1136
  };
1127
- const idempotencyStatus = await this.service.peekSuggestionSubmitIdempotency(request);
1128
- if (idempotencyStatus === "miss" && request.dryRun !== true) {
1129
- this.ensureWriteRateLimitAvailable();
1130
- }
1131
- const response = await this.service.suggestionSubmit(request);
1137
+ // Quota enforcement is solely authoritative inside suggestionSubmit
1138
+ // (enforceWriteQuota), atomic with the real miss and never on a replay; no
1139
+ // HTTP pre-check, so a stale peek can't 429 a safe replay (#1434 Codex review).
1140
+ const response = await this.service.suggestionSubmit(request, {
1141
+ enforceWriteQuota: () => this.ensureWriteRateLimitAvailable(),
1142
+ });
1132
1143
  if (this.shouldCountWriteRateLimit(response as { dryRun?: boolean; idempotencyReplay?: boolean })) {
1133
1144
  this.recordWriteRateLimitHit();
1134
1145
  }
@@ -262,6 +262,115 @@ test("MCP memory write tools reject malformed arguments before dispatch", async
262
262
  }
263
263
  });
264
264
 
265
+ test("MCP write tools accept and forward client-injected cwd/projectTag (#1434)", async () => {
266
+ for (const toolName of ["engram.memory_store", "engram.suggestion_submit"]) {
267
+ let received: Record<string, unknown> | undefined;
268
+ const service = {
269
+ ...makeMockService(),
270
+ memoryStore: async (args: Record<string, unknown>) => {
271
+ received = args;
272
+ return {
273
+ schemaVersion: 1,
274
+ operation: "memory_store",
275
+ namespace: "default",
276
+ dryRun: true,
277
+ accepted: true,
278
+ queued: false,
279
+ status: "validated",
280
+ };
281
+ },
282
+ suggestionSubmit: async (args: Record<string, unknown>) => {
283
+ received = args;
284
+ return {
285
+ schemaVersion: 1,
286
+ operation: "suggestion_submit",
287
+ namespace: "default",
288
+ dryRun: true,
289
+ accepted: true,
290
+ queued: false,
291
+ status: "validated",
292
+ };
293
+ },
294
+ } as unknown as EngramAccessService;
295
+ const server = new EngramMcpServer(service);
296
+
297
+ const response = await server.handleRequest(
298
+ makeToolRequest(toolName, {
299
+ content: "valid durable content",
300
+ category: "fact",
301
+ dryRun: true,
302
+ cwd: "/home/dev/project-x",
303
+ projectTag: "Blend/Supply",
304
+ }),
305
+ );
306
+ const result = (response as Record<string, unknown> & {
307
+ result?: { isError?: boolean };
308
+ }).result;
309
+
310
+ assert.equal(result?.isError, false, `${toolName} should accept cwd/projectTag`);
311
+ assert.equal(received?.cwd, "/home/dev/project-x", `${toolName} must forward cwd`);
312
+ assert.equal(received?.projectTag, "Blend/Supply", `${toolName} must forward projectTag`);
313
+ }
314
+ });
315
+
316
+ test("MCP write tools still reject genuinely-unknown keys after the cwd fix (#1434)", async () => {
317
+ let dispatched = false;
318
+ const service = {
319
+ ...makeMockService(),
320
+ memoryStore: async () => {
321
+ dispatched = true;
322
+ return {
323
+ schemaVersion: 1,
324
+ operation: "memory_store",
325
+ namespace: "default",
326
+ dryRun: true,
327
+ accepted: true,
328
+ queued: false,
329
+ status: "validated",
330
+ };
331
+ },
332
+ } as unknown as EngramAccessService;
333
+ const server = new EngramMcpServer(service);
334
+ const response = await server.handleRequest(
335
+ makeToolRequest("engram.memory_store", {
336
+ content: "valid durable content",
337
+ category: "fact",
338
+ dryRun: true,
339
+ cwd: "/ok",
340
+ bogusField: "must still be rejected",
341
+ }),
342
+ );
343
+ const result = (response as Record<string, unknown> & {
344
+ result?: { isError?: boolean; content?: Array<{ text?: string }> };
345
+ }).result;
346
+ assert.equal(result?.isError, true, "unknown keys must still be rejected");
347
+ assert.equal(dispatched, false, "malformed write must not dispatch");
348
+ });
349
+
350
+ test("MCP capsule tools tolerate client-injected cwd/projectTag (#1434)", async () => {
351
+ for (const toolName of [
352
+ "engram.capsule_list",
353
+ "engram.capsule_export",
354
+ "engram.capsule_import",
355
+ ]) {
356
+ const service = {
357
+ ...makeMockService(),
358
+ capsuleList: async () => ({ capsules: [] }),
359
+ capsuleExport: async () => ({ exported: true }),
360
+ capsuleImport: async () => ({ imported: true }),
361
+ } as unknown as EngramAccessService;
362
+ const server = new EngramMcpServer(service);
363
+ const args: Record<string, unknown> = { cwd: "/x", projectTag: "t" };
364
+ if (toolName === "engram.capsule_export") args.name = "cap-1";
365
+ if (toolName === "engram.capsule_import") args.archivePath = "/tmp/a.capsule.json.gz";
366
+ const response = await server.handleRequest(makeToolRequest(toolName, args));
367
+ const result = (response as Record<string, unknown> & {
368
+ result?: { isError?: boolean };
369
+ }).result;
370
+ assert.equal(result?.isError, false, `${toolName} should tolerate cwd/projectTag`);
371
+ }
372
+ });
373
+
265
374
  test("MCP session override is injected only into tools that accept sessionKey", async () => {
266
375
  let capsuleListArgs: Record<string, unknown> | undefined;
267
376
  let observeArgs: Record<string, unknown> | undefined;
package/src/access-mcp.ts CHANGED
@@ -115,6 +115,8 @@ const STRICT_MCP_SCHEMA_KEYS: Partial<Record<SchemaName, readonly string[]>> = {
115
115
  "entityRef",
116
116
  "ttl",
117
117
  "sourceReason",
118
+ "cwd",
119
+ "projectTag",
118
120
  ],
119
121
  suggestionSubmit: [
120
122
  "schemaVersion",
@@ -129,6 +131,8 @@ const STRICT_MCP_SCHEMA_KEYS: Partial<Record<SchemaName, readonly string[]>> = {
129
131
  "entityRef",
130
132
  "ttl",
131
133
  "sourceReason",
134
+ "cwd",
135
+ "projectTag",
132
136
  ],
133
137
  capsuleExport: [
134
138
  "name",
@@ -138,9 +142,40 @@ const STRICT_MCP_SCHEMA_KEYS: Partial<Record<SchemaName, readonly string[]>> = {
138
142
  "peerIds",
139
143
  "includeTranscripts",
140
144
  "encrypt",
145
+ "cwd",
146
+ "projectTag",
141
147
  ],
142
- capsuleImport: ["archivePath", "namespace", "mode", "passphrase"],
143
- capsuleList: ["namespace"],
148
+ capsuleImport: ["archivePath", "namespace", "mode", "passphrase", "cwd", "projectTag"],
149
+ capsuleList: ["namespace", "cwd", "projectTag"],
150
+ };
151
+
152
+ // Shared JSON-schema fragments for the client-injected git/project context
153
+ // fields (#1434). Declared once to avoid drift across tool definitions.
154
+ // `_SCOPED` is for write tools that resolve a project namespace from these
155
+ // fields; `_IGNORED` is for tools that merely tolerate them for MCP client
156
+ // compatibility (clients like Pi MCPorter auto-inject `cwd` on every call).
157
+ const MCP_GIT_CONTEXT_SCHEMA_PROPS_SCOPED: Record<string, unknown> = {
158
+ cwd: {
159
+ type: "string",
160
+ description:
161
+ "Optional working directory. When no explicit namespace is given, resolves the project namespace this write is stored in (mirrors recall/observe).",
162
+ },
163
+ projectTag: {
164
+ type: "string",
165
+ description:
166
+ "Optional project tag for non-git project scoping. When no explicit namespace is given, routes this write to the tagged project namespace.",
167
+ },
168
+ };
169
+ const MCP_GIT_CONTEXT_SCHEMA_PROPS_IGNORED: Record<string, unknown> = {
170
+ cwd: {
171
+ type: "string",
172
+ description:
173
+ "Accepted for MCP client compatibility (git-context auto-injection); ignored by this tool.",
174
+ },
175
+ projectTag: {
176
+ type: "string",
177
+ description: "Accepted for MCP client compatibility; ignored by this tool.",
178
+ },
144
179
  };
145
180
 
146
181
  function parseMcpRequest<N extends SchemaName>(
@@ -614,6 +649,7 @@ export class EngramMcpServer {
614
649
  },
615
650
  includeTranscripts: { type: "boolean" },
616
651
  encrypt: { type: "boolean" },
652
+ ...MCP_GIT_CONTEXT_SCHEMA_PROPS_IGNORED,
617
653
  },
618
654
  required: ["name"],
619
655
  additionalProperties: false,
@@ -639,6 +675,7 @@ export class EngramMcpServer {
639
675
  type: "string",
640
676
  description: "Passphrase for encrypted capsule archives.",
641
677
  },
678
+ ...MCP_GIT_CONTEXT_SCHEMA_PROPS_IGNORED,
642
679
  },
643
680
  required: ["archivePath"],
644
681
  additionalProperties: false,
@@ -651,6 +688,7 @@ export class EngramMcpServer {
651
688
  type: "object",
652
689
  properties: {
653
690
  namespace: { type: "string" },
691
+ ...MCP_GIT_CONTEXT_SCHEMA_PROPS_IGNORED,
654
692
  },
655
693
  additionalProperties: false,
656
694
  },
@@ -755,6 +793,7 @@ export class EngramMcpServer {
755
793
  entityRef: { type: "string" },
756
794
  ttl: { type: "string" },
757
795
  sourceReason: { type: "string" },
796
+ ...MCP_GIT_CONTEXT_SCHEMA_PROPS_SCOPED,
758
797
  },
759
798
  required: ["content"],
760
799
  additionalProperties: false,
@@ -778,6 +817,7 @@ export class EngramMcpServer {
778
817
  entityRef: { type: "string" },
779
818
  ttl: { type: "string" },
780
819
  sourceReason: { type: "string" },
820
+ ...MCP_GIT_CONTEXT_SCHEMA_PROPS_SCOPED,
781
821
  },
782
822
  required: ["content"],
783
823
  additionalProperties: false,
@@ -2502,6 +2542,8 @@ export class EngramMcpServer {
2502
2542
  entityRef: body.entityRef,
2503
2543
  ttl: body.ttl,
2504
2544
  sourceReason: body.sourceReason,
2545
+ cwd: body.cwd,
2546
+ projectTag: body.projectTag,
2505
2547
  });
2506
2548
  }
2507
2549
  case "engram.suggestion_submit": {
@@ -2520,6 +2562,8 @@ export class EngramMcpServer {
2520
2562
  entityRef: body.entityRef,
2521
2563
  ttl: body.ttl,
2522
2564
  sourceReason: body.sourceReason,
2565
+ cwd: body.cwd,
2566
+ projectTag: body.projectTag,
2523
2567
  });
2524
2568
  }
2525
2569
  case "engram.entity_get":
@@ -243,6 +243,17 @@ export const memoryStoreRequestSchema = z.object({
243
243
  entityRef: entityRefSchema,
244
244
  ttl: ttlSchema,
245
245
  sourceReason: sourceReasonSchema,
246
+ // Git/project context for project-scoped writes (#1434). When no explicit
247
+ // `namespace` is given, these route the write to the same project namespace
248
+ // recall/observe resolve from `cwd`/`projectTag` (issue #569, rule 42). Also
249
+ // lets MCP clients that auto-inject `cwd` (e.g. Pi MCPorter) call write tools.
250
+ cwd: z.string().trim().min(1, "cwd must be non-empty when provided").max(2048).optional(),
251
+ projectTag: z
252
+ .string()
253
+ .trim()
254
+ .min(1, "projectTag must be non-empty when provided")
255
+ .max(256)
256
+ .optional(),
246
257
  });
247
258
 
248
259
  export const suggestionSubmitRequestSchema = memoryStoreRequestSchema;