agentsmesh 0.35.0 → 0.37.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.
@@ -84,6 +84,8 @@ interface LessonsQuery {
84
84
  interface MatchedLesson {
85
85
  readonly id: string;
86
86
  readonly lesson: Lesson;
87
+ /** Set when the lesson was reached by its wording, not a trigger (lexical retrieval). */
88
+ readonly lexical?: true;
87
89
  }
88
90
  /**
89
91
  * Recall primitive — returns every active lesson whose triggers match any of
@@ -98,6 +100,8 @@ interface RankReason {
98
100
  readonly bm25: number;
99
101
  readonly specificity: number;
100
102
  readonly topicCoherence: number;
103
+ /** Reached by rule wording rather than a trigger (see lexical-retrieval.ts). */
104
+ readonly lexical?: true;
101
105
  }
102
106
  interface RankedLesson {
103
107
  readonly id: string;
@@ -379,13 +383,8 @@ declare function importLegacyLessons(projectRoot: string, options: ImportLegacyO
379
383
  /**
380
384
  * Cross-platform process lock backed by an atomic mkdir.
381
385
  *
382
- * Acquire semantics: mkdir with no `recursive` option is atomic per POSIX and
383
- * returns EEXIST if the directory already exists. That makes it a reliable
384
- * cross-process mutex without any third-party dependency.
385
- *
386
386
  * Stale recovery: the holder writes its PID and start timestamp into the lock
387
- * dir. On contention we peek at the PID: a dead same-host holder is evicted at
388
- * once. A live holder (or one on another host, whose PID we cannot probe) is
387
+ * dir. A dead same-host holder is evicted at once. A live or remote holder is
389
388
  * evicted only past `staleMs` — an hours-long bound that catches a hung
390
389
  * process or a recycled PID, never a slow but healthy run.
391
390
  */
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-C3pMnqoU.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-C3pMnqoU.js';
1
+ import { o as RankedLesson, j as LessonsQuery, A as AddLessonInput, a as AddLessonOptions, b as AddLessonResult } from './init-BtgoRmIs.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-BtgoRmIs.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/lessons.js CHANGED
@@ -2,11 +2,11 @@ import { z } from 'zod';
2
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
- import { createHash } from 'crypto';
5
+ import { createHash, randomUUID } from 'crypto';
6
6
  import picomatch from 'picomatch';
7
- import { mkdir, rm, writeFile, readFile, stat, lstat, unlink, rename, chmod } from 'fs/promises';
7
+ import { mkdir, rm, writeFile, readFile, stat, lstat, open, rename } from 'fs/promises';
8
8
  import { tmpdir, hostname } from 'os';
9
- import 'timers/promises';
9
+ import { setTimeout as setTimeout$1 } from 'timers/promises';
10
10
 
11
11
  // src/lessons/graph-schema.ts
12
12
  var CURRENT_GRAPH_VERSION = 2;
@@ -1190,8 +1190,7 @@ async function acquireProcessLock(lockPath, opts = {}) {
1190
1190
  if (acquired) return acquired;
1191
1191
  const existing = await inspectLock(lockPath);
1192
1192
  if (existing !== "young" && isStale(existing, stale)) {
1193
- await rm(lockPath, { recursive: true, force: true }).catch(() => {
1194
- });
1193
+ await rm(lockPath, { recursive: true, force: true });
1195
1194
  continue;
1196
1195
  }
1197
1196
  if (attempt >= retries) {
@@ -1215,7 +1214,13 @@ async function tryAcquire(lockPath) {
1215
1214
  started: Date.now(),
1216
1215
  hostname: getHostname()
1217
1216
  };
1218
- await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
1217
+ try {
1218
+ await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
1219
+ } catch (error) {
1220
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {
1221
+ });
1222
+ throw error;
1223
+ }
1219
1224
  let released = false;
1220
1225
  const cleanup = () => {
1221
1226
  if (released) return;
@@ -2169,6 +2174,42 @@ function safeRealpath(path) {
2169
2174
  }
2170
2175
  }
2171
2176
 
2177
+ // src/lessons/lexical-retrieval.ts
2178
+ var LEXICAL_LIMIT = 3;
2179
+ var LEXICAL_MIN_TERMS = 2;
2180
+ var GENERIC = new Set(
2181
+ "top left right bottom before after first last next new old use used using run make keep set get add all any each every not no never always only one two more less same other than then into from over under out up down off via per so if else when where which what who how why do does did done can could should would will may might has have had are was were been being also still just even very both either such these those they them their we you your our he she his her my me our us own same here there now then again once about above below between during through until while because but by need needs needed way ways thing things".split(" ")
2182
+ );
2183
+ function isKeywordOnlyQuery(query) {
2184
+ return query.keyword !== void 0 && query.file === void 0 && query.command === void 0;
2185
+ }
2186
+ function matchLessons(graph, query) {
2187
+ const triggered = queryLessons(graph, query);
2188
+ const lexical = isKeywordOnlyQuery(query) && query.keyword !== void 0 ? lexicalCandidates(
2189
+ graph,
2190
+ query.keyword,
2191
+ triggered.map((m) => m.id)
2192
+ ) : [];
2193
+ return { matches: [...triggered, ...lexical], lexicalCount: lexical.length };
2194
+ }
2195
+ function lexicalCandidates(graph, keyword, excludeIds) {
2196
+ const terms = [...new Set(tokenize(keyword))];
2197
+ if (terms.length < LEXICAL_MIN_TERMS) return [];
2198
+ const excluded = new Set(excludeIds);
2199
+ const corpus = buildCorpus(graph);
2200
+ const scored = [];
2201
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
2202
+ if (lesson.status !== "active" || lesson.scope === "always" || excluded.has(id)) continue;
2203
+ const ruleTerms = new Set(tokenize(lesson.rule));
2204
+ let shared = 0;
2205
+ for (const t of terms) if (!GENERIC.has(t) && ruleTerms.has(t)) shared += 1;
2206
+ if (shared < LEXICAL_MIN_TERMS) continue;
2207
+ scored.push({ id, lesson, score: bm25(terms, lesson.rule, corpus) });
2208
+ }
2209
+ scored.sort((a, b) => b.score !== a.score ? b.score - a.score : a.id < b.id ? -1 : 1);
2210
+ return scored.slice(0, LEXICAL_LIMIT).map(({ id, lesson }) => ({ id, lesson, lexical: true }));
2211
+ }
2212
+
2172
2213
  // src/lessons/ranking-signals.ts
2173
2214
  function buildFanout(graph) {
2174
2215
  const fanout = /* @__PURE__ */ new Map();
@@ -2235,7 +2276,7 @@ function rankLessons(graph, query, matches, options = {}) {
2235
2276
  const narrowness = buildNarrowness(graph);
2236
2277
  const coherence = buildTopicCoherence(matches);
2237
2278
  const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
2238
- const scored = matches.map(({ id, lesson }) => {
2279
+ const scored = matches.map(({ id, lesson, lexical }) => {
2239
2280
  const hitTriggers = lesson.triggers.filter((t) => matchedTriggerIds.has(t));
2240
2281
  let specificity = 0;
2241
2282
  for (const t of hitTriggers) {
@@ -2251,7 +2292,8 @@ function rankLessons(graph, query, matches, options = {}) {
2251
2292
  matchedTriggers: hitTriggers,
2252
2293
  // Absent from the log ⇒ neutral 1, so a lesson with no outcome data never
2253
2294
  // sinks below one that has merely been recorded.
2254
- effectiveness: options.effectiveness?.get(id) ?? 1
2295
+ effectiveness: options.effectiveness?.get(id) ?? 1,
2296
+ lexical
2255
2297
  };
2256
2298
  });
2257
2299
  const bm25Ranks = rankMap(scored.map((s) => ({ id: s.id, value: s.bm25 })));
@@ -2266,7 +2308,8 @@ function rankLessons(graph, query, matches, options = {}) {
2266
2308
  matchedTriggers: s.matchedTriggers,
2267
2309
  bm25: s.bm25,
2268
2310
  specificity: s.specificity,
2269
- topicCoherence: s.topicCoherence
2311
+ topicCoherence: s.topicCoherence,
2312
+ ...s.lexical === true ? { lexical: true } : {}
2270
2313
  }
2271
2314
  })).sort((a, b) => {
2272
2315
  if (b.score !== a.score) return b.score - a.score;
@@ -2324,6 +2367,33 @@ function loadRecallConfig(projectRoot) {
2324
2367
  return fallback;
2325
2368
  }
2326
2369
  }
2370
+
2371
+ // src/lessons/recall-telemetry.ts
2372
+ function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, options = {}) {
2373
+ if (!isTelemetryEnabled(process.env, projectRoot)) return;
2374
+ const byKind = collectMatchedTriggersByKind(graph, query);
2375
+ const countVia = (set) => matches.filter(({ lesson }) => lesson.triggers.some((t) => set.has(t))).length;
2376
+ const session = options.session ?? sessionId();
2377
+ appendRecallRecord(projectRoot, {
2378
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2379
+ hasFile: query.file !== void 0,
2380
+ hasCommand: query.command !== void 0,
2381
+ hasKeyword: query.keyword !== void 0,
2382
+ totalMatches: matches.length,
2383
+ returnedCount: lessons.length,
2384
+ returnedTokens: lessons.reduce((sum, l) => sum + estTokens(l.lesson.rule), 0),
2385
+ truncated: matches.length > lessons.length,
2386
+ matchedByKind: {
2387
+ file: countVia(byKind.file_glob),
2388
+ command: countVia(byKind.command_pattern),
2389
+ keyword: countVia(byKind.keyword),
2390
+ ...options.lexical !== void 0 ? { text: options.lexical } : {}
2391
+ },
2392
+ lessonIds: lessons.map((l) => l.id),
2393
+ bypassed: options.bypassed === true,
2394
+ ...session !== void 0 ? { session } : {}
2395
+ });
2396
+ }
2327
2397
  function outcomeLogPath(projectRoot) {
2328
2398
  return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
2329
2399
  }
@@ -2490,7 +2560,7 @@ async function recallLessons(projectRoot, query, options = {}) {
2490
2560
  const graph = load.graph;
2491
2561
  refreshCommandFastpath(projectRoot, graph, preReadStamp);
2492
2562
  const matchQuery = query.file === void 0 ? query : { ...query, file: normalizeRecallFile(query.file, projectRoot) };
2493
- const matches = queryLessons(graph, matchQuery);
2563
+ const { matches, lexicalCount } = matchLessons(graph, matchQuery);
2494
2564
  const dedup = openSessionDedup({
2495
2565
  explicit: options.sessionId,
2496
2566
  disabled: options.noDedup,
@@ -2515,34 +2585,11 @@ async function recallLessons(projectRoot, query, options = {}) {
2515
2585
  );
2516
2586
  recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, {
2517
2587
  bypassed: false,
2518
- session: options.sessionId
2588
+ session: options.sessionId,
2589
+ lexical: lexicalCount
2519
2590
  });
2520
2591
  return { lessons, totalMatches: matches.length, suppressed: matches.length - forRank.length };
2521
2592
  }
2522
- function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, options = {}) {
2523
- if (!isTelemetryEnabled(process.env, projectRoot)) return;
2524
- const byKind = collectMatchedTriggersByKind(graph, query);
2525
- const countVia = (set) => matches.filter(({ lesson }) => lesson.triggers.some((t) => set.has(t))).length;
2526
- const session = options.session ?? sessionId();
2527
- appendRecallRecord(projectRoot, {
2528
- ts: (/* @__PURE__ */ new Date()).toISOString(),
2529
- hasFile: query.file !== void 0,
2530
- hasCommand: query.command !== void 0,
2531
- hasKeyword: query.keyword !== void 0,
2532
- totalMatches: matches.length,
2533
- returnedCount: lessons.length,
2534
- returnedTokens: lessons.reduce((sum, l) => sum + estTokens(l.lesson.rule), 0),
2535
- truncated: matches.length > lessons.length,
2536
- matchedByKind: {
2537
- file: countVia(byKind.file_glob),
2538
- command: countVia(byKind.command_pattern),
2539
- keyword: countVia(byKind.keyword)
2540
- },
2541
- lessonIds: lessons.map((l) => l.id),
2542
- bypassed: options.bypassed === true,
2543
- ...session !== void 0 ? { session } : {}
2544
- });
2545
- }
2546
2593
 
2547
2594
  // src/lessons/prune.ts
2548
2595
  function planPrune(graph, options = {}) {
@@ -2996,6 +3043,91 @@ function shouldNormalizeLineEndings(path) {
2996
3043
  const base = basename(path).toLowerCase();
2997
3044
  return TEXT_DOTFILES.has(base);
2998
3045
  }
3046
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3047
+ ".png",
3048
+ ".jpg",
3049
+ ".jpeg",
3050
+ ".gif",
3051
+ ".bmp",
3052
+ ".ico",
3053
+ ".webp",
3054
+ ".avif",
3055
+ ".tiff",
3056
+ ".pdf",
3057
+ ".zip",
3058
+ ".gz",
3059
+ ".tgz",
3060
+ ".bz2",
3061
+ ".xz",
3062
+ ".7z",
3063
+ ".rar",
3064
+ ".jar",
3065
+ ".woff",
3066
+ ".woff2",
3067
+ ".ttf",
3068
+ ".otf",
3069
+ ".eot",
3070
+ ".mp3",
3071
+ ".mp4",
3072
+ ".wav",
3073
+ ".ogg",
3074
+ ".webm",
3075
+ ".mov",
3076
+ ".wasm",
3077
+ ".bin",
3078
+ ".dat",
3079
+ ".db",
3080
+ ".sqlite",
3081
+ ".so",
3082
+ ".dylib",
3083
+ ".dll",
3084
+ ".exe",
3085
+ ".class",
3086
+ ".pyc",
3087
+ // Office and design documents are zip or proprietary containers.
3088
+ ".xlsx",
3089
+ ".xls",
3090
+ ".docx",
3091
+ ".doc",
3092
+ ".pptx",
3093
+ ".ppt",
3094
+ ".odt",
3095
+ ".ods",
3096
+ ".psd",
3097
+ ".ai",
3098
+ ".sketch",
3099
+ ".fig",
3100
+ ".heic",
3101
+ ".heif",
3102
+ // Archives, columnar data, models and compressed payloads.
3103
+ ".tar",
3104
+ ".zst",
3105
+ ".br",
3106
+ ".lz4",
3107
+ ".parquet",
3108
+ ".avro",
3109
+ ".orc",
3110
+ ".pkl",
3111
+ ".npy",
3112
+ ".npz",
3113
+ ".onnx",
3114
+ ".pt",
3115
+ ".safetensors",
3116
+ ".gguf",
3117
+ ".sqlite3",
3118
+ ".avi",
3119
+ ".mkv",
3120
+ ".flac",
3121
+ ".aac",
3122
+ ".m4a",
3123
+ ".ttc"
3124
+ ]);
3125
+ function isBinaryPayloadPath(path) {
3126
+ return BINARY_EXTENSIONS.has(extname(path).toLowerCase());
3127
+ }
3128
+ function payloadEncodingFor(path) {
3129
+ return isBinaryPayloadPath(path) ? "latin1" : "utf-8";
3130
+ }
2999
3131
  function normalizeLineEndings(content) {
3000
3132
  return content.replace(/\r\n?/g, "\n");
3001
3133
  }
@@ -3003,11 +3135,28 @@ var EXECUTABLE_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zs
3003
3135
  function executableModeFor(path) {
3004
3136
  return EXECUTABLE_SCRIPT_EXTENSIONS.has(extname(path).toLowerCase()) ? 493 : void 0;
3005
3137
  }
3138
+ var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY", "EEXIST"]);
3139
+ async function renameWithRetry(from, to, options = {}) {
3140
+ const attempts = options.attempts ?? 5;
3141
+ const delayMs = options.delayMs ?? 50;
3142
+ for (let attempt = 0; ; attempt++) {
3143
+ try {
3144
+ await rename(from, to);
3145
+ return;
3146
+ } catch (err) {
3147
+ const code = err.code;
3148
+ const transient = code !== void 0 && TRANSIENT_RENAME_CODES.has(code);
3149
+ if (!transient || attempt >= attempts - 1) throw err;
3150
+ await setTimeout$1(delayMs * 2 ** attempt);
3151
+ }
3152
+ }
3153
+ }
3006
3154
 
3007
3155
  // src/utils/filesystem/fs.ts
3008
3156
  async function readFileSafe(path) {
3009
3157
  try {
3010
- const data = await readFile(path, "utf-8");
3158
+ const data = await readFile(path, payloadEncodingFor(path));
3159
+ if (isBinaryPayloadPath(path)) return data;
3011
3160
  return data.startsWith(UTF8_BOM) ? data.slice(UTF8_BOM.length) : data;
3012
3161
  } catch (err) {
3013
3162
  const e = err;
@@ -3031,40 +3180,28 @@ async function writeFileAtomic(path, content, options) {
3031
3180
  { errnoCode: "EISDIR" }
3032
3181
  );
3033
3182
  }
3034
- if (info.isSymbolicLink()) {
3035
- await unlink(path).catch((e) => {
3036
- if (e.code !== "ENOENT") throw e;
3037
- });
3038
- }
3039
3183
  } catch (err) {
3040
3184
  if (err instanceof FileSystemError) throw err;
3041
3185
  const e = err;
3042
3186
  if (e.code !== "ENOENT") throw err;
3043
3187
  }
3044
- const tmpPath = `${path}.tmp`;
3188
+ const tmpPath = `${path}.tmp-${randomUUID()}`;
3045
3189
  const payload = shouldNormalizeLineEndings(path) ? normalizeLineEndings(content) : content;
3046
3190
  const mode = executableModeFor(path);
3191
+ let handle;
3192
+ let ownsTemporaryFile = false;
3047
3193
  try {
3048
- try {
3049
- const tmpInfo = await lstat(tmpPath);
3050
- if (tmpInfo.isSymbolicLink()) {
3051
- await unlink(tmpPath);
3052
- }
3053
- } catch (tmpErr) {
3054
- if (tmpErr.code !== "ENOENT") throw tmpErr;
3055
- }
3056
- const writeOpts = {
3057
- encoding: "utf-8",
3058
- flag: "w"
3059
- };
3060
- if (mode !== void 0) writeOpts.mode = mode;
3061
- await writeFile(tmpPath, payload, writeOpts);
3062
- await rename(tmpPath, path);
3063
- if (mode !== void 0) {
3064
- await chmod(path, mode);
3065
- }
3194
+ handle = await open(tmpPath, "wx", mode);
3195
+ ownsTemporaryFile = true;
3196
+ await handle.writeFile(payload, payloadEncodingFor(path));
3197
+ if (mode !== void 0) await handle.chmod(mode);
3198
+ await handle.close();
3199
+ handle = void 0;
3200
+ await renameWithRetry(tmpPath, path);
3066
3201
  } catch (err) {
3067
- await rm(tmpPath, { force: true }).catch(() => {
3202
+ await handle?.close().catch(() => {
3203
+ });
3204
+ if (ownsTemporaryFile) await rm(tmpPath, { force: true }).catch(() => {
3068
3205
  });
3069
3206
  const e = err;
3070
3207
  throw new FileSystemError(