hippo-memory 1.63.2 → 1.64.1

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 (280) hide show
  1. package/README.md +11 -5
  2. package/dist/agent-memories/claude-code.js +3 -3
  3. package/dist/ambient-store.d.ts +2 -0
  4. package/dist/ambient-store.js +4 -3
  5. package/dist/api/assemble.d.ts +2 -0
  6. package/dist/api/assemble.js +11 -7
  7. package/dist/api/auth.d.ts +15 -11
  8. package/dist/api/auth.js +116 -22
  9. package/dist/api/context-select.d.ts +5 -3
  10. package/dist/api/context-select.js +6 -11
  11. package/dist/api/context-types.d.ts +2 -1
  12. package/dist/api/context.d.ts +1 -1
  13. package/dist/api/context.js +79 -55
  14. package/dist/api/dormant.d.ts +5 -5
  15. package/dist/api/dormant.js +13 -9
  16. package/dist/api/drill-down.d.ts +2 -0
  17. package/dist/api/drill-down.js +12 -7
  18. package/dist/api/forget.d.ts +1 -1
  19. package/dist/api/forget.js +8 -4
  20. package/dist/api/learn.js +5 -3
  21. package/dist/api/outcome.d.ts +1 -1
  22. package/dist/api/outcome.js +2 -1
  23. package/dist/api/promote.js +8 -5
  24. package/dist/api/recall-types.d.ts +4 -2
  25. package/dist/api/recall.js +51 -36
  26. package/dist/api/remember.d.ts +7 -0
  27. package/dist/api/remember.js +26 -2
  28. package/dist/api/sleep-run.d.ts +28 -0
  29. package/dist/api/sleep-run.js +267 -0
  30. package/dist/api/sleep.d.ts +5 -45
  31. package/dist/api/sleep.js +5 -252
  32. package/dist/api/types.d.ts +5 -0
  33. package/dist/api/types.js +4 -0
  34. package/dist/audit-prune.js +1 -1
  35. package/dist/audit.d.ts +1 -3
  36. package/dist/audit.js +7 -103
  37. package/dist/auth.d.ts +48 -4
  38. package/dist/auth.js +111 -42
  39. package/dist/autolearn.js +2 -2
  40. package/dist/capture/caller-session.d.ts +16 -0
  41. package/dist/capture/caller-session.js +58 -0
  42. package/dist/capture/command.d.ts +19 -1
  43. package/dist/capture/command.js +28 -21
  44. package/dist/capture/compact-resume-caller.d.ts +11 -0
  45. package/dist/capture/compact-resume-caller.js +35 -0
  46. package/dist/capture/compact.d.ts +0 -14
  47. package/dist/capture/compact.js +7 -160
  48. package/dist/capture/compaction-items-caller.d.ts +16 -0
  49. package/dist/capture/compaction-items-caller.js +32 -0
  50. package/dist/capture/extract.d.ts +2 -14
  51. package/dist/capture/extract.js +135 -346
  52. package/dist/capture/failure-caller.d.ts +21 -0
  53. package/dist/capture/failure-caller.js +74 -0
  54. package/dist/capture/failure-reading.d.ts +44 -0
  55. package/dist/capture/failure-reading.js +78 -0
  56. package/dist/capture/pre-compact-caller.d.ts +16 -0
  57. package/dist/capture/pre-compact-caller.js +39 -0
  58. package/dist/capture/session-end-caller.d.ts +21 -0
  59. package/dist/capture/session-end-caller.js +30 -0
  60. package/dist/capture/session-texts.d.ts +14 -0
  61. package/dist/capture/session-texts.js +42 -0
  62. package/dist/capture/transcript.d.ts +6 -1
  63. package/dist/capture/transcript.js +20 -16
  64. package/dist/capture/working-state.d.ts +21 -0
  65. package/dist/capture/working-state.js +154 -0
  66. package/dist/capture-error.d.ts +8 -22
  67. package/dist/capture-error.js +9 -61
  68. package/dist/cli/audit.js +25 -0
  69. package/dist/cli/auth.js +4 -3
  70. package/dist/cli/context.js +13 -158
  71. package/dist/cli/continuity.js +2 -1
  72. package/dist/cli/init.js +5 -3
  73. package/dist/cli/last-sleep.d.ts +6 -0
  74. package/dist/cli/last-sleep.js +68 -0
  75. package/dist/cli/maintenance.d.ts +2 -1
  76. package/dist/cli/maintenance.js +17 -11
  77. package/dist/cli/quality-repair-once.d.ts +3 -0
  78. package/dist/cli/quality-repair-once.js +36 -0
  79. package/dist/cli/recall.js +2 -1
  80. package/dist/cli/remember.d.ts +2 -0
  81. package/dist/cli/remember.js +4 -1
  82. package/dist/cli/session-hooks.d.ts +0 -3
  83. package/dist/cli/session-hooks.js +12 -49
  84. package/dist/cli/setup.js +10 -2
  85. package/dist/cli/shared.d.ts +7 -14
  86. package/dist/cli/shared.js +19 -78
  87. package/dist/cli/sleep.js +6 -3
  88. package/dist/cli/usage.js +18 -9
  89. package/dist/cli.d.ts +1 -1
  90. package/dist/cli.js +3 -2
  91. package/dist/client.js +1 -1
  92. package/dist/compaction-record.d.ts +19 -1
  93. package/dist/compaction-record.js +61 -132
  94. package/dist/compaction-spool.d.ts +45 -0
  95. package/dist/compaction-spool.js +454 -0
  96. package/dist/config.d.ts +8 -0
  97. package/dist/config.js +49 -0
  98. package/dist/connectors/github/deletion.d.ts +1 -1
  99. package/dist/connectors/github/deletion.js +5 -17
  100. package/dist/consolidate/conflicts.js +4 -0
  101. package/dist/consolidate/flush-units.d.ts +9 -0
  102. package/dist/consolidate/flush-units.js +63 -0
  103. package/dist/consolidate/llm-passes.js +5 -4
  104. package/dist/consolidate/merge.js +5 -1
  105. package/dist/consolidate/run.d.ts +2 -1
  106. package/dist/consolidate/sleep.d.ts +2 -0
  107. package/dist/consolidate/sleep.js +82 -15
  108. package/dist/context-render.d.ts +14 -0
  109. package/dist/context-render.js +28 -0
  110. package/dist/dag.js +6 -0
  111. package/dist/dashboard-actions.js +3 -2
  112. package/dist/dashboard.js +4 -3
  113. package/dist/db/backup.d.ts +6 -0
  114. package/dist/db/backup.js +29 -0
  115. package/dist/db/busy.d.ts +9 -0
  116. package/dist/db/busy.js +35 -0
  117. package/dist/db/continuity.d.ts +1 -0
  118. package/dist/db/continuity.js +31 -4
  119. package/dist/db/meta.d.ts +2 -0
  120. package/dist/db/meta.js +7 -0
  121. package/dist/db/migrate.js +2 -3
  122. package/dist/db/migrations/index.d.ts +1 -1
  123. package/dist/db/migrations/index.js +5 -1
  124. package/dist/db/migrations/v39.js +2 -7
  125. package/dist/db/migrations/v53.d.ts +3 -0
  126. package/dist/db/migrations/v53.js +16 -0
  127. package/dist/db/migrations/v54.d.ts +3 -0
  128. package/dist/db/migrations/v54.js +25 -0
  129. package/dist/db/open.d.ts +11 -0
  130. package/dist/db/open.js +27 -0
  131. package/dist/db/tables.d.ts +2 -0
  132. package/dist/db/tables.js +6 -2
  133. package/dist/db.d.ts +2 -2
  134. package/dist/db.js +2 -2
  135. package/dist/doctor.d.ts +1 -1
  136. package/dist/doctor.js +31 -15
  137. package/dist/dormant.d.ts +16 -20
  138. package/dist/dormant.js +24 -30
  139. package/dist/embedding-provider.js +4 -3
  140. package/dist/entry/json-hooks.d.ts +3 -0
  141. package/dist/entry/json-hooks.js +4 -0
  142. package/dist/entry/project-identity.d.ts +2 -0
  143. package/dist/entry/project-identity.js +3 -0
  144. package/dist/entry/session-text.d.ts +7 -0
  145. package/dist/entry/session-text.js +8 -0
  146. package/dist/extract.js +6 -0
  147. package/dist/failure-log.d.ts +10 -2
  148. package/dist/failure-log.js +19 -2
  149. package/dist/gated-write.js +1 -1
  150. package/dist/graph/delta.d.ts +44 -0
  151. package/dist/graph/delta.js +109 -0
  152. package/dist/graph/types.d.ts +5 -0
  153. package/dist/graph/write.d.ts +20 -20
  154. package/dist/graph/write.js +103 -36
  155. package/dist/graph-extract.d.ts +43 -9
  156. package/dist/graph-extract.js +140 -157
  157. package/dist/graph-recall.d.ts +1 -0
  158. package/dist/graph-recall.js +2 -2
  159. package/dist/graph-view.d.ts +1 -0
  160. package/dist/graph-view.js +25 -2
  161. package/dist/half-life-migration.js +5 -1
  162. package/dist/handoff-evidence.d.ts +5 -0
  163. package/dist/handoff-evidence.js +30 -0
  164. package/dist/home-path.d.ts +5 -0
  165. package/dist/home-path.js +24 -0
  166. package/dist/hooks/json-hooks.d.ts +13 -2
  167. package/dist/hooks/json-hooks.js +246 -117
  168. package/dist/hooks/opencode.js +1 -1
  169. package/dist/hooks/shared.d.ts +2 -0
  170. package/dist/hooks/shared.js +6 -1
  171. package/dist/http-util.d.ts +16 -7
  172. package/dist/http-util.js +65 -20
  173. package/dist/importers/vault.js +2 -0
  174. package/dist/index.d.ts +2 -1
  175. package/dist/index.js +1 -0
  176. package/dist/json.d.ts +2 -0
  177. package/dist/json.js +7 -0
  178. package/dist/ledger-db.d.ts +10 -0
  179. package/dist/ledger-db.js +42 -0
  180. package/dist/local-embedding.d.ts +2 -0
  181. package/dist/local-embedding.js +20 -4
  182. package/dist/mcp/admin-tools.d.ts +2 -2
  183. package/dist/mcp/admin-tools.js +47 -5
  184. package/dist/mcp/memory-tools.js +7 -2
  185. package/dist/mcp/protocol.d.ts +6 -0
  186. package/dist/mcp/protocol.js +4 -2
  187. package/dist/mcp/recall-tools.js +19 -6
  188. package/dist/mcp/request.js +4 -0
  189. package/dist/mcp/session-state.d.ts +4 -0
  190. package/dist/mcp/session-state.js +15 -6
  191. package/dist/mcp/shared-gate.d.ts +4 -0
  192. package/dist/mcp/shared-gate.js +22 -0
  193. package/dist/mcp/tools.js +1 -0
  194. package/dist/memory-quality.d.ts +35 -0
  195. package/dist/memory-quality.js +191 -0
  196. package/dist/memory.d.ts +4 -4
  197. package/dist/physics-state.js +1 -1
  198. package/dist/pilot-arm.d.ts +14 -3
  199. package/dist/pilot-arm.js +32 -10
  200. package/dist/postinstall.js +2 -2
  201. package/dist/project-briefs.js +4 -4
  202. package/dist/project-identity.d.ts +14 -10
  203. package/dist/project-identity.js +29 -11
  204. package/dist/project-merge.d.ts +0 -2
  205. package/dist/project-merge.js +9 -13
  206. package/dist/prompt-hook.d.ts +65 -0
  207. package/dist/prompt-hook.js +213 -0
  208. package/dist/prompt-recall.js +1 -1
  209. package/dist/quality-repair.d.ts +27 -0
  210. package/dist/quality-repair.js +220 -0
  211. package/dist/rate-limit.d.ts +1 -0
  212. package/dist/rate-limit.js +5 -3
  213. package/dist/raw-archive.d.ts +1 -1
  214. package/dist/raw-archive.js +6 -16
  215. package/dist/recall-scope.d.ts +28 -5
  216. package/dist/recall-scope.js +52 -7
  217. package/dist/recall-trace.js +1 -1
  218. package/dist/reject-flow.d.ts +2 -0
  219. package/dist/reject-flow.js +20 -6
  220. package/dist/secret-detect.js +7 -8
  221. package/dist/server/auth.d.ts +8 -6
  222. package/dist/server/auth.js +85 -38
  223. package/dist/server/client-ip.d.ts +2 -0
  224. package/dist/server/client-ip.js +16 -1
  225. package/dist/server/mcp-http.d.ts +9 -3
  226. package/dist/server/mcp-http.js +56 -13
  227. package/dist/server/request.d.ts +1 -1
  228. package/dist/server/request.js +5 -5
  229. package/dist/server/routes/admin.js +12 -6
  230. package/dist/server/routes/memories.js +9 -1
  231. package/dist/server/routes/recall.js +14 -2
  232. package/dist/server/types.d.ts +42 -5
  233. package/dist/server/validation.d.ts +5 -2
  234. package/dist/server/validation.js +19 -2
  235. package/dist/server.d.ts +21 -4
  236. package/dist/server.js +207 -49
  237. package/dist/session-digest.d.ts +0 -1
  238. package/dist/session-digest.js +3 -12
  239. package/dist/session-owners.d.ts +7 -0
  240. package/dist/session-owners.js +54 -0
  241. package/dist/share-scrub.d.ts +3 -0
  242. package/dist/share-scrub.js +8 -0
  243. package/dist/shared.d.ts +1 -0
  244. package/dist/shared.js +26 -18
  245. package/dist/store/candidates.d.ts +14 -3
  246. package/dist/store/candidates.js +48 -26
  247. package/dist/store/conflicts.d.ts +15 -0
  248. package/dist/store/conflicts.js +68 -44
  249. package/dist/store/delete-and-batch.d.ts +25 -1
  250. package/dist/store/delete-and-batch.js +121 -69
  251. package/dist/store/entry-reads.d.ts +5 -6
  252. package/dist/store/entry-reads.js +24 -12
  253. package/dist/store/entry-row.d.ts +7 -10
  254. package/dist/store/entry-row.js +40 -18
  255. package/dist/store/entry-writes.d.ts +3 -3
  256. package/dist/store/entry-writes.js +9 -23
  257. package/dist/store/handoffs.d.ts +9 -6
  258. package/dist/store/handoffs.js +72 -59
  259. package/dist/store/index-and-stats.js +2 -2
  260. package/dist/store/open.d.ts +3 -1
  261. package/dist/store/open.js +3 -3
  262. package/dist/store/search-rows.d.ts +8 -5
  263. package/dist/store/search-rows.js +15 -18
  264. package/dist/store/sessions.d.ts +25 -6
  265. package/dist/store/sessions.js +61 -19
  266. package/dist/store/summaries.d.ts +1 -1
  267. package/dist/store/summaries.js +3 -18
  268. package/dist/store/tenant-lookup.d.ts +7 -3
  269. package/dist/store/tenant-lookup.js +9 -9
  270. package/dist/store-port.d.ts +13 -0
  271. package/dist/store-port.js +20 -0
  272. package/dist/version.d.ts +5 -1
  273. package/dist/version.js +5 -1
  274. package/dist/working-memory.js +1 -1
  275. package/dist/write-budget.d.ts +15 -0
  276. package/dist/write-budget.js +15 -0
  277. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  278. package/extensions/openclaw-plugin/package.json +1 -1
  279. package/openclaw.plugin.json +1 -1
  280. package/package.json +8 -4
