hippo-memory 1.52.7 → 1.52.8
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 +1 -1
- package/dist/api.d.ts +1 -0
- package/dist/api.js +34 -10
- package/dist/autolearn.js +3 -2
- package/dist/capture.d.ts +3 -0
- package/dist/capture.js +48 -56
- package/dist/churn-git.js +2 -2
- package/dist/cli.d.ts +1 -4
- package/dist/cli.js +73 -53
- package/dist/dashboard.js +4 -0
- package/dist/extensions/openclaw-plugin/index.js +1 -0
- package/dist/graph-recall.js +8 -7
- package/dist/handoff.d.ts +2 -0
- package/dist/hooks.d.ts +4 -4
- package/dist/hooks.js +9 -3
- package/dist/mcp/server.js +3 -3
- package/dist/secret-detect.js +3 -3
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +6 -1
- package/dist/store.d.ts +3 -5
- package/dist/store.js +22 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-ui/assets/index-BhT8RvO6.js +61 -0
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/index.ts +1 -0
- 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-ui/assets/index-BgmA7Hwe.js +0 -61
package/dist/dashboard.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* Usage: hippo dashboard [--port 3333]
|
|
6
6
|
*/
|
|
7
7
|
import * as http from 'http';
|
|
8
|
+
import * as os from 'os';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import * as fs from 'fs';
|
|
11
|
+
import { extractPathTags } from './path-context.js';
|
|
10
12
|
import { loadAllEntries, listCards, listMemoryConflicts, readEntry, writeEntry } from './store.js';
|
|
11
13
|
import { calculateStrength, confidenceFacets } from './memory.js';
|
|
12
14
|
import { loadConfig } from './config.js';
|
|
@@ -110,6 +112,8 @@ function buildDashboardData(hippoRoot) {
|
|
|
110
112
|
defaultHalfLifeDays: config.defaultHalfLifeDays,
|
|
111
113
|
defaultBudget: config.defaultBudget,
|
|
112
114
|
embeddingsEnabled: config.embeddings.enabled,
|
|
115
|
+
// Every memory captured in the home folder carries these, so they name no project.
|
|
116
|
+
homePathTags: extractPathTags(os.homedir()),
|
|
113
117
|
},
|
|
114
118
|
};
|
|
115
119
|
}
|
|
@@ -172,6 +172,7 @@ function runHippo(args, cwd) {
|
|
|
172
172
|
encoding: 'utf8',
|
|
173
173
|
timeout: 30_000,
|
|
174
174
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
175
|
+
windowsHide: true,
|
|
175
176
|
});
|
|
176
177
|
// `encoding: 'utf8'` above selects the ExecFileSyncOptionsWithStringEncoding
|
|
177
178
|
// overload, so `result` is always a `string` here — no runtime check needed.
|
package/dist/graph-recall.js
CHANGED
|
@@ -61,12 +61,9 @@ function loadByIdsChunked(root, tenantId, ids) {
|
|
|
61
61
|
}
|
|
62
62
|
return out;
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
* stores. Pure reads.
|
|
68
|
-
*/
|
|
69
|
-
function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, hitsByOrigin, opts) {
|
|
64
|
+
/** Traverse one store's graph from its seeds into `hitsByOrigin`. Pure reads; mutates `seenMemoryIds`
|
|
65
|
+
* and `seenContent` so a memory, or a share/promote copy of it, surfaces at most once across stores. */
|
|
66
|
+
function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, seenContent, hitsByOrigin, opts) {
|
|
70
67
|
const { hops, maxNeighbors, tenantId, includeSuperseded, asOfDate, recallScope } = opts;
|
|
71
68
|
// Seeds = graph entities (in THIS store) whose source memory is a base result.
|
|
72
69
|
const seedEntities = loadEntitiesByMemoryId(root, tenantId, baseResults.map((r) => r.entry.id));
|
|
@@ -147,6 +144,8 @@ function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds,
|
|
|
147
144
|
continue; // not found / wrong tenant / already in base
|
|
148
145
|
if (seenMemoryIds.has(mem.id))
|
|
149
146
|
continue; // another reached entity already added it
|
|
147
|
+
if (seenContent.has(mem.content))
|
|
148
|
+
continue; // share/promote copy: same text, another id
|
|
150
149
|
const via = reached.get(ent.id);
|
|
151
150
|
// A node reached as the `to` endpoint of a `supersedes` edge IS the superseded
|
|
152
151
|
// (older) version — the graph is the authoritative signal (the memory mirror's
|
|
@@ -175,6 +174,7 @@ function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds,
|
|
|
175
174
|
const origin = originMemByEntityId.get(ent.id) ?? baseResults[0].entry.id;
|
|
176
175
|
const originScore = baseScoreByMemId.get(origin) ?? baseResults[baseResults.length - 1].score;
|
|
177
176
|
seenMemoryIds.add(mem.id);
|
|
177
|
+
seenContent.add(mem.content);
|
|
178
178
|
const hit = {
|
|
179
179
|
entry: mem,
|
|
180
180
|
score: originScore * (1 - HOP_DISCOUNT * via.hops),
|
|
@@ -209,11 +209,12 @@ export function graphExpandRecall(baseResults, opts) {
|
|
|
209
209
|
const recallScope = opts.recallScope ?? {};
|
|
210
210
|
const baseScoreByMemId = new Map(baseResults.map((r) => [r.entry.id, r.score]));
|
|
211
211
|
const seenMemoryIds = new Set(baseResults.map((r) => r.entry.id));
|
|
212
|
+
const seenContent = new Set(baseResults.map((r) => r.entry.content));
|
|
212
213
|
const hitsByOrigin = new Map();
|
|
213
214
|
// Expand against each distinct store the seeds may live in (local + global).
|
|
214
215
|
const roots = globalRoot && globalRoot !== hippoRoot ? [hippoRoot, globalRoot] : [hippoRoot];
|
|
215
216
|
for (const root of roots) {
|
|
216
|
-
produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, hitsByOrigin, {
|
|
217
|
+
produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, seenContent, hitsByOrigin, {
|
|
217
218
|
hops, maxNeighbors, tenantId, includeSuperseded, asOfDate, recallScope,
|
|
218
219
|
});
|
|
219
220
|
}
|
package/dist/handoff.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface HandoffEvidence {
|
|
|
12
12
|
gitRef?: string | null;
|
|
13
13
|
dirtyTree?: boolean | null;
|
|
14
14
|
testStatus?: 'pass' | 'fail' | 'unknown' | null;
|
|
15
|
+
/** 'transcript' when hippo read the handoff off the session's transcript at session end; a later exit may replace it. */
|
|
16
|
+
derivedFrom?: 'transcript';
|
|
15
17
|
}
|
|
16
18
|
/** Narrows an unvalidated value (e.g. CLI input or event content) to a HandoffOutcome. */
|
|
17
19
|
export declare function isHandoffOutcome(v: string | boolean | string[] | null | undefined): v is HandoffOutcome;
|
package/dist/hooks.d.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* sequence, writing both outputs to the log file. The parent returns in
|
|
11
11
|
* <100ms so the TUI teardown can't kill the child before it finishes.
|
|
12
12
|
* - SessionStart: `hippo last-sleep --path <path>` - prints the log
|
|
13
|
-
* written by the previous session's detached worker
|
|
14
|
-
*
|
|
13
|
+
* written by the previous session's detached worker to stderr, which
|
|
14
|
+
* keeps it out of the model's context, and then clears it.
|
|
15
15
|
* Earlier Claude Code forms are detected and migrated automatically:
|
|
16
16
|
* - < 0.20.2: `Stop` hook firing `hippo sleep` on every assistant turn.
|
|
17
17
|
* - < 0.21.0: bare `hippo sleep` in SessionEnd, no `--log-file`.
|
|
@@ -60,7 +60,7 @@ export interface CodexWrapperMetadata {
|
|
|
60
60
|
installedAt: string;
|
|
61
61
|
}
|
|
62
62
|
export interface EnsureCodexWrapperResult {
|
|
63
|
-
status: 'installed' | 'already-installed' | 'not-found';
|
|
63
|
+
status: 'installed' | 'already-installed' | 'not-found' | 'source-checkout';
|
|
64
64
|
metadataPath?: string;
|
|
65
65
|
realCodexPath?: string;
|
|
66
66
|
commandPath?: string;
|
|
@@ -172,7 +172,7 @@ export declare function isCodexWrapperInstalled(): boolean;
|
|
|
172
172
|
* doing it from postinstall or routine commands is a consent violation and
|
|
173
173
|
* reads as binary hijacking to security scanners (issue #133).
|
|
174
174
|
*/
|
|
175
|
-
export declare function repairCodexWrapperIfInstalled(): EnsureCodexWrapperResult;
|
|
175
|
+
export declare function repairCodexWrapperIfInstalled(hippoCliPath?: string): EnsureCodexWrapperResult;
|
|
176
176
|
export declare function resolveCodexSessionTranscript(options: CodexSessionTranscriptOptions): string | null;
|
|
177
177
|
export declare function resolveJsonHookPaths(target: JsonHookTarget): JsonHookPaths;
|
|
178
178
|
export declare function installJsonHooks(target: JsonHookTarget): InstallResult;
|
package/dist/hooks.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* sequence, writing both outputs to the log file. The parent returns in
|
|
11
11
|
* <100ms so the TUI teardown can't kill the child before it finishes.
|
|
12
12
|
* - SessionStart: `hippo last-sleep --path <path>` - prints the log
|
|
13
|
-
* written by the previous session's detached worker
|
|
14
|
-
*
|
|
13
|
+
* written by the previous session's detached worker to stderr, which
|
|
14
|
+
* keeps it out of the model's context, and then clears it.
|
|
15
15
|
* Earlier Claude Code forms are detected and migrated automatically:
|
|
16
16
|
* - < 0.20.2: `Stop` hook firing `hippo sleep` on every assistant turn.
|
|
17
17
|
* - < 0.21.0: bare `hippo sleep` in SessionEnd, no `--log-file`.
|
|
@@ -412,10 +412,16 @@ export function isCodexWrapperInstalled() {
|
|
|
412
412
|
* doing it from postinstall or routine commands is a consent violation and
|
|
413
413
|
* reads as binary hijacking to security scanners (issue #133).
|
|
414
414
|
*/
|
|
415
|
-
export function repairCodexWrapperIfInstalled() {
|
|
415
|
+
export function repairCodexWrapperIfInstalled(hippoCliPath = resolveHippoCliPath()) {
|
|
416
416
|
if (readCodexWrapperMetadata() === null) {
|
|
417
417
|
return { status: 'not-found' };
|
|
418
418
|
}
|
|
419
|
+
// A checkout's own `npm install` or CLI would point the user's launcher at a folder that may be deleted.
|
|
420
|
+
// SHORTCUT: any node_modules copy (npx cache, a project dependency) still repairs; record the opted-in CLI path in the metadata to close that.
|
|
421
|
+
const packageDir = path.dirname(path.dirname(hippoCliPath));
|
|
422
|
+
if (path.basename(path.dirname(packageDir)).toLowerCase() !== 'node_modules') {
|
|
423
|
+
return { status: 'source-checkout' };
|
|
424
|
+
}
|
|
419
425
|
return ensureCodexWrapperInstalled();
|
|
420
426
|
}
|
|
421
427
|
function collectFiles(dir) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -989,9 +989,9 @@ async function executeTool(name, args, ctx) {
|
|
|
989
989
|
// Auto-detect query from git
|
|
990
990
|
let query = '';
|
|
991
991
|
try {
|
|
992
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
993
|
-
const diff = execSync('git diff --cached --stat 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
994
|
-
const log = execSync('git log -1 --pretty=format:"%s" 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
992
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
|
|
993
|
+
const diff = execSync('git diff --cached --stat 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
|
|
994
|
+
const log = execSync('git log -1 --pretty=format:"%s" 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
|
|
995
995
|
query = [branch, log, diff].filter(Boolean).join(' ');
|
|
996
996
|
}
|
|
997
997
|
catch { /* not a git repo */ }
|
package/dist/secret-detect.js
CHANGED
|
@@ -39,7 +39,7 @@ const SECRET_PATTERNS = [
|
|
|
39
39
|
// sk-... (OpenAI/Anthropic style) and sk_<vendor>_... shapes. Both require
|
|
40
40
|
// a key-ish noun somewhere in the content (co-occurrence guard) so prose
|
|
41
41
|
// like "the sk- prefix identifies API keys" without an actual long token
|
|
42
|
-
// does not flag, but a stored key ("
|
|
42
|
+
// does not flag, but a stored key ("prod API key sk_live_...")
|
|
43
43
|
// does.
|
|
44
44
|
{ name: 'sk-style-key', re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
|
|
45
45
|
{ name: 'sk-underscore-key', re: /\bsk_[A-Za-z0-9]+_[A-Za-z0-9_]{6,}\b/ },
|
|
@@ -51,7 +51,7 @@ const SECRET_PATTERNS = [
|
|
|
51
51
|
// rollout worked well" were verified false positives that would silently
|
|
52
52
|
// hide real code-lesson memories from ambient context (post-merge
|
|
53
53
|
// adversarial review, 2026-07-02).
|
|
54
|
-
{ name: 'secret-assignment', re: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?(?=[A-Za-z0-9_
|
|
54
|
+
{ name: 'secret-assignment', re: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?(?=[A-Za-z0-9_\-+/]*\d)[A-Za-z0-9_\-+/=]{12,}/i },
|
|
55
55
|
];
|
|
56
56
|
const KEYISH_CONTEXT_RE = /key|token|secret|credential|bearer|auth|password/i;
|
|
57
57
|
const CO_OCCURRENCE_GUARDED = new Set(['sk-style-key', 'sk-underscore-key']);
|
|
@@ -59,7 +59,7 @@ const CO_OCCURRENCE_GUARDED = new Set(['sk-style-key', 'sk-underscore-key']);
|
|
|
59
59
|
const STRICT_ONLY_PATTERNS = [
|
|
60
60
|
/\bbearer\s+[A-Za-z0-9._~+/-]{16,}=*/gi,
|
|
61
61
|
/\bauthorization["']?\s*[:=]\s*["']?basic\s+[A-Za-z0-9+/]{8,}={0,2}/gi,
|
|
62
|
-
|
|
62
|
+
/(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g,
|
|
63
63
|
];
|
|
64
64
|
/**
|
|
65
65
|
* Scan a memory's tags + content for secret material.
|
package/dist/shared.d.ts
CHANGED
|
@@ -157,7 +157,7 @@ export declare function autoShare(localRoot: string, options?: {
|
|
|
157
157
|
}): MemoryEntry[];
|
|
158
158
|
/**
|
|
159
159
|
* Copy all global memories into the local store.
|
|
160
|
-
* Skips entries that already exist locally
|
|
160
|
+
* Skips entries that already exist locally, by ID or by text (promote and share copy under a new ID).
|
|
161
161
|
* Returns the count of newly copied entries.
|
|
162
162
|
*/
|
|
163
163
|
export declare function syncGlobalToLocal(localRoot: string, globalRoot: string, opts?: {
|
package/dist/shared.js
CHANGED
|
@@ -457,7 +457,7 @@ export function autoShare(localRoot, options = {}) {
|
|
|
457
457
|
}
|
|
458
458
|
/**
|
|
459
459
|
* Copy all global memories into the local store.
|
|
460
|
-
* Skips entries that already exist locally
|
|
460
|
+
* Skips entries that already exist locally, by ID or by text (promote and share copy under a new ID).
|
|
461
461
|
* Returns the count of newly copied entries.
|
|
462
462
|
*/
|
|
463
463
|
export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
@@ -468,6 +468,8 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
|
468
468
|
// the local-root context provides one.
|
|
469
469
|
const globalEntries = loadAllEntries(globalRoot);
|
|
470
470
|
const localIndex = loadIndex(localRoot);
|
|
471
|
+
const textKey = (e) => `${e.tenantId}\n${e.content}`;
|
|
472
|
+
const localText = new Set(loadAllEntries(localRoot).map(textKey));
|
|
471
473
|
// v39 (codex P1-4): syncing down must not re-import what ambient context
|
|
472
474
|
// excludes - other-project rows are skipped by default and secret rows
|
|
473
475
|
// are never copied. origin_project is preserved on the copy (writeEntry
|
|
@@ -484,6 +486,8 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
|
484
486
|
// Skip if already present by ID
|
|
485
487
|
if (localIndex.entries[entry.id])
|
|
486
488
|
continue;
|
|
489
|
+
if (localText.has(textKey(entry)))
|
|
490
|
+
continue;
|
|
487
491
|
if (detectSecret(entry).flagged)
|
|
488
492
|
continue;
|
|
489
493
|
if (!opts.includeCrossProject &&
|
|
@@ -499,6 +503,7 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
|
499
503
|
}
|
|
500
504
|
throw err;
|
|
501
505
|
}
|
|
506
|
+
localText.add(textKey(entry));
|
|
502
507
|
count++;
|
|
503
508
|
}
|
|
504
509
|
if (rejected > 0) {
|
package/dist/store.d.ts
CHANGED
|
@@ -652,12 +652,10 @@ export declare function loadLatestHandoff(hippoRoot: string, tenantId: string, s
|
|
|
652
652
|
export declare function loadHandoffById(hippoRoot: string, tenantId: string, id: number): SessionHandoff | null;
|
|
653
653
|
/** Stamp the outcome on a session's newest handoff, only if it has none yet. Returns rows changed. */
|
|
654
654
|
export declare function stampHandoffOutcome(hippoRoot: string, tenantId: string, sessionId: string, outcome: HandoffOutcome): number;
|
|
655
|
-
/**
|
|
656
|
-
* Auto-write a handoff at session-end from the session's active snapshot (DF1 T3).
|
|
655
|
+
/** Auto-write a handoff at session-end (DF1 T3) from the session's active snapshot, else from `derived`, its transcript state.
|
|
657
656
|
* @param evidence best-effort git state; outcome comes from the newest session_complete event.
|
|
658
|
-
* @returns null
|
|
659
|
-
|
|
660
|
-
export declare function writeSessionEndHandoff(hippoRoot: string, tenantId: string, sessionId: string, evidence: HandoffEvidence | null): SessionHandoff | null;
|
|
657
|
+
* @returns null when neither source is the session's, a newer handoff covers the snapshot, or the session's latest handoff was not read off its transcript. */
|
|
658
|
+
export declare function writeSessionEndHandoff(hippoRoot: string, tenantId: string, sessionId: string, evidence: HandoffEvidence | null, derived?: Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null): SessionHandoff | null;
|
|
661
659
|
export declare function transitionCard(db: DatabaseSyncLike, tenantId: string, cardId: string, from: CardStatus[], to: CardStatus, extra?: {
|
|
662
660
|
setSql?: string;
|
|
663
661
|
whereSql?: string;
|
package/dist/store.js
CHANGED
|
@@ -16,6 +16,7 @@ import { isRecallBoostAblated } from './ablation.js';
|
|
|
16
16
|
import { rarestPromptTerms, RAREST_TERM_COUNT } from './prompt-recall.js';
|
|
17
17
|
import { appendAuditEvent } from './audit.js';
|
|
18
18
|
import { resolveTenantId } from './tenant.js';
|
|
19
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
19
20
|
import { deriveOriginProject, originFromSource, findHippoStoreDir, realpathOrResolve } from './project-identity.js';
|
|
20
21
|
import { checkRejectionGuard, RejectedValueError, rejectionDigest, normalizeValueForRejection, insertRejectedValue, findRejectedValue, } from './rejection.js';
|
|
21
22
|
// AT1 (plan §5): resolveConflict's kind-aware loser removal needs
|
|
@@ -2146,7 +2147,7 @@ export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
|
|
|
2146
2147
|
const result = db.prepare(`
|
|
2147
2148
|
INSERT INTO task_snapshots(task, summary, next_step, status, source, session_id, scope, tenant_id, created_at, updated_at)
|
|
2148
2149
|
VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
|
|
2149
|
-
`).run(snapshot.task, snapshot.summary, snapshot.next_step, snapshot.source ?? 'cli', snapshot.session_id ?? null, snapshot.scope ?? null, tenantId, now, now);
|
|
2150
|
+
`).run(redactSecretsStrict(snapshot.task), redactSecretsStrict(snapshot.summary), redactSecretsStrict(snapshot.next_step), snapshot.source ?? 'cli', snapshot.session_id ?? null, snapshot.scope ?? null, tenantId, now, now);
|
|
2150
2151
|
db.exec('COMMIT');
|
|
2151
2152
|
const id = Number(result.lastInsertRowid ?? 0);
|
|
2152
2153
|
// SAFETY: row's shape matches the ten columns named in the SELECT above.
|
|
@@ -2904,20 +2905,28 @@ export function stampHandoffOutcome(hippoRoot, tenantId, sessionId, outcome) {
|
|
|
2904
2905
|
closeHippoDb(db);
|
|
2905
2906
|
}
|
|
2906
2907
|
}
|
|
2907
|
-
/**
|
|
2908
|
-
* Auto-write a handoff at session-end from the session's active snapshot (DF1 T3).
|
|
2908
|
+
/** Auto-write a handoff at session-end (DF1 T3) from the session's active snapshot, else from `derived`, its transcript state.
|
|
2909
2909
|
* @param evidence best-effort git state; outcome comes from the newest session_complete event.
|
|
2910
|
-
* @returns null
|
|
2911
|
-
|
|
2912
|
-
export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence) {
|
|
2910
|
+
* @returns null when neither source is the session's, a newer handoff covers the snapshot, or the session's latest handoff was not read off its transcript. */
|
|
2911
|
+
export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence, derived = null) {
|
|
2913
2912
|
assertTenantId('writeSessionEndHandoff', tenantId);
|
|
2914
|
-
const
|
|
2915
|
-
if (!snapshot || snapshot.session_id !== sessionId)
|
|
2916
|
-
return null;
|
|
2913
|
+
const active = loadActiveTaskSnapshot(hippoRoot, tenantId);
|
|
2917
2914
|
const existing = loadLatestHandoff(hippoRoot, tenantId, sessionId);
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2915
|
+
let snapshot;
|
|
2916
|
+
let handoffEvidence = evidence;
|
|
2917
|
+
if (active && active.session_id === sessionId) {
|
|
2918
|
+
// Strict '>': a same-millisecond tie must not swallow the session's only write (test 6e).
|
|
2919
|
+
if (existing && existing.updatedAt > active.updated_at)
|
|
2920
|
+
return null;
|
|
2921
|
+
snapshot = active;
|
|
2922
|
+
}
|
|
2923
|
+
else {
|
|
2924
|
+
// Only an earlier transcript read gives way; `hippo handoff create` and unmarked older handoffs keep winning.
|
|
2925
|
+
if (!derived || (existing && existing.evidence?.derivedFrom !== 'transcript'))
|
|
2926
|
+
return null;
|
|
2927
|
+
snapshot = { ...derived, scope: null };
|
|
2928
|
+
handoffEvidence = { ...evidence, derivedFrom: 'transcript' };
|
|
2929
|
+
}
|
|
2921
2930
|
const db = openHippoDb(hippoRoot);
|
|
2922
2931
|
let outcome = null;
|
|
2923
2932
|
try {
|
|
@@ -2947,7 +2956,7 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence)
|
|
|
2947
2956
|
nextAction: snapshot.next_step,
|
|
2948
2957
|
artifacts: carryForward ? existing.artifacts : [],
|
|
2949
2958
|
scope: snapshot.scope,
|
|
2950
|
-
evidence,
|
|
2959
|
+
evidence: handoffEvidence,
|
|
2951
2960
|
outcome,
|
|
2952
2961
|
constraints: carryForward ? existing.constraints : undefined,
|
|
2953
2962
|
targetRuntime: carryForward ? existing.targetRuntime : undefined,
|
package/dist/version.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export declare const PACKAGE_VERSION = "1.52.
|
|
19
|
+
export declare const PACKAGE_VERSION = "1.52.8";
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export declare function compareSemver(a: string, b: string): number;
|
|
22
22
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export const PACKAGE_VERSION = '1.52.
|
|
19
|
+
export const PACKAGE_VERSION = '1.52.8';
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export function compareSemver(a, b) {
|
|
22
22
|
const parse = (v) => {
|