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/service.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Every mutation path goes through here so snapshot-before-write, apply
|
|
4
4
|
* accounting, persistence, and result history are enforced in one place.
|
|
5
5
|
*/
|
|
6
|
-
import type { EntrySource, HarnessScope, RefinementProposal, RefinementResult } from "./types.js";
|
|
6
|
+
import type { EntrySource, HarnessScope, HarnessState, RefinementProposal, RefinementResult } from "./types.js";
|
|
7
7
|
import { applyRefinementProposal } from "./apply.js";
|
|
8
8
|
import type { HistoryRetention } from "./store.js";
|
|
9
9
|
/** A post-commit apply failure that still carries the durable applied result. */
|
|
@@ -37,13 +37,27 @@ export interface EvolutionEngineOptions {
|
|
|
37
37
|
historyRetain?: Partial<HistoryRetention>;
|
|
38
38
|
}
|
|
39
39
|
export declare function createEvolutionEngine(baseDir: string, hooks?: EvolutionHooks, opts?: EvolutionEngineOptions): {
|
|
40
|
-
load: (scope: HarnessScope, sessionId: string | undefined) =>
|
|
40
|
+
load: (scope: HarnessScope, sessionId: string | undefined) => HarnessState;
|
|
41
41
|
apply: (scope: HarnessScope, sessionId: string | undefined, proposal: RefinementProposal, context?: ApplyContext) => RefinementResult;
|
|
42
42
|
rollback: (scope: HarnessScope, sessionId: string | undefined, refinementId: string) => RefinementResult;
|
|
43
43
|
rollbackResult: (scope: HarnessScope, sessionId: string | undefined, target: RefinementResult) => RefinementResult;
|
|
44
44
|
history: (scope: HarnessScope, sessionId: string | undefined) => RefinementResult[];
|
|
45
|
+
exportStore: (scope: HarnessScope, sessionId: string | undefined) => ExportedStore;
|
|
46
|
+
importStore: (scope: HarnessScope, sessionId: string | undefined, payload: unknown) => {
|
|
47
|
+
entries: number;
|
|
48
|
+
refinements: number;
|
|
49
|
+
};
|
|
45
50
|
baseDir: string;
|
|
46
51
|
retention: HistoryRetention;
|
|
47
52
|
};
|
|
53
|
+
/** The on-disk shape `/evolve export` writes and `/evolve import` reads. */
|
|
54
|
+
export interface ExportedStore {
|
|
55
|
+
version: number;
|
|
56
|
+
scope: HarnessScope;
|
|
57
|
+
schema: number;
|
|
58
|
+
entries: HarnessState["entries"];
|
|
59
|
+
refinements: HarnessState["refinements"];
|
|
60
|
+
history: RefinementResult[];
|
|
61
|
+
}
|
|
48
62
|
export type EvolutionEngine = ReturnType<typeof createEvolutionEngine>;
|
|
49
63
|
//# sourceMappingURL=service.d.ts.map
|
package/lib/service.js
CHANGED
|
@@ -175,6 +175,79 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
|
|
|
175
175
|
function history(scope, sessionId) {
|
|
176
176
|
return loadResults(storePaths(baseDir, scope, sessionId));
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
/** Serialize one store (entries + refinement history) for `/evolve export`. */
|
|
179
|
+
function exportStore(scope, sessionId) {
|
|
180
|
+
const state = load(scope, sessionId);
|
|
181
|
+
return {
|
|
182
|
+
version: 1,
|
|
183
|
+
scope,
|
|
184
|
+
schema: state.schema,
|
|
185
|
+
entries: state.entries,
|
|
186
|
+
refinements: state.refinements,
|
|
187
|
+
history: history(scope, sessionId),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Replace one store from an export payload (raw state write — the same
|
|
192
|
+
* semantics the command always had: no snapshot, no per-edit validation,
|
|
193
|
+
* the payload is trusted because a human handed over the file).
|
|
194
|
+
*/
|
|
195
|
+
function importStore(scope, sessionId, payload) {
|
|
196
|
+
if (!isValidExport(payload)) {
|
|
197
|
+
throw new Error("invalid export file shape: expected {version, entries: {prompt, memory, skill, subagent}, refinements, history}");
|
|
198
|
+
}
|
|
199
|
+
const entriesBlock = payload["entries"];
|
|
200
|
+
const state = {
|
|
201
|
+
schema: typeof payload["schema"] === "number" ? payload["schema"] : 1,
|
|
202
|
+
entries: {
|
|
203
|
+
prompt: toEntryRecord(entriesBlock["prompt"]),
|
|
204
|
+
memory: toEntryRecord(entriesBlock["memory"]),
|
|
205
|
+
skill: toEntryRecord(entriesBlock["skill"]),
|
|
206
|
+
subagent: toEntryRecord(entriesBlock["subagent"]),
|
|
207
|
+
},
|
|
208
|
+
refinements: Array.isArray(payload["refinements"]) ? payload["refinements"] : [],
|
|
209
|
+
};
|
|
210
|
+
const paths = storePaths(baseDir, scope, sessionId);
|
|
211
|
+
saveHarnessState(paths.stateDir, state);
|
|
212
|
+
let refinements = 0;
|
|
213
|
+
if (Array.isArray(payload["history"])) {
|
|
214
|
+
for (const result of payload["history"]) {
|
|
215
|
+
if (isResultRecord(result)) {
|
|
216
|
+
appendResult(paths, result);
|
|
217
|
+
refinements += 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Storage hygiene (#20): an imported history obeys the same tail
|
|
221
|
+
// budget as a live one.
|
|
222
|
+
try {
|
|
223
|
+
pruneJsonlFile(paths.resultsPath, retention.refinements);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// ignored — the next apply retries
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const entries = Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
|
|
230
|
+
return { entries, refinements };
|
|
231
|
+
}
|
|
232
|
+
return { load, apply, rollback, rollbackResult, history, exportStore, importStore, baseDir, retention };
|
|
233
|
+
}
|
|
234
|
+
function toEntryRecord(value) {
|
|
235
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
236
|
+
return {};
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
function isValidExport(payload) {
|
|
241
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload))
|
|
242
|
+
return false;
|
|
243
|
+
const record = payload;
|
|
244
|
+
const entries = record["entries"];
|
|
245
|
+
if (typeof entries !== "object" || entries === null || Array.isArray(entries))
|
|
246
|
+
return false;
|
|
247
|
+
const kinds = ["prompt", "memory", "skill", "subagent"];
|
|
248
|
+
return kinds.every((kind) => Object.prototype.hasOwnProperty.call(entries, kind));
|
|
249
|
+
}
|
|
250
|
+
function isResultRecord(value) {
|
|
251
|
+
return typeof value === "object" && value !== null && "id" in value && "appliedEdits" in value;
|
|
179
252
|
}
|
|
180
253
|
//# sourceMappingURL=service.js.map
|
package/lib/store.d.ts
CHANGED
|
@@ -8,8 +8,7 @@ export declare const EVOLVE_DIR = "evolve";
|
|
|
8
8
|
* full past. Tunable via the `historyRetain` plugin config.
|
|
9
9
|
*/
|
|
10
10
|
export declare const DEFAULT_SNAPSHOT_RETAIN = 20;
|
|
11
|
-
export
|
|
12
|
-
export declare const DEFAULT_REVIEWS_RETAIN = 500;
|
|
11
|
+
export { DEFAULT_REFINEMENTS_RETAIN, DEFAULT_REVIEWS_RETAIN } from "./types.js";
|
|
13
12
|
export declare const DEFAULT_TOKEN_USAGE_RETAIN = 500;
|
|
14
13
|
/** Resolved retention set: how many snapshots / JSONL tail lines to keep. */
|
|
15
14
|
export interface HistoryRetention {
|
package/lib/store.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* no way to skip it — it runs inside the service, not in a prompt.
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { DEFAULT_REFINEMENTS_RETAIN, DEFAULT_REVIEWS_RETAIN } from "./types.js";
|
|
17
18
|
import { join } from "node:path";
|
|
18
19
|
import { sanitizeProjectKey } from "./project.js";
|
|
19
20
|
import { stateFilePath } from "./state.js";
|
|
@@ -26,8 +27,7 @@ export const EVOLVE_DIR = "evolve";
|
|
|
26
27
|
* full past. Tunable via the `historyRetain` plugin config.
|
|
27
28
|
*/
|
|
28
29
|
export const DEFAULT_SNAPSHOT_RETAIN = 20;
|
|
29
|
-
export
|
|
30
|
-
export const DEFAULT_REVIEWS_RETAIN = 500;
|
|
30
|
+
export { DEFAULT_REFINEMENTS_RETAIN, DEFAULT_REVIEWS_RETAIN } from "./types.js";
|
|
31
31
|
export const DEFAULT_TOKEN_USAGE_RETAIN = 500;
|
|
32
32
|
/** Defaults applied for absent / non-positive retention fields (fail loud never: clamp, don't throw). */
|
|
33
33
|
export function resolveHistoryRetention(raw) {
|
package/lib/tool.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* the engine. `global: true` is required explicitly for cross-session edits.
|
|
5
5
|
*/
|
|
6
6
|
import type { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
import type { RecordLanguagePreference } from "./record-language.js";
|
|
7
8
|
import type { HarnessScope } from "./types.js";
|
|
8
9
|
import type { EvolutionEngine } from "./service.js";
|
|
9
10
|
/**
|
|
@@ -22,6 +23,12 @@ export declare function scopeOf(value: unknown, fallback: HarnessScope): Harness
|
|
|
22
23
|
export declare function collectDeleteIds(id: unknown, ids: unknown): string[];
|
|
23
24
|
export interface ToolGateOptions {
|
|
24
25
|
requireGlobalApproval: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Plugin `recordLanguage` preference for the approval dialog copy;
|
|
28
|
+
* `auto`/absent defers to the durable client preference and the session's
|
|
29
|
+
* own user text (see `approval.ts`).
|
|
30
|
+
*/
|
|
31
|
+
recordLanguage?: RecordLanguagePreference;
|
|
25
32
|
}
|
|
26
33
|
export declare function registerEvolveTools(ctx: Context, engine: EvolutionEngine, opts: ToolGateOptions): void;
|
|
27
34
|
//# sourceMappingURL=tool.d.ts.map
|
package/lib/tool.js
CHANGED
|
@@ -8,7 +8,7 @@ import { entrySourceOf } from "./source.js";
|
|
|
8
8
|
import { formatRecallResult, recallMemories } from "./recall.js";
|
|
9
9
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
10
10
|
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
11
|
-
import { DEFAULT_REVIEWS_RETAIN } from "./
|
|
11
|
+
import { DEFAULT_REVIEWS_RETAIN } from "./types.js";
|
|
12
12
|
const SCOPES = ["local", "project", "global"];
|
|
13
13
|
/**
|
|
14
14
|
* Accept the string form (`scope: "project"`) with the legacy boolean tool
|
|
@@ -186,7 +186,7 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
186
186
|
const targetState = engine.load(scope, storeId);
|
|
187
187
|
const hit = mostSimilarEntry(Object.values(targetState.entries[args.kind]), args.title ?? "", args.content ?? "", CONFLICT_WARN_SCORE);
|
|
188
188
|
const conflictNote = hit ? ` ⚠️ ${buildConflictNotice(hit)}——建议改用 evolve_update` : "";
|
|
189
|
-
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → ${storeLabel(scope)}${conflictNote}
|
|
189
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → ${storeLabel(scope)}${conflictNote}`, opts.recordLanguage);
|
|
190
190
|
}
|
|
191
191
|
const edit = {
|
|
192
192
|
action: "create",
|
|
@@ -228,7 +228,7 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
228
228
|
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
229
229
|
const storeId = storeIdFor(scope, exec);
|
|
230
230
|
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
231
|
-
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → ${storeLabel(scope)}
|
|
231
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → ${storeLabel(scope)}`, opts.recordLanguage);
|
|
232
232
|
}
|
|
233
233
|
const edit = { action: "update", kind: args.kind, id: args.id };
|
|
234
234
|
if (args.title !== undefined)
|
|
@@ -263,7 +263,7 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
263
263
|
const storeId = storeIdFor(scope, exec);
|
|
264
264
|
const ids = collectDeleteIds(args.id, args.ids);
|
|
265
265
|
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
266
|
-
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${ids.length > 1 ? `${ids.length} entries (${ids.join(", ")})` : ids[0]} → ${storeLabel(scope)}
|
|
266
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${ids.length > 1 ? `${ids.length} entries (${ids.join(", ")})` : ids[0]} → ${storeLabel(scope)}`, opts.recordLanguage);
|
|
267
267
|
}
|
|
268
268
|
const edits = ids.map((id) => ({ action: "delete", kind: args.kind, id }));
|
|
269
269
|
return textResult(applyEditsText(engine, scope, storeId, edits, exec.agent, sessionIdOf(exec)));
|
package/lib/turn-snapshot.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { Context } from "@deepseek-ai/cordis";
|
|
10
10
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
11
11
|
/** Why a scheduler snapshot was captured. */
|
|
12
|
-
export type TurnSnapshotReason = "
|
|
12
|
+
export type TurnSnapshotReason = "compact" | "session_close" | "goal_blocked";
|
|
13
13
|
/** Mechanical reasons a snapshot can be skipped without an LLM call. */
|
|
14
14
|
export type SnapshotSkipReason = "no-new-events" | "internal-agent" | "direct-memory-write" | "no-user-prose";
|
|
15
15
|
/** One immutable scheduler input. The cursor is a durable event boundary. */
|
|
@@ -69,4 +69,10 @@ export declare function containsDirectMemoryWrite(events: readonly unknown[]): b
|
|
|
69
69
|
export declare function serializeSnapshotEvents(events: readonly unknown[], maxChars: number): string;
|
|
70
70
|
/** A DSH subagent header is an internal source when its origin says so. */
|
|
71
71
|
export declare function isInternalAgent(agent: Agent | undefined): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Durable-boundary ordering shared by the scheduler and the extraction
|
|
74
|
+
* phase: returns a positive number when `left` is newer than `right`,
|
|
75
|
+
* negative when older, and undefined when the two shapes are incomparable.
|
|
76
|
+
*/
|
|
77
|
+
export declare function compareReviewCursors(left: string, right: string): number | undefined;
|
|
72
78
|
//# sourceMappingURL=turn-snapshot.d.ts.map
|
package/lib/turn-snapshot.js
CHANGED
|
@@ -235,4 +235,18 @@ function readNestedString(value, keys) {
|
|
|
235
235
|
}
|
|
236
236
|
return undefined;
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Durable-boundary ordering shared by the scheduler and the extraction
|
|
240
|
+
* phase: returns a positive number when `left` is newer than `right`,
|
|
241
|
+
* negative when older, and undefined when the two shapes are incomparable.
|
|
242
|
+
*/
|
|
243
|
+
export function compareReviewCursors(left, right) {
|
|
244
|
+
if (left === right)
|
|
245
|
+
return 0;
|
|
246
|
+
const leftMatch = /^(seq|index):(\d+)$/.exec(left);
|
|
247
|
+
const rightMatch = /^(seq|index):(\d+)$/.exec(right);
|
|
248
|
+
if (!leftMatch || !rightMatch || leftMatch[1] !== rightMatch[1])
|
|
249
|
+
return undefined;
|
|
250
|
+
return Number(leftMatch[2]) - Number(rightMatch[2]);
|
|
251
|
+
}
|
|
238
252
|
//# sourceMappingURL=turn-snapshot.js.map
|
package/lib/types.d.ts
CHANGED
|
@@ -205,4 +205,14 @@ export declare function slug(raw: string, fallback: string): string;
|
|
|
205
205
|
export declare function emptyHarnessState(): HarnessState;
|
|
206
206
|
/** Deep clone helper (state is plain JSON data). */
|
|
207
207
|
export declare function cloneEntry(entry: HarnessEntry | undefined): HarnessEntry | undefined;
|
|
208
|
+
/**
|
|
209
|
+
* Storage-hygiene defaults (#20): JSONL tail lines kept per store history
|
|
210
|
+
* and per shared audit trail when no explicit `historyRetain` is set.
|
|
211
|
+
*/
|
|
212
|
+
export declare const DEFAULT_REFINEMENTS_RETAIN = 500;
|
|
213
|
+
export declare const DEFAULT_REVIEWS_RETAIN = 500;
|
|
214
|
+
/** Prefix-cache routing modes for extractor/planner inputs (see prefix-cache.ts). */
|
|
215
|
+
export type PlannerPrefixCacheMode = "auto" | "session" | "off";
|
|
216
|
+
/** The only two durable outcomes a scope-approval dialog may produce. */
|
|
217
|
+
export type ScopeApprovalDecision = "approved" | "declined";
|
|
208
218
|
//# sourceMappingURL=types.d.ts.map
|
package/lib/types.js
CHANGED
|
@@ -124,4 +124,10 @@ export function emptyHarnessState() {
|
|
|
124
124
|
export function cloneEntry(entry) {
|
|
125
125
|
return entry ? JSON.parse(JSON.stringify(entry)) : undefined;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Storage-hygiene defaults (#20): JSONL tail lines kept per store history
|
|
129
|
+
* and per shared audit trail when no explicit `historyRetain` is set.
|
|
130
|
+
*/
|
|
131
|
+
export const DEFAULT_REFINEMENTS_RETAIN = 500;
|
|
132
|
+
export const DEFAULT_REVIEWS_RETAIN = 500;
|
|
127
133
|
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap-up candidate domain: the mechanical audit of a session's local
|
|
3
|
+
* entries (candidates, global coverage detection, usage hints), the LLM
|
|
4
|
+
* assessment parser, and the shared wrap-up types. Pure logic — the LLM
|
|
5
|
+
* call lives in `wrapup.ts`, the proposal builders in
|
|
6
|
+
* `wrapup-proposals.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import type { HarnessEntry, HarnessState, RefinementKind } from "./types.js";
|
|
9
|
+
export type WrapupVerdict = "promote" | "archive" | "keep";
|
|
10
|
+
/** A classified local entry: `key` matches one audited candidate exactly. */
|
|
11
|
+
export interface WrapupItem {
|
|
12
|
+
/** `kind:id` of the candidate this verdict refers to. */
|
|
13
|
+
key: string;
|
|
14
|
+
verdict: WrapupVerdict;
|
|
15
|
+
reason: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional split-promotion payload (verdict "archive" only): the entry is
|
|
18
|
+
* archived as a whole, but a CLEANED cross-session-reusable part is
|
|
19
|
+
* offered for promotion — the durable fact distilled out of the mixed
|
|
20
|
+
* entry, with the ephemeral snapshot left behind in the archive.
|
|
21
|
+
*/
|
|
22
|
+
promote?: {
|
|
23
|
+
title: string;
|
|
24
|
+
content: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Valence signal (P1 效价反馈): the assessor found trajectory or global
|
|
28
|
+
* evidence contradicting this entry's content (user correction, overridden
|
|
29
|
+
* fact). Strictly parsed — only an explicit JSON `true` sets it. Keep
|
|
30
|
+
* verdicts with this flag get a negative-valence stamp; the assessor is
|
|
31
|
+
* instructed to prefer "archive" for contradicted entries.
|
|
32
|
+
*/
|
|
33
|
+
contradicted?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** A real global entry worth showing the assessor for the same topic. */
|
|
36
|
+
export interface GlobalHint {
|
|
37
|
+
id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
}
|
|
40
|
+
/** The model's full classification of a session's local entries. */
|
|
41
|
+
export interface WrapupAssessment {
|
|
42
|
+
items: WrapupItem[];
|
|
43
|
+
rationale: string;
|
|
44
|
+
}
|
|
45
|
+
/** A local entry offered for assessment, plus its deterministic audit flags. */
|
|
46
|
+
export interface WrapupCandidate {
|
|
47
|
+
kind: RefinementKind;
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
content: string;
|
|
51
|
+
path: string;
|
|
52
|
+
version: number;
|
|
53
|
+
metadata: Record<string, unknown>;
|
|
54
|
+
/**
|
|
55
|
+
* True when the global store already covers this topic by a STRONG
|
|
56
|
+
* signal: a title that normalizes equal to, or (beyond a length floor)
|
|
57
|
+
* contains, the candidate's title. Collisions on id alone with a wildly
|
|
58
|
+
* different title are deliberately NOT coverage — see {@link globalHintsFor}.
|
|
59
|
+
*/
|
|
60
|
+
coveredGlobally: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Actual global entries that touch the same topic (same id, equal
|
|
63
|
+
* normalized title, or title overlap). Shown to the assessor so it judges
|
|
64
|
+
* against real titles instead of a bare boolean; a bare same-id collision
|
|
65
|
+
* shows up here precisely so the model can tell whether the global copy
|
|
66
|
+
* really covers the local content.
|
|
67
|
+
*/
|
|
68
|
+
globalHints: GlobalHint[];
|
|
69
|
+
/**
|
|
70
|
+
* Injection usage count (gap B1): how many times this entry was included
|
|
71
|
+
* in a system-prompt assembly. Zero means the entry was never used — a
|
|
72
|
+
* strong staleness signal the assessor can weigh.
|
|
73
|
+
*/
|
|
74
|
+
injectionCount: number;
|
|
75
|
+
/**
|
|
76
|
+
* Staleness flag (gap B2): true when the entry has both zero injection
|
|
77
|
+
* usage AND a recency score below the staleness threshold (old + unused).
|
|
78
|
+
* The assessor is instructed to prefer "archive" for stale entries.
|
|
79
|
+
*/
|
|
80
|
+
stale: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Negative-valence counter (P1 效价反馈): how many prior assessments marked
|
|
83
|
+
* this entry contradicted (metadata {@link VALENCE_NEGATIVE_KEY}). Shown to
|
|
84
|
+
* the assessor so it can prefer "archive"; also demotes the entry in
|
|
85
|
+
* injection ranking (inject.ts rankEntries).
|
|
86
|
+
*/
|
|
87
|
+
negativeCount: number;
|
|
88
|
+
}
|
|
89
|
+
export declare function candidateKey(kind: RefinementKind, id: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Deterministic global-coverage check (STRONG signal): the global store
|
|
92
|
+
* already covers a topic when it holds a title that normalizes equal to, or
|
|
93
|
+
* (beyond a length floor) contains, the candidate's normalized title.
|
|
94
|
+
* Archived global entries count too — the topic was already judged
|
|
95
|
+
* cross-session; a local duplicate would only re-sediment it.
|
|
96
|
+
*
|
|
97
|
+
* The bare same-id case is deliberately NOT coverage: ids are slugs derived
|
|
98
|
+
* from titles, so a real collision is usually caught by the title check
|
|
99
|
+
* below. A same-id entry with a wildly different title is a weak signal — the
|
|
100
|
+
* caller routes it through {@link globalHintsFor} for the assessor to judge
|
|
101
|
+
* against the actual global title (real case: local `memory` "用户产品愿景与
|
|
102
|
+
* 收入需求(本会话)" vs global `memory` "用户画像(持续更新)").
|
|
103
|
+
*/
|
|
104
|
+
export declare function globalCoverageDetected(globalState: HarnessState, kind: RefinementKind, entry: Pick<HarnessEntry, "id" | "title">): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* The actual global entries that touch the same topic as a local candidate:
|
|
107
|
+
* same id (regardless of title — the weak collision signal that is NOT
|
|
108
|
+
* coverage on its own), equal normalized title, or title overlap. The raw ids
|
|
109
|
+
* and titles let the assessor judge enrichment against real global content
|
|
110
|
+
* (does the global copy already hold what the local one adds?) rather than a
|
|
111
|
+
* bare boolean. Bounded: a handful of best matches, never the whole store.
|
|
112
|
+
*/
|
|
113
|
+
export declare function globalHintsFor(globalState: HarnessState, kind: RefinementKind, entry: Pick<HarnessEntry, "id" | "title">): GlobalHint[];
|
|
114
|
+
export declare function listLocalCandidates(state: HarnessState, globalState: HarnessState, baseDir?: string): WrapupCandidate[];
|
|
115
|
+
/**
|
|
116
|
+
* Parse and validate the model's assessment JSON. Defense is mechanical:
|
|
117
|
+
* keys outside the candidate list are dropped, verdicts outside the enum
|
|
118
|
+
* collapse to "keep", and candidates the model omitted default to "keep" —
|
|
119
|
+
* a malformed reply can never change an entry's fate by itself.
|
|
120
|
+
*
|
|
121
|
+
* Split promotion (verdict "archive" with a `promote` sub-object): the
|
|
122
|
+
* sub-object is accepted ONLY on archive verdicts and ONLY when both cleaned
|
|
123
|
+
* title and content are non-empty strings — a dropped/malformed sub-object
|
|
124
|
+
* silently degrades to a plain archive (the entry is never half-promoted).
|
|
125
|
+
*/
|
|
126
|
+
export declare function parseWrapupAssessment(text: string, candidates: readonly WrapupCandidate[]): WrapupAssessment;
|
|
127
|
+
//# sourceMappingURL=wrapup-candidates.d.ts.map
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { PROMOTED_TO_KEY, VALENCE_NEGATIVE_KEY, isArchived } from "./types.js";
|
|
2
|
+
import { extractJsonObject } from "./plan.js";
|
|
3
|
+
import { getUsageCount, loadUsage } from "./usage.js";
|
|
4
|
+
import { recencyScore } from "./injection-rank.js";
|
|
5
|
+
export function candidateKey(kind, id) {
|
|
6
|
+
return `${kind}:${id}`;
|
|
7
|
+
}
|
|
8
|
+
/** Lowercase, punctuation-stripped title used for cheap coverage matching. */
|
|
9
|
+
function normalizeKey(value) {
|
|
10
|
+
return value.toLowerCase().replace(/[^a-z0-9\u3400-\u9fff]+/g, "").trim();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic global-coverage check (STRONG signal): the global store
|
|
14
|
+
* already covers a topic when it holds a title that normalizes equal to, or
|
|
15
|
+
* (beyond a length floor) contains, the candidate's normalized title.
|
|
16
|
+
* Archived global entries count too — the topic was already judged
|
|
17
|
+
* cross-session; a local duplicate would only re-sediment it.
|
|
18
|
+
*
|
|
19
|
+
* The bare same-id case is deliberately NOT coverage: ids are slugs derived
|
|
20
|
+
* from titles, so a real collision is usually caught by the title check
|
|
21
|
+
* below. A same-id entry with a wildly different title is a weak signal — the
|
|
22
|
+
* caller routes it through {@link globalHintsFor} for the assessor to judge
|
|
23
|
+
* against the actual global title (real case: local `memory` "用户产品愿景与
|
|
24
|
+
* 收入需求(本会话)" vs global `memory` "用户画像(持续更新)").
|
|
25
|
+
*/
|
|
26
|
+
export function globalCoverageDetected(globalState, kind, entry) {
|
|
27
|
+
const records = globalState.entries[kind];
|
|
28
|
+
const title = normalizeKey(entry.title);
|
|
29
|
+
if (title.length === 0)
|
|
30
|
+
return false;
|
|
31
|
+
for (const other of Object.values(records)) {
|
|
32
|
+
const otherTitle = normalizeKey(other.title);
|
|
33
|
+
if (otherTitle.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
if (otherTitle === title)
|
|
36
|
+
return true;
|
|
37
|
+
if (title.length >= 4 && otherTitle.length >= 4 && (title.includes(otherTitle) || otherTitle.includes(title))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The actual global entries that touch the same topic as a local candidate:
|
|
45
|
+
* same id (regardless of title — the weak collision signal that is NOT
|
|
46
|
+
* coverage on its own), equal normalized title, or title overlap. The raw ids
|
|
47
|
+
* and titles let the assessor judge enrichment against real global content
|
|
48
|
+
* (does the global copy already hold what the local one adds?) rather than a
|
|
49
|
+
* bare boolean. Bounded: a handful of best matches, never the whole store.
|
|
50
|
+
*/
|
|
51
|
+
export function globalHintsFor(globalState, kind, entry) {
|
|
52
|
+
const records = globalState.entries[kind];
|
|
53
|
+
const title = normalizeKey(entry.title);
|
|
54
|
+
const hints = [];
|
|
55
|
+
for (const other of Object.values(records)) {
|
|
56
|
+
const otherTitle = normalizeKey(other.title);
|
|
57
|
+
if (otherTitle.length === 0 && other.id !== entry.id)
|
|
58
|
+
continue;
|
|
59
|
+
const matches = other.id === entry.id ||
|
|
60
|
+
(otherTitle.length > 0 && (otherTitle === title || (title.length >= 4 && otherTitle.length >= 4 && (title.includes(otherTitle) || otherTitle.includes(title)))));
|
|
61
|
+
if (matches) {
|
|
62
|
+
hints.push({ id: other.id, title: other.title });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return hints;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The auditable local candidates of a session: every non-archived local
|
|
69
|
+
* entry that has not already been promoted (a promoted entry's lifecycle is
|
|
70
|
+
* finished — the global copy is the live one). Each carries its
|
|
71
|
+
* `coveredGlobally` flag so the assessor never wastes a promote on a topic
|
|
72
|
+
* the global store already owns.
|
|
73
|
+
*/
|
|
74
|
+
/** Staleness threshold: entries with recency below this AND zero usage are stale. */
|
|
75
|
+
const STALE_RECENCY_THRESHOLD = 0.1;
|
|
76
|
+
export function listLocalCandidates(state, globalState, baseDir) {
|
|
77
|
+
const usage = baseDir ? loadUsage(baseDir) : undefined;
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const candidates = [];
|
|
80
|
+
for (const kind of Object.keys(state.entries)) {
|
|
81
|
+
for (const entry of Object.values(state.entries[kind])) {
|
|
82
|
+
if (entry.scope !== "local")
|
|
83
|
+
continue;
|
|
84
|
+
if (isArchived(entry))
|
|
85
|
+
continue;
|
|
86
|
+
if (typeof entry.metadata[PROMOTED_TO_KEY] === "string")
|
|
87
|
+
continue;
|
|
88
|
+
const injectionCount = usage ? getUsageCount(usage, kind, entry.id) : 0;
|
|
89
|
+
const stale = injectionCount === 0 && recencyScore(entry, now) < STALE_RECENCY_THRESHOLD;
|
|
90
|
+
const rawNegative = entry.metadata[VALENCE_NEGATIVE_KEY];
|
|
91
|
+
const negativeCount = typeof rawNegative === "number" && Number.isFinite(rawNegative) && rawNegative > 0 ? rawNegative : 0;
|
|
92
|
+
candidates.push({
|
|
93
|
+
kind,
|
|
94
|
+
id: entry.id,
|
|
95
|
+
title: entry.title,
|
|
96
|
+
content: entry.content,
|
|
97
|
+
path: entry.path,
|
|
98
|
+
version: entry.version,
|
|
99
|
+
metadata: entry.metadata,
|
|
100
|
+
coveredGlobally: globalCoverageDetected(globalState, kind, entry),
|
|
101
|
+
globalHints: globalHintsFor(globalState, kind, entry),
|
|
102
|
+
injectionCount,
|
|
103
|
+
stale,
|
|
104
|
+
negativeCount,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return candidates;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse and validate the model's assessment JSON. Defense is mechanical:
|
|
112
|
+
* keys outside the candidate list are dropped, verdicts outside the enum
|
|
113
|
+
* collapse to "keep", and candidates the model omitted default to "keep" —
|
|
114
|
+
* a malformed reply can never change an entry's fate by itself.
|
|
115
|
+
*
|
|
116
|
+
* Split promotion (verdict "archive" with a `promote` sub-object): the
|
|
117
|
+
* sub-object is accepted ONLY on archive verdicts and ONLY when both cleaned
|
|
118
|
+
* title and content are non-empty strings — a dropped/malformed sub-object
|
|
119
|
+
* silently degrades to a plain archive (the entry is never half-promoted).
|
|
120
|
+
*/
|
|
121
|
+
export function parseWrapupAssessment(text, candidates) {
|
|
122
|
+
const value = extractJsonObject(text);
|
|
123
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
124
|
+
throw new Error("wrap-up assessment JSON must be an object");
|
|
125
|
+
}
|
|
126
|
+
const record = value;
|
|
127
|
+
const allowed = new Set(candidates.map((candidate) => candidateKey(candidate.kind, candidate.id)));
|
|
128
|
+
const items = [];
|
|
129
|
+
if (Array.isArray(record["items"])) {
|
|
130
|
+
for (const raw of record["items"]) {
|
|
131
|
+
if (typeof raw !== "object" || raw === null)
|
|
132
|
+
continue;
|
|
133
|
+
const item = raw;
|
|
134
|
+
const key = typeof item["key"] === "string" ? item["key"] : "";
|
|
135
|
+
if (!allowed.has(key))
|
|
136
|
+
continue;
|
|
137
|
+
const verdict = item["verdict"] === "promote" || item["verdict"] === "archive" ? item["verdict"] : "keep";
|
|
138
|
+
const built = { key, verdict, reason: typeof item["reason"] === "string" ? item["reason"] : "" };
|
|
139
|
+
if (item["contradicted"] === true) {
|
|
140
|
+
built.contradicted = true;
|
|
141
|
+
}
|
|
142
|
+
if (verdict === "archive" && typeof item["promote"] === "object" && item["promote"] !== null) {
|
|
143
|
+
const sub = item["promote"];
|
|
144
|
+
const subTitle = typeof sub["title"] === "string" ? sub["title"].trim() : "";
|
|
145
|
+
const subContent = typeof sub["content"] === "string" ? sub["content"].trim() : "";
|
|
146
|
+
if (subTitle.length > 0 && subContent.length > 0) {
|
|
147
|
+
built.promote = { title: subTitle, content: subContent };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
items.push(built);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const candidate of candidates) {
|
|
154
|
+
const key = candidateKey(candidate.kind, candidate.id);
|
|
155
|
+
if (!items.some((item) => item.key === key)) {
|
|
156
|
+
items.push({ key, verdict: "keep", reason: "not mentioned by the assessor" });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { items, rationale: typeof record["rationale"] === "string" ? record["rationale"] : "" };
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=wrapup-candidates.js.map
|
package/lib/wrapup-command.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ import type { Context } from "@deepseek-ai/cordis";
|
|
|
5
5
|
import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
|
|
6
6
|
import type { EvolutionEngine } from "./service.js";
|
|
7
7
|
import { type PromotionPolicy } from "./promotion.js";
|
|
8
|
-
|
|
8
|
+
import { type RecordLanguagePreference } from "./record-language.js";
|
|
9
|
+
export declare function executeWrapupCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, policy?: PromotionPolicy, recordLanguage?: RecordLanguagePreference): Promise<CommandResult>;
|
|
9
10
|
//# sourceMappingURL=wrapup-command.d.ts.map
|
package/lib/wrapup-command.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { questionServiceOf, requireGlobalApproval } from "./approval.js";
|
|
2
|
-
import {
|
|
2
|
+
import { wrapupArchiveCopy } from "./copy.js";
|
|
3
3
|
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, valenceStampProposal, wholePromoteProposals } from "./wrapup.js";
|
|
4
4
|
import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
|
|
5
|
+
import { normalizeRecordLanguage, resolveRecordLanguage } from "./record-language.js";
|
|
6
|
+
import { recentUserText } from "./inject.js";
|
|
5
7
|
function success(text) {
|
|
6
8
|
return { kind: "success", text };
|
|
7
9
|
}
|
|
8
|
-
export async function executeWrapupCommand(ctx, engine, invocation, policy = DEFAULT_PROMOTION_POLICY) {
|
|
10
|
+
export async function executeWrapupCommand(ctx, engine, invocation, policy = DEFAULT_PROMOTION_POLICY, recordLanguage) {
|
|
9
11
|
const sessionId = invocation.agent.id;
|
|
10
12
|
const localState = engine.load("local", sessionId);
|
|
11
13
|
const globalState = engine.load("global", undefined);
|
|
@@ -102,7 +104,7 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
|
|
|
102
104
|
].join("\n")}`;
|
|
103
105
|
let promoteAllowed = true;
|
|
104
106
|
try {
|
|
105
|
-
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what);
|
|
107
|
+
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what, recordLanguage);
|
|
106
108
|
}
|
|
107
109
|
catch (cause) {
|
|
108
110
|
promoteAllowed = false;
|
|
@@ -187,7 +189,8 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
|
|
|
187
189
|
const questionId = "evolve-wrapup-archive-review";
|
|
188
190
|
let archiveConfirmed = false;
|
|
189
191
|
try {
|
|
190
|
-
const copy = wrapupArchiveCopy(candidate.title,
|
|
192
|
+
const copy = wrapupArchiveCopy(candidate.title, normalizeRecordLanguage(recordLanguage) ??
|
|
193
|
+
resolveRecordLanguage({ ctx, trajectoryText: recentUserText(invocation.agent) }));
|
|
191
194
|
const answer = await userQuestions.ask({
|
|
192
195
|
questions: [
|
|
193
196
|
{
|