dsh-continual-evolve 0.10.5 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -31
- package/README.zh.md +18 -21
- package/lib/approval.d.ts +19 -6
- package/lib/approval.js +73 -12
- package/lib/audit-log.d.ts +19 -0
- package/lib/audit-log.js +52 -0
- package/lib/auto.js +1 -0
- package/lib/benchmark-command.d.ts +1 -1
- package/lib/benchmark-command.js +1 -1
- package/lib/command-util.d.ts +70 -0
- package/lib/command-util.js +123 -0
- package/lib/command.d.ts +7 -40
- package/lib/command.js +25 -459
- package/lib/copy.d.ts +4 -41
- package/lib/copy.js +0 -102
- package/lib/declines.d.ts +2 -0
- package/lib/declines.js +4 -0
- package/lib/evolve-event.js +2 -1
- package/lib/extraction-phase.d.ts +132 -0
- package/lib/extraction-phase.js +236 -0
- package/lib/index.d.ts +26 -42
- package/lib/index.js +24 -34
- package/lib/inject.d.ts +13 -119
- package/lib/inject.js +12 -260
- package/lib/injection-caps.d.ts +4 -0
- package/lib/injection-caps.js +4 -0
- package/lib/injection-directory.d.ts +29 -0
- package/lib/injection-directory.js +78 -0
- package/lib/injection-rank.d.ts +85 -0
- package/lib/injection-rank.js +161 -0
- package/lib/listener.d.ts +45 -0
- package/lib/listener.js +199 -0
- package/lib/maintenance-commands.d.ts +23 -0
- package/lib/maintenance-commands.js +136 -0
- package/lib/memory-agent.d.ts +7 -41
- package/lib/memory-agent.js +8 -276
- package/lib/memory-apply.d.ts +44 -0
- package/lib/memory-apply.js +202 -0
- package/lib/memory-manifest.d.ts +16 -0
- package/lib/memory-manifest.js +70 -0
- package/lib/mount-command.js +1 -1
- package/lib/notify.d.ts +9 -20
- package/lib/notify.js +0 -40
- package/lib/plan-command.d.ts +12 -0
- package/lib/plan-command.js +38 -0
- package/lib/prefix-cache.d.ts +2 -1
- package/lib/review-scheduler.d.ts +1 -1
- package/lib/review-scheduler.js +2 -10
- package/lib/runtime-commands.d.ts +14 -0
- package/lib/runtime-commands.js +115 -0
- package/lib/service.d.ts +16 -2
- package/lib/service.js +74 -1
- package/lib/store.d.ts +1 -2
- package/lib/store.js +2 -2
- package/lib/tool.d.ts +7 -0
- package/lib/tool.js +4 -4
- package/lib/turn-snapshot.d.ts +7 -1
- package/lib/turn-snapshot.js +14 -0
- package/lib/types.d.ts +10 -0
- package/lib/types.js +6 -0
- package/lib/wrapup-candidates.d.ts +127 -0
- package/lib/wrapup-candidates.js +161 -0
- package/lib/wrapup-command.d.ts +2 -1
- package/lib/wrapup-command.js +7 -4
- package/lib/wrapup-proposals.d.ts +101 -0
- package/lib/wrapup-proposals.js +281 -0
- package/lib/wrapup.d.ts +8 -212
- package/lib/wrapup.js +4 -438
- package/package.json +1 -1
package/lib/copy.js
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backend user-visible copy for the four evolution confirmation dialogs,
|
|
3
|
-
* in both shipped languages. Labels stay literal per language (the parsers
|
|
4
|
-
* match them); guidance lives in `description`.
|
|
5
|
-
*
|
|
6
|
-
* Language is never hardcoded at a call site: every builder takes an
|
|
7
|
-
* explicit `lang`, and dialog entry points resolve it per call
|
|
8
|
-
* (explicit config upstream, else the durable DSH client preference,
|
|
9
|
-
* else `en`) when the caller does not supply one.
|
|
10
|
-
*/
|
|
11
|
-
import { resolveRecordLanguage } from "./record-language.js";
|
|
12
1
|
/** Maximum edit-summary chars carried into a dialog; longer text truncates. */
|
|
13
2
|
export const COPY_WHAT_MAX_CHARS = 300;
|
|
14
3
|
function compactWhat(what) {
|
|
@@ -42,46 +31,6 @@ export function approvalCopy(scope, what, lang) {
|
|
|
42
31
|
],
|
|
43
32
|
};
|
|
44
33
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Local-fate homing consult (`evolve-fate-consult`).
|
|
47
|
-
*
|
|
48
|
-
* @param sections Pre-rendered per-item lines per group.
|
|
49
|
-
* @param lang Dialog language.
|
|
50
|
-
*/
|
|
51
|
-
export function fateConsultCopy(sections, lang) {
|
|
52
|
-
if (lang === "zh") {
|
|
53
|
-
const lines = ["自进化门禁:本会话 local 条目需要归宿处理"];
|
|
54
|
-
if (sections.promotable.length > 0 || sections.splits.length > 0) {
|
|
55
|
-
lines.push("【提升到全局(写入全局 store)】", ...sections.promotable, ...sections.splits);
|
|
56
|
-
}
|
|
57
|
-
if (sections.reviewArchives.length > 0) {
|
|
58
|
-
lines.push("【归档(本地隐藏,可恢复)】", ...sections.reviewArchives);
|
|
59
|
-
}
|
|
60
|
-
lines.push("提升写入后所有会话可见,归档隐藏但可恢复。是否执行?");
|
|
61
|
-
return {
|
|
62
|
-
question: lines.join("\n"),
|
|
63
|
-
options: [
|
|
64
|
-
{ label: "执行", description: "提升写全局,归档隐藏本地(均可恢复)" },
|
|
65
|
-
{ label: "不执行", description: "全部保留,10 回合内不再打扰" },
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
const lines = ["Evolution gate: this session's local entries need homing"];
|
|
70
|
-
if (sections.promotable.length > 0 || sections.splits.length > 0) {
|
|
71
|
-
lines.push("[Promote to global (write to the global store)]", ...sections.promotable, ...sections.splits);
|
|
72
|
-
}
|
|
73
|
-
if (sections.reviewArchives.length > 0) {
|
|
74
|
-
lines.push("[Archive (hidden locally, restorable)]", ...sections.reviewArchives);
|
|
75
|
-
}
|
|
76
|
-
lines.push("Promotions become visible to all sessions; archives hide but stay restorable. Proceed?");
|
|
77
|
-
return {
|
|
78
|
-
question: lines.join("\n"),
|
|
79
|
-
options: [
|
|
80
|
-
{ label: "Proceed", description: "Promote to global, archive locally (all reversible)" },
|
|
81
|
-
{ label: "Skip", description: "Keep everything, cooldown 10 turns" },
|
|
82
|
-
],
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
34
|
/**
|
|
86
35
|
* Single-entry wrap-up archive confirm (`evolve-wrapup-archive-review`).
|
|
87
36
|
*
|
|
@@ -106,55 +55,4 @@ export function wrapupArchiveCopy(title, lang) {
|
|
|
106
55
|
],
|
|
107
56
|
};
|
|
108
57
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Skill solidification consult (`evolve-skill-consult`).
|
|
111
|
-
*
|
|
112
|
-
* @param description Pre-rendered per-edit lines.
|
|
113
|
-
* @param lang Dialog language.
|
|
114
|
-
*/
|
|
115
|
-
export function skillConsultCopy(description, lang) {
|
|
116
|
-
if (lang === "zh") {
|
|
117
|
-
return {
|
|
118
|
-
question: `发现可复用的流程,建议固化为技能\n${description}\n\n固化后以后同类任务自动复用,可回滚。是否固化?`,
|
|
119
|
-
options: [
|
|
120
|
-
{ label: "固化", description: "生成技能,以后复用" },
|
|
121
|
-
{ label: "不固化", description: "本次跳过,10 回合内不再打扰" },
|
|
122
|
-
],
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
question: `Reusable workflow found, suggest solidifying as a skill\n${description}\n\nSolidified skills are reused by similar tasks later, reversible. Solidify?`,
|
|
127
|
-
options: [
|
|
128
|
-
{ label: "Solidify", description: "Create the skill for reuse" },
|
|
129
|
-
{ label: "Skip", description: "Skip this time, cooldown 10 turns" },
|
|
130
|
-
],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* One skill-edit line inside the solidification consult.
|
|
135
|
-
*
|
|
136
|
-
* @param action Edit action (`create` etc.).
|
|
137
|
-
* @param title Entry title or id fallback.
|
|
138
|
-
* @param skillKind `guidance` skills are SKILL.md documents.
|
|
139
|
-
* @param lang Dialog language.
|
|
140
|
-
*/
|
|
141
|
-
export function skillEditLine(action, title, skillKind, lang) {
|
|
142
|
-
if (lang === "zh") {
|
|
143
|
-
const form = skillKind === "guidance" ? "guidance 技能(SKILL.md 文档)" : "可执行技能";
|
|
144
|
-
return `- ${action}「${title}」(${form})`;
|
|
145
|
-
}
|
|
146
|
-
const form = skillKind === "guidance" ? "guidance skill (SKILL.md document)" : "executable skill";
|
|
147
|
-
return `- ${action} "${title}" (${form})`;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Resolve the dialog language for a call site that holds no explicit
|
|
151
|
-
* choice: durable client preference first, `en` otherwise. (Dialogs carry
|
|
152
|
-
* no trajectory text, so there is no detection tier here.)
|
|
153
|
-
*
|
|
154
|
-
* @param ctx Host context for the durable-preference read.
|
|
155
|
-
* @returns The dialog language.
|
|
156
|
-
*/
|
|
157
|
-
export function resolveDialogLanguage(ctx) {
|
|
158
|
-
return resolveRecordLanguage({ ctx });
|
|
159
|
-
}
|
|
160
58
|
//# sourceMappingURL=copy.js.map
|
package/lib/declines.d.ts
CHANGED
|
@@ -97,5 +97,7 @@ export interface DeclinedCheckpointHit {
|
|
|
97
97
|
* @param checkpointTokens - tokens of the pending checkpoint trajectory.
|
|
98
98
|
* @returns the strongest hit, or undefined when nothing clears the floor.
|
|
99
99
|
*/
|
|
100
|
+
/** Convenience: load the declined ledger and match one trajectory slice. */
|
|
101
|
+
export declare function matchDeclinedTrajectory(baseDir: string, trajectory: string): DeclinedCheckpointHit | undefined;
|
|
100
102
|
export declare function matchDeclinedCheckpoint(entries: readonly DeclinedMemoryBatch[], checkpointTokens: readonly string[]): DeclinedCheckpointHit | undefined;
|
|
101
103
|
//# sourceMappingURL=declines.d.ts.map
|
package/lib/declines.js
CHANGED
|
@@ -151,6 +151,10 @@ export function declinedContentTokens(edits) {
|
|
|
151
151
|
* @param checkpointTokens - tokens of the pending checkpoint trajectory.
|
|
152
152
|
* @returns the strongest hit, or undefined when nothing clears the floor.
|
|
153
153
|
*/
|
|
154
|
+
/** Convenience: load the declined ledger and match one trajectory slice. */
|
|
155
|
+
export function matchDeclinedTrajectory(baseDir, trajectory) {
|
|
156
|
+
return matchDeclinedCheckpoint(loadDeclinedMemory(baseDir), tokenize(trajectory));
|
|
157
|
+
}
|
|
154
158
|
export function matchDeclinedCheckpoint(entries, checkpointTokens) {
|
|
155
159
|
const pending = new Set(checkpointTokens);
|
|
156
160
|
let best;
|
package/lib/evolve-event.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { DEFAULT_REVIEWS_RETAIN
|
|
14
|
+
import { DEFAULT_REVIEWS_RETAIN } from "./types.js";
|
|
15
|
+
import { pruneJsonlFile, reviewsPath } from "./store.js";
|
|
15
16
|
/** Build a structured evolve-complete event from a refinement result. */
|
|
16
17
|
export function buildEvolveCompleteEvent(result, trigger, sessionId) {
|
|
17
18
|
return {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dedicated memory extraction phase: one bounded ZCode-style loop over a
|
|
3
|
+
* scheduler snapshot. Owns its request loop, frozen manifest, scope approval
|
|
4
|
+
* bookkeeping, and the phase-specific memory checkpoint that advances after a
|
|
5
|
+
* successful/no-op outcome — memory failures leave the boundary retryable.
|
|
6
|
+
*
|
|
7
|
+
* Triggered only by the low-frequency moments wired in `listener.ts`;
|
|
8
|
+
* per-turn extraction was removed with the 2026-10-03 B verdict.
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
11
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
12
|
+
import type { EvolutionEngine } from "./service.js";
|
|
13
|
+
import { type TurnSnapshot } from "./turn-snapshot.js";
|
|
14
|
+
import type { HarnessState, ScopeApprovalDecision } from "./types.js";
|
|
15
|
+
import type { PlannerPrefixCacheMode } from "./types.js";
|
|
16
|
+
import { type RecordLanguagePreference } from "./record-language.js";
|
|
17
|
+
export interface GateState {
|
|
18
|
+
turns: number;
|
|
19
|
+
/** Latest successful turn boundary seen by the listener. */
|
|
20
|
+
completedTurn: number;
|
|
21
|
+
/** Latest successful/no-op memory boundary. */
|
|
22
|
+
memoryCheckpoint?: string;
|
|
23
|
+
/** Per-boundary/scope approval decisions retained until the memory phase settles. */
|
|
24
|
+
memoryDecisions: Record<string, ScopeApprovalDecision>;
|
|
25
|
+
lastReviewAt: number;
|
|
26
|
+
/**
|
|
27
|
+
* Consecutive idle probes that observed the goal phase "blocked".
|
|
28
|
+
* Reset to 0 by any non-blocked probe and after a triggered moment.
|
|
29
|
+
*/
|
|
30
|
+
goalBlockStreak: number;
|
|
31
|
+
}
|
|
32
|
+
export interface AutoReviewConfig {
|
|
33
|
+
/** Initial runtime state when no runtime.json exists. */
|
|
34
|
+
enabledByDefault?: boolean;
|
|
35
|
+
/** ZCode-style minimum lexical words in one direct user text part. */
|
|
36
|
+
memoryMinUserWords?: number;
|
|
37
|
+
/** Trajectory slice handed to the extractor, in characters. */
|
|
38
|
+
maxInputChars: number;
|
|
39
|
+
/** Output budget for the extraction loop. */
|
|
40
|
+
budgetTokens: number;
|
|
41
|
+
/** Queue a visible receipt follow-up after an applied goal-blocked moment. */
|
|
42
|
+
notifyOnAutoReview: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Optional model override for the extractor (cheaper model).
|
|
45
|
+
* Format: "provider/model" or just "model" (same provider as the agent).
|
|
46
|
+
* When absent, the extractor uses the agent's own provider/model.
|
|
47
|
+
*/
|
|
48
|
+
reviewModel?: string;
|
|
49
|
+
/** Dedicated memory-agent writes to project/global require the normal human approval boundary. */
|
|
50
|
+
requireGlobalApproval?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Authoring language for this run's records. Absent/`auto` resolves per
|
|
53
|
+
* call through the record-language chain (durable client preference,
|
|
54
|
+
* then trajectory detection, then `en`); `zh`/`en` pin it.
|
|
55
|
+
*/
|
|
56
|
+
recordLanguage?: RecordLanguagePreference;
|
|
57
|
+
/**
|
|
58
|
+
* Prefix-cache routing for the extraction input. Absent mode → auto-detect;
|
|
59
|
+
* absent budget → the default prefix budget.
|
|
60
|
+
*/
|
|
61
|
+
prefixCacheMode?: PlannerPrefixCacheMode;
|
|
62
|
+
prefixMaxChars?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Goal-blocked trigger: after this many CONSECUTIVE idle probes that
|
|
65
|
+
* observe the session goal in phase "blocked", run one memory extraction
|
|
66
|
+
* so the blocked encounter is distilled. 0 disables. The streak resets on
|
|
67
|
+
* any non-blocked probe and after each triggered moment.
|
|
68
|
+
*/
|
|
69
|
+
goalBlockedWrapupTurns: number;
|
|
70
|
+
/**
|
|
71
|
+
* Storage hygiene (#20): tail lines kept in the shared reviews.jsonl
|
|
72
|
+
* audit trail. Absent → the store default (readers need a recent
|
|
73
|
+
* window, not the full past).
|
|
74
|
+
*/
|
|
75
|
+
reviewsRetain?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Session-close bounded drain: how long a disposed session's in-flight
|
|
78
|
+
* extraction may settle before its owner signal aborts. Absent → 15s;
|
|
79
|
+
* 0 restores the legacy immediate abort.
|
|
80
|
+
*/
|
|
81
|
+
sessionCloseDrainMs?: number;
|
|
82
|
+
}
|
|
83
|
+
export interface ReviewRecord {
|
|
84
|
+
timestamp: string;
|
|
85
|
+
sessionId: string;
|
|
86
|
+
reason: TurnSnapshot["reason"];
|
|
87
|
+
turnsSinceLastReview: number;
|
|
88
|
+
outcome: "approved" | "declined" | "failed" | "assessed" | "deferred" | "skipped" | "armed" | "applied" | "noop";
|
|
89
|
+
rationale?: string;
|
|
90
|
+
refinementId?: string;
|
|
91
|
+
/** P1 extraction stats (§6.7): wall time of the memory phase. */
|
|
92
|
+
durationMs?: number;
|
|
93
|
+
/** Extractor loop turns / manifest searches behind this row. */
|
|
94
|
+
memoryTurns?: number;
|
|
95
|
+
memorySearches?: number;
|
|
96
|
+
/** Applied edit count carried by an applied row. */
|
|
97
|
+
appliedEdits?: number;
|
|
98
|
+
}
|
|
99
|
+
export type ReviewRecorder = (entry: Omit<ReviewRecord, "timestamp">) => void;
|
|
100
|
+
export declare function stateFor(map: Map<string, GateState>, sessionId: string): GateState;
|
|
101
|
+
export declare function advanceMemoryCheckpoint(state: GateState, next: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* The state view the extractor judges: the session's local entries merged
|
|
104
|
+
* with the project and global stores, each entry carrying its real scope.
|
|
105
|
+
* Without the global half the extractor cannot see that a topic is already
|
|
106
|
+
* covered cross-session and happily re-sediments a local duplicate of it.
|
|
107
|
+
*
|
|
108
|
+
* The merged view is read-only context — applying still targets the raw
|
|
109
|
+
* local state (baseline checks compare local entries only).
|
|
110
|
+
*/
|
|
111
|
+
export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId: string, opts?: {
|
|
112
|
+
projectKey?: string;
|
|
113
|
+
}): HarnessState;
|
|
114
|
+
/**
|
|
115
|
+
* Gap C1: parse a "provider/model" or "model" string into its components.
|
|
116
|
+
* Returns undefined when the input is empty (no override). A bare model name
|
|
117
|
+
* falls back to the agent's provider, then "deepseek".
|
|
118
|
+
* Exported for unit testing; production resolves it inside
|
|
119
|
+
* runMemoryExtractionPhase.
|
|
120
|
+
*/
|
|
121
|
+
export declare function parseReviewModel(reviewModel: string | undefined, fallbackProvider: string | undefined): {
|
|
122
|
+
provider: string;
|
|
123
|
+
model: string;
|
|
124
|
+
} | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* One extraction run over a scheduler snapshot. Records noop / applied /
|
|
127
|
+
* declined audit rows with duration and loop stats, advances the memory
|
|
128
|
+
* checkpoint on settled outcomes, and queues a visible receipt only for an
|
|
129
|
+
* applied goal-blocked moment (compaction and session-close stay audit-only).
|
|
130
|
+
*/
|
|
131
|
+
export declare function runMemoryExtractionPhase(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, snapshot: TurnSnapshot, record: ReviewRecorder, signal?: AbortSignal): Promise<void>;
|
|
132
|
+
//# sourceMappingURL=extraction-phase.d.ts.map
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { notifyMemoryExtraction } from "./notify.js";
|
|
2
|
+
import { mergeHarnessStates } from "./state.js";
|
|
3
|
+
import { projectKeyOf } from "./project.js";
|
|
4
|
+
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
5
|
+
import { compareReviewCursors, sliceTurnSnapshot } from "./turn-snapshot.js";
|
|
6
|
+
import { matchDeclinedTrajectory } from "./declines.js";
|
|
7
|
+
import { runMemoryAgent, } from "./memory-agent.js";
|
|
8
|
+
import { applyMemoryExtractionProposal } from "./memory-apply.js";
|
|
9
|
+
import { buildMemoryManifest } from "./memory-manifest.js";
|
|
10
|
+
import { DEFAULT_REVIEWS_RETAIN } from "./types.js";
|
|
11
|
+
import { resolveRecordLanguage } from "./record-language.js";
|
|
12
|
+
export function stateFor(map, sessionId) {
|
|
13
|
+
let state = map.get(sessionId);
|
|
14
|
+
if (!state) {
|
|
15
|
+
state = {
|
|
16
|
+
turns: 0,
|
|
17
|
+
completedTurn: 0,
|
|
18
|
+
memoryDecisions: {},
|
|
19
|
+
lastReviewAt: 0,
|
|
20
|
+
goalBlockStreak: 0,
|
|
21
|
+
};
|
|
22
|
+
map.set(sessionId, state);
|
|
23
|
+
}
|
|
24
|
+
return state;
|
|
25
|
+
}
|
|
26
|
+
export function advanceMemoryCheckpoint(state, next) {
|
|
27
|
+
if (state.memoryCheckpoint === undefined) {
|
|
28
|
+
state.memoryCheckpoint = next;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const order = compareReviewCursors(next, state.memoryCheckpoint);
|
|
32
|
+
if (order === undefined || order > 0)
|
|
33
|
+
state.memoryCheckpoint = next;
|
|
34
|
+
}
|
|
35
|
+
function rememberMemoryDecision(state, key, decision) {
|
|
36
|
+
state.memoryDecisions[key] = decision;
|
|
37
|
+
const keys = Object.keys(state.memoryDecisions);
|
|
38
|
+
while (keys.length > 128)
|
|
39
|
+
delete state.memoryDecisions[keys.shift() ?? ""];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The state view the extractor judges: the session's local entries merged
|
|
43
|
+
* with the project and global stores, each entry carrying its real scope.
|
|
44
|
+
* Without the global half the extractor cannot see that a topic is already
|
|
45
|
+
* covered cross-session and happily re-sediments a local duplicate of it.
|
|
46
|
+
*
|
|
47
|
+
* The merged view is read-only context — applying still targets the raw
|
|
48
|
+
* local state (baseline checks compare local entries only).
|
|
49
|
+
*/
|
|
50
|
+
export function loadGateHarnessView(engine, sessionId, opts) {
|
|
51
|
+
const projectState = opts?.projectKey ? engine.load("project", opts.projectKey) : undefined;
|
|
52
|
+
return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId), projectState ? { projectState } : undefined);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Gap C1: parse a "provider/model" or "model" string into its components.
|
|
56
|
+
* Returns undefined when the input is empty (no override). A bare model name
|
|
57
|
+
* falls back to the agent's provider, then "deepseek".
|
|
58
|
+
* Exported for unit testing; production resolves it inside
|
|
59
|
+
* runMemoryExtractionPhase.
|
|
60
|
+
*/
|
|
61
|
+
export function parseReviewModel(reviewModel, fallbackProvider) {
|
|
62
|
+
if (!reviewModel || reviewModel.trim().length === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
const slash = reviewModel.indexOf("/");
|
|
65
|
+
if (slash > 0) {
|
|
66
|
+
return { provider: reviewModel.slice(0, slash), model: reviewModel.slice(slash + 1) };
|
|
67
|
+
}
|
|
68
|
+
return { provider: fallbackProvider ?? "deepseek", model: reviewModel };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* One extraction run over a scheduler snapshot. Records noop / applied /
|
|
72
|
+
* declined audit rows with duration and loop stats, advances the memory
|
|
73
|
+
* checkpoint on settled outcomes, and queues a visible receipt only for an
|
|
74
|
+
* applied goal-blocked moment (compaction and session-close stay audit-only).
|
|
75
|
+
*/
|
|
76
|
+
export async function runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
|
|
77
|
+
const sessionId = agent.id;
|
|
78
|
+
const turnsSinceLastReview = state.turns - state.lastReviewAt;
|
|
79
|
+
// This invocation becomes the new reference point for the next audit row.
|
|
80
|
+
state.lastReviewAt = state.turns;
|
|
81
|
+
const startedAt = Date.now();
|
|
82
|
+
const memorySnapshot = state.memoryCheckpoint ? sliceTurnSnapshot(snapshot, state.memoryCheckpoint) : snapshot;
|
|
83
|
+
if (!memorySnapshot.eligible || !memorySnapshot.trajectory) {
|
|
84
|
+
advanceMemoryCheckpoint(state, snapshot.cursor);
|
|
85
|
+
ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${agent.id}]: no new eligible evidence after checkpoint`);
|
|
86
|
+
record({
|
|
87
|
+
sessionId,
|
|
88
|
+
reason: snapshot.reason,
|
|
89
|
+
turnsSinceLastReview,
|
|
90
|
+
outcome: "noop",
|
|
91
|
+
rationale: `memory no-op (${snapshot.reason}): no new eligible evidence after checkpoint`,
|
|
92
|
+
durationMs: Date.now() - startedAt,
|
|
93
|
+
memoryTurns: 0,
|
|
94
|
+
memorySearches: 0,
|
|
95
|
+
appliedEdits: 0,
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const declinedHit = matchDeclinedTrajectory(engine.baseDir, memorySnapshot.trajectory);
|
|
100
|
+
if (declinedHit !== undefined) {
|
|
101
|
+
advanceMemoryCheckpoint(state, memorySnapshot.cursor);
|
|
102
|
+
ctx.logger("continual-evolve").info(`memory agent pre-suppressed (${snapshot.reason}) [${agent.id}]: checkpoint overlaps declined ${declinedHit.entry.scope}:${declinedHit.entry.fingerprint} (coverage ${(declinedHit.coverage * 100).toFixed(0)}%, ${declinedHit.hits} tokens) — agent not invoked`);
|
|
103
|
+
record({
|
|
104
|
+
sessionId,
|
|
105
|
+
reason: snapshot.reason,
|
|
106
|
+
turnsSinceLastReview,
|
|
107
|
+
outcome: "noop",
|
|
108
|
+
rationale: `memory pre-suppressed (${snapshot.reason}): checkpoint overlaps declined ${declinedHit.entry.scope}:${declinedHit.entry.fingerprint} — agent not invoked`,
|
|
109
|
+
durationMs: Date.now() - startedAt,
|
|
110
|
+
memoryTurns: 0,
|
|
111
|
+
memorySearches: 0,
|
|
112
|
+
appliedEdits: 0,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const projectKey = snapshot.projectKey ?? projectKeyOf(agent);
|
|
117
|
+
const baselines = {
|
|
118
|
+
local: engine.load("local", sessionId),
|
|
119
|
+
global: engine.load("global", undefined),
|
|
120
|
+
...(projectKey ? { project: engine.load("project", projectKey) } : {}),
|
|
121
|
+
};
|
|
122
|
+
const manifest = buildMemoryManifest(loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined));
|
|
123
|
+
const overrideRoute = parseReviewModel(config.reviewModel, agent.options.provider);
|
|
124
|
+
const provider = overrideRoute?.provider ?? agent.options.provider;
|
|
125
|
+
const model = overrideRoute?.model ?? agent.options.model;
|
|
126
|
+
if (!provider || !model)
|
|
127
|
+
throw new Error("evolve: no provider/model route for the memory extraction agent");
|
|
128
|
+
const tokenUsage = {
|
|
129
|
+
baseDir: engine.baseDir,
|
|
130
|
+
sessionId,
|
|
131
|
+
retain: engine.retention.tokenUsage,
|
|
132
|
+
onError: (cause) => ctx
|
|
133
|
+
.logger("continual-evolve")
|
|
134
|
+
.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
135
|
+
};
|
|
136
|
+
const run = await runMemoryAgent(ctx, {
|
|
137
|
+
provider,
|
|
138
|
+
model,
|
|
139
|
+
sessionId,
|
|
140
|
+
manifest,
|
|
141
|
+
trajectory: memorySnapshot.trajectory,
|
|
142
|
+
trajectoryEvents: memorySnapshot.events,
|
|
143
|
+
maxOutputTokens: config.budgetTokens,
|
|
144
|
+
...(signal ? { signal } : {}),
|
|
145
|
+
tokenUsage,
|
|
146
|
+
language: resolveRecordLanguage({ configured: config.recordLanguage, ctx, trajectoryText: memorySnapshot.trajectory }),
|
|
147
|
+
...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
|
|
148
|
+
? {
|
|
149
|
+
prefixCache: {
|
|
150
|
+
...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
|
|
151
|
+
...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
: {})),
|
|
155
|
+
});
|
|
156
|
+
if (run.proposal.edits.length === 0) {
|
|
157
|
+
advanceMemoryCheckpoint(state, memorySnapshot.cursor);
|
|
158
|
+
ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${sessionId}] after ${run.turns} turn(s): ${run.proposal.rationale}`);
|
|
159
|
+
record({
|
|
160
|
+
sessionId,
|
|
161
|
+
reason: snapshot.reason,
|
|
162
|
+
turnsSinceLastReview,
|
|
163
|
+
outcome: "noop",
|
|
164
|
+
rationale: `memory no-op (${snapshot.reason}) after ${run.turns} turn(s): ${run.proposal.rationale}`,
|
|
165
|
+
durationMs: Date.now() - startedAt,
|
|
166
|
+
memoryTurns: run.turns,
|
|
167
|
+
memorySearches: run.searches,
|
|
168
|
+
appliedEdits: 0,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const source = { sessionId, ...(memorySnapshot.sourceSeqs.length > 0 ? { seqs: [...memorySnapshot.sourceSeqs] } : {}) };
|
|
173
|
+
const application = await applyMemoryExtractionProposal(ctx, engine, run.proposal, {
|
|
174
|
+
agent,
|
|
175
|
+
baselines,
|
|
176
|
+
...(projectKey ? { projectKey } : {}),
|
|
177
|
+
requireApproval: config.requireGlobalApproval ?? true,
|
|
178
|
+
source,
|
|
179
|
+
decisionCursor: memorySnapshot.cursor,
|
|
180
|
+
scopeDecisions: state.memoryDecisions,
|
|
181
|
+
onScopeDecision: (key, decision) => rememberMemoryDecision(state, key, decision),
|
|
182
|
+
...(config.recordLanguage !== undefined ? { recordLanguage: config.recordLanguage } : {}),
|
|
183
|
+
...(signal ? { signal } : {}),
|
|
184
|
+
});
|
|
185
|
+
for (const result of application.results) {
|
|
186
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `memory_agent:${snapshot.reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
187
|
+
}
|
|
188
|
+
state.memoryCheckpoint = memorySnapshot.cursor;
|
|
189
|
+
const applied = application.results.reduce((count, result) => count + result.appliedEdits.filter((edit) => edit.applied).length, 0);
|
|
190
|
+
const declined = application.declinedScopes.length > 0 ? `; declined scopes=${application.declinedScopes.join(",")}` : "";
|
|
191
|
+
const durationMs = Date.now() - startedAt;
|
|
192
|
+
for (const result of application.results) {
|
|
193
|
+
record({
|
|
194
|
+
sessionId,
|
|
195
|
+
reason: snapshot.reason,
|
|
196
|
+
turnsSinceLastReview,
|
|
197
|
+
outcome: "applied",
|
|
198
|
+
rationale: `memory applied (${snapshot.reason}) after ${run.turns} turn(s): ${run.proposal.rationale}${declined}`,
|
|
199
|
+
refinementId: result.id,
|
|
200
|
+
durationMs,
|
|
201
|
+
memoryTurns: run.turns,
|
|
202
|
+
memorySearches: run.searches,
|
|
203
|
+
appliedEdits: result.appliedEdits.filter((edit) => edit.applied).length,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (application.declinedScopes.length > 0) {
|
|
207
|
+
record({
|
|
208
|
+
sessionId,
|
|
209
|
+
reason: snapshot.reason,
|
|
210
|
+
turnsSinceLastReview,
|
|
211
|
+
outcome: "declined",
|
|
212
|
+
rationale: `memory scopes declined (${snapshot.reason}): ${application.declinedScopes.join(",")}`,
|
|
213
|
+
durationMs,
|
|
214
|
+
memoryTurns: run.turns,
|
|
215
|
+
memorySearches: run.searches,
|
|
216
|
+
appliedEdits: 0,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
ctx.logger("continual-evolve").info(`memory agent applied ${applied} edit(s) across ${application.results.length} scope batch(es) [${sessionId}] after ${run.turns} turn(s)${declined}: ${run.proposal.rationale}`);
|
|
220
|
+
// Visibility (P1 unified receipt): only applied outcomes wake the agent
|
|
221
|
+
// with a follow-up — and only on the goal-blocked moment, which runs
|
|
222
|
+
// mid-session; compaction and session-close stay audit-only
|
|
223
|
+
// (reviews.jsonl) so they never wake the agent mid-compaction or after
|
|
224
|
+
// disposal.
|
|
225
|
+
if (config.notifyOnAutoReview && snapshot.reason === "goal_blocked" && applied > 0) {
|
|
226
|
+
notifyMemoryExtraction(ctx, agent, {
|
|
227
|
+
outcome: "applied",
|
|
228
|
+
results: application.results,
|
|
229
|
+
...(application.declinedScopes.length > 0 ? { declinedScopes: [...application.declinedScopes] } : {}),
|
|
230
|
+
turns: run.turns,
|
|
231
|
+
searches: run.searches,
|
|
232
|
+
durationMs,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=extraction-phase.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -19,9 +19,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
19
19
|
* Absent → 15s; 0 restores the legacy immediate abort on dispose.
|
|
20
20
|
*/
|
|
21
21
|
sessionCloseDrainMs: z<number, number, "defined">;
|
|
22
|
-
/**
|
|
23
|
-
reviewIntervalTurns: z<number, number, "defined">;
|
|
24
|
-
/** Trajectory slice handed to the gate, in characters. */
|
|
22
|
+
/** Trajectory slice handed to the extractor, in characters. */
|
|
25
23
|
maxReviewInputChars: z<number, number, "defined">;
|
|
26
24
|
/** Output budget for the cheap gate call. */
|
|
27
25
|
reviewBudgetTokens: z<number, number, "defined">;
|
|
@@ -31,7 +29,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
31
29
|
* cache-read evidence (dropping the redundant flat trajectory text),
|
|
32
30
|
* `session` always prefixes, `off` keeps the legacy flat-text input.
|
|
33
31
|
*/
|
|
34
|
-
plannerPrefixCache: z<"session" | "
|
|
32
|
+
plannerPrefixCache: z<"session" | "auto" | "off", "session" | "auto" | "off", "defined">;
|
|
35
33
|
/** Session-prefix budget for Route A planning inputs, in characters. */
|
|
36
34
|
plannerPrefixMaxChars: z<number, number, "defined">;
|
|
37
35
|
/** After an approved gate run with applied edits, queue a visible follow-up notice. */
|
|
@@ -44,7 +42,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
44
42
|
* `auto` follows the DSH client language (durable `locale.preference`,
|
|
45
43
|
* then trajectory detection, then `en`); `zh`/`en` pin it.
|
|
46
44
|
*/
|
|
47
|
-
recordLanguage: z<"
|
|
45
|
+
recordLanguage: z<"auto" | "zh" | "en", "auto" | "zh" | "en", "defined">;
|
|
48
46
|
/** Skills root for materialized skill entries; defaults to <dshHome>/skills. */
|
|
49
47
|
skillsDir: z<string, string, "plain">;
|
|
50
48
|
/** Passphrase for rubric encryption; falls back to DSH_EVOLVE_RUBRIC_KEY, then a local key file. */
|
|
@@ -64,29 +62,23 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
64
62
|
*/
|
|
65
63
|
autoCase: z<boolean, boolean, "defined">;
|
|
66
64
|
/**
|
|
67
|
-
*
|
|
65
|
+
* Optional model override for the memory extractor (cheaper model).
|
|
68
66
|
* Format: "provider/model" or just "model" (same provider as the agent).
|
|
69
|
-
* When absent, the
|
|
67
|
+
* When absent, the extractor uses the agent's own provider/model.
|
|
70
68
|
*/
|
|
71
69
|
reviewModel: z<string, string, "plain">;
|
|
72
70
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* disabled by default even when the automatic pipeline is explicitly enabled.
|
|
71
|
+
* Goal-blocked moment: after this many CONSECUTIVE idle probes that
|
|
72
|
+
* observe the session goal in phase "blocked", run one memory extraction
|
|
73
|
+
* so the stuck encounter is distilled. 0 disables.
|
|
77
74
|
*/
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Minimum turns between local-fate assessments on the turn-interval path
|
|
81
|
-
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
82
|
-
*/
|
|
83
|
-
fateIntervalTurns: z<number, number, "plain">;
|
|
75
|
+
goalBlockedWrapupTurns: z<number, number, "defined">;
|
|
84
76
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
77
|
+
* Promotion policy (2026-08-22): regex sources whose match in a
|
|
78
|
+
* candidate's title/content marks it project-scoped — such entries are
|
|
79
|
+
* never promoted to the cross-session global store. Replaces the built-in
|
|
80
|
+
* defaults when set.
|
|
88
81
|
*/
|
|
89
|
-
goalBlockedWrapupTurns: z<number, number, "defined">;
|
|
90
82
|
/**
|
|
91
83
|
* Promotion policy (2026-08-22): regex sources whose match in a
|
|
92
84
|
* candidate's title/content marks it project-scoped — such entries are
|
|
@@ -149,9 +141,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
149
141
|
* Absent → 15s; 0 restores the legacy immediate abort on dispose.
|
|
150
142
|
*/
|
|
151
143
|
sessionCloseDrainMs: z<number, number, "defined">;
|
|
152
|
-
/**
|
|
153
|
-
reviewIntervalTurns: z<number, number, "defined">;
|
|
154
|
-
/** Trajectory slice handed to the gate, in characters. */
|
|
144
|
+
/** Trajectory slice handed to the extractor, in characters. */
|
|
155
145
|
maxReviewInputChars: z<number, number, "defined">;
|
|
156
146
|
/** Output budget for the cheap gate call. */
|
|
157
147
|
reviewBudgetTokens: z<number, number, "defined">;
|
|
@@ -161,7 +151,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
161
151
|
* cache-read evidence (dropping the redundant flat trajectory text),
|
|
162
152
|
* `session` always prefixes, `off` keeps the legacy flat-text input.
|
|
163
153
|
*/
|
|
164
|
-
plannerPrefixCache: z<"session" | "
|
|
154
|
+
plannerPrefixCache: z<"session" | "auto" | "off", "session" | "auto" | "off", "defined">;
|
|
165
155
|
/** Session-prefix budget for Route A planning inputs, in characters. */
|
|
166
156
|
plannerPrefixMaxChars: z<number, number, "defined">;
|
|
167
157
|
/** After an approved gate run with applied edits, queue a visible follow-up notice. */
|
|
@@ -174,7 +164,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
174
164
|
* `auto` follows the DSH client language (durable `locale.preference`,
|
|
175
165
|
* then trajectory detection, then `en`); `zh`/`en` pin it.
|
|
176
166
|
*/
|
|
177
|
-
recordLanguage: z<"
|
|
167
|
+
recordLanguage: z<"auto" | "zh" | "en", "auto" | "zh" | "en", "defined">;
|
|
178
168
|
/** Skills root for materialized skill entries; defaults to <dshHome>/skills. */
|
|
179
169
|
skillsDir: z<string, string, "plain">;
|
|
180
170
|
/** Passphrase for rubric encryption; falls back to DSH_EVOLVE_RUBRIC_KEY, then a local key file. */
|
|
@@ -194,29 +184,23 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
194
184
|
*/
|
|
195
185
|
autoCase: z<boolean, boolean, "defined">;
|
|
196
186
|
/**
|
|
197
|
-
*
|
|
187
|
+
* Optional model override for the memory extractor (cheaper model).
|
|
198
188
|
* Format: "provider/model" or just "model" (same provider as the agent).
|
|
199
|
-
* When absent, the
|
|
189
|
+
* When absent, the extractor uses the agent's own provider/model.
|
|
200
190
|
*/
|
|
201
191
|
reviewModel: z<string, string, "plain">;
|
|
202
192
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* disabled by default even when the automatic pipeline is explicitly enabled.
|
|
193
|
+
* Goal-blocked moment: after this many CONSECUTIVE idle probes that
|
|
194
|
+
* observe the session goal in phase "blocked", run one memory extraction
|
|
195
|
+
* so the stuck encounter is distilled. 0 disables.
|
|
207
196
|
*/
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Minimum turns between local-fate assessments on the turn-interval path
|
|
211
|
-
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
212
|
-
*/
|
|
213
|
-
fateIntervalTurns: z<number, number, "plain">;
|
|
197
|
+
goalBlockedWrapupTurns: z<number, number, "defined">;
|
|
214
198
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
199
|
+
* Promotion policy (2026-08-22): regex sources whose match in a
|
|
200
|
+
* candidate's title/content marks it project-scoped — such entries are
|
|
201
|
+
* never promoted to the cross-session global store. Replaces the built-in
|
|
202
|
+
* defaults when set.
|
|
218
203
|
*/
|
|
219
|
-
goalBlockedWrapupTurns: z<number, number, "defined">;
|
|
220
204
|
/**
|
|
221
205
|
* Promotion policy (2026-08-22): regex sources whose match in a
|
|
222
206
|
* candidate's title/content marks it project-scoped — such entries are
|