@remnic/core 9.69.90 → 9.69.91

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.69.90",
3
+ "version": "9.69.91",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3327,7 +3327,7 @@
3327
3327
  "core"
3328
3328
  ],
3329
3329
  "peerDependencies": {
3330
- "@remnic/coding-graph": "^9.69.90"
3330
+ "@remnic/coding-graph": "^9.69.91"
3331
3331
  },
3332
3332
  "peerDependenciesMeta": {
3333
3333
  "@remnic/coding-graph": {
@@ -989,11 +989,19 @@ test("#2128: post-flush retained cleanup stops on abort or deadline", async (t)
989
989
  /replay extraction deadline exceeded \(retained_turn_cleanup\)/,
990
990
  );
991
991
  });
992
- test("#3140: a duration deadline reaches scope resolution and aborts it when it elapses", async () => {
992
+ test("#3140: a duration deadline reaches scope resolution and aborts it when it elapses", async (t) => {
993
993
  // Regression for issue #3140: `deadlineMs` is a BUDGET from now. The old
994
994
  // absolute-epoch reading compared a relative value (e.g. 30000) against
995
995
  // Date.now(), so a live-session force flush was rejected INSTANTLY with
996
996
  // `replay extraction deadline exceeded (scope_resolution)` before any work.
997
+ // The flush deadline is a real, unref'd timer in production
998
+ // (session-context.ts), so racing a 30ms wall-clock budget lets the event
999
+ // loop drain first under CI load: the flush promise then never settles and
1000
+ // the runner cancels the rest of the file. Mocked timers make the ordering
1001
+ // deterministic — the budget elapses only when ticked. The clock starts at
1002
+ // an epoch-scale `now` so a duration budget (30) is genuinely distinguished
1003
+ // from the old absolute-epoch reading the test guards against.
1004
+ t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 1_700_000_000_000 });
997
1005
  const probe = makeParityProbe({ namespacesEnabled: false } as Partial<PluginConfig>);
998
1006
  const service = new EngramAccessService(probe.orch);
999
1007
  let scopeResolutionStarted = false;
@@ -1005,19 +1013,25 @@ test("#3140: a duration deadline reaches scope resolution and aborts it when it
1005
1013
  return new Promise<never>(() => {});
1006
1014
  };
1007
1015
 
1008
- const started = Date.now();
1009
- await assert.rejects(
1010
- service.extractionForceFlush({
1011
- sessionKey: "duration-deadline-scope-resolution",
1012
- deadlineMs: 30,
1013
- }),
1014
- /scope_resolution/,
1015
- );
1016
+ const flush = service.extractionForceFlush({
1017
+ sessionKey: "duration-deadline-scope-resolution",
1018
+ deadlineMs: 30,
1019
+ });
1020
+ // Yield once so the stub is entered before the budget is spent; the mocked
1021
+ // clock does not move on its own.
1022
+ await new Promise<void>((resolve) => setImmediate(resolve));
1016
1023
  assert.equal(scopeResolutionStarted, true, "the budget lets scope resolution start");
1017
- assert.ok(
1018
- Date.now() - started >= 25,
1019
- "the flush waits out the budget instead of failing instantly",
1020
- );
1024
+ // Under mocked timers the budget cannot have elapsed yet, so the flush
1025
+ // must still be pending — proves the deadline does not fire before its
1026
+ // budget (#3140 regression: an absolute-epoch reading would reject
1027
+ // instantly with `scope_resolution` before the stub ran).
1028
+ const stillPending = Promise.race([
1029
+ flush.then(() => "rejected", () => "rejected"),
1030
+ new Promise<string>((resolve) => setImmediate(() => resolve("pending"))),
1031
+ ]);
1032
+ assert.equal(await stillPending, "pending");
1033
+ t.mock.timers.tick(30);
1034
+ await assert.rejects(flush, /scope_resolution/);
1021
1035
  assert.equal(probe.extractionForceFlushCalls.length, 0);
1022
1036
  });
1023
1037
 
@@ -1076,7 +1090,14 @@ test("#2206: abort during scope resolution cancels pre-resolution observe prepar
1076
1090
  assert.equal(probe.extractionCalls.length, 0);
1077
1091
  });
