hippo-memory 1.61.0 → 1.63.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 +37 -53
- package/dist/agent-memories/apply.d.ts +3 -1
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +4 -1
- package/dist/agent-memories/claude-code.js +53 -9
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.d.ts +3 -3
- package/dist/agent-memories/sync.js +46 -24
- package/dist/agent-memories/types.d.ts +0 -2
- package/dist/ambient-store.d.ts +4 -4
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +7 -10
- package/dist/api/assemble.js +62 -66
- package/dist/api/audit.d.ts +2 -2
- package/dist/api/audit.js +2 -2
- package/dist/api/auth.d.ts +6 -6
- package/dist/api/auth.js +5 -7
- package/dist/api/context-select.d.ts +50 -0
- package/dist/api/context-select.js +342 -0
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +5 -4
- package/dist/api/context.js +208 -535
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +56 -41
- package/dist/api/outcome.d.ts +8 -13
- package/dist/api/outcome.js +13 -17
- package/dist/api/promote.d.ts +5 -8
- package/dist/api/promote.js +55 -66
- package/dist/api/quarantine.js +3 -2
- package/dist/api/recall-types.d.ts +43 -55
- package/dist/api/recall.d.ts +3 -3
- package/dist/api/recall.js +310 -456
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +206 -220
- package/dist/api/tokens.d.ts +2 -2
- package/dist/api/tokens.js +2 -2
- package/dist/api/types.d.ts +6 -10
- package/dist/api/types.js +2 -4
- package/dist/audit-prune.d.ts +4 -6
- package/dist/audit-prune.js +3 -5
- package/dist/audit.js +11 -33
- package/dist/auth.d.ts +8 -9
- package/dist/auth.js +5 -7
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +1 -1
- package/dist/availability.js +3 -5
- package/dist/capture/command.d.ts +5 -4
- package/dist/capture/command.js +8 -22
- package/dist/capture/compact.d.ts +2 -2
- package/dist/capture/compact.js +11 -15
- package/dist/capture/extract.js +37 -115
- package/dist/capture-error.d.ts +1 -1
- package/dist/capture-error.js +1 -1
- package/dist/churn-git.d.ts +1 -1
- package/dist/churn-git.js +1 -1
- package/dist/cli/audit.js +3 -4
- package/dist/cli/auth.js +3 -6
- package/dist/cli/briefs.js +324 -306
- package/dist/cli/context.js +44 -34
- package/dist/cli/continuity.js +283 -271
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +43 -58
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +334 -345
- package/dist/cli/explain.js +68 -61
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/maintenance.js +62 -51
- package/dist/cli/playbooks.js +391 -379
- package/dist/cli/projects.js +11 -6
- package/dist/cli/recall.js +30 -44
- package/dist/cli/remember.js +118 -87
- package/dist/cli/session-hooks.js +106 -115
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +267 -250
- package/dist/cli/shared.js +3 -3
- package/dist/cli/slack.js +1 -1
- package/dist/cli/sleep.js +27 -1
- package/dist/cli/status.d.ts +4 -4
- package/dist/cli/status.js +80 -76
- package/dist/cli/transfer.js +88 -107
- package/dist/cli/usage.js +9 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -2
- package/dist/compaction-record.js +89 -68
- package/dist/compare.d.ts +11 -16
- package/dist/compare.js +11 -16
- package/dist/config.d.ts +19 -18
- package/dist/config.js +90 -67
- package/dist/connectors/github/backfill.d.ts +2 -2
- package/dist/connectors/github/backfill.js +8 -15
- package/dist/connectors/github/cli-impl.js +3 -8
- package/dist/connectors/github/deletion.d.ts +5 -12
- package/dist/connectors/github/deletion.js +5 -12
- package/dist/connectors/github/dlq.d.ts +6 -9
- package/dist/connectors/github/dlq.js +2 -3
- package/dist/connectors/github/ingest.d.ts +5 -7
- package/dist/connectors/github/ingest.js +8 -12
- package/dist/connectors/github/octokit-client.d.ts +3 -5
- package/dist/connectors/github/octokit-client.js +5 -6
- package/dist/connectors/github/signature.d.ts +9 -39
- package/dist/connectors/github/signature.js +9 -39
- package/dist/connectors/github/tenant-routing.d.ts +1 -1
- package/dist/connectors/github/tenant-routing.js +1 -1
- package/dist/connectors/github/transform.js +2 -2
- package/dist/connectors/github/types.d.ts +2 -10
- package/dist/connectors/github/types.js +1 -3
- package/dist/connectors/slack/deletion.d.ts +3 -8
- package/dist/connectors/slack/deletion.js +3 -8
- package/dist/connectors/slack/dlq.d.ts +1 -1
- package/dist/connectors/slack/ingest.d.ts +1 -1
- package/dist/connectors/slack/ingest.js +7 -16
- package/dist/connectors/slack/signature.d.ts +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +3 -5
- package/dist/connectors/slack/tenant-routing.js +3 -5
- package/dist/connectors/slack/transform.d.ts +5 -6
- package/dist/connectors/slack/transform.js +5 -6
- package/dist/connectors/slack/types.d.ts +2 -6
- package/dist/connectors/slack/types.js +1 -3
- package/dist/connectors/slack/web-client.js +10 -3
- package/dist/connectors/slack/workspaces.d.ts +3 -5
- package/dist/connectors/slack/workspaces.js +3 -5
- package/dist/consolidate/conflicts.js +3 -14
- package/dist/consolidate/decay.js +9 -29
- package/dist/consolidate/llm-passes.js +4 -5
- package/dist/consolidate/merge.js +8 -23
- package/dist/consolidate/run.d.ts +1 -8
- package/dist/consolidate/run.js +3 -25
- package/dist/consolidate/sleep.js +5 -17
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +82 -76
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +189 -203
- package/dist/db/continuity.js +2 -2
- package/dist/db/migrations/v14.js +1 -1
- package/dist/db/migrations/v15.js +1 -2
- package/dist/db/migrations/v16.js +3 -4
- package/dist/db/migrations/v17.js +2 -3
- package/dist/db/migrations/v19.js +1 -1
- package/dist/db/migrations/v20.js +1 -1
- package/dist/db/migrations/v21.js +2 -6
- package/dist/db/migrations/v22.js +2 -4
- package/dist/db/migrations/v23.js +1 -1
- package/dist/db/migrations/v24.js +4 -6
- package/dist/db/migrations/v25.js +2 -3
- package/dist/db/migrations/v26.js +3 -3
- package/dist/db/migrations/v27.js +2 -10
- package/dist/db/migrations/v28.js +5 -8
- package/dist/db/migrations/v29.js +3 -4
- package/dist/db/migrations/v30.js +2 -2
- package/dist/db/migrations/v31.js +1 -1
- package/dist/db/migrations/v32.js +1 -1
- package/dist/db/migrations/v33.js +3 -3
- package/dist/db/migrations/v34.js +1 -1
- package/dist/db/migrations/v35.js +3 -4
- package/dist/db/migrations/v36.js +3 -4
- package/dist/db/migrations/v37.js +5 -5
- package/dist/db/migrations/v38.js +7 -8
- package/dist/db/migrations/v39.js +1 -1
- package/dist/db/migrations/v40.js +4 -16
- package/dist/db/migrations/v41.js +3 -4
- package/dist/db/migrations/v42.js +3 -4
- package/dist/db/migrations/v45.js +1 -1
- package/dist/db/migrations/v46.js +1 -1
- package/dist/db/migrations/v47.js +1 -1
- package/dist/db/migrations/v48.js +1 -1
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +97 -80
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +129 -110
- package/dist/dormant.js +1 -4
- package/dist/embedding-provider.d.ts +4 -8
- package/dist/embedding-provider.js +4 -8
- package/dist/embeddings.js +55 -47
- package/dist/env.d.ts +1 -1
- package/dist/env.js +12 -12
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +10 -0
- package/dist/eval-stats.d.ts +1 -2
- package/dist/eval-stats.js +1 -2
- package/dist/eval-suite.js +27 -21
- package/dist/extract.js +4 -9
- package/dist/failure-log.d.ts +3 -3
- package/dist/failure-log.js +1 -1
- package/dist/forward-claim-detector.d.ts +2 -4
- package/dist/forward-claim-detector.js +6 -11
- package/dist/goals.d.ts +3 -3
- package/dist/goals.js +103 -91
- package/dist/graph/read.d.ts +2 -2
- package/dist/graph/read.js +5 -6
- package/dist/graph/types.d.ts +8 -8
- package/dist/graph/write.d.ts +7 -14
- package/dist/graph/write.js +16 -23
- package/dist/graph-extract.d.ts +7 -8
- package/dist/graph-extract.js +62 -72
- package/dist/graph-recall.d.ts +2 -2
- package/dist/graph-recall.js +55 -49
- package/dist/graph-stream.d.ts +5 -6
- package/dist/graph-stream.js +66 -57
- package/dist/graph-view.d.ts +2 -2
- package/dist/graph-view.js +7 -7
- package/dist/half-life-migration.d.ts +1 -2
- package/dist/half-life-migration.js +2 -3
- package/dist/hooks/codex-session.js +1 -1
- package/dist/hooks/codex-wrapper.d.ts +1 -1
- package/dist/hooks/codex-wrapper.js +3 -2
- package/dist/hooks/json-hooks.d.ts +2 -2
- package/dist/hooks/json-hooks.js +5 -4
- package/dist/hooks/opencode.d.ts +1 -1
- package/dist/hooks/opencode.js +5 -4
- package/dist/hooks/shared.d.ts +3 -7
- package/dist/hooks/shared.js +1 -8
- package/dist/http-util.d.ts +2 -3
- package/dist/http-util.js +3 -0
- package/dist/importers/core.d.ts +2 -9
- package/dist/importers/core.js +15 -30
- package/dist/importers/sources.js +2 -1
- package/dist/importers/vault.js +2 -20
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +46 -39
- package/dist/instruction-detect.d.ts +1 -1
- package/dist/instruction-detect.js +1 -1
- package/dist/invalidation.d.ts +3 -0
- package/dist/invalidation.js +160 -114
- package/dist/json.d.ts +5 -0
- package/dist/json.js +4 -0
- package/dist/judgment.js +1 -2
- package/dist/local-embedding.js +1 -1
- package/dist/mcp/admin-tools.js +7 -17
- package/dist/mcp/format.js +1 -1
- package/dist/mcp/framing.js +3 -6
- package/dist/mcp/protocol.d.ts +2 -5
- package/dist/mcp/protocol.js +1 -3
- package/dist/mcp/recall-tools.js +12 -15
- package/dist/mcp/request.js +4 -3
- package/dist/mcp/session-state.js +2 -3
- package/dist/mcp/stdio.js +2 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/memory-value-weights.d.ts +5 -8
- package/dist/memory-value-weights.js +5 -8
- package/dist/memory-value.d.ts +13 -13
- package/dist/memory-value.js +26 -37
- package/dist/memory.d.ts +20 -22
- package/dist/memory.js +24 -48
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +3 -2
- package/dist/owner-validation.d.ts +4 -5
- package/dist/owner-validation.js +4 -5
- package/dist/physics.d.ts +4 -4
- package/dist/physics.js +7 -9
- package/dist/policies.d.ts +9 -10
- package/dist/policies.js +96 -81
- package/dist/postinstall.js +3 -6
- package/dist/predictions/planning-fallacy.d.ts +9 -14
- package/dist/predictions/planning-fallacy.js +10 -16
- package/dist/predictions/store.d.ts +15 -23
- package/dist/predictions/store.js +36 -33
- package/dist/processes.d.ts +2 -7
- package/dist/processes.js +88 -72
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +141 -118
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +28 -5
- package/dist/project-merge.js +213 -46
- package/dist/project-remote.d.ts +12 -0
- package/dist/project-remote.js +138 -0
- package/dist/prompt-recall.js +1 -2
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.js +1 -1
- package/dist/raw-archive.d.ts +9 -0
- package/dist/raw-archive.js +70 -53
- package/dist/recall-history.d.ts +19 -20
- package/dist/recall-history.js +24 -42
- package/dist/recall-pipeline.js +4 -28
- package/dist/recall-scope.d.ts +7 -8
- package/dist/recall-scope.js +7 -8
- package/dist/recall-trace.d.ts +5 -9
- package/dist/recall-trace.js +6 -10
- package/dist/refine-llm.d.ts +1 -1
- package/dist/refine-llm.js +2 -2
- package/dist/reject-flow.d.ts +3 -4
- package/dist/reject-flow.js +122 -117
- package/dist/rejection.d.ts +5 -6
- package/dist/rejection.js +7 -15
- package/dist/rerankers/clef.d.ts +1 -1
- package/dist/rerankers/jev.d.ts +1 -2
- package/dist/rerankers/jev.js +4 -5
- package/dist/rerankers/llm.d.ts +1 -2
- package/dist/rerankers/llm.js +1 -2
- package/dist/rerankers/types.d.ts +1 -2
- package/dist/rrf.d.ts +2 -2
- package/dist/rrf.js +2 -2
- package/dist/search/bm25-search.d.ts +1 -1
- package/dist/search/bm25-search.js +2 -1
- package/dist/search/boosts.js +2 -1
- package/dist/search/hybrid.d.ts +1 -1
- package/dist/search/hybrid.js +2 -1
- package/dist/search/physics-search.d.ts +1 -1
- package/dist/search/physics-search.js +2 -1
- package/dist/search/types.d.ts +2 -0
- package/dist/search/types.js +3 -1
- package/dist/secret-detect.d.ts +4 -5
- package/dist/secret-detect.js +6 -10
- package/dist/server/auth.js +5 -5
- package/dist/server/client-ip.js +1 -1
- package/dist/server/cursor.js +2 -1
- package/dist/server/mcp-http.js +4 -4
- package/dist/server/request.d.ts +3 -6
- package/dist/server/request.js +6 -7
- package/dist/server/routes/admin.js +5 -4
- package/dist/server/routes/customer-notes.js +6 -5
- package/dist/server/routes/decisions.js +4 -3
- package/dist/server/routes/incidents.js +7 -5
- package/dist/server/routes/memories.js +7 -7
- package/dist/server/routes/policies.js +3 -2
- package/dist/server/routes/predictions.js +12 -15
- package/dist/server/routes/processes.js +3 -2
- package/dist/server/routes/project-briefs.js +8 -7
- package/dist/server/routes/recall.js +95 -93
- package/dist/server/routes/skills.js +6 -5
- package/dist/server/types.d.ts +1 -1
- package/dist/server/validation.d.ts +1 -2
- package/dist/server/validation.js +7 -14
- package/dist/server-detect.js +72 -58
- package/dist/server.d.ts +2 -2
- package/dist/server.js +131 -117
- package/dist/shared.d.ts +26 -17
- package/dist/shared.js +102 -104
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +88 -72
- package/dist/store/audit-event.d.ts +2 -2
- package/dist/store/audit-event.js +1 -1
- package/dist/store/candidates.d.ts +2 -2
- package/dist/store/candidates.js +4 -3
- package/dist/store/conflicts.js +30 -22
- package/dist/store/delete-and-batch.d.ts +11 -14
- package/dist/store/delete-and-batch.js +40 -91
- package/dist/store/entry-reads.d.ts +17 -25
- package/dist/store/entry-reads.js +59 -40
- package/dist/store/entry-row.d.ts +6 -24
- package/dist/store/entry-row.js +6 -24
- package/dist/store/entry-writes.d.ts +6 -7
- package/dist/store/entry-writes.js +13 -11
- package/dist/store/handoffs.d.ts +1 -1
- package/dist/store/handoffs.js +7 -10
- package/dist/store/index-and-stats.d.ts +2 -6
- package/dist/store/index-and-stats.js +4 -10
- package/dist/store/mirrors.d.ts +6 -19
- package/dist/store/mirrors.js +14 -39
- package/dist/store/open.js +9 -31
- package/dist/store/rows.d.ts +5 -11
- package/dist/store/rows.js +6 -11
- package/dist/store/search-rows.d.ts +17 -34
- package/dist/store/search-rows.js +34 -56
- package/dist/store/sessions.d.ts +4 -5
- package/dist/store/sessions.js +5 -6
- package/dist/store/summaries.d.ts +13 -17
- package/dist/store/summaries.js +26 -70
- package/dist/support-bundle.js +4 -8
- package/dist/tenant.d.ts +1 -5
- package/dist/token-ledger.d.ts +1 -1
- package/dist/token-ledger.js +3 -5
- package/dist/trace.js +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +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/project-merge.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
// `hippo projects`: list the project names a store holds, fold one into another, and
|
|
1
|
+
// `hippo projects`: list the project names a store holds, fold one into another, and repair old tags in one reversible pass.
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { setAsideRow } from './agent-memories/apply.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { transcriptNotesProject } from './agent-memories/claude-code.js';
|
|
6
|
+
import { containerId, containerPrefix } from './agent-memories/source.js';
|
|
7
|
+
import { AGENT_MEMORY_SOURCE_PREFIX, AGENT_MEMORY_TOOLS, toolSourcePrefix } from './agent-memories/tools.js';
|
|
8
|
+
import { appendAuditEvent, queryAuditEvents } from './audit.js';
|
|
9
|
+
import { getMeta, setMeta } from './db/meta.js';
|
|
7
10
|
import { insertDormantRow, listDormantSnapshots, replaceDormantEntry } from './dormant.js';
|
|
11
|
+
import { processEnv } from './env.js';
|
|
8
12
|
import { calculateStrength } from './memory.js';
|
|
13
|
+
import { isObjectLike, isStringValue } from './capture-contract.js';
|
|
14
|
+
import { isGlobalStoreRoot, projectNames, resolveProjectIdentity } from './project-identity.js';
|
|
15
|
+
import { duplicateKey } from './same-text.js';
|
|
9
16
|
import { removeEntryMirrors } from './store/mirrors.js';
|
|
10
17
|
import { deleteEntryRowInTx, writeEntryMirrors } from './store/entry-writes.js';
|
|
11
|
-
import { selectAllEntries } from './store/entry-reads.js';
|
|
18
|
+
import { selectAllEntries, selectLiveEntriesBySourcePrefix } from './store/entry-reads.js';
|
|
12
19
|
const ACTOR = 'cli';
|
|
13
20
|
function isImport(entry) {
|
|
14
21
|
return entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX);
|
|
@@ -25,8 +32,9 @@ export function listProjects(db, tenantId) {
|
|
|
25
32
|
for (const e of live) {
|
|
26
33
|
const origin = e.origin_project ?? null;
|
|
27
34
|
byOrigin.set(origin, [...(byOrigin.get(origin) ?? []), e]);
|
|
35
|
+
const key = duplicateKey(e.content);
|
|
28
36
|
if (isImport(e))
|
|
29
|
-
holders.set(
|
|
37
|
+
holders.set(key, (holders.get(key) ?? new Set()).add(origin));
|
|
30
38
|
}
|
|
31
39
|
return [...byOrigin].map(([origin, rows]) => {
|
|
32
40
|
const imports = rows.filter(isImport);
|
|
@@ -35,7 +43,7 @@ export function listProjects(db, tenantId) {
|
|
|
35
43
|
live: rows.length,
|
|
36
44
|
imported: imports.length,
|
|
37
45
|
newest: rows.reduce((max, e) => (e.created > max ? e.created : max), ''),
|
|
38
|
-
copiesElsewhere: imports.filter((e) => (holders.get(e.content)?.size ?? 0) > 1).length,
|
|
46
|
+
copiesElsewhere: imports.filter((e) => (holders.get(duplicateKey(e.content))?.size ?? 0) > 1).length,
|
|
39
47
|
};
|
|
40
48
|
}).sort((a, b) => b.newest.localeCompare(a.newest));
|
|
41
49
|
}
|
|
@@ -78,44 +86,174 @@ export function mergeProjects(db, hippoRoot, opts) {
|
|
|
78
86
|
const { tenantId, from, into, dryRun } = opts;
|
|
79
87
|
const backup = dryRun ? null : backupStore(db, hippoRoot, 'before-merge');
|
|
80
88
|
const result = inTransaction(db, dryRun, () => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const tag = toolTag(row.source);
|
|
85
|
-
if (!isImport(row) || row.superseded_by || row.kind === 'raw' || tag === null)
|
|
86
|
-
continue;
|
|
87
|
-
if (setAsideRow(db, tag, { ...row, origin_project: into }, 'project-merge').kind === 'dormant')
|
|
88
|
-
setAside.push(row.id);
|
|
89
|
-
}
|
|
90
|
-
const gone = new Set(setAside);
|
|
91
|
-
const restamped = rows.filter((e) => !gone.has(e.id)).map((e) => e.id);
|
|
92
|
-
db.prepare(`UPDATE memories SET origin_project = ?, updated_at = datetime('now') WHERE tenant_id = ? AND origin_project = ?`)
|
|
93
|
-
.run(into, tenantId, from);
|
|
94
|
-
const dormantRestamped = [];
|
|
95
|
-
for (const snap of listDormantSnapshots(db, tenantId)) {
|
|
96
|
-
if (snap.entry.origin_project !== from || gone.has(snap.entry.id))
|
|
97
|
-
continue;
|
|
98
|
-
replaceDormantEntry(db, tenantId, snap.entry.id, { ...snap.entry, origin_project: into });
|
|
99
|
-
dormantRestamped.push(snap.entry.id);
|
|
100
|
-
}
|
|
101
|
-
const compactions = Number(db.prepare(`UPDATE compactions SET origin_project = ? WHERE tenant_id = ? AND origin_project = ?`)
|
|
102
|
-
.run(into, tenantId, from).changes ?? 0);
|
|
103
|
-
appendAuditEvent(db, {
|
|
104
|
-
tenantId, actor: ACTOR, op: 'project_merge',
|
|
105
|
-
metadata: { from, into, backup, setAside, restamped, dormantRestamped, compactions },
|
|
106
|
-
});
|
|
107
|
-
return { from, into, setAside, restamped, dormantRestamped, compactions, backup };
|
|
89
|
+
const folded = foldInTx(db, tenantId, from, into);
|
|
90
|
+
appendAuditEvent(db, { tenantId, actor: ACTOR, op: 'project_merge', metadata: { from, into, backup, ...folded } });
|
|
91
|
+
return { from, into, ...folded, backup };
|
|
108
92
|
});
|
|
109
93
|
if (!dryRun)
|
|
110
94
|
refreshMirrors(db, hippoRoot, tenantId, result.restamped, result.setAside);
|
|
111
95
|
return result;
|
|
112
96
|
}
|
|
113
|
-
|
|
114
|
-
|
|
97
|
+
function foldInTx(db, tenantId, from, into) {
|
|
98
|
+
const all = selectAllEntries(db, tenantId);
|
|
99
|
+
const rows = all.filter((e) => e.origin_project === from);
|
|
100
|
+
// The sync renames the rest to `into`'s prefix and keeps their ids; only a note `into` already holds would show twice.
|
|
101
|
+
const held = new Set(all.filter((e) => e.origin_project === into && !e.superseded_by && isImport(e)).map(noteKey));
|
|
102
|
+
const setAside = [];
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
const tag = toolTag(row.source);
|
|
105
|
+
if (!isImport(row) || row.superseded_by || row.kind === 'raw' || tag === null || !held.has(noteKey(row)))
|
|
106
|
+
continue;
|
|
107
|
+
if (setAsideRow(db, tag, { ...row, origin_project: into }, 'project-merge').kind === 'dormant')
|
|
108
|
+
setAside.push(row.id);
|
|
109
|
+
}
|
|
110
|
+
const gone = new Set(setAside);
|
|
111
|
+
const restamped = rows.filter((e) => !gone.has(e.id)).map((e) => e.id);
|
|
112
|
+
db.prepare(`UPDATE memories SET origin_project = ?, updated_at = datetime('now') WHERE tenant_id = ? AND origin_project = ?`)
|
|
113
|
+
.run(into, tenantId, from);
|
|
114
|
+
const dormantRestamped = [];
|
|
115
|
+
for (const snap of listDormantSnapshots(db, tenantId)) {
|
|
116
|
+
if (snap.entry.origin_project !== from || gone.has(snap.entry.id))
|
|
117
|
+
continue;
|
|
118
|
+
replaceDormantEntry(db, tenantId, snap.entry.id, { ...snap.entry, origin_project: into });
|
|
119
|
+
dormantRestamped.push(snap.entry.id);
|
|
120
|
+
}
|
|
121
|
+
const compactions = Number(db.prepare(`UPDATE compactions SET origin_project = ? WHERE tenant_id = ? AND origin_project = ?`)
|
|
122
|
+
.run(into, tenantId, from).changes ?? 0);
|
|
123
|
+
return { setAside, restamped, dormantRestamped, compactions };
|
|
124
|
+
}
|
|
125
|
+
/** Tool, note key and hash of an import, without the container, whose id hashes the project name. */
|
|
126
|
+
function noteKey(entry) {
|
|
127
|
+
const rest = entry.source.slice(AGENT_MEMORY_SOURCE_PREFIX.length);
|
|
128
|
+
return `${rest.split(':')[0]}:${rest.slice(rest.indexOf('/') + 1)}`;
|
|
129
|
+
}
|
|
130
|
+
/** Live imports under a project name whose exact text a user-global import holds: a session folder's notes stamped with the folder it ended in. */
|
|
131
|
+
function importCopies(db, tenantId) {
|
|
132
|
+
const live = selectAllEntries(db, tenantId).filter((e) => !e.superseded_by && isImport(e));
|
|
133
|
+
const userGlobal = new Set(live.filter((e) => e.origin_project === '').map((e) => duplicateKey(e.content)));
|
|
134
|
+
return live.filter((e) => e.origin_project && e.kind !== 'raw' && !e.pinned && toolTag(e.source) !== null && userGlobal.has(duplicateKey(e.content)));
|
|
135
|
+
}
|
|
136
|
+
/** Imports to set aside. The global store also checks each Claude session folder a compaction recorded: its notes under any other project are misfiled, edited or not, and a text copy in the right folder is kept. */
|
|
137
|
+
function strayImports(db, hippoRoot, tenantId) {
|
|
138
|
+
const copies = importCopies(db, tenantId);
|
|
139
|
+
if (!isGlobalStoreRoot(hippoRoot))
|
|
140
|
+
return copies;
|
|
141
|
+
const machine = { platform: process.platform, env: processEnv() };
|
|
142
|
+
const { platform } = machine;
|
|
143
|
+
// SAFETY: the SELECT names the two columns of the row type.
|
|
144
|
+
const sessions = db.prepare(`SELECT DISTINCT transcript_path AS transcript, cwd FROM compactions WHERE tenant_id = ? AND transcript_path IS NOT NULL`)
|
|
145
|
+
.all(tenantId);
|
|
146
|
+
const owners = new Map();
|
|
147
|
+
for (const { transcript, cwd } of sessions) {
|
|
148
|
+
const project = transcriptNotesProject(transcript, cwd, machine);
|
|
149
|
+
const dir = path.join(path.dirname(transcript), 'memory');
|
|
150
|
+
if (project === null)
|
|
151
|
+
continue;
|
|
152
|
+
const ids = owners.get(dir) ?? new Map();
|
|
153
|
+
owners.set(dir, ids.set(project.name, [...new Set([...(ids.get(project.name) ?? []), ...projectNames(project)])]));
|
|
154
|
+
}
|
|
155
|
+
const tool = toolSourcePrefix('claude-code');
|
|
156
|
+
const live = selectLiveEntriesBySourcePrefix(db, tenantId, tool).filter((e) => e.kind !== 'raw' && !e.pinned);
|
|
157
|
+
const origins = new Set(live.map((e) => e.origin_project ?? ''));
|
|
158
|
+
const edges = foldEdges(db, tenantId);
|
|
159
|
+
const right = new Set();
|
|
160
|
+
const wrong = new Set();
|
|
161
|
+
for (const [dir, ids] of owners) {
|
|
162
|
+
if (ids.size !== 1)
|
|
163
|
+
continue;
|
|
164
|
+
// Its names before the id, and names folded into it, are the owner's too: the next sync moves those prefixes under the id.
|
|
165
|
+
const [names] = ids.values();
|
|
166
|
+
const own = new Set([...names, ...foldedInto(edges, names)]);
|
|
167
|
+
for (const origin of new Set([...origins, ...own]))
|
|
168
|
+
(own.has(origin) ? right : wrong).add(containerPrefix('claude-code', containerId(dir, 'project', platform, origin)));
|
|
169
|
+
}
|
|
170
|
+
const prefix = (e) => e.source.slice(0, e.source.indexOf('/', tool.length) + 1);
|
|
171
|
+
const misfiled = live.filter((e) => wrong.has(prefix(e)));
|
|
172
|
+
const seen = new Set(misfiled.map((e) => e.id));
|
|
173
|
+
return [...misfiled, ...copies.filter((e) => !seen.has(e.id) && !right.has(prefix(e)))];
|
|
174
|
+
}
|
|
175
|
+
/** The global store reads each name's recorded session folders, where a compaction's name came from its cwd; a project store folds only its own folder name. */
|
|
176
|
+
function planFolds(db, hippoRoot, tenantId, globalFolds) {
|
|
177
|
+
// A name someone merged into by hand stays: undoing their choice would rest on the resolver alone.
|
|
178
|
+
const chosen = new Set(queryAuditEvents(db, { tenantId, op: 'project_merge', limit: 10000 }).map((e) => e.metadata.into));
|
|
179
|
+
if (!isGlobalStoreRoot(hippoRoot))
|
|
180
|
+
return { folds: ownLegacyFold(db, hippoRoot, tenantId).filter((f) => !chosen.has(f.from)), collisions: [] };
|
|
181
|
+
// SAFETY: the SELECT names the two columns of the row type.
|
|
182
|
+
const rows = db.prepare(`SELECT DISTINCT origin_project AS origin, cwd FROM compactions WHERE tenant_id = ? AND origin_project <> '' AND cwd IS NOT NULL`)
|
|
183
|
+
.all(tenantId);
|
|
184
|
+
const today = new Map();
|
|
185
|
+
for (const { origin, cwd } of rows) {
|
|
186
|
+
if (fs.existsSync(cwd))
|
|
187
|
+
today.set(origin, (today.get(origin) ?? new Set()).add(resolveProjectIdentity(cwd).name));
|
|
188
|
+
}
|
|
189
|
+
const folds = [];
|
|
190
|
+
const collisions = [];
|
|
191
|
+
for (const [from, names] of today) {
|
|
192
|
+
const [into] = names;
|
|
193
|
+
if (chosen.has(from))
|
|
194
|
+
continue;
|
|
195
|
+
if (names.size > 1)
|
|
196
|
+
collisions.push({ name: from, ids: [...names].sort() });
|
|
197
|
+
else if (into !== '' && into !== from)
|
|
198
|
+
folds.push({ from, into });
|
|
199
|
+
}
|
|
200
|
+
// A fold into a name that itself folds would land rows by run order; the next repair takes the rest of the chain.
|
|
201
|
+
const sources = new Set(folds.map((f) => f.from));
|
|
202
|
+
return { folds: globalFolds ? folds.filter((f) => !sources.has(f.into)) : [], collisions };
|
|
203
|
+
}
|
|
204
|
+
/** A project store's rows, dormant snapshots and compaction records written before its id existed carry its folder name. */
|
|
205
|
+
function ownLegacyFold(db, hippoRoot, tenantId) {
|
|
206
|
+
const { name, legacyName } = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot)));
|
|
207
|
+
if (legacyName === '' || legacyName === name)
|
|
208
|
+
return [];
|
|
209
|
+
const heldIn = (table) => db.prepare(`SELECT 1 FROM ${table} WHERE tenant_id = ? AND origin_project = ? LIMIT 1`).get(tenantId, legacyName) !== undefined;
|
|
210
|
+
const held = heldIn('memories') || heldIn('compactions') || listDormantSnapshots(db, tenantId).some((s) => s.entry.origin_project === legacyName);
|
|
211
|
+
return held ? [{ from: legacyName, into: name }] : [];
|
|
212
|
+
}
|
|
213
|
+
function foldOf(value) {
|
|
214
|
+
if (!isObjectLike(value) || Array.isArray(value))
|
|
215
|
+
return [];
|
|
216
|
+
const { from, into } = value;
|
|
217
|
+
return isStringValue(from) && isStringValue(into) ? [{ from, into }] : [];
|
|
218
|
+
}
|
|
219
|
+
function foldEdges(db, tenantId) {
|
|
220
|
+
return [
|
|
221
|
+
...queryAuditEvents(db, { tenantId, op: 'project_merge', limit: 10000 }).flatMap((e) => foldOf(e.metadata)),
|
|
222
|
+
...queryAuditEvents(db, { tenantId, op: 'project_repair', limit: 10000 })
|
|
223
|
+
.flatMap((e) => (Array.isArray(e.metadata.folds) ? e.metadata.folds.flatMap(foldOf) : [])),
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
function foldedInto(edges, names) {
|
|
227
|
+
const seen = new Set(names);
|
|
228
|
+
let grew = true;
|
|
229
|
+
while (grew) {
|
|
230
|
+
grew = false;
|
|
231
|
+
for (const { from, into } of edges) {
|
|
232
|
+
if (!seen.has(into) || seen.has(from))
|
|
233
|
+
continue;
|
|
234
|
+
seen.add(from);
|
|
235
|
+
grew = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return [...seen].filter((n) => !names.includes(n));
|
|
239
|
+
}
|
|
240
|
+
/** Names folded, directly or through others, into one of `names`; the sync moves imports still filed under them. */
|
|
241
|
+
export function namesFoldedInto(db, tenantId, names) {
|
|
242
|
+
return foldedInto(foldEdges(db, tenantId), names);
|
|
243
|
+
}
|
|
244
|
+
/** Reads only, so doctor and a dry run take no write lock; merged rows are planned before any fold, so a few may re-tag differently once folds apply. */
|
|
245
|
+
export function planProjectRepair(db, hippoRoot, tenantId, globalFolds = true) {
|
|
246
|
+
const { folds, collisions } = planFolds(db, hippoRoot, tenantId, globalFolds);
|
|
247
|
+
return { copies: strayImports(db, hippoRoot, tenantId).map((e) => e.id), folds, collisions, ...planUserGlobalRepair(db, tenantId, folds) };
|
|
248
|
+
}
|
|
249
|
+
/** Parents read with `folds` already applied, so a plan matches what apply does after folding. */
|
|
250
|
+
function planUserGlobalRepair(db, tenantId, folds) {
|
|
115
251
|
const all = selectAllEntries(db, tenantId);
|
|
116
|
-
const
|
|
252
|
+
const renamed = new Map(folds.map((f) => [f.from, f.into]));
|
|
253
|
+
const after = (origin) => (origin ? renamed.get(origin) ?? origin : origin ?? null);
|
|
254
|
+
const origins = new Map(listDormantSnapshots(db, tenantId).map((s) => [s.entry.id, after(s.entry.origin_project)]));
|
|
117
255
|
for (const e of all)
|
|
118
|
-
origins.set(e.id, e.origin_project
|
|
256
|
+
origins.set(e.id, after(e.origin_project));
|
|
119
257
|
const toProject = [];
|
|
120
258
|
const setAside = [];
|
|
121
259
|
const untraced = [];
|
|
@@ -135,14 +273,22 @@ export function planUserGlobalRepair(db, tenantId) {
|
|
|
135
273
|
}
|
|
136
274
|
return { toProject, setAside, untraced };
|
|
137
275
|
}
|
|
138
|
-
/**
|
|
139
|
-
export function
|
|
140
|
-
const { tenantId, dryRun } = opts;
|
|
276
|
+
/** Sets aside stray imports, folds the names the resolver now maps elsewhere, then re-tags sleep's user-global merges by their parents; a dry run only plans. */
|
|
277
|
+
export function repairProjects(db, hippoRoot, opts) {
|
|
278
|
+
const { tenantId, dryRun, globalFolds = true } = opts;
|
|
141
279
|
if (dryRun)
|
|
142
|
-
return { ...
|
|
280
|
+
return { ...planProjectRepair(db, hippoRoot, tenantId, globalFolds), backup: null };
|
|
143
281
|
const backup = backupStore(db, hippoRoot, 'before-repair');
|
|
144
|
-
const result = inTransaction(db, false, () => {
|
|
145
|
-
const
|
|
282
|
+
const { result, rewrite, purge } = inTransaction(db, false, () => {
|
|
283
|
+
const copies = [];
|
|
284
|
+
for (const row of strayImports(db, hippoRoot, tenantId)) {
|
|
285
|
+
const tag = toolTag(row.source);
|
|
286
|
+
if (tag !== null && setAsideRow(db, tag, row, 'project-repair').kind === 'dormant')
|
|
287
|
+
copies.push(row.id);
|
|
288
|
+
}
|
|
289
|
+
const { folds, collisions } = planFolds(db, hippoRoot, tenantId, globalFolds);
|
|
290
|
+
const folded = folds.map((f) => foldInTx(db, tenantId, f.from, f.into));
|
|
291
|
+
const plan = planUserGlobalRepair(db, tenantId, []);
|
|
146
292
|
const stamp = db.prepare(`UPDATE memories SET origin_project = ?, updated_at = datetime('now') WHERE tenant_id = ? AND id = ?`);
|
|
147
293
|
for (const { id, origin } of plan.toProject)
|
|
148
294
|
stamp.run(origin, tenantId, id);
|
|
@@ -152,10 +298,31 @@ export function repairUserGlobalMerges(db, hippoRoot, opts) {
|
|
|
152
298
|
insertDormantRow(db, { entry: row, strength: calculateStrength(row, now), reason: 'project-repair', dormantAt: now.toISOString() });
|
|
153
299
|
deleteEntryRowInTx(db, row, ACTOR);
|
|
154
300
|
}
|
|
155
|
-
appendAuditEvent(db, { tenantId, actor: ACTOR, op: 'project_repair', metadata: { backup, ...plan } });
|
|
156
|
-
return {
|
|
301
|
+
appendAuditEvent(db, { tenantId, actor: ACTOR, op: 'project_repair', metadata: { backup, copies, folds, folded, ...plan } });
|
|
302
|
+
return {
|
|
303
|
+
result: { copies, folds, collisions, ...plan, backup },
|
|
304
|
+
rewrite: [...plan.toProject.map((r) => r.id), ...folded.flatMap((f) => f.restamped)],
|
|
305
|
+
purge: [...copies, ...plan.setAside, ...folded.flatMap((f) => f.setAside)],
|
|
306
|
+
};
|
|
157
307
|
});
|
|
158
|
-
refreshMirrors(db, hippoRoot, tenantId,
|
|
308
|
+
refreshMirrors(db, hippoRoot, tenantId, rewrite, purge);
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
const AUTO_REPAIR_META_KEY = 'project_repair_auto';
|
|
312
|
+
/** True when a repair would change nothing. */
|
|
313
|
+
function repairIsEmpty(r) {
|
|
314
|
+
return r.copies.length + r.folds.length + r.toProject.length + r.setAside.length === 0;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Sleep runs repair once per store, so an upgrade needs no command. Global-store folds stay
|
|
318
|
+
* with `hippo projects repair`: their evidence is compaction folders, blind to a same-named repo that never compacted.
|
|
319
|
+
*/
|
|
320
|
+
export function repairOnceOnSleep(db, hippoRoot, tenantId) {
|
|
321
|
+
if (getMeta(db, AUTO_REPAIR_META_KEY) === '1')
|
|
322
|
+
return null;
|
|
323
|
+
const plan = planProjectRepair(db, hippoRoot, tenantId, false);
|
|
324
|
+
const result = repairIsEmpty(plan) ? null : repairProjects(db, hippoRoot, { tenantId, dryRun: false, globalFolds: false });
|
|
325
|
+
setMeta(db, AUTO_REPAIR_META_KEY, '1');
|
|
159
326
|
return result;
|
|
160
327
|
}
|
|
161
328
|
/** After commit, as the agent memory sync does: a stale mirror would bring the old tag back on the next rebuild. */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The config file git reads for a checkout: `.git/config`, a linked worktree's `<commondir>/config`, a submodule's `<gitdir>/config`. */
|
|
2
|
+
export declare function gitConfigPath(gitRoot: string): string | null;
|
|
3
|
+
/** `remote.origin.url` from a config file's text, the first value as git fetch uses; the subsection name is case-sensitive. */
|
|
4
|
+
export declare function originUrlFromConfig(text: string): string | null;
|
|
5
|
+
/** `host/path` for a network remote, so ssh, scp and https forms agree; null for a local path, which would put a user path into rows. */
|
|
6
|
+
export declare function normaliseRemote(url: string): string | null;
|
|
7
|
+
/** The normalised origin of the checkout rooted at gitRoot, or null when it has none a project can be named by. */
|
|
8
|
+
export declare function originRemoteId(gitRoot: string): string | null;
|
|
9
|
+
export declare const PROJECT_FILE = ".hippo-project.json";
|
|
10
|
+
/** The `id` a team committed in `.hippo-project.json` at root, lowercased like every project name; null when absent or unusable. */
|
|
11
|
+
export declare function projectFileId(root: string): string | null;
|
|
12
|
+
//# sourceMappingURL=project-remote.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// A checkout's `origin` remote, read from its git config file and normalised into a project id; no git process, so the prompt hook can call it.
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { isObjectLike, isStringValue } from './capture-contract.js';
|
|
5
|
+
import { log } from './log.js';
|
|
6
|
+
/** The config file git reads for a checkout: `.git/config`, a linked worktree's `<commondir>/config`, a submodule's `<gitdir>/config`. */
|
|
7
|
+
export function gitConfigPath(gitRoot) {
|
|
8
|
+
const marker = path.join(gitRoot, '.git');
|
|
9
|
+
try {
|
|
10
|
+
if (fs.statSync(marker).isDirectory())
|
|
11
|
+
return path.join(marker, 'config');
|
|
12
|
+
const gitDir = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(marker, 'utf8'))?.[1]?.trim();
|
|
13
|
+
if (!gitDir)
|
|
14
|
+
return null;
|
|
15
|
+
const linkDir = path.resolve(gitRoot, gitDir);
|
|
16
|
+
const commondir = path.join(linkDir, 'commondir');
|
|
17
|
+
if (!fs.existsSync(commondir))
|
|
18
|
+
return path.join(linkDir, 'config');
|
|
19
|
+
return path.join(path.resolve(linkDir, fs.readFileSync(commondir, 'utf8').trim()), 'config');
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
log.debug(`project remote: no git config for ${gitRoot}: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** One config value with git's quoting: `"` toggles quoting, backslash escapes, and an unquoted `#` or `;` starts a comment. */
|
|
27
|
+
function configValue(raw) {
|
|
28
|
+
let out = '';
|
|
29
|
+
let quoted = false;
|
|
30
|
+
for (let i = 0; i < raw.length; i++) {
|
|
31
|
+
const c = raw[i];
|
|
32
|
+
if (c === '\\' && i + 1 < raw.length) {
|
|
33
|
+
const next = raw[++i];
|
|
34
|
+
out += next === 'n' ? '\n' : next === 't' ? '\t' : next === 'b' ? '' : next;
|
|
35
|
+
}
|
|
36
|
+
else if (c === '"') {
|
|
37
|
+
quoted = !quoted;
|
|
38
|
+
}
|
|
39
|
+
else if (!quoted && (c === '#' || c === ';')) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
out += c;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out.trim();
|
|
47
|
+
}
|
|
48
|
+
const SECTION = /^\[\s*([A-Za-z0-9.-]+)(?:\s+"((?:[^"\\]|\\.)*)")?\s*\](.*)$/;
|
|
49
|
+
const ENTRY = /^([A-Za-z][A-Za-z0-9-]*)\s*(?:=(.*))?$/;
|
|
50
|
+
/** `remote.origin.url` from a config file's text, the first value as git fetch uses; the subsection name is case-sensitive. */
|
|
51
|
+
export function originUrlFromConfig(text) {
|
|
52
|
+
let inOrigin = false;
|
|
53
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
54
|
+
let line = raw.trim();
|
|
55
|
+
const section = SECTION.exec(line);
|
|
56
|
+
if (section) {
|
|
57
|
+
const [, name, sub, rest] = section;
|
|
58
|
+
inOrigin = sub === undefined
|
|
59
|
+
? name.toLowerCase() === 'remote.origin'
|
|
60
|
+
: name.toLowerCase() === 'remote' && sub.replace(/\\(.)/g, '$1') === 'origin';
|
|
61
|
+
line = rest.trim();
|
|
62
|
+
}
|
|
63
|
+
const entry = inOrigin ? ENTRY.exec(line) : null;
|
|
64
|
+
if (entry && entry[1].toLowerCase() === 'url' && entry[2] !== undefined) {
|
|
65
|
+
const value = configValue(entry[2]);
|
|
66
|
+
if (value !== '')
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/** Azure DevOps names one repo three ways; all become `dev.azure.com/org/project/repo`. */
|
|
73
|
+
function azureDevOps(hostPath) {
|
|
74
|
+
const ssh = /^(?:ssh\.dev\.azure\.com|vs-ssh\.visualstudio\.com)\/v3\/(.+)$/.exec(hostPath);
|
|
75
|
+
if (ssh)
|
|
76
|
+
return `dev.azure.com/${ssh[1]}`;
|
|
77
|
+
const legacy = /^([^/.]+)\.visualstudio\.com\/(?:defaultcollection\/)?(.+)$/.exec(hostPath);
|
|
78
|
+
const https = legacy ? `dev.azure.com/${legacy[1]}/${legacy[2]}` : hostPath;
|
|
79
|
+
return https.startsWith('dev.azure.com/') ? https.replace('/_git/', '/') : https;
|
|
80
|
+
}
|
|
81
|
+
/** `host/path` for a network remote, so ssh, scp and https forms agree; null for a local path, which would put a user path into rows. */
|
|
82
|
+
export function normaliseRemote(url) {
|
|
83
|
+
// A query or fragment can carry a token (`?private_token=`) and never names the repo.
|
|
84
|
+
const u = url.trim().replace(/[?#].*$/, '');
|
|
85
|
+
if (u === '' || u.includes('::') || /^file:/i.test(u) || /^[A-Za-z]:[\\/]/.test(u) || /^[./\\~]/.test(u))
|
|
86
|
+
return null;
|
|
87
|
+
let hostPath;
|
|
88
|
+
const withScheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/]*)(.*)$/.exec(u);
|
|
89
|
+
if (withScheme) {
|
|
90
|
+
// Userinfo can hold a token, and a port differs between ssh and https.
|
|
91
|
+
hostPath = withScheme[1].replace(/^.*@/, '').replace(/:\d*$/, '') + withScheme[2];
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):(.*)$/.exec(u);
|
|
95
|
+
if (!scp)
|
|
96
|
+
return null;
|
|
97
|
+
hostPath = `${scp[1]}/${scp[2]}`;
|
|
98
|
+
}
|
|
99
|
+
const tidy = hostPath.toLowerCase().replace(/\\/g, '/').replace(/\/{2,}/g, '/').replace(/\/+$/, '').replace(/\.git$/, '').replace(/\/+$/, '');
|
|
100
|
+
const id = azureDevOps(tidy);
|
|
101
|
+
// A colon would break the `shared:<project>:` source format; whitespace means this was no URL; an `@` left in a path may be a credential.
|
|
102
|
+
return /^[^/]+\/./.test(id) && !/[:\s@]/.test(id) ? id : null;
|
|
103
|
+
}
|
|
104
|
+
/** The normalised origin of the checkout rooted at gitRoot, or null when it has none a project can be named by. */
|
|
105
|
+
export function originRemoteId(gitRoot) {
|
|
106
|
+
const file = gitConfigPath(gitRoot);
|
|
107
|
+
if (file === null)
|
|
108
|
+
return null;
|
|
109
|
+
try {
|
|
110
|
+
const url = originUrlFromConfig(fs.readFileSync(file, 'utf8'));
|
|
111
|
+
return url === null ? null : normaliseRemote(url);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
log.debug(`project remote: ${file} not read: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export const PROJECT_FILE = '.hippo-project.json';
|
|
119
|
+
/** The `id` a team committed in `.hippo-project.json` at root, lowercased like every project name; null when absent or unusable. */
|
|
120
|
+
export function projectFileId(root) {
|
|
121
|
+
const file = path.join(root, PROJECT_FILE);
|
|
122
|
+
if (!fs.existsSync(file))
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
126
|
+
const raw = isObjectLike(parsed) && 'id' in parsed ? parsed.id : undefined;
|
|
127
|
+
const id = isStringValue(raw) ? raw.trim().toLowerCase() : '';
|
|
128
|
+
// No slash: a committed file in an untrusted clone must not claim a remote-shaped id like `github.com/acme/api`.
|
|
129
|
+
if (id !== '' && id.length <= 200 && !/[:\s/\\]/.test(id))
|
|
130
|
+
return id;
|
|
131
|
+
log.warn(`${file}: "id" must be a non-empty string with no colon, slash or spaces; using the remote or folder name instead`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
log.warn(`${file} not read: ${err instanceof Error ? err.message : String(err)}; using the remote or folder name instead`);
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=project-remote.js.map
|
package/dist/prompt-recall.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* See docs/plans/2026-09-26-z1-prompt-recall.md. */
|
|
1
|
+
/** Recall gated on the hook prompt, not the five newest memories (pure, no I/O). */
|
|
3
2
|
import { tokenize } from './tokenize.js';
|
|
4
3
|
import { STOP_WORDS } from './audit.js';
|
|
5
4
|
// Latency bound, not tuned: fixed in the prereg regardless of gate config.
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-key token-bucket rate limiter for inbound /v1/* requests.
|
|
3
3
|
*
|
|
4
|
-
* Bounds api-key-id enumeration
|
|
4
|
+
* Bounds api-key-id enumeration: a
|
|
5
5
|
* client that drains its bucket is denied until it refills. Dependency-free
|
|
6
6
|
* and unit-testable in isolation via the injectable `now`.
|
|
7
7
|
*/
|
package/dist/rate-limit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-key token-bucket rate limiter for inbound /v1/* requests.
|
|
3
3
|
*
|
|
4
|
-
* Bounds api-key-id enumeration
|
|
4
|
+
* Bounds api-key-id enumeration: a
|
|
5
5
|
* client that drains its bucket is denied until it refills. Dependency-free
|
|
6
6
|
* and unit-testable in isolation via the injectable `now`.
|
|
7
7
|
*/
|
package/dist/raw-archive.d.ts
CHANGED
|
@@ -11,5 +11,14 @@ export interface ArchiveOpts {
|
|
|
11
11
|
*/
|
|
12
12
|
afterArchive?: (db: DatabaseSyncLike, archivedMemoryId: string) => void;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The only legitimate path to remove a `kind='raw'` row from `memories`.
|
|
16
|
+
*
|
|
17
|
+
* Snapshots the full row into `raw_archive`, flips `kind` to `'archived'` so the
|
|
18
|
+
* append-only trigger lets the delete through, then deletes the row. All in one
|
|
19
|
+
* SAVEPOINT so it can be nested inside an outer transaction (e.g. batchWriteAndDelete).
|
|
20
|
+
*
|
|
21
|
+
* Throws if the row does not exist or is not `kind='raw'`.
|
|
22
|
+
*/
|
|
14
23
|
export declare function archiveRawMemory(db: DatabaseSyncLike, id: string, opts: ArchiveOpts): void;
|
|
15
24
|
//# sourceMappingURL=raw-archive.d.ts.map
|
package/dist/raw-archive.js
CHANGED
|
@@ -2,7 +2,7 @@ import { BadRequestError, NotFoundError } from './api-errors.js';
|
|
|
2
2
|
import { isFtsAvailable } from './db.js';
|
|
3
3
|
import { appendAuditEvent, reportAuditWriteFailure } from './audit.js';
|
|
4
4
|
import { markSummaryDirtyInTx } from './summary-dirty.js';
|
|
5
|
-
|
|
5
|
+
function loadRawRow(db, id) {
|
|
6
6
|
// SAFETY: SELECT * FROM memories returns every column of the memories table; only
|
|
7
7
|
// kind, tenant_id, and dag_parent_id are read below, all guaranteed present (possibly
|
|
8
8
|
// null) by the memories schema.
|
|
@@ -12,65 +12,82 @@ export function archiveRawMemory(db, id, opts) {
|
|
|
12
12
|
if (row.kind !== 'raw') {
|
|
13
13
|
throw new BadRequestError(`memory ${id} is not raw (kind=${String(row.kind)})`);
|
|
14
14
|
}
|
|
15
|
+
return row;
|
|
16
|
+
}
|
|
17
|
+
function moveRowToArchive(db, id, row, opts) {
|
|
18
|
+
// GDPR: raw_archive stores ONLY metadata, not the original
|
|
19
|
+
// memory content. The audit_log row appended below carries op='archive_raw'
|
|
20
|
+
// for the compliance audit trail. True right-to-be-forgotten — the original
|
|
21
|
+
// content is unrecoverable from raw_archive after this point.
|
|
22
|
+
const archivedAt = new Date().toISOString();
|
|
23
|
+
const redactedPayload = JSON.stringify({
|
|
24
|
+
redacted: true,
|
|
25
|
+
archived_at: archivedAt,
|
|
26
|
+
tenant_id: row.tenant_id ?? 'default',
|
|
27
|
+
kind: row.kind,
|
|
28
|
+
reason: opts.reason,
|
|
29
|
+
});
|
|
30
|
+
db.prepare(`INSERT INTO raw_archive (memory_id, archived_at, reason, archived_by, payload_json) VALUES (?, ?, ?, ?, ?)`).run(id, archivedAt, opts.reason, opts.who, redactedPayload);
|
|
31
|
+
// Flip kind to 'archived' so the BEFORE DELETE trigger no longer fires, then delete.
|
|
32
|
+
db.prepare(`UPDATE memories SET kind = 'archived' WHERE id = ?`).run(id);
|
|
33
|
+
db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
|
|
34
|
+
// FTS5 is a virtual table — no FK CASCADE applies. Purge the FTS row so the
|
|
35
|
+
// archived content is not searchable after archive. Without this the original
|
|
36
|
+
// raw text remains in memories_fts until the next DB-open backfill, defeating
|
|
37
|
+
// GDPR right-to-be-forgotten.
|
|
38
|
+
if (isFtsAvailable(db)) {
|
|
39
|
+
try {
|
|
40
|
+
db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(id);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Best effort only. The DELETE on memories already succeeded; FTS will
|
|
44
|
+
// self-heal on next DB open via backfillFtsIndex.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Emit the archive_raw audit event inside the SAVEPOINT so the audit row is
|
|
49
|
+
// committed atomically with the row deletion. Use the row's own tenant_id
|
|
50
|
+
// (fetched above as part of SELECT *), not the env. Archives must be
|
|
51
|
+
// attributed to the tenant that owns the row, not whatever HIPPO_TENANT
|
|
52
|
+
// happens to be set to in the calling shell.
|
|
53
|
+
function auditArchive(db, id, row, opts) {
|
|
54
|
+
try {
|
|
55
|
+
appendAuditEvent(db, {
|
|
56
|
+
tenantId: String(row.tenant_id ?? 'default'),
|
|
57
|
+
actor: opts.who || 'cli',
|
|
58
|
+
op: 'archive_raw',
|
|
59
|
+
targetId: id,
|
|
60
|
+
metadata: { reason: opts.reason },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// The archive itself has already succeeded; an unwritable audit table must not undo it.
|
|
65
|
+
reportAuditWriteFailure('archive_raw', String(error), id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The only legitimate path to remove a `kind='raw'` row from `memories`.
|
|
70
|
+
*
|
|
71
|
+
* Snapshots the full row into `raw_archive`, flips `kind` to `'archived'` so the
|
|
72
|
+
* append-only trigger lets the delete through, then deletes the row. All in one
|
|
73
|
+
* SAVEPOINT so it can be nested inside an outer transaction (e.g. batchWriteAndDelete).
|
|
74
|
+
*
|
|
75
|
+
* Throws if the row does not exist or is not `kind='raw'`.
|
|
76
|
+
*/
|
|
77
|
+
export function archiveRawMemory(db, id, opts) {
|
|
78
|
+
const row = loadRawRow(db, id);
|
|
15
79
|
// SAVEPOINT (not BEGIN) so this works whether or not we're already inside a
|
|
16
80
|
// transaction. SQLite refuses BEGIN within a transaction; SAVEPOINT nests safely.
|
|
17
81
|
db.exec('SAVEPOINT archive_raw');
|
|
18
82
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const archivedAt = new Date().toISOString();
|
|
24
|
-
const redactedPayload = JSON.stringify({
|
|
25
|
-
redacted: true,
|
|
26
|
-
archived_at: archivedAt,
|
|
27
|
-
tenant_id: row.tenant_id ?? 'default',
|
|
28
|
-
kind: row.kind,
|
|
29
|
-
reason: opts.reason,
|
|
30
|
-
});
|
|
31
|
-
db.prepare(`INSERT INTO raw_archive (memory_id, archived_at, reason, archived_by, payload_json) VALUES (?, ?, ?, ?, ?)`).run(id, archivedAt, opts.reason, opts.who, redactedPayload);
|
|
32
|
-
// Flip kind to 'archived' so the BEFORE DELETE trigger no longer fires, then delete.
|
|
33
|
-
db.prepare(`UPDATE memories SET kind = 'archived' WHERE id = ?`).run(id);
|
|
34
|
-
db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
|
|
35
|
-
// FTS5 is a virtual table — no FK CASCADE applies. Purge the FTS row so the
|
|
36
|
-
// archived content is not searchable after archive. Without this the original
|
|
37
|
-
// raw text remains in memories_fts until the next DB-open backfill, defeating
|
|
38
|
-
// GDPR right-to-be-forgotten.
|
|
39
|
-
if (isFtsAvailable(db)) {
|
|
40
|
-
try {
|
|
41
|
-
db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(id);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Best effort only. The DELETE on memories already succeeded; FTS will
|
|
45
|
-
// self-heal on next DB open via backfillFtsIndex.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// A5 audit: emit archive_raw event inside the SAVEPOINT so the audit row is
|
|
49
|
-
// committed atomically with the row deletion. Use the row's own tenant_id
|
|
50
|
-
// (fetched above as part of SELECT *), not the env. Archives must be
|
|
51
|
-
// attributed to the tenant that owns the row, not whatever HIPPO_TENANT
|
|
52
|
-
// happens to be set to in the calling shell.
|
|
53
|
-
try {
|
|
54
|
-
appendAuditEvent(db, {
|
|
55
|
-
tenantId: String(row.tenant_id ?? 'default'),
|
|
56
|
-
actor: opts.who || 'cli',
|
|
57
|
-
op: 'archive_raw',
|
|
58
|
-
targetId: id,
|
|
59
|
-
metadata: { reason: opts.reason },
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
// The archive itself has already succeeded; an unwritable audit table must not undo it.
|
|
64
|
-
reportAuditWriteFailure('archive_raw', String(error), id);
|
|
65
|
-
}
|
|
66
|
-
// v0.30 / E2 — DAG live-coupling: archive of a child under a level-2
|
|
67
|
-
// summary marks parent dirty. Inside the SAVEPOINT so the dirty-mark
|
|
68
|
-
// commits atomically with the archive. row.dag_parent_id was fetched
|
|
69
|
-
// via SELECT * at L28 (schema v28 includes it).
|
|
83
|
+
moveRowToArchive(db, id, row, opts);
|
|
84
|
+
auditArchive(db, id, row, opts);
|
|
85
|
+
// Archiving a child under a level-2 summary marks the parent dirty, inside the
|
|
86
|
+
// SAVEPOINT so the dirty-mark commits atomically with the archive.
|
|
70
87
|
if (row.dag_parent_id) {
|
|
71
88
|
markSummaryDirtyInTx(db, String(row.dag_parent_id), String(row.tenant_id ?? 'default'), opts.who || 'cli');
|
|
72
89
|
}
|
|
73
|
-
// afterArchive hook
|
|
90
|
+
// afterArchive hook: connector-level idempotency markers
|
|
74
91
|
// (e.g. slack_event_log) must commit atomically with the archive itself.
|
|
75
92
|
// Throwing here rolls back the entire SAVEPOINT — both the archive and any
|
|
76
93
|
// hook side effects. The hook runs INSIDE the SAVEPOINT so its writes
|