dsh-continual-evolve 0.10.6 → 0.12.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.
Files changed (126) hide show
  1. package/README.md +35 -122
  2. package/README.zh.md +26 -97
  3. package/cordis.patch.yml +2 -2
  4. package/lib/index.d.ts +44 -241
  5. package/lib/index.js +30 -253
  6. package/lib/memory-guide.d.ts +13 -14
  7. package/lib/memory-guide.js +32 -21
  8. package/lib/memory-section.d.ts +77 -0
  9. package/lib/memory-section.js +189 -0
  10. package/package.json +2 -17
  11. package/lib/apply.d.ts +0 -22
  12. package/lib/apply.js +0 -157
  13. package/lib/approval.d.ts +0 -72
  14. package/lib/approval.js +0 -127
  15. package/lib/auto.d.ts +0 -236
  16. package/lib/auto.js +0 -773
  17. package/lib/autocase.d.ts +0 -38
  18. package/lib/autocase.js +0 -76
  19. package/lib/benchmark-command.d.ts +0 -9
  20. package/lib/benchmark-command.js +0 -357
  21. package/lib/benchmark.d.ts +0 -151
  22. package/lib/benchmark.js +0 -250
  23. package/lib/command.d.ts +0 -49
  24. package/lib/command.js +0 -568
  25. package/lib/consolidate.d.ts +0 -90
  26. package/lib/consolidate.js +0 -180
  27. package/lib/copy.d.ts +0 -74
  28. package/lib/copy.js +0 -160
  29. package/lib/declines.d.ts +0 -101
  30. package/lib/declines.js +0 -175
  31. package/lib/evaluate.d.ts +0 -74
  32. package/lib/evaluate.js +0 -271
  33. package/lib/evolve-event.d.ts +0 -40
  34. package/lib/evolve-event.js +0 -59
  35. package/lib/failures.d.ts +0 -42
  36. package/lib/failures.js +0 -171
  37. package/lib/fate.d.ts +0 -130
  38. package/lib/fate.js +0 -350
  39. package/lib/goal-command.d.ts +0 -7
  40. package/lib/goal-command.js +0 -37
  41. package/lib/goal.d.ts +0 -72
  42. package/lib/goal.js +0 -72
  43. package/lib/inject.d.ts +0 -196
  44. package/lib/inject.js +0 -412
  45. package/lib/llm-text.d.ts +0 -104
  46. package/lib/llm-text.js +0 -140
  47. package/lib/logfile.d.ts +0 -71
  48. package/lib/logfile.js +0 -159
  49. package/lib/memory-agent.d.ts +0 -129
  50. package/lib/memory-agent.js +0 -568
  51. package/lib/memory-benchmark.d.ts +0 -76
  52. package/lib/memory-benchmark.js +0 -170
  53. package/lib/memory-index.d.ts +0 -114
  54. package/lib/memory-index.js +0 -197
  55. package/lib/message-source.d.ts +0 -23
  56. package/lib/message-source.js +0 -12
  57. package/lib/mount-command.d.ts +0 -10
  58. package/lib/mount-command.js +0 -48
  59. package/lib/mount.d.ts +0 -42
  60. package/lib/mount.js +0 -212
  61. package/lib/notify.d.ts +0 -59
  62. package/lib/notify.js +0 -97
  63. package/lib/plan.d.ts +0 -16
  64. package/lib/plan.js +0 -126
  65. package/lib/planner.d.ts +0 -61
  66. package/lib/planner.js +0 -144
  67. package/lib/pool.d.ts +0 -7
  68. package/lib/pool.js +0 -25
  69. package/lib/prefix-cache.d.ts +0 -78
  70. package/lib/prefix-cache.js +0 -168
  71. package/lib/project.d.ts +0 -28
  72. package/lib/project.js +0 -66
  73. package/lib/projection.d.ts +0 -22
  74. package/lib/projection.js +0 -112
  75. package/lib/promotion.d.ts +0 -94
  76. package/lib/promotion.js +0 -171
  77. package/lib/recall.d.ts +0 -85
  78. package/lib/recall.js +0 -142
  79. package/lib/record-language.d.ts +0 -85
  80. package/lib/record-language.js +0 -152
  81. package/lib/render.d.ts +0 -13
  82. package/lib/render.js +0 -80
  83. package/lib/review-scheduler.d.ts +0 -38
  84. package/lib/review-scheduler.js +0 -191
  85. package/lib/review.d.ts +0 -60
  86. package/lib/review.js +0 -145
  87. package/lib/rollback.d.ts +0 -9
  88. package/lib/rollback.js +0 -68
  89. package/lib/rubric.d.ts +0 -29
  90. package/lib/rubric.js +0 -119
  91. package/lib/runtime.d.ts +0 -24
  92. package/lib/runtime.js +0 -87
  93. package/lib/score.d.ts +0 -64
  94. package/lib/score.js +0 -191
  95. package/lib/search.d.ts +0 -83
  96. package/lib/search.js +0 -136
  97. package/lib/service.d.ts +0 -49
  98. package/lib/service.js +0 -180
  99. package/lib/skill-render.d.ts +0 -23
  100. package/lib/skill-render.js +0 -69
  101. package/lib/skill.d.ts +0 -15
  102. package/lib/skill.js +0 -80
  103. package/lib/skillquality.d.ts +0 -85
  104. package/lib/skillquality.js +0 -320
  105. package/lib/source.d.ts +0 -30
  106. package/lib/source.js +0 -43
  107. package/lib/state.d.ts +0 -26
  108. package/lib/state.js +0 -151
  109. package/lib/store.d.ts +0 -62
  110. package/lib/store.js +0 -164
  111. package/lib/token-usage.d.ts +0 -68
  112. package/lib/token-usage.js +0 -211
  113. package/lib/tool.d.ts +0 -34
  114. package/lib/tool.js +0 -320
  115. package/lib/turn-snapshot.d.ts +0 -72
  116. package/lib/turn-snapshot.js +0 -238
  117. package/lib/types.d.ts +0 -208
  118. package/lib/types.js +0 -127
  119. package/lib/usage.d.ts +0 -36
  120. package/lib/usage.js +0 -98
  121. package/lib/validate.d.ts +0 -28
  122. package/lib/validate.js +0 -181
  123. package/lib/wrapup-command.d.ts +0 -10
  124. package/lib/wrapup-command.js +0 -243
  125. package/lib/wrapup.d.ts +0 -260
  126. package/lib/wrapup.js +0 -539
