dsh-continual-evolve 0.7.2 → 0.7.4

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.
@@ -1,90 +1,172 @@
1
1
  /**
2
- * Serial latest-pending scheduler for automatic review snapshots.
2
+ * Serial latest-boundary scheduler for automatic memory+review snapshots.
3
3
  *
4
- * This is the DSH equivalent of ZCode's memory extraction scheduler: one
5
- * runner per session, at most one running snapshot, and one replaceable
6
- * pending snapshot while the runner is busy. A cursor advances only after a
7
- * successful or no-op execution; errors and aborts leave it untouched so the
8
- * next snapshot retries the same durable boundary.
4
+ * One runner exists per session. A burst is coalesced by durable cursor rather
5
+ * than promise arrival: once multiple pending captures settle, the highest
6
+ * comparable boundary runs first. A cursor advances only after success/no-op;
7
+ * errors, aborts, and stale boundaries leave it unchanged.
9
8
  */
9
+ /** Compare two scheduler cursors; undefined means the cursor families differ. */
10
+ export function compareReviewCursors(left, right) {
11
+ if (left === right)
12
+ return 0;
13
+ const leftMatch = /^(seq|index):(\d+)$/.exec(left);
14
+ const rightMatch = /^(seq|index):(\d+)$/.exec(right);
15
+ if (!leftMatch || !rightMatch || leftMatch[1] !== rightMatch[1])
16
+ return undefined;
17
+ return Number(leftMatch[2]) - Number(rightMatch[2]);
18
+ }
19
+ /** Bound unresolved acquisitions; actual auto capture is serialized per session. */
20
+ const MAX_PENDING_ACQUISITIONS = 16;
10
21
  /**
11
- * Create a scheduler with ZCode-compatible coalescing and cursor semantics.
22
+ * Create a scheduler with ZCode-compatible serial/coalescing semantics.
12
23
  *
13
24
  * @param execute - one serial extraction/review operation
14
25
  * @param cursorOf - extracts the durable boundary from a snapshot
15
26
  */
