claude-mem-lite 3.92.0 → 3.93.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +13 -0
- package/hash-utils.mjs +12 -0
- package/hook-llm.mjs +40 -9
- package/hook-optimize.mjs +4 -1
- package/hook-shared.mjs +6 -1
- package/hook.mjs +45 -31
- package/install.mjs +15 -11
- package/lib/export-columns.mjs +45 -0
- package/lib/get-core.mjs +17 -0
- package/lib/hook-stdin.mjs +134 -0
- package/lib/plugin-key.mjs +44 -0
- package/lib/registry-core.mjs +48 -0
- package/lib/resolve-data-dir.mjs +58 -0
- package/mem-cli.mjs +21 -37
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -1
- package/registry-recommend.mjs +2 -2
- package/schema.mjs +18 -1
- package/scripts/hook-launcher.mjs +10 -0
- package/scripts/post-tool-recall.js +7 -4
- package/scripts/pre-agent-inject.js +12 -4
- package/scripts/pre-skill-bridge.js +9 -5
- package/scripts/pre-tool-recall.js +35 -7
- package/scripts/user-prompt-search.js +24 -24
- package/server.mjs +28 -46
- package/source-files.mjs +2 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// lib/plugin-key.mjs — the plugin's identity in Claude Code's settings, and the one
|
|
2
|
+
// predicate that reads it.
|
|
3
|
+
//
|
|
4
|
+
// Audit 2026-09-02 P2-7. `PLUGIN_KEY` and `isPluginExplicitlyDisabled` shipped twice in JS —
|
|
5
|
+
// `hook.mjs` (which exits 0 on every event when the plugin is off) and `install.mjs` (which
|
|
6
|
+
// reports it in `doctor`/`status` and branches on it during cleanup) — plus a third,
|
|
7
|
+
// deliberate copy in `scripts/post-tool-use.sh`.
|
|
8
|
+
//
|
|
9
|
+
// What makes the duplication load-bearing rather than untidy: `install.mjs` writes hooks
|
|
10
|
+
// DIRECTLY into `~/.claude/settings.json`, so turning the plugin off in the Claude UI does
|
|
11
|
+
// not remove them. They keep firing, and this predicate is the only thing that makes
|
|
12
|
+
// "disabled" mean disabled. A key that drifts on one side leaves the user with a plugin they
|
|
13
|
+
// switched off and a hook set that never noticed.
|
|
14
|
+
//
|
|
15
|
+
// ZERO DEPENDENCIES: `hook.mjs` imports this at module scope on the hottest path in the
|
|
16
|
+
// system, and the check runs before anything else can.
|
|
17
|
+
//
|
|
18
|
+
// The shell copy in `scripts/post-tool-use.sh` STAYS a copy and is not a defect. That path
|
|
19
|
+
// is the ~5 ms bash pre-filter whose entire purpose is never reaching `node`, so it cannot
|
|
20
|
+
// import anything; its own header documents the parity requirement and
|
|
21
|
+
// `tests/post-tool-use-disabled.test.mjs` pins it. Two homes with a tested bridge is a
|
|
22
|
+
// different thing from two homes and a comment.
|
|
23
|
+
|
|
24
|
+
/** Marketplace this plugin is published under. */
|
|
25
|
+
export const MARKETPLACE_KEY = 'sdsrss';
|
|
26
|
+
|
|
27
|
+
/** The key Claude Code writes under `enabledPlugins` in `~/.claude/settings.json`. */
|
|
28
|
+
export const PLUGIN_KEY = `claude-mem-lite@${MARKETPLACE_KEY}`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether the user has EXPLICITLY switched the plugin off.
|
|
32
|
+
*
|
|
33
|
+
* Strict `=== false`, not falsiness: an absent key means "never installed via the
|
|
34
|
+
* marketplace" (an npm-only install, which must keep working) and `true` means enabled.
|
|
35
|
+
* Only an explicit `false` is a decision to honour — treating absent as disabled would
|
|
36
|
+
* silently kill every direct-install user.
|
|
37
|
+
*
|
|
38
|
+
* @param {object|null|undefined} settings Parsed ~/.claude/settings.json, or nullish when
|
|
39
|
+
* it is missing or unreadable — which reads as NOT disabled, so an unparseable settings
|
|
40
|
+
* file fails open rather than disabling the plugin the user is trying to use.
|
|
41
|
+
*/
|
|
42
|
+
export function isPluginExplicitlyDisabled(settings) {
|
|
43
|
+
return settings?.enabledPlugins?.[PLUGIN_KEY] === false;
|
|
44
|
+
}
|
package/lib/registry-core.mjs
CHANGED
|
@@ -208,3 +208,51 @@ export async function enrichNamedResource(db, name, { confineTo, enrichResource,
|
|
|
208
208
|
const { status, error } = await enrichResourceRow(db, row, { confineTo, enrichResource, env });
|
|
209
209
|
return { status, name: row.name, error };
|
|
210
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* How a registry search hit should be invoked, and the path worth showing for it.
|
|
214
|
+
*
|
|
215
|
+
* Audit 2026-09-02 P2-6: this ran to ~15 lines in `mem-cli.mjs cmdRegistry` and again in
|
|
216
|
+
* `server.mjs mem_registry`, and the copies had already diverged on `portablePath`:
|
|
217
|
+
*
|
|
218
|
+
* MCP `isManaged ? toPortable(local_path) : ''`
|
|
219
|
+
* CLI `isManaged && local_path.startsWith(home) ? '~'+… : (local_path || '')`
|
|
220
|
+
*
|
|
221
|
+
* For a NON-managed resource the CLI therefore printed `Path: /home/<user>/…` — an absolute
|
|
222
|
+
* path, un-tilde'd, for a row whose `Use:` line is `Skill("x")` or `mem_use(name=…)` and
|
|
223
|
+
* never mentions the path at all. MCP printed nothing. The MCP behaviour is the one kept:
|
|
224
|
+
* the line carried no action for the reader and spelled out a home directory to do it.
|
|
225
|
+
*
|
|
226
|
+
* Returns DATA, not a rendered line. The two faces genuinely differ in dialect — the CLI
|
|
227
|
+
* indents four spaces and prints through `out()`, MCP indents two and emits Markdown bold —
|
|
228
|
+
* and collapsing that would replace a real duplicate with a formatting flag.
|
|
229
|
+
*
|
|
230
|
+
* @param {{name:string,type:string,local_path?:string,invocation_name?:string}} r
|
|
231
|
+
* @param {{home:string, managedPrefix:string}} ctx `managedPrefix` is `<dataDir>/managed/`,
|
|
232
|
+
* already including the trailing separator, so the caller owns CLAUDE_MEM_DIR resolution.
|
|
233
|
+
* @returns {{isManaged:boolean, portablePath:string, howToUse:string}}
|
|
234
|
+
*/
|
|
235
|
+
export function resourceUseHint(r, { home, managedPrefix }) {
|
|
236
|
+
const isManaged = Boolean(r.local_path && r.local_path.includes(managedPrefix));
|
|
237
|
+
const portablePath = isManaged
|
|
238
|
+
? (r.local_path.startsWith(home) ? '~' + r.local_path.slice(home.length) : r.local_path)
|
|
239
|
+
: '';
|
|
240
|
+
const agentArg = r.type === 'agent' ? ', type="agent"' : '';
|
|
241
|
+
|
|
242
|
+
let howToUse;
|
|
243
|
+
if (isManaged) {
|
|
244
|
+
// Managed: Read(path) or mem_use — Skill() does not resolve managed resources.
|
|
245
|
+
// Agents always carry a complete .md path; only skills can be a directory, which
|
|
246
|
+
// resolves to its SKILL.md.
|
|
247
|
+
const resolvedPath = portablePath.endsWith('.md') ? portablePath : `${portablePath}/SKILL.md`;
|
|
248
|
+
howToUse = `Read("${resolvedPath}") or mem_use(name="${r.name}"${agentArg})`;
|
|
249
|
+
} else if (r.invocation_name) {
|
|
250
|
+
// Native plugin/user resource: invoke by its full invocation name.
|
|
251
|
+
howToUse = r.type === 'skill'
|
|
252
|
+
? `Skill("${r.invocation_name}")`
|
|
253
|
+
: `Agent(subagent_type="${r.invocation_name}")`;
|
|
254
|
+
} else {
|
|
255
|
+
howToUse = `mem_use(name="${r.name}"${agentArg})`;
|
|
256
|
+
}
|
|
257
|
+
return { isManaged, portablePath, howToUse };
|
|
258
|
+
}
|
package/lib/resolve-data-dir.mjs
CHANGED
|
@@ -82,3 +82,61 @@ export function resolveDataDir(raw) {
|
|
|
82
82
|
}
|
|
83
83
|
return containInTests(raw);
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The runtime directory: episode buffers, per-session markers, cooldowns, hook telemetry.
|
|
88
|
+
*
|
|
89
|
+
* Audit 2026-09-02 P1-14. `CLAUDE_MEM_RUNTIME_DIR` was honoured by exactly the five
|
|
90
|
+
* standalone hook scripts and `scripts/hook-launcher.mjs`, and IGNORED by `hook-shared.mjs`
|
|
91
|
+
* (which `hook.mjs`, `server.mjs`, `hook-context.mjs` and `hook-episode.mjs` all take it
|
|
92
|
+
* from) and by `hook-optimize.mjs`. So setting it did not relocate the runtime dir — it
|
|
93
|
+
* SPLIT it: the `fyi` and `pretool` faces wrote markers to the override while `ups` and
|
|
94
|
+
* `keyctx` read them from the real one. A harness pointing the system at an isolated
|
|
95
|
+
* runtime got a half-isolated one, silently, with no error and no empty directory to
|
|
96
|
+
* notice.
|
|
97
|
+
*
|
|
98
|
+
* The audit's own suggestion was to delete the variable from the five scripts. Measured
|
|
99
|
+
* before copying it: it is load-bearing in ten test files, in `hook-launcher.mjs`, and in
|
|
100
|
+
* `experiment/lib/arms.mjs`, which uses it to keep an experiment arm off the real state.
|
|
101
|
+
* Removing it would have cost all of that to fix a split that one shared resolver fixes.
|
|
102
|
+
* The defect was never that the knob exists — it is that the rule for reading it had eight
|
|
103
|
+
* hand-written homes and two modules that had never heard of it.
|
|
104
|
+
*
|
|
105
|
+
* WHAT BELONGS BEHIND THIS RESOLVER, AND WHAT DELIBERATELY DOES NOT. The v3.93.0 pre-tag
|
|
106
|
+
* review found the first cut of P1-14 half-applied — `scripts/user-prompt-search.js`
|
|
107
|
+
* resolved its RUNTIME_DIR here and then built its cross-hook marker from
|
|
108
|
+
* `join(DB_DIR, 'runtime')` anyway, so under the override the `fyi` face wrote the SHARED
|
|
109
|
+
* marker to one directory while `pretool` wrote it to another. Four subsystems were split
|
|
110
|
+
* that way (marker + skill cooldown, hook-error telemetry, the metrics sink and its GC, and
|
|
111
|
+
* the native-binding breakage marker, which is the self-heal trigger). The rule that
|
|
112
|
+
* resolved them, stated so the next sweep does not overshoot:
|
|
113
|
+
*
|
|
114
|
+
* MOVES with the override — state a HOOK writes and another component reads back:
|
|
115
|
+
* cross-hook injected-ids markers, skill/pre-recall cooldowns, hook-error telemetry,
|
|
116
|
+
* the native-binding breakage marker, `metrics/`, `ep-flush-*` / `pending-*` buffers,
|
|
117
|
+
* the shadow-recommendation log.
|
|
118
|
+
*
|
|
119
|
+
* STAYS under the data dir — state about the ONE REAL INSTALLATION: `install.lock`,
|
|
120
|
+
* `update-state.json`, `swap-in-progress`, and the `.update-staging-*` /
|
|
121
|
+
* `.update-backup-*` residue scan. These must NOT follow a per-harness override: the
|
|
122
|
+
* lock exists to serialise concurrent installers, and two installers pointed at
|
|
123
|
+
* different override directories would each take their own lock and both proceed.
|
|
124
|
+
*
|
|
125
|
+
* `tests/runtime-dir-single-home.test.mjs` sweeps for the defect FORM (a shipped module
|
|
126
|
+
* building `join(…, 'runtime')` itself) with the stays-put sites as a named allowlist, so
|
|
127
|
+
* moving one out of that list is a deliberate edit rather than an omission.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} dataDir An already-resolved data dir (from resolveDataDir).
|
|
130
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
131
|
+
* @returns {string} An absolute runtime directory.
|
|
132
|
+
*/
|
|
133
|
+
export function resolveRuntimeDir(dataDir, env = process.env) {
|
|
134
|
+
const raw = env.CLAUDE_MEM_RUNTIME_DIR;
|
|
135
|
+
// Same "falsy means default" rule as above. A relative override is NOT rejected the way
|
|
136
|
+
// CLAUDE_MEM_DIR is: this variable is set by test harnesses that predate that check, and
|
|
137
|
+
// turning a previously-working relative path into a throw would break isolation setups
|
|
138
|
+
// in order to enforce tidiness. It is resolved against cwd instead, so the value is at
|
|
139
|
+
// least absolute by the time anything writes to it.
|
|
140
|
+
if (raw === undefined || raw === null || raw === '') return join(dataDir, 'runtime');
|
|
141
|
+
return isAbsolute(raw) ? raw : resolve(raw);
|
|
142
|
+
}
|
package/mem-cli.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
7
|
+
import { resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
|
|
7
8
|
import { truncate, typeIcon, inferProject, scrubSecrets, COMPRESSED_PENDING_PURGE } from './utils.mjs';
|
|
8
9
|
import { resolveProject } from './project-utils.mjs';
|
|
9
10
|
// READ commands resolve the project DB-aware: a subdirectory whose own name holds no rows
|
|
@@ -17,11 +18,11 @@ import { resolveCliProject as cliProject } from './lib/cli-project.mjs';
|
|
|
17
18
|
import { _resetVocabCache, vecTextForRow, vectorsEnabled } from './tfidf.mjs';
|
|
18
19
|
import { reRankWithContext } from './search-scoring.mjs';
|
|
19
20
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
20
|
-
import { fetchObsDetail, fetchPromptDetail, fetchEventDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS, PROMPT_DETAIL_FIELDS, EVENT_DETAIL_FIELDS, supersededNotice } from './lib/get-core.mjs';
|
|
21
|
+
import { fetchObsDetail, fetchPromptDetail, fetchEventDetail, fetchSessionDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS, PROMPT_DETAIL_FIELDS, EVENT_DETAIL_FIELDS, supersededNotice } from './lib/get-core.mjs';
|
|
21
22
|
import { collectBrowseTiers, getActiveMemorySessionId, BROWSE_TIERS, BROWSE_TIER_LABELS } from './lib/browse-core.mjs';
|
|
22
23
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
23
24
|
import { ensureRegistryDb, collectRegistryStats, listResourcesRanked, formatRegistryListLine } from './registry.mjs';
|
|
24
|
-
import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources, enrichResourceRow, enrichImportedResources, enrichNamedResource, REGISTRY_CONFINE_ENV } from './lib/registry-core.mjs';
|
|
25
|
+
import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources, enrichResourceRow, enrichImportedResources, enrichNamedResource, REGISTRY_CONFINE_ENV, resourceUseHint } from './lib/registry-core.mjs';
|
|
25
26
|
import { searchResources } from './registry-retriever.mjs';
|
|
26
27
|
import { computeFunnel, formatFunnel, computeSweep, formatSweep, DEFAULT_SWEEP_FLOORS, DEFAULT_SWEEP_MARGINS } from './registry-recommend.mjs';
|
|
27
28
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
@@ -56,7 +57,7 @@ import { CLI_PATH, CLI_INVOKE } from './cli-path.mjs';
|
|
|
56
57
|
import { parseArgs, out, outVerbatim, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints, rejectBareStringFlags, resolvePositionalAlias, suggestUnknownFlags, OBS_TIME_FIELDS, formatObsFieldValue, obsFieldLabel, formatPendingPurgeLine } from './cli/common.mjs';
|
|
57
58
|
import { saveObservation, saveWithClosures, formatSupersedeSkipped, formatSupersededNote } from './lib/save-observation.mjs';
|
|
58
59
|
import { normalizeScope, insertObservationVector, applyObsUpdate } from './lib/observation-write.mjs';
|
|
59
|
-
import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
|
|
60
|
+
import { EXPORT_COLUMNS_SQL, buildExportWhere } from './lib/export-columns.mjs';
|
|
60
61
|
import { recallByFile } from './lib/recall-core.mjs';
|
|
61
62
|
import { fetchRecent, RECENT_MAX } from './lib/recent-core.mjs';
|
|
62
63
|
import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentTimeline, fetchTimelineWindow } from './lib/timeline-core.mjs';
|
|
@@ -561,8 +562,7 @@ function renderObsRows(db, ids, requestedFields) {
|
|
|
561
562
|
}
|
|
562
563
|
|
|
563
564
|
function renderSessionRows(db, ids) {
|
|
564
|
-
const
|
|
565
|
-
const rows = db.prepare(`SELECT * FROM session_summaries WHERE id IN (${placeholders}) ORDER BY created_at_epoch ASC`).all(...ids);
|
|
565
|
+
const rows = fetchSessionDetail(db, ids);
|
|
566
566
|
if (rows.length === 0) return null;
|
|
567
567
|
const parts = [];
|
|
568
568
|
for (const r of rows) {
|
|
@@ -1248,7 +1248,7 @@ async function cmdStats(db, args) {
|
|
|
1248
1248
|
// recorded in the last 24h. Surfaces silent breakage (DB corruption,
|
|
1249
1249
|
// CC upstream field rename) that would otherwise stay invisible — the
|
|
1250
1250
|
// failure mode that left code-graph's matcher bug undetected for 10 sessions.
|
|
1251
|
-
const hookErrors24h = countRecentHookErrors(
|
|
1251
|
+
const hookErrors24h = countRecentHookErrors(resolveRuntimeDir(DB_DIR), now - DAY_MS);
|
|
1252
1252
|
|
|
1253
1253
|
// M-9 (audit 2026-08-14): disk footprint — a "lite" store had accumulated 360MB of
|
|
1254
1254
|
// pre-maintain snapshots against a 59MB DB with nothing reporting it. Cheap probes
|
|
@@ -1654,42 +1654,37 @@ function cmdExport(db, args) {
|
|
|
1654
1654
|
// script (`export --to "$END" > backup.json`) with an unset `$END` would silently
|
|
1655
1655
|
// write an empty backup and report success. Reject like cmdSearch does.
|
|
1656
1656
|
if (rejectBareStringFlags(flags, ['project', 'type', 'from', 'to'])) return;
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
//
|
|
1660
|
-
//
|
|
1661
|
-
// of the live-row pair (backup/export of tombstones is opt-in, retractions are not).
|
|
1662
|
-
if (flags['include-compressed'] === true || flags['include-compressed'] === 'true') {
|
|
1663
|
-
wheres.push('superseded_at IS NULL');
|
|
1664
|
-
} else {
|
|
1665
|
-
wheres.push(liveObsFilterSql(''));
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1657
|
+
// PARSE + VALIDATE here; the SQL predicate itself comes from buildExportWhere (P2-5),
|
|
1658
|
+
// shared with server.mjs runExport. The split is deliberate: the CLI `fail()`s with a
|
|
1659
|
+
// usage message where the MCP tool throws, and only this face has a stderr channel for
|
|
1660
|
+
// the inverted-range note below.
|
|
1668
1661
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1669
|
-
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
1670
1662
|
if (flags.type) {
|
|
1671
1663
|
// Reject unknown types — silently returning [] for `--type bogus` looked like a
|
|
1672
1664
|
// legitimate empty filter result, hiding the typo. Mirrors cmdSearch / cmdSave / cmdUpdate.
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
fail(`[mem] Invalid --type "${flags.type}". Valid: ${[...validObsTypes].join(', ')}`);
|
|
1665
|
+
if (!OBS_TYPE_SET.has(flags.type)) {
|
|
1666
|
+
fail(`[mem] Invalid --type "${flags.type}". Valid: ${[...OBS_TYPE_SET].join(', ')}`);
|
|
1676
1667
|
return;
|
|
1677
1668
|
}
|
|
1678
|
-
wheres.push('type = ?'); params.push(flags.type);
|
|
1679
1669
|
}
|
|
1680
1670
|
let exportFromEpoch = null;
|
|
1681
1671
|
let exportToEpoch = null;
|
|
1682
1672
|
if (flags.from) {
|
|
1683
1673
|
exportFromEpoch = new Date(flags.from).getTime();
|
|
1684
1674
|
if (isNaN(exportFromEpoch)) { fail(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
|
|
1685
|
-
wheres.push('created_at_epoch >= ?'); params.push(exportFromEpoch);
|
|
1686
1675
|
}
|
|
1687
1676
|
if (flags.to) {
|
|
1688
1677
|
exportToEpoch = new Date(flags.to).getTime();
|
|
1689
1678
|
if (isNaN(exportToEpoch)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
|
|
1690
1679
|
if (/^\d{4}-\d{2}-\d{2}$/.test(flags.to)) exportToEpoch += DAY_MS - 1;
|
|
1691
|
-
wheres.push('created_at_epoch <= ?'); params.push(exportToEpoch);
|
|
1692
1680
|
}
|
|
1681
|
+
// --include-compressed: include compressed observations (aligned with MCP mem_export).
|
|
1682
|
+
// Superseded rows are excluded either way; the flag only toggles the compressed half
|
|
1683
|
+
// of the live-row pair (backup/export of tombstones is opt-in, retractions are not).
|
|
1684
|
+
const { wheres, params } = buildExportWhere({
|
|
1685
|
+
includeCompressed: flags['include-compressed'] === true || flags['include-compressed'] === 'true',
|
|
1686
|
+
project, type: flags.type || null, fromEpoch: exportFromEpoch, toEpoch: exportToEpoch,
|
|
1687
|
+
});
|
|
1693
1688
|
if (exportFromEpoch !== null && exportToEpoch !== null && exportFromEpoch > exportToEpoch) {
|
|
1694
1689
|
process.stderr.write(`[mem] Note: --from "${flags.from}" is after --to "${flags.to}"; this range is empty\n`);
|
|
1695
1690
|
}
|
|
@@ -2211,19 +2206,8 @@ function cmdRegistry(_memDb, args) {
|
|
|
2211
2206
|
for (const r of results) {
|
|
2212
2207
|
const badge = r.quality_tier === 'installed' ? '[✓]' : r.quality_tier === 'verified' ? '[★]' : '[○]';
|
|
2213
2208
|
const categoryLabel = r.category ? ` [${r.category}]` : '';
|
|
2214
|
-
|
|
2215
|
-
const portablePath
|
|
2216
|
-
let howToUse;
|
|
2217
|
-
if (isManaged) {
|
|
2218
|
-
const resolvedPath = portablePath.endsWith('.md') ? portablePath : `${portablePath}/SKILL.md`;
|
|
2219
|
-
howToUse = `Read("${resolvedPath}") or mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
|
|
2220
|
-
} else if (r.invocation_name) {
|
|
2221
|
-
howToUse = r.type === 'skill'
|
|
2222
|
-
? `Skill("${r.invocation_name}")`
|
|
2223
|
-
: `Agent(subagent_type="${r.invocation_name}")`;
|
|
2224
|
-
} else {
|
|
2225
|
-
howToUse = `mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
|
|
2226
|
-
}
|
|
2209
|
+
// P2-6: the invocation rule is shared; only the four-space indent is this face's.
|
|
2210
|
+
const { portablePath, howToUse } = resourceUseHint(r, { home, managedPrefix: join(DB_DIR, 'managed') + sep });
|
|
2227
2211
|
const pathLine = portablePath ? `\n Path: ${portablePath}` : '';
|
|
2228
2212
|
out(` ${badge} ${r.type === 'skill' ? 'S' : 'A'} ${r.name}${categoryLabel} — ${truncate(r.capability_summary || '', 80)}${pathLine}\n Use: ${howToUse}`);
|
|
2229
2213
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.93.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.93.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
12
12
|
"better-sqlite3": "^12.6.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.93.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -87,6 +87,8 @@
|
|
|
87
87
|
"lib/lesson-bridge.mjs",
|
|
88
88
|
"lib/binding-probe.mjs",
|
|
89
89
|
"lib/install-shape.mjs",
|
|
90
|
+
"lib/hook-stdin.mjs",
|
|
91
|
+
"lib/plugin-key.mjs",
|
|
90
92
|
"lib/hook-stdout.mjs",
|
|
91
93
|
"lib/proc-lock.mjs",
|
|
92
94
|
"lib/atomic-write.mjs",
|
package/registry-recommend.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `off` skips all work.
|
|
8
8
|
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, appendFileSync, readdirSync, unlinkSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
|
-
import { resolveDataDir } from './lib/resolve-data-dir.mjs';
|
|
10
|
+
import { resolveDataDir, resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
|
|
11
11
|
import { searchResources, cjkIntentTokens } from './registry-retriever.mjs';
|
|
12
12
|
|
|
13
13
|
import { DAY_MS } from './lib/time-constants.mjs';
|
|
@@ -57,7 +57,7 @@ const RECO_COOLDOWN_MS = 300_000; // 5 min, mirrors T4 SKILL_COOLDOWN_MS
|
|
|
57
57
|
// DB_DIR formula) so tests sandbox via env without ESM-cache gymnastics, and prod reads
|
|
58
58
|
// the same dir as the rest of the app.
|
|
59
59
|
function recoRuntimeDir() {
|
|
60
|
-
return
|
|
60
|
+
return resolveRuntimeDir(resolveDataDir(process.env.CLAUDE_MEM_DIR));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const TOKEN_SPLIT = /[^a-z0-9一-鿿]+/;
|
package/schema.mjs
CHANGED
|
@@ -152,7 +152,12 @@ export const CODE_DIR = join(homedir(), '.claude-mem-lite');
|
|
|
152
152
|
// (citation_surface_log.surface) in LATEST_MIGRATION_COLUMNS: a table that only
|
|
153
153
|
// the forced pass can create is unreachable forever once the version row says
|
|
154
154
|
// "done", which is not a hypothetical — see the note there.
|
|
155
|
-
|
|
155
|
+
// v47: two additive indexes (P2-11 + ALGO-7). The bump is LOAD-BEARING, not bookkeeping:
|
|
156
|
+
// `initSchema`'s fast path returns before the `CREATE INDEX IF NOT EXISTS` block, so on
|
|
157
|
+
// every existing install at v46 a new index there would simply never be created. Same trap
|
|
158
|
+
// the FTS5 migration hit — a DDL change that is not reachable from the version the DB
|
|
159
|
+
// already reports is a no-op with a convincing diff.
|
|
160
|
+
export const CURRENT_SCHEMA_VERSION = 47;
|
|
156
161
|
|
|
157
162
|
// Sentinel columns for the LATEST migration set(s). The fast-path uses these
|
|
158
163
|
// to self-heal half-migrated DBs — schema_version bumped but column ALTERs
|
|
@@ -551,6 +556,18 @@ export function initSchema(db) {
|
|
|
551
556
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sdk_sessions(project)`);
|
|
552
557
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_obs_not_compressed ON observations(created_at_epoch DESC) WHERE COALESCE(compressed_into, 0) = 0`);
|
|
553
558
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_handoffs_project_time ON session_handoffs(project, type, created_at_epoch DESC)`);
|
|
559
|
+
// v47 (audit 2026-09-02 P2-11 + the previous round's ALGO-7), one additive migration for
|
|
560
|
+
// both. Additive only: new indexes on existing columns, no table rewrite, no data move.
|
|
561
|
+
//
|
|
562
|
+
// The first was the ONLY genuine full table scan in the 30 statements the audit ran
|
|
563
|
+
// through EXPLAIN QUERY PLAN. Stop and SessionStart both probe "does a summary exist for
|
|
564
|
+
// this memory session?", `session_summaries` is 10,160 rows here, and the cost grows
|
|
565
|
+
// linearly with the table for a question asked on every hook event.
|
|
566
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sess_sum_memory_session ON session_summaries(memory_session_id)`);
|
|
567
|
+
// The second narrows the live-row scan the injection faces run per project. Partial on the
|
|
568
|
+
// same predicate `liveObsFilterSql` uses, so the index covers exactly the rows those
|
|
569
|
+
// queries can return.
|
|
570
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_obs_project_live ON observations(project, created_at_epoch DESC) WHERE superseded_at IS NULL AND COALESCE(compressed_into, 0) = 0`);
|
|
554
571
|
|
|
555
572
|
// FTS5 migration: recreate observations_fts when columns are missing (one-time)
|
|
556
573
|
// Detect old FTS5 table missing lesson_learned or search_aliases and recreate with full column set
|
|
@@ -66,6 +66,16 @@ const BROKEN_MARKER = join(RUNTIME_DIR, 'hook-launcher-broken');
|
|
|
66
66
|
// Marker dir mirrors the standalone hook scripts (pre-tool-recall /
|
|
67
67
|
// pre-skill-bridge), which honor CLAUDE_MEM_RUNTIME_DIR — they write 78 of every
|
|
68
68
|
// 79 of these markers, so reading a different dir would mean never healing.
|
|
69
|
+
// The last hand-written copy of this rule, and it stays: this launcher runs BEFORE the
|
|
70
|
+
// native binding is known to work, so it imports only `node:` builtins on purpose — even
|
|
71
|
+
// `lib/resolve-data-dir.mjs` is a module resolution it declines to make on the path whose
|
|
72
|
+
// job is to survive a broken install. `lib/resolve-data-dir.mjs::resolveRuntimeDir` is the
|
|
73
|
+
// canonical rule (audit 2026-09-02 P1-14). It is NOT identical to it, and saying so was
|
|
74
|
+
// wrong: the resolver additionally makes a RELATIVE override absolute (`isAbsolute(raw) ?
|
|
75
|
+
// raw : resolve(raw)`), while this expression hands the relative value straight to `fs`,
|
|
76
|
+
// which resolves it against cwd at call time instead of at module load. They agree on
|
|
77
|
+
// unset, on empty and on an absolute override — the three cases that reach a real install.
|
|
78
|
+
// Keep the DEFAULTING behaviour in step; do not read "identical" into the difference.
|
|
69
79
|
const NB_RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || RUNTIME_DIR;
|
|
70
80
|
const NB_BROKEN_MARKER = join(NB_RUNTIME_DIR, 'native-binding-broken');
|
|
71
81
|
const NB_HEAL_MARKER = join(NB_RUNTIME_DIR, 'native-binding-lastheal');
|
|
@@ -20,19 +20,21 @@
|
|
|
20
20
|
|
|
21
21
|
import { existsSync, readFileSync } from 'fs';
|
|
22
22
|
import { basename, join } from 'path';
|
|
23
|
-
import { resolveDataDir } from '../lib/resolve-data-dir.mjs';
|
|
23
|
+
import { resolveDataDir, resolveRuntimeDir } from '../lib/resolve-data-dir.mjs';
|
|
24
24
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
25
25
|
// D#154: every envelope on this stdout goes through the one writer. This script has a
|
|
26
26
|
// single emit today, so the change buys nothing on its own — it buys that a SECOND
|
|
27
27
|
// emit added later merges instead of producing two JSON documents, which the host
|
|
28
28
|
// parses as neither (lib/hook-stdout.mjs). Import-free module over no runtime deps.
|
|
29
29
|
import { queueHookContext, flushHookStdout } from '../lib/hook-stdout.mjs';
|
|
30
|
+
// P1-9: one bounded stdin reader. Import-free, like hook-stdout.mjs beside it.
|
|
31
|
+
import { readHookStdin, TOOL_INPUT_FILE_MAX_BYTES } from '../lib/hook-stdin.mjs';
|
|
30
32
|
import { cooldownPathFor as sharedCooldownPathFor } from '../lib/cooldown-path.mjs';
|
|
31
33
|
|
|
32
34
|
const SALIENCE_BIND = process.env.CLAUDE_MEM_SALIENCE === 'bind';
|
|
33
35
|
|
|
34
36
|
const DATA_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
35
|
-
const RUNTIME_DIR =
|
|
37
|
+
const RUNTIME_DIR = resolveRuntimeDir(DATA_DIR);
|
|
36
38
|
const LEGACY_COOLDOWN_PATH = join(RUNTIME_DIR, 'pre-recall-cooldown.json');
|
|
37
39
|
|
|
38
40
|
// The no-session legacy fallback stays local: it is this script's own back-compat with
|
|
@@ -46,8 +48,9 @@ function cooldownPathFor(sessionId) {
|
|
|
46
48
|
async function main() {
|
|
47
49
|
if (!SALIENCE_BIND) return;
|
|
48
50
|
if (process.env.CLAUDE_MEM_HOOK_RUNNING) return;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
// Bounded stdin (P1-9) — was an unbounded `for await` accumulate with no cap or timeout.
|
|
52
|
+
// Same payload class as pre-tool-recall: a PostToolUse on `Write` carries the whole file.
|
|
53
|
+
const { text: input } = await readHookStdin({ maxBytes: TOOL_INPUT_FILE_MAX_BYTES });
|
|
51
54
|
let filePath, sessionId;
|
|
52
55
|
try {
|
|
53
56
|
const e = JSON.parse(input);
|
|
@@ -22,16 +22,24 @@ const ENABLED = process.env.CLAUDE_MEM_SUBAGENT_INJECT === 'on'
|
|
|
22
22
|
// 2026-08-14 M-5). Swallows everything: telemetry must never break a dispatch.
|
|
23
23
|
async function recordFailure(scope, err, ctx) {
|
|
24
24
|
try {
|
|
25
|
-
const [{ recordHookError }, { resolveDataDir
|
|
25
|
+
const [{ recordHookError }, { resolveDataDir, resolveRuntimeDir }] = await Promise.all([
|
|
26
26
|
import('../lib/hook-telemetry.mjs'),
|
|
27
27
|
import('../lib/resolve-data-dir.mjs'),
|
|
28
|
-
import('path'),
|
|
29
28
|
]);
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// P1-14: the shared resolver, not a fourth hand-written `env || join(...)`. This is on
|
|
30
|
+
// the error path, which already dynamic-imports, so the script's zero-import budget on
|
|
31
|
+
// the HAPPY path is untouched.
|
|
32
|
+
recordHookError(scope, err, resolveRuntimeDir(resolveDataDir(process.env.CLAUDE_MEM_DIR)), ctx);
|
|
32
33
|
} catch { /* never */ }
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// The ONE hand-written stdin reader left in the tree, and it stays deliberately (P1-9).
|
|
37
|
+
// The other five now share `lib/hook-stdin.mjs`; this script is the default-OFF path whose
|
|
38
|
+
// entire reason to exist is costing nothing when the feature is disabled, and it reaches
|
|
39
|
+
// this line before importing anything at all — even an import-free module is a module
|
|
40
|
+
// resolution. Its caliber (1.5 s, 262144, never rejects) is the same shape the shared
|
|
41
|
+
// reader implements with `rejectOnTimeout: false`, so if this ever gains an import, delete
|
|
42
|
+
// this function and call `readHookStdin({ timeoutMs: 1500, maxBytes: 262144 })`.
|
|
35
43
|
function readStdin() {
|
|
36
44
|
return new Promise((resolve) => {
|
|
37
45
|
let data = '';
|
|
@@ -7,17 +7,20 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
7
7
|
import { join, resolve, sep } from 'path';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
10
|
-
import { resolveDataDir } from '../lib/resolve-data-dir.mjs';
|
|
10
|
+
import { resolveDataDir, resolveRuntimeDir } from '../lib/resolve-data-dir.mjs';
|
|
11
11
|
// format-utils.mjs is import-free — pulling three defang helpers keeps this script
|
|
12
12
|
// inside its "lightweight standalone" budget (no heavy transitive deps).
|
|
13
13
|
import { neutralizeContextDelimiters, neutralizeSkillDelimiters, neutralizeSkillBridgeDelimiters } from '../format-utils.mjs';
|
|
14
14
|
// D#154: single envelope writer. Also import-free (no runtime deps), so it stays
|
|
15
15
|
// inside this script's "lightweight standalone" budget.
|
|
16
16
|
import { queueHookContext, flushHookStdout } from '../lib/hook-stdout.mjs';
|
|
17
|
+
// P1-9: bounded stdin. Also import-free, so it stays inside the "lightweight standalone"
|
|
18
|
+
// budget this script's header claims.
|
|
19
|
+
import { readHookStdin } from '../lib/hook-stdin.mjs';
|
|
17
20
|
|
|
18
21
|
// CLAUDE_MEM_DIR mirrors pre-tool-recall.js — one env var sandboxes everything.
|
|
19
22
|
const DATA_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
20
|
-
const RUNTIME_DIR =
|
|
23
|
+
const RUNTIME_DIR = resolveRuntimeDir(DATA_DIR);
|
|
21
24
|
// D#29: all data artifacts follow DATA_DIR (CLAUDE_MEM_DIR-aware), not a hardcoded
|
|
22
25
|
// homedir — previously REGISTRY_DB_PATH/MANAGED_BASE/MARKER pinned homedir while line 12
|
|
23
26
|
// honored the env, so relocated installs opened the wrong DB and the marker never matched
|
|
@@ -32,9 +35,10 @@ try {
|
|
|
32
35
|
// Skip if recursive hook
|
|
33
36
|
if (process.env.CLAUDE_MEM_HOOK_RUNNING) process.exit(0);
|
|
34
37
|
|
|
35
|
-
// Read stdin
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// Read stdin, bounded (P1-9). This used to be an unbounded `for await` accumulate: no
|
|
39
|
+
// cap, no timeout, the only limit being the host's own fail-open — which looks exactly
|
|
40
|
+
// like the hook having nothing to say.
|
|
41
|
+
const { text: input } = await readHookStdin();
|
|
38
42
|
|
|
39
43
|
// Parse event
|
|
40
44
|
let skillName;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
8
8
|
import { basename, join } from 'path';
|
|
9
|
-
import { resolveDataDir } from '../lib/resolve-data-dir.mjs';
|
|
9
|
+
import { resolveDataDir, resolveRuntimeDir } from '../lib/resolve-data-dir.mjs';
|
|
10
10
|
import { atomicWriteFileSync } from '../lib/atomic-write.mjs';
|
|
11
11
|
import { injectedIdsFileName, injectedIdKey, EVENT_ID_PREFIX, readInjectedMarker, mergeInjectedMarker } from '../lib/injected-ids.mjs';
|
|
12
12
|
import { liveObsFilterSql } from '../lib/inject-search-core.mjs';
|
|
@@ -39,6 +39,8 @@ import { neutralizeContextDelimiters } from '../format-utils.mjs';
|
|
|
39
39
|
//
|
|
40
40
|
// Import-free module, no runtime deps — nothing added to this script's load cost.
|
|
41
41
|
import { queueHookContext, flushHookStdout } from '../lib/hook-stdout.mjs';
|
|
42
|
+
// P1-9: one bounded stdin reader. Import-free, like hook-stdout.mjs beside it.
|
|
43
|
+
import { readHookStdin, TOOL_INPUT_FILE_MAX_BYTES, salvageTruncatedHookEvent } from '../lib/hook-stdin.mjs';
|
|
42
44
|
// Recall queries the SAVE-path project, so this MUST produce the same string as the
|
|
43
45
|
// save path. It used to be a hand-kept copy of the same 6 lines; that copy had already
|
|
44
46
|
// drifted once (missing the process.env.PWD fallback, so a symlinked project dir
|
|
@@ -53,7 +55,8 @@ import { DAY_MS } from '../lib/time-constants.mjs';
|
|
|
53
55
|
// per-component overrides for tests that mix isolated + real paths.
|
|
54
56
|
const DATA_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
55
57
|
const DB_PATH = process.env.CLAUDE_MEM_DB_PATH || join(DATA_DIR, 'claude-mem-lite.db');
|
|
56
|
-
const RUNTIME_DIR =
|
|
58
|
+
const RUNTIME_DIR = resolveRuntimeDir(DATA_DIR);
|
|
59
|
+
|
|
57
60
|
// A3 (v2.83): cross-hook dedup window. UPS writes
|
|
58
61
|
// `runtime/.claude-mem-injected-<project>` after each inject; we read it to drop IDs the
|
|
59
62
|
// agent already saw in this window. Imported, not inlined (ARCH-3): the copy's stated
|
|
@@ -301,9 +304,25 @@ try {
|
|
|
301
304
|
// Skip if DB doesn't exist
|
|
302
305
|
if (!existsSync(DB_PATH)) process.exit(0);
|
|
303
306
|
|
|
304
|
-
// Read stdin
|
|
305
|
-
|
|
306
|
-
|
|
307
|
+
// Read stdin, bounded (P1-9), at THIS path's own caliber — not the module default.
|
|
308
|
+
//
|
|
309
|
+
// A first cut called `readHookStdin()` bare, taking the 256 KB default, and justified it
|
|
310
|
+
// by saying a truncated payload "is what the host's 3 s fail-open did anyway". That was
|
|
311
|
+
// measured at the v3.93.0 pre-tag review and is FALSE: `JSON.parse` of a 5 MB payload
|
|
312
|
+
// takes 2.99 ms and 10 MB takes 10.8 ms — three orders of magnitude inside the fail-open,
|
|
313
|
+
// so the old unbounded read was not timing out, it was working. The cap did not make an
|
|
314
|
+
// existing loss deliberate, it CREATED one, on `pretool` — the highest-cite-rate injection
|
|
315
|
+
// face this project measures. This repo's own CHANGELOG.md is 1 MB, so a `Write` to it
|
|
316
|
+
// crossed the default cap and silently lost the recall while logging a hook error.
|
|
317
|
+
//
|
|
318
|
+
// Two changes, because they cover different halves. The cap is now a MEMORY backstop
|
|
319
|
+
// sized to the payload class (a whole file), not a functional gate. And a truncated read
|
|
320
|
+
// is salvaged rather than dropped, the same way `hook.mjs handlePostToolUse` salvages
|
|
321
|
+
// `tool_name` from a truncated prefix: everything below needs `file_path` / `tool_name` /
|
|
322
|
+
// `session_id`, all of which are scalars the host emits alongside `content`. Salvage is
|
|
323
|
+
// strictly better than the previous behaviour — when the prefix does not carry them we
|
|
324
|
+
// land exactly where the drop landed.
|
|
325
|
+
const { text: input, truncated } = await readHookStdin({ maxBytes: TOOL_INPUT_FILE_MAX_BYTES });
|
|
307
326
|
|
|
308
327
|
// Parse event
|
|
309
328
|
let filePath;
|
|
@@ -323,8 +342,17 @@ try {
|
|
|
323
342
|
const lim = event.tool_input?.limit;
|
|
324
343
|
isFullRead = (off === undefined || off === null) && (lim === undefined || lim === null);
|
|
325
344
|
} catch (e) {
|
|
326
|
-
|
|
327
|
-
|
|
345
|
+
const salvaged = truncated ? salvageTruncatedHookEvent(input) : null;
|
|
346
|
+
if (!salvaged) {
|
|
347
|
+
// A genuinely malformed payload. Kept distinct from the truncation case above so the
|
|
348
|
+
// hook-error log that `stats` and `doctor` read as an install-health signal does not
|
|
349
|
+
// fill with rows for perfectly normal large writes.
|
|
350
|
+
recordHookError('pre-recall:json', e, RUNTIME_DIR, { inputLen: input.length, truncated });
|
|
351
|
+
process.exit(0);
|
|
352
|
+
}
|
|
353
|
+
({ filePath, sessionId, toolName } = salvaged);
|
|
354
|
+
toolInput = {};
|
|
355
|
+
isFullRead = true;
|
|
328
356
|
}
|
|
329
357
|
|
|
330
358
|
// Upstream-shape probe: hook ran but neither field nor input shape matches the
|