agentsmesh 0.32.0 → 0.33.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.
@@ -469,7 +469,7 @@ declare function toRelPath(projectRoot: string, absolute: string): string;
469
469
  * survives generate → import → generate round-trip; only the wording inside each
470
470
  * block is tightened for maximum agent compliance.
471
471
  */
472
- declare const LESSONS_PROCEDURAL_RULE = "## Lessons (BLOCKING)\n\nGraph `.agentsmesh/lessons/lessons.json` is canonical; never hand-edit it. Manual: `lessons` skill.\n\n**Recall:** before every file edit or state-changing command, MUST run `agentsmesh lessons query --file <path> --cmd <command>` and obey matches; at task start, ALSO run `agentsmesh lessons query --keyword \"<task terms>\" --always` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.\n\n**Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run `agentsmesh lessons add \"<imperative rule>\" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>`.\n\n**Before final:** report `Lesson: captured <id>` or `Lesson: none`. No recall/capture gate = task incomplete. No shell: use `lessons_query` / `lessons_add`.";
472
+ declare const LESSONS_PROCEDURAL_RULE = "## Lessons (BLOCKING)\n\nGraph `.agentsmesh/lessons/lessons.json` is canonical; never hand-edit it. Manual: `lessons` skill.\n\n**Recall:** before every file edit or state-changing command, MUST run `agentsmesh lessons query --file <path> --cmd <command> --session auto` and obey matches; at task start, ALSO run `agentsmesh lessons query --keyword \"<task terms>\" --always --session auto` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.\n\n**Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run `agentsmesh lessons add \"<imperative rule>\" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>`.\n\n**Before final:** report `Lesson: captured <id>` or `Lesson: none`. No recall/capture gate = task incomplete. No shell: use `lessons_query` / `lessons_add`.";
473
473
 
474
474
  interface ScaffoldLessonsResult {
475
475
  readonly created: string[];
package/dist/lessons.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { o as RankedLesson, j as LessonsQuery, A as AddLessonInput, a as AddLessonOptions, b as AddLessonResult } from './init-CrZhoNTj.js';
2
- export { c as AddLessonTriggers, D as DEFAULT_RECALL_LIMIT, X as DEFAULT_RECALL_MAX_TOKENS, I as ImportLegacyOptions, d as ImportLegacyReport, Y as LESSONS_LOCK_FILENAME, L as LESSONS_PROCEDURAL_RULE, e as Lesson, f as LessonStatus, g as LessonsGraph, Z as LessonsGraphExistsError, h as LessonsGraphSchema, i as LessonsPaths, M as MatchedLesson, k as MergeLessonsOptions, l as MergeLessonsResult, m as MutateOptions, R as RankOptions, n as RankReason, S as ScaffoldLessonsResult, p as StripMarkersOptions, q as StripMarkersReport, T as Topic, r as Trigger, s as TriggerKind, U as UnknownTopicError, V as ValidationFinding, t as ValidationLevel, u as ValidationReport, v as acquireLessonsLock, w as addLesson, x as graphFilePath, y as importLegacyLessons, _ as lessonsLockPath, z as lessonsPaths, B as loadLessonsGraph, C as mergeLessons, E as mutateLessonsGraph, F as parseGraph, G as queryLessons, H as rankLessons, J as scaffoldLessons, K as serializeGraph, N as stripLegacyMarkers, O as stripMarkersInGraph, P as toRelPath, Q as tryLoadLessonsGraph, W as validateLessonsGraph } from './init-CrZhoNTj.js';
1
+ import { o as RankedLesson, j as LessonsQuery, A as AddLessonInput, a as AddLessonOptions, b as AddLessonResult } from './init-rvKjGLTB.js';
2
+ export { c as AddLessonTriggers, D as DEFAULT_RECALL_LIMIT, X as DEFAULT_RECALL_MAX_TOKENS, I as ImportLegacyOptions, d as ImportLegacyReport, Y as LESSONS_LOCK_FILENAME, L as LESSONS_PROCEDURAL_RULE, e as Lesson, f as LessonStatus, g as LessonsGraph, Z as LessonsGraphExistsError, h as LessonsGraphSchema, i as LessonsPaths, M as MatchedLesson, k as MergeLessonsOptions, l as MergeLessonsResult, m as MutateOptions, R as RankOptions, n as RankReason, S as ScaffoldLessonsResult, p as StripMarkersOptions, q as StripMarkersReport, T as Topic, r as Trigger, s as TriggerKind, U as UnknownTopicError, V as ValidationFinding, t as ValidationLevel, u as ValidationReport, v as acquireLessonsLock, w as addLesson, x as graphFilePath, y as importLegacyLessons, _ as lessonsLockPath, z as lessonsPaths, B as loadLessonsGraph, C as mergeLessons, E as mutateLessonsGraph, F as parseGraph, G as queryLessons, H as rankLessons, J as scaffoldLessons, K as serializeGraph, N as stripLegacyMarkers, O as stripMarkersInGraph, P as toRelPath, Q as tryLoadLessonsGraph, W as validateLessonsGraph } from './init-rvKjGLTB.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
@@ -34,6 +34,14 @@ interface RecallOptions {
34
34
  readonly sessionId?: string;
35
35
  /** Force dedup off even when a session correlator is present. */
36
36
  readonly noDedup?: boolean;
37
+ /**
38
+ * Expire suppressions this long after delivery. Set by callers that have NO
39
+ * context-reset signal (the MCP server never sees the client compact), so a
40
+ * lesson dropped from a summarized context cannot stay hidden forever. The
41
+ * hook path leaves this unset on purpose: it resets on SessionStart
42
+ * compact/clear, which is the exact signal a wall-clock TTL only approximates.
43
+ */
44
+ readonly ttlMs?: number;
37
45
  }
38
46
  interface RecallResult {
39
47
  /** Relevance-ranked, capped lessons (compact metadata lives on each entry). */
package/dist/lessons.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync, readdirSync, realpathSync, appendFileSync, statSync } from 'fs';
2
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync, renameSync, readdirSync, realpathSync, appendFileSync } from 'fs';
3
3
  import { resolve, dirname, join, relative, sep, basename, extname } from 'path';
