hippo-memory 1.68.0 → 1.69.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.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-memories/copilot.d.ts +3 -1
  3. package/dist/agent-memories/copilot.js +2 -2
  4. package/dist/api/context-select.d.ts +10 -0
  5. package/dist/api/context-select.js +16 -18
  6. package/dist/api/context.js +101 -60
  7. package/dist/api/forget.d.ts +2 -2
  8. package/dist/api/forget.js +11 -0
  9. package/dist/api/outcome.d.ts +4 -4
  10. package/dist/api/outcome.js +29 -6
  11. package/dist/api/promote.d.ts +3 -3
  12. package/dist/api/promote.js +45 -90
  13. package/dist/api/remember.d.ts +3 -2
  14. package/dist/api/remember.js +38 -11
  15. package/dist/capture/compact.js +21 -2
  16. package/dist/capture/copilot-transcript.d.ts +3 -0
  17. package/dist/capture/copilot-transcript.js +8 -1
  18. package/dist/capture/session-worker.d.ts +13 -0
  19. package/dist/capture/session-worker.js +194 -0
  20. package/dist/capture-contract.d.ts +3 -1
  21. package/dist/capture-contract.js +36 -0
  22. package/dist/cli/session-hooks.js +90 -22
  23. package/dist/cli/setup.js +70 -24
  24. package/dist/cli/usage.js +4 -1
  25. package/dist/cli.js +1 -1
  26. package/dist/connectors/github/ingest.js +2 -1
  27. package/dist/connectors/slack/deletion.js +2 -1
  28. package/dist/connectors/slack/ingest.js +2 -1
  29. package/dist/env.d.ts +2 -0
  30. package/dist/env.js +2 -0
  31. package/dist/hooks/copilot.d.ts +50 -8
  32. package/dist/hooks/copilot.js +116 -30
  33. package/dist/hooks/json-hooks.d.ts +2 -0
  34. package/dist/hooks/json-hooks.js +11 -2
  35. package/dist/hooks/shared.d.ts +7 -1
  36. package/dist/hooks/shared.js +18 -3
  37. package/dist/mcp/memory-tools.d.ts +2 -2
  38. package/dist/mcp/memory-tools.js +9 -8
  39. package/dist/mcp/request.js +3 -3
  40. package/dist/memory.d.ts +2 -0
  41. package/dist/memory.js +7 -0
  42. package/dist/prompt-recall.d.ts +5 -0
  43. package/dist/prompt-recall.js +13 -0
  44. package/dist/raw-archive.d.ts +2 -2
  45. package/dist/raw-archive.js +5 -3
  46. package/dist/recall-scope.d.ts +2 -5
  47. package/dist/recall-scope.js +5 -6
  48. package/dist/server/routes/memories.js +9 -6
  49. package/dist/server-detect.js +3 -2
  50. package/dist/server.d.ts +14 -1
  51. package/dist/server.js +16 -6
  52. package/dist/store/audit-event.d.ts +1 -1
  53. package/dist/store/audit-event.js +1 -1
  54. package/dist/store/entry-writes-group.d.ts +9 -0
  55. package/dist/store/entry-writes-group.js +157 -0
  56. package/dist/store/handoffs.d.ts +5 -2
  57. package/dist/store/handoffs.js +35 -5
  58. package/dist/store/open.d.ts +2 -0
  59. package/dist/store/open.js +10 -0
  60. package/dist/store/search-rows.js +3 -9
  61. package/dist/store/sessions.d.ts +5 -0
  62. package/dist/store/sessions.js +4 -1
  63. package/dist/store-port.d.ts +71 -0
  64. package/dist/store-port.js +23 -11
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  68. package/extensions/openclaw-plugin/package.json +1 -1
  69. package/openclaw.plugin.json +1 -1
  70. package/package.json +1 -1
@@ -21,11 +21,15 @@ import { cmdPreCompact, cmdPostCompact } from '../capture/compact.js';
21
21
  import { transcriptWorkingState } from '../capture/working-state.js';
22
22
  import { collectHandoffEvidence } from '../handoff-evidence.js';
23
23
  import { resolveLastSessionTranscript } from '../capture/transcript.js';
24
- import { copilotTranscriptFor } from '../capture/copilot-transcript.js';
24
+ import { copilotTranscriptFor, SESSION_ID_RE } from '../capture/copilot-transcript.js';
25
+ import { loadTurnPosition, runSessionWorker, saveTurnPosition, turnsAfter } from '../capture/session-worker.js';
26
+ import { isStringValue, readVscodeStop } from '../capture-contract.js';
27
+ import { loadConfig } from '../config.js';
28
+ import { countCreatedSinceLastSleep } from '../store/index-and-stats.js';
25
29
  import { truncateCodePointSafe } from '../transcript-tail.js';
