hippo-memory 1.63.2 → 1.64.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 +10 -4
- package/dist/agent-memories/claude-code.js +3 -3
- package/dist/ambient-store.d.ts +2 -0
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +2 -0
- package/dist/api/assemble.js +11 -7
- package/dist/api/auth.d.ts +15 -11
- package/dist/api/auth.js +116 -22
- package/dist/api/context-select.d.ts +5 -3
- package/dist/api/context-select.js +6 -11
- package/dist/api/context-types.d.ts +2 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.js +79 -55
- package/dist/api/dormant.d.ts +5 -5
- package/dist/api/dormant.js +13 -9
- package/dist/api/drill-down.d.ts +2 -0
- package/dist/api/drill-down.js +12 -7
- package/dist/api/forget.d.ts +1 -1
- package/dist/api/forget.js +8 -4
- package/dist/api/learn.js +5 -3
- package/dist/api/outcome.d.ts +1 -1
- package/dist/api/outcome.js +2 -1
- package/dist/api/promote.js +8 -5
- package/dist/api/recall-types.d.ts +4 -2
- package/dist/api/recall.js +51 -36
- package/dist/api/remember.d.ts +7 -0
- package/dist/api/remember.js +26 -2
- package/dist/api/sleep-run.d.ts +28 -0
- package/dist/api/sleep-run.js +267 -0
- package/dist/api/sleep.d.ts +5 -45
- package/dist/api/sleep.js +5 -252
- package/dist/api/types.d.ts +5 -0
- package/dist/api/types.js +4 -0
- package/dist/audit-prune.js +1 -1
- package/dist/audit.d.ts +1 -3
- package/dist/audit.js +7 -103
- package/dist/auth.d.ts +48 -4
- package/dist/auth.js +111 -42
- package/dist/autolearn.js +2 -2
- package/dist/capture/caller-session.d.ts +16 -0
- package/dist/capture/caller-session.js +58 -0
- package/dist/capture/command.d.ts +19 -1
- package/dist/capture/command.js +28 -21
- package/dist/capture/compact-resume-caller.d.ts +11 -0
- package/dist/capture/compact-resume-caller.js +35 -0
- package/dist/capture/compact.d.ts +0 -14
- package/dist/capture/compact.js +7 -160
- package/dist/capture/compaction-items-caller.d.ts +16 -0
- package/dist/capture/compaction-items-caller.js +32 -0
- package/dist/capture/extract.d.ts +2 -14
- package/dist/capture/extract.js +135 -346
- package/dist/capture/failure-caller.d.ts +21 -0
- package/dist/capture/failure-caller.js +74 -0
- package/dist/capture/failure-reading.d.ts +44 -0
- package/dist/capture/failure-reading.js +78 -0
- package/dist/capture/pre-compact-caller.d.ts +16 -0
- package/dist/capture/pre-compact-caller.js +39 -0
- package/dist/capture/session-end-caller.d.ts +21 -0
- package/dist/capture/session-end-caller.js +30 -0
- package/dist/capture/session-texts.d.ts +14 -0
- package/dist/capture/session-texts.js +42 -0
- package/dist/capture/transcript.d.ts +6 -1
- package/dist/capture/transcript.js +20 -16
- package/dist/capture/working-state.d.ts +21 -0
- package/dist/capture/working-state.js +154 -0
- package/dist/capture-error.d.ts +8 -22
- package/dist/capture-error.js +9 -61
- package/dist/cli/audit.js +25 -0
- package/dist/cli/auth.js +4 -3
- package/dist/cli/context.js +13 -158
- package/dist/cli/continuity.js +2 -1
- package/dist/cli/init.js +5 -3
- package/dist/cli/last-sleep.d.ts +6 -0
- package/dist/cli/last-sleep.js +68 -0
- package/dist/cli/quality-repair-once.d.ts +3 -0
- package/dist/cli/quality-repair-once.js +36 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +4 -1
- package/dist/cli/session-hooks.d.ts +0 -3
- package/dist/cli/session-hooks.js +12 -49
- package/dist/cli/setup.js +10 -2
- package/dist/cli/shared.d.ts +7 -14
- package/dist/cli/shared.js +19 -78
- package/dist/cli/sleep.js +6 -3
- package/dist/cli/usage.js +18 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/client.js +1 -1
- package/dist/compaction-record.d.ts +19 -1
- package/dist/compaction-record.js +61 -132
- package/dist/compaction-spool.d.ts +45 -0
- package/dist/compaction-spool.js +454 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +49 -0
- package/dist/connectors/github/deletion.d.ts +1 -1
- package/dist/connectors/github/deletion.js +5 -17
- package/dist/consolidate/conflicts.js +4 -0
- package/dist/consolidate/flush-units.d.ts +9 -0
- package/dist/consolidate/flush-units.js +63 -0
- package/dist/consolidate/llm-passes.js +5 -4
- package/dist/consolidate/merge.js +5 -1
- package/dist/consolidate/run.d.ts +2 -1
- package/dist/consolidate/sleep.d.ts +2 -0
- package/dist/consolidate/sleep.js +82 -15
- package/dist/context-render.d.ts +14 -0
- package/dist/context-render.js +28 -0
- package/dist/dag.js +6 -0
- package/dist/dashboard-actions.js +3 -2
- package/dist/dashboard.js +4 -3
- package/dist/db/backup.d.ts +6 -0
- package/dist/db/backup.js +29 -0
- package/dist/db/busy.d.ts +9 -0
- package/dist/db/busy.js +35 -0
- package/dist/db/continuity.d.ts +1 -0
- package/dist/db/continuity.js +31 -4
- package/dist/db/meta.d.ts +2 -0
- package/dist/db/meta.js +7 -0
- package/dist/db/migrate.js +2 -3
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +5 -1
- package/dist/db/migrations/v39.js +2 -7
- package/dist/db/migrations/v53.d.ts +3 -0
- package/dist/db/migrations/v53.js +16 -0
- package/dist/db/migrations/v54.d.ts +3 -0
- package/dist/db/migrations/v54.js +25 -0
- package/dist/db/open.d.ts +11 -0
- package/dist/db/open.js +27 -0
- package/dist/db/tables.d.ts +2 -0
- package/dist/db/tables.js +6 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -15
- package/dist/dormant.d.ts +16 -20
- package/dist/dormant.js +24 -30
- package/dist/entry/json-hooks.d.ts +3 -0
- package/dist/entry/json-hooks.js +4 -0
- package/dist/entry/project-identity.d.ts +2 -0
- package/dist/entry/project-identity.js +3 -0
- package/dist/entry/session-text.d.ts +7 -0
- package/dist/entry/session-text.js +8 -0
- package/dist/extract.js +6 -0
- package/dist/failure-log.d.ts +10 -2
- package/dist/failure-log.js +19 -2
- package/dist/gated-write.js +1 -1
- package/dist/graph/delta.d.ts +44 -0
- package/dist/graph/delta.js +109 -0
- package/dist/graph/types.d.ts +5 -0
- package/dist/graph/write.d.ts +20 -20
- package/dist/graph/write.js +103 -36
- package/dist/graph-extract.d.ts +43 -9
- package/dist/graph-extract.js +140 -157
- package/dist/graph-recall.d.ts +1 -0
- package/dist/graph-recall.js +2 -2
- package/dist/graph-view.d.ts +1 -0
- package/dist/graph-view.js +25 -2
- package/dist/half-life-migration.js +5 -1
- package/dist/handoff-evidence.d.ts +5 -0
- package/dist/handoff-evidence.js +30 -0
- package/dist/home-path.d.ts +5 -0
- package/dist/home-path.js +24 -0
- package/dist/hooks/json-hooks.d.ts +13 -2
- package/dist/hooks/json-hooks.js +246 -117
- package/dist/hooks/opencode.js +1 -1
- package/dist/hooks/shared.d.ts +2 -0
- package/dist/hooks/shared.js +6 -1
- package/dist/http-util.d.ts +16 -7
- package/dist/http-util.js +65 -20
- package/dist/importers/vault.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +7 -0
- package/dist/ledger-db.d.ts +10 -0
- package/dist/ledger-db.js +42 -0
- package/dist/mcp/admin-tools.d.ts +2 -2
- package/dist/mcp/admin-tools.js +47 -5
- package/dist/mcp/memory-tools.js +7 -2
- package/dist/mcp/protocol.d.ts +6 -0
- package/dist/mcp/protocol.js +4 -2
- package/dist/mcp/recall-tools.js +19 -6
- package/dist/mcp/request.js +4 -0
- package/dist/mcp/session-state.d.ts +4 -0
- package/dist/mcp/session-state.js +15 -6
- package/dist/mcp/shared-gate.d.ts +4 -0
- package/dist/mcp/shared-gate.js +22 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/memory-quality.d.ts +35 -0
- package/dist/memory-quality.js +191 -0
- package/dist/memory.d.ts +4 -4
- package/dist/physics-state.js +1 -1
- package/dist/pilot-arm.d.ts +14 -3
- package/dist/pilot-arm.js +32 -10
- package/dist/postinstall.js +2 -2
- package/dist/project-briefs.js +4 -4
- package/dist/project-identity.d.ts +14 -10
- package/dist/project-identity.js +29 -11
- package/dist/project-merge.d.ts +0 -2
- package/dist/project-merge.js +9 -13
- package/dist/prompt-hook.d.ts +65 -0
- package/dist/prompt-hook.js +213 -0
- package/dist/prompt-recall.js +1 -1
- package/dist/quality-repair.d.ts +27 -0
- package/dist/quality-repair.js +220 -0
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +5 -3
- package/dist/raw-archive.d.ts +1 -1
- package/dist/raw-archive.js +6 -16
- package/dist/recall-scope.d.ts +28 -5
- package/dist/recall-scope.js +52 -7
- package/dist/recall-trace.js +1 -1
- package/dist/reject-flow.d.ts +2 -0
- package/dist/reject-flow.js +20 -6
- package/dist/secret-detect.js +7 -8
- package/dist/server/auth.d.ts +8 -6
- package/dist/server/auth.js +85 -38
- package/dist/server/client-ip.d.ts +2 -0
- package/dist/server/client-ip.js +16 -1
- package/dist/server/mcp-http.d.ts +9 -3
- package/dist/server/mcp-http.js +56 -13
- package/dist/server/request.d.ts +1 -1
- package/dist/server/request.js +5 -5
- package/dist/server/routes/admin.js +12 -6
- package/dist/server/routes/memories.js +9 -1
- package/dist/server/routes/recall.js +14 -2
- package/dist/server/types.d.ts +42 -5
- package/dist/server/validation.d.ts +5 -2
- package/dist/server/validation.js +19 -2
- package/dist/server.d.ts +21 -4
- package/dist/server.js +207 -49
- package/dist/session-digest.d.ts +0 -1
- package/dist/session-digest.js +3 -12
- package/dist/session-owners.d.ts +7 -0
- package/dist/session-owners.js +54 -0
- package/dist/share-scrub.d.ts +3 -0
- package/dist/share-scrub.js +8 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +26 -18
- package/dist/store/candidates.d.ts +14 -3
- package/dist/store/candidates.js +48 -26
- package/dist/store/conflicts.d.ts +15 -0
- package/dist/store/conflicts.js +68 -44
- package/dist/store/delete-and-batch.d.ts +25 -1
- package/dist/store/delete-and-batch.js +121 -69
- package/dist/store/entry-reads.d.ts +5 -6
- package/dist/store/entry-reads.js +24 -12
- package/dist/store/entry-row.d.ts +7 -10
- package/dist/store/entry-row.js +40 -18
- package/dist/store/entry-writes.d.ts +3 -3
- package/dist/store/entry-writes.js +9 -23
- package/dist/store/handoffs.d.ts +9 -6
- package/dist/store/handoffs.js +72 -59
- package/dist/store/index-and-stats.js +2 -2
- package/dist/store/open.d.ts +3 -1
- package/dist/store/open.js +3 -3
- package/dist/store/search-rows.d.ts +8 -5
- package/dist/store/search-rows.js +15 -18
- package/dist/store/sessions.d.ts +25 -6
- package/dist/store/sessions.js +61 -19
- package/dist/store/summaries.d.ts +1 -1
- package/dist/store/summaries.js +3 -18
- package/dist/store/tenant-lookup.d.ts +7 -3
- package/dist/store/tenant-lookup.js +9 -9
- package/dist/store-port.d.ts +13 -0
- package/dist/store-port.js +20 -0
- package/dist/version.d.ts +5 -1
- package/dist/version.js +5 -1
- package/dist/working-memory.js +1 -1
- package/dist/write-budget.d.ts +15 -0
- package/dist/write-budget.js +15 -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 +8 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type MemoryEntry } from './memory.js';
|
|
2
|
+
export declare const STOP_WORDS: Set<string>;
|
|
3
|
+
export type AutomaticMemoryDefect = 'too-short' | 'release-activity' | 'sentence-fragment' | 'possible-fragment' | 'subjectless-outcome' | 'raw-output' | 'too-vague' | 'no-specificity';
|
|
4
|
+
/** `reason` names a defect even when `accepted`: a possible fragment is stored and listed for review. */
|
|
5
|
+
export interface AutomaticMemoryAssessment {
|
|
6
|
+
readonly accepted: boolean;
|
|
7
|
+
readonly reason: AutomaticMemoryDefect | null;
|
|
8
|
+
}
|
|
9
|
+
/** Counts spaced words plus CJK letters without lowering mixed-script counts. */
|
|
10
|
+
export declare function substantiveWordCount(text: string): number;
|
|
11
|
+
/** Preserves the auditor's existing destructive release-noise classification. */
|
|
12
|
+
export declare function isReleaseCommitNoise(text: string): boolean;
|
|
13
|
+
/** Short preferences and rules carry a useful relationship even without a path or number. */
|
|
14
|
+
export declare function hasNoSpecificity(text: string): boolean;
|
|
15
|
+
/** Names automatic-capture defects without inventing missing context. */
|
|
16
|
+
export declare function assessAutomaticMemory(content: string): AutomaticMemoryAssessment;
|
|
17
|
+
export declare function isCertainReason(reason: AutomaticMemoryDefect | null): boolean;
|
|
18
|
+
/** The bar for sleep's derived memories and for repair: a defect the checks are sure of, else null. */
|
|
19
|
+
export declare function certainDefect(content: string): AutomaticMemoryDefect | null;
|
|
20
|
+
export declare const BUNDLE_HEADER: RegExp;
|
|
21
|
+
type Provenance = Pick<MemoryEntry, 'source' | 'confidence' | 'content' | 'extracted_from' | 'dag_level' | 'tags'>;
|
|
22
|
+
/** A row hippo wrote that no person vouched for: hippo's writers stamp observed or inferred, a person's edit or restore verified. */
|
|
23
|
+
export declare function isAutomaticEntry(entry: Provenance): boolean;
|
|
24
|
+
/** The audit's check: a person's row is never judged, and a bundle is flagged only when every part is, as reuse and recent context judge it. */
|
|
25
|
+
export declare function automaticDefect(entry: Provenance): AutomaticMemoryDefect | null;
|
|
26
|
+
/** Whether sleep may derive from a row or share it: a person's text and watch failures always, hippo's own text only without a certain defect. */
|
|
27
|
+
export declare function isReusable(entry: Provenance): boolean;
|
|
28
|
+
/** The person floor's fragment test, on trimmed text. */
|
|
29
|
+
export declare function isFragment(text: string): boolean;
|
|
30
|
+
/** Admission check for text no person typed: capture and compaction items. */
|
|
31
|
+
export declare function isContentWorthStoring(content: string): boolean;
|
|
32
|
+
/** Recent-context floor: rows hippo wrote meet the automatic check, a person's rows only the older, looser floor. */
|
|
33
|
+
export declare function isWorthSurfacing(entry: MemoryEntry): boolean;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=memory-quality.d.ts.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { COMPACTION_MEMORY_TAG, COMPACTION_SOURCE_PREFIX } from './memory.js';
|
|
2
|
+
import { heldTexts } from './same-text.js';
|
|
3
|
+
export const STOP_WORDS = new Set([
|
|
4
|
+
'the', 'a', 'an', 'is', 'was', 'are', 'were', 'be', 'been', 'being',
|
|
5
|
+
'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'it',
|
|
6
|
+
'this', 'that', 'and', 'or', 'but', 'not', 'no', 'so', 'if', 'do',
|
|
7
|
+
'did', 'does', 'has', 'had', 'have', 'will', 'would', 'could', 'should',
|
|
8
|
+
'may', 'might', 'can', 'shall', 'we', 'i', 'you', 'they', 'he', 'she',
|
|
9
|
+
'my', 'our', 'your', 'its', 'his', 'her', 'their', 'up', 'out', 'just',
|
|
10
|
+
'also', 'then', 'than', 'some', 'all', 'any', 'each', 'very', 'too',
|
|
11
|
+
]);
|
|
12
|
+
const CJK_LETTERS = /(?=\p{L})(?:\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana})/gu;
|
|
13
|
+
const CHAT_ACRONYMS = /^(?:TODO|FYI|LGTM|IIRC|IMO|IMHO|FWIW|TBD|BTW|ASAP|AFAIK|WIP|NB|PS)$/;
|
|
14
|
+
/** Counts spaced words plus CJK letters without lowering mixed-script counts. */
|
|
15
|
+
export function substantiveWordCount(text) {
|
|
16
|
+
const cjkLetterCount = (text.match(CJK_LETTERS) ?? []).length;
|
|
17
|
+
const spacedCount = text.toLowerCase().split(/\s+/).filter(w => w.length > 2 && !STOP_WORDS.has(w)).length;
|
|
18
|
+
return spacedCount + Math.floor(cjkLetterCount / 2);
|
|
19
|
+
}
|
|
20
|
+
/** Preserves the auditor's existing destructive release-noise classification. */
|
|
21
|
+
export function isReleaseCommitNoise(text) {
|
|
22
|
+
return /^(?:bump|release|prep|tag)\s+(?:to\s+)?v?\d+\.\d+/i.test(text)
|
|
23
|
+
|| /^v?\d+\.\d+\.\d+\s*$/i.test(text)
|
|
24
|
+
|| /^chore(?:\([^)]+\))?:\s*(?:release|bump|version|tag|prep)\b/i.test(text)
|
|
25
|
+
|| /^(?:Merge branch|Merge pull request)\b/i.test(text)
|
|
26
|
+
|| /^WIP\b/i.test(text);
|
|
27
|
+
}
|
|
28
|
+
/** Short preferences and rules carry a useful relationship even without a path or number. */
|
|
29
|
+
export function hasNoSpecificity(text) {
|
|
30
|
+
const domainAcronyms = (text.match(/\b[A-Z]{2,6}\b/g) ?? []).filter(a => !CHAT_ACRONYMS.test(a));
|
|
31
|
+
if (/\d|[A-Z][a-z]{2,}|[/\\.]|[`_{}()\[\]]/.test(text))
|
|
32
|
+
return false;
|
|
33
|
+
if (domainAcronyms.length > 0 && /[a-z]/.test(text))
|
|
34
|
+
return false;
|
|
35
|
+
if (/^(?:prefer|use|avoid|never use|don't use)\s+(?!(?:the|that|this|it|them|those|these|one|other|something)\b)\S+/i.test(text))
|
|
36
|
+
return false;
|
|
37
|
+
if (/\b(?:never|always|must)\s+(?!(?:the|that|this|it|them|those|these|one|other|something)\b)\S+/i.test(text))
|
|
38
|
+
return false;
|
|
39
|
+
return text.split(/\s+/).length < 8 && /^[\w\s,.'"-]+$/.test(text);
|
|
40
|
+
}
|
|
41
|
+
const RELATION_WORD = /\b(?:when|if|unless|because|since|after|before|until|while|so|instead)\b/i;
|
|
42
|
+
const RULE_WORD = /\b(?:never|always|must|don't|do not|make sure|ensure|prefer|avoid)\b/i;
|
|
43
|
+
function isRoutineReleaseActivity(text) {
|
|
44
|
+
if (isReleaseCommitNoise(text))
|
|
45
|
+
return true;
|
|
46
|
+
// "Deploy only after the migration has run" is a rule about releases, not a log line.
|
|
47
|
+
if (RELATION_WORD.test(text) || RULE_WORD.test(text))
|
|
48
|
+
return false;
|
|
49
|
+
const subject = text.replace(/^(?:chore|ci|build)(?:\([^)]*\))?:\s*/i, '');
|
|
50
|
+
if (/^(?:bump|increment|increase|update|set)\s+(?:(?:ios|android|app)\s+)?(?:build(?:\s+number)?|version|release)\s+(?:to\s+)?(?:v?\d|#\d)/i.test(subject))
|
|
51
|
+
return true;
|
|
52
|
+
return /^(?:deploy(?:ed|ing)?|ship(?:ped|ping)?|release(?:d|ing)?)\s+(?:the\s+)?(?:build|release|version|v?\d)\s*(?:v?\d|#\d|\.\d)/i.test(subject)
|
|
53
|
+
|| /^(?:deployment|release|build)\s+(?:succeeded|completed|finished)\b/i.test(subject);
|
|
54
|
+
}
|
|
55
|
+
const bareText = (text) => text.replace(/[.!?,;:\s]+$/, '');
|
|
56
|
+
// These endings close whole sentences too ("the branch you push to"), so they are only a possible cut.
|
|
57
|
+
const POSSIBLE_CUT_ENDING = /\b(?:needs to|depends on|requires|while|with|without|into|for|of|by|to|is|are|was|were|must|should|could|would|might)$/i;
|
|
58
|
+
/** A short leading condition with no rule word, comma or "then": "If the build fails". */
|
|
59
|
+
function isBareCondition(bare) {
|
|
60
|
+
return !/[,;]|\bthen\b/i.test(bare) && !RULE_WORD.test(bare) && bare.split(/\s+/).length <= 6;
|
|
61
|
+
}
|
|
62
|
+
function isDanglingAssertion(text) {
|
|
63
|
+
const bare = bareText(text);
|
|
64
|
+
// Only a lowercase start is a cut: "To rebuild the index, run ..." opens an instruction.
|
|
65
|
+
if (/^(?:to|and|for)\s/.test(bare) && bare.length < 50)
|
|
66
|
+
return true;
|
|
67
|
+
// Prepositions, modals and "is" can end a whole sentence ("the branch it merges into"), so only cut-off endings count.
|
|
68
|
+
if (/\b(?:to be|has been|will be|instead of|rather than|such as|than|because|if|unless|when|until|and|or|but)$/i.test(bare) || /\b(?:the|a|an)$/.test(bare))
|
|
69
|
+
return true;
|
|
70
|
+
return /^(?:if|unless|when|while|until)\b/.test(bare) && isBareCondition(bare);
|
|
71
|
+
}
|
|
72
|
+
/** A capitalised short condition may carry its rule with no comma: "When CI fails we retry once". */
|
|
73
|
+
function isPossibleFragment(text) {
|
|
74
|
+
const bare = bareText(text);
|
|
75
|
+
return POSSIBLE_CUT_ENDING.test(bare) || (/^(?:If|Unless|When|While|Until)\b/.test(bare) && isBareCondition(bare));
|
|
76
|
+
}
|
|
77
|
+
const OUTCOME_LEAD = /^(?:succeeds?|succeeded|fails?|failed|passes|passed|completed|done|successful|success|returned|returns|inserted|updated|deleted)\b/i;
|
|
78
|
+
const OUTCOME_DETAIL = /\b(?:for|on|in|at|to|from|with|without|by|after|before|under|is|are|was|were|be|been|will|can|must|should|never|always)\b/i;
|
|
79
|
+
function isSubjectlessOutcome(text) {
|
|
80
|
+
const lead = OUTCOME_LEAD.exec(text);
|
|
81
|
+
if (lead === null || RELATION_WORD.test(text))
|
|
82
|
+
return false;
|
|
83
|
+
// "Returns 404 for unknown ids" and "Deleted rows must stay deleted" say when or what; "returned 3 matching rows" does not.
|
|
84
|
+
return !OUTCOME_DETAIL.test(text.slice(lead[0].length).replace(/\([^)]*\)/g, ' '));
|
|
85
|
+
}
|
|
86
|
+
const OUTPUT_LINE = /^(?:\$\s|>\s|(?:stdout|stderr|exit code):|Command\s+['"].*\s+failed\b|Traceback\b|npm\s+(?:ERR|WARN)\b|error\s+TS\d+\b)/i;
|
|
87
|
+
const STACK_FRAME = /^at\s+\S.*:\d+(?::\d+)?\)?$/;
|
|
88
|
+
const ERROR_LEAD = /^(?:[\w.$]+Error|[\w.$]+Exception):\s/;
|
|
89
|
+
// Log lines say "after" and "while" too, so only a rule word frees a timestamp or level lead: a dated freeze saying never merge.
|
|
90
|
+
const LOG_LEADS = [
|
|
91
|
+
/^\d{4}-\d{2}-\d{2}[T ][\d:.]+(?:Z|[+-][\d:]+)?\s+(?:\[?\w+\]?\s+)?/,
|
|
92
|
+
/^\[(?:info|warn|error|debug)\]/i,
|
|
93
|
+
];
|
|
94
|
+
const DATA_LEADS = [/^\{\s*["']?[\w$-]+["']?\s*:/, /^\[\s*(?:[{["]|-?\d[\d.]*\s*[,\]])/];
|
|
95
|
+
/** Prose after the last closing bracket: "{ retries: 3 } is the default policy". */
|
|
96
|
+
function hasProseAfterData(text) {
|
|
97
|
+
const tail = text.slice(Math.max(text.lastIndexOf('}'), text.lastIndexOf(']')) + 1);
|
|
98
|
+
return (tail.match(/\p{L}{2,}/gu) ?? []).length >= 2;
|
|
99
|
+
}
|
|
100
|
+
function isRawOutput(text) {
|
|
101
|
+
if (OUTPUT_LINE.test(text) || STACK_FRAME.test(text))
|
|
102
|
+
return true;
|
|
103
|
+
if (ERROR_LEAD.test(text))
|
|
104
|
+
return !RELATION_WORD.test(text) && !RULE_WORD.test(text);
|
|
105
|
+
if (LOG_LEADS.some((lead) => lead.test(text)))
|
|
106
|
+
return !RULE_WORD.test(text);
|
|
107
|
+
return DATA_LEADS.some((lead) => lead.test(text)) && !hasProseAfterData(text);
|
|
108
|
+
}
|
|
109
|
+
/** Names automatic-capture defects without inventing missing context. */
|
|
110
|
+
export function assessAutomaticMemory(content) {
|
|
111
|
+
const text = content.trim();
|
|
112
|
+
let reason = null;
|
|
113
|
+
if (text.length < 10)
|
|
114
|
+
reason = 'too-short';
|
|
115
|
+
else if (isRoutineReleaseActivity(text))
|
|
116
|
+
reason = 'release-activity';
|
|
117
|
+
else if (isRawOutput(text))
|
|
118
|
+
reason = 'raw-output';
|
|
119
|
+
else if (isDanglingAssertion(text))
|
|
120
|
+
reason = 'sentence-fragment';
|
|
121
|
+
else if (isSubjectlessOutcome(text))
|
|
122
|
+
reason = 'subjectless-outcome';
|
|
123
|
+
else if (isPossibleFragment(text))
|
|
124
|
+
reason = 'possible-fragment';
|
|
125
|
+
else if (substantiveWordCount(text) < 2)
|
|
126
|
+
reason = 'too-vague';
|
|
127
|
+
else if (text.length < 40 && hasNoSpecificity(text))
|
|
128
|
+
reason = 'no-specificity';
|
|
129
|
+
return { accepted: reason === null || reason === 'possible-fragment', reason };
|
|
130
|
+
}
|
|
131
|
+
/** Reasons the heuristics can get wrong: repair only lists them for review and derivation still admits them. */
|
|
132
|
+
const UNCERTAIN_REASONS = new Set(['too-short', 'possible-fragment', 'too-vague', 'no-specificity']);
|
|
133
|
+
export function isCertainReason(reason) {
|
|
134
|
+
return reason !== null && !UNCERTAIN_REASONS.has(reason);
|
|
135
|
+
}
|
|
136
|
+
/** The bar for sleep's derived memories and for repair: a defect the checks are sure of, else null. */
|
|
137
|
+
export function certainDefect(content) {
|
|
138
|
+
const { reason } = assessAutomaticMemory(content);
|
|
139
|
+
return isCertainReason(reason) ? reason : null;
|
|
140
|
+
}
|
|
141
|
+
// Watch failures ('autolearn') and tool failures keep a fixed "Command 'x' failed" format by design, so they are not judged.
|
|
142
|
+
const AUTOMATIC_SOURCES = new Set(['capture', 'git-learn', 'git', 'consolidation']);
|
|
143
|
+
// Promote and share rewrite the source but keep the tags, so the writer's tag still marks a copy; elsewhere a person may set it.
|
|
144
|
+
const AUTOMATIC_TAGS = new Set(['captured', COMPACTION_MEMORY_TAG, 'git-learned']);
|
|
145
|
+
const COPY_SOURCE = /^(?:promoted|shared):/;
|
|
146
|
+
export const BUNDLE_HEADER = /^\[Consolidated(?: from| pattern from) \d+ related memor(?:y|ies)(?:, newest first)?\]\n\n/;
|
|
147
|
+
/** A row hippo wrote that no person vouched for: hippo's writers stamp observed or inferred, a person's edit or restore verified. */
|
|
148
|
+
export function isAutomaticEntry(entry) {
|
|
149
|
+
if (entry.confidence !== 'observed' && entry.confidence !== 'inferred')
|
|
150
|
+
return false;
|
|
151
|
+
return AUTOMATIC_SOURCES.has(entry.source) || entry.source.startsWith(COMPACTION_SOURCE_PREFIX)
|
|
152
|
+
|| (COPY_SOURCE.test(entry.source) && entry.tags.some((tag) => AUTOMATIC_TAGS.has(tag)))
|
|
153
|
+
|| entry.extracted_from !== null || entry.dag_level >= 1 || BUNDLE_HEADER.test(entry.content);
|
|
154
|
+
}
|
|
155
|
+
/** A bundle's parts are other rows' words, possibly a person's, so a bundle is sure to be junk only when every part is. */
|
|
156
|
+
function bundleParts(entry) {
|
|
157
|
+
return BUNDLE_HEADER.test(entry.content) ? heldTexts({ content: entry.content, source: 'consolidation' }) : [];
|
|
158
|
+
}
|
|
159
|
+
function hasCertainDefect(entry) {
|
|
160
|
+
const parts = bundleParts(entry);
|
|
161
|
+
return (parts.length > 0 ? parts : [entry.content]).every((text) => certainDefect(text) !== null);
|
|
162
|
+
}
|
|
163
|
+
/** The audit's check: a person's row is never judged, and a bundle is flagged only when every part is, as reuse and recent context judge it. */
|
|
164
|
+
export function automaticDefect(entry) {
|
|
165
|
+
if (!isAutomaticEntry(entry))
|
|
166
|
+
return null;
|
|
167
|
+
const parts = bundleParts(entry);
|
|
168
|
+
const reasons = (parts.length > 0 ? parts : [entry.content.trim()]).map((text) => assessAutomaticMemory(text).reason);
|
|
169
|
+
return reasons.every((reason) => reason !== null) ? reasons[0] ?? null : null;
|
|
170
|
+
}
|
|
171
|
+
/** Whether sleep may derive from a row or share it: a person's text and watch failures always, hippo's own text only without a certain defect. */
|
|
172
|
+
export function isReusable(entry) {
|
|
173
|
+
return !isAutomaticEntry(entry) || !hasCertainDefect(entry);
|
|
174
|
+
}
|
|
175
|
+
/** The person floor's fragment test, on trimmed text. */
|
|
176
|
+
export function isFragment(text) {
|
|
177
|
+
return /^(?:to|for|and) /.test(text) && text.length < 50;
|
|
178
|
+
}
|
|
179
|
+
/** Admission check for text no person typed: capture and compaction items. */
|
|
180
|
+
export function isContentWorthStoring(content) {
|
|
181
|
+
return assessAutomaticMemory(content).accepted;
|
|
182
|
+
}
|
|
183
|
+
/** Recent-context floor: rows hippo wrote meet the automatic check, a person's rows only the older, looser floor. */
|
|
184
|
+
export function isWorthSurfacing(entry) {
|
|
185
|
+
if (isAutomaticEntry(entry))
|
|
186
|
+
return bundleParts(entry).length > 0 ? !hasCertainDefect(entry) : isContentWorthStoring(entry.content);
|
|
187
|
+
const text = entry.content.trim();
|
|
188
|
+
return text.length >= 10 && !isReleaseCommitNoise(text) && !isFragment(text)
|
|
189
|
+
&& substantiveWordCount(text) >= 2 && !(text.length < 40 && hasNoSpecificity(text));
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=memory-quality.js.map
|
package/dist/memory.d.ts
CHANGED
|
@@ -87,10 +87,10 @@ export interface MemoryEntry {
|
|
|
87
87
|
/**
|
|
88
88
|
* Memory scope isolation: owning project for ambient-context
|
|
89
89
|
* partitioning. A lowercased project name, '' for user-global (injectable
|
|
90
|
-
* everywhere), or null for
|
|
91
|
-
* context treats null as other-project (deny).
|
|
92
|
-
* at write time
|
|
93
|
-
* written.
|
|
90
|
+
* everywhere), or null for no known project (a legacy row, or a shared-store
|
|
91
|
+
* write that named none) - ambient context treats null as other-project (deny).
|
|
92
|
+
* Stamped at write time by store/entry-row.ts stampOriginProject via fallbackOrigin;
|
|
93
|
+
* undefined only on entries not yet written.
|
|
94
94
|
*/
|
|
95
95
|
origin_project?: string | null;
|
|
96
96
|
/**
|
package/dist/physics-state.js
CHANGED
|
@@ -100,7 +100,7 @@ export function savePhysicsState(db, particles) {
|
|
|
100
100
|
last_simulation = excluded.last_simulation,
|
|
101
101
|
updated_at = datetime('now')
|
|
102
102
|
`);
|
|
103
|
-
db.exec('BEGIN');
|
|
103
|
+
db.exec('BEGIN IMMEDIATE');
|
|
104
104
|
try {
|
|
105
105
|
for (const p of particles) {
|
|
106
106
|
stmt.run(p.memoryId, float32ToBuffer(p.position), float32ToBuffer(p.velocity), p.mass, p.charge, p.temperature, p.lastSimulation);
|
package/dist/pilot-arm.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { type DatabaseSyncLike } from './db.js';
|
|
2
|
+
import { type LedgerRootOpts } from './ledger-db.js';
|
|
2
3
|
export type PilotArm = 'hippo' | 'holdout';
|
|
4
|
+
export interface SessionArmOpts extends LedgerRootOpts {
|
|
5
|
+
/** Only the caller tenant's arm row counts, so a caller holding another tenant's session id cannot claim that session's arm. */
|
|
6
|
+
readonly ownTenantOnly?: boolean;
|
|
7
|
+
}
|
|
3
8
|
/** Deterministic split: the same session and rate always land in the same arm. */
|
|
4
9
|
export declare function hashArm(sessionId: string, rateBp: number): PilotArm;
|
|
5
|
-
/** The session's first stored arm, or null; never writes.
|
|
6
|
-
export declare function readPilotArm(db: DatabaseSyncLike, sessionId: string): PilotArm | null;
|
|
10
|
+
/** The session's first stored arm, or null; never writes. Without `tenantId` any tenant's row counts, since on one machine a session has one arm. */
|
|
11
|
+
export declare function readPilotArm(db: DatabaseSyncLike, sessionId: string, tenantId?: string): PilotArm | null;
|
|
7
12
|
/** The stored arm, else the hash arm written once; on any error the hash arm comes back unrecorded. */
|
|
8
|
-
export declare function ensurePilotArm(db: DatabaseSyncLike, tenantId: string, sessionId: string, rateBp: number,
|
|
13
|
+
export declare function ensurePilotArm(db: DatabaseSyncLike, tenantId: string, sessionId: string, rateBp: number, opts?: {
|
|
14
|
+
ownTenantOnly?: boolean;
|
|
15
|
+
now?: string;
|
|
16
|
+
}): PilotArm;
|
|
17
|
+
/** The session's arm, or null at rate 0, with no session id, or with no store.
|
|
18
|
+
* `write` books the arm row; a read-only caller (env-only id, sub-agent) follows the stored arm, else the hash. */
|
|
19
|
+
export declare function sessionPilotArm(hippoRoot: string, tenantId: string, sessionId: string | undefined, write: boolean, opts?: SessionArmOpts): PilotArm | null;
|
|
9
20
|
//# sourceMappingURL=pilot-arm.d.ts.map
|
package/dist/pilot-arm.js
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
// Pilot arm: one token_ledger row per session names its arm, `hippo` or `holdout`, so a pilot can compare them.
|
|
2
2
|
// `items` holds the holdout rate in basis points; readers outside this repo depend on these rows, so their shape is fixed.
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { execWithBusyRetry, HOOK_DB_WAIT_MS, scopedBusyWait } from './db.js';
|
|
6
|
+
import { ledgerRoot, withLedgerDb } from './ledger-db.js';
|
|
5
7
|
import { recordTokenUse } from './token-ledger.js';
|
|
6
8
|
/** Deterministic split: the same session and rate always land in the same arm. */
|
|
7
9
|
export function hashArm(sessionId, rateBp) {
|
|
8
10
|
const bucket = parseInt(createHash('sha256').update(sessionId).digest('hex').slice(0, 8), 16) % 10000;
|
|
9
11
|
return bucket < rateBp ? 'holdout' : 'hippo';
|
|
10
12
|
}
|
|
11
|
-
/** The session's first stored arm, or null; never writes.
|
|
12
|
-
export function readPilotArm(db, sessionId) {
|
|
13
|
+
/** The session's first stored arm, or null; never writes. Without `tenantId` any tenant's row counts, since on one machine a session has one arm. */
|
|
14
|
+
export function readPilotArm(db, sessionId, tenantId) {
|
|
15
|
+
const params = tenantId === undefined ? [sessionId] : [sessionId, tenantId];
|
|
13
16
|
// SAFETY: the SELECT names exactly this one column.
|
|
14
|
-
const row = db.prepare(`SELECT block_hash FROM token_ledger WHERE session_id =
|
|
17
|
+
const row = db.prepare(`SELECT block_hash FROM token_ledger WHERE session_id = ?${tenantId === undefined ? '' : ' AND tenant_id = ?'}
|
|
18
|
+
AND surface = 'pilot' AND event = 'arm' ORDER BY id LIMIT 1`).get(...params);
|
|
15
19
|
return row?.block_hash === 'holdout' || row?.block_hash === 'hippo' ? row.block_hash : null;
|
|
16
20
|
}
|
|
17
21
|
/** The stored arm, else the hash arm written once; on any error the hash arm comes back unrecorded. */
|
|
18
|
-
// The
|
|
19
|
-
export function ensurePilotArm(db, tenantId, sessionId, rateBp,
|
|
22
|
+
// The wait is the caller's lock-wait scope (a hook's 1 s, a server request's 250 ms); it holds on a handle opened in that scope.
|
|
23
|
+
export function ensurePilotArm(db, tenantId, sessionId, rateBp, opts = {}) {
|
|
20
24
|
const hashed = hashArm(sessionId, rateBp);
|
|
25
|
+
const readTenant = opts.ownTenantOnly ? tenantId : undefined;
|
|
21
26
|
let began = false;
|
|
22
27
|
try {
|
|
23
28
|
// A stored row is the common case after the first prompt, so it must not take the write lock.
|
|
24
|
-
const existing = readPilotArm(db, sessionId);
|
|
29
|
+
const existing = readPilotArm(db, sessionId, readTenant);
|
|
25
30
|
if (existing !== null)
|
|
26
31
|
return existing;
|
|
27
|
-
execWithBusyRetry(db, 'BEGIN IMMEDIATE', HOOK_DB_WAIT_MS);
|
|
32
|
+
execWithBusyRetry(db, 'BEGIN IMMEDIATE', scopedBusyWait() ?? HOOK_DB_WAIT_MS);
|
|
28
33
|
began = true;
|
|
29
|
-
const stored = readPilotArm(db, sessionId);
|
|
34
|
+
const stored = readPilotArm(db, sessionId, readTenant);
|
|
30
35
|
if (stored === null) {
|
|
31
|
-
recordTokenUse(db, { tenantId, sessionId, surface: 'pilot', event: 'arm', items: rateBp, tokens: 0, hash: hashed, now });
|
|
36
|
+
recordTokenUse(db, { tenantId, sessionId, surface: 'pilot', event: 'arm', items: rateBp, tokens: 0, hash: hashed, now: opts.now });
|
|
32
37
|
}
|
|
33
38
|
db.exec('COMMIT');
|
|
34
39
|
return stored ?? hashed;
|
|
@@ -44,4 +49,21 @@ export function ensurePilotArm(db, tenantId, sessionId, rateBp, now) {
|
|
|
44
49
|
return hashed;
|
|
45
50
|
}
|
|
46
51
|
}
|
|
52
|
+
/** The session's arm, or null at rate 0, with no session id, or with no store.
|
|
53
|
+
* `write` books the arm row; a read-only caller (env-only id, sub-agent) follows the stored arm, else the hash. */
|
|
54
|
+
export function sessionPilotArm(hippoRoot, tenantId, sessionId, write, opts = {}) {
|
|
55
|
+
if (sessionId === undefined || sessionId.trim() === '')
|
|
56
|
+
return null;
|
|
57
|
+
const root = ledgerRoot(hippoRoot, opts);
|
|
58
|
+
if (root === null)
|
|
59
|
+
return null;
|
|
60
|
+
const rate = loadConfig(root).pilot.holdoutRateBp;
|
|
61
|
+
if (rate <= 0)
|
|
62
|
+
return null;
|
|
63
|
+
const { ownTenantOnly } = opts;
|
|
64
|
+
const arm = withLedgerDb(hippoRoot, (db) => write
|
|
65
|
+
? ensurePilotArm(db, tenantId, sessionId, rate, { ownTenantOnly })
|
|
66
|
+
: readPilotArm(db, sessionId, ownTenantOnly ? tenantId : undefined) ?? hashArm(sessionId, rate), opts);
|
|
67
|
+
return arm ?? hashArm(sessionId, rate);
|
|
68
|
+
}
|
|
47
69
|
//# sourceMappingURL=pilot-arm.js.map
|
package/dist/postinstall.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { detectRealCodexPath, isCodexWrapperInstalled, repairCodexWrapperIfInstalled } from './hooks/codex-wrapper.js';
|
|
6
|
+
import { claudeConfigDir } from './hooks/shared.js';
|
|
6
7
|
function main() {
|
|
7
8
|
if (envSkipPostinstall())
|
|
8
9
|
return;
|
|
@@ -39,8 +40,7 @@ function main() {
|
|
|
39
40
|
* middle ground.
|
|
40
41
|
*/
|
|
41
42
|
function printClaudeCodeNudge() {
|
|
42
|
-
const
|
|
43
|
-
const claudeDir = path.join(home, '.claude');
|
|
43
|
+
const claudeDir = claudeConfigDir(os.homedir());
|
|
44
44
|
if (!fs.existsSync(claudeDir))
|
|
45
45
|
return; // Claude Code not installed — silent
|
|
46
46
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
package/dist/project-briefs.js
CHANGED
|
@@ -24,7 +24,7 @@ import { BadRequestError, ConflictError, NotFoundError } from './api-errors.js';
|
|
|
24
24
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
25
25
|
import { writeEntry } from './store/entry-writes.js';
|
|
26
26
|
import { assertTenantId } from './tenant.js';
|
|
27
|
-
import {
|
|
27
|
+
import { scopeAdmitSql } from './recall-scope.js';
|
|
28
28
|
import { markGraphDirty, removeGraphEntitiesForObject } from './graph/write.js';
|
|
29
29
|
import { createMemory, Layer } from './memory.js';
|
|
30
30
|
import { appendAuditEvent } from './audit.js';
|
|
@@ -388,7 +388,7 @@ function receiptHeadline(content) {
|
|
|
388
388
|
function loadBriefReceipts(hippoRoot, tenantId, normalizedRepo) {
|
|
389
389
|
const tag = `path:${normalizedRepo.toLowerCase()}`;
|
|
390
390
|
const likeParam = `%"${escapeLike(tag)}"%`;
|
|
391
|
-
const
|
|
391
|
+
const deny = scopeAdmitSql('');
|
|
392
392
|
const db = openHippoDb(hippoRoot);
|
|
393
393
|
try {
|
|
394
394
|
// SAFETY: SELECT projects exactly id, created, source, content (the
|
|
@@ -398,10 +398,10 @@ function loadBriefReceipts(hippoRoot, tenantId, normalizedRepo) {
|
|
|
398
398
|
WHERE tenant_id = ?
|
|
399
399
|
AND source != 'project_brief'
|
|
400
400
|
AND LOWER(tags_json) LIKE ? ESCAPE '\\'
|
|
401
|
-
AND
|
|
401
|
+
AND ${deny.sql}
|
|
402
402
|
ORDER BY created DESC, id DESC
|
|
403
403
|
LIMIT ?
|
|
404
|
-
`).all(tenantId, likeParam, ...
|
|
404
|
+
`).all(tenantId, likeParam, ...deny.params, MAX_BRIEF_RECEIPTS);
|
|
405
405
|
}
|
|
406
406
|
finally {
|
|
407
407
|
closeHippoDb(db);
|
|
@@ -60,15 +60,22 @@ export type ProjectRef = string | Pick<ProjectIdentity, 'name' | 'legacyName' |
|
|
|
60
60
|
export declare function projectId(project: ProjectRef): string;
|
|
61
61
|
/** Every origin_project value the reader's own rows carry: the id first, then each alias and the legacy name. */
|
|
62
62
|
export declare function projectNames(project: ProjectRef): readonly string[];
|
|
63
|
+
export declare const MAX_PROJECT_ALIASES = 10;
|
|
64
|
+
export declare const MCP_PROJECT_SCOPED_HEADER = "X-Hippo-Project-Scoped";
|
|
65
|
+
/** Refuses a caller's project that is blank (it would stamp user-global), past the caps, or unlike the resolver's ids: rows match verbatim, so a rewrite would split a project. Inner spaces pass, since a checkout with no remote is named by its folder. */
|
|
66
|
+
export declare function assertCallerProject(project: {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
readonly aliases?: readonly string[];
|
|
69
|
+
}): void;
|
|
63
70
|
/** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
|
|
64
71
|
export declare function originInSql(names: readonly string[], column?: string): string;
|
|
65
72
|
/**
|
|
66
73
|
* v39 memory scope isolation: classify a memory's origin_project against the
|
|
67
74
|
* active project. An empty current id means the session is not in a project
|
|
68
75
|
* (home dir or markerless cwd) - everything is in scope there, matching
|
|
69
|
-
* pre-isolation behavior. NULL/undefined origin
|
|
70
|
-
*
|
|
71
|
-
* security partition.
|
|
76
|
+
* pre-isolation behavior. NULL/undefined origin means no known project (a legacy
|
|
77
|
+
* row, or a shared-store write that named none) and is treated as cross-project
|
|
78
|
+
* (deny by default) - the safe direction for a security partition.
|
|
72
79
|
*/
|
|
73
80
|
export declare function classifyOriginProject(origin: string | null | undefined, current: ProjectRef): 'project' | 'user-global' | 'cross-project';
|
|
74
81
|
/**
|
|
@@ -94,12 +101,9 @@ export declare function isGlobalStoreRoot(p: string): boolean;
|
|
|
94
101
|
* origin evidence. Pure string logic - recorded paths may no longer exist.
|
|
95
102
|
*/
|
|
96
103
|
export declare function originFromSource(source: string | null | undefined, homeName?: string): string | null;
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
* Returns the project name, or '' for user-global (written at/under home or
|
|
100
|
-
* in a markerless directory) - injectable everywhere. Write sites must always
|
|
101
|
-
* persist this value; a NULL origin_project column is reserved for legacy
|
|
102
|
-
* pre-migration rows, which ambient context treats as deny.
|
|
103
|
-
*/
|
|
104
|
+
/** The origin to stamp on a memory written from cwd: its project name, or '' (user-global, injectable everywhere) outside a project.
|
|
105
|
+
* NULL means no known project: a legacy row with no evidence, or a write to a shared store that named none; ambient context treats it as deny. */
|
|
104
106
|
export declare function deriveOriginProject(cwd?: string, opts?: ResolveProjectIdentityOpts): string;
|
|
107
|
+
/** The origin for a write that names none: NULL on a shared store, whose folder is no caller's project, else the folder's project. */
|
|
108
|
+
export declare function fallbackOrigin(hippoRoot: string): string | null;
|
|
105
109
|
//# sourceMappingURL=project-identity.d.ts.map
|
package/dist/project-identity.js
CHANGED
|
@@ -2,7 +2,9 @@ import { envHippoHome, envXdgDataHome } from './env.js';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { BadRequestError } from './api-errors.js';
|
|
6
|
+
import { isSharedStore, loadConfig } from './config.js';
|
|
7
|
+
import { MAX_ID_LEN } from './http-util.js';
|
|
6
8
|
import { log } from './log.js';
|
|
7
9
|
import { originRemoteId, projectFileId } from './project-remote.js';
|
|
8
10
|
const MAX_WALK_DEPTH = 64;
|
|
@@ -179,6 +181,23 @@ export function projectNames(project) {
|
|
|
179
181
|
return [projectId(project)];
|
|
180
182
|
return [...new Set([project.name, ...(project.aliases ?? []), project.legacyName])].filter((n) => n !== '');
|
|
181
183
|
}
|
|
184
|
+
// Each project name is matched against every candidate row, so the caller's list stays short.
|
|
185
|
+
export const MAX_PROJECT_ALIASES = 10;
|
|
186
|
+
export const MCP_PROJECT_SCOPED_HEADER = 'X-Hippo-Project-Scoped';
|
|
187
|
+
/** Refuses a caller's project that is blank (it would stamp user-global), past the caps, or unlike the resolver's ids: rows match verbatim, so a rewrite would split a project. Inner spaces pass, since a checkout with no remote is named by its folder. */
|
|
188
|
+
export function assertCallerProject(project) {
|
|
189
|
+
const { name, aliases = [] } = project;
|
|
190
|
+
if (name.trim() === '')
|
|
191
|
+
throw new BadRequestError('project name must not be blank');
|
|
192
|
+
if (aliases.length > MAX_PROJECT_ALIASES)
|
|
193
|
+
throw new BadRequestError(`project aliases: at most ${MAX_PROJECT_ALIASES}`);
|
|
194
|
+
if ([name, ...aliases].some((n) => n.length > MAX_ID_LEN)) {
|
|
195
|
+
throw new BadRequestError(`project names: at most ${MAX_ID_LEN} characters each`);
|
|
196
|
+
}
|
|
197
|
+
if ([name, ...aliases].some((n) => n !== n.trim().toLowerCase() || /[:\p{Cc}]/u.test(n))) {
|
|
198
|
+
throw new BadRequestError('project names: lowercase, not padded, with no colon or control character');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
182
201
|
/** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
|
|
183
202
|
export function originInSql(names, column = 'origin_project') {
|
|
184
203
|
return names.length === 0 ? '0' : `${column} IN (${names.map(() => '?').join(', ')})`;
|
|
@@ -187,9 +206,9 @@ export function originInSql(names, column = 'origin_project') {
|
|
|
187
206
|
* v39 memory scope isolation: classify a memory's origin_project against the
|
|
188
207
|
* active project. An empty current id means the session is not in a project
|
|
189
208
|
* (home dir or markerless cwd) - everything is in scope there, matching
|
|
190
|
-
* pre-isolation behavior. NULL/undefined origin
|
|
191
|
-
*
|
|
192
|
-
* security partition.
|
|
209
|
+
* pre-isolation behavior. NULL/undefined origin means no known project (a legacy
|
|
210
|
+
* row, or a shared-store write that named none) and is treated as cross-project
|
|
211
|
+
* (deny by default) - the safe direction for a security partition.
|
|
193
212
|
*/
|
|
194
213
|
export function classifyOriginProject(origin, current) {
|
|
195
214
|
if (projectId(current) === '')
|
|
@@ -252,14 +271,13 @@ export function originFromSource(source, homeName) {
|
|
|
252
271
|
}
|
|
253
272
|
return null;
|
|
254
273
|
}
|
|
255
|
-
/**
|
|
256
|
-
*
|
|
257
|
-
* Returns the project name, or '' for user-global (written at/under home or
|
|
258
|
-
* in a markerless directory) - injectable everywhere. Write sites must always
|
|
259
|
-
* persist this value; a NULL origin_project column is reserved for legacy
|
|
260
|
-
* pre-migration rows, which ambient context treats as deny.
|
|
261
|
-
*/
|
|
274
|
+
/** The origin to stamp on a memory written from cwd: its project name, or '' (user-global, injectable everywhere) outside a project.
|
|
275
|
+
* NULL means no known project: a legacy row with no evidence, or a write to a shared store that named none; ambient context treats it as deny. */
|
|
262
276
|
export function deriveOriginProject(cwd, opts) {
|
|
263
277
|
return resolveProjectIdentity(cwd, opts).name;
|
|
264
278
|
}
|
|
279
|
+
/** The origin for a write that names none: NULL on a shared store, whose folder is no caller's project, else the folder's project. */
|
|
280
|
+
export function fallbackOrigin(hippoRoot) {
|
|
281
|
+
return isSharedStore(hippoRoot) ? null : deriveOriginProject(path.dirname(path.resolve(hippoRoot)));
|
|
282
|
+
}
|
|
265
283
|
//# sourceMappingURL=project-identity.js.map
|
package/dist/project-merge.d.ts
CHANGED
|
@@ -46,8 +46,6 @@ export interface RepairResult {
|
|
|
46
46
|
}
|
|
47
47
|
/** Live rows per project name, newest write first, with how many of its imported notes are copies held elsewhere. */
|
|
48
48
|
export declare function listProjects(db: DatabaseSyncLike, tenantId: string): ProjectSummary[];
|
|
49
|
-
/** Copies the database before a repair writes, so the audit ids plus this file are the way back. */
|
|
50
|
-
export declare function backupStore(db: DatabaseSyncLike, hippoRoot: string, label: string, now?: Date): string;
|
|
51
49
|
/** Refuses user-global and unknown: they are not projects, and folding them would leak or hide every row. */
|
|
52
50
|
export declare function validateMergeNames(from: string, into: string): string | null;
|
|
53
51
|
/** Folds project `from` into `into` for one tenant in one transaction; a dry run rolls back and writes nothing, mirrors included. */
|
package/dist/project-merge.js
CHANGED
|
@@ -6,6 +6,8 @@ import { transcriptNotesProject } from './agent-memories/claude-code.js';
|
|
|
6
6
|
import { containerId, containerPrefix } from './agent-memories/source.js';
|
|
7
7
|
import { AGENT_MEMORY_SOURCE_PREFIX, AGENT_MEMORY_TOOLS, toolSourcePrefix } from './agent-memories/tools.js';
|
|
8
8
|
import { appendAuditEvent, queryAuditEvents } from './audit.js';
|
|
9
|
+
import { isSharedStore } from './config.js';
|
|
10
|
+
import { withBackup } from './db/backup.js';
|
|
9
11
|
import { getMeta, setMeta } from './db/meta.js';
|
|
10
12
|
import { insertDormantRow, listDormantSnapshots, replaceDormantEntry } from './dormant.js';
|
|
11
13
|
import { processEnv } from './env.js';
|
|
@@ -47,14 +49,6 @@ export function listProjects(db, tenantId) {
|
|
|
47
49
|
};
|
|
48
50
|
}).sort((a, b) => b.newest.localeCompare(a.newest));
|
|
49
51
|
}
|
|
50
|
-
/** Copies the database before a repair writes, so the audit ids plus this file are the way back. */
|
|
51
|
-
export function backupStore(db, hippoRoot, label, now = new Date()) {
|
|
52
|
-
const dir = path.join(hippoRoot, 'backups');
|
|
53
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
-
const file = path.join(dir, `hippo-${label}-${now.toISOString().replace(/[:.]/g, '-')}.db`);
|
|
55
|
-
db.prepare('VACUUM INTO ?').run(file);
|
|
56
|
-
return file;
|
|
57
|
-
}
|
|
58
52
|
function inTransaction(db, dryRun, body) {
|
|
59
53
|
db.exec(dryRun ? 'BEGIN' : 'BEGIN IMMEDIATE');
|
|
60
54
|
try {
|
|
@@ -84,12 +78,12 @@ export function mergeProjects(db, hippoRoot, opts) {
|
|
|
84
78
|
if (refusal)
|
|
85
79
|
throw new Error(refusal);
|
|
86
80
|
const { tenantId, from, into, dryRun } = opts;
|
|
87
|
-
const
|
|
88
|
-
const result = inTransaction(db, dryRun, () => {
|
|
81
|
+
const fold = (backup) => inTransaction(db, dryRun, () => {
|
|
89
82
|
const folded = foldInTx(db, tenantId, from, into);
|
|
90
83
|
appendAuditEvent(db, { tenantId, actor: ACTOR, op: 'project_merge', metadata: { from, into, backup, ...folded } });
|
|
91
84
|
return { from, into, ...folded, backup };
|
|
92
85
|
});
|
|
86
|
+
const result = dryRun ? fold(null) : withBackup(db, hippoRoot, 'before-merge', fold);
|
|
93
87
|
if (!dryRun)
|
|
94
88
|
refreshMirrors(db, hippoRoot, tenantId, result.restamped, result.setAside);
|
|
95
89
|
return result;
|
|
@@ -203,6 +197,9 @@ function planFolds(db, hippoRoot, tenantId, globalFolds) {
|
|
|
203
197
|
}
|
|
204
198
|
/** A project store's rows, dormant snapshots and compaction records written before its id existed carry its folder name. */
|
|
205
199
|
function ownLegacyFold(db, hippoRoot, tenantId) {
|
|
200
|
+
// A shared store's rows carry their callers' names, so one matching its folder's is a member's project, not a legacy tag.
|
|
201
|
+
if (isSharedStore(hippoRoot))
|
|
202
|
+
return [];
|
|
206
203
|
const { name, legacyName } = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot)));
|
|
207
204
|
if (legacyName === '' || legacyName === name)
|
|
208
205
|
return [];
|
|
@@ -278,8 +275,7 @@ export function repairProjects(db, hippoRoot, opts) {
|
|
|
278
275
|
const { tenantId, dryRun, globalFolds = true } = opts;
|
|
279
276
|
if (dryRun)
|
|
280
277
|
return { ...planProjectRepair(db, hippoRoot, tenantId, globalFolds), backup: null };
|
|
281
|
-
const
|
|
282
|
-
const { result, rewrite, purge } = inTransaction(db, false, () => {
|
|
278
|
+
const { result, rewrite, purge } = withBackup(db, hippoRoot, 'before-repair', (backup) => inTransaction(db, false, () => {
|
|
283
279
|
const copies = [];
|
|
284
280
|
for (const row of strayImports(db, hippoRoot, tenantId)) {
|
|
285
281
|
const tag = toolTag(row.source);
|
|
@@ -304,7 +300,7 @@ export function repairProjects(db, hippoRoot, opts) {
|
|
|
304
300
|
rewrite: [...plan.toProject.map((r) => r.id), ...folded.flatMap((f) => f.restamped)],
|
|
305
301
|
purge: [...copies, ...plan.setAside, ...folded.flatMap((f) => f.setAside)],
|
|
306
302
|
};
|
|
307
|
-
});
|
|
303
|
+
}));
|
|
308
304
|
refreshMirrors(db, hippoRoot, tenantId, rewrite, purge);
|
|
309
305
|
return result;
|
|
310
306
|
}
|