4
4
  import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
5
5
  import { createHash } from 'crypto';
@@ -701,7 +701,7 @@ function getHostname() {
701
701
  return hostname();
702
702
  }
703
703
  function sleep(ms) {
704
- return new Promise((resolve7) => setTimeout(resolve7, ms));
704
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
705
705
  }
706
706
 
707
707
  // src/lessons/lessons-lock.ts
@@ -1702,7 +1702,7 @@ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
1702
1702
 
1703
1703
  Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
1704
1704
 
1705
- **Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command>\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
1705
+ **Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command> --session auto\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always --session auto\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
1706
1706
 
1707
1707
  **Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
1708
1708
 
@@ -1806,26 +1806,6 @@ async function maybeAutoMigrateLessons(projectRoot) {
1806
1806
  throw err;
1807
1807
  }
1808
1808
  }
1809
- function normalizeRecallFile(file, projectRoot) {
1810
- const forward = file.replaceAll("\\", "/");
1811
- const direct = relativize(projectRoot, forward);
1812
- if (!direct.startsWith("../")) return direct;
1813
- const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
1814
- return viaReal.startsWith("../") ? direct : viaReal;
1815
- }
1816
- function relativize(root, forward) {
1817
- const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
1818
- return rel === "" ? forward.replaceAll("\\", "/") : rel;
1819
- }
1820
- function safeRealpath(path) {
1821
- try {
1822
- return realpathSync(path);
1823
- } catch {
1824
- const parent = dirname(path);
1825
- if (parent === path) return path;
1826
- return resolve(safeRealpath(parent), basename(path));
1827
- }
1828
- }
1829
1809
 
1830
1810
  // src/lessons/keyword-match.ts
1831
1811
  function deriveHaystackTokens(query) {
@@ -1911,6 +1891,148 @@ function triggerMatches(trigger, query, budget) {
1911
1891
  return keywordMatches(trigger.pattern, query);
1912
1892
  }
1913
1893
  }
