@remnic/plugin-claude-code 10.0.40 → 10.0.41

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "remnic",
3
3
  "description": "Universal memory for AI agents — automatic recall, observation, and cross-agent knowledge sharing",
4
- "version": "10.0.40",
4
+ "version": "10.0.41",
5
5
  "author": {
6
6
  "name": "Joshua Warren"
7
7
  },
@@ -122,7 +122,7 @@ function cursorPath(home, sessionId) {
122
122
  test("session-start: healthy server returns recall context with codingContext cleared outside a repo", async () => {
123
123
  const home = mkHome();
124
124
  const { server, port, calls } = await startServer((req, res) => {
125
- if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
125
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
126
126
  if (req.url === "/engram/v1/recall") {
127
127
  return res.writeHead(200, { "Content-Type": "application/json" }).end(
128
128
  JSON.stringify({ context: "remembered preferences", count: 3, mode: "auto" }),
@@ -154,7 +154,7 @@ test("session-start: health probe carries the bearer token (auth-gated daemons)"
154
154
  const home = mkHome();
155
155
  let healthAuth = "unset";
156
156
  const { server, port } = await startServer((req, res) => {
157
- if (req.url === "/engram/v1/health") {
157
+ if (req.url === "/engram/v1/live") {
158
158
  healthAuth = req.headers.authorization || null;
159
159
  return res.writeHead(200).end("ok");
160
160
  }
@@ -176,10 +176,85 @@ test("session-start: health probe carries the bearer token (auth-gated daemons)"
176
176
  }
177
177
  });
178
178
 
179
- test("session-start: REMNIC_DAEMON_URL routes recall to the remote base URL (#2483)", async () => {
179
+ test("session-start: hung diagnostic /health does not stall the live probe (#3257)", async () => {
180
+ const home = mkHome();
181
+ const { server, port, calls } = await startServer((req, res) => {
182
+ // The expensive diagnostic route never answers; only /live is healthy.
183
+ if (req.url === "/engram/v1/health") return;
184
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
185
+ if (req.url === "/engram/v1/recall") {
186
+ return res.writeHead(200, { "Content-Type": "application/json" }).end(
187
+ JSON.stringify({ context: "live recall", count: 1, mode: "auto" }),
188
+ );
189
+ }
190
+ res.writeHead(404).end();
191
+ });
192
+ try {
193
+ const { json } = await runHook("session-start", { session_id: "s1", cwd: home }, { port, home });
194
+ assert.equal(json.continue, true);
195
+ assert.match(json.hookSpecificOutput.additionalContext, /live recall/);
196
+ assert.ok(calls.some((c) => c.url === "/engram/v1/recall"), "recall reached the daemon");
197
+ } finally {
198
+ server.close();
199
+ fs.rmSync(home, { recursive: true, force: true });
200
+ }
201
+ });
202
+
203
+ test("session-start: 404 live falls back to legacy health within one budget", async () => {
180
204
  const home = mkHome();
181
205
  const { server, port, calls } = await startServer((req, res) => {
206
+ if (req.url === "/engram/v1/live") return res.writeHead(404).end();
182
207
  if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
208
+ if (req.url === "/engram/v1/recall") {
209
+ return res.writeHead(200, { "Content-Type": "application/json" }).end(
210
+ JSON.stringify({ context: "legacy recall", count: 1, mode: "auto" }),
211
+ );
212
+ }
213
+ res.writeHead(404).end();
214
+ });
215
+ try {
216
+ const { json } = await runHook("session-start", { session_id: "s1", cwd: home }, { port, home });
217
+ assert.equal(json.continue, true);
218
+ assert.match(json.hookSpecificOutput.additionalContext, /legacy recall/);
219
+ const probeUrls = calls.filter((c) => c.url === "/engram/v1/live" || c.url === "/engram/v1/health").map((c) => c.url);
220
+ assert.deepEqual(probeUrls, ["/engram/v1/live", "/engram/v1/health"]);
221
+ } finally {
222
+ server.close();
223
+ fs.rmSync(home, { recursive: true, force: true });
224
+ }
225
+ });
226
+
227
+ test("session-start: 503 live is a final answer with no legacy retry", async () => {
228
+ const home = mkHome();
229
+ const { server, port, calls } = await startServer((req, res) => {
230
+ if (req.url === "/engram/v1/live") return res.writeHead(503).end();
231
+ // A 503 must NOT be re-probed on the legacy route, even when it would pass.
232
+ if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
233
+ if (req.url === "/engram/v1/recall") {
234
+ return res.writeHead(200, { "Content-Type": "application/json" }).end(
235
+ JSON.stringify({ context: "ctx", count: 1, mode: "auto" }),
236
+ );
237
+ }
238
+ res.writeHead(404).end();
239
+ });
240
+ try {
241
+ const { json } = await runHook("session-start", { session_id: "s1", cwd: home }, { port, home });
242
+ assert.match(json.hookSpecificOutput.additionalContext, /daemon not running/);
243
+ assert.ok(!calls.some((c) => c.url === "/engram/v1/recall"), "recall is skipped");
244
+ assert.deepEqual(
245
+ calls.filter((c) => c.url.startsWith("/engram/v1/")).map((c) => c.url),
246
+ ["/engram/v1/live", "/engram/v1/live"],
247
+ );
248
+ } finally {
249
+ server.close();
250
+ fs.rmSync(home, { recursive: true, force: true });
251
+ }
252
+ });
253
+
254
+ test("session-start: REMNIC_DAEMON_URL routes recall to the remote base URL (#2483)", async () => {
255
+ const home = mkHome();
256
+ const { server, port, calls } = await startServer((req, res) => {
257
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
183
258
  if (req.url === "/engram/v1/recall") {
184
259
  return res.writeHead(200, { "Content-Type": "application/json" }).end(
185
260
  JSON.stringify({ context: "remote recall", count: 1, mode: "auto" }),
@@ -217,7 +292,7 @@ test("session-start: REMNIC_DAEMON_URL routes recall to the remote base URL (#24
217
292
  test("recall body targets REMNIC_NAMESPACE when set, and omits namespace when unset", async () => {
218
293
  const mk = () =>
219
294
  startServer((req, res) => {
220
- if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
295
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
221
296
  if (req.url === "/engram/v1/recall") {
222
297
  return res
223
298
  .writeHead(200, { "Content-Type": "application/json" })
@@ -274,7 +349,7 @@ test("namespace targeting: ENGRAM_NAMESPACE fallback (recall) and observe body b
274
349
  {
275
350
  const home = mkHome();
276
351
  const { server, port, calls } = await startServer((req, res) => {
277
- if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
352
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
278
353
  if (req.url === "/engram/v1/recall") {
279
354
  return res
280
355
  .writeHead(200, { "Content-Type": "application/json" })
@@ -325,7 +400,7 @@ test("session-start: falls back to minimal mode when full recall fails", async (
325
400
  const home = mkHome();
326
401
  let recallHits = 0;
327
402
  const { server, port, calls } = await startServer((req, res) => {
328
- if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
403
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
329
404
  if (req.url === "/engram/v1/recall") {
330
405
  recallHits += 1;
331
406
  if (recallHits === 1) return res.writeHead(500).end("boom");
@@ -349,7 +424,7 @@ test("session-start: falls back to minimal mode when full recall fails", async (
349
424
  test("session-start: no token → claude-code install hint, no recall call", async () => {
350
425
  const home = mkHome();
351
426
  const { server, port, calls } = await startServer((req, res) => {
352
- if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
427
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
353
428
  res.writeHead(200).end("{}");
354
429
  });
355
430
  try {
@@ -617,7 +692,7 @@ test("token resolution: claude-code connector wins over openclaw in tokens.json"
617
692
  }),
618
693
  );
619
694
  const { server, port, calls } = await startServer((req, res) => {
620
- if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
695
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
621
696
  if (req.url === "/engram/v1/recall") return res.writeHead(200).end("{}");
622
697
  res.writeHead(200).end("{}");
623
698
  });
@@ -644,7 +719,7 @@ test("token resolution: legacy ~/.engram/tokens.json is the read fallback", asyn
644
719
  JSON.stringify({ tokens: [{ connector: "openclaw", token: "legacy-tok" }] }),
645
720
  );
646
721
  const { server, port, calls } = await startServer((req, res) => {
647
- if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
722
+ if (req.url === "/engram/v1/live") return res.writeHead(200).end("ok");
648
723
  if (req.url === "/engram/v1/recall") return res.writeHead(200).end("{}");
649
724
  res.writeHead(200).end("{}");
650
725
  });
@@ -671,7 +746,7 @@ test("token resolution: legacy ~/.engram/tokens.json is the read fallback", asyn
671
746
  function authEnvServer() {
672
747
  const seen = { health: "unset", recall: "unset" };
673
748
  return startServer((req, res) => {
674
- if (req.url === "/engram/v1/health") {
749
+ if (req.url === "/engram/v1/live") {
675
750
  seen.health = req.headers.authorization || null;
676
751
  return res.writeHead(200).end("ok");
677
752
  }
@@ -366,34 +366,48 @@ function run(config) {
366
366
  // ignore) and green-light a probe whose recall then 401s.
367
367
  //
368
368
  // When the daemon has an auth token configured, every route — including
369
- // /engram/v1/health — returns 401 to unauthenticated requests, so an
369
+ // /engram/v1/live — returns 401 to unauthenticated requests, so an
370
370
  // unauthenticated probe makes the hook wrongly report "daemon not running"
371
371
  // and skip recall/observe. Unauthenticated daemons ignore the header.
372
+ //
373
+ // One aggregate budget: readiness-gated /engram/v1/live first, and only a
374
+ // 404 (daemon predating the live route) retries the legacy diagnostic
375
+ // /engram/v1/health with the remaining time. 401/503 are final answers.
376
+ // Diagnostic health can outlive this liveness probe's deadline, which is
377
+ // why the gate probes liveness at all (#3257).
372
378
  function httpHealthy(timeoutMs, token) {
373
379
  if (DAEMON_URL === null) return Promise.resolve(false);
374
380
  const transport = DAEMON_URL.protocol === "https:" ? https : http;
375
- return new Promise((resolve) => {
381
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
382
+ const deadline = Date.now() + timeoutMs;
383
+ const probe = (path, budget) => new Promise((resolve) => {
376
384
  const req = transport.request(
377
385
  {
378
386
  protocol: DAEMON_URL.protocol,
379
387
  hostname: DAEMON_URL.hostname,
380
388
  port: DAEMON_URL.port || (DAEMON_URL.protocol === "https:" ? 443 : 80),
381
- path: DAEMON_BASE_PATH + "/engram/v1/health",
389
+ path: DAEMON_BASE_PATH + path,
382
390
  method: "GET",
383
- headers: token ? { Authorization: `Bearer ${token}` } : {},
391
+ headers,
384
392
  },
385
393
  (res) => {
386
394
  res.resume();
387
- resolve(res.statusCode >= 200 && res.statusCode < 300);
395
+ resolve(res.statusCode || 0);
388
396
  },
389
397
  );
390
- req.on("error", () => resolve(false));
391
- req.setTimeout(timeoutMs, () => {
398
+ req.on("error", () => resolve(0));
399
+ req.setTimeout(budget, () => {
392
400
  req.destroy();
393
- resolve(false);
401
+ resolve(0);
394
402
  });
395
403
  req.end();
396
404
  });
405
+ return probe("/engram/v1/live", timeoutMs).then((status) => {
406
+ if (status !== 404) return status >= 200 && status < 300;
407
+ const remaining = deadline - Date.now();
408
+ if (remaining <= 0) return false;
409
+ return probe("/engram/v1/health", remaining).then((legacy) => legacy >= 200 && legacy < 300);
410
+ });
397
411
  }
398
412
 
399
413
  // ── git coding-context (mirrors @remnic/core git-context.ts) ───────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/plugin-claude-code",
3
- "version": "10.0.40",
3
+ "version": "10.0.41",
4
4
  "description": "Remnic memory plugin for Claude Code — hooks, skills, MCP integration",
5
5
  "type": "module",
6
6
  "license": "MIT",