package/dist/auth.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
2
- import { openHippoDb, closeHippoDb } from './db.js';
2
+ import { raiseMinBinary } from './db/meta.js';
3
3
  import { keysetAfter } from './keyset.js';
4
+ import { EXPIRING_KEYS_MIN_BINARY } from './version.js';
4
5
  /** Every minted API key starts with this, so the server can route a bearer token by shape. */
5
6
  export const API_KEY_PREFIX = 'hk_';
6
7
  const ID_LEN = 24; // base32 chars after prefix
@@ -42,30 +43,64 @@ export function createApiKey(db, opts) {
42
43
  const hash = hashKey(plaintext);
43
44
  // openHippoDb runs runMigrations synchronously before returning the db handle,
44
45
  // so migration v26 (adds role column) is in place before this INSERT runs.
45
- db.prepare(`INSERT INTO api_keys (key_id, key_hash, tenant_id, label, created_at, role) VALUES (?, ?, ?, ?, ?, ?)`).run(keyId, hash, opts.tenantId, opts.label ?? null, new Date().toISOString(), opts.role ?? 'admin');
46
+ // An older binary ignores expires_at and would honour an expired key, so the store shuts it out before the first one exists.
47
+ if (opts.expiresAt !== undefined)
48
+ raiseMinBinary(db, EXPIRING_KEYS_MIN_BINARY);
49
+ db.prepare(`INSERT INTO api_keys (key_id, key_hash, tenant_id, label, created_at, role, owner_subject, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(keyId, hash, opts.tenantId, opts.label ?? null, new Date().toISOString(), opts.role ?? 'admin', opts.ownerSubject ?? null, opts.expiresAt ?? null);
46
50
  return { keyId, plaintext };
47
51
  }
48
- /** One full check against the store: shape, row, revocation, then scrypt. Null for any failure. */
49
- function lookupApiKey(db, plaintext) {
50
- if (!MINTED_KEY_PATTERN.test(plaintext))
51
- return null;
52
- const keyId = plaintext.slice(0, API_KEY_PREFIX.length + ID_LEN);
52
+ /** The api_keys row for `keyId` with its scope grants; null when no row matches. */
53
+ export function readApiKeyRecord(db, keyId) {
53
54
  // SAFETY: row comes from the SELECT above, which projects exactly
54
- // key_hash, tenant_id, revoked_at, role; `.get` returns undefined when no
55
+ // key_hash, tenant_id, revoked_at, role, expires_at, owner_subject; `.get` returns undefined when no
55
56
  // row matches key_id.
56
57
  const row = db
57
- .prepare(`SELECT key_hash, tenant_id, revoked_at, role FROM api_keys WHERE key_id = ?`)
58
+ .prepare(`SELECT key_hash, tenant_id, revoked_at, role, expires_at, owner_subject FROM api_keys WHERE key_id = ?`)
58
59
  .get(keyId);
60
+ if (!row)
61
+ return null;
62
+ return {
63
+ keyHash: row.key_hash, tenantId: row.tenant_id, revokedAt: row.revoked_at, role: row.role, scopes: listScopeGrants(db, keyId),
64
+ expiresAt: row.expires_at, ownerSubject: row.owner_subject,
65
+ };
66
+ }
67
+ /** When a key stops working, in epoch ms: Infinity for null, and already past for a missing field (a store that predates expiry) or a stamp that does not parse, so both fail closed. */
68
+ function keyExpiryMs(expiresAt) {
69
+ if (expiresAt === null)
70
+ return Infinity;
71
+ if (expiresAt === undefined)
72
+ return -Infinity;
73
+ const ms = Date.parse(expiresAt);
74
+ return Number.isNaN(ms) ? -Infinity : ms;
75
+ }
76
+ /** The key id of a minted-key-shaped token, else null. */
77
+ function mintedKeyId(plaintext) {
78
+ return MINTED_KEY_PATTERN.test(plaintext) ? plaintext.slice(0, API_KEY_PREFIX.length + ID_LEN) : null;
79
+ }
80
+ /** Revocation, expiry, then the secret, against a stored record. Null for any failure. */
81
+ function checkApiKey(keyId, record, now, secretMatches) {
59
82
  // Ids are 120 random bits and not secret, so padding the miss path with scrypt hid nothing and let junk tokens burn CPU.
60
- if (!row || row.revoked_at || !verifyKey(plaintext, row.key_hash))
83
+ if (!record)
84
+ return null;
85
+ const expiresAtMs = keyExpiryMs(record.expiresAt);
86
+ if (record.revokedAt || now >= expiresAtMs || !secretMatches(record.keyHash))
61
87
  return null;
62
88
  // Fail-safe to least privilege: any role value but 'admin' reads as 'member'.
63
- const role = row.role === 'admin' ? 'admin' : 'member';
64
- return { tenantId: row.tenant_id, keyId, role, scopes: listScopeGrants(db, keyId) };
89
+ const role = record.role === 'admin' ? 'admin' : 'member';
90
+ const key = { tenantId: record.tenantId, keyId, role, scopes: [...record.scopes] };
91
+ // Only a real name counts as an owner; anything else leaves the key keyed on its own id.
92
+ if (record.ownerSubject)
93
+ key.ownerSubject = record.ownerSubject;
94
+ return { key, expiresAtMs, keyHash: record.keyHash };
95
+ }
96
+ /** One full check against the store: shape, row, revocation, expiry, then scrypt. Null for any failure. */
97
+ function lookupApiKey(db, plaintext, now) {
98
+ const keyId = mintedKeyId(plaintext);
99
+ return keyId === null ? null : checkApiKey(keyId, readApiKeyRecord(db, keyId), now, (hash) => verifyKey(plaintext, hash));
65
100
  }
66
101
  export function validateApiKey(db, plaintext) {
67
- const key = lookupApiKey(db, plaintext);
68
- return key ? { valid: true, ...key } : { valid: false };
102
+ const found = lookupApiKey(db, plaintext, Date.now());
103
+ return found ? { valid: true, ...found.key } : { valid: false };
69
104
  }
70
105
  /** How long a verified key is trusted without a store read; also the ceiling on a revoke made by another process. */
71
106
  export const VERIFIED_KEY_TTL_MS = 60_000;
@@ -79,6 +114,7 @@ export class VerifiedKeyCache {
79
114
  ttlMs;
80
115
  // Map iterates in insertion order, so re-inserting on a hit makes the first key the least recent.
81
116
  entries = new Map();
117
+ deletes = 0;
82
118
  constructor(capacity, ttlMs) {
83
119
  this.capacity = capacity;
84
120
  this.ttlMs = ttlMs;
@@ -86,58 +122,70 @@ export class VerifiedKeyCache {
86
122
  get size() {
87
123
  return this.entries.size;
88
124
  }
89
- get(hippoRoot, keyId, plaintext, now) {
125
+ /** Moves on every delete, so a store read that straddled a revoke or grant is not cached. */
126
+ get epoch() {
127
+ return this.deletes;
128
+ }
129
+ /** The entry this exact token was verified into, fresh or lapsed. */
130
+ match(hippoRoot, keyId, plaintext) {
90
131
  const entry = this.entries.get(keyId);
91
132
  if (!entry || entry.hippoRoot !== hippoRoot)
92
133
  return undefined;
93
- if (now >= entry.expiresAt) {
94
- this.entries.delete(keyId);
95
- return undefined;
96
- }
97
134
  // A wrong secret misses but leaves the entry, so a flood of bad guesses cannot evict a good key.
98
- if (!timingSafeEqual(entry.digest, secretDigest(plaintext)))
135
+ return timingSafeEqual(entry.digest, secretDigest(plaintext)) ? entry : undefined;
136
+ }
137
+ get(hippoRoot, keyId, plaintext, now) {
138
+ const entry = this.match(hippoRoot, keyId, plaintext);
139
+ if (!entry || now >= entry.expiresAt)
99
140
  return undefined;
100
141
  this.entries.delete(keyId);
101
142
  this.entries.set(keyId, entry);
102
143
  return { ...entry.key, scopes: [...entry.key.scopes] };
103
144
  }
104
- set(hippoRoot, keyId, plaintext, key, now) {
145
+ /** The key hash this exact token was last verified against, kept past the TTL so a re-check can skip scrypt. */
146
+ verifiedHash(hippoRoot, keyId, plaintext) {
147
+ return this.match(hippoRoot, keyId, plaintext)?.keyHash;
148
+ }
149
+ set(hippoRoot, keyId, plaintext, found, now) {
105
150
  this.entries.delete(keyId);
106
151
  if (this.entries.size >= this.capacity) {
107
152
  const oldest = this.entries.keys().next();
108
153
  if (!oldest.done)
109
154
  this.entries.delete(oldest.value);
110
155
  }
111
- const frozen = Object.freeze({ ...key, scopes: [...key.scopes] });
112
- this.entries.set(keyId, { hippoRoot, digest: secretDigest(plaintext), key: frozen, expiresAt: now + this.ttlMs });
156
+ const frozen = Object.freeze({ ...found.key, scopes: [...found.key.scopes] });
157
+ // A key that expires inside the TTL leaves the cache at its expiry, so the cache never extends its life.
158
+ const expiresAt = Math.min(now + this.ttlMs, found.expiresAtMs);
159
+ this.entries.set(keyId, { hippoRoot, digest: secretDigest(plaintext), key: frozen, keyHash: found.keyHash, expiresAt });
113
160
  }
114
161
  delete(keyId) {
162
+ this.deletes++;
115
163
  this.entries.delete(keyId);
116
164
  }
117
165
  }
118
166
  // SHORTCUT: per-process cache, so a revoke or scope change made by another process (the CLI) lands within VERIFIED_KEY_TTL_MS; a shared revocation epoch in the store if that is too slow.
119
167
  const verifiedKeys = new VerifiedKeyCache(VERIFIED_KEY_CACHE_CAP, VERIFIED_KEY_TTL_MS);
120
- /** Verify a bearer API key for `hippoRoot`; a cache hit skips both scrypt and the DB open. Null when invalid. */
121
- export function verifyApiKeyCached(hippoRoot, plaintext) {
122
- if (!MINTED_KEY_PATTERN.test(plaintext))
168
+ /** Verify a bearer API key against `store`, served at `hippoRoot`; a cache hit skips both scrypt and the store. Null when invalid. A token whose cache entry lapsed re-reads its record without scrypt; a throw from `beforeScrypt` refuses before scrypt runs. */
169
+ export async function verifyApiKeyCached(hippoRoot, plaintext, store, beforeScrypt) {
170
+ const keyId = mintedKeyId(plaintext);
171
+ if (keyId === null)
123
172
  return null;
124
- const keyId = plaintext.slice(0, API_KEY_PREFIX.length + ID_LEN);
125
173
  const hit = verifiedKeys.get(hippoRoot, keyId, plaintext, Date.now());
126
174
  if (hit)
127
175
  return hit;
176
+ const provenHash = verifiedKeys.verifiedHash(hippoRoot, keyId, plaintext);
128
177
  verifyStats.storeLookups++;
129
- const db = openHippoDb(hippoRoot);
130
- let key;
131
- try {
132
- key = lookupApiKey(db, plaintext);
133
- }
134
- finally {
135
- closeHippoDb(db);
136
- }
178
+ const epoch = verifiedKeys.epoch;
179
+ const found = checkApiKey(keyId, await store.findApiKey(keyId), Date.now(), (hash) => {
180
+ if (hash === provenHash)
181
+ return true;
182
+ beforeScrypt?.();
183
+ return verifyKey(plaintext, hash);
184
+ });
137
185
  // Only successes are cached: caching misses would let junk tokens fill the cache and evict real keys.
138
- if (key)
139
- verifiedKeys.set(hippoRoot, keyId, plaintext, key, Date.now());
140
- return key;
186
+ if (found && verifiedKeys.epoch === epoch)
187
+ verifiedKeys.set(hippoRoot, keyId, plaintext, found, Date.now());
188
+ return found?.key ?? null;
141
189
  }
142
190
  export function revokeApiKey(db, keyId) {
143
191
  db.prepare(`UPDATE api_keys SET revoked_at = ? WHERE key_id = ? AND revoked_at IS NULL`)
@@ -167,19 +215,30 @@ export function listScopeGrants(db, keyId) {
167
215
  export function listApiKeyRows(db, opts) {
168
216
  const where = ['1 = 1'];
169
217
  const params = [];
170
- if (opts.active)
171
- where.push('revoked_at IS NULL');
218
+ if (opts.active) {
219
+ // In SQL so a page holds `limit` usable keys; toISOString stamps compare correctly as strings.
220
+ where.push('revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)');
221
+ params.push(new Date().toISOString());
222
+ }
172
223
  if (opts.tenantId !== undefined) {
173
224
  where.push('tenant_id = ?');
174
225
  params.push(opts.tenantId);
175
226
  }
227
+ if (opts.ownerSubject !== undefined) {
228
+ where.push('owner_subject = ?');
229
+ params.push(opts.ownerSubject);
230
+ }
231
+ if (opts.keyId !== undefined) {
232
+ where.push('key_id = ?');
233
+ params.push(opts.keyId);
234
+ }
176
235
  const after = keysetAfter('id', 'id', opts.after);
177
236
  params.push(...after.params);
178
237
  const limitSql = opts.limit === undefined ? '' : ' LIMIT ?';
179
238
  if (opts.limit !== undefined)
180
239
  params.push(opts.limit);
181
- const sql = `SELECT id, key_id, tenant_id, label, created_at, revoked_at, role FROM api_keys WHERE ${where.join(' AND ')}${after.sql} ORDER BY id DESC${limitSql}`;
182
- // SAFETY: the SELECT above projects exactly these 7 columns, in this order.
240
+ const sql = `SELECT id, key_id, tenant_id, label, created_at, revoked_at, role, expires_at, owner_subject FROM api_keys WHERE ${where.join(' AND ')}${after.sql} ORDER BY id DESC${limitSql}`;
241
+ // SAFETY: the SELECT above projects exactly these 9 columns, in this order.
183
242
  const rows = db.prepare(sql).all(...params);
184
243
  return rows.map(r => ({
185
244
  rowId: r.id,
@@ -188,10 +247,20 @@ export function listApiKeyRows(db, opts) {
188
247
  createdAt: r.created_at, revokedAt: r.revoked_at,
189
248
  role: r.role === 'admin' ? 'admin' : 'member',
190
249
  scopes: listScopeGrants(db, r.key_id),
250
+ expiresAt: r.expires_at, ownerSubject: r.owner_subject,
191
251
  },
192
252
  }));
193
253
  }
194
254
  export function listApiKeys(db, opts) {
195
255
  return listApiKeyRows(db, opts).map(r => r.key);
196
256
  }
257
+ /** Ids of the unrevoked, unexpired keys `ownerSubject` minted in `tenantId`, oldest first. */
258
+ export function listLiveOwnedKeyIds(db, tenantId, ownerSubject, now) {
259
+ // SAFETY: the SELECT names exactly these two columns.
260
+ const rows = db
261
+ .prepare(`SELECT key_id, expires_at FROM api_keys WHERE tenant_id = ? AND owner_subject = ? AND revoked_at IS NULL ORDER BY id`)
262
+ .all(tenantId, ownerSubject);
263
+ // Expiry is filtered here, not in SQL, so it follows the same Date.parse rule the key check uses.
264
+ return rows.filter((r) => now < keyExpiryMs(r.expires_at)).map((r) => r.key_id);
265
+ }
197
266
  //# sourceMappingURL=auth.js.map
package/dist/autolearn.js CHANGED
@@ -6,7 +6,7 @@ import { execSync, execFileSync, spawn } from 'child_process';
6
6
  import { createMemory, Layer, DEFAULT_HALF_LIFE_DAYS } from './memory.js';
7
7
  import { loadAllEntries } from './store/entry-reads.js';
8
8
  import { textOverlap } from './tokenize.js';
9
- import { isContentWorthStoring } from './audit.js';
9
+ import { assessAutomaticMemory } from './memory-quality.js';
10
10
  import { redactSecretsStrict } from './secret-detect.js';
11
11
  import { log } from './log.js';
12
12
  /** A memory of a failed command, "Command '<cmd>' failed: <truncated stderr>"; no store is in reach, so `hippo watch` re-derives its half-life from the store's config. */
@@ -87,7 +87,7 @@ export function partitionLessons(lessons) {
87
87
  const kept = [];
88
88
  const dropped = [];
89
89
  for (const lesson of lessons.map((l) => redactSecretsStrict(l))) {
90
- if (isContentWorthStoring(lesson)) {
90
+ if (assessAutomaticMemory(lesson).accepted) {
91
91
  kept.push(lesson);
92
92
  }
93
93
  else {
@@ -0,0 +1,16 @@
1
+ import { type Context } from '../api/types.js';
2
+ import { type DatabaseSyncLike } from '../db.js';
3
+ import { type CallerProject } from '../prompt-hook.js';
4
+ import type { ContinuityKey } from '../store/sessions.js';
5
+ import { type WorkingState } from './working-state.js';
6
+ /** Checks the ids, then binds the session to the caller's owner (a ConflictError when another owner holds it). Returns the key the call's rows go under. */
7
+ export declare function bindCaller(ctx: Context, sessionId: string, project: CallerProject): ContinuityKey;
8
+ /** Read only: the session's first prompt booked its arm, and a holdout session gets nothing from hippo. */
9
+ export declare function callerInHoldout(ctx: Context, sessionId: string): boolean;
10
+ /** Refuses a field past the cap a transcript read gives, naming it; scrubbed again, since the caller's scrub is not trusted, then cut back to the cap. */
11
+ export declare function checkedWorkingState(state: WorkingState | null): WorkingState | null;
12
+ export declare function assertTrigger(trigger: string | null): void;
13
+ export declare function assertRequestId(requestId: string): void;
14
+ /** Opens with the request's own lock wait, so a busy store becomes a 503 rather than a long stall. */
15
+ export declare function withCallerDb<T>(ctx: Context, fn: (db: DatabaseSyncLike) => T): T;
16
+ //# sourceMappingURL=caller-session.d.ts.map
@@ -0,0 +1,58 @@
1
+ // The checks every caller call shares: ids, project, the session's owner and the pilot arm, all from the request, never the server's env or folder.
2
+ import { BadRequestError } from '../api-errors.js';
3
+ import { ownerOrSubject } from '../api/types.js';
4
+ import { closeHippoDb, openHippoDb } from '../db.js';
5
+ import { MAX_ID_LEN } from '../http-util.js';
6
+ import { sessionPilotArm } from '../pilot-arm.js';
7
+ import { assertCallerIds } from '../prompt-hook.js';
8
+ import { assertCallerProject, projectNames } from '../project-identity.js';
9
+ import { maskEmails, redactSecretsStrict } from '../secret-detect.js';
10
+ import { bindSessionOwner } from '../session-owners.js';
11
+ import { fitWorkingState, WORKING_STATE_CAPS } from './working-state.js';
12
+ /** Checks the ids, then binds the session to the caller's owner (a ConflictError when another owner holds it). Returns the key the call's rows go under. */
13
+ export function bindCaller(ctx, sessionId, project) {
14
+ assertCallerIds(sessionId, project);
15
+ if (sessionId.trim() === '')
16
+ throw new BadRequestError('session id: required');
17
+ // A '' project would stamp user-global rows; a rewritten one would split a project.
18
+ assertCallerProject(project);
19
+ bindSessionOwner(ctx, sessionId);
20
+ return { owner: ownerOrSubject(ctx.actor), project: projectNames(project) };
21
+ }
22
+ /** Read only: the session's first prompt booked its arm, and a holdout session gets nothing from hippo. */
23
+ export function callerInHoldout(ctx, sessionId) {
24
+ return sessionPilotArm(ctx.hippoRoot, ctx.tenantId, sessionId, false, { sharedStore: true, ownTenantOnly: true }) === 'holdout';
25
+ }
26
+ /** Refuses a field past the cap a transcript read gives, naming it; scrubbed again, since the caller's scrub is not trusted, then cut back to the cap. */
27
+ export function checkedWorkingState(state) {
28
+ if (state === null)
29
+ return null;
30
+ for (const field of ['task', 'summary', 'next_step']) {
31
+ const cap = WORKING_STATE_CAPS[field];
32
+ if (state[field].length > cap)
33
+ throw new BadRequestError(`working state ${field}: at most ${cap} characters`);
34
+ }
35
+ const scrub = (text) => maskEmails(redactSecretsStrict(text));
36
+ return fitWorkingState({ task: scrub(state.task), summary: scrub(state.summary), next_step: scrub(state.next_step) });
37
+ }
38
+ export function assertTrigger(trigger) {
39
+ if (trigger !== null && trigger.length > MAX_ID_LEN)
40
+ throw new BadRequestError(`trigger: at most ${MAX_ID_LEN} characters`);
41
+ }
42
+ export function assertRequestId(requestId) {
43
+ if (requestId.trim() === '')
44
+ throw new BadRequestError('request id: required');
45
+ if (requestId.length > MAX_ID_LEN)
46
+ throw new BadRequestError(`request id: at most ${MAX_ID_LEN} characters`);
47
+ }
48
+ /** Opens with the request's own lock wait, so a busy store becomes a 503 rather than a long stall. */
49
+ export function withCallerDb(ctx, fn) {
50
+ const db = openHippoDb(ctx.hippoRoot);
51
+ try {
52
+ return fn(db);
53
+ }
54
+ finally {
55
+ closeHippoDb(db);
56
+ }
57
+ }
58
+ //# sourceMappingURL=caller-session.js.map
@@ -1,10 +1,11 @@
1
1
  import { type ProjectRef } from '../project-identity.js';
2
+ import { type ExtractedItem } from './extract.js';
2
3
  import { type SessionTurn } from './transcript.js';
3
4
  export interface CaptureOptions {
4
5
  source: 'stdin' | 'file' | 'last-session';
5
6
  filePath?: string;
6
7
  /** Explicit transcript path for `--last-session`. Without one, `stdinText`
7
- * is used, then auto-discovery under `~/.claude/projects/` on a manual run. */
8
+ * is used, then auto-discovery under `<claude config dir>/projects/` on a manual run. */
8
9
  transcriptPath?: string;
9
10
  /** Read from stdin by the caller (cli.ts), which owns the bounded wait.
10
11
  * `stdinTimedOut` marks an empty read "unknown", not "no payload". */
@@ -31,4 +32,21 @@ export interface CaptureOptions {
31
32
  originProject?: ProjectRef;
32
33
  }
33
34
  export declare function cmdCapture(hippoRoot: string, options: CaptureOptions): void;
35
+ export interface CaptureTally {
36
+ captured: number;
37
+ skipped: number;
38
+ rejected: number;
39
+ }
40
+ /** What a capture's rows carry. The CLI passes no tenant for the global store, so `global` never reaches the writes. */
41
+ export interface CaptureWriteOptions {
42
+ readonly dryRun: boolean;
43
+ readonly tenantId: string | undefined;
44
+ readonly originProject: ProjectRef | undefined;
45
+ readonly sessionId?: string;
46
+ /** The audit actor; the CLI's own writes leave it out and audit as `cli`. */
47
+ readonly actor?: string;
48
+ /** No stats and no embedding, since embedding runs a model in a server process. */
49
+ readonly lean: boolean;
50
+ }
51
+ export declare function captureExtractedItems(targetRoot: string, options: CaptureWriteOptions, extracted: readonly ExtractedItem[], keys: Set<string>): CaptureTally;
34
52
  //# sourceMappingURL=command.d.ts.map
@@ -17,8 +17,8 @@ import { loadConfig } from '../config.js';
17
17
  import { classifyOriginProject, projectId } from '../project-identity.js';
18
18
  import { isStringValue } from '../capture-contract.js';
19
19
  import { errorMessage, log } from '../log.js';
20
- import { extractFromText } from './extract.js';
21
- import { collectSessionTurns, summariseSessionTurns, resolveLastSessionTranscript } from './transcript.js';
20
+ import { extractFromTexts } from './extract.js';
21
+ import { collectSessionTurns, sessionTail, resolveLastSessionTranscript } from './transcript.js';
22
22
  export function cmdCapture(hippoRoot, options) {
23
23
  // Tee stdout/stderr to a log file when --log-file is set. Used by the
24
24
  // SessionEnd hook so output (otherwise swallowed by TUI teardown) surfaces
@@ -102,16 +102,15 @@ function cmdCaptureCore(hippoRoot, options) {
102
102
  process.exit(1);
103
103
  }
104
104
  }
105
- // Read input text
106
- const text = readCaptureText(options);
107
- if (text === null)
105
+ const texts = readCaptureTexts(options);
106
+ if (texts === null)
108
107
  return;
109
- if (!text || text.trim().length === 0) {
108
+ if (texts.every((text) => text.trim().length === 0)) {
110
109
  console.log('No text to capture from.');
111
110
  return;
112
111
  }
113
112
  // Scrub once here, as the snapshot fields are: every source can carry a pasted token (AGENTS.md: no secrets in memories).
114
- const extracted = extractFromText(maskEmails(redactSecretsStrict(text)));
113
+ const extracted = extractFromTexts(texts.map((text) => maskEmails(redactSecretsStrict(text))));
115
114
  if (extracted.length === 0) {
116
115
  console.log('No actionable items found in the input.');
117
116
  return;
@@ -123,19 +122,25 @@ function cmdCaptureCore(hippoRoot, options) {
123
122
  const keys = storedTextKeys(origin === undefined
124
123
  ? stored
125
124
  : stored.filter((e) => classifyOriginProject(e.origin_project, origin) !== 'cross-project'));
126
- const { captured, skipped, rejected } = captureExtractedItems(targetRoot, options, extracted, keys);
125
+ const writeOpts = {
126
+ dryRun: options.dryRun,
127
+ tenantId: useGlobal ? undefined : options.tenantId,
128
+ originProject: origin,
129
+ lean: false,
130
+ };
131
+ const { captured, skipped, rejected } = captureExtractedItems(targetRoot, writeOpts, extracted, keys);
127
132
  const prefix = options.dryRun ? '[dry-run] ' : '';
128
133
  const globalPrefix = useGlobal ? '[global] ' : '';
129
134
  console.log(`\n${prefix}${globalPrefix}Captured ${captured} items (${skipped} skipped as duplicates` +
130
135
  (rejected > 0 ? `, ${rejected} rejected` : '') +
131
136
  ')');
132
137
  }
133
- /** The raw text for the chosen source; null after printing why a last-session capture has nothing. */
134
- function readCaptureText(options) {
138
+ /** The raw texts for the chosen source, one per session turn; null after printing why a last-session capture has nothing. */
139
+ function readCaptureTexts(options) {
135
140
  switch (options.source) {
136
141
  case 'stdin': {
137
142
  try {
138
- return fs.readFileSync(0, 'utf8');
143
+ return [fs.readFileSync(0, 'utf8')];
139
144
  }
140
145
  catch {
141
146
  console.error('No input on stdin. Pipe text in or use --file <path>.');
@@ -151,7 +156,7 @@ function readCaptureText(options) {
151
156
  console.error(`File not found: ${options.filePath}`);
152
157
  process.exit(1);
153
158
  }
154
- return fs.readFileSync(options.filePath, 'utf8');
159
+ return [fs.readFileSync(options.filePath, 'utf8')];
155
160
  }
156
161
  case 'last-session': {
157
162
  let turns = options.sessionTurns;
@@ -163,16 +168,16 @@ function readCaptureText(options) {
163
168
  }
164
169
  turns = collectSessionTurns(fs.readFileSync(resolved, 'utf8'));
165
170
  }
166
- const text = summariseSessionTurns(turns);
167
- if (!text) {
171
+ const { users, assistants } = sessionTail(turns);
172
+ if (users.length === 0 && assistants.length === 0) {
168
173
  console.log('Transcript had no user/assistant messages to summarise.');
169
174
  return null;
170
175
  }
171
- return text;
176
+ return [...users, ...assistants];
172
177
  }
173
178
  }
174
179
  }
175
- function captureExtractedItems(targetRoot, options, extracted, keys) {
180
+ export function captureExtractedItems(targetRoot, options, extracted, keys) {
176
181
  const tally = { captured: 0, skipped: 0, rejected: 0 };
177
182
  const baseHalfLifeDays = loadConfig(targetRoot).defaultHalfLifeDays;
178
183
  // Dry run probes the same checkRejectionGuard the real write uses, on a read-only handle,
@@ -201,7 +206,6 @@ function captureExtractedItems(targetRoot, options, extracted, keys) {
201
206
  return tally;
202
207
  }
203
208
  function captureEntry(item, options, baseHalfLifeDays) {
204
- const useGlobal = options.global;
205
209
  // kind stays 'distilled': these are curated items, not raw transcript (see MEMORY_ENVELOPE.md).
206
210
  // The write tenant must match the dedup read's, or scoped dedup passes and the row lands in 'default'.
207
211
  const created = createMemory(item.content, {
@@ -209,7 +213,8 @@ function captureEntry(item, options, baseHalfLifeDays) {
209
213
  tags: item.tags,
210
214
  source: 'capture',
211
215
  confidence: 'observed',
212
- tenantId: useGlobal ? undefined : options.tenantId,
216
+ tenantId: options.tenantId,
217
+ source_session_id: options.sessionId,
213
218
  baseHalfLifeDays,
214
219
  });
215
220
  return options.originProject === undefined ? created : { ...created, origin_project: projectId(options.originProject) };
@@ -235,15 +240,17 @@ function captureOne(ctx, item, entry) {
235
240
  else if (writeDb !== null) {
236
241
  // One rejected item must not abort the rest of this capture's items.
237
242
  const stamped = stampOriginProject(targetRoot, entry);
238
- const outcome = gatedWrite(writeDb, targetRoot, stamped);
243
+ const outcome = gatedWrite(writeDb, targetRoot, stamped, { actor: options.actor });
239
244
  if (outcome === 'skipped:rejected')
240
245
  return 'rejected';
241
246
  if (outcome !== 'written')
242
247
  return 'skipped';
243
248
  writeEntryMirrors(targetRoot, stamped);
244
- updateStats(targetRoot, { remembered: 1 });
249
+ if (!options.lean)
250
+ updateStats(targetRoot, { remembered: 1 });
245
251
  keys.add(duplicateKey(item.content)); // within-batch dedup
246
- void embedMemory(targetRoot, entry);
252
+ if (!options.lean)
253
+ void embedMemory(targetRoot, entry);
247
254
  }
248
255
  return 'captured';
249
256
  }
@@ -0,0 +1,11 @@
1
+ import type { Context } from '../api/types.js';
2
+ import type { CallerProject } from '../prompt-hook.js';
3
+ import type { CallerHookOutput } from './pre-compact-caller.js';
4
+ export interface CallerCompactResumeRequest {
5
+ readonly sessionId: string;
6
+ readonly project: CallerProject;
7
+ /** The SessionStart payload's source; only 'compact' restores. */
8
+ readonly source: string;
9
+ }
10
+ export declare function compactResumeForCaller(ctx: Context, req: CallerCompactResumeRequest): CallerHookOutput;
11
+ //# sourceMappingURL=compact-resume-caller.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { COMPACT_RESUME_EVENT_CONTENT_CAP, COMPACT_RESUME_MAX_AGE_MS, compactResumeText } from '../context-render.js';
2
+ import { withLedgerDb } from '../ledger-db.js';
3
+ import { errorMessage, log } from '../log.js';
4
+ import { listSessionEvents, loadFreshActiveTaskSnapshot } from '../store/sessions.js';
5
+ import { estimateTokens, recordTokenUse } from '../token-ledger.js';
6
+ import { truncateCodePointSafe } from '../transcript-tail.js';
7
+ import { bindCaller, callerInHoldout } from './caller-session.js';
8
+ export function compactResumeForCaller(ctx, req) {
9
+ const key = bindCaller(ctx, req.sessionId, req.project);
10
+ if (req.source !== 'compact' || callerInHoldout(ctx, req.sessionId))
11
+ return { stdout: '' };
12
+ const snapshot = loadFreshActiveTaskSnapshot(ctx.hippoRoot, ctx.tenantId, { maxAgeMs: COMPACT_RESUME_MAX_AGE_MS }, key);
13
+ // One owner's concurrent sessions must not cross-restore; a snapshot with no session still restores, as on the local path.
14
+ if (snapshot === null || (snapshot.session_id !== null && snapshot.session_id !== req.sessionId))
15
+ return { stdout: '' };
16
+ const text = compactResumeText(snapshot, sessionTrail(ctx, snapshot.session_id));
17
+ withLedgerDb(ctx.hippoRoot, (db) => recordTokenUse(db, {
18
+ tenantId: ctx.tenantId, sessionId: req.sessionId, surface: 'compact_resume', event: 'inject', items: 1, tokens: estimateTokens(text),
19
+ }), { sharedStore: true });
20
+ return { stdout: text };
21
+ }
22
+ /** A bad trail row costs the trail, not the snapshot. */
23
+ function sessionTrail(ctx, sessionId) {
24
+ if (!sessionId)
25
+ return [];
26
+ try {
27
+ return listSessionEvents(ctx.hippoRoot, ctx.tenantId, { session_id: sessionId })
28
+ .map((e) => ({ ...e, content: truncateCodePointSafe(e.content, COMPACT_RESUME_EVENT_CONTENT_CAP) }));
29
+ }
30
+ catch (err) {
31
+ log.warn(`compact-resume: trail skipped: ${errorMessage(err)}`);
32
+ return [];
33
+ }
34
+ }
35
+ //# sourceMappingURL=compact-resume-caller.js.map
@@ -1,15 +1,3 @@
1
- import { type TaskSnapshot } from '../store/rows.js';
2
- export declare const PRE_COMPACT_TASK_CAP = 200;
3
- export declare const PRE_COMPACT_SUMMARY_CAP = 2000;
4
- export declare const PRE_COMPACT_NEXT_STEP_CAP = 500;
5
- /**
6
- * Cap from the RECENT end: summariseTranscript emits user turns oldest-to-
7
- * newest with assistant responses after them, so a head-first cap keeps
8
- * stale context and drops exactly the newest working state this feature
9
- * exists to preserve. Keep the LAST maxChars instead, aligned forward to a
10
- * nearby line start, with a trim marker.
11
- */
12
- export declare function truncateKeepNewest(text: string, maxChars: number): string;
13
1
  /**
14
2
  * Log-forgery guard: messages here interpolate payload-controlled values
15
3
  * (transcript paths, session ids). Strip C0 control chars — newlines above
@@ -19,8 +7,6 @@ export declare function truncateKeepNewest(text: string, maxChars: number): stri
19
7
  * guard, not per-file copies.
20
8
  */
21
9
  export declare function sanitizeLogMessage(message: string): string;
22
- /** A session's task, summary and next step from its transcript tail, secrets scrubbed and capped, '' where none; null with a logged reason when nothing is derivable. */
23
- export declare function transcriptWorkingState(transcriptPath: string, log: (message: string) => void): Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null;
24
10
  export interface PreCompactOptions {
25
11
  stdinText?: string;
26
12
  stdinTimedOut?: boolean;