1894
+ function appendJsonl(path, record, opts) {
1895
+ mkdirSync(dirname(path), { recursive: true });
1896
+ appendFileSync(path, `${JSON.stringify(record)}
1897
+ `, "utf8");
1898
+ if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
1899
+ }
1900
+ function capJsonl(path, maxRecords) {
1901
+ if (!existsSync(path)) return;
1902
+ const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
1903
+ if (lines.length <= maxRecords) return;
1904
+ const kept = lines.slice(lines.length - maxRecords);
1905
+ const tmp = `${path}.${process.pid}.tmp`;
1906
+ writeFileSync(tmp, `${kept.join("\n")}
1907
+ `, "utf8");
1908
+ renameSync(tmp, path);
1909
+ }
1910
+ function readJsonl(path) {
1911
+ if (!existsSync(path)) return [];
1912
+ const out = [];
1913
+ for (const line of readFileSync(path, "utf8").split("\n")) {
1914
+ if (line.trim().length === 0) continue;
1915
+ try {
1916
+ out.push(JSON.parse(line));
1917
+ } catch {
1918
+ }
1919
+ }
1920
+ return out;
1921
+ }
1922
+
1923
+ // src/lessons/telemetry.ts
1924
+ var MAX_RECALL_LOG_RECORDS = 5e3;
1925
+ var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
1926
+ var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
1927
+ var SESSION_ENV = "AGENTSMESH_SESSION_ID";
1928
+ function sessionId(env = process.env) {
1929
+ const raw = env[SESSION_ENV];
1930
+ return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
1931
+ }
1932
+ function recallLogPath(projectRoot) {
1933
+ return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
1934
+ }
1935
+ function isTelemetryEnabled(env = process.env) {
1936
+ return env[TELEMETRY_ENV] === "1";
1937
+ }
1938
+ function appendRecallRecord(projectRoot, record, env = process.env) {
1939
+ if (!isTelemetryEnabled(env)) return;
1940
+ appendJsonl(recallLogPath(projectRoot), record, {
1941
+ maxRecords: MAX_RECALL_LOG_RECORDS,
1942
+ trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
1943
+ });
1944
+ }
1945
+
1946
+ // src/lessons/cmd-fastpath.ts
1947
+ var FASTPATH_DIR = "agentsmesh-lessons-cmdidx";
1948
+ function shortHash(value) {
1949
+ let h = 5381;
1950
+ for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
1951
+ return h.toString(36);
1952
+ }
1953
+ function commandFastpathCachePath(projectRoot) {
1954
+ return join(tmpdir(), FASTPATH_DIR, `${shortHash(resolve(projectRoot))}.json`);
1955
+ }
1956
+ function currentGraphStamp(projectRoot) {
1957
+ try {
1958
+ const s = statSync(graphFilePath(projectRoot));
1959
+ return { mtimeMs: s.mtimeMs, size: s.size };
1960
+ } catch {
1961
+ return null;
1962
+ }
1963
+ }
1964
+ function isStringArray(v) {
1965
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
1966
+ }
1967
+ function readCache(path) {
1968
+ try {
1969
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
1970
+ if (typeof parsed !== "object" || parsed === null) return null;
1971
+ const c = parsed;
1972
+ const stamp = c.stamp;
1973
+ if (typeof stamp?.mtimeMs !== "number" || typeof stamp.size !== "number" || !isStringArray(c.commandPatterns) || !isStringArray(c.keywordPatterns)) {
1974
+ return null;
1975
+ }
1976
+ return {
1977
+ stamp: { mtimeMs: stamp.mtimeMs, size: stamp.size },
1978
+ commandPatterns: c.commandPatterns,
1979
+ keywordPatterns: c.keywordPatterns
1980
+ };
1981
+ } catch {
1982
+ return null;
1983
+ }
1984
+ }
1985
+ function refreshCommandFastpath(projectRoot, graph, preReadStamp) {
1986
+ try {
1987
+ if (preReadStamp === null) return;
1988
+ const stamp = currentGraphStamp(projectRoot);
1989
+ if (stamp === null) return;
1990
+ if (stamp.mtimeMs !== preReadStamp.mtimeMs || stamp.size !== preReadStamp.size) return;
1991
+ const path = commandFastpathCachePath(projectRoot);
1992
+ const existing = readCache(path);
1993
+ if (existing !== null && existing.stamp.mtimeMs === stamp.mtimeMs && existing.stamp.size === stamp.size) {
1994
+ return;
1995
+ }
1996
+ const reachable = /* @__PURE__ */ new Set();
1997
+ for (const lesson of Object.values(graph.lessons)) {
1998
+ if (lesson.status !== "active" || lesson.scope === "always") continue;
1999
+ for (const t of lesson.triggers) reachable.add(t);
2000
+ }
2001
+ const commandPatterns = [];
2002
+ const keywordPatterns = [];
2003
+ for (const [id, trigger] of Object.entries(graph.triggers)) {
2004
+ if (!reachable.has(id)) continue;
2005
+ if (trigger.kind === "command_pattern") commandPatterns.push(trigger.pattern);
2006
+ else if (trigger.kind === "keyword") keywordPatterns.push(trigger.pattern);
2007
+ }
2008
+ const cache2 = { stamp, commandPatterns, keywordPatterns };
2009
+ mkdirSync(dirname(path), { recursive: true });
2010
+ const tmp = `${path}.${process.pid}.tmp`;
2011
+ writeFileSync(tmp, JSON.stringify(cache2), "utf8");
2012
+ renameSync(tmp, path);
2013
+ } catch {
2014
+ }
2015
+ }
2016
+ function normalizeRecallFile(file, projectRoot) {
2017
+ const forward = file.replaceAll("\\", "/");
2018
+ const direct = relativize(projectRoot, forward);
2019
+ if (!direct.startsWith("../")) return direct;
2020
+ const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
2021
+ return viaReal.startsWith("../") ? direct : viaReal;
2022
+ }
2023
+ function relativize(root, forward) {
2024
+ const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
2025
+ return rel === "" ? forward.replaceAll("\\", "/") : rel;
2026
+ }
2027
+ function safeRealpath(path) {
2028
+ try {
2029
+ return realpathSync(path);
2030
+ } catch {
2031
+ const parent = dirname(path);
2032
+ if (parent === path) return path;
2033
+ return resolve(safeRealpath(parent), basename(path));
2034
+ }
2035
+ }
1914
2036
 