26
30
  import { COMPACTION_DB_WAIT_MS } from '../compaction-record.js';
27
31
  import { COMPACT_RESUME_EVENT_CONTENT_CAP, COMPACT_RESUME_MAX_AGE_MS, compactResumeText } from '../context-render.js';
28
- import { readHookStdin } from '../stdin.js';
32
+ import { normaliseHookPayload, readHookStdin, readStdinBounded } from '../stdin.js';
29
33
  import { resolveTenantId } from '../tenant.js';
30
34
  import { errorMessage, log } from '../log.js';
31
35
  import { withLedgerDb } from '../ledger-db.js';
@@ -143,11 +147,16 @@ function restoreCompactSnapshot(hippoRoot, payloadSessionId) {
143
147
  */
144
148
  export async function cmdSessionEnd(hippoRoot, flags) {
145
149
  const runtime = hookRuntime(flags);
150
+ const turn = flags['turn'] === true;
146
151
  // Copilot's hook command carries no path, since one quoted into it would need escaping for each shell; the log goes where the hook table used to point.
147
152
  const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : runtime === 'copilot' ? resolveJsonHookPaths('copilot').logFile : null;
148
153
  // Bounded read: extracts transcript_path + session_id for the detached worker's argv.
149
154
  let sessionId = null;
150
- const { text: stdinText } = await readHookStdin();
155
+ const raw = await readStdinBounded();
156
+ // Read before normalising: the Copilot CLI's agentStop shares this hook line, and camelCase keys would pass as VS Code's.
157
+ if (turn && !isVscodeStopPayload(raw))
158
+ return;
159
+ const stdinText = normaliseHookPayload(raw.text);
151
160
  // Before the spawn, since the worker finds its store from the folder it inherits.
152
161
  const root = payloadCwdRoot(hippoRoot, stdinText, runtime);
153
162
  try {
@@ -172,6 +181,8 @@ export async function cmdSessionEnd(hippoRoot, flags) {
172
181
  workerArgs.push('--transcript', transcriptPath);
173
182
  if (sessionId)
174
183
  workerArgs.push('--session-id', sessionId);
184
+ if (turn)
185
+ workerArgs.push('--turn');
175
186
  try {
176
187
  const child = spawn(process.execPath, workerArgs, {
177
188
  detached: true,
@@ -194,7 +205,16 @@ export async function cmdSessionEnd(hippoRoot, flags) {
194
205
  return;
195
206
  }
196
207
  }
208
+ /** A VS Code Stop payload with a session id that can name the lock file; anything else makes turn mode a silent no-op. */
209
+ function isVscodeStopPayload(raw) {
210
+ const receipt = readVscodeStop(raw.text, raw.timedOut);
211
+ return receipt.status === 'received' && receipt.input.sessionId !== null && SESSION_ID_RE.test(receipt.input.sessionId);
212
+ }
197
213
  export async function cmdSessionEndWorker(hippoRoot, flags) {
214
+ const sessionId = flags['session-id'];
215
+ await runSessionWorker(isStringValue(sessionId) ? sessionId : null, flags['turn'] === true ? 'turn' : 'full', (mode) => sessionEndWork(hippoRoot, flags, mode));
216
+ }
217
+ async function sessionEndWork(hippoRoot, flags, mode) {
198
218
  const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
199
219
  const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
200
220
  const closeSessionId = typeof flags['session-id'] === 'string' ? flags['session-id'] : null;
@@ -211,11 +231,18 @@ export async function cmdSessionEndWorker(hippoRoot, flags) {
211
231
  flushRereadLog();
212
232
  return;
213
233
  }
214
- await sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath);
234
+ if (mode === 'turn' && closeSessionId)
235
+ await sleepIfDue(hippoRoot, flags, closeLogFile, transcriptPath, closeSessionId);
236
+ else
237
+ await sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath);
215
238
  flushRereadLog();
216
239
  const digestLog = (message) => appendSessionEndCloseLog(closeLogFile, message);
217
240
  const scan = transcriptPath ? readSessionScan(transcriptPath, digestLog) : null;
218
- captureEndedSession(hippoRoot, store, flags, transcriptPath, scan);
241
+ const capture = (turns) => captureEndedSession(hippoRoot, store, flags, transcriptPath, turns);
242
+ if (mode === 'turn')
243
+ captureNewTurns(transcriptPath, closeSessionId, scan, capture, digestLog);
244
+ else
245
+ capture(scan?.turns);
219
246
  recordSessionDigest(hippoRoot, scan, {
220
247
  key: closeSessionId || path.basename(transcriptPath ?? '', '.jsonl'),
221
248
  tenantId: resolveTenantId({}),
@@ -225,7 +252,12 @@ export async function cmdSessionEndWorker(hippoRoot, flags) {
225
252
  // session-end may never fire (crash, kill -9) the freshness bound in loadFreshActiveTaskSnapshot is the backstop.
226
253
  // The handoff is written first, while the snapshot writeSessionEndHandoff reads is still active.
227
254
  if (closeSessionId)
228
- writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile);
255
+ writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile, mode === 'turn');
256
+ if (mode === 'turn') {
257
+ // The chat goes on after a reply, so its snapshot stays for the next compaction to restore.
258
+ appendSessionEndCloseLog(closeLogFile, 'skip snapshot close: turn mode');
259
+ return;
260
+ }
229
261
  try {
230
262
  if (closeSessionId) {
231
263
  const closed = closeTaskSnapshotsForSession(store, resolveTenantId({}), closeSessionId);
@@ -255,32 +287,68 @@ async function sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath)
255
287
  logSessionEndImport(closeLogFile, transcriptPath);
256
288
  }
257
289
  }
258
- function captureEndedSession(hippoRoot, store, flags, transcriptPath, scan) {
290
+ /** A close after every reply sleeps only at the MCP server's auto-sleep threshold; each line starts the log afresh, as a sleep does. */
291
+ async function sleepIfDue(hippoRoot, flags, closeLogFile, transcriptPath, sessionId) {
292
+ if (!isInitialized(hippoRoot))
293
+ return sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath);
294
+ const prefix = `turn close, session ${sessionId}`;
295
+ try {
296
+ const { enabled, threshold } = loadConfig(hippoRoot).autoSleep;
297
+ const count = enabled ? countCreatedSinceLastSleep(hippoRoot, resolveTenantId({})) : 0;
298
+ if (!enabled || count < threshold) {
299
+ const why = enabled ? `${count} new memories, threshold ${threshold}` : 'auto-sleep is off';
300
+ appendSessionEndCloseLog(closeLogFile, `${prefix}: skip sleep, ${why}`, { startFresh: true });
301
+ return;
302
+ }
303
+ await sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath);
304
+ appendSessionEndCloseLog(closeLogFile, `${prefix}: ran sleep at ${count} new memories (threshold ${threshold})`);
305
+ }
306
+ catch (err) {
307
+ appendSessionEndCloseLog(closeLogFile, `${prefix}: sleep check failed: ${errorMessage(err)}`, { startFresh: true });
308
+ }
309
+ }
310
+ /** Captures only the turns after this session's cursor, then moves the cursor, so each reply is extracted once. */
311
+ function captureNewTurns(transcriptPath, sessionId, scan, capture, log) {
312
+ if (!transcriptPath || !sessionId || !scan) {
313
+ log('skip capture: no readable transcript for this session');
314
+ return;
315
+ }
316
+ const fresh = turnsAfter(scan.turns, loadTurnPosition(sessionId, transcriptPath));
317
+ if (fresh.length === 0) {
318
+ log('skip capture: no new turns since the last reply');
319
+ return;
320
+ }
321
+ if (capture(fresh))
322
+ saveTurnPosition(sessionId, transcriptPath, scan.turns, log);
323
+ }
324
+ /** True when capture ran to the end, so a turn close may move its cursor past these turns. */
325
+ function captureEndedSession(hippoRoot, store, flags, transcriptPath, turns) {
259
326
  try {
260
327
  const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
261
328
  // With no stdin of its own, capture would read this as a manual run and scan every project.
262
329
  if (!transcriptPath) {
263
330
  appendSessionEndCloseLog(logFile ?? null, 'skip capture: no transcript for this session');
331
+ return false;
264
332
  }
265
- else {
266
- cmdCapture(store, {
267
- source: 'last-session',
268
- transcriptPath,
269
- logFile,
270
- dryRun: false,
271
- global: false,
272
- tenantId: resolveTenantId({}),
273
- // In the global store, rows would otherwise read as user-global and show up in every project.
274
- originProject: store === hippoRoot ? undefined : resolveProjectIdentity(process.cwd()),
275
- sessionTurns: scan?.turns,
276
- });
277
- }
333
+ cmdCapture(store, {
334
+ source: 'last-session',
335
+ transcriptPath,
336
+ logFile,
337
+ dryRun: false,
338
+ global: false,
339
+ tenantId: resolveTenantId({}),
340
+ // In the global store, rows would otherwise read as user-global and show up in every project.
341
+ originProject: store === hippoRoot ? undefined : resolveProjectIdentity(process.cwd()),
342
+ sessionTurns: turns,
343
+ });
344
+ return true;
278
345
  }
279
346
  catch {
280
347
  // Same treatment — the failure line is already in the log.
348
+ return false;
281
349
  }
282
350
  }
283
- function writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile) {
351
+ function writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile, inPlace = false) {
284
352
  try {
285
353
  const tenantId = resolveTenantId({});
286
354
  const ownSnapshot = loadActiveTaskSnapshot(store, tenantId)?.session_id === closeSessionId;
@@ -293,7 +361,7 @@ function writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile) {
293
361
  }
294
362
  else {
295
363
  const evidence = collectHandoffEvidence(process.cwd(), 'unknown');
296
- const handoff = writeSessionEndHandoff(store, tenantId, closeSessionId, evidence, derived);
364
+ const handoff = writeSessionEndHandoff(store, tenantId, closeSessionId, evidence, derived, undefined, { inPlace });
297
365
  appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` : `skip: kept the existing handoff for session ${closeSessionId}`);
298
366
  }
299
367
  }
package/dist/cli/setup.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import * as path from 'path';
3
3
  import * as fs from 'fs';
4
4
  import { execFileSync } from 'child_process';
5
- import { installJsonHooks, uninstallJsonHooks, checkUninstallable, resolveJsonHookPaths } from '../hooks/json-hooks.js';
5
+ import { installJsonHooks, uninstallJsonHooks, checkUninstallable, copilotHookEvents, resolveJsonHookPaths } from '../hooks/json-hooks.js';
6
6
  import { detectInstalledTools } from '../hooks/shared.js';
7
7
  import { ensureCodexWrapperInstalled, installCodexWrapper, detectRealCodexPath, uninstallCodexWrapper, } from '../hooks/codex-wrapper.js';
8
8
  import { installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath } from '../hooks/opencode.js';
9
- import { copilotMcpSnippet, copilotPaths, installCopilot, isMcpFailure, uninstallCopilot } from '../hooks/copilot.js';
9
+ import { COPILOT_CLI_MCP, VSCODE_MCP, copilotMcpSnippet, copilotPaths, installCopilot, isCopilotCliPresent, isMcpFailure, uninstallCopilot, } from '../hooks/copilot.js';
10
10
  import { isInitialized } from '../store/open.js';
11
11
  import { currentMachine, importUserMemories } from '../agent-memories/sync.js';
12
12
  import { getGlobalRoot } from '../shared.js';
@@ -42,7 +42,7 @@ function hookList() {
42
42
  for (const [name, hook] of Object.entries(HOOKS)) {
43
43
  console.log(` ${name.padEnd(15)} -> ${hook.file} (${hook.description})`);
44
44
  }
45
- console.log(` ${'copilot'.padEnd(15)} -> $COPILOT_HOME or ~/.copilot (GitHub Copilot: hooks, MCP server and instructions)`);
45
+ console.log(` ${'copilot'.padEnd(15)} -> $COPILOT_HOME or ~/.copilot, and each VS Code User folder (GitHub Copilot: hooks, MCP server and instructions)`);
46
46
  console.log('\nUsage: hippo hook install <name>');
47
47
  console.log(' hippo hook uninstall <name>');
48
48
  }
@@ -155,11 +155,12 @@ function installOpencodeHook() {
155
155
  console.log(`WARNING: opencode.json is unparseable; legacy hooks block could not be auto-removed. Fix the file manually.`);
156
156
  }
157
157
  }
158
+ const addByHand = (host) => `add this under "${host.key}" by hand: ${copilotMcpSnippet(host)}`;
158
159
  const MCP_MERGE_LINES = {
159
160
  added: (file) => `Added the "hippo" MCP server -> ${file}`,
160
161
  present: (file) => `The "hippo" MCP server is already in ${file}`,
161
162
  'user-owned': (file) => `Left the "hippo" MCP server in ${file} as it is: hippo did not write it`,
162
- unreadable: (file) => `WARNING: ${file} has comments or is not a JSON object hippo can merge into, so it was left unchanged; add this under "mcpServers" by hand: ${copilotMcpSnippet()}`,
163
+ unreadable: (file, host) => `WARNING: ${file} has comments or is not a JSON object hippo can merge into, so it was left unchanged; ${addByHand(host)}`,
163
164
  };
164
165
  const mcpFailureLine = (file, failure, fix) => `WARNING: hippo could not use ${file} (${failure.failed}), so it was left unchanged; ${fix}`;
165
166
  const unclosedLine = (file) => `WARNING: ${file} has ${HOOK_MARKERS.start} with no ${HOOK_MARKERS.end}, so hippo left it unchanged; fix the markers by hand`;
@@ -175,32 +176,74 @@ const INSTRUCTIONS_REMOVE_LINES = {
175
176
  kept: (file) => `Left the hippo block in ${file}: it is not the Copilot text hippo wrote. Delete it by hand if no agent needs it.`,
176
177
  unclosed: unclosedLine,
177
178
  };
179
+ const VSCODE_INSTRUCTIONS_INSTALL_LINES = {
180
+ written: (file) => `Wrote the hippo instructions file -> ${file}`,
181
+ present: (file) => `The hippo instructions file is already at ${file}`,
182
+ kept: (file) => `Kept ${file} as it is: it is not the text hippo writes, so it was edited or is someone else's`,
183
+ };
184
+ const VSCODE_INSTRUCTIONS_REMOVE_LINES = {
185
+ removed: (file) => `Removed the hippo instructions file ${file}`,
186
+ absent: () => '',
187
+ kept: (file) => `Left ${file}: it is not the text hippo wrote. Delete it by hand if no agent needs it.`,
188
+ };
189
+ const VSCODE_HOOKS_LINE = 'VS Code: hooks need VS Code 1.109.3 or later with chat.useHooks on (the default); older versions get the MCP server and the instructions file only.';
190
+ function mcpInstallLine(file, mcp, host) {
191
+ return isMcpFailure(mcp) ? mcpFailureLine(file, mcp, addByHand(host)) : MCP_MERGE_LINES[mcp](file, host);
192
+ }
193
+ function printVscodeInstall(result, indent) {
194
+ const { paths } = result;
195
+ console.log(`${indent}${mcpInstallLine(paths.mcpConfig, result.mcp, VSCODE_MCP)}`);
196
+ console.log(`${indent}${VSCODE_INSTRUCTIONS_INSTALL_LINES[result.instructions](paths.instructions)}`);
197
+ if (paths.profiles.length > 0) {
198
+ const names = paths.profiles.map((dir) => path.basename(dir)).join(', ');
199
+ console.log(`${indent}Found VS Code profiles (${names}) under ${path.join(paths.userDir, 'profiles')}: hippo set up the default profile only; copy the "hippo" server and prompts/hippo.instructions.md into a profile to use hippo there.`);
200
+ }
201
+ }
178
202
  function printCopilotInstall(result, indent) {
179
- const { paths, mcp } = result;
180
- const hooks = result.hooks ? "Installed hippo's Copilot hooks (sessionStart, postToolUseFailure, preCompact, sessionEnd) ->" : "hippo's Copilot hooks are already in";
203
+ const { paths } = result;
204
+ const hooks = result.hooks ? `Installed hippo's Copilot hooks (${copilotHookEvents().join(', ')}) ->` : "hippo's Copilot hooks are already in";
181
205
  console.log(`${indent}${hooks} ${paths.hooks}`);
182
- const mcpLine = isMcpFailure(mcp) ? mcpFailureLine(paths.mcpConfig, mcp, `add this under "mcpServers" by hand: ${copilotMcpSnippet()}`) : MCP_MERGE_LINES[mcp](paths.mcpConfig);
183
- console.log(`${indent}${mcpLine}`);
184
- console.log(`${indent}${INSTRUCTIONS_INSTALL_LINES[result.instructions](paths.instructions)}`);
206
+ if (result.mcp === null || result.instructions === null) {
207
+ console.log(`${indent}No Copilot CLI files in ${path.dirname(paths.mcpConfig)}, so hippo skipped mcp-config.json and copilot-instructions.md, which only the Copilot CLI reads`);
208
+ }
209
+ else {
210
+ console.log(`${indent}${mcpInstallLine(paths.mcpConfig, result.mcp, COPILOT_CLI_MCP)}`);
211
+ console.log(`${indent}${INSTRUCTIONS_INSTALL_LINES[result.instructions](paths.instructions)}`);
212
+ }
213
+ for (const vscode of result.vscode)
214
+ printVscodeInstall(vscode, indent);
215
+ if (result.vscode.length > 0)
216
+ console.log(`${indent}${VSCODE_HOOKS_LINE}`);
185
217
  console.log(`${indent}VS Code Copilot runs new hooks in a new chat session.`);
186
218
  }
187
- function printCopilotUninstall(result) {
188
- const { paths, mcp } = result;
189
- if (result.hooks)
190
- console.log(`Removed hippo's Copilot hooks file ${paths.hooks}`);
219
+ /** The lines for one MCP config file; empty when hippo's server was not there. */
220
+ function mcpRemoveLines(file, mcp) {
191
221
  if (isMcpFailure(mcp))
192
- console.log(mcpFailureLine(paths.mcpConfig, mcp, 'remove the "hippo" server by hand if hippo added it'));
222
+ return [mcpFailureLine(file, mcp, 'remove the "hippo" server by hand if hippo added it')];
193
223
  if (mcp === 'removed')
194
- console.log(`Removed the "hippo" MCP server from ${paths.mcpConfig}`);
224
+ return [`Removed the "hippo" MCP server from ${file}`];
195
225
  if (mcp === 'user-owned')
196
- console.log(`Left the "hippo" MCP server in ${paths.mcpConfig}: hippo did not write it`);
226
+ return [`Left the "hippo" MCP server in ${file}: hippo did not write it`];
197
227
  if (mcp === 'unreadable')
198
- console.log(`WARNING: ${paths.mcpConfig} has comments or is not a JSON object hippo can edit, so it was left unchanged; remove the "hippo" server by hand if hippo added it`);
199
- const instructions = INSTRUCTIONS_REMOVE_LINES[result.instructions](paths.instructions);
200
- if (instructions)
201
- console.log(instructions);
202
- if (!result.hooks && mcp === 'absent' && result.instructions === 'absent')
203
- console.log('No hippo Copilot hooks, MCP server or instructions block found.');
228
+ return [`WARNING: ${file} has comments or is not a JSON object hippo can edit, so it was left unchanged; remove the "hippo" server by hand if hippo added it`];
229
+ return [];
230
+ }
231
+ function printCopilotUninstall(result) {
232
+ const { paths, mcp } = result;
233
+ const lines = [];
234
+ if (result.hooks)
235
+ lines.push(`Removed hippo's Copilot hooks file ${paths.hooks}`);
236
+ lines.push(...mcpRemoveLines(paths.mcpConfig, mcp));
237
+ lines.push(INSTRUCTIONS_REMOVE_LINES[result.instructions](paths.instructions));
238
+ for (const vscode of result.vscode) {
239
+ lines.push(...mcpRemoveLines(vscode.paths.mcpConfig, vscode.mcp));
240
+ lines.push(VSCODE_INSTRUCTIONS_REMOVE_LINES[vscode.instructions](vscode.paths.instructions));
241
+ }
242
+ const printed = lines.filter((line) => line !== '');
243
+ for (const line of printed)
244
+ console.log(line);
245
+ if (printed.length === 0)
246
+ console.log('No hippo Copilot hooks, MCP server or instructions found.');
204
247
  }
205
248
  function hookUninstall(target) {
206
249
  if (target === 'copilot')
@@ -330,8 +373,11 @@ function setupCopilot(dryRun) {
330
373
  const indent = ` ${'copilot'.padEnd(14)} `;
331
374
  if (!dryRun)
332
375
  return printCopilotInstall(installCopilot(), indent);
333
- const { hooks, mcpConfig, instructions } = copilotPaths();
334
- console.log(`[dry-run] would install hooks in ${hooks}, the MCP server in ${mcpConfig} and the hippo block in ${instructions}`);
376
+ const { hooks, mcpConfig, instructions, vscode } = copilotPaths();
377
+ const cliFiles = isCopilotCliPresent() ? `, the MCP server in ${mcpConfig} and the hippo block in ${instructions}` : '';
378
+ console.log(`[dry-run] would install hooks in ${hooks}${cliFiles}`);
379
+ for (const dir of vscode)
380
+ console.log(`[dry-run] would add the MCP server to ${dir.mcpConfig} and write ${dir.instructions}`);
335
381
  }
336
382
  function setupJsonTool(tool, dryRun) {
337
383
  if (tool.name === 'copilot')
package/dist/cli/usage.js CHANGED
@@ -258,7 +258,10 @@ export const VERB_USAGE = {
258
258
  --log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')
259
259
  --runtime copilot The payload came from a Copilot hook: use the store of its cwd and find
260
260
  the Copilot CLI session log by session id; with no --log-file, log to
261
- ~/.hippo/logs/copilot-sleep.log`],
261
+ ~/.hippo/logs/copilot-sleep.log
262
+ --turn VS Code Stop hook, after each reply: capture only the new turns, sleep
263
+ only at the auto-sleep threshold, and keep the session's snapshot; does
264
+ nothing for any other payload`],
262
265
  'pre-compact': [`
263
266
  pre-compact PreCompact hook: record the compaction, save a working-state snapshot, and
264
267
  ask the summariser to end with a "Memories for hippo" list
package/dist/cli.js CHANGED
@@ -57,7 +57,7 @@ export const BOOLEAN_FLAGS = new Set([
57
57
  'no-learn', 'no-mmr', 'no-propagate', 'no-schedule', 'no-share', 'no-summarize-older',
58
58
  'observed', 'open', 'physics', 'pin', 'pinned-only', 'reject-loser', 'rerank-utility',
59
59
  'reset-physics', 'save-baseline', 'show-cases', 'stats', 'stdin',
60
- 'strict', 'suite', 'value-aware', 'verified', 'version', 'why',
60
+ 'strict', 'suite', 'turn', 'value-aware', 'verified', 'version', 'why',
61
61
  ]);
62
62
  // Every flag some command reads. Anything else is a typo that no command would act on.
63
63
  export const KNOWN_FLAGS = new Set([
@@ -132,7 +132,8 @@ function markKeySeenWithoutMemory(hippoRoot, idempotencyKey, input) {
132
132
  }
133
133
  function rememberWithEventLog(ctx, input, idempotencyKey, opts) {
134
134
  // No `|| 'connector:github'` fallback (see rememberWithEventLog in slack/ingest.ts for rationale).
135
- const result = remember(ctx, {
135
+ const result = remember({ ...ctx, store: undefined }, // the event log row commits with the memory on hippo.db's own handle, never through a store
136
+ {
136
137
  ...opts,
137
138
  untrusted: true,
138
139
  afterWrite: (innerDb, memoryId) => {
@@ -44,7 +44,8 @@ export function handleMessageDeleted(ctx, input) {
44
44
  // Archive + event-log mark commit together via afterArchive. The hook
45
45
  // receives the same db handle the archive is using, so the INSERT lives
46
46
  // inside the SAVEPOINT.
47
- archiveRaw(ctx, memoryId, `source_deleted:slack:${input.teamId}:${input.channelId}:${input.deletedTs}`, {
47
+ archiveRaw({ ...ctx, store: undefined }, // afterArchive runs on hippo.db's own handle, never through a store
48
+ memoryId, `source_deleted:slack:${input.teamId}:${input.channelId}:${input.deletedTs}`, {
48
49
  afterArchive: (sameDb, archivedId) => {
49
50
  markEventSeen(sameDb, input.eventId, archivedId);
50
51
  },
@@ -63,7 +63,8 @@ export function ingestMessage(ctx, input) {
63
63
  function rememberWithEventLog(ctx, eventId, opts) {
64
64
  // No `|| 'connector:slack'` fallback: the caller always builds ctx with the connector subject, and with
65
65
  // an object-shaped Context.actor an OR-fallback would never fire anyway.
66
- const result = remember(ctx, {
66
+ const result = remember({ ...ctx, store: undefined }, // the event log row commits with the memory on hippo.db's own handle, never through a store
67
+ {
67
68
  ...opts,
68
69
  untrusted: true,
69
70
  afterWrite: (innerDb, memoryId) => {
package/dist/env.d.ts CHANGED
@@ -41,6 +41,8 @@ export declare function envRequireSessionScopedFreshTail(): boolean;
41
41
  export declare function envStdinWaitMs(): number | undefined;
42
42
  export declare function envLogLevel(): string;
43
43
  export declare function envPort(): string | undefined;
44
+ /** How long a client waits for a running server's /health before writing to the store directly; server-detect.ts holds the default. */
45
+ export declare function envHealthProbeMs(): number | undefined;
44
46
  export declare function envRequireAuth(): boolean;
45
47
  export declare function envV1Rps(): string | undefined;
46
48
  export declare function envApiKey(): string | undefined;
package/dist/env.js CHANGED
@@ -83,6 +83,8 @@ export function envStdinWaitMs() { return positiveInt('HIPPO_STDIN_WAIT_MS'); }
83
83
  export function envLogLevel() { return raw('HIPPO_LOG')?.trim().toLowerCase() ?? ''; }
84
84
  // Server.
85
85
  export function envPort() { return raw('HIPPO_PORT'); }
86
+ /** How long a client waits for a running server's /health before writing to the store directly; server-detect.ts holds the default. */
87
+ export function envHealthProbeMs() { return positiveInt('HIPPO_HEALTH_PROBE_MS'); }
86
88
  export function envRequireAuth() { return isOne('HIPPO_REQUIRE_AUTH'); }
87
89
  export function envV1Rps() { return raw('HIPPO_V1_RPS'); }
88
90
  export function envApiKey() { return raw('HIPPO_API_KEY'); }
@@ -1,21 +1,41 @@
1
+ import type { JsonObject } from '../working-memory.js';
1
2
  /** Copilot drops what a per-prompt hook prints, so per-task recall goes through the MCP tool the block names. */
2
3
  export declare const COPILOT_INSTRUCTIONS: string;
3
- /** The three files hippo writes for Copilot. */
4
+ export declare const VSCODE_INSTRUCTIONS: string;
5
+ /** The files hippo writes in one VS Code User folder. */
6
+ export interface VscodePaths {
7
+ readonly userDir: string;
8
+ readonly mcpConfig: string;
9
+ readonly instructions: string;
10
+ /** Profile folders, which keep their own mcp.json and prompts; hippo installs into the default profile only. */
11
+ readonly profiles: readonly string[];
12
+ }
13
+ /** The three files hippo writes for Copilot, plus the two it writes in each VS Code User folder. */
4
14
  export interface CopilotPaths {
5
15
  readonly hooks: string;
6
16
  readonly mcpConfig: string;
7
17
  readonly instructions: string;
18
+ readonly vscode: readonly VscodePaths[];
8
19
  }
9
20
  export declare function copilotPaths(): CopilotPaths;
10
- /** What to paste under "mcpServers" when hippo cannot edit the file itself. */
11
- export declare function copilotMcpSnippet(): string;
21
+ /** The app that reads an MCP config: where it keeps its servers, and hippo's entry there. */
22
+ export interface McpHost {
23
+ readonly key: 'mcpServers' | 'servers';
24
+ server(): JsonObject;
25
+ }
26
+ /** The Copilot CLI's mcp-config.json keeps servers under "mcpServers", each with an env and a tool list. */
27
+ export declare const COPILOT_CLI_MCP: McpHost;
28
+ /** VS Code's mcp.json keeps stdio servers under "servers"; it has no "mcpServers" key. */
29
+ export declare const VSCODE_MCP: McpHost;
30
+ /** What to paste under the host's key when hippo cannot edit the file itself. */
31
+ export declare function copilotMcpSnippet(host?: McpHost): string;
12
32
  export type McpMergeStatus = 'added' | 'present' | 'user-owned' | 'unreadable';
13
33
  /** Adds hippo's server and keeps every other key; a file hippo cannot parse, or a "hippo" key it did not write, stays as it is. */
14
- export declare function mergeMcpServer(file: string): McpMergeStatus;
34
+ export declare function mergeMcpServer(file: string, host?: McpHost): McpMergeStatus;
15
35
  export type McpRemoveStatus = 'removed' | 'absent' | 'user-owned' | 'unreadable';
16
36
  /** Removes the "hippo" key only when hippo wrote it. */
17
- export declare function removeMcpServer(file: string): McpRemoveStatus;
18
- /** A filesystem error on mcp-config.json (EACCES, EISDIR, EBUSY), kept as its own message so setup can report it and go on. */
37
+ export declare function removeMcpServer(file: string, host?: McpHost): McpRemoveStatus;
38
+ /** A filesystem error on an MCP config file (EACCES, EISDIR, EBUSY), kept as its own message so setup can report it and go on. */
19
39
  export interface McpFailure {
20
40
  readonly failed: string;
21
41
  }
@@ -27,19 +47,41 @@ export declare function ensureInstructionsBlock(file: string): InstructionsInsta
27
47
  export type InstructionsRemoveStatus = 'removed' | 'absent' | 'kept' | 'unclosed';
28
48
  /** Splices out only hippo's own Copilot block and keeps every other byte; a file left with nothing but whitespace goes. */
29
49
  export declare function removeInstructionsBlock(file: string): InstructionsRemoveStatus;
50
+ /** `kept`: the file is there but is not the text hippo writes, so the user edited it or it is someone else's. */
51
+ export type VscodeInstructionsInstallStatus = 'written' | 'present' | 'kept';
52
+ /** The file is hippo's alone, so it is written whole; an earlier hippo text is upgraded, and any other text already there stays. */
53
+ export declare function ensureVscodeInstructions(file: string): VscodeInstructionsInstallStatus;
54
+ export type VscodeInstructionsRemoveStatus = 'removed' | 'absent' | 'kept';
55
+ /** Deletes the file only when it still holds exactly what a hippo release wrote. */
56
+ export declare function removeVscodeInstructions(file: string): VscodeInstructionsRemoveStatus;
57
+ export interface VscodeInstallResult {
58
+ readonly paths: VscodePaths;
59
+ readonly mcp: McpMergeStatus | McpFailure;
60
+ readonly instructions: VscodeInstructionsInstallStatus;
61
+ }
30
62
  export interface CopilotInstallResult {
31
63
  readonly paths: CopilotPaths;
32
64
  /** True when the hooks file was written; false when it already held hippo's current table. */
33
65
  readonly hooks: boolean;
34
- readonly mcp: McpMergeStatus | McpFailure;
35
- readonly instructions: InstructionsInstallStatus;
66
+ /** Null, like `instructions`, when the Copilot CLI is not installed, since only it reads these two files. */
67
+ readonly mcp: McpMergeStatus | McpFailure | null;
68
+ readonly instructions: InstructionsInstallStatus | null;
69
+ readonly vscode: readonly VscodeInstallResult[];
36
70
  }
71
+ /** The CLI's folder holds more than the hooks folder setup makes for VS Code, so a rerun on a VS Code-only machine still finds no CLI. */
72
+ export declare function isCopilotCliPresent(home?: string): boolean;
37
73
  export declare function installCopilot(): CopilotInstallResult;
74
+ export interface VscodeUninstallResult {
75
+ readonly paths: VscodePaths;
76
+ readonly mcp: McpRemoveStatus | McpFailure;
77
+ readonly instructions: VscodeInstructionsRemoveStatus;
78
+ }
38
79
  export interface CopilotUninstallResult {
39
80
  readonly paths: CopilotPaths;
40
81
  readonly hooks: boolean;
41
82
  readonly mcp: McpRemoveStatus | McpFailure;
42
83
  readonly instructions: InstructionsRemoveStatus;
84
+ readonly vscode: readonly VscodeUninstallResult[];
43
85
  }
44
86
  export declare function uninstallCopilot(): CopilotUninstallResult;
45
87
  //# sourceMappingURL=copilot.d.ts.map