1078
1092
 
1079
- test("#2206: scope-resolution deadline cancels preparations by raw projectTag and cwd hints", async () => {
1093
+ test("#2206: scope-resolution deadline cancels preparations by raw projectTag and cwd hints", async (t) => {
1094
+ // The deadline budget is a real, unref'd timer in production
1095
+ // (session-context.ts), so a 1ms wall-clock budget can elapse BEFORE the
1096
+ // flush enters the blocked scope stub on a loaded runner (assertion then
1097
+ // sees flushScopeStarted === false), and a drained event loop can leave
1098
+ // the flush pending forever. Mocked timers pin the order: the stub is
1099
+ // entered first, the budget elapses only when ticked, and the observe tail
1100
+ // runs after timers are restored.
1080
1101
  const rawHintCases = [
1081
1102
  { projectTag: "Acme/Webshop" },
1082
1103
  { cwd: "/workspace/acme/webshop" },
@@ -1119,17 +1140,29 @@ test("#2206: scope-resolution deadline cancels preparations by raw projectTag an
1119
1140
  ...rawHintCase,
1120
1141
  }));
1121
1142
  await observeScopeStarted;
1122
- await assert.rejects(
1123
- service.extractionForceFlush({
1143
+ // Mock the clock only for the flush: the observe is already parked on
1144
+ // the gate, and its tail below needs the real clock back. A try/finally
1145
+ // makes sure a failing assertion cannot leak the mocked window or the
1146
+ // parked observe, which would otherwise reproduce the same
1147
+ // event-loop-drain cascade this PR fixes.
1148
+ t.mock.timers.enable({ apis: ["setTimeout", "Date"] });
1149
+ try {
1150
+ const flush = service.extractionForceFlush({
1124
1151
  sessionKey: "pi-geek:raw-hint-deadline",
1125
1152
  authenticatedPrincipal: "pi-geek",
1126
1153
  deadlineMs: 1,
1127
1154
  ...rawHintCase,
1128
- }),
1129
- /scope_resolution/,
1130
- );
1131
- assert.equal(flushScopeStarted, true);
1132
- releaseObserveScope();
1155
+ });
1156
+ // Yield once so the stub is entered before the budget is spent; the
1157
+ // mocked clock does not move on its own.
1158
+ await new Promise<void>((resolve) => setImmediate(resolve));
1159
+ assert.equal(flushScopeStarted, true);
1160
+ t.mock.timers.tick(1);
1161
+ await assert.rejects(flush, /scope_resolution/);
1162
+ } finally {
1163
+ t.mock.timers.reset();
1164
+ releaseObserveScope();
1165
+ }
1133
1166
  const response = await observe;
1134
1167
  assert.equal(response.extractionQueued, false);
1135
1168
  assert.equal(probe.extractionCalls.length, 0);
@@ -8,9 +8,19 @@ import { parseConfig } from "./config.js";
8
8
  import { Orchestrator } from "./orchestrator.js";
9
9
  import { NoopSearchBackend } from "./search/noop-backend.js";
10
10
 
11
+ /**
12
+ * Watchdog for startup lifecycle awaits. This is a HANG guard, not a speed
13
+ * budget: a real Orchestrator.initialize() does real fs/sqlite discovery and
14
+ * can take well over a second on a loaded CI runner (a merge to main failed
15
+ * its shard when initialize settled ~40ms after the old 1250ms deadline had
16
+ * already resolved), and the discovery paths can block the loop so even
17
+ * in-file timers stall meanwhile. The deadline only needs to fire when the
18
+ * awaited lifecycle genuinely never settles, so keep it far above any
19
+ * legitimate startup cost.
20
+ */
11
21
  async function withTestDeadline<T>(
12
22
  promise: Promise<T>,
13
- timeoutMs = 1_250,
23
+ timeoutMs = 10_000,
14
24
  ): Promise<T | "deadline"> {
15
25
  let timer: NodeJS.Timeout | undefined;
16
26
  const deadline = new Promise<"deadline">((resolve) => {