1915
2037
  // src/lessons/ranking-signals.ts
1916
2038
  function buildFanout(graph) {
@@ -2054,57 +2176,6 @@ function loadRecallConfig(projectRoot) {
2054
2176
  return fallback;
2055
2177
  }
2056
2178
  }
2057
- function appendJsonl(path, record, opts) {
2058
- mkdirSync(dirname(path), { recursive: true });
2059
- appendFileSync(path, `${JSON.stringify(record)}
2060
- `, "utf8");
2061
- if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
2062
- }
2063
- function capJsonl(path, maxRecords) {
2064
- if (!existsSync(path)) return;
2065
- const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
2066
- if (lines.length <= maxRecords) return;
2067
- const kept = lines.slice(lines.length - maxRecords);
2068
- const tmp = `${path}.${process.pid}.tmp`;
2069
- writeFileSync(tmp, `${kept.join("\n")}
2070
- `, "utf8");
2071
- renameSync(tmp, path);
2072
- }
2073
- function readJsonl(path) {
2074
- if (!existsSync(path)) return [];
2075
- const out = [];
2076
- for (const line of readFileSync(path, "utf8").split("\n")) {
2077
- if (line.trim().length === 0) continue;
2078
- try {
2079
- out.push(JSON.parse(line));
2080
- } catch {
2081
- }
2082
- }
2083
- return out;
2084
- }
2085
- var MAX_RECALL_LOG_RECORDS = 5e3;
2086
- var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
2087
- var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
2088
- var SESSION_ENV = "AGENTSMESH_SESSION_ID";
2089
- function sessionId(env = process.env) {
2090
- const raw = env[SESSION_ENV];
2091
- return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
2092
- }
2093
- function recallLogPath(projectRoot) {
2094
- return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
2095
- }
2096
- function isTelemetryEnabled(env = process.env) {
2097
- return env[TELEMETRY_ENV] === "1";
2098
- }
2099
- function appendRecallRecord(projectRoot, record, env = process.env) {
2100
- if (!isTelemetryEnabled(env)) return;
2101
- appendJsonl(recallLogPath(projectRoot), record, {
2102
- maxRecords: MAX_RECALL_LOG_RECORDS,
2103
- trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
2104
- });
2105
- }
2106
-
2107
- // src/lessons/outcome-log.ts
2108
2179
  function outcomeLogPath(projectRoot) {
2109
2180
  return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
2110
2181
  }
