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.
- package/CHANGELOG.md +315 -0
- package/README.md +29 -28
- package/dist/canonical.d.ts +2 -2
- package/dist/canonical.js +16467 -9745
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +283 -229
- package/dist/engine.d.ts +8 -2
- package/dist/engine.js +12360 -5565
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +12420 -5609
- package/dist/index.js.map +1 -1
- package/dist/{init-CrZhoNTj.d.ts → init-rvKjGLTB.d.ts} +1 -1
- package/dist/lessons.d.ts +10 -2
- package/dist/lessons.js +265 -106
- package/dist/lessons.js.map +1 -1
- package/dist/{schema-CH_JJep8.d.ts → schema-BbywZEB9.d.ts} +6 -0
- package/dist/{target-descriptor-CaLUz7SR.d.ts → target-descriptor-DeS4XOtV.d.ts} +15 -2
- package/dist/targets.d.ts +3 -3
- package/dist/targets.js +16536 -9713
- package/dist/targets.js.map +1 -1
- package/package.json +2 -2
- package/schemas/agentsmesh.json +9 -0
- package/schemas/installs.json +3 -0
- package/schemas/pack.json +3 -0
|
@@ -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
|
|
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-
|
|
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-
|
|
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
|
|
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((
|
|
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
|
|
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
|
|
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
|
|
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}__${
|
|
2222
|
+
const scoped = projectRoot === void 0 ? safe : `${safe}__${shortHash2(resolve(projectRoot))}`;
|
|
2159
2223
|
return join(tmpdir(), SEEN_DIR, `${scoped}.json`);
|
|
2160
2224
|
}
|
|
2161
|
-
function
|
|
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 (
|
|
2166
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2900
|
-
universal always-on lessons \u2014 the manual equivalent of the automatic prompt
|
|
2901
|
-
hook-capable tools.
|
|
2902
|
-
so
|
|
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
|
|