hippo-memory 1.7.3 → 1.7.4

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/dist/cli.js CHANGED
@@ -41,7 +41,7 @@ import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
41
41
  import { computeSystemEnergy, vecNorm } from './physics.js';
42
42
  import { loadConfig } from './config.js';
43
43
  import { openHippoDb, closeHippoDb } from './db.js';
44
- import { getActiveGoalsWithDb, MAX_FINAL_MULTIPLIER, pushGoal, getActiveGoals, completeGoal, suspendGoal, resumeGoal } from './goals.js';
44
+ import { pushGoal, getActiveGoals, completeGoal, suspendGoal, resumeGoal, applyGoalStackBoost } from './goals.js';
45
45
  import { rowToGoal } from './goals.js';
46
46
  import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
47
47
  import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
@@ -825,137 +825,23 @@ async function cmdRecall(hippoRoot, query, flags) {
825
825
  .map((r) => (r.entry.tags?.includes(goalTag) ? { ...r, score: r.score * 1.5 } : r))
826
826
  .sort((a, b) => b.score - a.score);
827
827
  }
828
- // dlPFC depth (B3, v0.38). When HIPPO_SESSION_ID is set (env or
829
- // --session-id flag) and the (tenant, session) has active goals, boost
830
- // memories whose tags overlap any active goal's name. Final multiplier is
831
- // hard-capped at MAX_FINAL_MULTIPLIER (3.0x). Each boosted (memory, goal)
832
- // pair is logged into goal_recall_log for outcome propagation.
833
- //
834
- // Runs AFTER the explicit `--goal <tag>` block so an explicit flag always
835
- // wins: if the user passed `--goal X`, this block is skipped entirely
836
- // (gated on `goalTag === ''`).
837
- //
838
- // db-handle note (plan-eng-review fix #5): the surrounding cmdRecall path
839
- // does NOT keep an open db handle in scope at this point — earlier search
840
- // helpers (loadSearchEntries, hybridSearch, ...) each open and close their
841
- // own short-lived handles. Reusing isn't practical here; we open a fresh
842
- // short-lived handle for this block, mirroring the existing CLI pattern
843
- // (e.g. emitCliAudit). Closed in `finally`.
828
+ // dlPFC depth (B3, v0.38; lifted v1.7.4 into applyGoalStackBoost). When
829
+ // HIPPO_SESSION_ID is set (env or --session-id flag) and the
830
+ // (tenant, session) has active goals, the helper boosts memories whose tags
831
+ // overlap any active goal's name and logs (memory, goal) pairs into
832
+ // goal_recall_log. Runs AFTER the explicit `--goal <tag>` block so an
833
+ // explicit flag always wins (gated on `goalTag === ''`).
844
834
  const sessionId = (flags['session-id'] !== undefined
845
835
  ? String(flags['session-id'])
846
836
  : process.env.HIPPO_SESSION_ID ?? '').trim();
