@kolisachint/hoocode-agent 0.5.17 → 0.5.19
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 +247 -0
- package/dist/core/learn/audit.d.ts +136 -0
- package/dist/core/learn/audit.d.ts.map +1 -0
- package/dist/core/learn/audit.js +316 -0
- package/dist/core/learn/audit.js.map +1 -0
- package/dist/core/learn/cache.d.ts +58 -0
- package/dist/core/learn/cache.d.ts.map +1 -0
- package/dist/core/learn/cache.js +132 -0
- package/dist/core/learn/cache.js.map +1 -0
- package/dist/core/learn/cluster.d.ts +78 -0
- package/dist/core/learn/cluster.d.ts.map +1 -0
- package/dist/core/learn/cluster.js +184 -0
- package/dist/core/learn/cluster.js.map +1 -0
- package/dist/core/learn/coverage.d.ts +58 -0
- package/dist/core/learn/coverage.d.ts.map +1 -0
- package/dist/core/learn/coverage.js +144 -0
- package/dist/core/learn/coverage.js.map +1 -0
- package/dist/core/learn/digest.d.ts +13 -0
- package/dist/core/learn/digest.d.ts.map +1 -1
- package/dist/core/learn/digest.js +113 -14
- package/dist/core/learn/digest.js.map +1 -1
- package/dist/core/learn/extract.d.ts +108 -105
- package/dist/core/learn/extract.d.ts.map +1 -1
- package/dist/core/learn/extract.js +308 -447
- package/dist/core/learn/extract.js.map +1 -1
- package/dist/core/learn/mine.d.ts +178 -0
- package/dist/core/learn/mine.d.ts.map +1 -0
- package/dist/core/learn/mine.js +390 -0
- package/dist/core/learn/mine.js.map +1 -0
- package/dist/core/learn/reduce.d.ts +89 -0
- package/dist/core/learn/reduce.d.ts.map +1 -0
- package/dist/core/learn/reduce.js +179 -0
- package/dist/core/learn/reduce.js.map +1 -0
- package/dist/core/learn/state.d.ts +19 -18
- package/dist/core/learn/state.d.ts.map +1 -1
- package/dist/core/learn/state.js +35 -31
- package/dist/core/learn/state.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +5 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/startup-progress.d.ts +12 -7
- package/dist/core/startup-progress.d.ts.map +1 -1
- package/dist/core/startup-progress.js +12 -7
- package/dist/core/startup-progress.js.map +1 -1
- package/dist/extensions/core/learn.d.ts +8 -4
- package/dist/extensions/core/learn.d.ts.map +1 -1
- package/dist/extensions/core/learn.js +292 -56
- package/dist/extensions/core/learn.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -25
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/progress-bar.d.ts +50 -0
- package/dist/modes/interactive/components/progress-bar.d.ts.map +1 -0
- package/dist/modes/interactive/components/progress-bar.js +77 -0
- package/dist/modes/interactive/components/progress-bar.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts +6 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.js +18 -14
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
- package/docs/settings.md +9 -6
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/core/learn/normalize.d.ts +0 -65
- package/dist/core/learn/normalize.d.ts.map +0 -1
- package/dist/core/learn/normalize.js +0 -245
- package/dist/core/learn/normalize.js.map +0 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reduce half of `/learn`: turn per-session observations into counts.
|
|
3
|
+
*
|
|
4
|
+
* The split with `mine.ts` is the whole design. Deciding that "we're on bun
|
|
5
|
+
* now" and "stop using npm" mean the same thing is semantics, and the model is
|
|
6
|
+
* better at it than any normalizer — so the model does it, by emitting a shared
|
|
7
|
+
* `label`. Deciding that the shared label occurred nine times across five
|
|
8
|
+
* sessions is arithmetic, and arithmetic stays here, because a model asked to
|
|
9
|
+
* count over a long context will be approximately right, and an approximately
|
|
10
|
+
* right number is worse than none when the number is the thing the reader acts
|
|
11
|
+
* on.
|
|
12
|
+
*
|
|
13
|
+
* Nothing in this file filters on content. The only gate is the repeat
|
|
14
|
+
* threshold, which is a dial the user sets and the digest reports, not a
|
|
15
|
+
* whitelist they cannot see.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Group every candidate of one kind by its label.
|
|
19
|
+
*
|
|
20
|
+
* `lastSeen` takes the session's last activity rather than anything the model
|
|
21
|
+
* reports: the model is reading a transcript and has no reliable clock, and
|
|
22
|
+
* `lastSeen` drives suppression, where a wrong value silently hides a live
|
|
23
|
+
* signal or resurfaces a dead one. It is deliberately not the session's start
|
|
24
|
+
* either — a long-running session would date today's words to the day it was
|
|
25
|
+
* opened, and suppression would call them old news.
|
|
26
|
+
*/
|
|
27
|
+
function groupByLabel(sessions, kind) {
|
|
28
|
+
const acc = new Map();
|
|
29
|
+
for (const session of sessions) {
|
|
30
|
+
for (const candidate of session.candidates) {
|
|
31
|
+
if (candidate.kind !== kind)
|
|
32
|
+
continue;
|
|
33
|
+
const existing = acc.get(candidate.label);
|
|
34
|
+
if (existing) {
|
|
35
|
+
existing.count++;
|
|
36
|
+
existing.sessions.add(session.sessionId);
|
|
37
|
+
if (session.lastActivity > existing.lastSeen)
|
|
38
|
+
existing.lastSeen = session.lastActivity;
|
|
39
|
+
// Keep the fullest quote: a longer one carries more of the reasoning.
|
|
40
|
+
if (candidate.text.length > existing.text.length)
|
|
41
|
+
existing.text = candidate.text;
|
|
42
|
+
existing.rationale ??= candidate.rationale;
|
|
43
|
+
existing.samples.push(candidate);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
acc.set(candidate.label, {
|
|
47
|
+
label: candidate.label,
|
|
48
|
+
text: candidate.text,
|
|
49
|
+
rationale: candidate.rationale,
|
|
50
|
+
count: 1,
|
|
51
|
+
sessions: new Set([session.sessionId]),
|
|
52
|
+
lastSeen: session.lastActivity,
|
|
53
|
+
samples: [candidate],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...acc.values()];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Merge accumulators whose representative quote is the same sentence.
|
|
62
|
+
*
|
|
63
|
+
* The model labels each session independently and has no way to see what it
|
|
64
|
+
* called the same thing last time, so identical text routinely arrives under
|
|
65
|
+
* two labels — and then reads as two separate proposals, the same words twice.
|
|
66
|
+
* Matching on the quote costs nothing and is the one case where sameness is not
|
|
67
|
+
* a judgement call.
|
|
68
|
+
*
|
|
69
|
+
* This runs before the repeat threshold on purpose: two occurrences split
|
|
70
|
+
* across two labels are each below the bar, and merging them afterwards would
|
|
71
|
+
* mean neither was ever considered.
|
|
72
|
+
*
|
|
73
|
+
* The surviving label is the lexicographically smallest of the merged set, not
|
|
74
|
+
* the first one seen. First-seen follows session order, which changes whenever
|
|
75
|
+
* a session is added — so the merged cluster would silently change its name
|
|
76
|
+
* between runs, the state key with it, and every item the reader had already
|
|
77
|
+
* decided on would come back as new. A merge has to be a pure function of what
|
|
78
|
+
* was merged.
|
|
79
|
+
*/
|
|
80
|
+
function mergeIdenticalText(entries) {
|
|
81
|
+
const byText = new Map();
|
|
82
|
+
const order = [];
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const key = entry.text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
85
|
+
const existing = byText.get(key);
|
|
86
|
+
if (!existing) {
|
|
87
|
+
byText.set(key, entry);
|
|
88
|
+
order.push(entry);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
existing.count += entry.count;
|
|
92
|
+
for (const session of entry.sessions)
|
|
93
|
+
existing.sessions.add(session);
|
|
94
|
+
if (entry.lastSeen > existing.lastSeen)
|
|
95
|
+
existing.lastSeen = entry.lastSeen;
|
|
96
|
+
existing.rationale ??= entry.rationale;
|
|
97
|
+
existing.samples.push(...entry.samples);
|
|
98
|
+
if (entry.label < existing.label)
|
|
99
|
+
existing.label = entry.label;
|
|
100
|
+
}
|
|
101
|
+
return order;
|
|
102
|
+
}
|
|
103
|
+
/** Distinct sessions first, then raw count: five sessions beats nine times in one. */
|
|
104
|
+
function byEvidence(a, b) {
|
|
105
|
+
return b.sessions - a.sessions || b.count - a.count || a.label.localeCompare(b.label);
|
|
106
|
+
}
|
|
107
|
+
/** Merge repeated string fields across a cluster's samples, preserving order and dropping dupes. */
|
|
108
|
+
function mergeStrings(samples, pick) {
|
|
109
|
+
const out = [];
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
for (const sample of samples) {
|
|
112
|
+
for (const value of pick(sample) ?? []) {
|
|
113
|
+
if (seen.has(value))
|
|
114
|
+
continue;
|
|
115
|
+
seen.add(value);
|
|
116
|
+
out.push(value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out.slice(0, 12);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The repeat threshold counts distinct sessions, not occurrences.
|
|
123
|
+
*
|
|
124
|
+
* Saying a thing twice inside one session is the commonest thing in a
|
|
125
|
+
* transcript and usually means the opposite of durable: the agent ignored it
|
|
126
|
+
* the first time, so it was restated. Two *sessions* is a claim about how you
|
|
127
|
+
* work; two lines in one session is a claim about one afternoon.
|
|
128
|
+
*/
|
|
129
|
+
function repeatedEnough(entry, minRepeats) {
|
|
130
|
+
return entry.sessions.size >= minRepeats;
|
|
131
|
+
}
|
|
132
|
+
export function reduceDirectives(sessions, minRepeats) {
|
|
133
|
+
return mergeIdenticalText(groupByLabel(sessions, "directive"))
|
|
134
|
+
.filter((entry) => repeatedEnough(entry, minRepeats))
|
|
135
|
+
.map((entry) => ({
|
|
136
|
+
key: `directive:${entry.label}`,
|
|
137
|
+
label: entry.label,
|
|
138
|
+
text: entry.text,
|
|
139
|
+
rationale: entry.rationale,
|
|
140
|
+
count: entry.count,
|
|
141
|
+
sessions: entry.sessions.size,
|
|
142
|
+
lastSeen: entry.lastSeen,
|
|
143
|
+
// Coverage is decided later, by a model that can tell a paraphrase from a
|
|
144
|
+
// coincidence. Everything starts `new` and is corrected in place.
|
|
145
|
+
status: "new",
|
|
146
|
+
previouslyDeclined: false,
|
|
147
|
+
}))
|
|
148
|
+
.sort(byEvidence);
|
|
149
|
+
}
|
|
150
|
+
export function reduceFixes(sessions, minRepeats) {
|
|
151
|
+
return groupByLabel(sessions, "fix")
|
|
152
|
+
.filter((entry) => repeatedEnough(entry, minRepeats))
|
|
153
|
+
.map((entry) => ({
|
|
154
|
+
key: `fix:${entry.label}`,
|
|
155
|
+
label: entry.label,
|
|
156
|
+
command: entry.samples.find((s) => s.command)?.command ?? entry.text,
|
|
157
|
+
errorExcerpt: entry.samples.find((s) => s.errorExcerpt)?.errorExcerpt ?? "",
|
|
158
|
+
interveningCommands: mergeStrings(entry.samples, (s) => s.interveningCommands),
|
|
159
|
+
editedFiles: mergeStrings(entry.samples, (s) => s.editedFiles),
|
|
160
|
+
count: entry.count,
|
|
161
|
+
sessions: entry.sessions.size,
|
|
162
|
+
lastSeen: entry.lastSeen,
|
|
163
|
+
}))
|
|
164
|
+
.sort(byEvidence);
|
|
165
|
+
}
|
|
166
|
+
export function reduceRequests(sessions, minRepeats) {
|
|
167
|
+
return mergeIdenticalText(groupByLabel(sessions, "request"))
|
|
168
|
+
.filter((entry) => repeatedEnough(entry, minRepeats))
|
|
169
|
+
.map((entry) => ({
|
|
170
|
+
key: `request:${entry.label}`,
|
|
171
|
+
label: entry.label,
|
|
172
|
+
text: entry.text,
|
|
173
|
+
count: entry.count,
|
|
174
|
+
sessions: entry.sessions.size,
|
|
175
|
+
lastSeen: entry.lastSeen,
|
|
176
|
+
}))
|
|
177
|
+
.sort(byEvidence);
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=reduce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reduce.js","sourceRoot":"","sources":["../../../src/core/learn/reduce.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The reduce half of `/learn`: turn per-session observations into counts.\n *\n * The split with `mine.ts` is the whole design. Deciding that \"we're on bun\n * now\" and \"stop using npm\" mean the same thing is semantics, and the model is\n * better at it than any normalizer — so the model does it, by emitting a shared\n * `label`. Deciding that the shared label occurred nine times across five\n * sessions is arithmetic, and arithmetic stays here, because a model asked to\n * count over a long context will be approximately right, and an approximately\n * right number is worse than none when the number is the thing the reader acts\n * on.\n *\n * Nothing in this file filters on content. The only gate is the repeat\n * threshold, which is a dial the user sets and the digest reports, not a\n * whitelist they cannot see.\n */\n\nimport type { LabelledCandidate, MinedCandidate } from \"./mine.js\";\n\n/** Where a repeated directive already lives, if anywhere. */\nexport type DirectiveStatus = \"new\" | \"restated\" | \"has-skill\";\n\n/** Fields every proposable item shares, so suppression can be applied uniformly. */\nexport interface Proposable {\n\t/** Stable identity across runs — what the state file remembers. */\n\tkey: string;\n\t/** Newest occurrence in the window, ISO. */\n\tlastSeen: string;\n}\n\nexport interface DirectiveCluster extends Proposable {\n\t/** The model's canonical name for what was meant. The clustering key. */\n\tlabel: string;\n\t/** Representative verbatim quote, the longest seen in the cluster. */\n\ttext: string;\n\t/** Why it is durable, in the model's words. */\n\trationale?: string;\n\t/** Total times said. */\n\tcount: number;\n\t/** Distinct sessions it was said in — the stronger of the two counts. */\n\tsessions: number;\n\tstatus: DirectiveStatus;\n\t/** The existing rule line matched, when status is `restated`. */\n\texistingRule?: string;\n\t/** The skill that already covers this, when status is `has-skill`. */\n\texistingSkill?: string;\n\t/**\n\t * Shown before and still not written down anywhere — neither as a rule nor as\n\t * a skill — so the reader saw this proposal and passed on it.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\nexport interface FixCandidate extends Proposable {\n\tlabel: string;\n\t/** The failing command. */\n\tcommand: string;\n\t/** Short excerpt of the real error text. */\n\terrorExcerpt: string;\n\t/** Commands run between the failure and the pass. */\n\tinterveningCommands: string[];\n\t/** Files edited between the failure and the pass. */\n\teditedFiles: string[];\n\tcount: number;\n\tsessions: number;\n}\n\n/**\n * A piece of work the user keeps asking for by name.\n *\n * This is the slash-command signal, and it was being thrown away: the miner\n * used to be told not to report task requests at all. But a request repeated\n * across sessions is exactly what a slash command is for, and the evidence for\n * it is stronger than for most rules — the same job, asked for again, in\n * someone's own words.\n */\nexport interface RequestCandidate extends Proposable {\n\tlabel: string;\n\t/** How the user phrased it, so the proposal can quote rather than invent. */\n\ttext: string;\n\tcount: number;\n\tsessions: number;\n}\n\n/** One session's mining output, named and tagged with the identity the counts need. */\nexport interface MinedSession {\n\tsessionId: string;\n\t/** When the session was last written to — the clock suppression runs on. */\n\tlastActivity: string;\n\t/** Named by the global clustering pass; grouping here is exact-label arithmetic. */\n\tcandidates: LabelledCandidate[];\n}\n\ninterface Acc {\n\tlabel: string;\n\ttext: string;\n\trationale?: string;\n\tcount: number;\n\tsessions: Set<string>;\n\tlastSeen: string;\n\tsamples: MinedCandidate[];\n}\n\n/**\n * Group every candidate of one kind by its label.\n *\n * `lastSeen` takes the session's last activity rather than anything the model\n * reports: the model is reading a transcript and has no reliable clock, and\n * `lastSeen` drives suppression, where a wrong value silently hides a live\n * signal or resurfaces a dead one. It is deliberately not the session's start\n * either — a long-running session would date today's words to the day it was\n * opened, and suppression would call them old news.\n */\nfunction groupByLabel(sessions: MinedSession[], kind: MinedCandidate[\"kind\"]): Acc[] {\n\tconst acc = new Map<string, Acc>();\n\n\tfor (const session of sessions) {\n\t\tfor (const candidate of session.candidates) {\n\t\t\tif (candidate.kind !== kind) continue;\n\t\t\tconst existing = acc.get(candidate.label);\n\t\t\tif (existing) {\n\t\t\t\texisting.count++;\n\t\t\t\texisting.sessions.add(session.sessionId);\n\t\t\t\tif (session.lastActivity > existing.lastSeen) existing.lastSeen = session.lastActivity;\n\t\t\t\t// Keep the fullest quote: a longer one carries more of the reasoning.\n\t\t\t\tif (candidate.text.length > existing.text.length) existing.text = candidate.text;\n\t\t\t\texisting.rationale ??= candidate.rationale;\n\t\t\t\texisting.samples.push(candidate);\n\t\t\t} else {\n\t\t\t\tacc.set(candidate.label, {\n\t\t\t\t\tlabel: candidate.label,\n\t\t\t\t\ttext: candidate.text,\n\t\t\t\t\trationale: candidate.rationale,\n\t\t\t\t\tcount: 1,\n\t\t\t\t\tsessions: new Set([session.sessionId]),\n\t\t\t\t\tlastSeen: session.lastActivity,\n\t\t\t\t\tsamples: [candidate],\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...acc.values()];\n}\n\n/**\n * Merge accumulators whose representative quote is the same sentence.\n *\n * The model labels each session independently and has no way to see what it\n * called the same thing last time, so identical text routinely arrives under\n * two labels — and then reads as two separate proposals, the same words twice.\n * Matching on the quote costs nothing and is the one case where sameness is not\n * a judgement call.\n *\n * This runs before the repeat threshold on purpose: two occurrences split\n * across two labels are each below the bar, and merging them afterwards would\n * mean neither was ever considered.\n *\n * The surviving label is the lexicographically smallest of the merged set, not\n * the first one seen. First-seen follows session order, which changes whenever\n * a session is added — so the merged cluster would silently change its name\n * between runs, the state key with it, and every item the reader had already\n * decided on would come back as new. A merge has to be a pure function of what\n * was merged.\n */\nfunction mergeIdenticalText(entries: Acc[]): Acc[] {\n\tconst byText = new Map<string, Acc>();\n\tconst order: Acc[] = [];\n\n\tfor (const entry of entries) {\n\t\tconst key = entry.text.replace(/\\s+/g, \" \").trim().toLowerCase();\n\t\tconst existing = byText.get(key);\n\t\tif (!existing) {\n\t\t\tbyText.set(key, entry);\n\t\t\torder.push(entry);\n\t\t\tcontinue;\n\t\t}\n\t\texisting.count += entry.count;\n\t\tfor (const session of entry.sessions) existing.sessions.add(session);\n\t\tif (entry.lastSeen > existing.lastSeen) existing.lastSeen = entry.lastSeen;\n\t\texisting.rationale ??= entry.rationale;\n\t\texisting.samples.push(...entry.samples);\n\t\tif (entry.label < existing.label) existing.label = entry.label;\n\t}\n\n\treturn order;\n}\n\n/** Distinct sessions first, then raw count: five sessions beats nine times in one. */\nfunction byEvidence(a: { sessions: number; count: number; label: string }, b: typeof a): number {\n\treturn b.sessions - a.sessions || b.count - a.count || a.label.localeCompare(b.label);\n}\n\n/** Merge repeated string fields across a cluster's samples, preserving order and dropping dupes. */\nfunction mergeStrings(samples: MinedCandidate[], pick: (c: MinedCandidate) => string[] | undefined): string[] {\n\tconst out: string[] = [];\n\tconst seen = new Set<string>();\n\tfor (const sample of samples) {\n\t\tfor (const value of pick(sample) ?? []) {\n\t\t\tif (seen.has(value)) continue;\n\t\t\tseen.add(value);\n\t\t\tout.push(value);\n\t\t}\n\t}\n\treturn out.slice(0, 12);\n}\n\n/**\n * The repeat threshold counts distinct sessions, not occurrences.\n *\n * Saying a thing twice inside one session is the commonest thing in a\n * transcript and usually means the opposite of durable: the agent ignored it\n * the first time, so it was restated. Two *sessions* is a claim about how you\n * work; two lines in one session is a claim about one afternoon.\n */\nfunction repeatedEnough(entry: Acc, minRepeats: number): boolean {\n\treturn entry.sessions.size >= minRepeats;\n}\n\nexport function reduceDirectives(sessions: MinedSession[], minRepeats: number): DirectiveCluster[] {\n\treturn mergeIdenticalText(groupByLabel(sessions, \"directive\"))\n\t\t.filter((entry) => repeatedEnough(entry, minRepeats))\n\t\t.map((entry) => ({\n\t\t\tkey: `directive:${entry.label}`,\n\t\t\tlabel: entry.label,\n\t\t\ttext: entry.text,\n\t\t\trationale: entry.rationale,\n\t\t\tcount: entry.count,\n\t\t\tsessions: entry.sessions.size,\n\t\t\tlastSeen: entry.lastSeen,\n\t\t\t// Coverage is decided later, by a model that can tell a paraphrase from a\n\t\t\t// coincidence. Everything starts `new` and is corrected in place.\n\t\t\tstatus: \"new\" as DirectiveStatus,\n\t\t\tpreviouslyDeclined: false,\n\t\t}))\n\t\t.sort(byEvidence);\n}\n\nexport function reduceFixes(sessions: MinedSession[], minRepeats: number): FixCandidate[] {\n\treturn groupByLabel(sessions, \"fix\")\n\t\t.filter((entry) => repeatedEnough(entry, minRepeats))\n\t\t.map((entry) => ({\n\t\t\tkey: `fix:${entry.label}`,\n\t\t\tlabel: entry.label,\n\t\t\tcommand: entry.samples.find((s) => s.command)?.command ?? entry.text,\n\t\t\terrorExcerpt: entry.samples.find((s) => s.errorExcerpt)?.errorExcerpt ?? \"\",\n\t\t\tinterveningCommands: mergeStrings(entry.samples, (s) => s.interveningCommands),\n\t\t\teditedFiles: mergeStrings(entry.samples, (s) => s.editedFiles),\n\t\t\tcount: entry.count,\n\t\t\tsessions: entry.sessions.size,\n\t\t\tlastSeen: entry.lastSeen,\n\t\t}))\n\t\t.sort(byEvidence);\n}\n\nexport function reduceRequests(sessions: MinedSession[], minRepeats: number): RequestCandidate[] {\n\treturn mergeIdenticalText(groupByLabel(sessions, \"request\"))\n\t\t.filter((entry) => repeatedEnough(entry, minRepeats))\n\t\t.map((entry) => ({\n\t\t\tkey: `request:${entry.label}`,\n\t\t\tlabel: entry.label,\n\t\t\ttext: entry.text,\n\t\t\tcount: entry.count,\n\t\t\tsessions: entry.sessions.size,\n\t\t\tlastSeen: entry.lastSeen,\n\t\t}))\n\t\t.sort(byEvidence);\n}\n"]}
|
|
@@ -31,6 +31,13 @@ export interface SurfacedItem {
|
|
|
31
31
|
* told from a declined one, without asking.
|
|
32
32
|
*/
|
|
33
33
|
coveredWhenSurfaced: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Representative wording, kept so `/learn stats` can ask about coverage using
|
|
36
|
+
* what was actually said. The key alone is a slug, and judging "is this
|
|
37
|
+
* written down?" from a slug is a much weaker question than judging it from
|
|
38
|
+
* the sentence the slug stands for.
|
|
39
|
+
*/
|
|
40
|
+
text?: string;
|
|
34
41
|
}
|
|
35
42
|
export interface LearnState {
|
|
36
43
|
version: number;
|
|
@@ -84,35 +91,29 @@ export interface LearnStats {
|
|
|
84
91
|
total: number;
|
|
85
92
|
directives: number;
|
|
86
93
|
fixes: number;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Directive proposals that were genuinely open questions — not already
|
|
90
|
-
* written down when they were shown. Only these can be adopted or passed
|
|
91
|
-
* over, so they are the denominator of the rate.
|
|
92
|
-
*/
|
|
93
|
-
open: number;
|
|
94
|
-
adopted: number;
|
|
95
|
-
declined: number;
|
|
94
|
+
requests: number;
|
|
96
95
|
earliest?: string;
|
|
97
96
|
latest?: string;
|
|
98
97
|
lastRun?: string;
|
|
99
98
|
}
|
|
100
99
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* Nothing extra is recorded to make this work: each entry already stores whether
|
|
104
|
-
* it was covered when it was shown, and coverage now is recomputable, so the
|
|
105
|
-
* pair is enough to tell an adopted proposal from one that was passed over.
|
|
100
|
+
* Count what has been proposed, and when.
|
|
106
101
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
102
|
+
* Deliberately no adoption rate. There used to be one, derived by re-judging
|
|
103
|
+
* coverage and calling the delta "adopted", and it was wrong in both
|
|
104
|
+
* directions: a failed judge at either end moved the number, and a proposal
|
|
105
|
+
* correctly rejected as junk was indistinguishable from one ignored. It shipped
|
|
106
|
+
* with two disclaimers explaining how not to misread it, which is the clearest
|
|
107
|
+
* possible sign that it should not have shipped. What the reader actually wants
|
|
108
|
+
* — is the always-loaded surface growing or shrinking — is measurable exactly,
|
|
109
|
+
* from the context files themselves, and is reported instead.
|
|
110
110
|
*/
|
|
111
|
-
export declare function summarizeLearnState(state: LearnState
|
|
111
|
+
export declare function summarizeLearnState(state: LearnState): LearnStats;
|
|
112
112
|
/** Record everything this run put on screen, so the next run can suppress it. */
|
|
113
113
|
export declare function recordSurfaced(state: LearnState, items: Array<{
|
|
114
114
|
key: string;
|
|
115
115
|
lastSeen: string;
|
|
116
116
|
covered: boolean;
|
|
117
|
+
text?: string;
|
|
117
118
|
}>, now?: Date): LearnState;
|
|
118
119
|
//# sourceMappingURL=state.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/learn/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,MAAM,WAAW,YAAY;IAC5B,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,mBAAmB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACvC;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAavD;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,GAAE,IAAiB,GAAG,IAAI,CAe7F;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IAClC,qEAAqE;IACrE,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,gBAAgB,GAAG,kBAAkB,CAcpF;AAED,MAAM,WAAW,UAAU;IAC1B,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,GAAG,UAAU,CA6BhH;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAC7B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,EACjE,GAAG,GAAE,IAAiB,GACpB,UAAU,CAOZ","sourcesContent":["/**\n * Per-directory memory of what `/learn` has already shown you.\n *\n * The extractor computes a sliding-window snapshot — \"in your last N sessions\n * you said this M times\" — which is stateless by design and correct to\n * recompute from scratch every run. What it cannot know on its own is whether\n * you have already *seen* a given item and made a call on it. Without that, a\n * rule you accepted last week comes back forever (its occurrences are still\n * inside the window), and worse, it comes back flagged `restated` — accusing a\n * rule that is working of not working. An item you declined comes back too,\n * unchanged, every single run.\n *\n * So one bookmark per directory: what was surfaced, and when. An item is shown\n * again only once something new has happened — you said it again *after* it was\n * last put in front of you. That single rule fixes both cases, and it keeps the\n * `restated` label honest, because the only way to earn it is to repeat yourself\n * after the rule already existed.\n *\n * This is a bookmark, not an index. No cards, no retrieval, no embeddings — the\n * transcripts remain the source of truth and this file can be deleted at any\n * time with no loss beyond re-proposing things once.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { writeFileAtomicSync } from \"../../utils/atomic-file.js\";\n\n/** Bump when the on-disk shape changes; an unknown version is discarded, not migrated. */\nconst STATE_VERSION = 1;\n\n/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */\nconst STATE_RETENTION_DAYS = 180;\n\nexport interface SurfacedItem {\n\t/** When this item was last put in front of the user, ISO. */\n\tsurfacedAt: string;\n\t/** Newest occurrence the run knew about at that point, ISO. */\n\tlastOccurrence: string;\n\t/**\n\t * Whether the item was covered by a context file the last time it was\n\t * surfaced. Comparing that against coverage now is how an adopted proposal is\n\t * told from a declined one, without asking.\n\t */\n\tcoveredWhenSurfaced: boolean;\n}\n\nexport interface LearnState {\n\tversion: number;\n\tlastRun?: string;\n\tsurfaced: Record<string, SurfacedItem>;\n}\n\nfunction emptyState(): LearnState {\n\treturn { version: STATE_VERSION, surfaced: {} };\n}\n\n/**\n * State file for a directory.\n *\n * Keyed off the *name* of the cwd's session directory, which encodes the cwd —\n * so the bookmark is per working directory, matching what the digest describes.\n * Callers pass the cwd-derived path rather than whatever directory the live\n * session happens to be reading: a session with no directory of its own would\n * otherwise land every project on one nameless state file, and projects sharing\n * a custom `sessionDir` would share one bookmark between them.\n */\nexport function getLearnStatePath(agentDir: string, sessionDir: string): string {\n\treturn join(agentDir, \"learn\", `${basename(sessionDir)}.json`);\n}\n\n/** Read state, treating any unreadable or unknown-version file as empty. */\nexport function readLearnState(path: string): LearnState {\n\ttry {\n\t\tif (!existsSync(path)) return emptyState();\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as Partial<LearnState>;\n\t\tif (parsed.version !== STATE_VERSION || typeof parsed.surfaced !== \"object\" || parsed.surfaced === null) {\n\t\t\treturn emptyState();\n\t\t}\n\t\treturn { version: STATE_VERSION, lastRun: parsed.lastRun, surfaced: parsed.surfaced };\n\t} catch {\n\t\t// A corrupt bookmark is not worth failing a command over; the cost of\n\t\t// starting fresh is proposing a few things a second time.\n\t\treturn emptyState();\n\t}\n}\n\n/** Persist state, pruning entries nothing has referenced in a long time. */\nexport function writeLearnState(path: string, state: LearnState, now: Date = new Date()): void {\n\tconst cutoff = now.getTime() - STATE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n\tconst surfaced: Record<string, SurfacedItem> = {};\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tconst at = Date.parse(item.surfacedAt);\n\t\tif (Number.isNaN(at) || at >= cutoff) surfaced[key] = item;\n\t}\n\n\ttry {\n\t\tmkdirSync(join(path, \"..\"), { recursive: true });\n\t\twriteFileAtomicSync(path, `${JSON.stringify({ ...state, surfaced }, null, 2)}\\n`);\n\t} catch {\n\t\t// Losing the bookmark degrades /learn to its previous behaviour (proposing\n\t\t// already-decided items again). Never worth failing the command.\n\t}\n}\n\n/** What the caller needs to decide whether an item is worth showing again. */\nexport interface SuppressionInput {\n\tkey: string;\n\t/** Newest occurrence of this item in the current window, ISO. */\n\tlastSeen: string;\n\t/** Whether a context file covers it right now. */\n\tcovered: boolean;\n}\n\nexport interface SuppressionVerdict {\n\t/** Drop it: already surfaced, and nothing new has happened since. */\n\tsuppressed: boolean;\n\t/**\n\t * Surfaced before, still not covered by any context file — you saw it and\n\t * chose not to write it down. Worth telling the model so it proposes again\n\t * tentatively instead of pressing the same case twice.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\n/**\n * Decide whether an item should be shown again.\n *\n * An item is suppressed when it was surfaced before and has not recurred since:\n * `lastSeen <= surfacedAt` means every occurrence backing it was already on\n * screen when you made your call. Say it again and `lastSeen` moves past\n * `surfacedAt`, and it returns — which is exactly when it is worth returning.\n */\nexport function judge(state: LearnState, input: SuppressionInput): SuppressionVerdict {\n\tconst previous = state.surfaced[input.key];\n\tif (!previous) return { suppressed: false, previouslyDeclined: false };\n\n\tconst surfacedAt = Date.parse(previous.surfacedAt);\n\tconst lastSeen = Date.parse(input.lastSeen);\n\tconst recurredSince = !Number.isNaN(surfacedAt) && !Number.isNaN(lastSeen) && lastSeen > surfacedAt;\n\n\treturn {\n\t\tsuppressed: !recurredSince,\n\t\t// Covered now but not when it was surfaced means the proposal was taken up.\n\t\t// Still uncovered means it was passed over.\n\t\tpreviouslyDeclined: !input.covered && !previous.coveredWhenSurfaced,\n\t};\n}\n\nexport interface LearnStats {\n\t/** Proposals on record, across all categories. */\n\ttotal: number;\n\tdirectives: number;\n\tfixes: number;\n\tworkflows: number;\n\t/**\n\t * Directive proposals that were genuinely open questions — not already\n\t * written down when they were shown. Only these can be adopted or passed\n\t * over, so they are the denominator of the rate.\n\t */\n\topen: number;\n\tadopted: number;\n\tdeclined: number;\n\tearliest?: string;\n\tlatest?: string;\n\tlastRun?: string;\n}\n\n/**\n * Reconstruct what happened to past proposals.\n *\n * Nothing extra is recorded to make this work: each entry already stores whether\n * it was covered when it was shown, and coverage now is recomputable, so the\n * pair is enough to tell an adopted proposal from one that was passed over.\n *\n * Directives only. Fixes and workflows have no coverage signal — a fix may have\n * become a rule, a skill, or a habit, and which one is not recoverable — so\n * counting them here would mean inventing an outcome.\n */\nexport function summarizeLearnState(state: LearnState, isCoveredNow: (normalized: string) => boolean): LearnStats {\n\tconst stats: LearnStats = { total: 0, directives: 0, fixes: 0, workflows: 0, open: 0, adopted: 0, declined: 0 };\n\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tstats.total++;\n\t\tif (!stats.earliest || item.surfacedAt < stats.earliest) stats.earliest = item.surfacedAt;\n\t\tif (!stats.latest || item.surfacedAt > stats.latest) stats.latest = item.surfacedAt;\n\n\t\tif (key.startsWith(\"fix:\")) {\n\t\t\tstats.fixes++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (key.startsWith(\"workflow:\")) {\n\t\t\tstats.workflows++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!key.startsWith(\"directive:\")) continue;\n\n\t\tstats.directives++;\n\t\t// Already written down when shown, so there was no decision to make.\n\t\tif (item.coveredWhenSurfaced) continue;\n\n\t\tstats.open++;\n\t\tif (isCoveredNow(key.slice(\"directive:\".length))) stats.adopted++;\n\t\telse stats.declined++;\n\t}\n\n\tstats.lastRun = state.lastRun;\n\treturn stats;\n}\n\n/** Record everything this run put on screen, so the next run can suppress it. */\nexport function recordSurfaced(\n\tstate: LearnState,\n\titems: Array<{ key: string; lastSeen: string; covered: boolean }>,\n\tnow: Date = new Date(),\n): LearnState {\n\tconst surfaced = { ...state.surfaced };\n\tconst at = now.toISOString();\n\tfor (const item of items) {\n\t\tsurfaced[item.key] = { surfacedAt: at, lastOccurrence: item.lastSeen, coveredWhenSurfaced: item.covered };\n\t}\n\treturn { version: STATE_VERSION, lastRun: at, surfaced };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/learn/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAuBH,MAAM,WAAW,YAAY;IAC5B,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,mBAAmB,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACvC;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAavD;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,GAAE,IAAiB,GAAG,IAAI,CAe7F;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IAClC,qEAAqE;IACrE,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,gBAAgB,GAAG,kBAAkB,CAcpF;AAED,MAAM,WAAW,UAAU;IAC1B,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAejE;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAC7B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EAChF,GAAG,GAAE,IAAiB,GACpB,UAAU,CAYZ","sourcesContent":["/**\n * Per-directory memory of what `/learn` has already shown you.\n *\n * The extractor computes a sliding-window snapshot — \"in your last N sessions\n * you said this M times\" — which is stateless by design and correct to\n * recompute from scratch every run. What it cannot know on its own is whether\n * you have already *seen* a given item and made a call on it. Without that, a\n * rule you accepted last week comes back forever (its occurrences are still\n * inside the window), and worse, it comes back flagged `restated` — accusing a\n * rule that is working of not working. An item you declined comes back too,\n * unchanged, every single run.\n *\n * So one bookmark per directory: what was surfaced, and when. An item is shown\n * again only once something new has happened — you said it again *after* it was\n * last put in front of you. That single rule fixes both cases, and it keeps the\n * `restated` label honest, because the only way to earn it is to repeat yourself\n * after the rule already existed.\n *\n * This is a bookmark, not an index. No cards, no retrieval, no embeddings — the\n * transcripts remain the source of truth and this file can be deleted at any\n * time with no loss beyond re-proposing things once.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { writeFileAtomicSync } from \"../../utils/atomic-file.js\";\n\n/**\n * Bump when the on-disk shape changes; an unknown version is discarded, not\n * migrated.\n *\n * v2: keys hold the miner's semantic label (`directive:use-bun-not-npm`) where\n * v1 held normalized directive text (`directive:always use bun not npm`).\n *\n * v3: `workflow:` keys are gone — the category was removed — and labels now come\n * from a global naming pass rather than per-session guesses, so the keys a v2\n * file holds were drawn from a vocabulary that no longer exists. Keeping them\n * would suppress nothing and count as proposals that were never revisited.\n */\nconst STATE_VERSION = 3;\n\n/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */\nconst STATE_RETENTION_DAYS = 180;\n\nexport interface SurfacedItem {\n\t/** When this item was last put in front of the user, ISO. */\n\tsurfacedAt: string;\n\t/** Newest occurrence the run knew about at that point, ISO. */\n\tlastOccurrence: string;\n\t/**\n\t * Whether the item was covered by a context file the last time it was\n\t * surfaced. Comparing that against coverage now is how an adopted proposal is\n\t * told from a declined one, without asking.\n\t */\n\tcoveredWhenSurfaced: boolean;\n\t/**\n\t * Representative wording, kept so `/learn stats` can ask about coverage using\n\t * what was actually said. The key alone is a slug, and judging \"is this\n\t * written down?\" from a slug is a much weaker question than judging it from\n\t * the sentence the slug stands for.\n\t */\n\ttext?: string;\n}\n\nexport interface LearnState {\n\tversion: number;\n\tlastRun?: string;\n\tsurfaced: Record<string, SurfacedItem>;\n}\n\nfunction emptyState(): LearnState {\n\treturn { version: STATE_VERSION, surfaced: {} };\n}\n\n/**\n * State file for a directory.\n *\n * Keyed off the *name* of the cwd's session directory, which encodes the cwd —\n * so the bookmark is per working directory, matching what the digest describes.\n * Callers pass the cwd-derived path rather than whatever directory the live\n * session happens to be reading: a session with no directory of its own would\n * otherwise land every project on one nameless state file, and projects sharing\n * a custom `sessionDir` would share one bookmark between them.\n */\nexport function getLearnStatePath(agentDir: string, sessionDir: string): string {\n\treturn join(agentDir, \"learn\", `${basename(sessionDir)}.json`);\n}\n\n/** Read state, treating any unreadable or unknown-version file as empty. */\nexport function readLearnState(path: string): LearnState {\n\ttry {\n\t\tif (!existsSync(path)) return emptyState();\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as Partial<LearnState>;\n\t\tif (parsed.version !== STATE_VERSION || typeof parsed.surfaced !== \"object\" || parsed.surfaced === null) {\n\t\t\treturn emptyState();\n\t\t}\n\t\treturn { version: STATE_VERSION, lastRun: parsed.lastRun, surfaced: parsed.surfaced };\n\t} catch {\n\t\t// A corrupt bookmark is not worth failing a command over; the cost of\n\t\t// starting fresh is proposing a few things a second time.\n\t\treturn emptyState();\n\t}\n}\n\n/** Persist state, pruning entries nothing has referenced in a long time. */\nexport function writeLearnState(path: string, state: LearnState, now: Date = new Date()): void {\n\tconst cutoff = now.getTime() - STATE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n\tconst surfaced: Record<string, SurfacedItem> = {};\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tconst at = Date.parse(item.surfacedAt);\n\t\tif (Number.isNaN(at) || at >= cutoff) surfaced[key] = item;\n\t}\n\n\ttry {\n\t\tmkdirSync(join(path, \"..\"), { recursive: true });\n\t\twriteFileAtomicSync(path, `${JSON.stringify({ ...state, surfaced }, null, 2)}\\n`);\n\t} catch {\n\t\t// Losing the bookmark degrades /learn to its previous behaviour (proposing\n\t\t// already-decided items again). Never worth failing the command.\n\t}\n}\n\n/** What the caller needs to decide whether an item is worth showing again. */\nexport interface SuppressionInput {\n\tkey: string;\n\t/** Newest occurrence of this item in the current window, ISO. */\n\tlastSeen: string;\n\t/** Whether a context file covers it right now. */\n\tcovered: boolean;\n}\n\nexport interface SuppressionVerdict {\n\t/** Drop it: already surfaced, and nothing new has happened since. */\n\tsuppressed: boolean;\n\t/**\n\t * Surfaced before, still not covered by any context file — you saw it and\n\t * chose not to write it down. Worth telling the model so it proposes again\n\t * tentatively instead of pressing the same case twice.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\n/**\n * Decide whether an item should be shown again.\n *\n * An item is suppressed when it was surfaced before and has not recurred since:\n * `lastSeen <= surfacedAt` means every occurrence backing it was already on\n * screen when you made your call. Say it again and `lastSeen` moves past\n * `surfacedAt`, and it returns — which is exactly when it is worth returning.\n */\nexport function judge(state: LearnState, input: SuppressionInput): SuppressionVerdict {\n\tconst previous = state.surfaced[input.key];\n\tif (!previous) return { suppressed: false, previouslyDeclined: false };\n\n\tconst surfacedAt = Date.parse(previous.surfacedAt);\n\tconst lastSeen = Date.parse(input.lastSeen);\n\tconst recurredSince = !Number.isNaN(surfacedAt) && !Number.isNaN(lastSeen) && lastSeen > surfacedAt;\n\n\treturn {\n\t\tsuppressed: !recurredSince,\n\t\t// Covered now but not when it was surfaced means the proposal was taken up.\n\t\t// Still uncovered means it was passed over.\n\t\tpreviouslyDeclined: !input.covered && !previous.coveredWhenSurfaced,\n\t};\n}\n\nexport interface LearnStats {\n\t/** Proposals on record, across all categories. */\n\ttotal: number;\n\tdirectives: number;\n\tfixes: number;\n\trequests: number;\n\tearliest?: string;\n\tlatest?: string;\n\tlastRun?: string;\n}\n\n/**\n * Count what has been proposed, and when.\n *\n * Deliberately no adoption rate. There used to be one, derived by re-judging\n * coverage and calling the delta \"adopted\", and it was wrong in both\n * directions: a failed judge at either end moved the number, and a proposal\n * correctly rejected as junk was indistinguishable from one ignored. It shipped\n * with two disclaimers explaining how not to misread it, which is the clearest\n * possible sign that it should not have shipped. What the reader actually wants\n * — is the always-loaded surface growing or shrinking — is measurable exactly,\n * from the context files themselves, and is reported instead.\n */\nexport function summarizeLearnState(state: LearnState): LearnStats {\n\tconst stats: LearnStats = { total: 0, directives: 0, fixes: 0, requests: 0 };\n\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tstats.total++;\n\t\tif (!stats.earliest || item.surfacedAt < stats.earliest) stats.earliest = item.surfacedAt;\n\t\tif (!stats.latest || item.surfacedAt > stats.latest) stats.latest = item.surfacedAt;\n\n\t\tif (key.startsWith(\"fix:\")) stats.fixes++;\n\t\telse if (key.startsWith(\"request:\")) stats.requests++;\n\t\telse if (key.startsWith(\"directive:\")) stats.directives++;\n\t}\n\n\tstats.lastRun = state.lastRun;\n\treturn stats;\n}\n\n/** Record everything this run put on screen, so the next run can suppress it. */\nexport function recordSurfaced(\n\tstate: LearnState,\n\titems: Array<{ key: string; lastSeen: string; covered: boolean; text?: string }>,\n\tnow: Date = new Date(),\n): LearnState {\n\tconst surfaced = { ...state.surfaced };\n\tconst at = now.toISOString();\n\tfor (const item of items) {\n\t\tsurfaced[item.key] = {\n\t\t\tsurfacedAt: at,\n\t\t\tlastOccurrence: item.lastSeen,\n\t\t\tcoveredWhenSurfaced: item.covered,\n\t\t\ttext: item.text,\n\t\t};\n\t}\n\treturn { version: STATE_VERSION, lastRun: at, surfaced };\n}\n"]}
|
package/dist/core/learn/state.js
CHANGED
|
@@ -23,8 +23,19 @@
|
|
|
23
23
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
24
24
|
import { basename, join } from "node:path";
|
|
25
25
|
import { writeFileAtomicSync } from "../../utils/atomic-file.js";
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Bump when the on-disk shape changes; an unknown version is discarded, not
|
|
28
|
+
* migrated.
|
|
29
|
+
*
|
|
30
|
+
* v2: keys hold the miner's semantic label (`directive:use-bun-not-npm`) where
|
|
31
|
+
* v1 held normalized directive text (`directive:always use bun not npm`).
|
|
32
|
+
*
|
|
33
|
+
* v3: `workflow:` keys are gone — the category was removed — and labels now come
|
|
34
|
+
* from a global naming pass rather than per-session guesses, so the keys a v2
|
|
35
|
+
* file holds were drawn from a vocabulary that no longer exists. Keeping them
|
|
36
|
+
* would suppress nothing and count as proposals that were never revisited.
|
|
37
|
+
*/
|
|
38
|
+
const STATE_VERSION = 3;
|
|
28
39
|
/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */
|
|
29
40
|
const STATE_RETENTION_DAYS = 180;
|
|
30
41
|
function emptyState() {
|
|
@@ -101,43 +112,31 @@ export function judge(state, input) {
|
|
|
101
112
|
};
|
|
102
113
|
}
|
|
103
114
|
/**
|
|
104
|
-
*
|
|
115
|
+
* Count what has been proposed, and when.
|
|
105
116
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
117
|
+
* Deliberately no adoption rate. There used to be one, derived by re-judging
|
|
118
|
+
* coverage and calling the delta "adopted", and it was wrong in both
|
|
119
|
+
* directions: a failed judge at either end moved the number, and a proposal
|
|
120
|
+
* correctly rejected as junk was indistinguishable from one ignored. It shipped
|
|
121
|
+
* with two disclaimers explaining how not to misread it, which is the clearest
|
|
122
|
+
* possible sign that it should not have shipped. What the reader actually wants
|
|
123
|
+
* — is the always-loaded surface growing or shrinking — is measurable exactly,
|
|
124
|
+
* from the context files themselves, and is reported instead.
|
|
113
125
|
*/
|
|
114
|
-
export function summarizeLearnState(state
|
|
115
|
-
const stats = { total: 0, directives: 0, fixes: 0,
|
|
126
|
+
export function summarizeLearnState(state) {
|
|
127
|
+
const stats = { total: 0, directives: 0, fixes: 0, requests: 0 };
|
|
116
128
|
for (const [key, item] of Object.entries(state.surfaced)) {
|
|
117
129
|
stats.total++;
|
|
118
130
|
if (!stats.earliest || item.surfacedAt < stats.earliest)
|
|
119
131
|
stats.earliest = item.surfacedAt;
|
|
120
132
|
if (!stats.latest || item.surfacedAt > stats.latest)
|
|
121
133
|
stats.latest = item.surfacedAt;
|
|
122
|
-
if (key.startsWith("fix:"))
|
|
134
|
+
if (key.startsWith("fix:"))
|
|
123
135
|
stats.fixes++;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (key.startsWith("
|
|
127
|
-
stats.
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (!key.startsWith("directive:"))
|
|
131
|
-
continue;
|
|
132
|
-
stats.directives++;
|
|
133
|
-
// Already written down when shown, so there was no decision to make.
|
|
134
|
-
if (item.coveredWhenSurfaced)
|
|
135
|
-
continue;
|
|
136
|
-
stats.open++;
|
|
137
|
-
if (isCoveredNow(key.slice("directive:".length)))
|
|
138
|
-
stats.adopted++;
|
|
139
|
-
else
|
|
140
|
-
stats.declined++;
|
|
136
|
+
else if (key.startsWith("request:"))
|
|
137
|
+
stats.requests++;
|
|
138
|
+
else if (key.startsWith("directive:"))
|
|
139
|
+
stats.directives++;
|
|
141
140
|
}
|
|
142
141
|
stats.lastRun = state.lastRun;
|
|
143
142
|
return stats;
|
|
@@ -147,7 +146,12 @@ export function recordSurfaced(state, items, now = new Date()) {
|
|
|
147
146
|
const surfaced = { ...state.surfaced };
|
|
148
147
|
const at = now.toISOString();
|
|
149
148
|
for (const item of items) {
|
|
150
|
-
surfaced[item.key] = {
|
|
149
|
+
surfaced[item.key] = {
|
|
150
|
+
surfacedAt: at,
|
|
151
|
+
lastOccurrence: item.lastSeen,
|
|
152
|
+
coveredWhenSurfaced: item.covered,
|
|
153
|
+
text: item.text,
|
|
154
|
+
};
|
|
151
155
|
}
|
|
152
156
|
return { version: STATE_VERSION, lastRun: at, surfaced };
|
|
153
157
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/core/learn/state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-directory memory of what `/learn` has already shown you.\n *\n * The extractor computes a sliding-window snapshot — \"in your last N sessions\n * you said this M times\" — which is stateless by design and correct to\n * recompute from scratch every run. What it cannot know on its own is whether\n * you have already *seen* a given item and made a call on it. Without that, a\n * rule you accepted last week comes back forever (its occurrences are still\n * inside the window), and worse, it comes back flagged `restated` — accusing a\n * rule that is working of not working. An item you declined comes back too,\n * unchanged, every single run.\n *\n * So one bookmark per directory: what was surfaced, and when. An item is shown\n * again only once something new has happened — you said it again *after* it was\n * last put in front of you. That single rule fixes both cases, and it keeps the\n * `restated` label honest, because the only way to earn it is to repeat yourself\n * after the rule already existed.\n *\n * This is a bookmark, not an index. No cards, no retrieval, no embeddings — the\n * transcripts remain the source of truth and this file can be deleted at any\n * time with no loss beyond re-proposing things once.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { writeFileAtomicSync } from \"../../utils/atomic-file.js\";\n\n/** Bump when the on-disk shape changes; an unknown version is discarded, not migrated. */\nconst STATE_VERSION = 1;\n\n/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */\nconst STATE_RETENTION_DAYS = 180;\n\nexport interface SurfacedItem {\n\t/** When this item was last put in front of the user, ISO. */\n\tsurfacedAt: string;\n\t/** Newest occurrence the run knew about at that point, ISO. */\n\tlastOccurrence: string;\n\t/**\n\t * Whether the item was covered by a context file the last time it was\n\t * surfaced. Comparing that against coverage now is how an adopted proposal is\n\t * told from a declined one, without asking.\n\t */\n\tcoveredWhenSurfaced: boolean;\n}\n\nexport interface LearnState {\n\tversion: number;\n\tlastRun?: string;\n\tsurfaced: Record<string, SurfacedItem>;\n}\n\nfunction emptyState(): LearnState {\n\treturn { version: STATE_VERSION, surfaced: {} };\n}\n\n/**\n * State file for a directory.\n *\n * Keyed off the *name* of the cwd's session directory, which encodes the cwd —\n * so the bookmark is per working directory, matching what the digest describes.\n * Callers pass the cwd-derived path rather than whatever directory the live\n * session happens to be reading: a session with no directory of its own would\n * otherwise land every project on one nameless state file, and projects sharing\n * a custom `sessionDir` would share one bookmark between them.\n */\nexport function getLearnStatePath(agentDir: string, sessionDir: string): string {\n\treturn join(agentDir, \"learn\", `${basename(sessionDir)}.json`);\n}\n\n/** Read state, treating any unreadable or unknown-version file as empty. */\nexport function readLearnState(path: string): LearnState {\n\ttry {\n\t\tif (!existsSync(path)) return emptyState();\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as Partial<LearnState>;\n\t\tif (parsed.version !== STATE_VERSION || typeof parsed.surfaced !== \"object\" || parsed.surfaced === null) {\n\t\t\treturn emptyState();\n\t\t}\n\t\treturn { version: STATE_VERSION, lastRun: parsed.lastRun, surfaced: parsed.surfaced };\n\t} catch {\n\t\t// A corrupt bookmark is not worth failing a command over; the cost of\n\t\t// starting fresh is proposing a few things a second time.\n\t\treturn emptyState();\n\t}\n}\n\n/** Persist state, pruning entries nothing has referenced in a long time. */\nexport function writeLearnState(path: string, state: LearnState, now: Date = new Date()): void {\n\tconst cutoff = now.getTime() - STATE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n\tconst surfaced: Record<string, SurfacedItem> = {};\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tconst at = Date.parse(item.surfacedAt);\n\t\tif (Number.isNaN(at) || at >= cutoff) surfaced[key] = item;\n\t}\n\n\ttry {\n\t\tmkdirSync(join(path, \"..\"), { recursive: true });\n\t\twriteFileAtomicSync(path, `${JSON.stringify({ ...state, surfaced }, null, 2)}\\n`);\n\t} catch {\n\t\t// Losing the bookmark degrades /learn to its previous behaviour (proposing\n\t\t// already-decided items again). Never worth failing the command.\n\t}\n}\n\n/** What the caller needs to decide whether an item is worth showing again. */\nexport interface SuppressionInput {\n\tkey: string;\n\t/** Newest occurrence of this item in the current window, ISO. */\n\tlastSeen: string;\n\t/** Whether a context file covers it right now. */\n\tcovered: boolean;\n}\n\nexport interface SuppressionVerdict {\n\t/** Drop it: already surfaced, and nothing new has happened since. */\n\tsuppressed: boolean;\n\t/**\n\t * Surfaced before, still not covered by any context file — you saw it and\n\t * chose not to write it down. Worth telling the model so it proposes again\n\t * tentatively instead of pressing the same case twice.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\n/**\n * Decide whether an item should be shown again.\n *\n * An item is suppressed when it was surfaced before and has not recurred since:\n * `lastSeen <= surfacedAt` means every occurrence backing it was already on\n * screen when you made your call. Say it again and `lastSeen` moves past\n * `surfacedAt`, and it returns — which is exactly when it is worth returning.\n */\nexport function judge(state: LearnState, input: SuppressionInput): SuppressionVerdict {\n\tconst previous = state.surfaced[input.key];\n\tif (!previous) return { suppressed: false, previouslyDeclined: false };\n\n\tconst surfacedAt = Date.parse(previous.surfacedAt);\n\tconst lastSeen = Date.parse(input.lastSeen);\n\tconst recurredSince = !Number.isNaN(surfacedAt) && !Number.isNaN(lastSeen) && lastSeen > surfacedAt;\n\n\treturn {\n\t\tsuppressed: !recurredSince,\n\t\t// Covered now but not when it was surfaced means the proposal was taken up.\n\t\t// Still uncovered means it was passed over.\n\t\tpreviouslyDeclined: !input.covered && !previous.coveredWhenSurfaced,\n\t};\n}\n\nexport interface LearnStats {\n\t/** Proposals on record, across all categories. */\n\ttotal: number;\n\tdirectives: number;\n\tfixes: number;\n\tworkflows: number;\n\t/**\n\t * Directive proposals that were genuinely open questions — not already\n\t * written down when they were shown. Only these can be adopted or passed\n\t * over, so they are the denominator of the rate.\n\t */\n\topen: number;\n\tadopted: number;\n\tdeclined: number;\n\tearliest?: string;\n\tlatest?: string;\n\tlastRun?: string;\n}\n\n/**\n * Reconstruct what happened to past proposals.\n *\n * Nothing extra is recorded to make this work: each entry already stores whether\n * it was covered when it was shown, and coverage now is recomputable, so the\n * pair is enough to tell an adopted proposal from one that was passed over.\n *\n * Directives only. Fixes and workflows have no coverage signal — a fix may have\n * become a rule, a skill, or a habit, and which one is not recoverable — so\n * counting them here would mean inventing an outcome.\n */\nexport function summarizeLearnState(state: LearnState, isCoveredNow: (normalized: string) => boolean): LearnStats {\n\tconst stats: LearnStats = { total: 0, directives: 0, fixes: 0, workflows: 0, open: 0, adopted: 0, declined: 0 };\n\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tstats.total++;\n\t\tif (!stats.earliest || item.surfacedAt < stats.earliest) stats.earliest = item.surfacedAt;\n\t\tif (!stats.latest || item.surfacedAt > stats.latest) stats.latest = item.surfacedAt;\n\n\t\tif (key.startsWith(\"fix:\")) {\n\t\t\tstats.fixes++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (key.startsWith(\"workflow:\")) {\n\t\t\tstats.workflows++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!key.startsWith(\"directive:\")) continue;\n\n\t\tstats.directives++;\n\t\t// Already written down when shown, so there was no decision to make.\n\t\tif (item.coveredWhenSurfaced) continue;\n\n\t\tstats.open++;\n\t\tif (isCoveredNow(key.slice(\"directive:\".length))) stats.adopted++;\n\t\telse stats.declined++;\n\t}\n\n\tstats.lastRun = state.lastRun;\n\treturn stats;\n}\n\n/** Record everything this run put on screen, so the next run can suppress it. */\nexport function recordSurfaced(\n\tstate: LearnState,\n\titems: Array<{ key: string; lastSeen: string; covered: boolean }>,\n\tnow: Date = new Date(),\n): LearnState {\n\tconst surfaced = { ...state.surfaced };\n\tconst at = now.toISOString();\n\tfor (const item of items) {\n\t\tsurfaced[item.key] = { surfacedAt: at, lastOccurrence: item.lastSeen, coveredWhenSurfaced: item.covered };\n\t}\n\treturn { version: STATE_VERSION, lastRun: at, surfaced };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/core/learn/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;;;;;;;;;GAWG;AACH,MAAM,aAAa,GAAG,CAAC,CAAC;AAExB,0FAA0F;AAC1F,MAAM,oBAAoB,GAAG,GAAG,CAAC;AA4BjC,SAAS,UAAU,GAAe;IACjC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAAA,CAChD;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,UAAkB,EAAU;IAC/E,OAAO,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AAAA,CAC/D;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,IAAY,EAAc;IACxD,IAAI,CAAC;QACJ,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,UAAU,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAwB,CAAC;QAC9E,IAAI,MAAM,CAAC,OAAO,KAAK,aAAa,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YACzG,OAAO,UAAU,EAAE,CAAC;QACrB,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;IACvF,CAAC;IAAC,MAAM,CAAC;QACR,sEAAsE;QACtE,0DAA0D;QAC1D,OAAO,UAAU,EAAE,CAAC;IACrB,CAAC;AAAA,CACD;AAED,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAiB,EAAE,GAAG,GAAS,IAAI,IAAI,EAAE,EAAQ;IAC9F,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,oBAAoB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC1E,MAAM,QAAQ,GAAiC,EAAE,CAAC;IAClD,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1D,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACvC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,MAAM;YAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC5D,CAAC;IAED,IAAI,CAAC;QACJ,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,mBAAmB,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACnF,CAAC;IAAC,MAAM,CAAC;QACR,2EAA2E;QAC3E,iEAAiE;IAClE,CAAC;AAAA,CACD;AAsBD;;;;;;;GAOG;AACH,MAAM,UAAU,KAAK,CAAC,KAAiB,EAAE,KAAuB,EAAsB;IACrF,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC;IAEvE,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,UAAU,CAAC;IAEpG,OAAO;QACN,UAAU,EAAE,CAAC,aAAa;QAC1B,4EAA4E;QAC5E,4CAA4C;QAC5C,kBAAkB,EAAE,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAC,mBAAmB;KACnE,CAAC;AAAA,CACF;AAaD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAiB,EAAc;IAClE,MAAM,KAAK,GAAe,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IAE7E,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1D,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,QAAQ;YAAE,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;QAC1F,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,MAAM;YAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;QAEpF,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,KAAK,EAAE,CAAC;aACrC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;aACjD,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC;YAAE,KAAK,CAAC,UAAU,EAAE,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,OAAO,KAAK,CAAC;AAAA,CACb;AAED,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAC7B,KAAiB,EACjB,KAAgF,EAChF,GAAG,GAAS,IAAI,IAAI,EAAE,EACT;IACb,MAAM,QAAQ,GAAG,EAAE,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;IACvC,MAAM,EAAE,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG;YACpB,UAAU,EAAE,EAAE;YACd,cAAc,EAAE,IAAI,CAAC,QAAQ;YAC7B,mBAAmB,EAAE,IAAI,CAAC,OAAO;YACjC,IAAI,EAAE,IAAI,CAAC,IAAI;SACf,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC;AAAA,CACzD","sourcesContent":["/**\n * Per-directory memory of what `/learn` has already shown you.\n *\n * The extractor computes a sliding-window snapshot — \"in your last N sessions\n * you said this M times\" — which is stateless by design and correct to\n * recompute from scratch every run. What it cannot know on its own is whether\n * you have already *seen* a given item and made a call on it. Without that, a\n * rule you accepted last week comes back forever (its occurrences are still\n * inside the window), and worse, it comes back flagged `restated` — accusing a\n * rule that is working of not working. An item you declined comes back too,\n * unchanged, every single run.\n *\n * So one bookmark per directory: what was surfaced, and when. An item is shown\n * again only once something new has happened — you said it again *after* it was\n * last put in front of you. That single rule fixes both cases, and it keeps the\n * `restated` label honest, because the only way to earn it is to repeat yourself\n * after the rule already existed.\n *\n * This is a bookmark, not an index. No cards, no retrieval, no embeddings — the\n * transcripts remain the source of truth and this file can be deleted at any\n * time with no loss beyond re-proposing things once.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { writeFileAtomicSync } from \"../../utils/atomic-file.js\";\n\n/**\n * Bump when the on-disk shape changes; an unknown version is discarded, not\n * migrated.\n *\n * v2: keys hold the miner's semantic label (`directive:use-bun-not-npm`) where\n * v1 held normalized directive text (`directive:always use bun not npm`).\n *\n * v3: `workflow:` keys are gone — the category was removed — and labels now come\n * from a global naming pass rather than per-session guesses, so the keys a v2\n * file holds were drawn from a vocabulary that no longer exists. Keeping them\n * would suppress nothing and count as proposals that were never revisited.\n */\nconst STATE_VERSION = 3;\n\n/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */\nconst STATE_RETENTION_DAYS = 180;\n\nexport interface SurfacedItem {\n\t/** When this item was last put in front of the user, ISO. */\n\tsurfacedAt: string;\n\t/** Newest occurrence the run knew about at that point, ISO. */\n\tlastOccurrence: string;\n\t/**\n\t * Whether the item was covered by a context file the last time it was\n\t * surfaced. Comparing that against coverage now is how an adopted proposal is\n\t * told from a declined one, without asking.\n\t */\n\tcoveredWhenSurfaced: boolean;\n\t/**\n\t * Representative wording, kept so `/learn stats` can ask about coverage using\n\t * what was actually said. The key alone is a slug, and judging \"is this\n\t * written down?\" from a slug is a much weaker question than judging it from\n\t * the sentence the slug stands for.\n\t */\n\ttext?: string;\n}\n\nexport interface LearnState {\n\tversion: number;\n\tlastRun?: string;\n\tsurfaced: Record<string, SurfacedItem>;\n}\n\nfunction emptyState(): LearnState {\n\treturn { version: STATE_VERSION, surfaced: {} };\n}\n\n/**\n * State file for a directory.\n *\n * Keyed off the *name* of the cwd's session directory, which encodes the cwd —\n * so the bookmark is per working directory, matching what the digest describes.\n * Callers pass the cwd-derived path rather than whatever directory the live\n * session happens to be reading: a session with no directory of its own would\n * otherwise land every project on one nameless state file, and projects sharing\n * a custom `sessionDir` would share one bookmark between them.\n */\nexport function getLearnStatePath(agentDir: string, sessionDir: string): string {\n\treturn join(agentDir, \"learn\", `${basename(sessionDir)}.json`);\n}\n\n/** Read state, treating any unreadable or unknown-version file as empty. */\nexport function readLearnState(path: string): LearnState {\n\ttry {\n\t\tif (!existsSync(path)) return emptyState();\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as Partial<LearnState>;\n\t\tif (parsed.version !== STATE_VERSION || typeof parsed.surfaced !== \"object\" || parsed.surfaced === null) {\n\t\t\treturn emptyState();\n\t\t}\n\t\treturn { version: STATE_VERSION, lastRun: parsed.lastRun, surfaced: parsed.surfaced };\n\t} catch {\n\t\t// A corrupt bookmark is not worth failing a command over; the cost of\n\t\t// starting fresh is proposing a few things a second time.\n\t\treturn emptyState();\n\t}\n}\n\n/** Persist state, pruning entries nothing has referenced in a long time. */\nexport function writeLearnState(path: string, state: LearnState, now: Date = new Date()): void {\n\tconst cutoff = now.getTime() - STATE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n\tconst surfaced: Record<string, SurfacedItem> = {};\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tconst at = Date.parse(item.surfacedAt);\n\t\tif (Number.isNaN(at) || at >= cutoff) surfaced[key] = item;\n\t}\n\n\ttry {\n\t\tmkdirSync(join(path, \"..\"), { recursive: true });\n\t\twriteFileAtomicSync(path, `${JSON.stringify({ ...state, surfaced }, null, 2)}\\n`);\n\t} catch {\n\t\t// Losing the bookmark degrades /learn to its previous behaviour (proposing\n\t\t// already-decided items again). Never worth failing the command.\n\t}\n}\n\n/** What the caller needs to decide whether an item is worth showing again. */\nexport interface SuppressionInput {\n\tkey: string;\n\t/** Newest occurrence of this item in the current window, ISO. */\n\tlastSeen: string;\n\t/** Whether a context file covers it right now. */\n\tcovered: boolean;\n}\n\nexport interface SuppressionVerdict {\n\t/** Drop it: already surfaced, and nothing new has happened since. */\n\tsuppressed: boolean;\n\t/**\n\t * Surfaced before, still not covered by any context file — you saw it and\n\t * chose not to write it down. Worth telling the model so it proposes again\n\t * tentatively instead of pressing the same case twice.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\n/**\n * Decide whether an item should be shown again.\n *\n * An item is suppressed when it was surfaced before and has not recurred since:\n * `lastSeen <= surfacedAt` means every occurrence backing it was already on\n * screen when you made your call. Say it again and `lastSeen` moves past\n * `surfacedAt`, and it returns — which is exactly when it is worth returning.\n */\nexport function judge(state: LearnState, input: SuppressionInput): SuppressionVerdict {\n\tconst previous = state.surfaced[input.key];\n\tif (!previous) return { suppressed: false, previouslyDeclined: false };\n\n\tconst surfacedAt = Date.parse(previous.surfacedAt);\n\tconst lastSeen = Date.parse(input.lastSeen);\n\tconst recurredSince = !Number.isNaN(surfacedAt) && !Number.isNaN(lastSeen) && lastSeen > surfacedAt;\n\n\treturn {\n\t\tsuppressed: !recurredSince,\n\t\t// Covered now but not when it was surfaced means the proposal was taken up.\n\t\t// Still uncovered means it was passed over.\n\t\tpreviouslyDeclined: !input.covered && !previous.coveredWhenSurfaced,\n\t};\n}\n\nexport interface LearnStats {\n\t/** Proposals on record, across all categories. */\n\ttotal: number;\n\tdirectives: number;\n\tfixes: number;\n\trequests: number;\n\tearliest?: string;\n\tlatest?: string;\n\tlastRun?: string;\n}\n\n/**\n * Count what has been proposed, and when.\n *\n * Deliberately no adoption rate. There used to be one, derived by re-judging\n * coverage and calling the delta \"adopted\", and it was wrong in both\n * directions: a failed judge at either end moved the number, and a proposal\n * correctly rejected as junk was indistinguishable from one ignored. It shipped\n * with two disclaimers explaining how not to misread it, which is the clearest\n * possible sign that it should not have shipped. What the reader actually wants\n * — is the always-loaded surface growing or shrinking — is measurable exactly,\n * from the context files themselves, and is reported instead.\n */\nexport function summarizeLearnState(state: LearnState): LearnStats {\n\tconst stats: LearnStats = { total: 0, directives: 0, fixes: 0, requests: 0 };\n\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tstats.total++;\n\t\tif (!stats.earliest || item.surfacedAt < stats.earliest) stats.earliest = item.surfacedAt;\n\t\tif (!stats.latest || item.surfacedAt > stats.latest) stats.latest = item.surfacedAt;\n\n\t\tif (key.startsWith(\"fix:\")) stats.fixes++;\n\t\telse if (key.startsWith(\"request:\")) stats.requests++;\n\t\telse if (key.startsWith(\"directive:\")) stats.directives++;\n\t}\n\n\tstats.lastRun = state.lastRun;\n\treturn stats;\n}\n\n/** Record everything this run put on screen, so the next run can suppress it. */\nexport function recordSurfaced(\n\tstate: LearnState,\n\titems: Array<{ key: string; lastSeen: string; covered: boolean; text?: string }>,\n\tnow: Date = new Date(),\n): LearnState {\n\tconst surfaced = { ...state.surfaced };\n\tconst at = now.toISOString();\n\tfor (const item of items) {\n\t\tsurfaced[item.key] = {\n\t\t\tsurfacedAt: at,\n\t\t\tlastOccurrence: item.lastSeen,\n\t\t\tcoveredWhenSurfaced: item.covered,\n\t\t\ttext: item.text,\n\t\t};\n\t}\n\treturn { version: STATE_VERSION, lastRun: at, surfaced };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuFT,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tpluginInstallScope: \"user\",\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlearnMaxSessions: 20,\n\tlearnMaxAgeDays: 30,\n\tlearnMinRepeats: 2,\n\
|
|
1
|
+
{"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuFT,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tpluginInstallScope: \"user\",\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlearnMaxSessions: 20,\n\tlearnMaxAgeDays: 30,\n\tlearnMinRepeats: 2,\n\tlearnMinRequestRepeats: 3,\n\tlearnMaxProposals: 8,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;QACvB,eAAe,EAAE,IAAI;KACrB;IACD,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,QAAQ,EAAE,GAAG;KACb;IACD,SAAS,EAAE;QACV,OAAO,EAAE,IAAI;KACb;IACD,KAAK,EAAE;QACN,SAAS,EAAE,GAAG;KACd;IACD,QAAQ,EAAE;QACT,WAAW,EAAE,EAAE;KACf;IACD,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,UAAU;IAC7B,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,KAAK;IACxB,kBAAkB,EAAE,MAAM;IAC1B,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,oBAAoB,EAAE,IAAI;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,gBAAgB,EAAE,EAAE;IACpB,eAAe,EAAE,EAAE;IACnB,eAAe,EAAE,CAAC;IAClB,
|
|
1
|
+
{"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;QACvB,eAAe,EAAE,IAAI;KACrB;IACD,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,QAAQ,EAAE,GAAG;KACb;IACD,SAAS,EAAE;QACV,OAAO,EAAE,IAAI;KACb;IACD,KAAK,EAAE;QACN,SAAS,EAAE,GAAG;KACd;IACD,QAAQ,EAAE;QACT,WAAW,EAAE,EAAE;KACf;IACD,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,UAAU;IAC7B,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,KAAK;IACxB,kBAAkB,EAAE,MAAM;IAC1B,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,oBAAoB,EAAE,IAAI;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,gBAAgB,EAAE,EAAE;IACpB,eAAe,EAAE,EAAE;IACnB,eAAe,EAAE,CAAC;IAClB,sBAAsB,EAAE,CAAC;IACzB,iBAAiB,EAAE,CAAC;IACpB,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;QAC3B,mBAAmB,EAAE,KAAK;KAC1B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tpluginInstallScope: \"user\",\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlearnMaxSessions: 20,\n\tlearnMaxAgeDays: 30,\n\tlearnMinRepeats: 2,\n\tlearnMinRequestRepeats: 3,\n\tlearnMaxProposals: 8,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SettingsError, type SettingsScope, type SettingsStorage } from "./settings-storage.js";
|
|
2
2
|
import type { BranchSummarySettings, CompactionSettings, PackageSource, Settings, ThinkingBudgetsSettings, TransportSetting, WarningSettings } from "./settings-types.js";
|
|
3
3
|
/** The `/learn` thresholds, which are flat top-level numeric settings. */
|
|
4
|
-
export type LearnSettingKey = "learnMaxSessions" | "learnMaxAgeDays" | "learnMinRepeats" | "
|
|
4
|
+
export type LearnSettingKey = "learnMaxSessions" | "learnMaxAgeDays" | "learnMinRepeats" | "learnMinRequestRepeats" | "learnMaxProposals";
|
|
5
5
|
export type { SettingsError, SettingsScope, SettingsStorage };
|
|
6
6
|
export type { BranchSummarySettings, CompactionSettings, ImageSettings, PackageSource, RetrySettings, Settings, ThinkingBudgetsSettings, TransportSetting, WarningSettings, } from "./settings-types.js";
|
|
7
7
|
export declare class SettingsManager {
|
|
@@ -81,9 +81,11 @@ export declare class SettingsManager {
|
|
|
81
81
|
maxSessions: number;
|
|
82
82
|
maxAgeDays: number;
|
|
83
83
|
minRepeats: number;
|
|
84
|
-
|
|
84
|
+
minRequestRepeats: number;
|
|
85
85
|
maxProposals: number;
|
|
86
86
|
};
|
|
87
|
+
/** Configured `fast`/`standard`/`capable` tiers, for callers resolving a category. */
|
|
88
|
+
getModelCategories(): Settings["modelCategories"];
|
|
87
89
|
/**
|
|
88
90
|
* Persist one `/learn` threshold to the user scope.
|
|
89
91
|
*
|