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/failures.js DELETED
@@ -1,171 +0,0 @@
1
- /**
2
- * Failure-signature aggregation (gap R/D1 — observation stage): turn the
3
- * free-text failure records the system already produces (review-gate failures
4
- * in reviews.jsonl, failed cells in benchmark scoreboards) into a structured
5
- * count by failure CLASS. This is deliberately NOT the full failure-signature
6
- * Refiner from gap D1 — no routing, no policy — it is the data layer that
7
- * lets a later patch decide whether a given failure class recurs often enough
8
- * to deserve one.
9
- *
10
- * Classes are extracted with pure prefix rules (see classifyFailure), so the
11
- * aggregation is deterministic and unit-testable: the same failure text
12
- * always lands in the same class.
13
- */
14
- import { existsSync, readFileSync, readdirSync } from "node:fs";
15
- import { join } from "node:path";
16
- /**
17
- * Classify a failure message by prefix rules. Deterministic and additive:
18
- * unknown text falls into "other" so the summary never drops a failure.
19
- */
20
- export function classifyFailure(message) {
21
- const text = (message ?? "").trim();
22
- const lower = text.toLowerCase();
23
- if (lower.includes("rubric decrypt failed"))
24
- return "rubric-decrypt";
25
- if (lower.includes("materials changed"))
26
- return "material-drift";
27
- if (lower.includes("executor failed") || lower.includes("executor stopped"))
28
- return "executor";
29
- if (lower.includes("reviewer failed") || lower.includes("reviewer stopped"))
30
- return "reviewer";
31
- if (lower.includes("fate assessment error"))
32
- return "fate-assessor";
33
- if (lower.includes("trajectory unavailable"))
34
- return "trajectory";
35
- if (lower.includes("output budget exhausted") || lower.includes("max-tokens"))
36
- return "max-tokens";
37
- if (lower.includes("llm call aborted") || lower.includes("aborted"))
38
- return "aborted";
39
- if (lower.includes("llm call failed"))
40
- return "llm";
41
- if (lower.includes("gate error"))
42
- return "gate";
43
- if (lower.includes("casecheck") || lower.includes("case check"))
44
- return "casecheck";
45
- return "other";
46
- }
47
- /** Aggregate a record list into counts. Empty input yields an all-zero summary. */
48
- export function summarizeFailures(records) {
49
- const byKind = {};
50
- const bySource = {};
51
- for (const record of records) {
52
- byKind[record.kind] = (byKind[record.kind] ?? 0) + 1;
53
- bySource[record.source] = (bySource[record.source] ?? 0) + 1;
54
- }
55
- return {
56
- total: records.length,
57
- byKind: Object.fromEntries(Object.entries(byKind).sort((a, b) => b[1] - a[1])),
58
- bySource: Object.fromEntries(Object.entries(bySource).sort((a, b) => b[1] - a[1])),
59
- };
60
- }
61
- /**
62
- * Read failed review-gate records from `<baseDir>/evolve/reviews.jsonl`
63
- * (outcome === "failed"). Tolerant of a missing/corrupt file.
64
- */
65
- export function readReviewFailures(baseDir) {
66
- const path = join(baseDir, "evolve", "reviews.jsonl");
67
- if (!existsSync(path))
68
- return [];
69
- const records = [];
70
- try {
71
- for (const line of readFileSync(path, "utf8").split("\n")) {
72
- if (line.trim().length === 0)
73
- continue;
74
- try {
75
- const raw = JSON.parse(line);
76
- if (raw.outcome !== "failed" || !raw.rationale)
77
- continue;
78
- records.push({
79
- ...(raw.timestamp ? { timestamp: raw.timestamp } : {}),
80
- source: `review-gate${raw.reason ? `:${raw.reason}` : ""}`,
81
- kind: classifyFailure(raw.rationale),
82
- message: raw.rationale,
83
- });
84
- }
85
- catch {
86
- // skip malformed lines — the audit file must never break reporting
87
- }
88
- }
89
- }
90
- catch {
91
- return [];
92
- }
93
- return records;
94
- }
95
- /**
96
- * Read failed cells from every benchmark scoreboard under
97
- * `<baseDir>/evolve/benchmarks/<bid>/`. Tolerant of missing/corrupt data.
98
- */
99
- export function readBenchmarkFailures(baseDir) {
100
- const root = join(baseDir, "evolve", "benchmarks");
101
- if (!existsSync(root))
102
- return [];
103
- const records = [];
104
- let bids;
105
- try {
106
- bids = readdirSync(root);
107
- }
108
- catch {
109
- return [];
110
- }
111
- for (const bid of bids) {
112
- const boardPath = join(root, bid, "scoreboard.json");
113
- if (!existsSync(boardPath))
114
- continue;
115
- let board;
116
- try {
117
- board = JSON.parse(readFileSync(boardPath, "utf8"));
118
- }
119
- catch {
120
- continue;
121
- }
122
- const cellLists = [];
123
- if (board.reference?.cells)
124
- cellLists.push({ label: "reference", cells: board.reference.cells });
125
- for (const entry of board.candidates ?? []) {
126
- if (entry.cells)
127
- cellLists.push({ label: entry.label ?? "candidate", cells: entry.cells });
128
- }
129
- for (const list of cellLists) {
130
- for (const cell of list.cells) {
131
- const c = cell;
132
- if (c.status !== "failed" || !c.notes)
133
- continue;
134
- records.push({
135
- source: `benchmark:${bid}:${c.caseId ?? "?"}`,
136
- kind: classifyFailure(c.notes),
137
- message: c.notes,
138
- });
139
- }
140
- }
141
- }
142
- return records;
143
- }
144
- /** Combine both sources into one summary. */
145
- export function collectFailureSummary(baseDir) {
146
- const records = [...readReviewFailures(baseDir), ...readBenchmarkFailures(baseDir)];
147
- return { summary: summarizeFailures(records), records };
148
- }
149
- /** Human-readable report for the command line. */
150
- export function formatFailureSummary(summary) {
151
- const lines = [`failure summary: ${summary.total} total`];
152
- const kinds = Object.entries(summary.byKind);
153
- if (kinds.length > 0) {
154
- lines.push("by class:");
155
- for (const [kind, count] of kinds) {
156
- lines.push(` ${kind}: ${count}`);
157
- }
158
- }
159
- else {
160
- lines.push("by class: (none)");
161
- }
162
- const sources = Object.entries(summary.bySource);
163
- if (sources.length > 0) {
164
- lines.push("by source:");
165
- for (const [source, count] of sources) {
166
- lines.push(` ${source}: ${count}`);
167
- }
168
- }
169
- return lines.join("\n");
170
- }
171
- //# sourceMappingURL=failures.js.map
package/lib/fate.d.ts DELETED
@@ -1,130 +0,0 @@
1
- /**
2
- * Gate local-fate dimension (#11 P2): the automatic review gate also decides
3
- * what happens to a session's local entries while the session is STILL
4
- * running — not only at wrap-up.
5
- *
6
- * Local entries default to orphans: a later session (not on the parentSession
7
- * chain) never sees them, and nothing promotes or archives them — the
8
- * exploration results effectively "die" with the session. This module gives
9
- * the gate its own fate cadence on top of the existing review:
10
- *
11
- * - `promote` → the entry moves into the global store. The global store is a
12
- * governed resource: the user is consulted FIRST (the consultSkillEdits
13
- * pattern — never written silently), with a cooldown after a decline.
14
- * - `archive` → hidden from injection (data stays restorable). Archives that
15
- * would bury possibly-reusable content (not covered globally AND distilled
16
- * from real user messages) ask the user first; covered/operational entries
17
- * archive silently, exactly like the wrap-up command does.
18
- * - `keep` → nothing.
19
- *
20
- * At compaction the gate NEVER opens a dialog (the agent is mid-compaction):
21
- * only deterministic silent archives apply, everything governed is deferred
22
- * with an audit record pointing at `/evolve wrapup`.
23
- *
24
- * Division of labor is the same as wrap-up: the mechanical audit proposes
25
- * (listLocalCandidates + coverage guards), the LLM classifies
26
- * (assessLocalEntries), the user approves, the code applies deterministically
27
- * (wholePromoteProposals / splitPromoteProposals — the SAME edits the wrap-up
28
- * command applies). Every decision lands in reviews.jsonl via the gate's
29
- * record callback.
30
- */
31
- import type { Context } from "@deepseek-ai/cordis";
32
- import type { Agent } from "@deepseek-ai/dsh-agent";
33
- import type { HarnessState, RefinementResult } from "./types.js";
34
- import type { EvolutionEngine } from "./service.js";
35
- import type { AutoRefineReason } from "./review.js";
36
- import type { AutoReviewConfig, GateState, ReviewRecord } from "./auto.js";
37
- import { type PromotionPolicy } from "./promotion.js";
38
- import { type RecordLanguage } from "./record-language.js";
39
- import { type WrapupCandidate, type WrapupItem } from "./wrapup.js";
40
- /** Turns a declined local-fate proposal stays silent before being offered again. */
41
- export declare const FATE_CONSULT_COOLDOWN_TURNS = 10;
42
- /** What the gate decided to do with the session's local entries. */
43
- export interface FatePlan {
44
- candidates: readonly WrapupCandidate[];
45
- /** Whole promotions that passed the deterministic global-coverage guard. */
46
- promotable: WrapupItem[];
47
- /** Split promotions (archive + cleaned promote payload) that passed the guard. */
48
- splits: {
49
- item: WrapupItem;
50
- candidate: WrapupCandidate;
51
- }[];
52
- /** Archives that may proceed silently (covered globally / no real distillation source). */
53
- silentArchives: WrapupItem[];
54
- /** Archives that must ask the user first (uncovered + real source). */
55
- reviewArchives: WrapupItem[];
56
- /** Promotes blocked by the deterministic guard, with why. */
57
- skipped: {
58
- key: string;
59
- reason: string;
60
- }[];
61
- /** Split promotions blocked by the deterministic guard, with why. */
62
- splitSkipped: {
63
- key: string;
64
- reason: string;
65
- }[];
66
- }
67
- /**
68
- * Partition an assessed wrap-up classification into concrete fate actions,
69
- * re-running the deterministic guards against the LIVE global store (state
70
- * may have changed while the LLM call was in flight). Pure and unit-tested;
71
- * mirrors the partition step of the wrap-up command.
72
- */
73
- export declare function planLocalFates(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[], globalState: HarnessState, policy?: PromotionPolicy): FatePlan;
74
- /**
75
- * The cooldown key of a candidate set: the sorted `kind:id` list. The set is
76
- * the unit of consultation — a declined proposal is not offered again within
77
- * the cooldown window, and a changed set (new entries appeared) starts a
78
- * fresh consultation.
79
- */
80
- export declare function fateSetKey(candidates: readonly WrapupCandidate[]): string;
81
- /**
82
- * Whether the local-fate dimension is due for this gate run. Turn-interval
83
- * gates respect the fate cadence (an independent counter — goal-driven
84
- * sessions run the review EVERY round, the fate assessment must not);
85
- * compaction is unconditional: experiences about to be summarized away get
86
- * their fate check regardless. Goal-blocked assessments are unconditional
87
- * here too — the gate's own streak counter (auto.ts runGoalBlockedFate)
88
- * already gates their frequency, so the cadence must not re-block them.
89
- */
90
- export declare function fateCadenceDue(state: GateState, reason: AutoRefineReason, intervalTurns: number): boolean;
91
- export interface FateConsultResult {
92
- approved: boolean;
93
- asked: boolean;
94
- reason: "nothing-to-ask" | "consented" | "declined" | "cooldown" | "unavailable" | "error";
95
- }
96
- /**
97
- * Ask the user whether to execute the gate's local-fate proposal. ONE dialog
98
- * covers every governed action (promotes, split promotions, review-required
99
- * archives) — the gate never spams questions. Conservative on every edge:
100
- * no question service → not approved; the question call fails → not approved;
101
- * the same candidate set was declined within the cooldown → not asked again.
102
- * A decline records the cooldown (the consultSkillEdits pattern).
103
- */
104
- export declare function consultLocalFates(ctx: Context, agent: Agent, plan: FatePlan, gate: GateState, language?: RecordLanguage): Promise<FateConsultResult>;
105
- export type FateApplyMode = "full" | "silent-only";
106
- export interface FateApplyResult {
107
- /** Human-readable lines of what was applied (for the notice and the audit record). */
108
- applied: string[];
109
- /** Every refinement result produced, for rollback discovery. */
110
- results: RefinementResult[];
111
- }
112
- /**
113
- * Deterministically apply the fate plan. `"full"` applies everything
114
- * (promotes, splits, silent + review archives); `"silent-only"` applies only
115
- * the deterministic silent archives (the compaction path — nothing governed).
116
- * Promotes go through the SAME proposals as the wrap-up command
117
- * (wholePromoteProposals / splitPromoteProposals), so both paths write
118
- * identical global entries and local retirement stamps.
119
- */
120
- export declare function applyLocalFates(engine: EvolutionEngine, sessionId: string, plan: FatePlan, localState: HarnessState, mode: FateApplyMode): FateApplyResult;
121
- /**
122
- * The gate's local-fate phase. Runs after the review phase on every gate
123
- * trigger (turn_snapshot / compact), subject to cadence and cooldown. All
124
- * failures are contained and recorded — a broken fate dimension never
125
- * disturbs the agent loop.
126
- */
127
- export declare function runLocalFatePhase(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, reason: AutoRefineReason, record: (entry: Omit<ReviewRecord, "timestamp">) => void): Promise<void>;
128
- /** The user-visible notice after the gate applied local-fate actions. */
129
- export declare function buildFateNotice(applied: readonly string[]): string;
130
- //# sourceMappingURL=fate.d.ts.map
package/lib/fate.js DELETED
@@ -1,350 +0,0 @@
1
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
- import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
3
- import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
4
- import { questionServiceOf } from "./approval.js";
5
- import { fateConsultCopy, resolveDialogLanguage } from "./copy.js";
6
- import { resolveRecordLanguage } from "./record-language.js";
7
- import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals, } from "./wrapup.js";
8
- /** Turns a declined local-fate proposal stays silent before being offered again. */
9
- export const FATE_CONSULT_COOLDOWN_TURNS = 10;
10
- /**
11
- * Partition an assessed wrap-up classification into concrete fate actions,
12
- * re-running the deterministic guards against the LIVE global store (state
13
- * may have changed while the LLM call was in flight). Pure and unit-tested;
14
- * mirrors the partition step of the wrap-up command.
15
- */
16
- export function planLocalFates(items, candidates, globalState, policy = DEFAULT_PROMOTION_POLICY) {
17
- const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
18
- const { promotable, skipped } = filterPromotable(items, globalState, candidates, policy);
19
- const promoteItems = promotable.filter((item) => item.verdict === "promote");
20
- const archiveItems = items.filter((item) => item.verdict === "archive");
21
- const splits = [];
22
- const splitSkipped = [];
23
- for (const item of archiveItems) {
24
- if (!item.promote)
25
- continue;
26
- const candidate = byKey.get(item.key);
27
- if (!candidate) {
28
- splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
29
- continue;
30
- }
31
- const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
32
- if (blocked) {
33
- splitSkipped.push({ key: item.key, reason: blocked });
34
- continue;
35
- }
36
- splits.push({ item, candidate });
37
- }
38
- const plainArchives = archiveItems.filter((item) => !item.promote);
39
- const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
40
- return { candidates, promotable: promoteItems, splits, silentArchives, reviewArchives, skipped, splitSkipped };
41
- }
42
- /**
43
- * The cooldown key of a candidate set: the sorted `kind:id` list. The set is
44
- * the unit of consultation — a declined proposal is not offered again within
45
- * the cooldown window, and a changed set (new entries appeared) starts a
46
- * fresh consultation.
47
- */
48
- export function fateSetKey(candidates) {
49
- return candidates
50
- .map((candidate) => candidateKey(candidate.kind, candidate.id))
51
- .sort()
52
- .join("|");
53
- }
54
- /**
55
- * Whether the local-fate dimension is due for this gate run. Turn-interval
56
- * gates respect the fate cadence (an independent counter — goal-driven
57
- * sessions run the review EVERY round, the fate assessment must not);
58
- * compaction is unconditional: experiences about to be summarized away get
59
- * their fate check regardless. Goal-blocked assessments are unconditional
60
- * here too — the gate's own streak counter (auto.ts runGoalBlockedFate)
61
- * already gates their frequency, so the cadence must not re-block them.
62
- */
63
- export function fateCadenceDue(state, reason, intervalTurns) {
64
- // Compact and goal-blocked assessments bypass the cadence (see header).
65
- if (reason === "compact" || reason === "goal_blocked")
66
- return true;
67
- return state.turns - state.lastFateAt >= intervalTurns;
68
- }
69
- /**
70
- * Ask the user whether to execute the gate's local-fate proposal. ONE dialog
71
- * covers every governed action (promotes, split promotions, review-required
72
- * archives) — the gate never spams questions. Conservative on every edge:
73
- * no question service → not approved; the question call fails → not approved;
74
- * the same candidate set was declined within the cooldown → not asked again.
75
- * A decline records the cooldown (the consultSkillEdits pattern).
76
- */
77
- export async function consultLocalFates(ctx, agent, plan, gate, language) {
78
- const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
79
- if (!needsDialog) {
80
- return { approved: true, asked: false, reason: "nothing-to-ask" };
81
- }
82
- const setKey = fateSetKey(plan.candidates);
83
- const lastReject = gate.fateRejects.get(setKey);
84
- if (lastReject !== undefined && gate.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
85
- return { approved: false, asked: false, reason: "cooldown" };
86
- }
87
- const userQuestions = questionServiceOf(ctx);
88
- if (!userQuestions) {
89
- return { approved: false, asked: false, reason: "unavailable" };
90
- }
91
- const lang = language ?? resolveDialogLanguage(ctx);
92
- try {
93
- const copy = fateConsultCopy(sectionsOf(plan, lang), lang);
94
- const answer = await userQuestions.ask({
95
- questions: [
96
- {
97
- id: "evolve-fate-consult",
98
- question: copy.question,
99
- options: copy.options,
100
- },
101
- ],
102
- agent,
103
- });
104
- const selected = answer.answers?.find((entry) => entry.id === "evolve-fate-consult")?.selected ?? [];
105
- const approved = selected.includes("执行") || selected.includes("Proceed");
106
- if (!approved) {
107
- gate.fateRejects.set(setKey, gate.turns);
108
- }
109
- return { approved, asked: true, reason: approved ? "consented" : "declined" };
110
- }
111
- catch {
112
- return { approved: false, asked: true, reason: "error" };
113
- }
114
- }
115
- /** Split a fate plan into pre-rendered per-group lines for the dialog copy. */
116
- function sectionsOf(plan, lang) {
117
- const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
118
- const promotable = [];
119
- const splits = [];
120
- const reviewArchives = [];
121
- for (const item of plan.promotable) {
122
- promotable.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
123
- }
124
- for (const { item } of plan.splits) {
125
- splits.push(lang === "zh"
126
- ? `- ${item.key} → 拆出提升「${item.promote?.title}」(原条目随之归档)`
127
- : `- ${item.key} → split-promote "${item.promote?.title}" (original archived alongside)`);
128
- }
129
- for (const item of plan.reviewArchives) {
130
- reviewArchives.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
131
- }
132
- return { promotable, splits, reviewArchives };
133
- }
134
- /**
135
- * Deterministically apply the fate plan. `"full"` applies everything
136
- * (promotes, splits, silent + review archives); `"silent-only"` applies only
137
- * the deterministic silent archives (the compaction path — nothing governed).
138
- * Promotes go through the SAME proposals as the wrap-up command
139
- * (wholePromoteProposals / splitPromoteProposals), so both paths write
140
- * identical global entries and local retirement stamps.
141
- */
142
- export function applyLocalFates(engine, sessionId, plan, localState, mode) {
143
- const applied = [];
144
- const results = [];
145
- const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
146
- if (mode === "full") {
147
- for (const item of plan.promotable) {
148
- const candidate = byKey.get(item.key);
149
- if (!candidate)
150
- continue;
151
- const proposals = wholePromoteProposals(item, candidate, sessionId);
152
- const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
153
- const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
154
- const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
155
- scope: "local",
156
- baselineState: localState,
157
- });
158
- results.push(globalResult, localResult);
159
- applied.push(`提升 ${item.key} → 全局 store(global:${createdId},本地副本已归档)`);
160
- }
161
- for (const { item, candidate } of plan.splits) {
162
- if (!item.promote)
163
- continue;
164
- const proposals = splitPromoteProposals(item, candidate, sessionId);
165
- const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
166
- const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
167
- const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
168
- scope: "local",
169
- baselineState: localState,
170
- });
171
- results.push(globalResult, localResult);
172
- applied.push(`拆解 ${item.key} → 清洗「${item.promote.title}」入全局(原条目已归档)`);
173
- }
174
- }
175
- const archives = mode === "full" ? [...plan.silentArchives, ...plan.reviewArchives] : plan.silentArchives;
176
- for (const item of archives) {
177
- const candidate = byKey.get(item.key);
178
- if (!candidate)
179
- continue;
180
- const proposal = {
181
- summary: `gate fate: archive local ${item.key} — ${item.reason}`,
182
- rationale: item.reason,
183
- expectedOutcome: "The entry stops being injected but stays restorable.",
184
- edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
185
- };
186
- const result = engine.apply("local", sessionId, proposal, { scope: "local", baselineState: localState });
187
- results.push(result);
188
- applied.push(`归档 ${item.key}「${candidate.title}」`);
189
- }
190
- return { applied, results };
191
- }
192
- /**
193
- * The gate's local-fate phase. Runs after the review phase on every gate
194
- * trigger (turn_snapshot / compact), subject to cadence and cooldown. All
195
- * failures are contained and recorded — a broken fate dimension never
196
- * disturbs the agent loop.
197
- */
198
- export async function runLocalFatePhase(ctx, engine, agent, config, state, reason, record) {
199
- if (!config.localFate)
200
- return;
201
- const logger = ctx.logger("continual-evolve");
202
- const sessionId = agent.id;
203
- const tokenUsage = {
204
- baseDir: engine.baseDir,
205
- sessionId,
206
- retain: engine.retention.tokenUsage,
207
- onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
208
- };
209
- const localState = engine.load("local", sessionId);
210
- const globalState = engine.load("global", undefined);
211
- const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
212
- if (candidates.length === 0)
213
- return;
214
- if (!fateCadenceDue(state, reason, config.fateIntervalTurns))
215
- return;
216
- const setKey = fateSetKey(candidates);
217
- const lastReject = state.fateRejects.get(setKey);
218
- if (lastReject !== undefined && state.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
219
- logger.info(`auto-review local-fate skipped [${sessionId}]: candidate set declined ${state.turns - lastReject} turns ago (cooldown)`);
220
- return;
221
- }
222
- const turnsSinceFate = state.turns - state.lastFateAt;
223
- state.lastFateAt = state.turns;
224
- let assessment;
225
- const fateLanguage = resolveRecordLanguage({ configured: config.recordLanguage, ctx });
226
- try {
227
- assessment = await assessLocalEntries(ctx, agent, candidates, { tokenUsage, tokenUsagePhase: "fate", language: fateLanguage });
228
- }
229
- catch (cause) {
230
- const message = cause instanceof Error ? cause.message : String(cause);
231
- logger.warn(`auto-review local-fate failed for ${sessionId}: ${message}`);
232
- record({
233
- sessionId,
234
- reason,
235
- turnsSinceLastReview: turnsSinceFate,
236
- outcome: "failed",
237
- rationale: `fate assessment error: ${message}`,
238
- });
239
- return;
240
- }
241
- const plan = planLocalFates(assessment.items, candidates, globalState, config.promotionPolicy);
242
- const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
243
- let consent = { approved: false, asked: false, reason: "nothing-to-ask" };
244
- // Turn-path tightening (2026-09-25): turn_snapshot/turn_interval never open
245
- // a dialog — governed actions are deferred to `/evolve wrapup`. Only an
246
- // explicit goal_blocked signal (own streak counter) may consult.
247
- if (reason === "goal_blocked" && needsDialog) {
248
- consent = await consultLocalFates(ctx, agent, plan, state, fateLanguage);
249
- }
250
- if (consent.approved) {
251
- const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "full");
252
- logger.info(`auto-review local-fate approved (${reason}) [${sessionId}]: ${plan.promotable.length} promoted, ${plan.splits.length} split, ${plan.reviewArchives.length} review-archived, ${plan.silentArchives.length} silent-archived — ${assessment.rationale}`);
253
- record({
254
- sessionId,
255
- reason,
256
- turnsSinceLastReview: turnsSinceFate,
257
- outcome: "approved",
258
- rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
259
- refinementId: results.map((result) => result.id).join(","),
260
- });
261
- return;
262
- }
263
- if (consent.reason === "declined" || consent.reason === "unavailable" || consent.reason === "error") {
264
- const withheld = consent.reason;
265
- const outcome = withheld === "declined" ? "declined" : "deferred";
266
- logger.info(`auto-review local-fate ${outcome} (${reason}) [${sessionId}]: ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives withheld — ${assessment.rationale}`);
267
- record({
268
- sessionId,
269
- reason,
270
- turnsSinceLastReview: turnsSinceFate,
271
- outcome,
272
- rationale: `fate ${withheld}: ${assessment.rationale} (${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives withheld)`,
273
- });
274
- return;
275
- }
276
- if (needsDialog && reason !== "goal_blocked") {
277
- // Turn/compact path: no dialog. Only deterministic silent archives apply;
278
- // governed actions are deferred with an audit record pointing at wrapup.
279
- const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
280
- if (applied.length > 0) {
281
- logger.info(`auto-review local-fate (${reason}) [${sessionId}]: silent-archived ${applied.length} — ${assessment.rationale}`);
282
- record({
283
- sessionId,
284
- reason,
285
- turnsSinceLastReview: turnsSinceFate,
286
- outcome: "approved",
287
- rationale: `fate (${reason}): ${assessment.rationale} (${applied.join("; ")})`,
288
- refinementId: results.map((result) => result.id).join(","),
289
- });
290
- }
291
- record({
292
- sessionId,
293
- reason,
294
- turnsSinceLastReview: turnsSinceFate,
295
- outcome: "deferred",
296
- rationale: `fate (${reason}): ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives deferred — run /evolve wrapup for a full session exit`,
297
- });
298
- return;
299
- }
300
- // Silent archives on a plain turn-interval gate with nothing to ask.
301
- if (plan.silentArchives.length > 0) {
302
- const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
303
- logger.info(`auto-review local-fate (${reason}) [${sessionId}]: silent-archived ${applied.length} covered/operational entries — ${assessment.rationale}`);
304
- record({
305
- sessionId,
306
- reason,
307
- turnsSinceLastReview: turnsSinceFate,
308
- outcome: "approved",
309
- rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
310
- refinementId: results.map((result) => result.id).join(","),
311
- });
312
- if (config.notifyOnAutoReview && (reason === "turn_snapshot" || reason === "turn_interval")) {
313
- notifyFateApplied(ctx, agent, applied);
314
- }
315
- return;
316
- }
317
- // Assessed, nothing to do.
318
- logger.info(`auto-review local-fate assessed (${reason}) [${sessionId}]: ${candidates.length} candidates, no action — ${assessment.rationale}`);
319
- record({
320
- sessionId,
321
- reason,
322
- turnsSinceLastReview: turnsSinceFate,
323
- outcome: "assessed",
324
- rationale: `fate: ${assessment.rationale} (${candidates.length} candidates, no action)`,
325
- });
326
- }
327
- /** The user-visible notice after the gate applied local-fate actions. */
328
- export function buildFateNotice(applied) {
329
- return [
330
- "🔎 自动进化门禁:本会话 local 条目归宿处理完成:",
331
- applied.map((line) => `- ${line}`).join("\n"),
332
- "查看全部条目:/evolve list;撤销:/evolve rollback <refinement id>",
333
- "请用一句话简短确认即可,不要调用任何工具。",
334
- ].join("\n");
335
- }
336
- /** Queue the follow-up notice turn (successful-turn path only). */
337
- function notifyFateApplied(ctx, agent, applied) {
338
- try {
339
- agent.followup(createUserMessage({
340
- content: [{ type: "text", text: buildFateNotice(applied) }],
341
- source: EVOLVE_MESSAGE_SOURCE,
342
- }));
343
- }
344
- catch (cause) {
345
- ctx
346
- .logger("continual-evolve")
347
- .warn(`local-fate notice failed for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
348
- }
349
- }
350
- //# sourceMappingURL=fate.js.map
@@ -1,7 +0,0 @@
1
- /**
2
- * The `/evolve goal` subcommand handler. Extracted from command.ts (P2-2).
3
- */
4
- import type { Context } from "@deepseek-ai/cordis";
5
- import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
6
- export declare function executeGoalCommand(ctx: Context, invocation: CommandInvocation, rest: string[]): CommandResult;
7
- //# sourceMappingURL=goal-command.d.ts.map