@elvisw/dsh-magic-context 0.0.0-stage → 0.14.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/LICENSE +23 -0
- package/NOTICE +93 -0
- package/README.md +501 -2
- package/README.zh-CN.md +398 -0
- package/cordis.patch.yml +20 -0
- package/lib/client.js +2753 -0
- package/lib/core/compartments.js +424 -0
- package/lib/core/corpus-store.js +176 -0
- package/lib/core/directive-detect.js +147 -0
- package/lib/core/domain-reconcile.js +394 -0
- package/lib/core/domains.js +127 -0
- package/lib/core/embedding-local.js +247 -0
- package/lib/core/global-autostage.js +223 -0
- package/lib/core/global-domains.js +302 -0
- package/lib/core/journal.js +248 -0
- package/lib/core/llm-deadline.js +53 -0
- package/lib/core/machine-domain.js +106 -0
- package/lib/core/maintenance.js +380 -0
- package/lib/core/memory-bridge.js +818 -0
- package/lib/core/memory-categories.js +84 -0
- package/lib/core/memory-scoring.js +65 -0
- package/lib/core/memory-search.js +414 -0
- package/lib/core/memory-write.js +1176 -0
- package/lib/core/policy.js +111 -0
- package/lib/core/profile-confirmation.js +55 -0
- package/lib/core/profile-review.js +375 -0
- package/lib/core/profile-write.js +283 -0
- package/lib/core/reference-seeds.js +63 -0
- package/lib/core/route-health.js +106 -0
- package/lib/core/stores.js +1021 -0
- package/lib/core/tagger.js +7 -0
- package/lib/core/types.js +15 -0
- package/lib/core/vector-backfill.js +140 -0
- package/lib/core/vector-lane.js +184 -0
- package/lib/dsh/command-surface.js +376 -0
- package/lib/dsh/compaction-engine.js +231 -0
- package/lib/dsh/config-switches.js +56 -0
- package/lib/dsh/config.js +13 -0
- package/lib/dsh/context-pressure.js +36 -0
- package/lib/dsh/drop-landing.js +42 -0
- package/lib/dsh/emergency.js +89 -0
- package/lib/dsh/flush-command.js +148 -0
- package/lib/dsh/global-review.js +412 -0
- package/lib/dsh/historian.js +907 -0
- package/lib/dsh/live-journal.js +81 -0
- package/lib/dsh/maintenance-wiring.js +229 -0
- package/lib/dsh/management-pass.js +564 -0
- package/lib/dsh/memory-lane.js +535 -0
- package/lib/dsh/pre-step.js +675 -0
- package/lib/dsh/project-drift.js +116 -0
- package/lib/dsh/recomp.js +192 -0
- package/lib/dsh/reduce-guidance.js +188 -0
- package/lib/dsh/seams.js +588 -0
- package/lib/dsh/search-hint.js +186 -0
- package/lib/dsh/session-notice.js +100 -0
- package/lib/dsh/status-command.js +75 -0
- package/lib/dsh/telemetry.js +349 -0
- package/lib/dsh/tool-bridge.js +1009 -0
- package/lib/dsh/volatile.js +89 -0
- package/lib/index.js +931 -0
- package/package.json +108 -4
- package/tools/migrate-profile-package-name.mjs +317 -0
- package/vendor/onnx/LICENSE +27 -0
- package/vendor/onnx/ort-wasm-simd-threaded.mjs +59 -0
- package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
- package/vendor/onnx/ort.node.min.mjs +8 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { decidePass, pressurePercent, pressureWindowTokens } from "../core/policy.js";
|
|
2
|
+
import { PROFILE_M0_BUDGET_TOKENS, resolveIdentitySpec } from "../core/memory-bridge.js";
|
|
3
|
+
import { errText } from "../core/types.js";
|
|
4
|
+
import { estimateTokens } from "../core/compartments.js";
|
|
5
|
+
import { DSH_EVENTS, enumerateSurfaceToolResults, requestContextOf, wireStrandedResults, } from "./seams.js";
|
|
6
|
+
import { runManagementPass } from "./management-pass.js";
|
|
7
|
+
import { landPendingDrops } from "./drop-landing.js";
|
|
8
|
+
import { syncMachineDomain } from "../core/machine-domain.js";
|
|
9
|
+
import { buildChannel1Reminder, buildChannel2Reminder, buildDeskSection, decideReduceNudge, } from "./reduce-guidance.js";
|
|
10
|
+
import { appendSessionNotice } from "./session-notice.js";
|
|
11
|
+
import { detectProjectDrift } from "./project-drift.js";
|
|
12
|
+
import { liveBoolean, liveRoute, liveString } from "./volatile.js";
|
|
13
|
+
import { planSearchHint } from "./search-hint.js";
|
|
14
|
+
/**
|
|
15
|
+
* The newest REAL user prompt in this request, for the auto-search hint. Host
|
|
16
|
+
* context nodes and this adapter's own entries are user-role messages too, so they
|
|
17
|
+
* are skipped by their markers instead of by role alone.
|
|
18
|
+
*/
|
|
19
|
+
function latestUserPromptText(messages) {
|
|
20
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
21
|
+
const message = messages[index];
|
|
22
|
+
if (message === undefined || message.role !== 'user' || !Array.isArray(message.content))
|
|
23
|
+
continue;
|
|
24
|
+
const text = message.content
|
|
25
|
+
.filter(block => block.type === 'text' && typeof block.text === 'string')
|
|
26
|
+
.map(block => block.text)
|
|
27
|
+
.join('\n');
|
|
28
|
+
if (text.trim().length === 0)
|
|
29
|
+
continue;
|
|
30
|
+
if (text.includes('<magic-context>') || text.startsWith('Magic Context ·'))
|
|
31
|
+
continue;
|
|
32
|
+
return { text, turnKey: typeof message.id === 'string' ? message.id : text.slice(0, 64) };
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
export function preStepListener(deps) {
|
|
37
|
+
const { ctx, config, stores, journal, llm, reduceTracker, lane, projects, seenSessions, activePolicy } = deps;
|
|
38
|
+
const { seenIdentities, sessionIdentity } = projects;
|
|
39
|
+
const { memoryRecall, memoryWriter, profileWriter, visibleMemoryIdsBySession } = lane;
|
|
40
|
+
/** Consecutive defers per session, keyed by reason, to log transitions only. */
|
|
41
|
+
const deferStreak = new Map();
|
|
42
|
+
/** One-shot flag: a drifted pre-step payload must warn once, not every step. */
|
|
43
|
+
let payloadDriftWarned = false;
|
|
44
|
+
return async (payload, next) => {
|
|
45
|
+
// Engine mount/unmount rides the step band: reconcile is two boolean
|
|
46
|
+
// reads when nothing changed and must never fail the turn.
|
|
47
|
+
deps.reconcileEngine?.();
|
|
48
|
+
const decision = await next();
|
|
49
|
+
if (decision.kind !== 'enter')
|
|
50
|
+
return decision;
|
|
51
|
+
// Shape-tolerant identity read: this line sits BEFORE every fail-open band
|
|
52
|
+
// below, so a host refactor that drops `agent` from the pre-step payload
|
|
53
|
+
// (the agent dispatcher injects it today) would otherwise throw into the
|
|
54
|
+
// host's waterfall and fail the whole turn. Skipping this step's adapter
|
|
55
|
+
// work degrades to "adapter idle" instead; the doctor heartbeat is what
|
|
56
|
+
// surfaces the drift durably.
|
|
57
|
+
const sessionId = payload.agent?.session?.id;
|
|
58
|
+
if (sessionId === undefined) {
|
|
59
|
+
if (!payloadDriftWarned) {
|
|
60
|
+
payloadDriftWarned = true;
|
|
61
|
+
journal?.error('error', 'pre-step payload has no agent.session.id — adapter bands skipped this step (host payload drift?)', { scope: 'pre-step' });
|
|
62
|
+
}
|
|
63
|
+
return decision;
|
|
64
|
+
}
|
|
65
|
+
// Admission stamps NOTHING — deliberately. `ctx_reduce` and the drop /
|
|
66
|
+
// emergency lanes resolve tags against surface TOOL RESULTS; `ctx_expand`
|
|
67
|
+
// resolves the persisted tag record and its call's original event, so it
|
|
68
|
+
// needs no live placeholder. A handle on a user message or on a
|
|
69
|
+
// host-injected context node (runtime-context, skill-catalog, time-context)
|
|
70
|
+
// could never be acted on: `ctx_reduce` would answer "no such tool-output
|
|
71
|
+
// tag on the current surface", and `ctx_expand` would answer "not a
|
|
72
|
+
// tool-result tag". Unlike opencode/pi — which stamp a throwaway per-call
|
|
73
|
+
// array, so the stamp never reaches the record — DSH has ONE surface shared
|
|
74
|
+
// by the model and the UI, and no wire-only seam. A dead handle is not
|
|
75
|
+
// free: it shows up in the conversation transcript, and `session-title`
|
|
76
|
+
// derives its deterministic fallback from the first `source: 'user'`
|
|
77
|
+
// message, so it becomes the session title too (measured live: seq 9
|
|
78
|
+
// `§1§ …` -> seq 15 `title: "§1§ 一个新的问题:现在会话"`). Only tool
|
|
79
|
+
// results carry handles; the stream is untouched.
|
|
80
|
+
const messages = decision.messages;
|
|
81
|
+
// Manual `/ctx-flush`: the command only persisted markers. Land them here,
|
|
82
|
+
// before the pressure decision, so the NEXT request sees the placeholders
|
|
83
|
+
// even when the ordinary execute band would defer. This is the same landing
|
|
84
|
+
// helper the automatic pass uses; it never starts a Historian or a summary.
|
|
85
|
+
// `flushLanded` also makes the pressure reading stale: the surface changed
|
|
86
|
+
// after that reading, so the same turn's force band must not reclaim a
|
|
87
|
+
// second batch against it.
|
|
88
|
+
let flushLanded = false;
|
|
89
|
+
const replacedTags = [];
|
|
90
|
+
const generationBeforeFlush = payload.agent.session.surface?.replaceGeneration;
|
|
91
|
+
try {
|
|
92
|
+
const flushTags = deps.stores.listFlushRequestedDrops(sessionId);
|
|
93
|
+
if (flushTags.length > 0) {
|
|
94
|
+
const landing = landPendingDrops({
|
|
95
|
+
session: payload.agent.session,
|
|
96
|
+
sessionId,
|
|
97
|
+
stores: deps.stores,
|
|
98
|
+
tags: flushTags,
|
|
99
|
+
journal: deps.journal,
|
|
100
|
+
onReplaced: tag => {
|
|
101
|
+
flushLanded = true;
|
|
102
|
+
replacedTags.push(tag);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
flushLanded = flushLanded || landing.landed.length > 0;
|
|
106
|
+
deps.stores.clearFlushRequests(sessionId, flushTags);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Keep the markers so a later preStep can retry; the command already
|
|
111
|
+
// told the user the change is pending, not applied. No session notice is
|
|
112
|
+
// appended: the flush lane must not mutate the surface outside landing.
|
|
113
|
+
deps.journal?.warn('error', `flush landing failed: ${errText(error)}`, { scope: 'flush', replacedTags }, sessionId);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
// A partial landing can throw after replacing a node but before the
|
|
117
|
+
// caller sees `landing`; the surface generation is the authoritative
|
|
118
|
+
// signal that this turn's pressure reading is stale.
|
|
119
|
+
if (generationBeforeFlush !== undefined
|
|
120
|
+
&& payload.agent.session.surface.replaceGeneration !== generationBeforeFlush) {
|
|
121
|
+
flushLanded = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Reduce guidance (desk section + Channel 1/2) is wired through the
|
|
125
|
+
// SystemPrompt service below — a static section and a dynamic context —
|
|
126
|
+
// not by rewriting messages here. The only preStep duty is the REAL USER
|
|
127
|
+
// TURN counter the sticky refire gap needs: observing the newest message
|
|
128
|
+
// id is idempotent per step, so repeated preSteps on one turn count once.
|
|
129
|
+
if (liveBoolean(config.reduceGuidance, true)) {
|
|
130
|
+
try {
|
|
131
|
+
const newest = messages.at(-1);
|
|
132
|
+
if (newest !== undefined)
|
|
133
|
+
reduceTracker.observeUserMessage(sessionId, newest.id);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// counting is advisory; never fail the turn for it
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Auto-search hint (upstream `auto-search-runner`): one hint per user turn,
|
|
140
|
+
// built from the user's own words. Awaited, because the hint has to ride
|
|
141
|
+
// the request it was computed for, but every path is fail-open and the
|
|
142
|
+
// search carries its own deadline.
|
|
143
|
+
//
|
|
144
|
+
// The hint does NOT append to the session here. This waterfall band runs
|
|
145
|
+
// BEFORE the host writes step/start and the first system/message, so a
|
|
146
|
+
// direct append on a fresh session's first step took surface node 0 and the
|
|
147
|
+
// loader rejected the log on replay (the 2026-10-09 protected-head
|
|
148
|
+
// incident). The hint instead travels inside the admitted batch: the
|
|
149
|
+
// appendix below joins `decision.messages`, which the host persists after
|
|
150
|
+
// its own system-prompt commits — the head always lands first, on every
|
|
151
|
+
// turn, and the hint still rides this request. First-half stopgap (skip
|
|
152
|
+
// while the surface was headless) retired by this carrier change.
|
|
153
|
+
let hintAppendix;
|
|
154
|
+
if (deps.searchHints !== undefined && liveBoolean(config.searchHint, true)) {
|
|
155
|
+
try {
|
|
156
|
+
const prompt = latestUserPromptText(messages);
|
|
157
|
+
if (prompt !== undefined) {
|
|
158
|
+
const outcome = await planSearchHint({
|
|
159
|
+
search: lane.searchForSession,
|
|
160
|
+
tracker: deps.searchHints,
|
|
161
|
+
...journal === undefined ? {} : { journal },
|
|
162
|
+
}, { sessionId, session: payload.agent.session, text: prompt.text, turnKey: prompt.turnKey });
|
|
163
|
+
if (outcome.action === 'hinted')
|
|
164
|
+
hintAppendix = outcome.message;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// a hint is an optimisation; never fail the turn for it
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Cross-project drift observation: the turn's own text is the only signal
|
|
172
|
+
// DSH gives about WHERE work is happening (there is no per-turn working
|
|
173
|
+
// directory). Foreign git roots in the newest message get one warn-level
|
|
174
|
+
// journal row per turn — the data that quantifies how often cross-project
|
|
175
|
+
// sessions actually happen (and whether facts land in the wrong project).
|
|
176
|
+
try {
|
|
177
|
+
const newestMessage = messages.at(-1);
|
|
178
|
+
const driftText = newestMessage === undefined
|
|
179
|
+
? ''
|
|
180
|
+
: newestMessage.content
|
|
181
|
+
.map(block => (block.type === 'text' && typeof block.text === 'string' ? block.text : ''))
|
|
182
|
+
.join('\n');
|
|
183
|
+
if (driftText.length >= 8) {
|
|
184
|
+
const drift = detectProjectDrift(driftText, payload.agent.session.header.cwd);
|
|
185
|
+
if (drift !== undefined) {
|
|
186
|
+
journal?.warn('project-drift', `cross-project work observed (${String(drift.foreignRoots.length)} foreign root(s))`, {
|
|
187
|
+
sessionRoot: drift.sessionRoot,
|
|
188
|
+
foreignRoots: drift.foreignRoots,
|
|
189
|
+
}, sessionId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// observation only; never fail the turn for it
|
|
195
|
+
}
|
|
196
|
+
// Register the session's project identity for the maintenance sweep HERE,
|
|
197
|
+
// not only in the pass-execute branch: a quiet conversation never crosses
|
|
198
|
+
// the compaction threshold, and the sweep then had no identity to serve —
|
|
199
|
+
// every tick spun empty (measured live 18:24, zero maintenance rows after
|
|
200
|
+
// a remount despite an active session).
|
|
201
|
+
try {
|
|
202
|
+
const projectSpec = config.memory?.project ?? payload.agent.session.header.cwd;
|
|
203
|
+
const memoryIdentity = projectSpec === undefined ? undefined : resolveIdentitySpec(projectSpec);
|
|
204
|
+
if (memoryIdentity !== undefined) {
|
|
205
|
+
seenIdentities.add(memoryIdentity);
|
|
206
|
+
sessionIdentity.set(sessionId, memoryIdentity);
|
|
207
|
+
if (sessionIdentity.size > 512) {
|
|
208
|
+
const oldest = sessionIdentity.keys().next().value;
|
|
209
|
+
if (oldest !== undefined)
|
|
210
|
+
sessionIdentity.delete(oldest);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// registration is advisory; never fail the turn for it
|
|
216
|
+
}
|
|
217
|
+
// ── Pressure decision + the execute-band management pass (Phase 1 + 2) ──
|
|
218
|
+
// The session surface is mutated only inside the execute band or the
|
|
219
|
+
// explicit `/ctx-flush` lane above: the execute band is where a priced
|
|
220
|
+
// bust buys reclaim (MC's "drops land on the next bust cycle", translated
|
|
221
|
+
// to durable surface); flush is the user's explicit exception.
|
|
222
|
+
try {
|
|
223
|
+
if (!seenSessions.has(sessionId)) {
|
|
224
|
+
seenSessions.add(sessionId);
|
|
225
|
+
journal?.info('session-first-seen', 'session first seen by the adapter', {
|
|
226
|
+
turn: payload.turn,
|
|
227
|
+
step: payload.step,
|
|
228
|
+
}, sessionId);
|
|
229
|
+
}
|
|
230
|
+
const latest = stores.latestUsage(sessionId);
|
|
231
|
+
const context = requestContextOf(payload.agent.session);
|
|
232
|
+
// Live thresholds: a settings-form edit reaches this pass immediately.
|
|
233
|
+
const policy = activePolicy();
|
|
234
|
+
// A route that discloses no window is scheduled against the assumed one:
|
|
235
|
+
// an undefined denominator defers 'no-window' on every step, so the
|
|
236
|
+
// execute band — and every drop waiting behind it — would never open.
|
|
237
|
+
const pressureWindow = pressureWindowTokens(context.contextWindow, policy);
|
|
238
|
+
const pressure = pressurePercent(latest, pressureWindow);
|
|
239
|
+
// A reading recorded before the injected surface last changed measures a
|
|
240
|
+
// prompt that no longer exists (a compartment landed / the baseline was
|
|
241
|
+
// re-frozen after it). It remains the only size signal for the pass
|
|
242
|
+
// decision, but the force band must not reclaim against it — that is how
|
|
243
|
+
// compaction ends up dropping a second batch of content for nothing.
|
|
244
|
+
const historyChangedAt = stores.latestHistoryChangeAt(sessionId);
|
|
245
|
+
const pressureStale = flushLanded || (latest !== undefined && historyChangedAt !== undefined
|
|
246
|
+
&& historyChangedAt > latest.recordedAt);
|
|
247
|
+
const pass = decidePass({
|
|
248
|
+
latest,
|
|
249
|
+
pressure,
|
|
250
|
+
now: Date.now(),
|
|
251
|
+
policy,
|
|
252
|
+
// Iron rule: a stranded tool_result 400s every strict-provider request,
|
|
253
|
+
// so it forces the execute band regardless of pressure.
|
|
254
|
+
wireDefects: wireStrandedResults(payload.agent.session).length,
|
|
255
|
+
});
|
|
256
|
+
const usage = latest === undefined ? undefined : {
|
|
257
|
+
input: latest.inputTokens ?? 0,
|
|
258
|
+
cacheRead: latest.cacheReadTokens ?? 0,
|
|
259
|
+
cacheWrite: latest.cacheWriteTokens ?? 0,
|
|
260
|
+
};
|
|
261
|
+
if (context.provider !== undefined && context.model !== undefined) {
|
|
262
|
+
deps.noteSessionRoute?.({ provider: context.provider, model: context.model });
|
|
263
|
+
}
|
|
264
|
+
const basePassData = {
|
|
265
|
+
reason: pass.reason,
|
|
266
|
+
pressure: pressure === undefined ? undefined : Number(pressure.toFixed(1)),
|
|
267
|
+
// Visible in the pass row: a stale reading explains why a high
|
|
268
|
+
// percentage did not reclaim (see the force-band guard).
|
|
269
|
+
...pressureStale ? { pressureStale: true } : {},
|
|
270
|
+
// The denominator the decision actually used, flagged when it was the
|
|
271
|
+
// assumption rather than the route's own reading.
|
|
272
|
+
contextWindow: pressureWindow,
|
|
273
|
+
...context.contextWindow === undefined ? { contextWindowAssumed: true } : {},
|
|
274
|
+
route: context.provider === undefined || context.model === undefined
|
|
275
|
+
? undefined
|
|
276
|
+
: `${context.provider}/${context.model}`,
|
|
277
|
+
usage,
|
|
278
|
+
};
|
|
279
|
+
if (pass.kind === 'execute') {
|
|
280
|
+
// The project identity is resolved once per pass (the git lookup is
|
|
281
|
+
// cached for the process). An explicit `git:`/`dir:` value is already
|
|
282
|
+
// an identity; only a directory has to be resolved.
|
|
283
|
+
const projectSpec = config.memory?.project ?? payload.agent.session.header.cwd;
|
|
284
|
+
const memoryIdentity = projectSpec === undefined
|
|
285
|
+
? undefined
|
|
286
|
+
: resolveIdentitySpec(projectSpec);
|
|
287
|
+
// The maintenance sweep maintains every identity this process has
|
|
288
|
+
// actually served (the configured project is added unconditionally),
|
|
289
|
+
// and the session→identity map scopes the telemetry panel's stats to
|
|
290
|
+
// the project the viewed conversation is working on.
|
|
291
|
+
if (memoryIdentity !== undefined) {
|
|
292
|
+
seenIdentities.add(memoryIdentity);
|
|
293
|
+
if (sessionId !== undefined) {
|
|
294
|
+
sessionIdentity.set(sessionId, memoryIdentity);
|
|
295
|
+
if (sessionIdentity.size > 512) {
|
|
296
|
+
const oldest = sessionIdentity.keys().next().value;
|
|
297
|
+
if (oldest !== undefined)
|
|
298
|
+
sessionIdentity.delete(oldest);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// WP10: keep the machine domain's membership in step with the projects
|
|
303
|
+
// this process has actually served, BEFORE the recall resolves peers —
|
|
304
|
+
// otherwise a project's first pass would read the domain without being
|
|
305
|
+
// in it. Idempotent, and a no-op until `memory.global.machine.enabled`.
|
|
306
|
+
if (memoryRecall !== undefined) {
|
|
307
|
+
syncMachineDomain(stores, {
|
|
308
|
+
identities: [...seenIdentities],
|
|
309
|
+
config: {
|
|
310
|
+
...config.memory?.global?.machine?.enabled === undefined
|
|
311
|
+
? {}
|
|
312
|
+
: { enabled: liveBoolean(config.memory?.global?.machine?.enabled, true) },
|
|
313
|
+
...config.memory?.global?.machine?.excludeProjects === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { excludeProjects: config.memory.global.machine.excludeProjects },
|
|
316
|
+
},
|
|
317
|
+
now: Date.now(),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const memories = memoryRecall?.recall({
|
|
321
|
+
...projectSpec === undefined ? {} : { project: projectSpec },
|
|
322
|
+
domainProjects: config.memory?.domainProjects,
|
|
323
|
+
shareCategories: config.memory?.shareCategories,
|
|
324
|
+
budgetTokens: policy.memoryBudgetTokens,
|
|
325
|
+
limit: policy.memoryLimit,
|
|
326
|
+
// B2 (MC m0_compose.rs:104-116): the frozen prefix evaluates expiry
|
|
327
|
+
// against the HARD-rebuild clock — a row expiring mid-cycle must not
|
|
328
|
+
// bust the cache. The next HARD rebuild re-composes with a live clock
|
|
329
|
+
// and freezes the new time.
|
|
330
|
+
now: stores.baselineState(sessionId)?.frozenAt ?? Date.now(),
|
|
331
|
+
// The additive lane: everything above the stored watermark is an
|
|
332
|
+
// in-session addition and renders in m[1], keeping m[0] byte-stable.
|
|
333
|
+
...stores.baselineState(sessionId)?.maxMemoryId === undefined
|
|
334
|
+
? {}
|
|
335
|
+
: { maxMemoryId: stores.baselineState(sessionId)?.maxMemoryId },
|
|
336
|
+
// S(t) decay scoring (schema v7): the sidecar stats + live config;
|
|
337
|
+
// `enabled` inside gates the ranking takeover, stats always ride.
|
|
338
|
+
...memoryIdentity === undefined ? {} : { scoring: lane.memoryScoring(memoryIdentity) },
|
|
339
|
+
});
|
|
340
|
+
// WP8: the user profile. Its own read with its own watermark — the
|
|
341
|
+
// profile is unaffected by project identity, and this lane can be
|
|
342
|
+
// switched off (`memory.global.user.render`, default true) without
|
|
343
|
+
// touching memory recall at all.
|
|
344
|
+
const profileEnabled = liveBoolean(config.memory?.global?.user?.render, true);
|
|
345
|
+
const profile = !profileEnabled || memoryRecall === undefined
|
|
346
|
+
? undefined
|
|
347
|
+
: memoryRecall.profileRecall({
|
|
348
|
+
// Frozen-prefix discipline, same as memory recall: the m[0] profile
|
|
349
|
+
// evaluates against the HARD-rebuild clock, not a live one.
|
|
350
|
+
now: stores.baselineState(sessionId)?.frozenAt ?? Date.now(),
|
|
351
|
+
budgetTokens: PROFILE_M0_BUDGET_TOKENS,
|
|
352
|
+
...stores.baselineState(sessionId)?.profileWatermark === undefined
|
|
353
|
+
? {}
|
|
354
|
+
: { maxId: stores.baselineState(sessionId)?.profileWatermark },
|
|
355
|
+
});
|
|
356
|
+
// A2 (MC getVisibleMemoryIds): the ids the injected block renders NOW
|
|
357
|
+
// are what ctx_memory search suppresses as already-visible. Replaced
|
|
358
|
+
// per pass, process-memory only, fail-open when absent.
|
|
359
|
+
if (memories !== undefined) {
|
|
360
|
+
visibleMemoryIdsBySession.delete(sessionId);
|
|
361
|
+
visibleMemoryIdsBySession.set(sessionId, new Set([
|
|
362
|
+
...memories.rows.map(row => row.id),
|
|
363
|
+
...memories.newRows.map(row => row.id),
|
|
364
|
+
]));
|
|
365
|
+
if (visibleMemoryIdsBySession.size > 256) {
|
|
366
|
+
const oldest = visibleMemoryIdsBySession.keys().next().value;
|
|
367
|
+
if (oldest !== undefined)
|
|
368
|
+
visibleMemoryIdsBySession.delete(oldest);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// S(t) sidecar (schema v7): recall injections strengthen the rows for
|
|
372
|
+
// future ranking, and the orphan sweep drops stats rows whose memory
|
|
373
|
+
// left the visible pool. Shadow collection — runs whether or not the
|
|
374
|
+
// ranking takeover is enabled. Fail-open: scoring stats are advisory.
|
|
375
|
+
if (memories !== undefined && memoryIdentity !== undefined) {
|
|
376
|
+
try {
|
|
377
|
+
const statsNow = Date.now();
|
|
378
|
+
for (const row of memories.rows)
|
|
379
|
+
stores.recordMemoryEvent(memoryIdentity, row.id, 'injection', statsNow);
|
|
380
|
+
for (const row of memories.newRows)
|
|
381
|
+
stores.recordMemoryEvent(memoryIdentity, row.id, 'injection', statsNow);
|
|
382
|
+
const knownIds = new Set(memories.eligibleIds);
|
|
383
|
+
for (const row of memories.newRows)
|
|
384
|
+
knownIds.add(row.id);
|
|
385
|
+
stores.pruneMemoryStats(memoryIdentity, [...knownIds]);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
/* scoring stats are advisory */
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// The watermark is read straight from the store, so a HARD rebuild can
|
|
392
|
+
// freeze it even when this pass recalled no rows at all (the first
|
|
393
|
+
// pass of a fresh session, which is exactly when it matters).
|
|
394
|
+
const currentMaxMemoryId = memoryRecall?.maxMemoryId(projectSpec) ?? 0;
|
|
395
|
+
// The NON-additive lane (contract §6): `memory_mutation_log` rows above
|
|
396
|
+
// the frozen cursor render as `<memory-updates>` in the m[1] region, so
|
|
397
|
+
// an update/archive/supersede reaches the model without rebuilding m[0].
|
|
398
|
+
// `afterId` is the cursor frozen by the last HARD rebuild (0 for a
|
|
399
|
+
// session that never folded), and the epoch fingerprint is passed on
|
|
400
|
+
// EVERY pass: management-pass only flags an epoch change when both the
|
|
401
|
+
// previous fingerprint and this one are present.
|
|
402
|
+
const mutations = memoryRecall?.mutations({
|
|
403
|
+
...projectSpec === undefined ? {} : { project: projectSpec },
|
|
404
|
+
domainProjects: config.memory?.domainProjects,
|
|
405
|
+
shareCategories: config.memory?.shareCategories,
|
|
406
|
+
afterId: stores.baselineState(sessionId)?.maxMemoryMutationId ?? 0,
|
|
407
|
+
limit: policy.memoryLimit,
|
|
408
|
+
now: Date.now(),
|
|
409
|
+
});
|
|
410
|
+
if (memories !== undefined && (memories.lines.length > 0 || memories.newLines.length > 0)) {
|
|
411
|
+
journal?.info('memory-recall', `recalled ${String(memories.rows.length)} project memories into m[0]`
|
|
412
|
+
+ `${memories.newLines.length > 0 ? `, ${String(memories.newLines.length)} new in m[1]` : ''}`, {
|
|
413
|
+
rows: memories.rows.length,
|
|
414
|
+
newRows: memories.newLines.length,
|
|
415
|
+
digest: memories.digest.slice(0, 12),
|
|
416
|
+
budgetTokens: policy.memoryBudgetTokens,
|
|
417
|
+
estTokens: memories.lines.reduce((total, line) => total + estimateTokens(line), 0),
|
|
418
|
+
identity: memories.identity,
|
|
419
|
+
maxMemoryId: memories.currentMaxMemoryId,
|
|
420
|
+
domains: memories.domains,
|
|
421
|
+
members: memories.members?.length,
|
|
422
|
+
projects: [...new Set(memories.rows.map(row => row.projectPath.slice(0, 24)))],
|
|
423
|
+
}, sessionId);
|
|
424
|
+
}
|
|
425
|
+
if (profile !== undefined && (profile.lines.length > 0 || profile.newLines.length > 0)) {
|
|
426
|
+
journal?.info('profile-recall', `injected ${String(profile.lines.length)} profile row(s) into m[0]`
|
|
427
|
+
+ `${profile.newLines.length > 0 ? `, ${String(profile.newLines.length)} new in m[1]` : ''}`, {
|
|
428
|
+
rows: profile.rows.length,
|
|
429
|
+
newRows: profile.newRows.length,
|
|
430
|
+
digest: profile.digest.slice(0, 12),
|
|
431
|
+
watermark: stores.baselineState(sessionId)?.profileWatermark,
|
|
432
|
+
currentMaxId: profile.currentMaxId,
|
|
433
|
+
estTokens: profile.lines.reduce((total, line) => total + estimateTokens(line), 0),
|
|
434
|
+
}, sessionId);
|
|
435
|
+
}
|
|
436
|
+
// The generation pin is a live settings-form field: re-read it every
|
|
437
|
+
// pass, and treat a half-filled pair as "follow the session route".
|
|
438
|
+
const generationRoute = liveRoute(config.historian?.provider, config.historian?.model);
|
|
439
|
+
const generationEffort = liveString(config.historian?.reasoningEffort, '').trim();
|
|
440
|
+
// Auto follows the session route; the effort still applies. Only when
|
|
441
|
+
// neither a pin nor a live session route exists is there nothing to
|
|
442
|
+
// attach it to.
|
|
443
|
+
const sessionRoute = context.provider === undefined || context.model === undefined
|
|
444
|
+
? undefined
|
|
445
|
+
: { provider: context.provider, model: context.model };
|
|
446
|
+
const historianBaseRoute = generationRoute ?? sessionRoute;
|
|
447
|
+
const historianRoute = historianBaseRoute === undefined
|
|
448
|
+
? undefined
|
|
449
|
+
: generationEffort === ''
|
|
450
|
+
? historianBaseRoute
|
|
451
|
+
: { ...historianBaseRoute, reasoningEffort: generationEffort };
|
|
452
|
+
const outcome = runManagementPass({
|
|
453
|
+
session: payload.agent.session,
|
|
454
|
+
sessionId,
|
|
455
|
+
stores,
|
|
456
|
+
policy,
|
|
457
|
+
logger: ctx.logger,
|
|
458
|
+
pass,
|
|
459
|
+
pressure,
|
|
460
|
+
...pressureStale ? { pressureStale: true } : {},
|
|
461
|
+
...journal === undefined ? {} : { journal },
|
|
462
|
+
...context.provider === undefined || context.model === undefined
|
|
463
|
+
? {}
|
|
464
|
+
: { route: { provider: context.provider, model: context.model } },
|
|
465
|
+
...context.contextWindow === undefined ? {} : { contextWindow: context.contextWindow },
|
|
466
|
+
...liveBoolean(config.historianEnabled, true) === false || llm === undefined
|
|
467
|
+
? {}
|
|
468
|
+
: {
|
|
469
|
+
historian: {
|
|
470
|
+
llm,
|
|
471
|
+
...historianRoute === undefined ? {} : { route: historianRoute },
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
...memoryRecall === undefined
|
|
475
|
+
? {}
|
|
476
|
+
: {
|
|
477
|
+
memories: {
|
|
478
|
+
digest: memories?.digest ?? '',
|
|
479
|
+
lines: memories?.lines ?? [],
|
|
480
|
+
newLines: memories?.newLines ?? [],
|
|
481
|
+
// An `update` on a memory that is still above the watermark
|
|
482
|
+
// writes a mutation-log row without changing the id, so the row
|
|
483
|
+
// would otherwise render once in `<new-memories>` (current
|
|
484
|
+
// content) and again in `<memory-updates>` (same content).
|
|
485
|
+
// Passing the ids lets the renderer suppress the duplicate.
|
|
486
|
+
newMemoryIds: memories?.newRows.map(row => row.id) ?? [],
|
|
487
|
+
// MC only announces a mutation whose target is a row the m[0]
|
|
488
|
+
// baseline actually rendered (inject-compartments.ts:2843-2848):
|
|
489
|
+
// mutations for budget-truncated rows would be phantom pointers.
|
|
490
|
+
baselineIds: memories?.rows.map(row => row.id) ?? [],
|
|
491
|
+
// A1: the pre-budget visible set — the authority for forced
|
|
492
|
+
// rows in `<memory-updates>`/`<new-memories>`
|
|
493
|
+
// (inject-compartments.ts:2822-2869).
|
|
494
|
+
eligibleIds: memories?.eligibleIds ?? [],
|
|
495
|
+
eligibleRows: memories?.eligibleRows ?? [],
|
|
496
|
+
maxMemoryId: currentMaxMemoryId,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
...profile === undefined ? {} : { profile },
|
|
500
|
+
// `cursor` is deliberately the CURRENT max mutation id, not the paged
|
|
501
|
+
// read cursor: on a HARD rebuild m[0] swallows everything up to now,
|
|
502
|
+
// so freezing a partial page would re-render rows it already folded.
|
|
503
|
+
...mutations === undefined
|
|
504
|
+
? {}
|
|
505
|
+
: {
|
|
506
|
+
memoryUpdates: {
|
|
507
|
+
updates: mutations.updates,
|
|
508
|
+
cursor: mutations.currentCursor,
|
|
509
|
+
projectEpochFingerprint: mutations.projectEpochFingerprint,
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
...memoryWriter === undefined || memoryIdentity === undefined || config.memory?.autoPromote === false
|
|
513
|
+
? {}
|
|
514
|
+
: { memoryPromote: (promote) => memoryWriter.promoteFacts(promote), memoryIdentity },
|
|
515
|
+
// WP5: the `user` domain's collection switch — read LIVE, so it can be
|
|
516
|
+
// flipped without a remount, and default OFF because this lane stages
|
|
517
|
+
// rows in the shared contract. The historian journals what it staged
|
|
518
|
+
// (including what normalization dropped), so the switch's effect is
|
|
519
|
+
// visible from the journal rather than inferred from the pool size.
|
|
520
|
+
...profileWriter === undefined || profileWriter.status.ok !== true
|
|
521
|
+
|| !liveBoolean(config.memory?.global?.user?.collect, true)
|
|
522
|
+
? {}
|
|
523
|
+
: {
|
|
524
|
+
profileCollect: (collect) => profileWriter.recordCandidates(collect),
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
journal?.info('pass', `execute (${pass.reason}) at ${pressure?.toFixed(1) ?? '?'}%`, {
|
|
528
|
+
decision: 'execute',
|
|
529
|
+
...basePassData,
|
|
530
|
+
landed: outcome.landedDrops,
|
|
531
|
+
published: outcome.published,
|
|
532
|
+
forceDropped: outcome.forceDropped,
|
|
533
|
+
baseline: outcome.baselineUpdated ? 'refreshed' : 'unchanged',
|
|
534
|
+
historianStarted: outcome.historianStarted,
|
|
535
|
+
// Net reclaim proof: surface estimate before/after this pass.
|
|
536
|
+
tokensBefore: outcome.tokensBefore,
|
|
537
|
+
tokensAfter: outcome.tokensAfter,
|
|
538
|
+
tokensDelta: outcome.tokensAfter - outcome.tokensBefore,
|
|
539
|
+
factsPromoted: outcome.factsPromoted,
|
|
540
|
+
factsSeenAgain: outcome.factsSeenAgain,
|
|
541
|
+
}, sessionId);
|
|
542
|
+
// Channel A of the live session stream: one notice entry per pass
|
|
543
|
+
// that actually did something (publish / force-drop / promotion).
|
|
544
|
+
// `appendSessionNotice` is fail-open, quiet on nothing-notable, and
|
|
545
|
+
// refuses a headless surface (an out-of-band append before the host
|
|
546
|
+
// wrote surface node 0 would make the log unloadable — 2026-10-09
|
|
547
|
+
// incident; see docs/dsh-surface-write-audit-2026-10-09.md). A refusal
|
|
548
|
+
// means the execute band opened on a headless surface, which should be
|
|
549
|
+
// impossible (no surface ⇒ no usage and no wire defects ⇒ defer), so it
|
|
550
|
+
// is journaled as an anomaly rather than silently dropped.
|
|
551
|
+
if (liveBoolean(config.sessionNotices, true)) {
|
|
552
|
+
const notice = appendSessionNotice({ session: payload.agent.session, result: outcome });
|
|
553
|
+
if (notice.ok) {
|
|
554
|
+
journal?.info('session-notice', notice.summary, { seq: notice.seq }, sessionId);
|
|
555
|
+
}
|
|
556
|
+
else if (notice.reason === 'headless-surface') {
|
|
557
|
+
journal?.warn('session-notice', 'notice suppressed: session surface has no protected head yet', {
|
|
558
|
+
reason: notice.reason,
|
|
559
|
+
}, sessionId);
|
|
560
|
+
}
|
|
561
|
+
else if (notice.reason === 'append-failed') {
|
|
562
|
+
journal?.warn('session-notice', `notice append failed: ${notice.detail ?? '?'}`, {
|
|
563
|
+
detail: notice.detail,
|
|
564
|
+
}, sessionId);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
deferStreak.delete(sessionId);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// Defer passes repeat every step: record the reason TRANSITIONS only,
|
|
571
|
+
// carrying how many consecutive defers shared the current reason, so
|
|
572
|
+
// the journal stays reviewable instead of flooding.
|
|
573
|
+
const streak = deferStreak.get(sessionId);
|
|
574
|
+
if (streak === undefined || streak.reason !== pass.reason) {
|
|
575
|
+
deferStreak.set(sessionId, { reason: pass.reason, count: 1 });
|
|
576
|
+
journal?.info('pass', `defer (${pass.reason}) at ${pressure?.toFixed(1) ?? '?'}%`, {
|
|
577
|
+
decision: 'defer',
|
|
578
|
+
...basePassData,
|
|
579
|
+
consecutive: 1,
|
|
580
|
+
}, sessionId);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
streak.count += 1;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
const detail = errText(error);
|
|
589
|
+
journal?.error('error', `management pass failed: ${detail}`, { scope: 'pre-step', detail }, sessionId);
|
|
590
|
+
// A failed management pass must never wedge the turn: log and continue
|
|
591
|
+
// with the tagged messages (MC per-turn fail-open). The next pass
|
|
592
|
+
// re-decides from durable state.
|
|
593
|
+
ctx.logger.warn(`magic-context: management pass failed (continuing): ${detail}`);
|
|
594
|
+
}
|
|
595
|
+
return { ...decision, messages: hintAppendix === undefined ? messages : [...messages, hintAppendix] };
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Reduce guidance through the SystemPrompt service (MC parity): a STATIC desk
|
|
600
|
+
* section (identical bytes every assembly, so the system prompt stays
|
|
601
|
+
* prefix-cache stable) plus a DYNAMIC context whose provider re-measures the
|
|
602
|
+
* unstamped tail at every assembly and renders the MC reminder copy only when
|
|
603
|
+
* a band fires — an empty text contributes nothing. The listeners dispose with
|
|
604
|
+
* the inject fiber (a scope reload re-runs the whole inject).
|
|
605
|
+
*/
|
|
606
|
+
export function injectReduceGuidance(deps) {
|
|
607
|
+
const { ctx, config, stores, journal, reduceTracker, lane } = deps;
|
|
608
|
+
if (liveBoolean(config.reduceGuidance, true) === false)
|
|
609
|
+
return;
|
|
610
|
+
ctx.inject(['systemPrompt'], (scope) => {
|
|
611
|
+
const systemPrompt = scope.systemPrompt;
|
|
612
|
+
if (systemPrompt === undefined)
|
|
613
|
+
return;
|
|
614
|
+
scope.effect(() => systemPrompt.section({
|
|
615
|
+
name: 'magic-context-desk',
|
|
616
|
+
text: buildDeskSection(lane.memoryEnabled),
|
|
617
|
+
order: 900,
|
|
618
|
+
}));
|
|
619
|
+
scope.effect(() => systemPrompt.context({
|
|
620
|
+
name: 'magic-context-reduce',
|
|
621
|
+
order: 910,
|
|
622
|
+
text: (assembly) => {
|
|
623
|
+
try {
|
|
624
|
+
const agent = assembly.agent;
|
|
625
|
+
const session = agent?.session;
|
|
626
|
+
if (session === undefined)
|
|
627
|
+
return '';
|
|
628
|
+
const sessionId = session.id;
|
|
629
|
+
const tagged = enumerateSurfaceToolResults(session, callId => stores.getTag(sessionId, { kind: 'tool-result', callId }))
|
|
630
|
+
.filter(node => node.tagNumber !== undefined);
|
|
631
|
+
if (tagged.length === 0)
|
|
632
|
+
return '';
|
|
633
|
+
const tailToolTokens = tagged.reduce((total, node) => total + Math.ceil(node.textLength / 4), 0);
|
|
634
|
+
const undropped = tagged.filter(node => !node.isPlaceholder);
|
|
635
|
+
const undroppedTokens = undropped.reduce((total, node) => total + Math.ceil(node.textLength / 4), 0);
|
|
636
|
+
const state = reduceTracker.state(sessionId);
|
|
637
|
+
const nudge = decideReduceNudge({
|
|
638
|
+
tailToolTokens,
|
|
639
|
+
undroppedTokens,
|
|
640
|
+
realUserTurnCount: state.realUserTurnCount,
|
|
641
|
+
...state.lastBand === undefined ? {} : { lastBand: state.lastBand },
|
|
642
|
+
lastOrdinal: state.lastOrdinal,
|
|
643
|
+
...state.lastOrdinalTurn === undefined ? {} : { lastOrdinalTurn: state.lastOrdinalTurn },
|
|
644
|
+
});
|
|
645
|
+
// The turn MUST be the session's real user-turn count, not a literal:
|
|
646
|
+
// record persists it as lastOrdinalTurn, and decideReduceNudge's
|
|
647
|
+
// sticky gap is realUserTurnCount - lastOrdinalTurn. A literal 0 made
|
|
648
|
+
// the gap read as the WHOLE turn count, so any session past 5 user
|
|
649
|
+
// turns re-fired its channel2 reminder on every system-prompt
|
|
650
|
+
// assembly (measured live 2026-10-25: 33 reminders in 11 minutes,
|
|
651
|
+
// each double-journaled by the host's assemble→preStep-append→
|
|
652
|
+
// reassemble cycle).
|
|
653
|
+
reduceTracker.record(sessionId, nudge, state.realUserTurnCount);
|
|
654
|
+
if (!nudge.fire || nudge.level === undefined)
|
|
655
|
+
return '';
|
|
656
|
+
const hints = undropped.slice(0, 4).map(node => ({ tagNumber: node.tagNumber }));
|
|
657
|
+
const reminder = nudge.level === 'channel2'
|
|
658
|
+
? buildChannel2Reminder(undroppedTokens, undropped.length, hints)
|
|
659
|
+
: buildChannel1Reminder(nudge.level, undroppedTokens, undropped.length, hints, nudge.sticky);
|
|
660
|
+
journal?.info('pass', `reduce nudge (${nudge.level})`, {
|
|
661
|
+
decision: 'reduce-nudge',
|
|
662
|
+
band: nudge.band,
|
|
663
|
+
tailToolTokens,
|
|
664
|
+
undroppedTokens,
|
|
665
|
+
}, sessionId);
|
|
666
|
+
return reminder;
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// A closed store mid-remount must degrade to no reminder.
|
|
670
|
+
return '';
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
}));
|
|
674
|
+
});
|
|
675
|
+
}
|