hippo-memory 1.72.0 → 2.0.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 +6 -4
- package/dist/agent-memories/apply.js +5 -5
- package/dist/api/assemble.js +2 -1
- package/dist/api/audit.d.ts +7 -7
- package/dist/api/audit.js +19 -17
- package/dist/api/auth.d.ts +5 -6
- package/dist/api/auth.js +28 -33
- package/dist/api/conflicts.js +1 -1
- package/dist/api/context-auto.js +4 -2
- package/dist/api/context-select.js +8 -20
- package/dist/api/context.js +12 -10
- package/dist/api/drill-down.js +2 -1
- package/dist/api/forget.d.ts +2 -2
- package/dist/api/forget.js +6 -7
- package/dist/api/graph.js +2 -1
- package/dist/api/learn.d.ts +1 -1
- package/dist/api/learn.js +45 -44
- package/dist/api/memories.d.ts +4 -2
- package/dist/api/memories.js +16 -5
- package/dist/api/objects.js +2 -1
- package/dist/api/outcome.d.ts +5 -5
- package/dist/api/outcome.js +16 -15
- package/dist/api/predictions.js +2 -1
- package/dist/api/promote.d.ts +3 -3
- package/dist/api/promote.js +25 -27
- package/dist/api/prompt-hook.d.ts +3 -1
- package/dist/api/prompt-hook.js +1 -0
- package/dist/api/quarantine.js +2 -1
- package/dist/api/recall-core.js +8 -3
- package/dist/api/recall-pipeline.js +17 -17
- package/dist/api/recall-types.d.ts +3 -1
- package/dist/api/recall.js +32 -29
- package/dist/api/remember-local.d.ts +5 -2
- package/dist/api/remember-local.js +14 -6
- package/dist/api/remember.d.ts +5 -2
- package/dist/api/remember.js +16 -14
- package/dist/api/session-end.d.ts +24 -0
- package/dist/api/session-end.js +31 -1
- package/dist/api/sleep-run.js +5 -4
- package/dist/api/sleep.js +4 -3
- package/dist/api/tokens.js +1 -1
- package/dist/api/types.d.ts +1 -7
- package/dist/capture/command.js +1 -1
- package/dist/capture/compaction-spool.d.ts +3 -5
- package/dist/capture/compaction-spool.js +41 -46
- package/dist/capture/session-end-caller.js +1 -1
- package/dist/capture/session-worker.js +19 -6
- package/dist/cli/audit.js +10 -10
- package/dist/cli/auth.js +4 -4
- package/dist/cli/client.d.ts +1 -2
- package/dist/cli/client.js +13 -13
- package/dist/cli/context.js +1 -1
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +9 -9
- package/dist/cli/decisions.js +4 -5
- package/dist/cli/hook-runtime.d.ts +3 -1
- package/dist/cli/hook-runtime.js +2 -2
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +9 -9
- package/dist/cli/object-verbs.d.ts +2 -2
- package/dist/cli/object-verbs.js +4 -4
- package/dist/cli/predictions.js +2 -3
- package/dist/cli/quality-repair-once.d.ts +1 -1
- package/dist/cli/quality-repair-once.js +2 -2
- package/dist/cli/refine-llm.js +2 -1
- package/dist/cli/remember.js +5 -5
- package/dist/cli/serve.js +9 -4
- package/dist/cli/session-hooks.d.ts +0 -4
- package/dist/cli/session-hooks.js +57 -66
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +2 -2
- package/dist/cli/shared.d.ts +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli/sleep.js +2 -2
- package/dist/cli/transfer.d.ts +3 -3
- package/dist/cli/transfer.js +34 -15
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +1 -1
- package/dist/cli/verb-row.d.ts +2 -0
- package/dist/cli/verbs/health.js +2 -0
- package/dist/cli/verbs/memory.js +43 -51
- package/dist/cli/verbs/objects.js +6 -3
- package/dist/cli/verbs/sessions.js +4 -2
- package/dist/cli.js +1 -2
- package/dist/connectors/dlq.js +2 -1
- package/dist/connectors/github/deletion.js +2 -1
- package/dist/connectors/github/ingest.js +2 -1
- package/dist/connectors/github/tenant-routing.js +2 -1
- package/dist/connectors/github/webhook.js +4 -2
- package/dist/connectors/ingest.js +2 -1
- package/dist/connectors/slack/deletion.js +2 -1
- package/dist/connectors/slack/ingest.js +2 -1
- package/dist/connectors/slack/tenant-routing.js +2 -1
- package/dist/consolidate/conflicts.js +1 -1
- package/dist/consolidate/dag.js +14 -9
- package/dist/consolidate/dedupe.js +1 -1
- package/dist/consolidate/memory-value.js +1 -1
- package/dist/consolidate/merge.js +6 -6
- package/dist/consolidate/sleep.js +1 -1
- package/dist/consolidate/traces.js +2 -2
- package/dist/core/config.js +15 -16
- package/dist/core/markdown.js +2 -2
- package/dist/core/memory.d.ts +4 -4
- package/dist/core/memory.js +10 -5
- package/dist/core/physics.d.ts +3 -1
- package/dist/core/physics.js +17 -2
- package/dist/core/project-identity.js +2 -1
- package/dist/core/search-types.d.ts +1 -1
- package/dist/dashboard/dashboard-actions.d.ts +2 -2
- package/dist/dashboard/dashboard-actions.js +5 -5
- package/dist/dashboard/dashboard-queries.js +2 -7
- package/dist/dashboard/dashboard-snapshot.js +1 -3
- package/dist/dashboard/dashboard.js +16 -5
- package/dist/db/connect.d.ts +2 -1
- package/dist/db/connect.js +2 -1
- package/dist/db/migrate.js +14 -7
- package/dist/db/migrations/v11.js +1 -0
- package/dist/db/migrations/v14.js +1 -0
- package/dist/db/physics-state.d.ts +0 -4
- package/dist/db/physics-state.js +5 -31
- package/dist/db/request-stores.d.ts +1 -0
- package/dist/db/request-stores.js +2 -1
- package/dist/db/vector-store.js +2 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +7 -7
- package/dist/embeddings/local.js +2 -2
- package/dist/embeddings/provider.js +1 -10
- package/dist/entry/store-kit.d.ts +37 -0
- package/dist/entry/store-kit.js +25 -0
- package/dist/entry/testing.d.ts +6 -0
- package/dist/entry/testing.js +7 -0
- package/dist/eval/judgment.js +1 -1
- package/dist/hooks/json-hooks.js +6 -2
- package/dist/importers/core.js +3 -3
- package/dist/importers/vault.d.ts +1 -1
- package/dist/importers/vault.js +18 -18
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/learn/autolearn.d.ts +6 -2
- package/dist/learn/autolearn.js +8 -6
- package/dist/learn/extract.js +1 -1
- package/dist/learn/invalidation.d.ts +2 -1
- package/dist/learn/invalidation.js +10 -5
- package/dist/mcp/memory-tools.d.ts +1 -1
- package/dist/mcp/memory-tools.js +6 -9
- package/dist/mcp/request.js +20 -19
- package/dist/mcp/stdio.js +13 -8
- package/dist/rerankers/cross-encoder.js +10 -3
- package/dist/search/embed-memories.d.ts +5 -2
- package/dist/search/embed-memories.js +40 -28
- package/dist/search/engine.d.ts +8 -0
- package/dist/search/engine.js +10 -0
- package/dist/search/vector.js +5 -5
- package/dist/server/auth.d.ts +1 -1
- package/dist/server/auth.js +7 -8
- package/dist/server/boot.d.ts +2 -1
- package/dist/server/boot.js +28 -19
- package/dist/server/client-ip.d.ts +9 -4
- package/dist/server/client-ip.js +35 -17
- package/dist/server/route-table.js +5 -6
- package/dist/server/routes/memories.js +4 -2
- package/dist/server/sleep-offload.js +3 -3
- package/dist/server/tls.d.ts +7 -2
- package/dist/server/tls.js +17 -5
- package/dist/server/types.d.ts +7 -2
- package/dist/server.d.ts +4 -41
- package/dist/server.js +3 -30
- package/dist/sharing/global-store.d.ts +4 -5
- package/dist/sharing/global-store.js +6 -11
- package/dist/sharing/global-sync.js +1 -1
- package/dist/sharing/search-both.js +9 -2
- package/dist/sharing/share.js +2 -4
- package/dist/store/ambient.js +2 -1
- package/dist/store/candidates.js +18 -8
- package/dist/store/cards.js +5 -4
- package/dist/store/compactions-record.js +8 -7
- package/dist/store/conflicts.d.ts +7 -1
- package/dist/store/conflicts.js +6 -5
- package/dist/store/connectors/dlq.js +2 -1
- package/dist/store/connectors/event-log.d.ts +13 -0
- package/dist/store/connectors/event-log.js +24 -0
- package/dist/store/connectors/github.js +6 -8
- package/dist/store/connectors/slack.js +6 -10
- package/dist/store/dashboard-reads.js +1 -0
- package/dist/store/delete-and-batch.js +5 -4
- package/dist/store/delivery-recorder.d.ts +2 -2
- package/dist/store/delivery-recorder.js +1 -6
- package/dist/store/descendant-page.js +2 -1
- package/dist/store/dormant.js +7 -3
- package/dist/store/entry-reads.d.ts +25 -3
- package/dist/store/entry-reads.js +43 -12
- package/dist/store/entry-row.js +9 -2
- package/dist/store/entry-writes.d.ts +10 -7
- package/dist/store/entry-writes.js +23 -11
- package/dist/store/goals.js +3 -2
- package/dist/store/graph-reads.js +5 -4
- package/dist/store/graph-view-rows.js +2 -1
- package/dist/store/half-life.js +2 -2
- package/dist/store/handoffs.d.ts +9 -3
- package/dist/store/handoffs.js +4 -3
- package/dist/store/index.d.ts +2 -2
- package/dist/store/index.js +1 -1
- package/dist/store/mirrors.js +9 -4
- package/dist/store/open.d.ts +2 -1
- package/dist/store/open.js +2 -1
- package/dist/store/port.d.ts +59 -3
- package/dist/store/quality-repair.js +1 -1
- package/dist/store/recall-trace.js +2 -1
- package/dist/store/rejected-values.js +14 -9
- package/dist/store/rows.js +2 -2
- package/dist/store/rule-sql.d.ts +2 -0
- package/dist/store/rule-sql.js +12 -6
- package/dist/store/search-rows.d.ts +5 -5
- package/dist/store/search-rows.js +9 -12
- package/dist/store/set-aside.js +1 -1
- package/dist/store/sqlite/connector-writes-group.js +3 -1
- package/dist/store/sqlite/entry-writes-group.d.ts +3 -3
- package/dist/store/sqlite/entry-writes-group.js +1 -1
- package/dist/store/sqlite/held-connection.d.ts +2 -0
- package/dist/store/sqlite/held-connection.js +1 -1
- package/dist/store/sqlite/last-recall-group.d.ts +13 -0
- package/dist/store/sqlite/last-recall-group.js +57 -0
- package/dist/store/sqlite/local.d.ts +0 -17
- package/dist/store/sqlite/local.js +0 -31
- package/dist/store/sqlite/maintenance-groups.d.ts +6 -0
- package/dist/store/sqlite/maintenance-groups.js +60 -0
- package/dist/store/sqlite/objects-group.js +14 -6
- package/dist/store/sqlite/store.d.ts +1 -8
- package/dist/store/sqlite/store.js +13 -24
- package/dist/store/summaries.d.ts +7 -0
- package/dist/store/summaries.js +49 -7
- package/dist/store/token-ledger-rows.js +3 -2
- package/dist/store/vector-index.js +5 -2
- package/dist/util/anthropic-messages.d.ts +3 -1
- package/dist/util/anthropic-messages.js +4 -2
- package/dist/util/atomic-write.d.ts +5 -2
- package/dist/util/atomic-write.js +12 -8
- package/dist/util/env.d.ts +10 -1
- package/dist/util/env.js +10 -1
- package/dist/util/format.d.ts +2 -0
- package/dist/util/format.js +4 -0
- package/dist/util/request-scope.d.ts +6 -2
- package/dist/util/request-scope.js +7 -3
- package/dist/util/spawn-argv.d.ts +9 -0
- package/dist/util/spawn-argv.js +57 -0
- package/dist/util/sql.d.ts +3 -0
- package/dist/util/sql.js +6 -0
- package/dist/util/vector-codec.d.ts +4 -0
- package/dist/util/vector-codec.js +15 -0
- package/dist/util/version.d.ts +1 -1
- package/dist/util/version.js +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 +3 -1
- package/dist/api/on-store.d.ts +0 -16
- package/dist/api/on-store.js +0 -38
- package/dist/cli/lenient-id.d.ts +0 -2
- package/dist/cli/lenient-id.js +0 -13
|
@@ -29,11 +29,6 @@ const BAD = /^([\w-]+)\.(?:\w+\.)?bad$/;
|
|
|
29
29
|
const BAD_TMP = /^[\w-]+\.\w+\.bad\.[0-9a-f]{8}\.tmp$/;
|
|
30
30
|
const NEW_STEM = /^(\d{13})-[0-9a-f]{8}$/;
|
|
31
31
|
const LEGACY_STEM_TIME = /-(\d+)$/;
|
|
32
|
-
let fsx = fs;
|
|
33
|
-
/** Test-only seam, so a test can make one fs call fail; null restores the real fs. */
|
|
34
|
-
export function _setSpoolFsForTests(next) {
|
|
35
|
-
fsx = next ?? fs;
|
|
36
|
-
}
|
|
37
32
|
const stamp = (ms) => String(Math.trunc(ms)).padStart(STAMP_DIGITS, '0');
|
|
38
33
|
const isTime = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
39
34
|
const idle = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -99,7 +94,7 @@ function claimOf(name) {
|
|
|
99
94
|
function spoolFile(hippoRoot) {
|
|
100
95
|
return path.join(hippoRoot, SPOOL_DIR, `${stamp(Date.now())}-${randomBytes(4).toString('hex')}.a0.json`);
|
|
101
96
|
}
|
|
102
|
-
export function spool(hippoRoot, tenantId, payload, text, at) {
|
|
97
|
+
export function spool(hippoRoot, tenantId, payload, text, at, fsx = fs) {
|
|
103
98
|
const file = spoolFile(hippoRoot);
|
|
104
99
|
fsx.mkdirSync(path.dirname(file), { recursive: true });
|
|
105
100
|
const body = {
|
|
@@ -160,7 +155,7 @@ function parseLock(text) {
|
|
|
160
155
|
return { pid, at: raw.at, token };
|
|
161
156
|
}
|
|
162
157
|
/** The lock's body; its mtime stands in for the time when the body does not parse. */
|
|
163
|
-
function readLock(lock, log) {
|
|
158
|
+
function readLock(fsx, lock, log) {
|
|
164
159
|
let text = '';
|
|
165
160
|
const read = settle(() => { text = fsx.readFileSync(lock, 'utf8'); }, log, 'spool lock not read');
|
|
166
161
|
if (read !== 'done')
|
|
@@ -172,7 +167,7 @@ function readLock(lock, log) {
|
|
|
172
167
|
const stat = settle(() => { mtime = fsx.statSync(lock).mtimeMs; }, log, 'spool lock not read');
|
|
173
168
|
return stat === 'done' ? { pid: null, at: mtime, token: null } : stat;
|
|
174
169
|
}
|
|
175
|
-
function createLock(lock, body, log) {
|
|
170
|
+
function createLock(fsx, lock, body, log) {
|
|
176
171
|
try {
|
|
177
172
|
fsx.writeFileSync(lock, body, { encoding: 'utf8', flag: 'wx' });
|
|
178
173
|
return 'taken';
|
|
@@ -188,18 +183,18 @@ function createLock(lock, body, log) {
|
|
|
188
183
|
}
|
|
189
184
|
}
|
|
190
185
|
/** This run's token and take time once it holds `replay.lock`, else null: another replayer holds it, or it could not be made. */
|
|
191
|
-
function takeLock(dir, log) {
|
|
186
|
+
function takeLock(fsx, dir, log) {
|
|
192
187
|
const lock = path.join(dir, LOCK);
|
|
193
188
|
const token = randomBytes(8).toString('hex');
|
|
194
189
|
const at = Date.now();
|
|
195
190
|
const body = JSON.stringify({ pid: process.pid, at, token });
|
|
196
191
|
for (let tries = 0; tries < 2; tries++) {
|
|
197
|
-
const made = createLock(lock, body, log);
|
|
192
|
+
const made = createLock(fsx, lock, body, log);
|
|
198
193
|
if (made === 'taken')
|
|
199
194
|
return { token, at };
|
|
200
195
|
if (made === 'skip')
|
|
201
196
|
break;
|
|
202
|
-
const held = readLock(lock, log);
|
|
197
|
+
const held = readLock(fsx, lock, log);
|
|
203
198
|
if (held === 'gone')
|
|
204
199
|
continue;
|
|
205
200
|
if (held === 'busy')
|
|
@@ -216,24 +211,24 @@ function takeLock(dir, log) {
|
|
|
216
211
|
return null;
|
|
217
212
|
}
|
|
218
213
|
/** False, after saying so, once `replay.lock` no longer holds this run's token or cannot be read. */
|
|
219
|
-
function ownsLock(dir, token, log) {
|
|
220
|
-
const held = readLock(path.join(dir, LOCK), log);
|
|
214
|
+
function ownsLock(fsx, dir, token, log) {
|
|
215
|
+
const held = readLock(fsx, path.join(dir, LOCK), log);
|
|
221
216
|
if (held !== 'gone' && held !== 'busy' && held.token === token)
|
|
222
217
|
return true;
|
|
223
218
|
log(held === 'busy' ? 'spool lock could not be read, stopping' : 'spool left to another replayer (lock taken over)');
|
|
224
219
|
return false;
|
|
225
220
|
}
|
|
226
221
|
/** Removes `replay.lock` only while it holds this run's token, so a run never deletes a successor's lock. Never throws. */
|
|
227
|
-
function releaseLock(dir, mine, log) {
|
|
222
|
+
function releaseLock(fsx, dir, mine, log) {
|
|
228
223
|
const lock = path.join(dir, LOCK);
|
|
229
|
-
const held = readLock(lock, log);
|
|
224
|
+
const held = readLock(fsx, lock, log);
|
|
230
225
|
// A takeover needs a lock over STALE_MS old, so a busy read of one this run took more recently is still this run's.
|
|
231
226
|
const ours = held === 'busy' ? Date.now() - mine.at < STALE_MS : held !== 'gone' && held.token === mine.token;
|
|
232
227
|
if (ours)
|
|
233
228
|
settle(() => fsx.unlinkSync(lock), log, 'spool lock not released');
|
|
234
229
|
}
|
|
235
230
|
/** Claim time sits in the claim's name, so a claim over STALE_MS away from now belongs to a replayer that never finished. True when one was put back. */
|
|
236
|
-
function recoverStaleClaims(dir, names, log) {
|
|
231
|
+
function recoverStaleClaims(fsx, dir, names, log) {
|
|
237
232
|
const now = Date.now();
|
|
238
233
|
// A claim whose unlink stayed busy after its `.bad` landed is a spare copy, not a second failure.
|
|
239
234
|
const setAside = new Set(names.flatMap((name) => BAD.exec(name)?.[1] ?? []));
|
|
@@ -256,21 +251,21 @@ function recoverStaleClaims(dir, names, log) {
|
|
|
256
251
|
putBackOne = true;
|
|
257
252
|
}
|
|
258
253
|
}
|
|
259
|
-
else if (moveBad(dir, file, { stem: claim.stem, attempt: claim.attempt, cause: 'interrupted', log })) {
|
|
254
|
+
else if (moveBad(fsx, dir, file, { stem: claim.stem, attempt: claim.attempt, cause: 'interrupted', log })) {
|
|
260
255
|
log(`spool file ${claim.name} was claimed by a replayer that never finished ${MAX_ATTEMPTS} times, set aside as .bad`);
|
|
261
256
|
}
|
|
262
257
|
}
|
|
263
258
|
return putBackOne;
|
|
264
259
|
}
|
|
265
260
|
/** Returns a claim to the waiting files as `<stem>.a<attempt>.json`; one that stays busy is left for stale recovery. */
|
|
266
|
-
function putBack(dir, claim, stem, attempt, log) {
|
|
261
|
+
function putBack(fsx, dir, claim, stem, attempt, log) {
|
|
267
262
|
const name = path.basename(claim);
|
|
268
263
|
if (settle(() => fsx.renameSync(claim, path.join(dir, `${stem}.a${attempt}.json`)), log, `spool file ${name} not put back`) === 'busy') {
|
|
269
264
|
log(`spool file ${name} could not be put back; it returns ${STALE_MS / MINUTE_MS} minutes after its claim`);
|
|
270
265
|
}
|
|
271
266
|
}
|
|
272
267
|
/** Writes `<stem>.<cause>.bad` through a random temp file, so the rename replaces any torn `.bad` a crash left. */
|
|
273
|
-
function writeBad(dir, options) {
|
|
268
|
+
function writeBad(fsx, dir, options) {
|
|
274
269
|
const { stem, cause, text, log, what } = options;
|
|
275
270
|
const tmp = path.join(dir, `${stem}.${cause}.bad.${randomBytes(4).toString('hex')}.tmp`);
|
|
276
271
|
const written = settle(() => fsx.writeFileSync(tmp, text, { encoding: 'utf8', flag: 'wx' }), log, what);
|
|
@@ -280,21 +275,21 @@ function writeBad(dir, options) {
|
|
|
280
275
|
return moved === 'done';
|
|
281
276
|
}
|
|
282
277
|
/** Sets a claim aside as `.bad` before the claim goes, so a crash between leaves a whole copy; a busy write puts the claim back. */
|
|
283
|
-
function moveBad(dir, claim, options) {
|
|
278
|
+
function moveBad(fsx, dir, claim, options) {
|
|
284
279
|
const { stem, attempt, cause, log, raw } = options;
|
|
285
280
|
const what = `spool file ${path.basename(claim)} not set aside`;
|
|
286
281
|
let text = raw ?? '';
|
|
287
282
|
if (raw === undefined && settle(() => { text = fsx.readFileSync(claim, 'utf8'); }, log, what) !== 'done')
|
|
288
283
|
return false;
|
|
289
|
-
if (!writeBad(dir, { stem, cause, text, log, what })) {
|
|
290
|
-
putBack(dir, claim, stem, attempt, log);
|
|
284
|
+
if (!writeBad(fsx, dir, { stem, cause, text, log, what })) {
|
|
285
|
+
putBack(fsx, dir, claim, stem, attempt, log);
|
|
291
286
|
return false;
|
|
292
287
|
}
|
|
293
288
|
settle(() => fsx.unlinkSync(claim), log, what);
|
|
294
289
|
return true;
|
|
295
290
|
}
|
|
296
291
|
/** A tmp over STALE_MS old belongs to a spool that never renamed it: a whole one joins the waiting files, a torn one is set aside. True when one joined. */
|
|
297
|
-
function promoteTmp(dir, names, tenantId, log) {
|
|
292
|
+
function promoteTmp(fsx, dir, names, tenantId, log) {
|
|
298
293
|
const now = Date.now();
|
|
299
294
|
let promoted = false;
|
|
300
295
|
for (const name of names) {
|
|
@@ -320,7 +315,7 @@ function promoteTmp(dir, names, tenantId, log) {
|
|
|
320
315
|
promoted = true;
|
|
321
316
|
}
|
|
322
317
|
}
|
|
323
|
-
else if (writeBad(dir, { stem: m[1], cause: 'unreadable', text, log, what })) {
|
|
318
|
+
else if (writeBad(fsx, dir, { stem: m[1], cause: 'unreadable', text, log, what })) {
|
|
324
319
|
settle(() => fsx.unlinkSync(file), log, what);
|
|
325
320
|
log(`spool file ${name} was never finished, set aside as .bad`);
|
|
326
321
|
}
|
|
@@ -328,13 +323,13 @@ function promoteTmp(dir, names, tenantId, log) {
|
|
|
328
323
|
return promoted;
|
|
329
324
|
}
|
|
330
325
|
/** Removes a claim once the store holds its summary; a claim left behind is imported again, so the log says so. */
|
|
331
|
-
function removeClaim(claim, name, log) {
|
|
326
|
+
function removeClaim(fsx, claim, name, log) {
|
|
332
327
|
const removed = settleCode(() => fsx.unlinkSync(claim), log, `spool file ${name} claim not removed`);
|
|
333
328
|
if (removed.settled === 'busy')
|
|
334
329
|
log(`spool file ${name} saved; its claim could not be removed (${removed.code}); it will be imported again`);
|
|
335
330
|
}
|
|
336
331
|
/** After an importer throw: a busy store stops the run with the count unchanged, any other error counts one try toward .bad. */
|
|
337
|
-
function importFailed(dir, claim, entry, options) {
|
|
332
|
+
function importFailed(fsx, dir, claim, entry, options) {
|
|
338
333
|
const { text, recorded, cause, log } = options;
|
|
339
334
|
const { name } = entry;
|
|
340
335
|
const busy = isSqliteBusy(cause);
|
|
@@ -346,22 +341,22 @@ function importFailed(dir, claim, entry, options) {
|
|
|
346
341
|
return busy ? 'stop' : 'skipped';
|
|
347
342
|
}
|
|
348
343
|
if (busy) {
|
|
349
|
-
putBack(dir, claim, entry.stem, entry.attempt, log);
|
|
344
|
+
putBack(fsx, dir, claim, entry.stem, entry.attempt, log);
|
|
350
345
|
log(`spool file ${name} waits for the next run: the store is busy`);
|
|
351
346
|
return 'stop';
|
|
352
347
|
}
|
|
353
348
|
const next = entry.attempt + 1;
|
|
354
349
|
if (next < MAX_ATTEMPTS) {
|
|
355
|
-
putBack(dir, claim, entry.stem, next, log);
|
|
350
|
+
putBack(fsx, dir, claim, entry.stem, next, log);
|
|
356
351
|
log(`spool file ${name} failed to import (try ${next} of ${MAX_ATTEMPTS}): ${errorMessage(cause)}`);
|
|
357
352
|
}
|
|
358
|
-
else if (moveBad(dir, claim, { stem: entry.stem, attempt: entry.attempt, cause: 'failed', log, raw: text })) {
|
|
353
|
+
else if (moveBad(fsx, dir, claim, { stem: entry.stem, attempt: entry.attempt, cause: 'failed', log, raw: text })) {
|
|
359
354
|
log(`spool file ${name} set aside as .bad after ${MAX_ATTEMPTS} tries: ${errorMessage(cause)}`);
|
|
360
355
|
}
|
|
361
356
|
return 'skipped';
|
|
362
357
|
}
|
|
363
358
|
/** Claims, reads and imports one waiting file; `stop` ends the run because the store is busy. */
|
|
364
|
-
function importFile(dir, entry, tenantId, log, importOne) {
|
|
359
|
+
function importFile(fsx, dir, entry, tenantId, log, importOne) {
|
|
365
360
|
const { name } = entry;
|
|
366
361
|
const claim = path.join(dir, `${entry.stem}.a${entry.attempt}.claim-${stamp(Date.now())}`);
|
|
367
362
|
const taken = settleCode(() => fsx.renameSync(path.join(dir, name), claim), log, `spool file ${name} not claimed`);
|
|
@@ -374,12 +369,12 @@ function importFile(dir, entry, tenantId, log, importOne) {
|
|
|
374
369
|
if (read === 'gone')
|
|
375
370
|
log(`spool file ${name} vanished`);
|
|
376
371
|
if (read === 'busy')
|
|
377
|
-
putBack(dir, claim, entry.stem, entry.attempt, log);
|
|
372
|
+
putBack(fsx, dir, claim, entry.stem, entry.attempt, log);
|
|
378
373
|
if (read !== 'done')
|
|
379
374
|
return 'skipped';
|
|
380
375
|
const spooled = parseSpooled(text, tenantId);
|
|
381
376
|
if (spooled === null) {
|
|
382
|
-
if (moveBad(dir, claim, { stem: entry.stem, attempt: entry.attempt, cause: 'unreadable', log, raw: text })) {
|
|
377
|
+
if (moveBad(fsx, dir, claim, { stem: entry.stem, attempt: entry.attempt, cause: 'unreadable', log, raw: text })) {
|
|
383
378
|
log(`spool file ${name} is not readable, set aside as .bad`);
|
|
384
379
|
}
|
|
385
380
|
return 'skipped';
|
|
@@ -389,7 +384,7 @@ function importFile(dir, entry, tenantId, log, importOne) {
|
|
|
389
384
|
if (recorded)
|
|
390
385
|
return;
|
|
391
386
|
recorded = true;
|
|
392
|
-
removeClaim(claim, name, log);
|
|
387
|
+
removeClaim(fsx, claim, name, log);
|
|
393
388
|
};
|
|
394
389
|
try {
|
|
395
390
|
importOne(spooled, markRecorded);
|
|
@@ -397,10 +392,10 @@ function importFile(dir, entry, tenantId, log, importOne) {
|
|
|
397
392
|
return 'imported';
|
|
398
393
|
}
|
|
399
394
|
catch (err) {
|
|
400
|
-
return importFailed(dir, claim, entry, { text, recorded, cause: err, log });
|
|
395
|
+
return importFailed(fsx, dir, claim, entry, { text, recorded, cause: err, log });
|
|
401
396
|
}
|
|
402
397
|
}
|
|
403
|
-
function mtimeOf(file) {
|
|
398
|
+
function mtimeOf(fsx, file) {
|
|
404
399
|
try {
|
|
405
400
|
return fsx.statSync(file).mtimeMs;
|
|
406
401
|
}
|
|
@@ -413,7 +408,7 @@ function mtimeOf(file) {
|
|
|
413
408
|
}
|
|
414
409
|
}
|
|
415
410
|
/** What doctor reports, read only: files a replay should have taken by now, claims and tmp files a dead process left, and `.bad` files. */
|
|
416
|
-
export function spoolCounts(hippoRoot, now, waitingAfterMs) {
|
|
411
|
+
export function spoolCounts(hippoRoot, now, waitingAfterMs, fsx = fs) {
|
|
417
412
|
const counts = { waiting: 0, stale: 0, bad: 0 };
|
|
418
413
|
const dir = path.join(hippoRoot, SPOOL_DIR);
|
|
419
414
|
if (!fsx.existsSync(dir))
|
|
@@ -426,12 +421,12 @@ export function spoolCounts(hippoRoot, now, waitingAfterMs) {
|
|
|
426
421
|
if (name.endsWith('.bad'))
|
|
427
422
|
counts.bad++;
|
|
428
423
|
else if (waiting !== null) {
|
|
429
|
-
const spooled = NEW_STEM.test(waiting[1]) ? created(waiting[1]) : mtimeOf(file);
|
|
424
|
+
const spooled = NEW_STEM.test(waiting[1]) ? created(waiting[1]) : mtimeOf(fsx, file);
|
|
430
425
|
if (spooled !== null && at - spooled > waitingAfterMs)
|
|
431
426
|
counts.waiting++;
|
|
432
427
|
}
|
|
433
428
|
else if (claim !== null || TMP.test(name)) {
|
|
434
|
-
const since = claim?.claimedAt ?? mtimeOf(file);
|
|
429
|
+
const since = claim?.claimedAt ?? mtimeOf(fsx, file);
|
|
435
430
|
if (since !== null && Math.abs(at - since) > STALE_MS)
|
|
436
431
|
counts.stale++;
|
|
437
432
|
}
|
|
@@ -439,25 +434,25 @@ export function spoolCounts(hippoRoot, now, waitingAfterMs) {
|
|
|
439
434
|
return counts;
|
|
440
435
|
}
|
|
441
436
|
/** One replayer at a time holds `replay.lock`, so only one process claims a file even where Windows lets two renames of it both land. */
|
|
442
|
-
export function importSpool(hippoRoot, tenantId, log, deadline, importOne) {
|
|
437
|
+
export function importSpool(hippoRoot, tenantId, log, deadline, importOne, fsx = fs) {
|
|
443
438
|
const dir = path.join(hippoRoot, SPOOL_DIR);
|
|
444
439
|
if (!fsx.existsSync(dir))
|
|
445
440
|
return 0;
|
|
446
|
-
const mine = takeLock(dir, log);
|
|
441
|
+
const mine = takeLock(fsx, dir, log);
|
|
447
442
|
if (mine === null)
|
|
448
443
|
return 0;
|
|
449
444
|
try {
|
|
450
|
-
if (!ownsLock(dir, mine.token, log))
|
|
445
|
+
if (!ownsLock(fsx, dir, mine.token, log))
|
|
451
446
|
return 0;
|
|
452
447
|
const names = fsx.readdirSync(dir);
|
|
453
|
-
const promoted = promoteTmp(dir, names, tenantId, log);
|
|
454
|
-
const recovered = recoverStaleClaims(dir, names, log);
|
|
448
|
+
const promoted = promoteTmp(fsx, dir, names, tenantId, log);
|
|
449
|
+
const recovered = recoverStaleClaims(fsx, dir, names, log);
|
|
455
450
|
let finished = 0;
|
|
456
451
|
// Only promotion and recovery add waiting names, so the folder is listed again only after one of them did.
|
|
457
452
|
for (const entry of waitingFiles(promoted || recovered ? fsx.readdirSync(dir) : names)) {
|
|
458
|
-
if (Date.now() > deadline || !ownsLock(dir, mine.token, log))
|
|
453
|
+
if (Date.now() > deadline || !ownsLock(fsx, dir, mine.token, log))
|
|
459
454
|
break;
|
|
460
|
-
const outcome = importFile(dir, entry, tenantId, log, importOne);
|
|
455
|
+
const outcome = importFile(fsx, dir, entry, tenantId, log, importOne);
|
|
461
456
|
if (outcome === 'imported')
|
|
462
457
|
finished++;
|
|
463
458
|
if (outcome === 'stop')
|
|
@@ -466,7 +461,7 @@ export function importSpool(hippoRoot, tenantId, log, deadline, importOne) {
|
|
|
466
461
|
return finished;
|
|
467
462
|
}
|
|
468
463
|
finally {
|
|
469
|
-
releaseLock(dir, mine, log);
|
|
464
|
+
releaseLock(fsx, dir, mine, log);
|
|
470
465
|
}
|
|
471
466
|
}
|
|
472
467
|
//# sourceMappingURL=compaction-spool.js.map
|
|
@@ -24,7 +24,7 @@ export function sessionEndHandoffForCaller(ctx, req) {
|
|
|
24
24
|
const state = checkedWorkingState(req.workingState);
|
|
25
25
|
const evidence = checkedEvidence(req.evidence);
|
|
26
26
|
const key = bindCaller(ctx, req.sessionId, req.project);
|
|
27
|
-
const handoff = writeSessionEndHandoff(ctx.hippoRoot, ctx.tenantId, req.sessionId, evidence, state, key);
|
|
27
|
+
const handoff = writeSessionEndHandoff(ctx.hippoRoot, { tenantId: ctx.tenantId, sessionId: req.sessionId, evidence, derived: state, key });
|
|
28
28
|
const snapshotsClosed = closeTaskSnapshotsForSession(ctx.hippoRoot, ctx.tenantId, req.sessionId, 'session-ended', key);
|
|
29
29
|
return { handoffWritten: handoff !== null, snapshotsClosed };
|
|
30
30
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { homeDir } from '../util/agent-homes.js';
|
|
6
|
-
import { errorMessage, log as logger } from '../util/log.js';
|
|
6
|
+
import { errorCode, errorMessage, log as logger } from '../util/log.js';
|
|
7
7
|
import { isObjectLike, isStringValue } from '../core/capture-contract.js';
|
|
8
8
|
import { SESSION_ID_RE } from './copilot-transcript.js';
|
|
9
9
|
import { blockHash } from '../util/token-text.js';
|
|
@@ -78,10 +78,16 @@ function releaseSessionLock(lock) {
|
|
|
78
78
|
logger.debug(`session lock not removed: ${errorMessage(err)}`);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
/** False when the
|
|
81
|
+
/** False when the request could not be queued; the caller then refuses the run, since an unlocked worker could race a live one. */
|
|
82
82
|
function queueRun(files, mode) {
|
|
83
83
|
try {
|
|
84
84
|
fs.mkdirSync(path.dirname(files.queued), { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.warn(`session worker: cannot create ${path.dirname(files.queued)}, so the ${mode} close is not run: ${errorMessage(err)}`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
85
91
|
// A full close outranks a turn close: it overwrites a queued turn, and a turn never replaces a queued full close.
|
|
86
92
|
if (mode === 'full')
|
|
87
93
|
fs.writeFileSync(files.queued, 'full');
|
|
@@ -90,7 +96,10 @@ function queueRun(files, mode) {
|
|
|
90
96
|
return true;
|
|
91
97
|
}
|
|
92
98
|
catch (err) {
|
|
93
|
-
|
|
99
|
+
if (errorCode(err) === 'EEXIST')
|
|
100
|
+
return true;
|
|
101
|
+
logger.warn(`session worker: cannot queue the ${mode} close in ${files.queued}, so it is not run: ${errorMessage(err)}`);
|
|
102
|
+
return false;
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
/** Takes the queued request by renaming it, so one queued while this reads it lands in a fresh file. */
|
|
@@ -99,8 +108,10 @@ function takeQueued(queued) {
|
|
|
99
108
|
try {
|
|
100
109
|
fs.renameSync(queued, taken);
|
|
101
110
|
}
|
|
102
|
-
catch {
|
|
103
|
-
//
|
|
111
|
+
catch (err) {
|
|
112
|
+
// ENOENT means nothing is queued; any other refusal leaves the request queued for the next round, but is told.
|
|
113
|
+
if (errorCode(err) !== 'ENOENT')
|
|
114
|
+
logger.warn(`session worker: cannot take the queued request ${queued}: ${errorMessage(err)}`);
|
|
104
115
|
return null;
|
|
105
116
|
}
|
|
106
117
|
try {
|
|
@@ -113,8 +124,10 @@ function takeQueued(queued) {
|
|
|
113
124
|
/** Runs `work` unless a live worker holds this session, which then runs the request after its own so the last reply is never dropped. */
|
|
114
125
|
export async function runSessionWorker(sessionId, mode, work) {
|
|
115
126
|
const files = sessionStateFiles(sessionId);
|
|
116
|
-
if (files === null
|
|
127
|
+
if (files === null)
|
|
117
128
|
return work(mode);
|
|
129
|
+
if (!queueRun(files, mode))
|
|
130
|
+
return;
|
|
118
131
|
for (let take = takeSessionLock(files.lock); take !== 'held'; take = takeSessionLock(files.lock)) {
|
|
119
132
|
if (take === 'unusable')
|
|
120
133
|
return work(mode);
|
package/dist/cli/audit.js
CHANGED
|
@@ -72,7 +72,7 @@ async function cmdAuditList(hippoRoot, tenantId, flags) {
|
|
|
72
72
|
console.log(formatAuditRow(ev));
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
function cmdAuditPrune(hippoRoot, ctxTenantId, flags) {
|
|
75
|
+
async function cmdAuditPrune(hippoRoot, ctxTenantId, flags) {
|
|
76
76
|
const olderThanRaw = stringFlag(flags, 'older-than') ?? '';
|
|
77
77
|
if (!olderThanRaw) {
|
|
78
78
|
printError('Usage: hippo audit prune --older-than <Nd> [--dry-run] [--tenant <t>]');
|
|
@@ -89,7 +89,7 @@ function cmdAuditPrune(hippoRoot, ctxTenantId, flags) {
|
|
|
89
89
|
const tenantId = stringFlag(flags, 'tenant')?.trim() || ctxTenantId;
|
|
90
90
|
const dryRun = flagIsTrue(flags, 'dry-run');
|
|
91
91
|
const asJson = boolFlag(flags, 'json');
|
|
92
|
-
const result = api.pruneAuditLog(cliApiContext(hippoRoot, tenantId), { olderThanDays, dryRun });
|
|
92
|
+
const result = await api.pruneAuditLog(cliApiContext(hippoRoot, tenantId), { olderThanDays, dryRun });
|
|
93
93
|
if (asJson) {
|
|
94
94
|
console.log(JSON.stringify(result));
|
|
95
95
|
return;
|
|
@@ -107,15 +107,15 @@ async function cmdAuditLog(hippoRoot, tenantId, args, flags) {
|
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
if (sub === 'prune') {
|
|
110
|
-
cmdAuditPrune(hippoRoot, tenantId, flags);
|
|
110
|
+
await cmdAuditPrune(hippoRoot, tenantId, flags);
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
printError(`Unknown audit subcommand: ${sub}. Expected: list | prune.`);
|
|
114
114
|
throw new CliExit(1);
|
|
115
115
|
}
|
|
116
|
-
function auditRepair(hippoRoot, tenantId, flags) {
|
|
116
|
+
async function auditRepair(hippoRoot, tenantId, flags) {
|
|
117
117
|
const apply = flagIsTrue(flags, 'apply') && flags['dry-run'] !== true;
|
|
118
|
-
const result = api.repairMemoryQuality(cliApiContext(flags['global'] ? getGlobalRoot() : hippoRoot, tenantId), apply);
|
|
118
|
+
const result = await api.repairMemoryQuality(cliApiContext(flags['global'] ? getGlobalRoot() : hippoRoot, tenantId), apply);
|
|
119
119
|
if (flags['json']) {
|
|
120
120
|
console.log(JSON.stringify(result));
|
|
121
121
|
return;
|
|
@@ -137,7 +137,7 @@ function auditRepair(hippoRoot, tenantId, flags) {
|
|
|
137
137
|
if (!apply)
|
|
138
138
|
console.log('Preview only. Add --apply to move set-aside memories to dormant storage. Pin a review memory to keep it.');
|
|
139
139
|
}
|
|
140
|
-
function fixAuditErrors(hippoRoot, tenantId, result, flags) {
|
|
140
|
+
async function fixAuditErrors(hippoRoot, tenantId, result, flags) {
|
|
141
141
|
const errors = result.issues.filter(i => i.severity === 'error');
|
|
142
142
|
if (errors.length > 0 && flagIsTrue(flags, 'dry-run')) {
|
|
143
143
|
console.log(`\nWould remove ${errors.length} error-severity memories (dry run, nothing deleted).`);
|
|
@@ -145,7 +145,7 @@ function fixAuditErrors(hippoRoot, tenantId, result, flags) {
|
|
|
145
145
|
}
|
|
146
146
|
else if (errors.length > 0) {
|
|
147
147
|
const ctx = cliApiContext(hippoRoot, tenantId);
|
|
148
|
-
const removedCount = api.removeAuditErrors(ctx, 'audit --fix', errors).length;
|
|
148
|
+
const removedCount = (await api.removeAuditErrors(ctx, 'audit --fix', errors)).length;
|
|
149
149
|
console.log(`\nRemoved ${removedCount} error-severity memories.`);
|
|
150
150
|
console.log(`${result.issues.length - errors.length} warnings remain (review manually).`);
|
|
151
151
|
}
|
|
@@ -155,7 +155,7 @@ function fixAuditErrors(hippoRoot, tenantId, result, flags) {
|
|
|
155
155
|
}
|
|
156
156
|
export async function handleAudit({ hippoRoot, tenantId, args, flags }) {
|
|
157
157
|
if (args[0] === 'repair') {
|
|
158
|
-
auditRepair(hippoRoot, tenantId, flags);
|
|
158
|
+
await auditRepair(hippoRoot, tenantId, flags);
|
|
159
159
|
return;
|
|
160
160
|
}
|
|
161
161
|
// `audit list` and `audit prune` are the audit-log subcommands; other forms (no sub, --fix) keep the memory-quality auditor.
|
|
@@ -164,7 +164,7 @@ export async function handleAudit({ hippoRoot, tenantId, args, flags }) {
|
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
166
|
requireInit(hippoRoot);
|
|
167
|
-
const result = api.auditMemoryQuality(cliApiContext(hippoRoot, tenantId));
|
|
167
|
+
const result = await api.auditMemoryQuality(cliApiContext(hippoRoot, tenantId));
|
|
168
168
|
const shouldFix = boolFlag(flags, 'fix');
|
|
169
169
|
if (result.issues.length === 0) {
|
|
170
170
|
console.log(`All ${result.total} memories passed quality checks.`);
|
|
@@ -177,7 +177,7 @@ export async function handleAudit({ hippoRoot, tenantId, args, flags }) {
|
|
|
177
177
|
console.log(` "${truncateWithEllipsis(issue.content, 80)}"`);
|
|
178
178
|
}
|
|
179
179
|
if (shouldFix) {
|
|
180
|
-
fixAuditErrors(hippoRoot, tenantId, result, flags);
|
|
180
|
+
await fixAuditErrors(hippoRoot, tenantId, result, flags);
|
|
181
181
|
}
|
|
182
182
|
else {
|
|
183
183
|
console.log(`\nRun with --fix to auto-remove error-severity issues.`);
|
package/dist/cli/auth.js
CHANGED
|
@@ -28,7 +28,7 @@ function noteMintDefaults(roleGiven, expiryGiven) {
|
|
|
28
28
|
if (notes.length > 0)
|
|
29
29
|
printError(`hippo auth create: ${notes.join('; ')}.`);
|
|
30
30
|
}
|
|
31
|
-
function cmdAuthCreate(hippoRoot, defaultTenantId, flags) {
|
|
31
|
+
async function cmdAuthCreate(hippoRoot, defaultTenantId, flags) {
|
|
32
32
|
const root = resolveAuthRoot(hippoRoot, flags);
|
|
33
33
|
const tenantFlag = stringFlag(flags, 'tenant');
|
|
34
34
|
const labelFlag = stringFlag(flags, 'label');
|
|
@@ -45,7 +45,7 @@ function cmdAuthCreate(hippoRoot, defaultTenantId, flags) {
|
|
|
45
45
|
const ctx = cliApiContext(root, tenantFlag ?? defaultTenantId);
|
|
46
46
|
let result;
|
|
47
47
|
try {
|
|
48
|
-
result = api.authCreate(ctx, { label: labelFlag, role, ttlDays, noExpiry: noExpiry || undefined });
|
|
48
|
+
result = await api.authCreate(ctx, { label: labelFlag, role, ttlDays, noExpiry: noExpiry || undefined });
|
|
49
49
|
}
|
|
50
50
|
catch (err) {
|
|
51
51
|
if (!(err instanceof api.BadRequestError))
|
|
@@ -112,7 +112,7 @@ async function cmdAuthRevoke(hippoRoot, tenantId, keyId, flags) {
|
|
|
112
112
|
const ctx = await keyContext(resolveAuthRoot(hippoRoot, flags), tenantId, keyId);
|
|
113
113
|
let revokedAt;
|
|
114
114
|
try {
|
|
115
|
-
revokedAt = api.authRevoke(ctx, keyId).revokedAt;
|
|
115
|
+
revokedAt = (await api.authRevoke(ctx, keyId)).revokedAt;
|
|
116
116
|
}
|
|
117
117
|
catch (err) {
|
|
118
118
|
printError(`Error: ${errorMessage(err)}`);
|
|
@@ -152,7 +152,7 @@ export async function handleAuth({ hippoRoot, tenantId, args, flags }) {
|
|
|
152
152
|
const subArgs = args.slice(1);
|
|
153
153
|
switch (sub) {
|
|
154
154
|
case 'create':
|
|
155
|
-
cmdAuthCreate(hippoRoot, tenantId, flags);
|
|
155
|
+
await cmdAuthCreate(hippoRoot, tenantId, flags);
|
|
156
156
|
return;
|
|
157
157
|
case 'list':
|
|
158
158
|
cmdAuthList(hippoRoot, tenantId, flags);
|
package/dist/cli/client.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/** HTTP client wrapper for `hippo serve`: only the routed writes (remember, forget, archive, promote); every other command opens the store directly.
|
|
2
2
|
* Server errors rethrow with the message verbatim so CLI substring matches still work; network errors propagate as the native fetch failure. */
|
|
3
3
|
import type { RememberOpts, RememberResult } from '../api/index.js';
|
|
4
|
-
/** An error the server answered with, not one the transport raised
|
|
5
|
-
* and a server message quotes the caller's own id or content back verbatim. */
|
|
4
|
+
/** An error the server answered with, not one the transport raised, so the transport classifier can skip it by class. */
|
|
6
5
|
export declare class HttpResponseError extends Error {
|
|
7
6
|
readonly status: number;
|
|
8
7
|
constructor(message: string, status: number);
|
package/dist/cli/client.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { fetchWithRetry } from '../util/http-retry.js';
|
|
4
4
|
import { readCappedJson } from '../util/capped-json.js';
|
|
5
5
|
import { isJsonObject, isJsonString } from '../util/json.js';
|
|
6
|
+
import { isObjectLike } from '../core/capture-contract.js';
|
|
6
7
|
/** A write the local server has not answered in this long is stuck; the caller treats it as delivery-unknown. */
|
|
7
8
|
const SERVER_TIMEOUT_MS = 30_000;
|
|
8
9
|
/** Five tries at the server's 1 s Retry-After keeps the roughly 5 s wait the CLI had before the server answered busy with 503. */
|
|
@@ -26,8 +27,7 @@ function buildHeaders(apiKey, withBody) {
|
|
|
26
27
|
headers['authorization'] = `Bearer ${apiKey}`;
|
|
27
28
|
return headers;
|
|
28
29
|
}
|
|
29
|
-
/** An error the server answered with, not one the transport raised
|
|
30
|
-
* and a server message quotes the caller's own id or content back verbatim. */
|
|
30
|
+
/** An error the server answered with, not one the transport raised, so the transport classifier can skip it by class. */
|
|
31
31
|
export class HttpResponseError extends Error {
|
|
32
32
|
status;
|
|
33
33
|
constructor(message, status) {
|
|
@@ -109,29 +109,29 @@ export async function archiveRaw(serverUrl, apiKey, id, reason) {
|
|
|
109
109
|
await throwForStatus(res);
|
|
110
110
|
return readReply(res, 'POST /v1/memories/:id/archive', (b) => isJsonString(b.archivedAt));
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
/** The syscall code Node puts on a system error itself (`http` errors) or on the `cause` of a fetch `TypeError`; '' when there is none. */
|
|
113
|
+
function transportCode(err) {
|
|
114
|
+
if ('code' in err)
|
|
115
|
+
return String(err.code);
|
|
116
|
+
// Node's fetch attaches the syscall code (ECONNREFUSED, ECONNRESET) on a non-null object `cause`.
|
|
117
|
+
const cause = err.cause;
|
|
118
|
+
return isObjectLike(cause) && 'code' in cause ? String(cause.code) : '';
|
|
116
119
|
}
|
|
117
120
|
export function classifyTransportFailure(err) {
|
|
118
121
|
// The server answered, so the transport worked, whatever the message says.
|
|
119
122
|
if (err instanceof HttpResponseError)
|
|
120
123
|
return 'none';
|
|
121
|
-
const
|
|
122
|
-
const code = hasObjectCause(err) ? err.cause.code : undefined;
|
|
124
|
+
const code = transportCode(err);
|
|
123
125
|
// Connect-phase failures: no request bytes ever left the client, so a stale
|
|
124
126
|
// pidfile can be healed and the call replayed on the direct path.
|
|
125
127
|
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EAI_AGAIN')
|
|
126
128
|
return 'never-sent';
|
|
127
|
-
|
|
128
|
-
return 'never-sent';
|
|
129
|
-
// The socket died mid-exchange. Node fetch surfaces this as a bare
|
|
130
|
-
// 'fetch failed' with the syscall code on the cause, so check both shapes.
|
|
129
|
+
// The socket died mid-exchange.
|
|
131
130
|
if (code === 'ECONNRESET' || code === 'ECONNABORTED' || code === 'EPIPE' || code === 'UND_ERR_SOCKET') {
|
|
132
131
|
return 'delivery-unknown';
|
|
133
132
|
}
|
|
134
|
-
|
|
133
|
+
// Fetch rejects every network failure as a TypeError, with a cause only when the syscall is known, so no code means the conservative answer.
|
|
134
|
+
if (err instanceof TypeError)
|
|
135
135
|
return 'delivery-unknown';
|
|
136
136
|
// The request was sent and the server went quiet, so it may have committed.
|
|
137
137
|
if (err.name === 'TimeoutError')
|
package/dist/cli/context.js
CHANGED
|
@@ -54,7 +54,7 @@ async function renderContext(hippoRoot, resolvedTenant, args, flags, stdinText,
|
|
|
54
54
|
const session = resolveContextSession(stdinText);
|
|
55
55
|
const { currentSessionId, ledgerSessionId, payloadSessionId } = session;
|
|
56
56
|
// The pilot arm is booked at the first hook call whatever the flags, so the holdout sees no budget or content branch.
|
|
57
|
-
if (inPilotHoldout(hippoRoot, resolvedTenant, currentSessionId, payloadSessionId !== undefined)) {
|
|
57
|
+
if (inPilotHoldout(hippoRoot, resolvedTenant, currentSessionId, { write: payloadSessionId !== undefined })) {
|
|
58
58
|
rec?.disabled();
|
|
59
59
|
return;
|
|
60
60
|
}
|
package/dist/cli/curate.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CommandContext } from './flag-values.js';
|
|
2
|
-
export declare function handleOutcome({ hippoRoot, tenantId, flags }: CommandContext): void
|
|
2
|
+
export declare function handleOutcome({ hippoRoot, tenantId, flags }: CommandContext): Promise<void>;
|
|
3
3
|
export declare function handleConflicts({ hippoRoot, tenantId, flags }: CommandContext): void;
|
|
4
4
|
export declare function handleResolve({ hippoRoot, tenantId, args, flags }: CommandContext): Promise<void>;
|
|
5
5
|
export declare function handleReject({ hippoRoot, tenantId, args, flags }: CommandContext): void;
|
package/dist/cli/curate.js
CHANGED
|
@@ -21,7 +21,7 @@ const CONFLICT_PREVIEW_CHARS = 120;
|
|
|
21
21
|
const REJECTED_DIGEST_LIST_CHARS = 16;
|
|
22
22
|
const DORMANT_PREVIEW_CHARS = 100;
|
|
23
23
|
const STRENGTH_DECIMALS = 3;
|
|
24
|
-
export function handleOutcome({ hippoRoot, tenantId, flags }) {
|
|
24
|
+
export async function handleOutcome({ hippoRoot, tenantId, flags }) {
|
|
25
25
|
requireInit(hippoRoot);
|
|
26
26
|
const good = boolFlag(flags, 'good');
|
|
27
27
|
const bad = boolFlag(flags, 'bad');
|
|
@@ -34,10 +34,10 @@ export function handleOutcome({ hippoRoot, tenantId, flags }) {
|
|
|
34
34
|
const specificId = flags['id'] ? String(flags['id']) : null;
|
|
35
35
|
let updated;
|
|
36
36
|
if (specificId) {
|
|
37
|
-
updated = api.outcome(ctx, [specificId], good).applied;
|
|
37
|
+
updated = (await api.outcome(ctx, [specificId], good)).applied;
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
40
|
-
const r = api.outcomeForLastRecall(ctx, good);
|
|
40
|
+
const r = await api.outcomeForLastRecall(ctx, good);
|
|
41
41
|
if (r.ids.length === 0) {
|
|
42
42
|
console.log('No recent recall to apply outcome to. Use --id <id> to target a specific memory.');
|
|
43
43
|
return;
|
|
@@ -48,17 +48,17 @@ export function handleOutcome({ hippoRoot, tenantId, flags }) {
|
|
|
48
48
|
}
|
|
49
49
|
// Shared between the forget dispatch and cmdForget so the message can't drift.
|
|
50
50
|
const ARCHIVE_REASON_REQUIRED = 'hippo forget --archive requires --reason "<why>" (recorded on the archive).';
|
|
51
|
-
function cmdForget(hippoRoot, tenantId, id, flags) {
|
|
51
|
+
async function cmdForget(hippoRoot, tenantId, id, flags) {
|
|
52
52
|
requireInit(hippoRoot);
|
|
53
53
|
const ctx = cliApiContext(hippoRoot, tenantId);
|
|
54
54
|
// Raw memories (connector ingestion) are append-only: a BEFORE-DELETE trigger aborts any delete,
|
|
55
55
|
// so archiveRaw is the sanctioned removal path and records ctx.actor as the archiver.
|
|
56
56
|
if (flagIsTrue(flags, 'archive')) {
|
|
57
|
-
archiveForgottenRaw(ctx, id, stringFlag(flags, 'reason') ?? null);
|
|
57
|
+
await archiveForgottenRaw(ctx, id, stringFlag(flags, 'reason') ?? null);
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
try {
|
|
61
|
-
api.forget(ctx, id);
|
|
61
|
+
await api.forget(ctx, id);
|
|
62
62
|
console.log(`Forgot ${id}`);
|
|
63
63
|
}
|
|
64
64
|
catch (err) {
|
|
@@ -66,13 +66,13 @@ function cmdForget(hippoRoot, tenantId, id, flags) {
|
|
|
66
66
|
throw new CliExit(1);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
function archiveForgottenRaw(ctx, id, reason) {
|
|
69
|
+
async function archiveForgottenRaw(ctx, id, reason) {
|
|
70
70
|
if (!reason) {
|
|
71
71
|
printError(ARCHIVE_REASON_REQUIRED);
|
|
72
72
|
throw new CliExit(1);
|
|
73
73
|
}
|
|
74
74
|
try {
|
|
75
|
-
api.archiveRaw(ctx, id, reason);
|
|
75
|
+
await api.archiveRaw(ctx, id, reason);
|
|
76
76
|
console.log(`Archived ${id}`);
|
|
77
77
|
}
|
|
78
78
|
catch (err) {
|
|
@@ -453,7 +453,7 @@ export async function handleForget({ hippoRoot, tenantId, args, flags }) {
|
|
|
453
453
|
});
|
|
454
454
|
if (routed)
|
|
455
455
|
return;
|
|
456
|
-
cmdForget(hippoRoot, tenantId, id, flags);
|
|
456
|
+
await cmdForget(hippoRoot, tenantId, id, flags);
|
|
457
457
|
}
|
|
458
458
|
function invalidateChurn(hippoRoot, tenantId, args, flags) {
|
|
459
459
|
if (args[0] || flags['id'] !== undefined) {
|