847
837
  if (sessionId && goalTag === '') {
848
- // Use the same tenant as the recall path — see cmdRecall:778.
849
- const tenantIdForGoals = tenantId;
850
838
  const dbForGoals = openHippoDb(hippoRoot);
851
839
  try {
852
- const active = getActiveGoalsWithDb(dbForGoals, {
840
+ results = applyGoalStackBoost(dbForGoals, results, {
853
841
  sessionId,
854
- tenantId: tenantIdForGoals,
842
+ tenantId,
843
+ limit,
855
844
  });
856
- if (active.length > 0) {
857
- const goalsByTag = new Map(active.map((g) => [g.goalName, g]));
858
- // Task 7: load retrieval_policy rows for active goals so per-policy
859
- // multipliers can compose onto the base goal-tag boost. The composed
860
- // result is hard-capped at MAX_FINAL_MULTIPLIER (3.0x) BEFORE applying
861
- // to score — even an `errorPriority: 9.0` policy cannot exceed 3.0x.
862
- const policiesByGoalId = new Map();
863
- for (const g of active) {
864
- if (!g.retrievalPolicyId)
865
- continue;
866
- const row = dbForGoals.prepare(`
867
- SELECT id, goal_id, policy_type, weight_schema_fit, weight_recency, weight_outcome, error_priority
868
- FROM retrieval_policy WHERE id = ?
869
- `).get(g.retrievalPolicyId);
870
- if (row) {
871
- policiesByGoalId.set(g.id, {
872
- id: row.id,
873
- goalId: row.goal_id,
874
- policyType: row.policy_type,
875
- weightSchemaFit: row.weight_schema_fit,
876
- weightRecency: row.weight_recency,
877
- weightOutcome: row.weight_outcome,
878
- errorPriority: row.error_priority,
879
- });
880
- }
881
- }
882
- results = results
883
- .map((r) => {
884
- const tags = r.entry.tags ?? [];
885
- const matches = tags.filter((t) => goalsByTag.has(t));
886
- if (matches.length === 0)
887
- return r;
888
- // Base 2.0x for first match, +0.5x per additional, capped at 3.0x.
889
- let multiplier = Math.min(2.0 + 0.5 * (matches.length - 1), MAX_FINAL_MULTIPLIER);
890
- // Compose per-policy multipliers per matched tag.
891
- for (const tag of matches) {
892
- const goal = goalsByTag.get(tag);
893
- const policy = policiesByGoalId.get(goal.id);
894
- if (!policy)
895
- continue;
896
- if (policy.policyType === 'error-prioritized' && tags.includes('error')) {
897
- multiplier *= policy.errorPriority;
898
- }
899
- else if (policy.policyType === 'schema-fit-biased') {
900
- // Linearly weight schema_fit in [0,1] up to (weightSchemaFit)x.
901
- // Default 1.0 is a no-op.
902
- multiplier *=
903
- 1.0 +
904
- Math.max(0, policy.weightSchemaFit - 1.0) *
905
- (r.entry.schema_fit ?? 0.5);
906
- }
907
- else if (policy.policyType === 'recency-first') {
908
- multiplier *= policy.weightRecency;
909
- }
910
- else if (policy.policyType === 'hybrid') {
911
- multiplier *= policy.weightOutcome;
912
- }
913
- }
914
- // Hard cap AFTER all composition.
915
- multiplier = Math.min(multiplier, MAX_FINAL_MULTIPLIER);
916
- return {
917
- ...r,
918
- score: r.score * multiplier,
919
- _goalMatches: matches,
920
- };
921
- })
922
- .sort((a, b) => b.score - a.score);
923
- // Filter to local memories only — global memory IDs aren't in this
924
- // DB's memories table, so the FK on goal_recall_log.memory_id would
925
- // fail. dlPFC depth's outcome propagation is session-scoped to local;
926
- // boost on ranking still applies to global results, just no log row
927
- // -> no propagation.
928
- const topKIds = results.slice(0, limit).map((r) => r.entry.id);
929
- const localIds = new Set();
930
- if (topKIds.length > 0) {
931
- const placeholders = topKIds.map(() => '?').join(',');
932
- const localRows = dbForGoals.prepare(`SELECT id FROM memories WHERE id IN (${placeholders})`).all(...topKIds);
933
- for (const row of localRows)
934
- localIds.add(row.id);
935
- }
936
- // Log top-K boosted recalls. INSERT OR IGNORE because
937
- // UNIQUE(memory_id, goal_id) means a re-recall during the same goal
938
- // life is a no-op for outcome attribution.
939
- const recalledAt = new Date().toISOString();
940
- const insertLog = dbForGoals.prepare(`
941
- INSERT OR IGNORE INTO goal_recall_log
942
- (goal_id, memory_id, tenant_id, session_id, recalled_at, score)
943
- VALUES (?, ?, ?, ?, ?, ?)
944
- `);
945
- for (const r of results.slice(0, limit)) {
946
- if (!localIds.has(r.entry.id))
947
- continue; // global -> skip log insert
948
- const matches = r._goalMatches;
949
- if (!matches || matches.length === 0)
950
- continue;
951
- for (const tag of matches) {
952
- const goal = goalsByTag.get(tag);
953
- if (!goal)
954
- continue;
955
- insertLog.run(goal.id, r.entry.id, tenantIdForGoals, sessionId, recalledAt, r.score);
956
- }
957
- }
958
- }
959
845
  }
960
846
  finally {
961
847
  closeHippoDb(dbForGoals);
@@ -4481,7 +4367,7 @@ function cmdGoalList(hippoRoot, flags) {
4481
4367
  function cmdGoalComplete(hippoRoot, args, flags) {
4482
4368
  const id = args[0];
4483
4369
  if (!id) {
4484
- console.error('Usage: hippo goal complete <id> [--outcome <0..1>]');
4370
+ console.error('Usage: hippo goal complete <id> [--outcome <0..1>] [--no-propagate]');
4485
4371
  process.exit(1);
4486
4372
  }
4487
4373
  let outcomeScore;
@@ -4498,7 +4384,8 @@ function cmdGoalComplete(hippoRoot, args, flags) {
4498
4384
  }
4499
4385
  outcomeScore = parsed;
4500
4386
  }
4501
- completeGoal(hippoRoot, id, { outcomeScore });
4387
+ const noPropagate = flags['no-propagate'] === true;
4388
+ completeGoal(hippoRoot, id, { outcomeScore, noPropagate });
4502
4389
  console.log('ok');
4503
4390
  }
4504
4391
  function cmdGoalSuspend(hippoRoot, args) {
@@ -4961,6 +4848,7 @@ Commands:
4961
4848
  --all Include suspended/completed goals
4962
4849
  goal complete <id> Mark a goal completed
4963
4850
  --outcome <0..1> Outcome score; >=0.7 boosts, <0.3 decays recalled mems
4851
+ --no-propagate Close the goal without applying strength side-effects
4964
4852
  goal suspend <id> Move an active goal to suspended
4965
4853
  goal resume <id> Move a suspended goal back to active (depth-capped)
4966
4854
  auth <sub> Manage API keys (A5 stub auth)