package/lib/wrapup.js DELETED
@@ -1,539 +0,0 @@
1
- import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, VALENCE_NEGATIVE_KEY, isArchived } from "./types.js";
2
- import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
3
- import { extractJsonObject } from "./plan.js";
4
- import { compactText } from "./render.js";
5
- import { asLlmSessionId, streamText } from "./llm-text.js";
6
- import { getUsageCount, loadUsage } from "./usage.js";
7
- import { createTokenUsageObserver } from "./token-usage.js";
8
- import { recordLanguageInstruction, resolveRecordLanguage } from "./record-language.js";
9
- import { recencyScore } from "./inject.js";
10
- import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason, secretLeakReason } from "./promotion.js";
11
- export function candidateKey(kind, id) {
12
- return `${kind}:${id}`;
13
- }
14
- /** Lowercase, punctuation-stripped title used for cheap coverage matching. */
15
- function normalizeKey(value) {
16
- return value.toLowerCase().replace(/[^a-z0-9\u3400-\u9fff]+/g, "").trim();
17
- }
18
- /**
19
- * Deterministic global-coverage check (STRONG signal): the global store
20
- * already covers a topic when it holds a title that normalizes equal to, or
21
- * (beyond a length floor) contains, the candidate's normalized title.
22
- * Archived global entries count too — the topic was already judged
23
- * cross-session; a local duplicate would only re-sediment it.
24
- *
25
- * The bare same-id case is deliberately NOT coverage: ids are slugs derived
26
- * from titles, so a real collision is usually caught by the title check
27
- * below. A same-id entry with a wildly different title is a weak signal — the
28
- * caller routes it through {@link globalHintsFor} for the assessor to judge
29
- * against the actual global title (real case: local `memory` "用户产品愿景与
30
- * 收入需求(本会话)" vs global `memory` "用户画像(持续更新)").
31
- */
32
- export function globalCoverageDetected(globalState, kind, entry) {
33
- const records = globalState.entries[kind];
34
- const title = normalizeKey(entry.title);
35
- if (title.length === 0)
36
- return false;
37
- for (const other of Object.values(records)) {
38
- const otherTitle = normalizeKey(other.title);
39
- if (otherTitle.length === 0)
40
- continue;
41
- if (otherTitle === title)
42
- return true;
43
- if (title.length >= 4 && otherTitle.length >= 4 && (title.includes(otherTitle) || otherTitle.includes(title))) {
44
- return true;
45
- }
46
- }
47
- return false;
48
- }
49
- /**
50
- * The actual global entries that touch the same topic as a local candidate:
51
- * same id (regardless of title — the weak collision signal that is NOT
52
- * coverage on its own), equal normalized title, or title overlap. The raw ids
53
- * and titles let the assessor judge enrichment against real global content
54
- * (does the global copy already hold what the local one adds?) rather than a
55
- * bare boolean. Bounded: a handful of best matches, never the whole store.
56
- */
57
- export function globalHintsFor(globalState, kind, entry) {
58
- const records = globalState.entries[kind];
59
- const title = normalizeKey(entry.title);
60
- const hints = [];
61
- for (const other of Object.values(records)) {
62
- const otherTitle = normalizeKey(other.title);
63
- if (otherTitle.length === 0 && other.id !== entry.id)
64
- continue;
65
- const matches = other.id === entry.id ||
66
- (otherTitle.length > 0 && (otherTitle === title || (title.length >= 4 && otherTitle.length >= 4 && (title.includes(otherTitle) || otherTitle.includes(title)))));
67
- if (matches) {
68
- hints.push({ id: other.id, title: other.title });
69
- }
70
- }
71
- return hints;
72
- }
73
- /**
74
- * The auditable local candidates of a session: every non-archived local
75
- * entry that has not already been promoted (a promoted entry's lifecycle is
76
- * finished — the global copy is the live one). Each carries its
77
- * `coveredGlobally` flag so the assessor never wastes a promote on a topic
78
- * the global store already owns.
79
- */
80
- /** Staleness threshold: entries with recency below this AND zero usage are stale. */
81
- const STALE_RECENCY_THRESHOLD = 0.1;
82
- export function listLocalCandidates(state, globalState, baseDir) {
83
- const usage = baseDir ? loadUsage(baseDir) : undefined;
84
- const now = Date.now();
85
- const candidates = [];
86
- for (const kind of Object.keys(state.entries)) {
87
- for (const entry of Object.values(state.entries[kind])) {
88
- if (entry.scope !== "local")
89
- continue;
90
- if (isArchived(entry))
91
- continue;
92
- if (typeof entry.metadata[PROMOTED_TO_KEY] === "string")
93
- continue;
94
- const injectionCount = usage ? getUsageCount(usage, kind, entry.id) : 0;
95
- const stale = injectionCount === 0 && recencyScore(entry, now) < STALE_RECENCY_THRESHOLD;
96
- const rawNegative = entry.metadata[VALENCE_NEGATIVE_KEY];
97
- const negativeCount = typeof rawNegative === "number" && Number.isFinite(rawNegative) && rawNegative > 0 ? rawNegative : 0;
98
- candidates.push({
99
- kind,
100
- id: entry.id,
101
- title: entry.title,
102
- content: entry.content,
103
- path: entry.path,
104
- version: entry.version,
105
- metadata: entry.metadata,
106
- coveredGlobally: globalCoverageDetected(globalState, kind, entry),
107
- globalHints: globalHintsFor(globalState, kind, entry),
108
- injectionCount,
109
- stale,
110
- negativeCount,
111
- });
112
- }
113
- }
114
- return candidates;
115
- }
116
- /**
117
- * Parse and validate the model's assessment JSON. Defense is mechanical:
118
- * keys outside the candidate list are dropped, verdicts outside the enum
119
- * collapse to "keep", and candidates the model omitted default to "keep" —
120
- * a malformed reply can never change an entry's fate by itself.
121
- *
122
- * Split promotion (verdict "archive" with a `promote` sub-object): the
123
- * sub-object is accepted ONLY on archive verdicts and ONLY when both cleaned
124
- * title and content are non-empty strings — a dropped/malformed sub-object
125
- * silently degrades to a plain archive (the entry is never half-promoted).
126
- */
127
- export function parseWrapupAssessment(text, candidates) {
128
- const value = extractJsonObject(text);
129
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
130
- throw new Error("wrap-up assessment JSON must be an object");
131
- }
132
- const record = value;
133
- const allowed = new Set(candidates.map((candidate) => candidateKey(candidate.kind, candidate.id)));
134
- const items = [];
135
- if (Array.isArray(record["items"])) {
136
- for (const raw of record["items"]) {
137
- if (typeof raw !== "object" || raw === null)
138
- continue;
139
- const item = raw;
140
- const key = typeof item["key"] === "string" ? item["key"] : "";
141
- if (!allowed.has(key))
142
- continue;
143
- const verdict = item["verdict"] === "promote" || item["verdict"] === "archive" ? item["verdict"] : "keep";
144
- const built = { key, verdict, reason: typeof item["reason"] === "string" ? item["reason"] : "" };
145
- if (item["contradicted"] === true) {
146
- built.contradicted = true;
147
- }
148
- if (verdict === "archive" && typeof item["promote"] === "object" && item["promote"] !== null) {
149
- const sub = item["promote"];
150
- const subTitle = typeof sub["title"] === "string" ? sub["title"].trim() : "";
151
- const subContent = typeof sub["content"] === "string" ? sub["content"].trim() : "";
152
- if (subTitle.length > 0 && subContent.length > 0) {
153
- built.promote = { title: subTitle, content: subContent };
154
- }
155
- }
156
- items.push(built);
157
- }
158
- }
159
- for (const candidate of candidates) {
160
- const key = candidateKey(candidate.kind, candidate.id);
161
- if (!items.some((item) => item.key === key)) {
162
- items.push({ key, verdict: "keep", reason: "not mentioned by the assessor" });
163
- }
164
- }
165
- return { items, rationale: typeof record["rationale"] === "string" ? record["rationale"] : "" };
166
- }
167
- /**
168
- * Apply-time deterministic guard: re-check every promote verdict against the
169
- * global store right before it lands. The LLM classification may be stale
170
- * (a gate ran while assessing) or wrong; this ensures a promote never writes
171
- * a duplicate, project-scoped, or too-thin global entry. Pure and unit-tested.
172
- *
173
- * Guards (2026-08-22 promotion policy):
174
- * - audited candidate list + title coverage (pre-existing),
175
- * - project-scoped content markers (absolute paths / session ids) — the
176
- * global store is shared across projects and must stay portable,
177
- * - thin content below the policy floor (framing outweighs the fact),
178
- * - near-duplicate of an existing global entry by content overlap.
179
- */
180
- export function filterPromotable(items, globalState, candidates, policy = DEFAULT_PROMOTION_POLICY) {
181
- const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
182
- const promotable = [];
183
- const skipped = [];
184
- for (const item of items) {
185
- if (item.verdict !== "promote")
186
- continue;
187
- const candidate = byKey.get(item.key);
188
- if (!candidate) {
189
- skipped.push({ key: item.key, reason: "not in the audited candidate list" });
190
- continue;
191
- }
192
- if (candidate.coveredGlobally || globalCoverageDetected(globalState, candidate.kind, candidate)) {
193
- skipped.push({ key: item.key, reason: "already covered globally" });
194
- continue;
195
- }
196
- if (candidate.kind === "memory" && !isMemoryType(candidate.metadata[MEMORY_TYPE_KEY])) {
197
- skipped.push({
198
- key: item.key,
199
- reason: `memory has no recall type (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update the local entry to classify it first, then promote`,
200
- });
201
- continue;
202
- }
203
- const scoped = projectScopedReason(`${candidate.title}\n${candidate.content}`, policy);
204
- if (scoped) {
205
- skipped.push({ key: item.key, reason: scoped });
206
- continue;
207
- }
208
- // Metadata screens too: wholePromoteProposals copies candidate.metadata
209
- // verbatim into the global entry, so a credential planted in local
210
- // metadata must not ride the promotion path into the shared store
211
- // (review audit 2026-08-28 B2).
212
- const secret = secretLeakReason([candidate.title, candidate.content, JSON.stringify(candidate.metadata ?? {})].join("\n"));
213
- if (secret) {
214
- skipped.push({ key: item.key, reason: secret });
215
- continue;
216
- }
217
- if (candidate.content.length < policy.minPromoteChars) {
218
- skipped.push({
219
- key: item.key,
220
- reason: `too thin to promote (${candidate.content.length} < ${policy.minPromoteChars} chars) — keep local or merge`,
221
- });
222
- continue;
223
- }
224
- const similar = mostSimilarGlobalEntry(globalState, candidate.kind, candidate.title, candidate.content, policy);
225
- if (similar) {
226
- skipped.push({
227
- key: item.key,
228
- reason: `near-duplicate of global ${candidate.kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)}) — update that entry instead`,
229
- });
230
- continue;
231
- }
232
- promotable.push(item);
233
- }
234
- return { promotable, skipped };
235
- }
236
- /**
237
- * The symmetric archive guard. `filterPromotable` is one-directional: it
238
- * stops the model from WRITING duplicate global entries, but nothing stopped
239
- * an unfounded ARCHIVE from hiding content that was actually only local.
240
- * Guard criteria: an archive needs user confirmation when it is NOT covered
241
- * globally AND the entry carries a real distillation source (sourceSeqs /
242
- * sourceSession — i.e. it was distilled from actual user messages, so it
243
- * may hold reusable value). Operational/empty entries archive silently as
244
- * before. Split archives (archive + promote sub-object) skip this check:
245
- * their promotion already crosses a human approval gate, so the archive is
246
- * the completion of an approved action, not a silent burial.
247
- */
248
- export function needsArchiveReview(item, candidate) {
249
- if (item.verdict !== "archive")
250
- return false;
251
- if (item.promote)
252
- return false;
253
- if (candidate.coveredGlobally)
254
- return false;
255
- const seqs = candidate.metadata[SOURCE_SEQS_KEY];
256
- const session = candidate.metadata[SOURCE_SESSION_KEY];
257
- return (Array.isArray(seqs) && seqs.length > 0) || (typeof session === "string" && session.length > 0);
258
- }
259
- /** Partition archive items into silent vs review-required (see needsArchiveReview). */
260
- export function splitArchiveGuards(items, candidates) {
261
- const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
262
- const silent = [];
263
- const review = [];
264
- for (const item of items) {
265
- if (item.verdict !== "archive")
266
- continue;
267
- const candidate = byKey.get(item.key);
268
- if (candidate && needsArchiveReview(item, candidate)) {
269
- review.push(item);
270
- }
271
- else {
272
- silent.push(item);
273
- }
274
- }
275
- return { silent, review };
276
- }
277
- /**
278
- * Apply-time guard for a split promotion (archive + promote sub-object):
279
- * the cleaned payload must pass the same promotion policy as a whole
280
- * promote — no global coverage duplicate, no project-scoped content, not
281
- * too thin, no near-duplicate global entry. A blocked split is dropped (the
282
- * entry still archives plain) rather than half-promoting a redundancy.
283
- */
284
- export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY, candidateMetadata) {
285
- if (!item.promote)
286
- return "no split payload";
287
- if (kind === "memory" && !isMemoryType(candidateMetadata?.[MEMORY_TYPE_KEY])) {
288
- return `split promotion needs the source entry classified (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update it first`;
289
- }
290
- if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
291
- return "split promotion duplicates a globally covered topic";
292
- }
293
- const scoped = projectScopedReason(`${item.promote.title}\n${item.promote.content}`, policy);
294
- if (scoped)
295
- return `split promotion is ${scoped}`;
296
- const secret = secretLeakReason([item.promote.title, item.promote.content].join("\n"));
297
- if (secret)
298
- return `split promotion blocked: ${secret}`;
299
- if (item.promote.content.length < policy.minPromoteChars) {
300
- return `split promotion too thin (${item.promote.content.length} < ${policy.minPromoteChars} chars)`;
301
- }
302
- const similar = mostSimilarGlobalEntry(globalState, kind, item.promote.title, item.promote.content, policy);
303
- if (similar) {
304
- return `split promotion near-duplicates global ${kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)})`;
305
- }
306
- return undefined;
307
- }
308
- /**
309
- * Shared proposal builders for a WHOLE promotion — used by both the
310
- * `/evolve wrapup` command and the gate's local-fate dimension so the two
311
- * paths apply IDENTICAL edits (global create + local retirement stamp).
312
- *
313
- * The local stamp is a factory: the `promotedTo` id is only known after the
314
- * global create lands (validation may slugify the id), so the caller applies
315
- * the global proposal first and stamps the local copy with the created id.
316
- */
317
- export function wholePromoteProposals(item, candidate, sessionId) {
318
- const now = new Date().toISOString();
319
- return {
320
- global: {
321
- summary: `wrapup: promote local ${item.key} to the global store`,
322
- rationale: item.reason,
323
- expectedOutcome: `The entry is now visible to every session via the global store (sourcedFromLocal=${sessionId}:${candidate.id}).`,
324
- edits: [
325
- {
326
- action: "create",
327
- kind: candidate.kind,
328
- id: candidate.id,
329
- title: candidate.title,
330
- content: candidate.content,
331
- path: candidate.path,
332
- metadata: {
333
- ...candidate.metadata,
334
- [SOURCED_FROM_KEY]: `${sessionId}:${candidate.id}`,
335
- [PROMOTED_AT_KEY]: now,
336
- },
337
- },
338
- ],
339
- },
340
- localStamp: (createdId) => ({
341
- summary: `wrapup: stamp local ${item.key} as promoted to ${createdId} and retire it from injection`,
342
- rationale: item.reason,
343
- expectedOutcome: `The local copy keeps its data but stops being injected; the global copy is the live one.`,
344
- edits: [
345
- {
346
- action: "update",
347
- kind: candidate.kind,
348
- id: candidate.id,
349
- title: candidate.title,
350
- content: candidate.content,
351
- metadata: {
352
- ...candidate.metadata,
353
- [PROMOTED_TO_KEY]: createdId,
354
- [PROMOTED_AT_KEY]: now,
355
- [ARCHIVED_AT_KEY]: now,
356
- },
357
- },
358
- ],
359
- }),
360
- };
361
- }
362
- /**
363
- * Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
364
- * local entry but promote ONLY the cleaned durable part the model extracted.
365
- * Same usage contract as {@link wholePromoteProposals}: apply the global
366
- * create, then stamp the original local entry with the created id.
367
- */
368
- export function splitPromoteProposals(item, candidate, sessionId) {
369
- if (!item.promote)
370
- throw new Error("split promote proposals require a promote payload");
371
- const now = new Date().toISOString();
372
- return {
373
- global: {
374
- summary: `wrapup: split — promote cleaned part of ${item.key} to the global store`,
375
- rationale: item.reason,
376
- expectedOutcome: `Only the durable part becomes visible globally; the snapshot half stays archived with the original.`,
377
- edits: [
378
- {
379
- action: "create",
380
- kind: candidate.kind,
381
- id: candidate.id,
382
- title: item.promote.title,
383
- content: item.promote.content,
384
- path: candidate.path,
385
- metadata: {
386
- ...candidate.metadata,
387
- [SOURCED_FROM_KEY]: `${sessionId}:${candidate.id}`,
388
- [PROMOTED_AT_KEY]: now,
389
- },
390
- },
391
- ],
392
- },
393
- localStamp: (createdId) => ({
394
- summary: `wrapup: split — archive original ${item.key}, stamped as promoted to ${createdId}`,
395
- rationale: item.reason,
396
- expectedOutcome: `The original leaves injection (data kept, restorable); the cleaned global copy is the live one.`,
397
- edits: [
398
- {
399
- action: "update",
400
- kind: candidate.kind,
401
- id: candidate.id,
402
- title: candidate.title,
403
- content: candidate.content,
404
- metadata: {
405
- ...candidate.metadata,
406
- [PROMOTED_TO_KEY]: createdId,
407
- [PROMOTED_AT_KEY]: now,
408
- [ARCHIVED_AT_KEY]: now,
409
- },
410
- },
411
- ],
412
- }),
413
- };
414
- }
415
- /**
416
- * Valence stamp (P1 效价反馈): a keep-verdict item the assessor marked
417
- * contradicted gets its negative-valence counter bumped — the entry stays
418
- * live but sinks in injection ranking and the next assessment prefers
419
- * archiving it. Promotes pass untouched (the human-approved global copy is
420
- * the live truth); archives don't need the stamp (they already leave
421
- * injection). Undefined when nothing applies.
422
- */
423
- export function valenceStampProposal(item, candidate) {
424
- if (item.verdict !== "keep" || !item.contradicted)
425
- return undefined;
426
- return {
427
- summary: `wrapup: stamp contradicted valence on ${item.key} (${candidate.negativeCount + 1})`,
428
- rationale: item.reason,
429
- expectedOutcome: "The entry's negative-valence counter rises; injection downranks it and the next assessment prefers archiving it.",
430
- edits: [
431
- {
432
- action: "update",
433
- kind: candidate.kind,
434
- id: candidate.id,
435
- title: candidate.title,
436
- content: candidate.content,
437
- metadata: { ...candidate.metadata, [VALENCE_NEGATIVE_KEY]: candidate.negativeCount + 1 },
438
- },
439
- ],
440
- };
441
- }
442
- export const WRAPUP_ASSESS_SYSTEM_PROMPT = `You are the /evolve session wrap-up assessor.
443
- A session is ending and its local harness entries need a fate. Classify each
444
- listed entry exactly once:
445
-
446
- - "promote" — the content is a stable, durable, CROSS-SESSION reusable lesson:
447
- a durable user preference, a project-level fact or convention, a reusable
448
- procedure or skill. Future sessions would benefit from seeing it.
449
- - "archive" — the content is session-specific task progress, one-off noise,
450
- superseded or obsolete, or already covered by the global store (note
451
- "covered globally" in the reason), or stale (old + never injected — note
452
- "stale (injectionCount=0, recency low)" in the reason), or CONTRADICTED —
453
- newer evidence in the trajectory or the global store disproves or overrides
454
- the entry's content (note what contradicts it in the reason).
455
- - "keep" — still actively useful to this session, or genuinely uncertain.
456
-
457
- Rules:
458
- - CONTRADICTED: when evidence contradicts an entry, set "contradicted": true
459
- on that item (besides the verdict). Entries marked "(contradicted N×
460
- before)" were contradicted in earlier assessments — prefer "archive" for
461
- them unless you have concrete evidence the content is valid again.
462
- - When an entry is marked "covered globally" in the listing, prefer "archive"
463
- or "keep" over "promote" — promoting a duplicate gains nothing.
464
- - When an entry is marked "stale" (injectionCount=0 and low recency), prefer
465
- "archive" — the entry has never been used and is old, so it is unlikely to
466
- be needed again. Only "keep" if the content is clearly valuable despite low
467
- usage (e.g. a safety policy that rarely triggers but is critical).
468
- - Do not promote local task state, work-in-progress notes, or content tied to
469
- one session's ephemeral details.
470
- - Memory entries carry a recall type (user/feedback/project/reference): a
471
- memory WITHOUT one cannot be promoted (the guard skips it) — verdict such
472
- entries "keep" (or "archive" when they qualify) rather than "promote".
473
- - Skills: only "promote" a skill entry that is a genuinely reusable procedure
474
- meeting the DSH skill quality standard; one-off workflows are "archive" or
475
- "keep".
476
- - SPLIT PROMOTION: when an entry mixes a stable, cross-session-reusable part
477
- WITH session-specific snapshot details, do NOT promote it whole. Instead
478
- give verdict "archive" WITH a "promote" sub-object holding a CLEANED
479
- version of only the durable part (a stable title + the persistent facts,
480
- stripped of dates/states/one-off figures). Ephemeral snapshot content stays
481
- out of the sub-object — it is left behind in the archive. A sub-object is
482
- only meaningful on "archive" verdicts.
483
-
484
- Return JSON only:
485
- {
486
- "rationale": "one or two sentences",
487
- "items": [
488
- {"key": "memory:foo", "verdict": "promote|archive|keep", "reason": "why"},
489
- {"key": "memory:bar", "verdict": "archive", "reason": "why", "contradicted": true,
490
- "promote": {"title": "cleaned stable title", "content": "cleaned durable part only"}}
491
- ]
492
- }
493
- Only keys from the provided list are allowed; any entry you omit defaults to "keep".`;
494
- /**
495
- * Ask the model to classify the audited local candidates. Routes through the
496
- * calling agent's own provider/model (same model the session runs on), with
497
- * reasoning disabled so the output budget goes to the JSON verdicts.
498
- */
499
- export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
500
- if (candidates.length === 0) {
501
- return { items: [], rationale: "No local candidates to assess." };
502
- }
503
- if (!agent.options.provider || !agent.options.model) {
504
- throw new Error("evolve: no provider/model route for the wrap-up assessor");
505
- }
506
- const candidateText = candidates
507
- .map((candidate) => {
508
- const key = candidateKey(candidate.kind, candidate.id);
509
- const covered = candidate.coveredGlobally ? " (covered globally)" : "";
510
- const stale = candidate.stale ? ` (stale: injectionCount=${candidate.injectionCount}, recency low)` : "";
511
- const negated = candidate.negativeCount > 0 ? ` (contradicted ${candidate.negativeCount}× before)` : "";
512
- const hints = candidate.globalHints.length > 0
513
- ? ` | global≈${candidate.globalHints.map((hint) => hint.id + ":" + hint.title).join(", ")}`
514
- : "";
515
- return `- ${key} [${candidate.path}, v${candidate.version}] "${candidate.title}"${covered}${stale}${negated}${hints}: ${compactText(candidate.content, 220)}`;
516
- })
517
- .join("\n");
518
- const userPrompt = [
519
- `A local session is wrapping up. Classify each entry below for its fate.`,
520
- `<local_entries>\n${candidateText}\n</local_entries>`,
521
- "Return only JSON. Every item must reference one of the keys above.",
522
- ].join("\n\n");
523
- const text = await streamText(ctx, {
524
- provider: agent.options.provider,
525
- model: agent.options.model,
526
- sessionId: asLlmSessionId(agent.id),
527
- system: `${WRAPUP_ASSESS_SYSTEM_PROMPT}\n\n${recordLanguageInstruction(options.language ?? resolveRecordLanguage({ ctx }))}`,
528
- prompt: userPrompt,
529
- maxTokens: options.maxOutputTokens ?? 4096,
530
- signal: options.signal,
531
- ...(options.tokenUsage
532
- ? {
533
- onUsage: createTokenUsageObserver(options.tokenUsage, options.tokenUsagePhase ?? "wrapup", agent.options.provider, agent.options.model),
534
- }
535
- : {}),
536
- });
537
- return parseWrapupAssessment(text, candidates);
538
- }
539
- //# sourceMappingURL=wrapup.js.map