16
27
  export function createReviewScheduler(execute, cursorOf) {
17
28
  let cursor;
18
- let latestPending;
19
29
  let running;
20
30
  let shuttingDown = false;
31
+ let wakePending;
32
+ const pending = [];
21
33
  const shutdownController = new AbortController();
34
+ const signal = shutdownController.signal;
35
+ const trackPending = (acquisition) => {
36
+ const entry = {};
37
+ entry.promise = acquisition.then((result) => {
38
+ entry.result = result;
39
+ wakePending?.();
40
+ return result;
41
+ });
42
+ pending.push(entry);
43
+ while (pending.length > MAX_PENDING_ACQUISITIONS)
44
+ pending.shift();
45
+ };
46
+ const takeHighestPending = async () => {
47
+ while (!signal.aborted) {
48
+ for (let index = pending.length - 1; index >= 0; index -= 1) {
49
+ if (pending[index]?.result?.status === "error")
50
+ pending.splice(index, 1);
51
+ }
52
+ if (pending.length === 0)
53
+ return undefined;
54
+ const settled = pending.filter((entry) => entry.result !== undefined);
55
+ if (settled.length > 0) {
56
+ let bestIndex = -1;
57
+ let bestCursor;
58
+ for (let index = 0; index < settled.length; index += 1) {
59
+ const entry = settled[index];
60
+ if (!entry || entry.result?.status !== "acquired")
61
+ continue;
62
+ const candidate = cursorOf(entry.result.snapshot);
63
+ const order = bestCursor === undefined || candidate === undefined
64
+ ? 1
65
+ : compareReviewCursors(candidate, bestCursor);
66
+ if (bestIndex < 0 || order === undefined || order > 0) {
67
+ bestIndex = pending.indexOf(entry);
68
+ bestCursor = candidate;
69
+ }
70
+ }
71
+ if (bestIndex < 0) {
72
+ pending.length = 0;
73
+ return undefined;
74
+ }
75
+ const chosen = pending[bestIndex];
76
+ pending.splice(bestIndex, 1);
77
+ for (let index = pending.length - 1; index >= 0; index -= 1) {
78
+ if (pending[index]?.result !== undefined)
79
+ pending.splice(index, 1);
80
+ }
81
+ return chosen?.result;
82
+ }
83
+ await new Promise((resolve) => {
84
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
85
+ const onAbort = () => {
86
+ cleanup();
87
+ resolve();
88
+ };
89
+ wakePending = () => {
90
+ cleanup();
91
+ wakePending = undefined;
92
+ resolve();
93
+ };
94
+ signal.addEventListener("abort", onAbort, { once: true });
95
+ });
96
+ }
97
+ return undefined;
98
+ };
22
99
  const processSnapshot = async (snapshot) => {
23
- if (shuttingDown || shutdownController.signal.aborted)
100
+ if (shuttingDown || signal.aborted)
24
101
  return;
102
+ const nextCursor = cursorOf(snapshot);
103
+ if (cursor !== undefined && nextCursor !== undefined) {
104
+ const order = compareReviewCursors(nextCursor, cursor);
105
+ if (order !== undefined && order <= 0)
106
+ return;
107
+ }
25
108
  let status;
26
109
  try {
27
- status = await execute({ snapshot, signal: shutdownController.signal });
110
+ status = await execute({ snapshot, signal });
28
111
  }
29
112
  catch {
30
- // A failed acquisition/extraction is deliberately contained. The
31
- // cursor remains unchanged and the next snapshot retries it.
32
113
  return;
33
114
  }
34
- if (!shuttingDown && (status === "success" || status === "no-op")) {
35
- cursor = cursorOf(snapshot);
115
+ if (!shuttingDown && (status === "success" || status === "no-op") && nextCursor !== undefined) {
116
+ const order = cursor === undefined ? 1 : compareReviewCursors(nextCursor, cursor);
117
+ if (order === undefined || order > 0)
118
+ cursor = nextCursor;
36
119
  }
37
120
  };
38
121
  const run = async (first) => {
39
122
  try {
40
- let current = first;
41
- while (current && !shuttingDown) {
42
- const acquisition = await waitForSnapshotAcquisitionOrShutdown(current, shutdownController.signal);
43
- if (acquisition.status === "shutdown" || shuttingDown)
44
- break;
45
- if (acquisition.status === "acquired") {
123
+ let acquisition = await waitForSnapshotAcquisitionOrShutdown(first, signal);
124
+ while (acquisition.status !== "shutdown" && !shuttingDown) {
125
+ if (acquisition.status === "acquired")
46
126
  await processSnapshot(acquisition.snapshot);
47
- }
48
- current = shuttingDown ? undefined : latestPending;
49
- latestPending = undefined;
127
+ const next = await takeHighestPending();
128
+ if (!next)
129
+ break;
130
+ acquisition = next;
50
131
  }
51
132
  }
52
133
  finally {
53
- if (shuttingDown)
54
- latestPending = undefined;
134
+ if (shuttingDown) {
135
+ pending.length = 0;
136
+ wakePending?.();
137
+ wakePending = undefined;
138
+ }
55
139
  running = undefined;
56
140
  }
57
141
  };
58
142
  return {
59
143
  async drain() {
60
- while (running) {
144
+ while (running)
61
145
  await running;
62
- }
63
146
  },
64
147
  getCursor() {
65
148
  return cursor;
66
149
  },
67
150
  hasPendingWork() {
68
- return running !== undefined || latestPending !== undefined;
151
+ return running !== undefined || pending.length > 0;
69
152
  },
70
153
  schedule(snapshot) {
71
154
  if (shuttingDown)
72
155
  return;
73
156
  const acquisition = acquireSnapshot(snapshot);
74
- if (running) {
75
- // Replace, never queue: a burst of turns contributes one latest
76
- // boundary and the next extraction consumes all intervening rows.
77
- latestPending = acquisition;
78
- return;
79
- }
80
- running = run(acquisition);
157
+ if (running)
158
+ trackPending(acquisition);
159
+ else
160
+ running = run(acquisition);
81
161
  },
82
162
  shutdown() {
83
163
  if (shuttingDown)
84
164
  return;
85
165
  shuttingDown = true;
86
- latestPending = undefined;
166
+ pending.length = 0;
87
167
  shutdownController.abort();
168
+ wakePending?.();
169
+ wakePending = undefined;
88
170
  },
89
171
  };
90
172
  }
package/lib/review.d.ts CHANGED
@@ -44,7 +44,7 @@ export interface ReviewOptions {
44
44
  /** Direct-call token ledger destination; the review phase is fixed by this module. */
45
45
  tokenUsage?: TokenUsageTarget;
46
46
  }
47
- export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should run /evolve. Auto /evolve writes local\nharness state by default, so approve when the trajectory contains evidence\nuseful to this session's future turns: a repeated failure, a reusable tactic,\na repeated delegation role, a durable fact or preference, a user correction\nthat should persist, or a narrow behavioral policy.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nSkill-related trajectories (the evidence concerns creating or improving a\nskill entry) are judged against the DSH skill quality standard (skill-audit\ndimensions: frontmatter routing, the 7 structural features, paragraph\nskeleton, no duplication of the official 11 skills or covered skills).\nApprove only when the trajectory shows a REAL trigger scenario and the\nresulting skill would meet the standard; otherwise decline and say in the\nrationale what must improve \u2014 drafting follows skill-creator, and the\nplanner receives the standard as its <skill_quality_standard> block.\n\nRepeated multi-step workflows (session start/end routines, recurring\nwrap-up or handoff procedures) are a valid refine target: approve with\ninstructions telling the planner to propose a guidance skill (kind=skill,\nskill_kind=guidance \u2014 a SKILL.md document, no python reference). Only\npropose when the same workflow recurs in the trajectory \u2014 never for\none-off flows. Auto-created skills are always offered to the user for a\ndecision before they land; the gate never writes a skill silently.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
47
+ export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should continue the non-memory /evolve path.\nA dedicated memory extraction agent has already processed the same snapshot\nwith a frozen memory manifest, so do NOT approve solely to create, update,\narchive, or delete a memory entry. Decline with a rationale that the memory\nphase owns that evidence. For the remaining kinds, approve repeated failures,\nreusable tactics, repeated delegation roles, or narrow behavioral policies.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nSkill-related trajectories (the evidence concerns creating or improving a\nskill entry) are judged against the DSH skill quality standard (skill-audit\ndimensions: frontmatter routing, the 7 structural features, paragraph\nskeleton, no duplication of the official 11 skills or covered skills).\nApprove only when the trajectory shows a REAL trigger scenario and the\nresulting skill would meet the standard; otherwise decline and say in the\nrationale what must improve \u2014 drafting follows skill-creator, and the\nplanner receives the standard as its <skill_quality_standard> block.\n\nRepeated multi-step workflows (session start/end routines, recurring\nwrap-up or handoff procedures) are a valid refine target: approve with\ninstructions telling the planner to propose a guidance skill (kind=skill,\nskill_kind=guidance \u2014 a SKILL.md document, no python reference). Only\npropose when the same workflow recurs in the trajectory \u2014 never for\none-off flows. Auto-created skills are always offered to the user for a\ndecision before they land; the gate never writes a skill silently.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
48
48
  /** Parse the gate's JSON reply. */
49
49
  export declare function parseAutoRefineReview(text: string): AutoRefineReview;
50
50
  /** Serialize surface events to bounded role-prefixed text. */
package/lib/review.js CHANGED
@@ -2,15 +2,16 @@ import { extractJsonObject } from "./plan.js";
2
2
  import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
3
3
  import { sessionEventsOf } from "./inject.js";
4
4
  import { buildPrefixMessages, detectPlannerRoute, resolvePrefixCache } from "./prefix-cache.js";
5
- import { streamText } from "./llm-text.js";
5
+ import { asLlmSessionId, streamText } from "./llm-text.js";
6
6
  import { createTokenUsageObserver } from "./token-usage.js";
7
7
  export const AUTO_REVIEW_SYSTEM_PROMPT = `You are the automatic /evolve review gate.
8
8
 
9
- Decide whether this checkpoint should run /evolve. Auto /evolve writes local
10
- harness state by default, so approve when the trajectory contains evidence
11
- useful to this session's future turns: a repeated failure, a reusable tactic,
12
- a repeated delegation role, a durable fact or preference, a user correction
13
- that should persist, or a narrow behavioral policy.
9
+ Decide whether this checkpoint should continue the non-memory /evolve path.
10
+ A dedicated memory extraction agent has already processed the same snapshot
11
+ with a frozen memory manifest, so do NOT approve solely to create, update,
12
+ archive, or delete a memory entry. Decline with a rationale that the memory
13
+ phase owns that evidence. For the remaining kinds, approve repeated failures,
14
+ reusable tactics, repeated delegation roles, or narrow behavioral policies.
14
15
 
15
16
  The current harness state below includes GLOBAL entries (scope=global) plus
16
17
  this session's local entries (scope=local). When a topic is already covered
@@ -125,6 +126,7 @@ export async function reviewAutoRefine(ctx, options) {
125
126
  const text = await streamText(ctx, {
126
127
  provider,
127
128
  model,
129
+ sessionId: asLlmSessionId(agent.id),
128
130
  system: AUTO_REVIEW_SYSTEM_PROMPT,
129
131
  prompt: userPrompt,
130
132
  maxTokens: options.budgetTokens ?? 8000,
package/lib/service.d.ts CHANGED
@@ -6,6 +6,14 @@
6
6
  import type { EntrySource, HarnessScope, RefinementProposal, RefinementResult } from "./types.js";
7
7
  import { applyRefinementProposal } from "./apply.js";
8
8
  import type { HistoryRetention } from "./store.js";
9
+ /** A post-commit apply failure that still carries the durable applied result. */
10
+ export declare class EvolutionApplyPostCommitError extends Error {
11
+ readonly result: RefinementResult;
12
+ readonly scope: HarnessScope;
13
+ readonly storeId: string | undefined;
14
+ readonly cause: unknown;
15
+ constructor(message: string, result: RefinementResult, scope: HarnessScope, storeId: string | undefined, cause: unknown);
16
+ }
9
17
  export interface ApplyContext {
10
18
  scope: HarnessScope;
11
19
  sessionId?: string;
@@ -32,6 +40,7 @@ export declare function createEvolutionEngine(baseDir: string, hooks?: Evolution
32
40
  load: (scope: HarnessScope, sessionId: string | undefined) => import("./types.js").HarnessState;
33
41
  apply: (scope: HarnessScope, sessionId: string | undefined, proposal: RefinementProposal, context?: ApplyContext) => RefinementResult;
34
42
  rollback: (scope: HarnessScope, sessionId: string | undefined, refinementId: string) => RefinementResult;
43
+ rollbackResult: (scope: HarnessScope, sessionId: string | undefined, target: RefinementResult) => RefinementResult;
35
44
  history: (scope: HarnessScope, sessionId: string | undefined) => RefinementResult[];
36
45
  baseDir: string;
37
46
  retention: HistoryRetention;
package/lib/service.js CHANGED
@@ -5,6 +5,20 @@ import { rollbackProposal } from "./rollback.js";
5
5
  import { loadHarnessState, saveHarnessState } from "./state.js";
6
6
  import { appendResult, loadResults, pruneJsonlFile, pruneSnapshots, resolveHistoryRetention, snapshotBefore, storePaths } from "./store.js";
7
7
  import { CONFLICT_BLOCK_SCORE, CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry, secretLeakReason } from "./promotion.js";
8
+ /** A post-commit apply failure that still carries the durable applied result. */
9
+ export class EvolutionApplyPostCommitError extends Error {
10
+ result;
11
+ scope;
12
+ storeId;
13
+ cause;
14
+ constructor(message, result, scope, storeId, cause) {
15
+ super(message);
16
+ this.result = result;
17
+ this.scope = scope;
18
+ this.storeId = storeId;
19
+ this.cause = cause;
20
+ }
21
+ }
8
22
  export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
9
23
  const retention = resolveHistoryRetention(opts.historyRetain);
10
24
  /**
@@ -26,27 +40,29 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
26
40
  // 2026-08-28 B1: the two roles were previously folded into one object,
27
41
  // which made the advertised guard unreachable).
28
42
  const state = load(scope, sessionId);
29
- // Write-time conflict guard (R2): global creates are checked against
30
- // the existing same-kind entries BEFORE any side effect — a
31
- // near-duplicate is rejected with an actionable error (evolve_update
32
- // instead), a moderate overlap proceeds stamped with
43
+ // Write-time conflict guard (R2): persistent-scope creates are
44
+ // checked against the existing same-kind entries BEFORE any side
45
+ // effect — a near-duplicate is rejected with an actionable error
46
+ // (evolve_update instead), a moderate overlap proceeds stamped with
33
47
  // CONFLICT_HINT_KEY. Rollbacks bypass the guard: re-creating an entry
34
48
  // that resembles its successor is the point of rollback. Local scope
35
49
  // is never blocked (scratch space); the wrapup/fate promotion path
36
50
  // already enforces its own overlap policy there.
37
51
  //
38
- // Secret-leak guard (P0, same throat): global creates AND updates are
39
- // screened for credential-shaped literals before any side effect — a
40
- // secret reaching the cross-session store is a leak even when the
41
- // entry itself is legitimate. The screen covers every field the edit
42
- // can plant: title, content, and the JSON forms of reference,
43
- // arguments, and metadata (mount embeds reference verbatim into the
44
- // generated plugin file). Fixed patterns, not policy-configurable.
52
+ // Secret-leak guard (P0, same throat): every persistent-scope create
53
+ // and update is screened before any side effect. Project and global
54
+ // stores both outlive the current session, so approval never turns a
55
+ // credential-shaped literal into an allowed write. The screen covers
56
+ // every field the edit can plant: title, content, and the JSON forms
57
+ // of reference, arguments, and metadata. Fixed patterns, not policy-
58
+ // configurable.
45
59
  const warnHits = new Map();
46
- if (scope === "global" && !context?.rollbackOf) {
60
+ if ((scope === "project" || scope === "global") && !context?.rollbackOf) {
47
61
  for (const [index, edit] of proposal.edits.entries()) {
48
62
  if (edit.action === "create" || edit.action === "update") {
49
63
  const screenable = [
64
+ typeof edit.id === "string" ? edit.id : "",
65
+ typeof edit.path === "string" ? edit.path : "",
50
66
  typeof edit.title === "string" ? edit.title : "",
51
67
  typeof edit.content === "string" ? edit.content : "",
52
68
  edit.reference !== undefined ? JSON.stringify(edit.reference) : "",
@@ -100,7 +116,12 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
100
116
  }
101
117
  }
102
118
  saveHarnessState(paths.stateDir, state);
103
- appendResult(paths, result);
119
+ try {
120
+ appendResult(paths, result);
121
+ }
122
+ catch (cause) {
123
+ throw new EvolutionApplyPostCommitError(`Refinement ${result.id} persisted state but failed to append history: ${cause instanceof Error ? cause.message : String(cause)}`, result, scope, sessionId, cause);
124
+ }
104
125
  // Storage hygiene (#20): bound the append-only past at write time —
105
126
  // snapshots keep the newest N, the per-store history keeps its tail.
106
127
  // Best-effort (never throws): a prune failure must not fail the apply.
@@ -116,9 +137,18 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
116
137
  catch {
117
138
  // ignored — the next apply retries
118
139
  }
119
- hooks.onApplied?.(result);
140
+ try {
141
+ hooks.onApplied?.(result);
142
+ }
143
+ catch (cause) {
144
+ throw new EvolutionApplyPostCommitError(`Refinement ${result.id} persisted state but a post-commit hook failed: ${cause instanceof Error ? cause.message : String(cause)}`, result, scope, sessionId, cause);
145
+ }
120
146
  return result;
121
147
  }
148
+ /** Roll back an already-observed result without consulting history. */
149
+ function rollbackResult(scope, sessionId, target) {
150
+ return apply(scope, sessionId, rollbackProposal(target), { scope, rollbackOf: target.id });
151
+ }
122
152
  function rollback(scope, sessionId, refinementId) {
123
153
  const paths = storePaths(baseDir, scope, sessionId);
124
154
  const history = loadResults(paths);
@@ -126,15 +156,11 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
126
156
  if (!target) {
127
157
  throw new Error(`Refinement ${refinementId} not found in ${scope} history`);
128
158
  }
129
- const proposal = rollbackProposal(target);
130
- // The rollback refinement carries rollbackOf so the audit chain links
131
- // the inverse operation back to its origin (previously the rollback
132
- // record only echoed "Rollback refinement <id>" in its summary text).
133
- return apply(scope, sessionId, proposal, { scope, rollbackOf: refinementId });
159
+ return rollbackResult(scope, sessionId, target);
134
160
  }
135
161
  function history(scope, sessionId) {
136
162
  return loadResults(storePaths(baseDir, scope, sessionId));
137
163
  }
138
- return { load, apply, rollback, history, baseDir, retention };
164
+ return { load, apply, rollback, rollbackResult, history, baseDir, retention };
139
165
  }
140
166
  //# sourceMappingURL=service.js.map
@@ -2,7 +2,7 @@ import type { TokenUsage } from "@deepseek-ai/dsh-llm";
2
2
  import type { StreamTextObservation, StreamTextUsageObserver } from "./llm-text.js";
3
3
  export declare const TOKEN_USAGE_LEDGER_VERSION = 1;
4
4
  /** Plugin-owned direct call paths represented in the ledger. */
5
- export declare const TOKEN_USAGE_PHASES: readonly ["review", "planner", "wrapup", "fate"];
5
+ export declare const TOKEN_USAGE_PHASES: readonly ["review", "memory", "planner", "wrapup", "fate"];
6
6
  export type TokenUsagePhase = (typeof TOKEN_USAGE_PHASES)[number];
7
7
  /** Whether the provider supplied a usage sample; missing is never treated as zero. */
8
8
  export type TokenUsageStatus = "reported" | "missing";
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Provider-reported token accounting for plugin-owned direct LLM calls.
3
3
  *
4
- * The review gate, planner, wrap-up command, and local-fate classifier all use
5
- * the shared `streamText` helper. These calls do not belong to an Agent turn
6
- * (they pass no sessionId), so the host session token-meter cannot attribute
7
- * them. This ledger records the exact `TokenUsage` emitted by the adapter at
8
- * the one shared stream boundary instead.
4
+ * The review gate, planner, dedicated memory agent, wrap-up command, and
5
+ * local-fate classifier all use the shared model-call boundary. These calls do
6
+ * not belong to an Agent turn (they pass no sessionId), so the host session
7
+ * token-meter cannot attribute them. This ledger records the exact
8
+ * `TokenUsage` emitted by the adapter at the one shared stream boundary.
9
9
  *
10
10
  * Storage: `<baseDir>/evolve/token-usage.jsonl`. The file contains route and
11
11
  * count metadata only — never prompts, responses, or tool arguments — and is
@@ -17,7 +17,7 @@ import { EVOLVE_DIR, pruneJsonlFile } from "./store.js";
17
17
  const TOKEN_USAGE_FILE = "token-usage.jsonl";
18
18
  export const TOKEN_USAGE_LEDGER_VERSION = 1;
19
19
  /** Plugin-owned direct call paths represented in the ledger. */
20
- export const TOKEN_USAGE_PHASES = ["review", "planner", "wrapup", "fate"];
20
+ export const TOKEN_USAGE_PHASES = ["review", "memory", "planner", "wrapup", "fate"];
21
21
  /** Absolute path of the direct-call token ledger. */
22
22
  export function tokenUsagePath(baseDir) {
23
23
  return join(baseDir, EVOLVE_DIR, TOKEN_USAGE_FILE);
@@ -139,7 +139,7 @@ export function renderTokenUsageReport(ledger, retain) {
139
139
  return [
140
140
  `direct LLM token usage: no valid calls in the retained tail.${corrupt}`,
141
141
  `window: last ${retain} call(s), not a lifetime total.`,
142
- "scope: review/planner/wrapup/fate only; host benchmark subagents, their agent-loop calls, and per-entry injection cost are excluded.",
142
+ "scope: review/memory/planner/wrapup/fate only; host benchmark subagents, their agent-loop calls, and per-entry injection cost are excluded.",
143
143
  ];
144
144
  }
145
145
  const totals = aggregateTokenUsage(records);
@@ -166,7 +166,7 @@ export function renderTokenUsageReport(ledger, retain) {
166
166
  const lastTotal = last.usage ? exactTokenTotal(last.usage) : "usage unavailable";
167
167
  lines.push(`last: ${last.phase} ${last.provider}/${last.model} · ${last.outcome} · ${lastTotal} tokens · ${last.timestamp}`);
168
168
  }
169
- lines.push("scope: review/planner/wrapup/fate only; host benchmark subagents, their agent-loop calls, and per-entry injection cost are excluded.");
169
+ lines.push("scope: review/memory/planner/wrapup/fate only; host benchmark subagents, their agent-loop calls, and per-entry injection cost are excluded.");
170
170
  return lines;
171
171
  }
172
172
  function isTokenUsageRecord(value) {
@@ -23,6 +23,9 @@ export interface TurnSnapshot {
23
23
  readonly trajectory: string;
24
24
  readonly userText: string;
25
25
  readonly sourceSeqs: readonly number[];
26
+ readonly maxChars: number;
27
+ /** Cursor supplied by the shared scheduler before this snapshot selected events. */
28
+ readonly capturedAfterCursor?: string;
26
29
  readonly eligible: boolean;
27
30
  readonly skipReason?: SnapshotSkipReason;
28
31
  }
@@ -43,6 +46,13 @@ export declare function captureTurnSnapshot(ctx: Context, agent: Agent, opts: {
43
46
  cursor?: string;
44
47
  maxChars: number;
45
48
  }): Promise<TurnSnapshot>;
49
+ /**
50
+ * Rebase one captured snapshot onto a phase-specific cursor while preserving
51
+ * its outer boundary. The memory phase uses this after a downstream review or
52
+ * fate failure: already-processed evidence is removed, but newly captured rows
53
+ * remain available without moving the shared scheduler cursor backwards.
54
+ */
55
+ export declare function sliceTurnSnapshot(snapshot: TurnSnapshot, cursor: string): TurnSnapshot;
46
56
  /**
47
57
  * Decide whether the incremental rows deserve a model call. This intentionally
48
58
  * does not decide whether a fact is valuable; that remains the review model's
@@ -21,6 +21,27 @@ export async function captureTurnSnapshot(ctx, agent, opts) {
21
21
  trajectory: eligibility.eligible ? serializeSnapshotEvents(events, opts.maxChars) : "",
22
22
  userText: eligibility.userText,
23
23
  sourceSeqs: directUserSeqs(events),
24
+ maxChars: opts.maxChars,
25
+ ...(opts.cursor === undefined ? {} : { capturedAfterCursor: opts.cursor }),
26
+ eligible: eligibility.eligible,
27
+ ...(eligibility.reason ? { skipReason: eligibility.reason } : {}),
28
+ };
29
+ }
30
+ /**
31
+ * Rebase one captured snapshot onto a phase-specific cursor while preserving
32
+ * its outer boundary. The memory phase uses this after a downstream review or
33
+ * fate failure: already-processed evidence is removed, but newly captured rows
34
+ * remain available without moving the shared scheduler cursor backwards.
35
+ */
36
+ export function sliceTurnSnapshot(snapshot, cursor) {
37
+ const events = sliceSnapshotEvents(snapshot, cursor);
38
+ const eligibility = evaluateSnapshotEligibility(events, false);
39
+ return {
40
+ ...snapshot,
41
+ events,
42
+ trajectory: eligibility.eligible ? serializeSnapshotEvents(events, snapshot.maxChars) : "",
43
+ userText: eligibility.userText,
44
+ sourceSeqs: directUserSeqs(events),
24
45
  eligible: eligibility.eligible,
25
46
  ...(eligibility.reason ? { skipReason: eligibility.reason } : {}),
26
47
  };
@@ -93,6 +114,19 @@ function boundaryCursor(events) {
93
114
  const seqs = events.map(eventSeq).filter((seq) => seq !== undefined);
94
115
  return seqs.length > 0 ? `seq:${Math.max(...seqs)}` : `index:${events.length}`;
95
116
  }
117
+ function sliceSnapshotEvents(snapshot, cursor) {
118
+ if (snapshot.cursor.startsWith("index:") && cursor.startsWith("index:") && snapshot.capturedAfterCursor?.startsWith("index:")) {
119
+ const start = Number(snapshot.capturedAfterCursor.slice("index:".length));
120
+ const phaseBoundary = Number(cursor.slice("index:".length));
121
+ if (!Number.isInteger(start) || !Number.isInteger(phaseBoundary))
122
+ return snapshot.events;
123
+ const relative = phaseBoundary - start;
124
+ if (relative <= 0)
125
+ return snapshot.events;
126
+ return relative >= snapshot.events.length ? [] : snapshot.events.slice(relative);
127
+ }
128
+ return eventsAfterCursor(snapshot.events, cursor);
129
+ }
96
130
  function eventsAfterCursor(events, cursor) {
97
131
  if (cursor === undefined)
98
132
  return events;
package/lib/wrapup.js CHANGED
@@ -2,7 +2,7 @@ import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOU
2
2
  import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
3
3
  import { extractJsonObject } from "./plan.js";
4
4
  import { compactText } from "./render.js";
5
- import { streamText } from "./llm-text.js";
5
+ import { asLlmSessionId, streamText } from "./llm-text.js";
6
6
  import { getUsageCount, loadUsage } from "./usage.js";
7
7
  import { createTokenUsageObserver } from "./token-usage.js";
8
8
  import { recencyScore } from "./inject.js";
@@ -522,6 +522,7 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
522
522
  const text = await streamText(ctx, {
523
523
  provider: agent.options.provider,
524
524
  model: agent.options.model,
525
+ sessionId: asLlmSessionId(agent.id),
525
526
  system: WRAPUP_ASSESS_SYSTEM_PROMPT,
526
527
  prompt: userPrompt,
527
528
  maxTokens: options.maxOutputTokens ?? 4096,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
5
5
  "license": "MIT",
6
6
  "repository": {