@@ -2141,48 +2212,116 @@ function loadEffectiveness(projectRoot) {
2141
2212
  return map;
2142
2213
  }
2143
2214
  var SEEN_DIR = "agentsmesh-lessons-seen";
2144
- function openSessionDedup(options = {}) {
2145
- if (options.disabled === true) return null;
2146
- const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
2147
- if (id === void 0) return null;
2148
- const path = seenPath(id, options.projectRoot);
2149
- return { sessionId: id, seen: loadSeen(path), path };
2150
- }
2151
- function shortHash(value) {
2215
+ function shortHash2(value) {
2152
2216
  let h = 5381;
2153
2217
  for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
2154
2218
  return h.toString(36);
2155
2219
  }
2156
- function seenPath(id, projectRoot) {
2220
+ function seenStorePath(id, projectRoot) {
2157
2221
  const safe = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 200);
2158
- const scoped = projectRoot === void 0 ? safe : `${safe}__${shortHash(resolve(projectRoot))}`;
2222
+ const scoped = projectRoot === void 0 ? safe : `${safe}__${shortHash2(resolve(projectRoot))}`;
2159
2223
  return join(tmpdir(), SEEN_DIR, `${scoped}.json`);
2160
2224
  }
2161
- function loadSeen(path) {
2162
- if (!existsSync(path)) return /* @__PURE__ */ new Set();
2225
+ function readSeenStore(path) {
2226
+ if (!existsSync(path)) return { ids: /* @__PURE__ */ new Set(), stamps: null };
2163
2227
  try {
2164
2228
  const parsed = JSON.parse(readFileSync(path, "utf8"));
2165
- if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
2166
- return new Set(parsed.filter((x) => typeof x === "string"));
2229
+ if (Array.isArray(parsed)) {
2230
+ const ids = new Set(parsed.filter((x) => typeof x === "string"));
2231
+ return { ids, stamps: null };
2232
+ }
2233
+ if (typeof parsed === "object" && parsed !== null) {
2234
+ const seen = parsed.seen;
2235
+ if (typeof seen === "object" && seen !== null) {
2236
+ const stamps = /* @__PURE__ */ new Map();
2237
+ for (const [id, ms] of Object.entries(seen)) {
2238
+ if (typeof ms === "number") stamps.set(id, ms);
2239
+ }
2240
+ const lastAt = parsed.lastAt;
2241
+ return {
2242
+ ids: new Set(stamps.keys()),
2243
+ stamps,
2244
+ ...typeof lastAt === "number" ? { lastAt } : {}
2245
+ };
2246
+ }
2247
+ }
2248
+ return { ids: /* @__PURE__ */ new Set(), stamps: null };
2167
2249
  } catch {
2168
- return /* @__PURE__ */ new Set();
2250
+ return { ids: /* @__PURE__ */ new Set(), stamps: null };
2169
2251
  }
2170
2252
  }
2253
+ function writeSeenStore(path, data, lastAt) {
2254
+ try {
2255
+ mkdirSync(dirname(path), { recursive: true });
2256
+ const body = data instanceof Map ? JSON.stringify({ v: 2, lastAt: lastAt ?? Date.now(), seen: Object.fromEntries(data) }) : JSON.stringify(data);
2257
+ const tmp = `${path}.${process.pid}.tmp`;
2258
+ writeFileSync(tmp, body, "utf8");
2259
+ renameSync(tmp, path);
2260
+ } catch {
2261
+ }
2262
+ }
2263
+ var AUTO_SESSION_IDLE_MS = 30 * 60 * 1e3;
2264
+ var FUTURE_TOLERANCE_MS = 6e4;
2265
+ function stampAgeMs(stamp, now = Date.now()) {
2266
+ if (stamp - now > FUTURE_TOLERANCE_MS) return Number.POSITIVE_INFINITY;
2267
+ return Math.max(0, now - stamp);
2268
+ }
2269
+ function isIdleSession(stamps, lastAt) {
2270
+ if (stamps === null || stamps.size === 0) return false;
2271
+ let newest = lastAt ?? 0;
2272
+ for (const ms of stamps.values()) if (ms > newest) newest = ms;
2273
+ return stampAgeMs(newest) > AUTO_SESSION_IDLE_MS;
2274
+ }
2275
+
2276
+ // src/lessons/seen-cache.ts
2277
+ function openSessionDedup(options = {}) {
2278
+ if (options.disabled === true) return null;
2279
+ const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
2280
+ if (id === void 0) return null;
2281
+ const path = seenStorePath(id, options.projectRoot);
2282
+ const store = readSeenStore(path);
2283
+ const stale = options.ttlMs !== void 0 && isIdleSession(store.stamps, store.lastAt);
2284
+ const stamps = stale ? null : store.stamps;
2285
+ return {
2286
+ sessionId: id,
2287
+ seen: stale ? /* @__PURE__ */ new Set() : visibleSeen(store.ids, stamps, options.ttlMs),
2288
+ path,
2289
+ stamps,
2290
+ ...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
2291
+ };
2292
+ }
2293
+ function visibleSeen(ids, stamps, ttlMs) {
2294
+ if (ttlMs === void 0) return ids;
2295
+ if (stamps === null) return /* @__PURE__ */ new Set();
2296
+ const now = Date.now();
2297
+ const fresh = /* @__PURE__ */ new Set();
2298
+ for (const [id, ms] of stamps) if (stampAgeMs(ms, now) <= ttlMs) fresh.add(id);
2299
+ return fresh;
2300
+ }
2171
2301
  function filterUnseen(dedup, matches) {
2172
2302
  return matches.filter((m) => !dedup.seen.has(m.id));
2173
2303
  }
2174
2304
  function commitSeen(dedup, returnedIds) {
2175
- if (returnedIds.length === 0) return;
2305
+ if (returnedIds.length === 0) {
2306
+ if (dedup.ttlMs !== void 0 && dedup.stamps !== null) {
2307
+ writeSeenStore(dedup.path, dedup.stamps);
2308
+ }
2309
+ return;
2310
+ }
2311
+ if (dedup.ttlMs !== void 0 || dedup.stamps !== null) {
2312
+ const now = Date.now();
2313
+ const merged = /* @__PURE__ */ new Map();
2314
+ for (const [id, ms] of dedup.stamps ?? []) {
2315
+ if (dedup.ttlMs === void 0 || now - ms <= dedup.ttlMs) merged.set(id, ms);
2316
+ }
2317
+ for (const id of returnedIds) merged.set(id, now);
2318
+ writeSeenStore(dedup.path, merged);
2319
+ return;
2320
+ }
2176
2321
  const union3 = new Set(dedup.seen);
2177
2322
  for (const id of returnedIds) union3.add(id);
2178
2323
  if (union3.size === dedup.seen.size) return;
2179
- try {
2180
- mkdirSync(dirname(dedup.path), { recursive: true });
2181
- const tmp = `${dedup.path}.${process.pid}.tmp`;
2182
- writeFileSync(tmp, JSON.stringify([...union3]), "utf8");
2183
- renameSync(tmp, dedup.path);
2184
- } catch {
2185
- }
2324
+ writeSeenStore(dedup.path, [...union3]);
2186
2325
  }
2187
2326
 
2188
2327
  // src/lessons/recall.ts
@@ -2191,6 +2330,7 @@ async function recallLessons(projectRoot, query, options = {}) {
2191
2330
  await maybeAutoMigrateLessons(projectRoot);
2192
2331
  } catch {
2193
2332
  }
2333
+ const preReadStamp = currentGraphStamp(projectRoot);
2194
2334
  const load = loadLessonsGraphResilient(projectRoot);
2195
2335
  if (load.status === "corrupt") {
2196
2336
  return { lessons: [], totalMatches: 0, suppressed: 0, corrupt: true };
@@ -2200,12 +2340,14 @@ async function recallLessons(projectRoot, query, options = {}) {
2200
2340
  }
2201
2341
  if (load.status === "absent") return { lessons: [], totalMatches: 0, suppressed: 0 };
2202
2342
  const graph = load.graph;
2343
+ refreshCommandFastpath(projectRoot, graph, preReadStamp);
2203
2344
  const matchQuery = query.file === void 0 ? query : { ...query, file: normalizeRecallFile(query.file, projectRoot) };
2204
2345
  const matches = queryLessons(graph, matchQuery);
2205
2346
  const dedup = openSessionDedup({
2206
2347
  explicit: options.sessionId,
2207
2348
  disabled: options.noDedup,
2208
- projectRoot
2349
+ projectRoot,
2350
+ ...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
2209
2351
  });
2210
2352
  const forRank = dedup === null ? matches : filterUnseen(dedup, matches);
2211
2353
  const cfg = loadRecallConfig(projectRoot);
@@ -2213,8 +2355,10 @@ async function recallLessons(projectRoot, query, options = {}) {
2213
2355
  limit: options.limit ?? cfg.limit,
2214
2356
  maxTokens: options.maxTokens === null ? void 0 : options.maxTokens ?? cfg.maxTokens,
2215
2357
  // Down-rank proven fire-but-fail lessons (empty ⇒ neutral, so recall is
2216
- // unchanged until the outcome log has real signal). Read from the side-channel.
2217
- effectiveness: loadEffectiveness(projectRoot)
2358
+ // unchanged until the outcome log has real signal). Read from the side-channel
2359
+ // only when something survived matching+dedup — a no-match recall must not pay
2360
+ // the (up to 2MB) outcome-log read for a ranking of nothing.
2361
+ effectiveness: forRank.length === 0 ? /* @__PURE__ */ new Map() : loadEffectiveness(projectRoot)
2218
2362
  });
2219
2363
  if (dedup !== null)
2220
2364
  commitSeen(
@@ -2835,6 +2979,17 @@ ${placed}` : placed;
2835
2979
 
2836
2980
  // src/targets/projection/lessons-paragraph.ts
2837
2981
  var LEGACY_RAW_FORMS = [
2982
+ // Pre-`--session auto` wording (2026-07): strip sentinel-less copies so a
2983
+ // project scaffolded before the dedup correlator dedups on the next scaffold.
2984
+ `## Lessons (BLOCKING)
2985
+
2986
+ Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
2987
+
2988
+ **Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command>\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
2989
+
2990
+ **Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
2991
+
2992
+ **Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`,
2838
2993
  `## Lessons (BLOCKING REQUIREMENT \u2014 MUST run both, no exceptions; the user will check)
2839
2994
 
2840
2995
  Graph \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit. Manual: the \`lessons\` skill.
@@ -2892,14 +3047,18 @@ regression / wrong assumption / surprise and you have not captured (nor stated
2892
3047
 
2893
3048
  ## Recall \u2014 before each edit/command, and at task start
2894
3049
 
2895
- \`agentsmesh lessons query --file <path> --cmd <command>\`, then apply every rule.
2896
- Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
3050
+ \`agentsmesh lessons query --file <path> --cmd <command> --session auto\`, then apply every
3051
+ rule. Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
2897
3052
  for a specific edit is the anti-pattern** \u2014 anchor those to \`--file\`/\`--cmd\`. But at the
2898
3053
  START of a task (or when planning), run \`agentsmesh lessons query --keyword "<the task's
2899
- key terms>" --always\`: that surfaces the conceptual rules no file/command names PLUS the
2900
- universal always-on lessons \u2014 the manual equivalent of the automatic prompt recall on
2901
- hook-capable tools. Author a \`keyword\` trigger beside a \`file_glob\` on conceptual lessons
2902
- so they are reachable both ways. No shell \u2192 MCP \`lessons_query\` (\`file\`/\`command\`/\`keyword\`/\`always\`).
3054
+ key terms>" --always --session auto\`: that surfaces the conceptual rules no file/command
3055
+ names PLUS the universal always-on lessons \u2014 the manual equivalent of the automatic prompt
3056
+ recall on hook-capable tools. **Always pass \`--session auto\`**: it suppresses rules already
3057
+ shown this session so repeat recalls stay quiet (without it every recall re-delivers the
3058
+ whole matched set); \`--no-dedup\` re-shows everything after a context reset. Author a
3059
+ \`keyword\` trigger beside a \`file_glob\` on conceptual lessons so they are reachable both
3060
+ ways. No shell \u2192 MCP \`lessons_query\` (\`file\`/\`command\`/\`keyword\`/\`always\`; session dedup
3061
+ is on by default there \u2014 \`no_dedup:true\` to re-show).
2903
3062
 
2904
3063
  ## Capture \u2014 Gate Function (before any completion claim)
2905
3064