hippo-memory 1.57.0 → 1.58.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 +11 -0
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/api-errors.d.ts +27 -0
- package/dist/api-errors.js +37 -0
- package/dist/api.d.ts +5 -5
- package/dist/api.js +40 -47
- package/dist/audit.d.ts +4 -0
- package/dist/audit.js +11 -0
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +7 -5
- package/dist/capture-contract.d.ts +47 -0
- package/dist/capture-contract.js +49 -0
- package/dist/capture-error.js +2 -1
- package/dist/capture.d.ts +0 -13
- package/dist/capture.js +5 -66
- package/dist/cli/shared.js +10 -6
- package/dist/cli.js +100 -39
- package/dist/client.js +9 -0
- package/dist/codex-patch.js +1 -1
- package/dist/compaction-record.d.ts +1 -1
- package/dist/compaction-record.js +3 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/connectors/github/dlq.js +5 -2
- package/dist/connectors/github/octokit-client.js +4 -2
- package/dist/connectors/slack/dlq.js +6 -2
- package/dist/connectors/slack/web-client.js +7 -5
- package/dist/consolidate.d.ts +10 -0
- package/dist/consolidate.js +36 -34
- package/dist/customer-notes.js +14 -13
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +1 -1
- package/dist/db.d.ts +12 -0
- package/dist/db.js +62 -1
- package/dist/decisions.js +9 -8
- package/dist/doctor.js +5 -0
- package/dist/embedding-provider.js +3 -3
- package/dist/embeddings.d.ts +4 -4
- package/dist/embeddings.js +72 -16
- package/dist/extract.js +3 -2
- package/dist/http-retry.d.ts +21 -0
- package/dist/http-retry.js +50 -0
- package/dist/http-util.d.ts +8 -0
- package/dist/http-util.js +10 -0
- package/dist/importers.d.ts +2 -0
- package/dist/importers.js +16 -5
- package/dist/incidents.js +11 -10
- package/dist/judgment.js +10 -17
- package/dist/log.d.ts +25 -0
- package/dist/log.js +48 -0
- package/dist/mcp/server.js +52 -24
- package/dist/mcp/tool-args.d.ts +21 -0
- package/dist/mcp/tool-args.js +80 -0
- package/dist/memory.js +3 -2
- package/dist/overlap-index.d.ts +7 -0
- package/dist/overlap-index.js +38 -0
- package/dist/pilot-arm.d.ts +9 -0
- package/dist/pilot-arm.js +47 -0
- package/dist/policies.js +12 -11
- package/dist/predictions.js +9 -8
- package/dist/processes.js +14 -13
- package/dist/project-briefs.js +16 -15
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +25 -1
- package/dist/raw-archive.js +7 -6
- package/dist/recall-scope.d.ts +2 -1
- package/dist/recall-scope.js +2 -1
- package/dist/refine-llm.js +3 -2
- package/dist/reject-flow.js +6 -9
- package/dist/rejection.d.ts +2 -1
- package/dist/rejection.js +2 -1
- package/dist/search.js +14 -2
- package/dist/secret-detect.d.ts +13 -1
- package/dist/secret-detect.js +33 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +180 -409
- package/dist/session-digest.js +2 -1
- package/dist/shared.js +7 -6
- package/dist/skills.js +15 -14
- package/dist/store.js +4 -4
- package/dist/token-ledger.d.ts +4 -2
- package/dist/token-ledger.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/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 +1 -1
- package/dist/connectors/slack/ratelimit.d.ts +0 -9
- package/dist/connectors/slack/ratelimit.js +0 -18
package/dist/capture.js
CHANGED
|
@@ -25,6 +25,7 @@ import { RejectedValueError, checkRejectionGuard } from './rejection.js';
|
|
|
25
25
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
26
26
|
import { loadConfig } from './config.js';
|
|
27
27
|
import { classifyOriginProject } from './project-identity.js';
|
|
28
|
+
import { isObjectLike, isStringValue, readClaudeCodePreCompact } from './capture-contract.js';
|
|
28
29
|
// Sentence-level patterns
|
|
29
30
|
//
|
|
30
31
|
// T1 (DF2): each pattern now carries TWO capture groups — group 1 is the
|
|
@@ -471,23 +472,6 @@ export function extractFromText(text) {
|
|
|
471
472
|
}
|
|
472
473
|
return items;
|
|
473
474
|
}
|
|
474
|
-
/**
|
|
475
|
-
* Runtime shape guards used throughout this file wherever a value arrives
|
|
476
|
-
* unparsed (JSONL transcript records, stdout/stderr write() chunks). Generic
|
|
477
|
-
* over the input so the parameter is never annotated `unknown` directly — TS
|
|
478
|
-
* infers it from the call site — while the check itself avoids `typeof` by
|
|
479
|
-
* testing identity against the coercion (`String(x) === x`) /
|
|
480
|
-
* prototype-chain (`instanceof Object`) instead. Behaviourally equivalent to
|
|
481
|
-
* `typeof x === 'string'` / a truthy `typeof x === 'object'` check for
|
|
482
|
-
* anything `JSON.parse` can produce (the only divergence is boxed
|
|
483
|
-
* primitives, which JSON.parse never yields).
|
|
484
|
-
*/
|
|
485
|
-
export function isStringValue(value) {
|
|
486
|
-
return String(value) === value;
|
|
487
|
-
}
|
|
488
|
-
export function isObjectLike(value) {
|
|
489
|
-
return value !== null && value instanceof Object;
|
|
490
|
-
}
|
|
491
475
|
/** Message for a caught value of unknown shape. `cause` names the sanctioned unknown-input case (error-cause enrichment). */
|
|
492
476
|
export function errorMessage(cause) {
|
|
493
477
|
return cause instanceof Error ? cause.message : String(cause);
|
|
@@ -1161,57 +1145,12 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
|
|
|
1161
1145
|
appendPreCompactLog(logFile, 'skip: store not initialized');
|
|
1162
1146
|
return;
|
|
1163
1147
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
appendPreCompactLog(logFile, 'skip: no PreCompact payload arrived before the stdin wait window closed');
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
// A true manual invocation has no stdin at all (TTY, or a non-TTY pipe
|
|
1171
|
-
// that yielded an empty read) — that's the ONLY case newest-transcript
|
|
1172
|
-
// auto-discovery is allowed to run. Any other non-empty stdin must
|
|
1173
|
-
// JSON-parse to an object carrying a string transcript_path, or it is
|
|
1174
|
-
// treated as malformed input and skipped (X4) rather than silently
|
|
1175
|
-
// falling back to discovery, which could snapshot an unrelated session's
|
|
1176
|
-
// transcript under this payload's session_id.
|
|
1177
|
-
const manualInvocation = !stdinText || stdinText.trim() === '';
|
|
1178
|
-
let sessionId = null;
|
|
1179
|
-
let payloadTranscriptPath = null;
|
|
1180
|
-
let payloadCwd = null;
|
|
1181
|
-
let payloadTrigger = null;
|
|
1182
|
-
if (!manualInvocation) {
|
|
1183
|
-
let payload;
|
|
1184
|
-
try {
|
|
1185
|
-
payload = JSON.parse(stdinText.trim());
|
|
1186
|
-
}
|
|
1187
|
-
catch {
|
|
1188
|
-
// payload stays undefined; the isObjectLike check below rejects it
|
|
1189
|
-
// the same way it would reject an explicit null.
|
|
1190
|
-
}
|
|
1191
|
-
if (!isObjectLike(payload) || !('transcript_path' in payload) || !isStringValue(payload.transcript_path)) {
|
|
1192
|
-
// Covers non-JSON stdin, a JSON value that isn't an object, and
|
|
1193
|
-
// `"transcript_path": null` (or the key missing entirely) — all fail
|
|
1194
|
-
// the string check. Log and skip; never fall through to auto-discovery.
|
|
1195
|
-
appendPreCompactLog(logFile, 'skip: malformed or incomplete PreCompact payload (missing string transcript_path)');
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
if ('session_id' in payload && isStringValue(payload.session_id))
|
|
1199
|
-
sessionId = payload.session_id;
|
|
1200
|
-
payloadTranscriptPath = payload.transcript_path;
|
|
1201
|
-
payloadCwd = 'cwd' in payload && isStringValue(payload.cwd) ? payload.cwd : null;
|
|
1202
|
-
payloadTrigger = 'trigger' in payload && isStringValue(payload.trigger) ? payload.trigger : null;
|
|
1203
|
-
}
|
|
1204
|
-
// X11: payload transcript_path must end .jsonl. No directory-containment
|
|
1205
|
-
// check is applied on top of this — CLAUDE_CONFIG_DIR can relocate the
|
|
1206
|
-
// transcript root entirely, so a path-prefix allowlist would just reject
|
|
1207
|
-
// legitimate relocated installs. The trust boundary here is process
|
|
1208
|
-
// identity, not path shape: a local process able to feed this hook
|
|
1209
|
-
// arbitrary stdin already runs as the same user who owns every transcript
|
|
1210
|
-
// this check could gate on, so containment buys no real isolation.
|
|
1211
|
-
if (payloadTranscriptPath !== null && !/\.jsonl$/i.test(payloadTranscriptPath)) {
|
|
1212
|
-
appendPreCompactLog(logFile, `skip: payload transcript_path is not a .jsonl file: ${payloadTranscriptPath}`);
|
|
1148
|
+
const receipt = readClaudeCodePreCompact(stdinText, stdinTimedOut);
|
|
1149
|
+
if (receipt.status !== 'received') {
|
|
1150
|
+
appendPreCompactLog(logFile, `skip: ${receipt.reason}`);
|
|
1213
1151
|
return;
|
|
1214
1152
|
}
|
|
1153
|
+
const { sessionId, transcriptPath: payloadTranscriptPath, cwd: payloadCwd, trigger: payloadTrigger } = receipt.input;
|
|
1215
1154
|
// The record is the "something saved before every compaction", so it lands even when no snapshot is derivable below.
|
|
1216
1155
|
let recordId = null;
|
|
1217
1156
|
if (sessionId !== null && sessionId !== '') {
|
package/dist/cli/shared.js
CHANGED
|
@@ -10,7 +10,7 @@ import { RejectedValueError } from '../rejection.js';
|
|
|
10
10
|
import { explainMatch } from '../search.js';
|
|
11
11
|
import { embedMemory } from '../embeddings.js';
|
|
12
12
|
import { loadConfig } from '../config.js';
|
|
13
|
-
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
13
|
+
import { openHippoDb, closeHippoDb, isSqliteBusy, noteStoreBusy } from '../db.js';
|
|
14
14
|
import { hookPayloadSessionId, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
|
|
15
15
|
import { isGitRepo, fetchGitLog, extractLessons, partitionLessons } from '../autolearn.js';
|
|
16
16
|
import { storedTextKeys, duplicateKey } from '../same-text.js';
|
|
@@ -22,7 +22,7 @@ import { extractPathTags } from '../path-context.js';
|
|
|
22
22
|
import { getGlobalRoot, initGlobal } from '../shared.js';
|
|
23
23
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, buildSchtasksCreateArgs, buildWindowsTaskRun } from '../scheduler.js';
|
|
24
24
|
import { sanitizeLogMessage } from '../capture.js';
|
|
25
|
-
import { appendAuditEvent } from '../audit.js';
|
|
25
|
+
import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
26
26
|
import { createHash } from 'node:crypto';
|
|
27
27
|
import * as client from '../client.js';
|
|
28
28
|
import { detectServer, removePidfileIfOwned } from '../server-detect.js';
|
|
@@ -77,8 +77,9 @@ export function emitCliAudit(hippoRoot, op, targetId, metadata) {
|
|
|
77
77
|
closeHippoDb(db);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
catch {
|
|
81
|
-
//
|
|
80
|
+
catch (error) {
|
|
81
|
+
// Best effort: the command already did its work.
|
|
82
|
+
reportAuditWriteFailure(op, String(error), targetId);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
export function requireInit(hippoRoot) {
|
|
@@ -158,7 +159,7 @@ export async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
158
159
|
// Every caller of this helper is a non-idempotent write, so replaying on
|
|
159
160
|
// the direct path would store a row the server may already have committed.
|
|
160
161
|
// Leave the pidfile alone: the next command's connect-phase failure heals it.
|
|
161
|
-
console.error(`hippo: the connection to ${info.url} dropped mid-request, so the write may already have been applied. Not retrying locally. Check with \`hippo recall\` before running this again.`);
|
|
162
|
+
console.error(`hippo: the connection to ${info.url} dropped or timed out mid-request, so the write may already have been applied. Not retrying locally. Check with \`hippo recall\` before running this again.`);
|
|
162
163
|
process.exit(1);
|
|
163
164
|
}
|
|
164
165
|
throw err;
|
|
@@ -485,7 +486,10 @@ export function withLedgerDb(hippoRoot, fn) {
|
|
|
485
486
|
db = openHippoDb(root);
|
|
486
487
|
return fn(db);
|
|
487
488
|
}
|
|
488
|
-
catch {
|
|
489
|
+
catch (error) {
|
|
490
|
+
// Best effort, but a busy store is the one failure an operator can act on, so it warns once.
|
|
491
|
+
if (isSqliteBusy(error))
|
|
492
|
+
noteStoreBusy('token ledger row skipped');
|
|
489
493
|
return undefined;
|
|
490
494
|
}
|
|
491
495
|
finally {
|
package/dist/cli.js
CHANGED
|
@@ -59,7 +59,8 @@ import { resolveEmbeddingProvider } from './embedding-provider.js';
|
|
|
59
59
|
import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
60
60
|
import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
61
61
|
import { loadConfig } from './config.js';
|
|
62
|
-
import {
|
|
62
|
+
import { ensurePilotArm, hashArm, readPilotArm } from './pilot-arm.js';
|
|
63
|
+
import { openHippoDb, closeHippoDb, withSharedStoreHandles, HOOK_DB_WAIT_MS, isSqliteBusy, noteStoreBusy } from './db.js';
|
|
63
64
|
import { runDoctor, formatDoctor } from './doctor.js';
|
|
64
65
|
import { buildSupportBundle, TAIL_MAX_LINES } from './support-bundle.js';
|
|
65
66
|
import { PACKAGE_VERSION } from './version.js';
|
|
@@ -88,6 +89,7 @@ import { listApiKeys } from './auth.js';
|
|
|
88
89
|
import { buildProvenanceCoverage } from './provenance-coverage.js';
|
|
89
90
|
import { buildCorrectionLatency } from './correction-latency.js';
|
|
90
91
|
import * as api from './api.js';
|
|
92
|
+
import { vetSecrets } from './secret-detect.js';
|
|
91
93
|
import * as predictionsModule from './predictions.js';
|
|
92
94
|
import { computePlanningFallacyOutput } from './predictions.js';
|
|
93
95
|
import * as decisionsModule from './decisions.js';
|
|
@@ -695,6 +697,8 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
695
697
|
console.log(` Tags: ${entry.tags.join(', ')}`);
|
|
696
698
|
if (entry.pinned)
|
|
697
699
|
console.log(' Pinned (no decay)');
|
|
700
|
+
for (const w of vetSecrets(entry.content, entry.tags, false).warnings)
|
|
701
|
+
console.error(`Warning: ${w}`);
|
|
698
702
|
void embedMemory(targetRoot, entry);
|
|
699
703
|
const config = loadConfig(targetRoot);
|
|
700
704
|
const shouldExtract = flags['extract'] || config.extraction.enabled === true;
|
|
@@ -1908,6 +1912,10 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
1908
1912
|
}
|
|
1909
1913
|
}
|
|
1910
1914
|
}
|
|
1915
|
+
// A compaction follows a prompt or SessionStart that booked the arm, so this only reads it.
|
|
1916
|
+
if (!suppressOutput && payloadSessionId !== null && inPilotHoldout(hippoRoot, resolveTenantId({}), payloadSessionId, false)) {
|
|
1917
|
+
suppressOutput = true;
|
|
1918
|
+
}
|
|
1911
1919
|
if (!suppressOutput) {
|
|
1912
1920
|
const tenantId = resolveTenantId({});
|
|
1913
1921
|
const snapshot = loadFreshActiveTaskSnapshot(hippoRoot, tenantId, { maxAgeMs: COMPACT_RESUME_MAX_AGE_MS });
|
|
@@ -5191,6 +5199,18 @@ function cmdCurrent(hippoRoot, args, flags) {
|
|
|
5191
5199
|
console.error('Usage: hippo current <show>');
|
|
5192
5200
|
process.exit(1);
|
|
5193
5201
|
}
|
|
5202
|
+
/** Hook commands share one handle per store and wait at most HOOK_DB_WAIT_MS for a lock; a store still busy after that skips the hook's work with one warning, exit 0. */
|
|
5203
|
+
async function runHookWithStores(fn) {
|
|
5204
|
+
try {
|
|
5205
|
+
return await withSharedStoreHandles(fn, { busyWaitMs: HOOK_DB_WAIT_MS });
|
|
5206
|
+
}
|
|
5207
|
+
catch (error) {
|
|
5208
|
+
if (!isSqliteBusy(error))
|
|
5209
|
+
throw error;
|
|
5210
|
+
noteStoreBusy('hook skipped');
|
|
5211
|
+
return undefined;
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5194
5214
|
async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
5195
5215
|
const rec = startDeliveryRecorder(hippoRoot, flags, stdinText);
|
|
5196
5216
|
// No try/finally: a render throw keeps its own exit code and writes no event.
|
|
@@ -5231,6 +5251,22 @@ function flushDeliveryRecorder(rec, db) {
|
|
|
5231
5251
|
console.error(`[hippo] delivery ledger write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5232
5252
|
}
|
|
5233
5253
|
}
|
|
5254
|
+
/**
|
|
5255
|
+
* Whether this session sits in the pilot's holdout arm (src/pilot-arm.ts). Off at rate 0 and with no session id.
|
|
5256
|
+
* `write` books the arm row; a read-only caller (env-only id, sub-agent) follows the stored arm, else the hash.
|
|
5257
|
+
*/
|
|
5258
|
+
function inPilotHoldout(hippoRoot, tenantId, sessionId, write) {
|
|
5259
|
+
if (sessionId === undefined || sessionId.trim() === '')
|
|
5260
|
+
return false;
|
|
5261
|
+
const root = isInitialized(hippoRoot) ? hippoRoot : isInitialized(getGlobalRoot()) ? getGlobalRoot() : null;
|
|
5262
|
+
if (root === null)
|
|
5263
|
+
return false;
|
|
5264
|
+
const rate = loadConfig(root).pilot.holdoutRateBp;
|
|
5265
|
+
if (rate <= 0)
|
|
5266
|
+
return false;
|
|
5267
|
+
const arm = withLedgerDb(hippoRoot, (db) => write ? ensurePilotArm(db, tenantId, sessionId, rate) : readPilotArm(db, sessionId) ?? hashArm(sessionId, rate));
|
|
5268
|
+
return (arm ?? hashArm(sessionId, rate)) === 'holdout';
|
|
5269
|
+
}
|
|
5234
5270
|
async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
5235
5271
|
// --pinned-only fires on every UserPromptSubmit — including in directories
|
|
5236
5272
|
// that don't have a local .hippo. Skip requireInit for that path and fall
|
|
@@ -5240,30 +5276,6 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
5240
5276
|
if (!pinnedOnly) {
|
|
5241
5277
|
requireInit(hippoRoot);
|
|
5242
5278
|
}
|
|
5243
|
-
const budget = parseBudgetFlag(flags['budget'], 1500);
|
|
5244
|
-
if (budget <= 0) {
|
|
5245
|
-
rec?.disabled();
|
|
5246
|
-
return;
|
|
5247
|
-
}
|
|
5248
|
-
// Resolve query: explicit args, --auto (git diff via CLI-side helper), or
|
|
5249
|
-
// fall through to api.getContext's '*' fallback. api.getContext is host-
|
|
5250
|
-
// agnostic so the auto-detect (which shells out to git) stays CLI-side.
|
|
5251
|
-
let query = args.join(' ').trim();
|
|
5252
|
-
if (!query && flags['auto']) {
|
|
5253
|
-
query = autoDetectContext();
|
|
5254
|
-
}
|
|
5255
|
-
// Scope detection (CLI-side: uses cwd). api.getContext takes the resolved
|
|
5256
|
-
// scope via opts.scope to stay host-agnostic.
|
|
5257
|
-
const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
5258
|
-
const ctxActiveScope = ctxExplicitScope || detectScope();
|
|
5259
|
-
const ctx = {
|
|
5260
|
-
hippoRoot,
|
|
5261
|
-
tenantId: resolveTenantId({}),
|
|
5262
|
-
actor: api.adminActor('cli'),
|
|
5263
|
-
};
|
|
5264
|
-
// v39 memory scope isolation: --cross-project re-includes other-project
|
|
5265
|
-
// memories (rendered under a demarcated section below).
|
|
5266
|
-
const crossProject = flags['cross-project'] === true;
|
|
5267
5279
|
// DF1 T2: resolve the calling session's id for the bounded active-task-
|
|
5268
5280
|
// snapshot read (api.getContext -> loadFreshActiveTaskSnapshot). Stdin
|
|
5269
5281
|
// payload (the UserPromptSubmit hook JSON) wins; falls back to
|
|
@@ -5291,6 +5303,36 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
5291
5303
|
const ledgerSessionId = subagent ? undefined : currentSessionId;
|
|
5292
5304
|
if (subagent)
|
|
5293
5305
|
payloadSessionId = undefined;
|
|
5306
|
+
// The pilot arm is booked at the first hook call whatever the flags, so the holdout sees no budget or content branch.
|
|
5307
|
+
const resolvedTenant = resolveTenantId({});
|
|
5308
|
+
if (inPilotHoldout(hippoRoot, resolvedTenant, currentSessionId, payloadSessionId !== undefined)) {
|
|
5309
|
+
rec?.disabled();
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5312
|
+
const budget = parseBudgetFlag(flags['budget'], 1500);
|
|
5313
|
+
if (budget <= 0) {
|
|
5314
|
+
rec?.disabled();
|
|
5315
|
+
return;
|
|
5316
|
+
}
|
|
5317
|
+
// Resolve query: explicit args, --auto (git diff via CLI-side helper), or
|
|
5318
|
+
// fall through to api.getContext's '*' fallback. api.getContext is host-
|
|
5319
|
+
// agnostic so the auto-detect (which shells out to git) stays CLI-side.
|
|
5320
|
+
let query = args.join(' ').trim();
|
|
5321
|
+
if (!query && flags['auto']) {
|
|
5322
|
+
query = autoDetectContext();
|
|
5323
|
+
}
|
|
5324
|
+
// Scope detection (CLI-side: uses cwd). api.getContext takes the resolved
|
|
5325
|
+
// scope via opts.scope to stay host-agnostic.
|
|
5326
|
+
const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
5327
|
+
const ctxActiveScope = ctxExplicitScope || detectScope();
|
|
5328
|
+
const ctx = {
|
|
5329
|
+
hippoRoot,
|
|
5330
|
+
tenantId: resolvedTenant,
|
|
5331
|
+
actor: api.adminActor('cli'),
|
|
5332
|
+
};
|
|
5333
|
+
// v39 memory scope isolation: --cross-project re-includes other-project
|
|
5334
|
+
// memories (rendered under a demarcated section below).
|
|
5335
|
+
const crossProject = flags['cross-project'] === true;
|
|
5294
5336
|
const format = String(flags['format'] ?? 'markdown');
|
|
5295
5337
|
const framing = String(flags['framing'] ?? 'observe');
|
|
5296
5338
|
const opts = {
|
|
@@ -5448,7 +5490,10 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
5448
5490
|
items: staticItems.length, tokens: estimateTokens(finalStatic), hash: blockHash(finalStatic),
|
|
5449
5491
|
});
|
|
5450
5492
|
}
|
|
5451
|
-
catch {
|
|
5493
|
+
catch (error) {
|
|
5494
|
+
if (isSqliteBusy(error))
|
|
5495
|
+
noteStoreBusy('token ledger row skipped');
|
|
5496
|
+
}
|
|
5452
5497
|
}
|
|
5453
5498
|
if (recallBlock) {
|
|
5454
5499
|
try {
|
|
@@ -5457,7 +5502,10 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
5457
5502
|
items: recallItems.length, tokens: estimateTokens(recallBlock), hash: blockHash(recallBlock),
|
|
5458
5503
|
});
|
|
5459
5504
|
}
|
|
5460
|
-
catch {
|
|
5505
|
+
catch (error) {
|
|
5506
|
+
if (isSqliteBusy(error))
|
|
5507
|
+
noteStoreBusy('token ledger row skipped');
|
|
5508
|
+
}
|
|
5461
5509
|
}
|
|
5462
5510
|
flushDeliveryRecorder(rec, db);
|
|
5463
5511
|
});
|
|
@@ -5678,6 +5726,10 @@ function cmdLearn(hippoRoot, flags) {
|
|
|
5678
5726
|
// ---------------------------------------------------------------------------
|
|
5679
5727
|
// Import command
|
|
5680
5728
|
// ---------------------------------------------------------------------------
|
|
5729
|
+
function warnRedacted(count) {
|
|
5730
|
+
if (count)
|
|
5731
|
+
console.error(`Warning: secret-shaped text was redacted from ${count} imported ${count === 1 ? 'entry' : 'entries'} before storing`);
|
|
5732
|
+
}
|
|
5681
5733
|
function cmdImport(hippoRoot, args, flags) {
|
|
5682
5734
|
const useGlobal = Boolean(flags['global']);
|
|
5683
5735
|
const dryRun = Boolean(flags['dry-run']);
|
|
@@ -5758,6 +5810,7 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5758
5810
|
if ((vaultResult.rejected ?? 0) > 0) {
|
|
5759
5811
|
console.log(` Rejected (tombstoned): ${vaultResult.rejected}`);
|
|
5760
5812
|
}
|
|
5813
|
+
warnRedacted(vaultResult.redacted);
|
|
5761
5814
|
console.log(` ${dryRun ? 'Would archive: ' : 'Archived (removed): '}${vaultResult.archived ?? 0}`);
|
|
5762
5815
|
console.log(` Store: ${hippoRoot}`);
|
|
5763
5816
|
// Batch producer, same contract as the single-file import below: vault rows
|
|
@@ -5829,6 +5882,7 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5829
5882
|
if ((result.rejected ?? 0) > 0) {
|
|
5830
5883
|
console.log(` Rejected (tombstoned): ${result.rejected}`);
|
|
5831
5884
|
}
|
|
5885
|
+
warnRedacted(result.redacted);
|
|
5832
5886
|
if (dryRun) {
|
|
5833
5887
|
console.log('\n (dry run - nothing written)');
|
|
5834
5888
|
if (result.entries.length > 0) {
|
|
@@ -7922,6 +7976,8 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7922
7976
|
});
|
|
7923
7977
|
console.log(`Remembered [${result.id}] (via ${info.url})`);
|
|
7924
7978
|
console.log(` Kind: ${result.kind} | Tenant: ${result.tenantId}`);
|
|
7979
|
+
for (const w of result.warnings ?? [])
|
|
7980
|
+
console.error(`Warning: ${w}`);
|
|
7925
7981
|
});
|
|
7926
7982
|
if (remembered)
|
|
7927
7983
|
break;
|
|
@@ -8012,11 +8068,13 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8012
8068
|
case 'pre-compact': {
|
|
8013
8069
|
// Bounded wait, not a TTY guard: an idle non-TTY pipe must not hang.
|
|
8014
8070
|
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8071
|
+
await runHookWithStores(async () => {
|
|
8072
|
+
resetHookInjection(hippoRoot, stdinText, null);
|
|
8073
|
+
await cmdPreCompact(hookStoreRoot(hippoRoot), {
|
|
8074
|
+
stdinText,
|
|
8075
|
+
stdinTimedOut,
|
|
8076
|
+
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
8077
|
+
});
|
|
8020
8078
|
});
|
|
8021
8079
|
break;
|
|
8022
8080
|
}
|
|
@@ -8025,7 +8083,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8025
8083
|
const { text } = await readStdinBounded();
|
|
8026
8084
|
const logFlag = flags['log-file'];
|
|
8027
8085
|
const store = hookStoreRoot(hippoRoot);
|
|
8028
|
-
const line = cmdPostCompact(store, {
|
|
8086
|
+
const line = await runHookWithStores(() => cmdPostCompact(store, {
|
|
8029
8087
|
stdinText: text,
|
|
8030
8088
|
logFile: logFlag === true || logFlag === false || Array.isArray(logFlag) ? undefined : logFlag,
|
|
8031
8089
|
// Passed in, since capture.ts importing the sync would close an import cycle.
|
|
@@ -8037,8 +8095,8 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8037
8095
|
for (const warning of report.warnings)
|
|
8038
8096
|
log(`agent memories: ${warning}`);
|
|
8039
8097
|
},
|
|
8040
|
-
});
|
|
8041
|
-
if (line !== null)
|
|
8098
|
+
}));
|
|
8099
|
+
if (line !== null && line !== undefined)
|
|
8042
8100
|
console.log(line);
|
|
8043
8101
|
break;
|
|
8044
8102
|
}
|
|
@@ -8051,7 +8109,8 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8051
8109
|
const payload = (text ?? '').trim();
|
|
8052
8110
|
if (isInitialized(root) && payload) {
|
|
8053
8111
|
// SAFETY: JSON.parse returns a JSON value by definition.
|
|
8054
|
-
|
|
8112
|
+
const failure = JSON.parse(payload);
|
|
8113
|
+
await runHookWithStores(() => captureToolFailure(root, resolveTenantId({}), failure));
|
|
8055
8114
|
}
|
|
8056
8115
|
}
|
|
8057
8116
|
catch {
|
|
@@ -8061,8 +8120,10 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8061
8120
|
}
|
|
8062
8121
|
case 'compact-resume': {
|
|
8063
8122
|
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8064
|
-
|
|
8065
|
-
|
|
8123
|
+
await runHookWithStores(() => {
|
|
8124
|
+
resetHookInjection(hippoRoot, stdinText, 'compact');
|
|
8125
|
+
cmdCompactResume(hookStoreRoot(hippoRoot), stdinText, stdinTimedOut);
|
|
8126
|
+
});
|
|
8066
8127
|
break;
|
|
8067
8128
|
}
|
|
8068
8129
|
case 'codex-run':
|
|
@@ -8337,7 +8398,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8337
8398
|
// Bounded, not a TTY guard (DF1 T2, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
8338
8399
|
// the hot stdin path and a manual run share this one command.
|
|
8339
8400
|
const { text: stdinText } = await readStdinBounded();
|
|
8340
|
-
await cmdContext(hookStoreRoot(hippoRoot), args, flags, stdinText);
|
|
8401
|
+
await runHookWithStores(() => cmdContext(hookStoreRoot(hippoRoot), args, flags, stdinText));
|
|
8341
8402
|
break;
|
|
8342
8403
|
}
|
|
8343
8404
|
case 'hook':
|
package/dist/client.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* Network errors (ECONNREFUSED on a stale pidfile, etc.) propagate as the
|
|
14
14
|
* native fetch failure so the caller can detect them and self-heal.
|
|
15
15
|
*/
|
|
16
|
+
/** A write the local server has not answered in this long is stuck; the caller treats it as delivery-unknown. */
|
|
17
|
+
const SERVER_TIMEOUT_MS = 30_000;
|
|
16
18
|
function buildHeaders(apiKey, withBody) {
|
|
17
19
|
const headers = {};
|
|
18
20
|
if (withBody)
|
|
@@ -58,6 +60,7 @@ export async function remember(serverUrl, apiKey, opts) {
|
|
|
58
60
|
const res = await fetch(`${serverUrl}/v1/memories`, {
|
|
59
61
|
method: 'POST',
|
|
60
62
|
headers: buildHeaders(apiKey, true),
|
|
63
|
+
signal: AbortSignal.timeout(SERVER_TIMEOUT_MS),
|
|
61
64
|
body: JSON.stringify(opts),
|
|
62
65
|
});
|
|
63
66
|
if (!res.ok)
|
|
@@ -69,6 +72,7 @@ export async function forget(serverUrl, apiKey, id) {
|
|
|
69
72
|
const res = await fetch(`${serverUrl}/v1/memories/${encodeURIComponent(id)}`, {
|
|
70
73
|
method: 'DELETE',
|
|
71
74
|
headers: buildHeaders(apiKey, false),
|
|
75
|
+
signal: AbortSignal.timeout(SERVER_TIMEOUT_MS),
|
|
72
76
|
});
|
|
73
77
|
if (!res.ok)
|
|
74
78
|
await throwForStatus(res);
|
|
@@ -79,6 +83,7 @@ export async function promote(serverUrl, apiKey, id) {
|
|
|
79
83
|
const res = await fetch(`${serverUrl}/v1/memories/${encodeURIComponent(id)}/promote`, {
|
|
80
84
|
method: 'POST',
|
|
81
85
|
headers: buildHeaders(apiKey, false),
|
|
86
|
+
signal: AbortSignal.timeout(SERVER_TIMEOUT_MS),
|
|
82
87
|
});
|
|
83
88
|
if (!res.ok)
|
|
84
89
|
await throwForStatus(res);
|
|
@@ -89,6 +94,7 @@ export async function archiveRaw(serverUrl, apiKey, id, reason) {
|
|
|
89
94
|
const res = await fetch(`${serverUrl}/v1/memories/${encodeURIComponent(id)}/archive`, {
|
|
90
95
|
method: 'POST',
|
|
91
96
|
headers: buildHeaders(apiKey, true),
|
|
97
|
+
signal: AbortSignal.timeout(SERVER_TIMEOUT_MS),
|
|
92
98
|
body: JSON.stringify({ reason }),
|
|
93
99
|
});
|
|
94
100
|
if (!res.ok)
|
|
@@ -123,6 +129,9 @@ export function classifyTransportFailure(err) {
|
|
|
123
129
|
}
|
|
124
130
|
if (message.includes('socket hang up') || message.includes('fetch failed'))
|
|
125
131
|
return 'delivery-unknown';
|
|
132
|
+
// The request was sent and the server went quiet, so it may have committed.
|
|
133
|
+
if (err.name === 'TimeoutError')
|
|
134
|
+
return 'delivery-unknown';
|
|
126
135
|
return 'none';
|
|
127
136
|
}
|
|
128
137
|
//# sourceMappingURL=client.js.map
|
package/dist/codex-patch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Files named by a Codex apply_patch call, read the way openai/codex codex-rs/apply-patch reads them
|
|
2
2
|
// (parser.rs for the body, invocation.rs for the shell forms it runs as apply_patch).
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import { isStringValue } from './capture.js';
|
|
4
|
+
import { isStringValue } from './capture-contract.js';
|
|
5
5
|
/** Codex prints this line only when a patch applied (apply-patch lib.rs). */
|
|
6
6
|
export const PATCH_SUCCESS_LINE = 'Success. Updated the following files:';
|
|
7
7
|
const BEGIN_PATCH = '*** Begin Patch';
|
|
@@ -2,7 +2,7 @@ import { type DatabaseSyncLike } from './db.js';
|
|
|
2
2
|
/** PostCompact has 10 s in all (PreCompact 30 s), so a locked store must be given up on early. */
|
|
3
3
|
export declare const COMPACTION_DB_WAIT_MS = 2000;
|
|
4
4
|
/** Tested verbatim: Claude Code hands PreCompact stdout to the summariser as instructions. */
|
|
5
|
-
export declare const PRE_COMPACT_INSTRUCTION = "In your summary, add a last section titled 'Memories for hippo'. List, one per line starting with '- ', each lesson learned, decision made (with its reason) and correction the user gave in this session that should outlive it. Write each as a standalone sentence that names its subject. Leave out anything an earlier summary already listed under 'Memories for hippo'
|
|
5
|
+
export declare const PRE_COMPACT_INSTRUCTION = "In your summary, add a last section titled 'Memories for hippo'. List, one per line starting with '- ', each lesson learned, decision made (with its reason) and correction the user gave in this session that should outlive it. Write each as a standalone sentence that names its subject. Leave out anything an earlier summary already listed under 'Memories for hippo', and anything this session already saved with `hippo remember`. Write '- none' if nothing new remains.";
|
|
6
6
|
/** Long enough that a live hook has finished with its own record. */
|
|
7
7
|
export declare const REPLAY_AFTER_MS: number;
|
|
8
8
|
/** Claude Code deletes transcripts after 30 days, so an older gap can never be filled. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// One record per Claude Code compaction, and what turns its summary into kept memories.
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import { errorMessage,
|
|
4
|
+
import { errorMessage, readTranscriptTail, truncateCodePointSafe } from './capture.js';
|
|
5
|
+
import { isObjectLike, isStringValue } from './capture-contract.js';
|
|
5
6
|
import { compactSummaryBody, parseCompactionItems, selectItemRows } from './compaction-items.js';
|
|
6
7
|
import { loadConfig } from './config.js';
|
|
7
8
|
import { closeHippoDb, isSqliteBusy, openHippoDb } from './db.js';
|
|
@@ -14,7 +15,7 @@ import { resolveTenantId } from './tenant.js';
|
|
|
14
15
|
/** PostCompact has 10 s in all (PreCompact 30 s), so a locked store must be given up on early. */
|
|
15
16
|
export const COMPACTION_DB_WAIT_MS = 2000;
|
|
16
17
|
/** Tested verbatim: Claude Code hands PreCompact stdout to the summariser as instructions. */
|
|
17
|
-
export const PRE_COMPACT_INSTRUCTION = "In your summary, add a last section titled 'Memories for hippo'. List, one per line starting with '- ', each lesson learned, decision made (with its reason) and correction the user gave in this session that should outlive it. Write each as a standalone sentence that names its subject. Leave out anything an earlier summary already listed under 'Memories for hippo'
|
|
18
|
+
export const PRE_COMPACT_INSTRUCTION = "In your summary, add a last section titled 'Memories for hippo'. List, one per line starting with '- ', each lesson learned, decision made (with its reason) and correction the user gave in this session that should outlive it. Write each as a standalone sentence that names its subject. Leave out anything an earlier summary already listed under 'Memories for hippo', and anything this session already saved with `hippo remember`. Write '- none' if nothing new remains.";
|
|
18
19
|
const SUMMARY_MAX_CHARS = 256 * 1024;
|
|
19
20
|
/** Long enough that a live hook has finished with its own record. */
|
|
20
21
|
export const REPLAY_AFTER_MS = 10 * 60_000;
|
package/dist/config.d.ts
CHANGED
|
@@ -145,6 +145,11 @@ export interface HippoConfig {
|
|
|
145
145
|
deliveryLedger: {
|
|
146
146
|
enabled: boolean;
|
|
147
147
|
};
|
|
148
|
+
/** Pilot holdout (src/pilot-arm.ts): share of sessions, in basis points 0-10000, that get no memories pushed.
|
|
149
|
+
* Default 0 = off. Read from the store the token ledger writes to. */
|
|
150
|
+
pilot: {
|
|
151
|
+
holdoutRateBp: number;
|
|
152
|
+
};
|
|
148
153
|
}
|
|
149
154
|
export declare function loadConfig(hippoRoot: string): HippoConfig;
|
|
150
155
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
CHANGED
|
@@ -88,6 +88,9 @@ const DEFAULT_CONFIG = {
|
|
|
88
88
|
deliveryLedger: {
|
|
89
89
|
enabled: false,
|
|
90
90
|
},
|
|
91
|
+
pilot: {
|
|
92
|
+
holdoutRateBp: 0,
|
|
93
|
+
},
|
|
91
94
|
};
|
|
92
95
|
function isMemoryValueConfig(value) {
|
|
93
96
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -115,6 +118,19 @@ function deliveryLedgerEnabled(value) {
|
|
|
115
118
|
`(got ${JSON.stringify(value)}) - using false.`);
|
|
116
119
|
return false;
|
|
117
120
|
}
|
|
121
|
+
// Only an integer 0..10000 counts; anything else warns and turns the pilot off.
|
|
122
|
+
function pilotHoldoutRate(value) {
|
|
123
|
+
if (value === undefined)
|
|
124
|
+
return 0;
|
|
125
|
+
const rate = value?.holdoutRateBp;
|
|
126
|
+
if (rate === undefined && value !== null && value.constructor === Object)
|
|
127
|
+
return 0;
|
|
128
|
+
if (Number.isInteger(rate) && rate >= 0 && rate <= 10000)
|
|
129
|
+
return rate;
|
|
130
|
+
console.error(`Warning: config.json's "pilot" must be an object like {"holdoutRateBp": 2000}, an integer from 0 to 10000 ` +
|
|
131
|
+
`(got ${JSON.stringify(value)}) - using 0 (pilot off).`);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
118
134
|
function agentMemoryTools(value) {
|
|
119
135
|
if (value === undefined || value === null)
|
|
120
136
|
return null;
|
|
@@ -226,6 +242,7 @@ export function loadConfig(hippoRoot) {
|
|
|
226
242
|
},
|
|
227
243
|
agentMemories: { tools: agentMemoryTools(raw.agentMemories?.tools) },
|
|
228
244
|
deliveryLedger: { enabled: deliveryLedgerEnabled(raw.deliveryLedger) },
|
|
245
|
+
pilot: { holdoutRateBp: pilotHoldoutRate(raw.pilot) },
|
|
229
246
|
};
|
|
230
247
|
}
|
|
231
248
|
catch (err) {
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { openHippoDb, closeHippoDb } from '../../db.js';
|
|
2
2
|
import { verifyGitHubSignature } from './signature.js';
|
|
3
3
|
import { isGitHubWebhookEnvelope } from './types.js';
|
|
4
|
+
import { redactPayload, DLQ_REDACTED_NOTE } from '../../secret-detect.js';
|
|
4
5
|
export function writeToDlq(db, opts) {
|
|
6
|
+
const rawPayload = redactPayload(opts.rawPayload);
|
|
7
|
+
const redacted = rawPayload !== opts.rawPayload;
|
|
5
8
|
const result = db
|
|
6
9
|
.prepare(`INSERT INTO github_dlq
|
|
7
10
|
(tenant_id, raw_payload, error, event_name, delivery_id, signature,
|
|
8
11
|
installation_id, repo_full_name, received_at, bucket)
|
|
9
12
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
10
|
-
.run(opts.tenantId ?? '__unroutable__',
|
|
13
|
+
.run(opts.tenantId ?? '__unroutable__', rawPayload, redacted ? `${opts.error}; ${DLQ_REDACTED_NOTE}` : opts.error, opts.eventName ?? null, opts.deliveryId ?? null, redacted ? null : opts.signature ?? null, opts.installationId ?? null, opts.repoFullName ?? null, new Date().toISOString(), opts.bucket ?? 'parse_error');
|
|
11
14
|
return Number(result.lastInsertRowid);
|
|
12
15
|
}
|
|
13
16
|
const SELECT_COLUMNS = `id, tenant_id, raw_payload, error, event_name, delivery_id,
|
|
@@ -107,7 +110,7 @@ export async function replayDlqEntry(ctx, id, opts = {}) {
|
|
|
107
110
|
status: 'sig_missing',
|
|
108
111
|
memoryId: null,
|
|
109
112
|
retryCount: row.retryCount,
|
|
110
|
-
reason: 'legacy
|
|
113
|
+
reason: 'row has no signature (legacy, or redacted before storing); pass --force to replay',
|
|
111
114
|
};
|
|
112
115
|
}
|
|
113
116
|
const sigOk = verifyGitHubSignature({
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* operator signal, so this code path is now load-bearing.
|
|
11
11
|
*/
|
|
12
12
|
import { parseRateLimit } from './ratelimit.js';
|
|
13
|
+
import { fetchWithRetry } from '../../http-retry.js';
|
|
13
14
|
export class GitHubFetchError extends Error {
|
|
14
15
|
status;
|
|
15
16
|
bodyExcerpt;
|
|
@@ -42,14 +43,15 @@ function headersToRecord(h) {
|
|
|
42
43
|
});
|
|
43
44
|
return Object.fromEntries(entries);
|
|
44
45
|
}
|
|
46
|
+
const GITHUB_TIMEOUT_MS = 30_000;
|
|
45
47
|
export const realGitHubFetcher = async ({ url, token }) => {
|
|
46
|
-
const res = await
|
|
48
|
+
const res = await fetchWithRetry(url, {
|
|
47
49
|
headers: {
|
|
48
50
|
Authorization: `Bearer ${token}`,
|
|
49
51
|
Accept: 'application/vnd.github+json',
|
|
50
52
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
51
53
|
},
|
|
52
|
-
});
|
|
54
|
+
}, { timeoutMs: GITHUB_TIMEOUT_MS });
|
|
53
55
|
const headers = headersToRecord(res.headers);
|
|
54
56
|
const rateLimit = parseRateLimit(headers, res.status);
|
|
55
57
|
// Codex P1 #4: don't silently turn 401/403/404/500 into empty pages.
|
|
@@ -5,12 +5,16 @@ import { resolveTenantForTeam } from './tenant-routing.js';
|
|
|
5
5
|
import { verifySlackSignature } from './signature.js';
|
|
6
6
|
import { isSlackEventEnvelope, isSlackMessageEvent } from './types.js';
|
|
7
7
|
import { handleMessageDeleted } from './deletion.js';
|
|
8
|
+
import { redactPayload, DLQ_REDACTED_NOTE } from '../../secret-detect.js';
|
|
8
9
|
export function writeToDlq(db, opts) {
|
|
10
|
+
const rawPayload = redactPayload(opts.rawPayload);
|
|
11
|
+
// A redacted body can never match its signature, so the row keeps none and replay needs --force.
|
|
12
|
+
const redacted = rawPayload !== opts.rawPayload;
|
|
9
13
|
const result = db
|
|
10
14
|
.prepare(`INSERT INTO slack_dlq
|
|
11
15
|
(tenant_id, team_id, raw_payload, error, received_at, bucket, signature, slack_timestamp)
|
|
12
16
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
13
|
-
.run(opts.tenantId ?? '__unroutable__', opts.teamId ?? null,
|
|
17
|
+
.run(opts.tenantId ?? '__unroutable__', opts.teamId ?? null, rawPayload, redacted ? `${opts.error}; ${DLQ_REDACTED_NOTE}` : opts.error, new Date().toISOString(), opts.bucket ?? 'parse_error', redacted ? null : opts.signature ?? null, opts.slackTimestamp ?? null);
|
|
14
18
|
return Number(result.lastInsertRowid);
|
|
15
19
|
}
|
|
16
20
|
export function listDlq(db, opts) {
|
|
@@ -95,7 +99,7 @@ export function replayDlqEntry(ctx, id, opts = {}) {
|
|
|
95
99
|
status: 'sig_missing',
|
|
96
100
|
memoryId: null,
|
|
97
101
|
retryCount: row.retryCount,
|
|
98
|
-
reason: '
|
|
102
|
+
reason: 'row has no signature/timestamp (legacy, or redacted before storing); pass --force to replay',
|
|
99
103
|
};
|
|
100
104
|
}
|
|
101
105
|
if (opts.signingSecret) {
|