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.
- package/README.md +8 -4
- package/dist/card.d.ts +3 -1
- package/dist/card.d.ts.map +1 -1
- package/dist/card.js +4 -1
- package/dist/card.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +106 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/rerankers/cross-encoder.d.ts +1 -8
- package/dist/rerankers/cross-encoder.d.ts.map +1 -1
- package/dist/rerankers/cross-encoder.js +58 -52
- package/dist/rerankers/cross-encoder.js.map +1 -1
- package/dist/rerankers/index.d.ts.map +1 -1
- package/dist/rerankers/index.js +2 -0
- package/dist/rerankers/index.js.map +1 -1
- package/dist/rerankers/jev.d.ts +9 -0
- package/dist/rerankers/jev.d.ts.map +1 -0
- package/dist/rerankers/jev.js +116 -0
- package/dist/rerankers/jev.js.map +1 -0
- package/dist/rerankers/types.d.ts +7 -4
- package/dist/rerankers/types.d.ts.map +1 -1
- package/dist/src/card.js +4 -1
- package/dist/src/card.js.map +1 -1
- package/dist/src/cli.js +106 -24
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/rerankers/cross-encoder.js +58 -52
- package/dist/src/rerankers/cross-encoder.js.map +1 -1
- package/dist/src/rerankers/index.js +2 -0
- package/dist/src/rerankers/index.js.map +1 -1
- package/dist/src/rerankers/jev.js +116 -0
- package/dist/src/rerankers/jev.js.map +1 -0
- package/dist/src/store.js +122 -22
- package/dist/src/store.js.map +1 -1
- package/dist/src/version.js +2 -15
- package/dist/src/version.js.map +1 -1
- package/dist/store.d.ts +14 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +122 -22
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +2 -15
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -15
- package/dist/version.js.map +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- 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';
|
package/dist/src/index.js.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
17
|
-
return name;
|
|
12
|
+
return import.meta.resolve(name);
|
|
18
13
|
}
|
|
19
14
|
catch {
|
|
20
|
-
//
|
|
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
|
|
32
|
-
if (!
|
|
30
|
+
const url = resolveTransformersPackage();
|
|
31
|
+
if (!url)
|
|
33
32
|
return null;
|
|
34
33
|
try {
|
|
35
|
-
const mod = await
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
//
|
|
88
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
122
|
-
//
|
|
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":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|