hippo-memory 1.62.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 +9 -0
- package/dist/agent-memories/apply.d.ts +2 -0
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +2 -1
- package/dist/agent-memories/claude-code.js +4 -4
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.js +37 -23
- 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 +7 -12
- 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 +4 -3
- package/dist/api/context-select.js +9 -11
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +3 -2
- package/dist/api/context.js +20 -20
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +22 -10
- 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/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 +7 -18
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +3 -2
- 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 +1 -1
- package/dist/capture/compact.js +9 -13
- 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/curate.d.ts +1 -1
- package/dist/cli/curate.js +8 -24
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +10 -21
- package/dist/cli/explain.js +2 -1
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/playbooks.js +4 -9
- package/dist/cli/projects.js +3 -1
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +6 -18
- package/dist/cli/session-hooks.js +21 -40
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +2 -6
- 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 +7 -12
- package/dist/cli/transfer.js +8 -13
- 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 -0
- 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 +6 -9
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +23 -73
- 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 +6 -6
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +119 -104
- 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 +5 -6
- 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 +1 -1
- 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 +12 -14
- 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 +3 -3
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +9 -13
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +17 -2
- package/dist/project-merge.js +109 -27
- 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 +17 -17
- package/dist/shared.js +93 -97
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +8 -8
- 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 +31 -59
- 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-briefs.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (docs/plans/2026-05-30-e2-project-brief-object.md).
|
|
2
|
+
* Project_brief first-class object.
|
|
4
3
|
*
|
|
5
4
|
* A `project_brief` is the living, repo-scoped summary of a repository's state: a
|
|
6
5
|
* `summary` body scoped to a `repo`, evolving via the supersede delta lifecycle.
|
|
@@ -31,6 +30,7 @@ import { createMemory, Layer } from './memory.js';
|
|
|
31
30
|
import { appendAuditEvent } from './audit.js';
|
|
32
31
|
import { objectHalfLifeDays } from './half-life-migration.js';
|
|
33
32
|
import { keysetAfter } from './keyset.js';
|
|
33
|
+
import { escapeLike } from './escape.js';
|
|
34
34
|
export const VALID_BRIEF_STATES = new Set([
|
|
35
35
|
'active',
|
|
36
36
|
'superseded',
|
|
@@ -106,7 +106,7 @@ function buildBriefContent(repo, summary) {
|
|
|
106
106
|
// Preflight the supersede target BEFORE inserting the new row (so the new
|
|
107
107
|
// autoincrement id can never be its own supersede target); read the
|
|
108
108
|
// predecessor version in the same SELECT for server-derived versioning.
|
|
109
|
-
// Mirrors saveSkill / saveProcess
|
|
109
|
+
// Mirrors saveSkill / saveProcess.
|
|
110
110
|
function preflightBriefSupersede(db, tenantId, supersedesId) {
|
|
111
111
|
// SAFETY: SELECT projects exactly status, version; .get() returns that
|
|
112
112
|
// shape for the matching row, or undefined when no brief/tenant pair matches.
|
|
@@ -276,7 +276,7 @@ export function closeProjectBrief(hippoRoot, tenantId, id, actor = 'cli') {
|
|
|
276
276
|
// Closing removes the object from the graph. Remove its rows DIRECTLY (deterministic),
|
|
277
277
|
// not only via an enqueued rebuild whose queue item is lost if the mirror is later
|
|
278
278
|
// forgotten (the queue row cascade-deletes with the memory), which would leave the closed
|
|
279
|
-
// object stale and could block that forget
|
|
279
|
+
// object stale and could block that forget. Still enqueue when a mirror exists
|
|
280
280
|
// so a concurrent rebuild re-derives consistently (harmless if it also runs).
|
|
281
281
|
removeGraphEntitiesForObject(hippoRoot, tenantId, 'project', closed.id);
|
|
282
282
|
if (closed.memoryId) {
|
|
@@ -351,7 +351,7 @@ export function loadProjectBriefs(hippoRoot, tenantId, opts = {}) {
|
|
|
351
351
|
/**
|
|
352
352
|
* The repo's CURRENT active brief, or null. By convention there is one active brief
|
|
353
353
|
* per (tenant, repo); if an operator created more than one (the DB does not prevent
|
|
354
|
-
* it, consistent with every other
|
|
354
|
+
* it, consistent with every other first-class object), the MOST-RECENT active row wins.
|
|
355
355
|
*/
|
|
356
356
|
export function loadActiveBriefForRepo(hippoRoot, tenantId, repo) {
|
|
357
357
|
assertTenantId('loadActiveBriefForRepo', tenantId);
|
|
@@ -375,10 +375,6 @@ export function loadActiveBriefForRepo(hippoRoot, tenantId, repo) {
|
|
|
375
375
|
// ---------------------------------------------------------------------------
|
|
376
376
|
// Refresh assembler (the distinguishing deliverable)
|
|
377
377
|
// ---------------------------------------------------------------------------
|
|
378
|
-
/** Escape LIKE wildcards in operator-supplied text (mirror of store/search-rows.ts). */
|
|
379
|
-
function escapeLike(term) {
|
|
380
|
-
return term.replace(/[%_\\]/g, '\\$&');
|
|
381
|
-
}
|
|
382
378
|
/** Single-line headline for a receipt: first non-empty line, newline-stripped,
|
|
383
379
|
* truncated. Deterministic + safe for the markdown bullet list. */
|
|
384
380
|
function receiptHeadline(content) {
|
|
@@ -414,9 +410,9 @@ function loadBriefReceipts(hippoRoot, tenantId, normalizedRepo) {
|
|
|
414
410
|
// NOTE on ordering: the `id DESC` tiebreak is lexical on a random-ish memory id
|
|
415
411
|
// (e.g. `sem_<hex>`), NOT chronological — within the same `created` timestamp the
|
|
416
412
|
// order is stable-but-arbitrary, not insertion order. `created DESC` is the real
|
|
417
|
-
// recency ordering.
|
|
413
|
+
// recency ordering.
|
|
418
414
|
//
|
|
419
|
-
// Budget-aware assembly
|
|
415
|
+
// Budget-aware assembly: the digest is the
|
|
420
416
|
// brief `summary`, which saveProjectBrief caps at MAX_BRIEF_SUMMARY_LEN. The
|
|
421
417
|
// receipt/headline caps (50 x ~200) could otherwise build an ~11KB body that the
|
|
422
418
|
// store then REJECTS, breaking refresh for inputs within the advertised caps. So
|
|
@@ -518,8 +514,8 @@ export function refreshBrief(hippoRoot, tenantId, repo, actor = 'cli') {
|
|
|
518
514
|
supersedesBriefId: active ? active.id : undefined,
|
|
519
515
|
refreshReceiptCount: receiptCount,
|
|
520
516
|
// Tag the refreshed brief's mirror as repo-local so path-aware recall boosts
|
|
521
|
-
// it like the manual `brief new`/`supersede` paths do
|
|
522
|
-
//
|
|
517
|
+
// it like the manual `brief new`/`supersede` paths do.
|
|
518
|
+
// Safe vs self-recursion: assembleBriefFromReceipts excludes
|
|
523
519
|
// source='project_brief', so the brief never becomes its own receipt.
|
|
524
520
|
extraTags: [`path:${normalizedRepo.toLowerCase()}`],
|
|
525
521
|
}, actor);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project identity resolution for memory scope isolation
|
|
3
|
-
* [Committed] "Memory scope isolation"; plan docs/plans/2026-07-01-memory-scope-isolation.md S1).
|
|
2
|
+
* Project identity resolution for memory scope isolation.
|
|
4
3
|
*
|
|
5
4
|
* Resolution rules:
|
|
6
5
|
* - The nearest ancestor of cwd (including cwd itself) containing a `.hippo`
|
|
@@ -11,6 +10,9 @@
|
|
|
11
10
|
* - A directory with no marker anywhere up the walk is NOT a project: it
|
|
12
11
|
* resolves to the user-global identity (empty name), so memories written
|
|
13
12
|
* there stay injectable everywhere (matches pre-isolation behavior).
|
|
13
|
+
* - A project's name is its id: `.hippo-project.json` at the root, else the
|
|
14
|
+
* checkout's normalised `origin` remote, else the folder rule (legacyName),
|
|
15
|
+
* so two repos in folders of one name stay apart.
|
|
14
16
|
*
|
|
15
17
|
* NOTE: this module must stay free of imports from shared.ts / store.ts /
|
|
16
18
|
* api.ts so any of them can import it without creating a cycle.
|
|
@@ -19,8 +21,12 @@
|
|
|
19
21
|
export interface ProjectIdentity {
|
|
20
22
|
/** Realpath-resolved root directory of the project (the start dir when not in a project). */
|
|
21
23
|
root: string;
|
|
22
|
-
/**
|
|
24
|
+
/** The project id rows are stamped with: the project file's id, the origin remote, or legacyName; '' outside a project. */
|
|
23
25
|
name: string;
|
|
26
|
+
/** The folder rule rows written before ids carry: the root's lowercased basename, or its repo's for a linked worktree with no `.hippo`. */
|
|
27
|
+
legacyName: string;
|
|
28
|
+
/** Every other name this root resolves to (the project file id, the origin remote even with the rule off, legacyName), so rows written under an earlier id stay readable. */
|
|
29
|
+
aliases?: readonly string[];
|
|
24
30
|
/** True when the directory resolves to the user home working set. */
|
|
25
31
|
isHome: boolean;
|
|
26
32
|
}
|
|
@@ -33,7 +39,7 @@ export interface ResolveProjectIdentityOpts {
|
|
|
33
39
|
homeDir?: string;
|
|
34
40
|
stopDir?: string;
|
|
35
41
|
}
|
|
36
|
-
/** Clear the per-process identity cache (test seam). */
|
|
42
|
+
/** Clear the per-process identity cache and the remote switch it read (test seam). */
|
|
37
43
|
export declare function clearProjectIdentityCache(): void;
|
|
38
44
|
/**
|
|
39
45
|
* Canonicalize a path via realpath, falling back to path.resolve when the
|
|
@@ -46,17 +52,25 @@ export declare function realpathOrResolve(p: string): string;
|
|
|
46
52
|
*/
|
|
47
53
|
export declare function resolveProjectIdentity(cwd?: string, opts?: ResolveProjectIdentityOpts): ProjectIdentity;
|
|
48
54
|
/** Nearest ancestor `.hippo` below home and the temp root (never projects; on Windows the temp root sits inside home).
|
|
49
|
-
* Everything is realpath'd so a symlinked temp root or cwd still matches its bound.
|
|
55
|
+
* Everything is realpath'd so a symlinked temp root or cwd still matches its bound. */
|
|
50
56
|
export declare function findHippoStoreDir(cwd?: string, opts?: ResolveProjectIdentityOpts): string | null;
|
|
57
|
+
/** A reader's project: a bare name, or an identity whose own rows may also carry its legacy folder name. */
|
|
58
|
+
export type ProjectRef = string | Pick<ProjectIdentity, 'name' | 'legacyName' | 'aliases'>;
|
|
59
|
+
/** The id a reader's new rows are stamped with; '' outside a project. */
|
|
60
|
+
export declare function projectId(project: ProjectRef): string;
|
|
61
|
+
/** Every origin_project value the reader's own rows carry: the id first, then each alias and the legacy name. */
|
|
62
|
+
export declare function projectNames(project: ProjectRef): readonly string[];
|
|
63
|
+
/** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
|
|
64
|
+
export declare function originInSql(names: readonly string[], column?: string): string;
|
|
51
65
|
/**
|
|
52
66
|
* v39 memory scope isolation: classify a memory's origin_project against the
|
|
53
|
-
* active project.
|
|
67
|
+
* active project. An empty current id means the session is not in a project
|
|
54
68
|
* (home dir or markerless cwd) - everything is in scope there, matching
|
|
55
69
|
* pre-isolation behavior. NULL/undefined origin is a legacy pre-v39 row and
|
|
56
70
|
* is treated as cross-project (deny by default) - the safe direction for a
|
|
57
71
|
* security partition.
|
|
58
72
|
*/
|
|
59
|
-
export declare function classifyOriginProject(origin: string | null | undefined,
|
|
73
|
+
export declare function classifyOriginProject(origin: string | null | undefined, current: ProjectRef): 'project' | 'user-global' | 'cross-project';
|
|
60
74
|
/**
|
|
61
75
|
* The global Hippo store directory, resolved the same way shared.ts does:
|
|
62
76
|
* $HIPPO_HOME > $XDG_DATA_HOME/hippo > ~/.hippo. Lives here (leaf module) so
|
|
@@ -85,8 +99,7 @@ export declare function originFromSource(source: string | null | undefined, home
|
|
|
85
99
|
* Returns the project name, or '' for user-global (written at/under home or
|
|
86
100
|
* in a markerless directory) - injectable everywhere. Write sites must always
|
|
87
101
|
* persist this value; a NULL origin_project column is reserved for legacy
|
|
88
|
-
* pre-migration rows, which ambient context treats as deny
|
|
89
|
-
* docs/plans/2026-07-01-memory-scope-isolation.md "Origin model").
|
|
102
|
+
* pre-migration rows, which ambient context treats as deny.
|
|
90
103
|
*/
|
|
91
104
|
export declare function deriveOriginProject(cwd?: string, opts?: ResolveProjectIdentityOpts): string;
|
|
92
105
|
//# sourceMappingURL=project-identity.d.ts.map
|
package/dist/project-identity.js
CHANGED
|
@@ -2,12 +2,31 @@ 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 { loadConfig } from './config.js';
|
|
5
6
|
import { log } from './log.js';
|
|
7
|
+
import { originRemoteId, projectFileId } from './project-remote.js';
|
|
6
8
|
const MAX_WALK_DEPTH = 64;
|
|
7
9
|
const identityCache = new Map();
|
|
8
|
-
|
|
10
|
+
let remoteSwitch = null;
|
|
11
|
+
/** Clear the per-process identity cache and the remote switch it read (test seam). */
|
|
9
12
|
export function clearProjectIdentityCache() {
|
|
10
13
|
identityCache.clear();
|
|
14
|
+
remoteSwitch = null;
|
|
15
|
+
}
|
|
16
|
+
// The global store's config only: a per-store switch would stamp one checkout two ways.
|
|
17
|
+
function remoteRuleOn() {
|
|
18
|
+
const globalRoot = resolveGlobalRootDir();
|
|
19
|
+
if (remoteSwitch?.globalRoot !== globalRoot)
|
|
20
|
+
remoteSwitch = { globalRoot, on: loadConfig(globalRoot).projectIdentity.remote };
|
|
21
|
+
return remoteSwitch.on;
|
|
22
|
+
}
|
|
23
|
+
/** The id a root names itself by plus every rung that resolves; only the root's own `.git` is read, so a store nested in another checkout keeps its folder name. */
|
|
24
|
+
function namesAt(root, legacyName) {
|
|
25
|
+
const fileId = projectFileId(root);
|
|
26
|
+
const remoteId = originRemoteId(root);
|
|
27
|
+
const name = fileId ?? (remoteRuleOn() ? remoteId : null) ?? legacyName;
|
|
28
|
+
const aliases = [...new Set([fileId, remoteId, legacyName])].filter((n) => n !== null && n !== name);
|
|
29
|
+
return aliases.length > 0 ? { name, aliases } : { name };
|
|
11
30
|
}
|
|
12
31
|
/**
|
|
13
32
|
* Canonicalize a path via realpath, falling back to path.resolve when the
|
|
@@ -72,16 +91,16 @@ export function resolveProjectIdentity(cwd, opts) {
|
|
|
72
91
|
let identity;
|
|
73
92
|
const root = hippoRoot ?? gitRoot;
|
|
74
93
|
if (root !== null) {
|
|
75
|
-
const
|
|
76
|
-
identity = { root,
|
|
94
|
+
const legacyName = (hippoRoot === null ? linkedWorktreeRepoName(root, home) : null) ?? path.basename(root).toLowerCase();
|
|
95
|
+
identity = { root, ...namesAt(root, legacyName), legacyName, isHome: false };
|
|
77
96
|
}
|
|
78
97
|
else if (reachedHome || isUnder(start, home)) {
|
|
79
|
-
identity = { root: home, name: '', isHome: true };
|
|
98
|
+
identity = { root: home, name: '', legacyName: '', isHome: true };
|
|
80
99
|
}
|
|
81
100
|
else {
|
|
82
101
|
// No markers anywhere: not a project. Empty name keeps these memories
|
|
83
102
|
// user-global rather than fabricating an origin from a basename.
|
|
84
|
-
identity = { root: start, name: '', isHome: false };
|
|
103
|
+
identity = { root: start, name: '', legacyName: '', isHome: false };
|
|
85
104
|
}
|
|
86
105
|
if (cacheable)
|
|
87
106
|
identityCache.set(startInput, identity);
|
|
@@ -137,7 +156,7 @@ function walkProjectMarkers(start, home, stopDirs) {
|
|
|
137
156
|
return { hippoRoot, gitRoot, reachedHome };
|
|
138
157
|
}
|
|
139
158
|
/** Nearest ancestor `.hippo` below home and the temp root (never projects; on Windows the temp root sits inside home).
|
|
140
|
-
* Everything is realpath'd so a symlinked temp root or cwd still matches its bound.
|
|
159
|
+
* Everything is realpath'd so a symlinked temp root or cwd still matches its bound. */
|
|
141
160
|
export function findHippoStoreDir(cwd, opts) {
|
|
142
161
|
const home = realpathOrResolve(opts?.homeDir ?? os.homedir());
|
|
143
162
|
const stops = [realpathOrResolve(os.tmpdir())];
|
|
@@ -147,22 +166,39 @@ export function findHippoStoreDir(cwd, opts) {
|
|
|
147
166
|
const { hippoRoot } = walkProjectMarkers(start, home, stops);
|
|
148
167
|
return hippoRoot === null ? null : path.join(hippoRoot, '.hippo');
|
|
149
168
|
}
|
|
169
|
+
function isBareName(project) {
|
|
170
|
+
return typeof project === 'string';
|
|
171
|
+
}
|
|
172
|
+
/** The id a reader's new rows are stamped with; '' outside a project. */
|
|
173
|
+
export function projectId(project) {
|
|
174
|
+
return isBareName(project) ? project : project.name;
|
|
175
|
+
}
|
|
176
|
+
/** Every origin_project value the reader's own rows carry: the id first, then each alias and the legacy name. */
|
|
177
|
+
export function projectNames(project) {
|
|
178
|
+
if (isBareName(project) || project.name === '')
|
|
179
|
+
return [projectId(project)];
|
|
180
|
+
return [...new Set([project.name, ...(project.aliases ?? []), project.legacyName])].filter((n) => n !== '');
|
|
181
|
+
}
|
|
182
|
+
/** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
|
|
183
|
+
export function originInSql(names, column = 'origin_project') {
|
|
184
|
+
return names.length === 0 ? '0' : `${column} IN (${names.map(() => '?').join(', ')})`;
|
|
185
|
+
}
|
|
150
186
|
/**
|
|
151
187
|
* v39 memory scope isolation: classify a memory's origin_project against the
|
|
152
|
-
* active project.
|
|
188
|
+
* active project. An empty current id means the session is not in a project
|
|
153
189
|
* (home dir or markerless cwd) - everything is in scope there, matching
|
|
154
190
|
* pre-isolation behavior. NULL/undefined origin is a legacy pre-v39 row and
|
|
155
191
|
* is treated as cross-project (deny by default) - the safe direction for a
|
|
156
192
|
* security partition.
|
|
157
193
|
*/
|
|
158
|
-
export function classifyOriginProject(origin,
|
|
159
|
-
if (
|
|
194
|
+
export function classifyOriginProject(origin, current) {
|
|
195
|
+
if (projectId(current) === '')
|
|
160
196
|
return 'project';
|
|
161
197
|
if (origin === undefined || origin === null)
|
|
162
198
|
return 'cross-project';
|
|
163
199
|
if (origin === '')
|
|
164
200
|
return 'user-global';
|
|
165
|
-
return origin
|
|
201
|
+
return projectNames(current).includes(origin) ? 'project' : 'cross-project';
|
|
166
202
|
}
|
|
167
203
|
/**
|
|
168
204
|
* The global Hippo store directory, resolved the same way shared.ts does:
|
|
@@ -221,8 +257,7 @@ export function originFromSource(source, homeName) {
|
|
|
221
257
|
* Returns the project name, or '' for user-global (written at/under home or
|
|
222
258
|
* in a markerless directory) - injectable everywhere. Write sites must always
|
|
223
259
|
* persist this value; a NULL origin_project column is reserved for legacy
|
|
224
|
-
* pre-migration rows, which ambient context treats as deny
|
|
225
|
-
* docs/plans/2026-07-01-memory-scope-isolation.md "Origin model").
|
|
260
|
+
* pre-migration rows, which ambient context treats as deny.
|
|
226
261
|
*/
|
|
227
262
|
export function deriveOriginProject(cwd, opts) {
|
|
228
263
|
return resolveProjectIdentity(cwd, opts).name;
|
package/dist/project-merge.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface ProjectSummary {
|
|
|
11
11
|
export interface MergeResult {
|
|
12
12
|
readonly from: string;
|
|
13
13
|
readonly into: string;
|
|
14
|
-
/**
|
|
14
|
+
/** Imports `into` already holds for the same note, moved to dormant storage; the rest are re-tagged and the next sync files them under `into`. */
|
|
15
15
|
readonly setAside: readonly string[];
|
|
16
16
|
readonly restamped: readonly string[];
|
|
17
17
|
readonly dormantRestamped: readonly string[];
|
|
@@ -22,11 +22,18 @@ export interface ProjectFold {
|
|
|
22
22
|
readonly from: string;
|
|
23
23
|
readonly into: string;
|
|
24
24
|
}
|
|
25
|
+
/** An old name whose session folders now resolve to several projects: no fold can say whose rows are whose. */
|
|
26
|
+
export interface ProjectCollision {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly ids: readonly string[];
|
|
29
|
+
}
|
|
25
30
|
export interface RepairResult {
|
|
26
31
|
/** Imported notes filed under the wrong project, or under a project name when a user-global import holds the same text: set aside. */
|
|
27
32
|
readonly copies: readonly string[];
|
|
28
33
|
/** Names whose recorded session folders all resolve to one other project today, folded as `merge` would. */
|
|
29
34
|
readonly folds: readonly ProjectFold[];
|
|
35
|
+
/** Names left unfolded because their folders now resolve to more than one project; only `merge` by hand can split them. */
|
|
36
|
+
readonly collisions: readonly ProjectCollision[];
|
|
30
37
|
readonly toProject: ReadonlyArray<{
|
|
31
38
|
readonly id: string;
|
|
32
39
|
readonly origin: string;
|
|
@@ -50,11 +57,19 @@ export declare function mergeProjects(db: DatabaseSyncLike, hippoRoot: string, o
|
|
|
50
57
|
into: string;
|
|
51
58
|
dryRun: boolean;
|
|
52
59
|
}): MergeResult;
|
|
60
|
+
/** Names folded, directly or through others, into one of `names`; the sync moves imports still filed under them. */
|
|
61
|
+
export declare function namesFoldedInto(db: DatabaseSyncLike, tenantId: string, names: readonly string[]): string[];
|
|
53
62
|
/** 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. */
|
|
54
|
-
export declare function planProjectRepair(db: DatabaseSyncLike, hippoRoot: string, tenantId: string): Omit<RepairResult, 'backup'>;
|
|
63
|
+
export declare function planProjectRepair(db: DatabaseSyncLike, hippoRoot: string, tenantId: string, globalFolds?: boolean): Omit<RepairResult, 'backup'>;
|
|
55
64
|
/** 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. */
|
|
56
65
|
export declare function repairProjects(db: DatabaseSyncLike, hippoRoot: string, opts: {
|
|
57
66
|
tenantId: string;
|
|
58
67
|
dryRun: boolean;
|
|
68
|
+
globalFolds?: boolean;
|
|
59
69
|
}): RepairResult;
|
|
70
|
+
/**
|
|
71
|
+
* Sleep runs repair once per store, so an upgrade needs no command. Global-store folds stay
|
|
72
|
+
* with `hippo projects repair`: their evidence is compaction folders, blind to a same-named repo that never compacted.
|
|
73
|
+
*/
|
|
74
|
+
export declare function repairOnceOnSleep(db: DatabaseSyncLike, hippoRoot: string, tenantId: string): RepairResult | null;
|
|
60
75
|
//# sourceMappingURL=project-merge.d.ts.map
|
package/dist/project-merge.js
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
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 {
|
|
5
|
+
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 { getMeta, setMeta } from './db/meta.js';
|
|
9
10
|
import { insertDormantRow, listDormantSnapshots, replaceDormantEntry } from './dormant.js';
|
|
10
11
|
import { processEnv } from './env.js';
|
|
11
12
|
import { calculateStrength } from './memory.js';
|
|
12
|
-
import {
|
|
13
|
+
import { isObjectLike, isStringValue } from './capture-contract.js';
|
|
14
|
+
import { isGlobalStoreRoot, projectNames, resolveProjectIdentity } from './project-identity.js';
|
|
13
15
|
import { duplicateKey } from './same-text.js';
|
|
14
16
|
import { removeEntryMirrors } from './store/mirrors.js';
|
|
15
17
|
import { deleteEntryRowInTx, writeEntryMirrors } from './store/entry-writes.js';
|
|
@@ -93,11 +95,14 @@ export function mergeProjects(db, hippoRoot, opts) {
|
|
|
93
95
|
return result;
|
|
94
96
|
}
|
|
95
97
|
function foldInTx(db, tenantId, from, into) {
|
|
96
|
-
const
|
|
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));
|
|
97
102
|
const setAside = [];
|
|
98
103
|
for (const row of rows) {
|
|
99
104
|
const tag = toolTag(row.source);
|
|
100
|
-
if (!isImport(row) || row.superseded_by || row.kind === 'raw' || tag === null)
|
|
105
|
+
if (!isImport(row) || row.superseded_by || row.kind === 'raw' || tag === null || !held.has(noteKey(row)))
|
|
101
106
|
continue;
|
|
102
107
|
if (setAsideRow(db, tag, { ...row, origin_project: into }, 'project-merge').kind === 'dormant')
|
|
103
108
|
setAside.push(row.id);
|
|
@@ -117,6 +122,11 @@ function foldInTx(db, tenantId, from, into) {
|
|
|
117
122
|
.run(into, tenantId, from).changes ?? 0);
|
|
118
123
|
return { setAside, restamped, dormantRestamped, compactions };
|
|
119
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
|
+
}
|
|
120
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. */
|
|
121
131
|
function importCopies(db, tenantId) {
|
|
122
132
|
const live = selectAllEntries(db, tenantId).filter((e) => !e.superseded_by && isImport(e));
|
|
@@ -135,51 +145,106 @@ function strayImports(db, hippoRoot, tenantId) {
|
|
|
135
145
|
.all(tenantId);
|
|
136
146
|
const owners = new Map();
|
|
137
147
|
for (const { transcript, cwd } of sessions) {
|
|
138
|
-
const
|
|
148
|
+
const project = transcriptNotesProject(transcript, cwd, machine);
|
|
139
149
|
const dir = path.join(path.dirname(transcript), 'memory');
|
|
140
|
-
if (
|
|
141
|
-
|
|
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)])]));
|
|
142
154
|
}
|
|
143
155
|
const tool = toolSourcePrefix('claude-code');
|
|
144
156
|
const live = selectLiveEntriesBySourcePrefix(db, tenantId, tool).filter((e) => e.kind !== 'raw' && !e.pinned);
|
|
145
157
|
const origins = new Set(live.map((e) => e.origin_project ?? ''));
|
|
158
|
+
const edges = foldEdges(db, tenantId);
|
|
146
159
|
const right = new Set();
|
|
147
160
|
const wrong = new Set();
|
|
148
|
-
for (const [dir,
|
|
149
|
-
if (
|
|
161
|
+
for (const [dir, ids] of owners) {
|
|
162
|
+
if (ids.size !== 1)
|
|
150
163
|
continue;
|
|
151
|
-
|
|
152
|
-
|
|
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)));
|
|
153
169
|
}
|
|
154
170
|
const prefix = (e) => e.source.slice(0, e.source.indexOf('/', tool.length) + 1);
|
|
155
171
|
const misfiled = live.filter((e) => wrong.has(prefix(e)));
|
|
156
172
|
const seen = new Set(misfiled.map((e) => e.id));
|
|
157
173
|
return [...misfiled, ...copies.filter((e) => !seen.has(e.id) && !right.has(prefix(e)))];
|
|
158
174
|
}
|
|
159
|
-
/**
|
|
160
|
-
function planFolds(db, tenantId) {
|
|
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: [] };
|
|
161
181
|
// SAFETY: the SELECT names the two columns of the row type.
|
|
162
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`)
|
|
163
183
|
.all(tenantId);
|
|
164
184
|
const today = new Map();
|
|
165
185
|
for (const { origin, cwd } of rows) {
|
|
166
186
|
if (fs.existsSync(cwd))
|
|
167
|
-
today.set(origin, (today.get(origin) ?? new Set()).add(
|
|
187
|
+
today.set(origin, (today.get(origin) ?? new Set()).add(resolveProjectIdentity(cwd).name));
|
|
168
188
|
}
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const
|
|
189
|
+
const folds = [];
|
|
190
|
+
const collisions = [];
|
|
191
|
+
for (const [from, names] of today) {
|
|
172
192
|
const [into] = names;
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
}
|
|
175
200
|
// A fold into a name that itself folds would land rows by run order; the next repair takes the rest of the chain.
|
|
176
201
|
const sources = new Set(folds.map((f) => f.from));
|
|
177
|
-
return folds.filter((f) => !sources.has(f.into));
|
|
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);
|
|
178
243
|
}
|
|
179
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. */
|
|
180
|
-
export function planProjectRepair(db, hippoRoot, tenantId) {
|
|
181
|
-
const folds
|
|
182
|
-
return { copies: strayImports(db, hippoRoot, tenantId).map((e) => e.id), folds, ...planUserGlobalRepair(db, tenantId, folds) };
|
|
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) };
|
|
183
248
|
}
|
|
184
249
|
/** Parents read with `folds` already applied, so a plan matches what apply does after folding. */
|
|
185
250
|
function planUserGlobalRepair(db, tenantId, folds) {
|
|
@@ -210,9 +275,9 @@ function planUserGlobalRepair(db, tenantId, folds) {
|
|
|
210
275
|
}
|
|
211
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. */
|
|
212
277
|
export function repairProjects(db, hippoRoot, opts) {
|
|
213
|
-
const { tenantId, dryRun } = opts;
|
|
278
|
+
const { tenantId, dryRun, globalFolds = true } = opts;
|
|
214
279
|
if (dryRun)
|
|
215
|
-
return { ...planProjectRepair(db, hippoRoot, tenantId), backup: null };
|
|
280
|
+
return { ...planProjectRepair(db, hippoRoot, tenantId, globalFolds), backup: null };
|
|
216
281
|
const backup = backupStore(db, hippoRoot, 'before-repair');
|
|
217
282
|
const { result, rewrite, purge } = inTransaction(db, false, () => {
|
|
218
283
|
const copies = [];
|
|
@@ -221,7 +286,7 @@ export function repairProjects(db, hippoRoot, opts) {
|
|
|
221
286
|
if (tag !== null && setAsideRow(db, tag, row, 'project-repair').kind === 'dormant')
|
|
222
287
|
copies.push(row.id);
|
|
223
288
|
}
|
|
224
|
-
const folds
|
|
289
|
+
const { folds, collisions } = planFolds(db, hippoRoot, tenantId, globalFolds);
|
|
225
290
|
const folded = folds.map((f) => foldInTx(db, tenantId, f.from, f.into));
|
|
226
291
|
const plan = planUserGlobalRepair(db, tenantId, []);
|
|
227
292
|
const stamp = db.prepare(`UPDATE memories SET origin_project = ?, updated_at = datetime('now') WHERE tenant_id = ? AND id = ?`);
|
|
@@ -235,7 +300,7 @@ export function repairProjects(db, hippoRoot, opts) {
|
|
|
235
300
|
}
|
|
236
301
|
appendAuditEvent(db, { tenantId, actor: ACTOR, op: 'project_repair', metadata: { backup, copies, folds, folded, ...plan } });
|
|
237
302
|
return {
|
|
238
|
-
result: { copies, folds, ...plan, backup },
|
|
303
|
+
result: { copies, folds, collisions, ...plan, backup },
|
|
239
304
|
rewrite: [...plan.toProject.map((r) => r.id), ...folded.flatMap((f) => f.restamped)],
|
|
240
305
|
purge: [...copies, ...plan.setAside, ...folded.flatMap((f) => f.setAside)],
|
|
241
306
|
};
|
|
@@ -243,6 +308,23 @@ export function repairProjects(db, hippoRoot, opts) {
|
|
|
243
308
|
refreshMirrors(db, hippoRoot, tenantId, rewrite, purge);
|
|
244
309
|
return result;
|
|
245
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');
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
246
328
|
/** After commit, as the agent memory sync does: a stale mirror would bring the old tag back on the next rebuild. */
|
|
247
329
|
function refreshMirrors(db, hippoRoot, tenantId, rewrite, purge) {
|
|
248
330
|
const ids = new Set(rewrite);
|
|
@@ -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
|