hippo-memory 1.40.0 → 1.42.0

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 (54) hide show
  1. package/README.md +8 -4
  2. package/dist/card.d.ts +3 -1
  3. package/dist/card.d.ts.map +1 -1
  4. package/dist/card.js +4 -1
  5. package/dist/card.js.map +1 -1
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +106 -24
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/rerankers/cross-encoder.d.ts +1 -8
  15. package/dist/rerankers/cross-encoder.d.ts.map +1 -1
  16. package/dist/rerankers/cross-encoder.js +58 -52
  17. package/dist/rerankers/cross-encoder.js.map +1 -1
  18. package/dist/rerankers/index.d.ts.map +1 -1
  19. package/dist/rerankers/index.js +2 -0
  20. package/dist/rerankers/index.js.map +1 -1
  21. package/dist/rerankers/jev.d.ts +9 -0
  22. package/dist/rerankers/jev.d.ts.map +1 -0
  23. package/dist/rerankers/jev.js +116 -0
  24. package/dist/rerankers/jev.js.map +1 -0
  25. package/dist/rerankers/types.d.ts +7 -4
  26. package/dist/rerankers/types.d.ts.map +1 -1
  27. package/dist/src/card.js +4 -1
  28. package/dist/src/card.js.map +1 -1
  29. package/dist/src/cli.js +106 -24
  30. package/dist/src/cli.js.map +1 -1
  31. package/dist/src/index.js +2 -2
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/rerankers/cross-encoder.js +58 -52
  34. package/dist/src/rerankers/cross-encoder.js.map +1 -1
  35. package/dist/src/rerankers/index.js +2 -0
  36. package/dist/src/rerankers/index.js.map +1 -1
  37. package/dist/src/rerankers/jev.js +116 -0
  38. package/dist/src/rerankers/jev.js.map +1 -0
  39. package/dist/src/store.js +122 -22
  40. package/dist/src/store.js.map +1 -1
  41. package/dist/src/version.js +2 -15
  42. package/dist/src/version.js.map +1 -1
  43. package/dist/store.d.ts +14 -8
  44. package/dist/store.d.ts.map +1 -1
  45. package/dist/store.js +122 -22
  46. package/dist/store.js.map +1 -1
  47. package/dist/version.d.ts +2 -15
  48. package/dist/version.d.ts.map +1 -1
  49. package/dist/version.js +2 -15
  50. package/dist/version.js.map +1 -1
  51. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  52. package/extensions/openclaw-plugin/package.json +1 -1
  53. package/openclaw.plugin.json +1 -1
  54. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -5,11 +5,11 @@ export { Layer, calculateStrength, resolveConfidence, confidenceFacets, createMe
5
5
  export { search, hybridSearch, physicsSearch, markRetrieved, estimateTokens, textOverlap, tokenize, explainMatch, detectTemporalDirection, temporalBoost, computeTemporalRange } from './search.js';
6
6
  export { multihopSearch } from './multihop.js';
7
7
  export { graphExpandRecall, MAX_HOPS, DEFAULT_MAX_NEIGHBORS } from './graph-recall.js';
8
- export { initStore, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, writeEntry, readEntry, deleteEntry, loadIndex, rebuildIndex, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, SNAPSHOT_AMBIENT_MAX_AGE_MS, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, replaceDetectedConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, loadSessionDecayContext, createCard, loadCard, listCards, loadCardDeps, loadCardRuns, loadCardComments, claimCard, blockCard, reviewCard, completeCard, addCardComment, loadLatestHandoffForCard, } from './store.js';
8
+ export { initStore, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, writeEntry, readEntry, deleteEntry, loadIndex, rebuildIndex, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, SNAPSHOT_AMBIENT_MAX_AGE_MS, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, replaceDetectedConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, loadSessionDecayContext, createCard, loadCard, listCards, loadCardDeps, loadCardRuns, loadCardComments, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, loadLatestHandoffForCard, } from './store.js';
9
9
  // Feature 5: Session handoff
10
10
  export { isHandoffOutcome } from './handoff.js';
11
11
  // W2a: work-queue cards
12
- export { isCardStatus, CARD_TRANSITIONS } from './card.js';
12
+ export { isCardStatus, CARD_TRANSITIONS, CARD_LEASE_MS } from './card.js';
13
13
  export { consolidate } from './consolidate.js';
14
14
  // Announced public in CHANGELOG 1.26.3 but never re-exported; the rest of dedupe.js stays internal.
15
15
  export { strengthBucket } from './dedupe.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAe,KAAK,EAAmD,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAyB,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC3O,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,uBAAuB,EAAE,aAAa,EAAE,oBAAoB,EAAkC,MAAM,aAAa,CAAC;AACpO,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,qBAAqB,EAAwB,MAAM,mBAAmB,CAAC;AAC7G,OAAO,EACL,SAAS,EACT,cAAc,EACd,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,EACT,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,4BAA4B,EAC5B,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,wBAAwB,EACxB,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,EAEvB,UAAU,EACV,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,UAAU,EACV,YAAY,EACZ,cAAc,EACd,wBAAwB,GACzB,MAAM,YAAY,CAAC;AAEpB,6BAA6B;AAC7B,OAAO,EAAmD,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACjG,wBAAwB;AACxB,OAAO,EAA2D,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACpH,OAAO,EAAE,WAAW,EAAuB,MAAM,kBAAkB,CAAC;AACpE,oGAAoG;AACpG,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,8BAA8B;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAEzB,oCAAoC;AACpC,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,WAAW,GACZ,MAAM,gBAAgB,CAAC;AAExB,uCAAuC;AACvC,OAAO,EACL,aAAa,EACb,UAAU,EACV,eAAe,EACf,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,aAAa,EACb,WAAW,EACX,SAAS,EACT,SAAS,GACV,MAAM,aAAa,CAAC;AAErB,4BAA4B;AAC5B,OAAO,EACL,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EAEP,cAAc,GACf,MAAM,qBAAqB,CAAC;AAE7B,8BAA8B;AAC9B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,aAAa,GAGd,MAAM,gBAAgB,CAAC;AAExB,qBAAqB;AACrB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAEzB,8BAA8B;AAC9B,OAAO,EACL,eAAe,GAIhB,MAAM,eAAe,CAAC;AAEvB,qCAAqC;AACrC,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,GAEpB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAe,KAAK,EAAmD,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAyB,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC3O,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,uBAAuB,EAAE,aAAa,EAAE,oBAAoB,EAAkC,MAAM,aAAa,CAAC;AACpO,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,qBAAqB,EAAwB,MAAM,mBAAmB,CAAC;AAC7G,OAAO,EACL,SAAS,EACT,cAAc,EACd,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,EACT,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,4BAA4B,EAC5B,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,wBAAwB,EACxB,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,EAEvB,UAAU,EACV,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,SAAS,EACT,UAAU,EACV,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,wBAAwB,GACzB,MAAM,YAAY,CAAC;AAEpB,6BAA6B;AAC7B,OAAO,EAAmD,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACjG,wBAAwB;AACxB,OAAO,EAA2D,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACnI,OAAO,EAAE,WAAW,EAAuB,MAAM,kBAAkB,CAAC;AACpE,oGAAoG;AACpG,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,8BAA8B;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAEzB,oCAAoC;AACpC,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,WAAW,GACZ,MAAM,gBAAgB,CAAC;AAExB,uCAAuC;AACvC,OAAO,EACL,aAAa,EACb,UAAU,EACV,eAAe,EACf,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,aAAa,EACb,WAAW,EACX,SAAS,EACT,SAAS,GACV,MAAM,aAAa,CAAC;AAErB,4BAA4B;AAC5B,OAAO,EACL,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EAEP,cAAc,GACf,MAAM,qBAAqB,CAAC;AAE7B,8BAA8B;AAC9B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,aAAa,GAGd,MAAM,gBAAgB,CAAC;AAExB,qBAAqB;AACrB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAEzB,8BAA8B;AAC9B,OAAO,EACL,eAAe,GAIhB,MAAM,eAAe,CAAC;AAEvB,qCAAqC;AACrC,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,GAEpB,MAAM,cAAc,CAAC"}
@@ -1,48 +1,46 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { pathToFileURL } from 'node:url';
2
3
  const MODEL_NAME = 'Xenova/ms-marco-MiniLM-L-6-v2';
3
- // Use Function constructor to bypass TypeScript static module resolution
4
- // for optional peer dependencies that may not be installed (mirrors the
5
- // pattern in src/embeddings.ts).
6
- // SAFETY: this declares the contract we require from the dynamically
7
- // imported package (a `pipeline` factory, top-level or under `default`);
8
- // loadTransformersModule validates the resolved value is callable via
9
- // isPipelineFactory before it is ever invoked.
10
- const _dynImport = new Function('s', 'return import(s)');
11
4
  const _require = createRequire(import.meta.url);
12
5
  const TRANSFORMERS_PACKAGES = ['@huggingface/transformers', '@xenova/transformers'];
6
+ // Returns the ESM entry URL, the same build src/embeddings.ts imports. A
7
+ // require-style resolve picks the CommonJS build and puts a second copy of
8
+ // the library, with its own ONNX sessions, in the process.
13
9
  function resolveTransformersPackage() {
14
10
  for (const name of TRANSFORMERS_PACKAGES) {
15
11
  try {
16
- _require.resolve(name);
17
- return name;
12
+ return import.meta.resolve(name);
18
13
  }
19
14
  catch {
20
- // Try the legacy fallback only when the preferred package is not installed.
15
+ // import.meta.resolve is missing under some module runners (vitest's
16
+ // included); fall through to the require-based resolve there.
17
+ try {
18
+ return pathToFileURL(_require.resolve(name)).href;
19
+ }
20
+ catch {
21
+ // Try the legacy fallback only when the preferred package is not installed.
22
+ }
21
23
  }
22
24
  }
23
25
  return null;
24
26
  }
25
- function isPipelineFactory(value) {
26
- return typeof value === 'function';
27
- }
28
27
  async function loadTransformersModule() {
29
28
  // Import one backend only. Loading both native ONNX runtimes in one process
30
29
  // can abort during finalization; Hugging Face is the maintained default.
31
- const name = resolveTransformersPackage();
32
- if (!name)
30
+ const url = resolveTransformersPackage();
31
+ if (!url)
33
32
  return null;
34
33
  try {
35
- const mod = await _dynImport(name);
36
- const pipeline = mod.pipeline ?? mod.default?.pipeline;
37
- return isPipelineFactory(pipeline)
38
- ? { pipeline }
39
- : null;
34
+ const mod = await import(/* @vite-ignore */ url);
35
+ const tok = mod.AutoTokenizer ?? mod.default?.AutoTokenizer;
36
+ const seq = mod.AutoModelForSequenceClassification ?? mod.default?.AutoModelForSequenceClassification;
37
+ return tok && seq ? { AutoTokenizer: tok, AutoModelForSequenceClassification: seq } : null;
40
38
  }
41
39
  catch {
42
40
  return null;
43
41
  }
44
42
  }
45
- let cachedPipeline = null;
43
+ let pipelineLoading = null;
46
44
  let warnedOnFallback = false;
47
45
  /**
48
46
  * True if a Transformers.js backend is importable. Note: this does NOT confirm
@@ -53,40 +51,55 @@ let warnedOnFallback = false;
53
51
  export async function isCrossEncoderAvailable() {
54
52
  return (await loadTransformersModule()) !== null;
55
53
  }
56
- async function loadPipeline() {
57
- if (cachedPipeline)
58
- return cachedPipeline;
54
+ // NOT the text-classification pipeline: this model is a num_labels=1
55
+ // regression head, and that pipeline softmaxes a length-1 logit vector, which
56
+ // is identically 1.0 for every input. Read the logit, then squash it.
57
+ async function buildPipeline() {
59
58
  try {
60
59
  const mod = await loadTransformersModule();
61
60
  if (!mod)
62
61
  return null;
63
- const p = await mod.pipeline('text-classification', MODEL_NAME);
64
- cachedPipeline = async (query, candidate) => {
65
- const out = await p(`${query} [SEP] ${candidate}`);
66
- return Array.isArray(out) ? out : [out];
62
+ const [tokenizer, model] = await Promise.all([
63
+ mod.AutoTokenizer.from_pretrained(MODEL_NAME),
64
+ mod.AutoModelForSequenceClassification.from_pretrained(MODEL_NAME),
65
+ ]);
66
+ return async (query, candidate) => {
67
+ const inputs = await tokenizer(query, {
68
+ text_pair: candidate,
69
+ padding: true,
70
+ truncation: true,
71
+ });
72
+ const { logits } = await model(inputs);
73
+ const score = 1 / (1 + Math.exp(-Number(logits.data[0])));
74
+ // NaN would make the sort comparator a no-op; throwing hands this
75
+ // candidate to the per-candidate fallback instead.
76
+ if (!Number.isFinite(score))
77
+ throw new Error('cross-encoder returned a non-finite score');
78
+ return score;
67
79
  };
68
- return cachedPipeline;
69
80
  }
70
81
  catch {
71
82
  return null;
72
83
  }
73
84
  }
74
- /**
75
- * Track 2 reranker: MS-MARCO MiniLM cross-encoder.
76
- * Loads model on first call, then sub-100ms per query for top-K=50 candidates
77
- * on a typical developer laptop CPU. Falls back to identity ordering if the
78
- * model fails to load (no transformers, no network for first download, etc.).
79
- *
80
- * See docs/plans/2026-05-10-f6-reranker-hardening.md Task 6.
81
- */
85
+ // One shared in-flight load: two first calls must not fetch the model twice.
86
+ // A failed load clears the slot so a later call can try again.
87
+ function loadPipeline() {
88
+ pipelineLoading ??= buildPipeline().then((pipe) => {
89
+ if (!pipe)
90
+ pipelineLoading = null;
91
+ return pipe;
92
+ });
93
+ return pipelineLoading;
94
+ }
95
+ /** Track 2 reranker: MS-MARCO MiniLM cross-encoder, identity fallback if the model will not load. */
82
96
  export const crossEncoderReranker = async (query, results, options) => {
83
97
  const topK = options?.topK ?? 50;
84
98
  const head = results.slice(0, topK);
85
99
  const pipe = await loadPipeline();
86
100
  if (!pipe) {
87
- // Fallback: identity ordering with rerankScore = original score. Warn
88
- // once per process so a silent identity-fallback doesn't mislead users
89
- // into thinking the cross-encoder is doing work it isn't.
101
+ // Warn once per process: a silent identity fallback otherwise reads as a
102
+ // working reranker.
90
103
  if (!warnedOnFallback) {
91
104
  warnedOnFallback = true;
92
105
  // eslint-disable-next-line no-console
@@ -102,13 +115,10 @@ export const crossEncoderReranker = async (query, results, options) => {
102
115
  const scored = await Promise.all(head.map(async (r, i) => {
103
116
  let ceScore;
104
117
  try {
105
- const out = await pipe(query, r.entry.content);
106
- ceScore = Array.isArray(out) && out.length > 0 ? out[0].score : 0;
118
+ ceScore = await pipe(query, r.entry.content);
107
119
  }
108
120
  catch {
109
- // Per-call inference failure (transient tensor error, bad input,
110
- // etc.): fall back to original score for this candidate so a
111
- // single bad inference doesn't sink the whole rerank pass.
121
+ // One bad inference must not sink the whole pass.
112
122
  ceScore = r.score;
113
123
  }
114
124
  return {
@@ -118,12 +128,8 @@ export const crossEncoderReranker = async (query, results, options) => {
118
128
  postRerankRank: 0,
119
129
  };
120
130
  }));
121
- // T2 note: PLAIN stable score sort on purpose. The input `head` is already
122
- // deterministically ordered (upstream content tail), so stability inherits
123
- // that -- and when the cross-encoder produces tied scores (degenerate or
124
- // no-signal cases), ties MUST fall back to the prior relevance order, not
125
- // an arbitrary content order (the reranker-cross-encoder micro fixture
126
- // fails otherwise: an all-tie rerank pass reordered its input).
131
+ // Plain stable sort on purpose: tied scores MUST fall back to the prior
132
+ // relevance order, never an arbitrary content order.
127
133
  scored.sort((a, b) => b.rerankScore - a.rerankScore);
128
134
  scored.forEach((r, i) => (r.postRerankRank = i + 1));
129
135
  return scored;
@@ -1 +1 @@
1
- {"version":3,"file":"cross-encoder.js","sourceRoot":"","sources":["../../../src/rerankers/cross-encoder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cross-encoder.js","sourceRoot":"","sources":["../../../src/rerankers/cross-encoder.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,9 @@
1
1
  import { crossEncoderReranker } from './cross-encoder.js';
2
+ import { jevReranker } from './jev.js';
2
3
  import { llmReranker } from './llm.js';
3
4
  const REGISTRY = {
4
5
  'cross-encoder': crossEncoderReranker,
6
+ jev: jevReranker,
5
7
  llm: llmReranker,
6
8
  };
7
9
  function isRegisteredRerankerName(name) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rerankers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGvC,MAAM,QAAQ,GAAG;IACf,eAAe,EAAE,oBAAoB;IACrC,GAAG,EAAE,WAAW;CACoB,CAAC;AAIvC,SAAS,wBAAwB,CAAC,IAAY;IAC5C,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAA+B;IACzD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,qBAAqB,IAAI,gBAAgB,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rerankers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGvC,MAAM,QAAQ,GAAG;IACf,eAAe,EAAE,oBAAoB;IACrC,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,WAAW;CACoB,CAAC;AAIvC,SAAS,wBAAwB,CAAC,IAAY;IAC5C,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAA+B;IACzD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,qBAAqB,IAAI,gBAAgB,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC"}
@@ -0,0 +1,116 @@
1
+ import { crossEncoderReranker } from './cross-encoder.js';
2
+ const ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
3
+ const DEFAULT_TIMEOUT_MS = 5_000;
4
+ const TRUNCATE_CHARS = 1200;
5
+ // The pool size every number in docs/evals/2026-09-19-jev-reranker.md was measured at.
6
+ export const JEV_DEFAULT_TOP_K = 40;
7
+ function truncate(s, n) {
8
+ return s.length <= n ? s : `${s.slice(0, n)}...`;
9
+ }
10
+ // Number.isFinite rejects a string or null without coercing it, which the
11
+ // declared type cannot promise about a third-party payload.
12
+ function isProbability(v) {
13
+ return v !== undefined && Number.isFinite(v) && v >= 0 && v <= 1;
14
+ }
15
+ // A half-scored list ranks worse than the order it would replace, so one bad
16
+ // answer voids the whole response.
17
+ function parseScores(answers, n) {
18
+ const out = [];
19
+ for (let i = 1; i <= n; i++) {
20
+ const v = answers?.[`c${i}`]?.noul;
21
+ if (!isProbability(v))
22
+ return null;
23
+ out.push(v);
24
+ }
25
+ return out;
26
+ }
27
+ /** One batched request for the whole candidate list. Rejects with the reason when there are no usable scores. */
28
+ async function requestScores(query, head) {
29
+ const key = process.env.TYPESAFE_API_KEY;
30
+ if (!key)
31
+ throw new Error('TYPESAFE_API_KEY not set');
32
+ const lines = head.map((r, i) => `[${i + 1}] ${truncate(r.entry.content, TRUNCATE_CHARS)}`);
33
+ const state = `Query: ${query}\n\nNumbered candidate memories from an AI coding agent's project store:\n\n${lines.join('\n\n')}`;
34
+ const questions = {};
35
+ for (let i = 1; i <= head.length; i++) {
36
+ questions[`c${i}`] = {
37
+ type: 'noul',
38
+ instructions: `Probability that candidate ${i} (numbered in the state above) helps answer the query.`,
39
+ };
40
+ }
41
+ const parsed = Number.parseInt(process.env.HIPPO_JEV_TIMEOUT_MS ?? '', 10);
42
+ const timeoutMs = parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
43
+ const controller = new AbortController();
44
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
45
+ try {
46
+ const resp = await fetch(ENDPOINT, {
47
+ method: 'POST',
48
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
49
+ body: JSON.stringify({
50
+ state,
51
+ model: process.env.HIPPO_JEV_MODEL ?? 'jev-latest',
52
+ questions,
53
+ }),
54
+ signal: controller.signal,
55
+ });
56
+ if (!resp.ok) {
57
+ // A third-party header ends up on stderr, so keep printable ASCII only.
58
+ const requestId = resp.headers.get('x-request-id')?.replace(/[^\x20-\x7e]/g, '').slice(0, 64);
59
+ await resp.body?.cancel();
60
+ throw new Error(`HTTP ${resp.status}${requestId ? `, request ${requestId}` : ''}`);
61
+ }
62
+ const body = await resp.json();
63
+ const scores = parseScores(body.answers, head.length);
64
+ if (!scores)
65
+ throw new Error('incomplete or out-of-range answers');
66
+ return scores;
67
+ }
68
+ catch (err) {
69
+ if (err instanceof Error && err.name === 'AbortError') {
70
+ throw new Error(`no answer within ${timeoutMs} ms`);
71
+ }
72
+ throw err;
73
+ }
74
+ finally {
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+ /** Builds a Jev reranker around the reranker it degrades to. Exported so a test can pass a stand-in. */
79
+ export function createJevReranker(localFallback) {
80
+ let warned = false;
81
+ return async (query, results, options) => {
82
+ const head = results.slice(0, options?.topK ?? JEV_DEFAULT_TOP_K);
83
+ if (head.length === 0)
84
+ return [];
85
+ let scores;
86
+ try {
87
+ scores = await requestScores(query, head);
88
+ }
89
+ catch (err) {
90
+ // A read path never throws, and never hands back the unranked order
91
+ // without saying so: warn once, then use the free local reranker.
92
+ if (!warned) {
93
+ warned = true;
94
+ const reason = err instanceof Error ? err.message : 'unknown error';
95
+ // eslint-disable-next-line no-console
96
+ console.warn(`[hippo] jev reranker unavailable (${reason}); falling back to the local cross-encoder. Subsequent calls will not repeat this warning.`);
97
+ }
98
+ return localFallback(query, head, options);
99
+ }
100
+ const scored = head.map((r, i) => ({
101
+ ...r,
102
+ rerankScore: scores[i],
103
+ preRerankRank: r.preRerankRank ?? i + 1,
104
+ postRerankRank: 0,
105
+ }));
106
+ // Stable sort: ties fall back to the prior relevance order.
107
+ scored.sort((a, b) => b.rerankScore - a.rerankScore);
108
+ scored.forEach((r, i) => (r.postRerankRank = i + 1));
109
+ return scored;
110
+ };
111
+ }
112
+ /** Track 4 reranker: hosted TypeSafe Jev, opt-in and paid (TYPESAFE_API_KEY), one batched call per recall.
113
+ * Any failure warns once and delegates to the local cross-encoder. Scores are not bit-stable run to run.
114
+ * Cost, env vars, evidence and limits: docs/evals/2026-09-19-jev-reranker.md. */
115
+ export const jevReranker = createJevReranker(crossEncoderReranker);
116
+ //# sourceMappingURL=jev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jev.js","sourceRoot":"","sources":["../../../src/rerankers/jev.ts"],"names":[],"mappings":"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"}
package/dist/src/store.js CHANGED
@@ -10,7 +10,7 @@ import { Layer, generateId } from './memory.js';
10
10
  import { dumpFrontmatter, parseFrontmatter } from './yaml.js';
11
11
  import { openHippoDb, closeHippoDb, getMeta, setMeta, isFtsAvailable, pruneConsolidationRuns, getHippoDbPath, } from './db.js';
12
12
  import { rowToSessionHandoff, isHandoffOutcome } from './handoff.js';
13
- import { CARD_TRANSITIONS } from './card.js';
13
+ import { CARD_TRANSITIONS, CARD_LEASE_MS } from './card.js';
14
14
  import { tokenize } from './search.js';
15
15
  import { appendAuditEvent } from './audit.js';
16
16
  import { resolveTenantId } from './tenant.js';
@@ -2868,6 +2868,29 @@ function insertCardComment(db, tenantId, cardId, author, body) {
2868
2868
  const id = Number(result.lastInsertRowid ?? 0);
2869
2869
  return { id, cardId, author, body, createdAt: now };
2870
2870
  }
2871
+ function leaseUntilFrom(now) {
2872
+ return new Date(Date.parse(now) + CARD_LEASE_MS).toISOString();
2873
+ }
2874
+ function assertRunId(runId) {
2875
+ if (!Number.isSafeInteger(runId) || runId <= 0) {
2876
+ throw new Error(`Invalid run id: ${runId} (expected a positive integer)`);
2877
+ }
2878
+ }
2879
+ // A second run that has not ended means a corrupt store; throw rather than guess which run the caller means.
2880
+ function isLiveRun(db, tenantId, cardId, runId) {
2881
+ // SAFETY: rows' shape matches the single `id` column named in the SELECT below.
2882
+ const rows = db.prepare(`SELECT id FROM card_runs WHERE tenant_id = ? AND card = ? AND ended IS NULL`).all(tenantId, cardId);
2883
+ if (rows.length > 1) {
2884
+ throw new Error(`card ${cardId} has ${rows.length} runs that have not ended`);
2885
+ }
2886
+ return rows[0]?.id === runId;
2887
+ }
2888
+ function closeLiveRun(db, tenantId, cardId, outcome, now) {
2889
+ db.prepare(`
2890
+ UPDATE card_runs SET ended = ?, outcome = ?, updated_at = ?
2891
+ WHERE card = ? AND tenant_id = ? AND ended IS NULL
2892
+ `).run(now, outcome, now, cardId, tenantId);
2893
+ }
2871
2894
  // The single status-mutating seam (rule 15): CARD_TRANSITIONS is the one
2872
2895
  // runtime authority, so a hand-copied wrong `from` list fails fast here.
2873
2896
  export function transitionCard(db, tenantId, cardId, from, to, extra) {
@@ -2877,12 +2900,14 @@ export function transitionCard(db, tenantId, cardId, from, to, extra) {
2877
2900
  }
2878
2901
  }
2879
2902
  const now = new Date().toISOString();
2903
+ // Lease columns follow status: set on the move to running, cleared on every other move (rule 15).
2904
+ const lease = to === 'running' ? [leaseUntilFrom(now), now] : [null, null];
2880
2905
  const fromPlaceholders = from.map(() => '?').join(', ');
2881
2906
  const sql = `
2882
- UPDATE cards SET status = ?, updated_at = ?${extra?.setSql ? `, ${extra.setSql}` : ''}
2907
+ UPDATE cards SET status = ?, updated_at = ?, lease_until = ?, heartbeat_at = ?${extra?.setSql ? `, ${extra.setSql}` : ''}
2883
2908
  WHERE id = ? AND tenant_id = ? AND status IN (${fromPlaceholders})${extra?.whereSql ? ` AND ${extra.whereSql}` : ''}
2884
2909
  `;
2885
- const params = [to, now, ...(extra?.params ?? []), cardId, tenantId, ...from];
2910
+ const params = [to, now, ...lease, ...(extra?.params ?? []), cardId, tenantId, ...from];
2886
2911
  const result = db.prepare(sql).run(...params);
2887
2912
  return Number(result.changes ?? 0);
2888
2913
  }
@@ -3046,7 +3071,7 @@ export function loadLatestHandoffForCard(hippoRoot, tenantId, cardId) {
3046
3071
  closeHippoDb(db);
3047
3072
  }
3048
3073
  }
3049
- /** Atomic claim: WHERE status IN (ready, blocked) AND assignee_runtime IS NULL decides the race. Throws on an unknown card id; returns null for a card not ready/blocked or already claimed. */
3074
+ /** Atomic claim: WHERE status IN (ready, blocked) AND assignee_runtime IS NULL decides the race. Throws on an unknown card id; returns null for a card not ready/blocked or already claimed. Sets a CARD_LEASE_MS lease and returns the new run's id as runId. */
3050
3075
  export function claimCard(hippoRoot, tenantId, id, runtime, sessionId) {
3051
3076
  assertTenantId('claimCard', tenantId);
3052
3077
  if (runtime.trim() === '') {
@@ -3056,6 +3081,7 @@ export function claimCard(hippoRoot, tenantId, id, runtime, sessionId) {
3056
3081
  const db = openHippoDb(hippoRoot);
3057
3082
  try {
3058
3083
  db.exec('BEGIN IMMEDIATE');
3084
+ let runId = 0;
3059
3085
  try {
3060
3086
  const changes = transitionCard(db, tenantId, id, ['ready', 'blocked'], 'running', {
3061
3087
  setSql: 'assignee_runtime = ?',
@@ -3070,10 +3096,46 @@ export function claimCard(hippoRoot, tenantId, id, runtime, sessionId) {
3070
3096
  return null;
3071
3097
  }
3072
3098
  const now = new Date().toISOString();
3073
- db.prepare(`
3099
+ const insert = db.prepare(`
3074
3100
  INSERT INTO card_runs (card, runtime, session_id, started, created_at, updated_at, tenant_id)
3075
3101
  VALUES (?, ?, ?, ?, ?, ?, ?)
3076
3102
  `).run(id, runtime, sessionId ?? null, now, now, now, tenantId);
3103
+ runId = Number(insert.lastInsertRowid ?? 0);
3104
+ db.exec('COMMIT');
3105
+ }
3106
+ catch (error) {
3107
+ try {
3108
+ db.exec('ROLLBACK');
3109
+ }
3110
+ catch { /* commit may have already rolled back */ }
3111
+ throw error;
3112
+ }
3113
+ return { ...loadCardRow(db, tenantId, id), runId };
3114
+ }
3115
+ finally {
3116
+ closeHippoDb(db);
3117
+ }
3118
+ }
3119
+ /** Moves a running card's lease to CARD_LEASE_MS from now and records the heartbeat; updated_at is left alone. Throws on an unknown card id or a run id that is not a positive integer; returns null unless the card is running and runId is its live run. */
3120
+ export function heartbeatCard(hippoRoot, tenantId, id, runId) {
3121
+ assertTenantId('heartbeatCard', tenantId);
3122
+ assertRunId(runId);
3123
+ initStore(hippoRoot);
3124
+ const db = openHippoDb(hippoRoot);
3125
+ try {
3126
+ db.exec('BEGIN IMMEDIATE');
3127
+ try {
3128
+ const card = loadCardRow(db, tenantId, id);
3129
+ if (!card) {
3130
+ throw new Error(`unknown card id: ${id}`);
3131
+ }
3132
+ if (card.status !== 'running' || !isLiveRun(db, tenantId, id, runId)) {
3133
+ db.exec('ROLLBACK');
3134
+ return null;
3135
+ }
3136
+ const now = new Date().toISOString();
3137
+ db.prepare(`UPDATE cards SET lease_until = ?, heartbeat_at = ? WHERE id = ? AND tenant_id = ?`)
3138
+ .run(leaseUntilFrom(now), now, id, tenantId);
3077
3139
  db.exec('COMMIT');
3078
3140
  }
3079
3141
  catch (error) {
@@ -3089,18 +3151,21 @@ export function claimCard(hippoRoot, tenantId, id, runtime, sessionId) {
3089
3151
  closeHippoDb(db);
3090
3152
  }
3091
3153
  }
3092
- /** Requires the card be running; closes the live run as blocked and files reason as a comment. Throws on an unknown card id; returns null for a card not running. */
3093
- export function blockCard(hippoRoot, tenantId, id, reason) {
3154
+ /** Requires the card be running; closes the live run as blocked and files reason as a comment. Throws on an unknown card id; returns null for a card not running. When runId is given, returns null unless it is the card's live run. */
3155
+ export function blockCard(hippoRoot, tenantId, id, reason, runId) {
3094
3156
  assertTenantId('blockCard', tenantId);
3095
3157
  if (reason.trim() === '') {
3096
3158
  throw new Error('reason must not be empty');
3097
3159
  }
3160
+ if (runId !== undefined)
3161
+ assertRunId(runId);
3098
3162
  initStore(hippoRoot);
3099
3163
  const db = openHippoDb(hippoRoot);
3100
3164
  try {
3101
3165
  db.exec('BEGIN IMMEDIATE');
3102
3166
  try {
3103
- const changes = transitionCard(db, tenantId, id, ['running'], 'blocked', { setSql: 'assignee_runtime = NULL' });
3167
+ const allowed = runId === undefined || isLiveRun(db, tenantId, id, runId);
3168
+ const changes = allowed ? transitionCard(db, tenantId, id, ['running'], 'blocked', { setSql: 'assignee_runtime = NULL' }) : 0;
3104
3169
  if (changes === 0) {
3105
3170
  if (!loadCardRow(db, tenantId, id)) {
3106
3171
  throw new Error(`unknown card id: ${id}`);
@@ -3110,10 +3175,7 @@ export function blockCard(hippoRoot, tenantId, id, reason) {
3110
3175
  }
3111
3176
  const now = new Date().toISOString();
3112
3177
  // Close the interrupted run here so completeCard's ended IS NULL scope only ever matches the live run.
3113
- db.prepare(`
3114
- UPDATE card_runs SET ended = ?, outcome = 'blocked', updated_at = ?
3115
- WHERE card = ? AND tenant_id = ? AND ended IS NULL
3116
- `).run(now, now, id, tenantId);
3178
+ closeLiveRun(db, tenantId, id, 'blocked', now);
3117
3179
  insertCardComment(db, tenantId, id, 'system', reason);
3118
3180
  db.exec('COMMIT');
3119
3181
  }
@@ -3130,15 +3192,18 @@ export function blockCard(hippoRoot, tenantId, id, reason) {
3130
3192
  closeHippoDb(db);
3131
3193
  }
3132
3194
  }
3133
- /** Requires the card be running; moves it to review with no other side effects. Throws on an unknown card id; returns null for a card not running. */
3134
- export function reviewCard(hippoRoot, tenantId, id) {
3195
+ /** Requires the card be running; moves it to review, clearing its lease and heartbeat and keeping its live run. When runId is given, returns null unless it is the card's live run. Throws on an unknown card id; returns null for a card not running. */
3196
+ export function reviewCard(hippoRoot, tenantId, id, runId) {
3135
3197
  assertTenantId('reviewCard', tenantId);
3198
+ if (runId !== undefined)
3199
+ assertRunId(runId);
3136
3200
  initStore(hippoRoot);
3137
3201
  const db = openHippoDb(hippoRoot);
3138
3202
  try {
3139
3203
  db.exec('BEGIN IMMEDIATE');
3140
3204
  try {
3141
- const changes = transitionCard(db, tenantId, id, ['running'], 'review');
3205
+ const allowed = runId === undefined || isLiveRun(db, tenantId, id, runId);
3206
+ const changes = allowed ? transitionCard(db, tenantId, id, ['running'], 'review') : 0;
3142
3207
  if (changes === 0) {
3143
3208
  if (!loadCardRow(db, tenantId, id)) {
3144
3209
  throw new Error(`unknown card id: ${id}`);
@@ -3161,12 +3226,14 @@ export function reviewCard(hippoRoot, tenantId, id) {
3161
3226
  closeHippoDb(db);
3162
3227
  }
3163
3228
  }
3164
- /** Requires the card be in review; closes the live run with outcome. Outcome 'success' moves the card to done and, in the same transaction, promotes any child whose parents are now all done; 'failure' or 'partial' moves it to shelved and promotes nothing. Throws on an unknown card id; returns null for a card not in review. */
3165
- export function completeCard(hippoRoot, tenantId, id, outcome) {
3229
+ /** Requires the card be in review; closes the live run with outcome. Outcome 'success' moves the card to done and, in the same transaction, promotes any child whose parents are now all done; 'failure' or 'partial' moves it to shelved and promotes nothing. Throws on an unknown card id; returns null for a card not in review. When runId is given, returns null unless it is the card's live run. */
3230
+ export function completeCard(hippoRoot, tenantId, id, outcome, runId) {
3166
3231
  assertTenantId('completeCard', tenantId);
3167
3232
  if (!isHandoffOutcome(outcome)) {
3168
3233
  throw new Error(`invalid card outcome: ${String(outcome)}`);
3169
3234
  }
3235
+ if (runId !== undefined)
3236
+ assertRunId(runId);
3170
3237
  initStore(hippoRoot);
3171
3238
  const db = openHippoDb(hippoRoot);
3172
3239
  try {
@@ -3174,7 +3241,8 @@ export function completeCard(hippoRoot, tenantId, id, outcome) {
3174
3241
  let promotedChildren = [];
3175
3242
  try {
3176
3243
  const target = outcome === 'success' ? 'done' : 'shelved';
3177
- const changes = transitionCard(db, tenantId, id, ['review'], target);
3244
+ const allowed = runId === undefined || isLiveRun(db, tenantId, id, runId);
3245
+ const changes = allowed ? transitionCard(db, tenantId, id, ['review'], target) : 0;
3178
3246
  if (changes === 0) {
3179
3247
  if (!loadCardRow(db, tenantId, id)) {
3180
3248
  throw new Error(`unknown card id: ${id}`);
@@ -3183,10 +3251,7 @@ export function completeCard(hippoRoot, tenantId, id, outcome) {
3183
3251
  return null;
3184
3252
  }
3185
3253
  const now = new Date().toISOString();
3186
- db.prepare(`
3187
- UPDATE card_runs SET ended = ?, outcome = ?, updated_at = ?
3188
- WHERE card = ? AND tenant_id = ? AND ended IS NULL
3189
- `).run(now, outcome, now, id, tenantId);
3254
+ closeLiveRun(db, tenantId, id, outcome, now);
3190
3255
  // Not best-effort (rule 12): promotion runs in this same transaction, so a
3191
3256
  // card can never be `done` with an un-evaluated child.
3192
3257
  if (target === 'done') {
@@ -3223,6 +3288,41 @@ export function completeCard(hippoRoot, tenantId, id, outcome) {
3223
3288
  closeHippoDb(db);
3224
3289
  }
3225
3290
  }
3291
+ /** Returns to ready every running card of the tenant whose lease has expired or is missing: clears its assignee, closes its live run as 'reclaimed' and leaves its handoffs alone, all in one write transaction. Returns the reclaimed card ids in id order. */
3292
+ export function reclaimExpiredCards(hippoRoot, tenantId) {
3293
+ assertTenantId('reclaimExpiredCards', tenantId);
3294
+ initStore(hippoRoot);
3295
+ const db = openHippoDb(hippoRoot);
3296
+ try {
3297
+ db.exec('BEGIN IMMEDIATE');
3298
+ try {
3299
+ // Read lease times under the write lock, so a heartbeat that committed while we waited wins.
3300
+ const now = new Date().toISOString();
3301
+ // SAFETY: rows' shape matches the single `id` column named in the SELECT below.
3302
+ const ids = db.prepare(`
3303
+ SELECT id FROM cards
3304
+ WHERE tenant_id = ? AND status = 'running' AND (lease_until IS NULL OR lease_until < ?)
3305
+ ORDER BY id
3306
+ `).all(tenantId, now).map((r) => r.id);
3307
+ for (const id of ids) {
3308
+ transitionCard(db, tenantId, id, ['running'], 'ready', { setSql: 'assignee_runtime = NULL' });
3309
+ closeLiveRun(db, tenantId, id, 'reclaimed', now);
3310
+ }
3311
+ db.exec('COMMIT');
3312
+ return ids;
3313
+ }
3314
+ catch (error) {
3315
+ try {
3316
+ db.exec('ROLLBACK');
3317
+ }
3318
+ catch { /* commit may have already rolled back */ }
3319
+ throw error;
3320
+ }
3321
+ }
3322
+ finally {
3323
+ closeHippoDb(db);
3324
+ }
3325
+ }
3226
3326
  /** Appends a comment to cardId in any card status; throws if cardId is not a card of this tenant. */
3227
3327
  export function addCardComment(hippoRoot, tenantId, cardId, author, body) {
3228
3328
  assertTenantId('addCardComment', tenantId);