@treeseed/dsh-chapters 0.1.24 → 0.2.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 +2 -1
- package/cordis.patch.yml +49 -1
- package/docs/architecture.md +1 -496
- package/docs/backlog.md +234 -4
- package/docs/contract.md +127 -1
- package/docs/development.md +248 -12
- package/docs/host-compaction-seam.md +108 -0
- package/docs/knowledge-repo.md +532 -16
- package/docs/project-state.md +323 -0
- package/docs/verify.md +217 -0
- package/lib/archive.d.ts +49 -1
- package/lib/archive.js +56 -2
- package/lib/archive.js.map +1 -1
- package/lib/arrival.js +24 -10
- package/lib/arrival.js.map +1 -1
- package/lib/attach.d.ts +46 -0
- package/lib/attach.js +120 -0
- package/lib/attach.js.map +1 -0
- package/lib/auto-save.d.ts +74 -0
- package/lib/auto-save.js +162 -26
- package/lib/auto-save.js.map +1 -1
- package/lib/client.js +413 -11
- package/lib/client.js.map +1 -1
- package/lib/commands.d.ts +8 -1
- package/lib/commands.js +135 -7
- package/lib/commands.js.map +1 -1
- package/lib/compose.js +25 -6
- package/lib/compose.js.map +1 -1
- package/lib/continue-core.d.ts +24 -1
- package/lib/continue-core.js +41 -9
- package/lib/continue-core.js.map +1 -1
- package/lib/diag.d.ts +47 -0
- package/lib/diag.js +141 -0
- package/lib/diag.js.map +1 -0
- package/lib/engine-core.d.ts +51 -1
- package/lib/engine-core.js +63 -7
- package/lib/engine-core.js.map +1 -1
- package/lib/engine.d.ts +93 -0
- package/lib/engine.js +477 -36
- package/lib/engine.js.map +1 -1
- package/lib/enrich-queue.js +28 -2
- package/lib/enrich-queue.js.map +1 -1
- package/lib/enrich-wire.d.ts +92 -2
- package/lib/enrich-wire.js +135 -17
- package/lib/enrich-wire.js.map +1 -1
- package/lib/enrich.d.ts +27 -2
- package/lib/enrich.js +114 -18
- package/lib/enrich.js.map +1 -1
- package/lib/index.d.ts +51 -0
- package/lib/index.js +180 -2
- package/lib/index.js.map +1 -1
- package/lib/indexing.d.ts +7 -0
- package/lib/indexing.js.map +1 -1
- package/lib/injections.d.ts +1 -0
- package/lib/injections.js +38 -0
- package/lib/injections.js.map +1 -1
- package/lib/last-resort.d.ts +24 -0
- package/lib/last-resort.js +27 -0
- package/lib/last-resort.js.map +1 -0
- package/lib/prompt.d.ts +130 -0
- package/lib/prompt.js +255 -0
- package/lib/prompt.js.map +1 -0
- package/lib/registry.d.ts +39 -1
- package/lib/registry.js +76 -9
- package/lib/registry.js.map +1 -1
- package/lib/render.d.ts +31 -3
- package/lib/render.js +75 -3
- package/lib/render.js.map +1 -1
- package/lib/repair-run.d.ts +49 -0
- package/lib/repair-run.js +423 -0
- package/lib/repair-run.js.map +1 -0
- package/lib/repair.d.ts +200 -0
- package/lib/repair.js +354 -0
- package/lib/repair.js.map +1 -0
- package/lib/rules-commands.d.ts +22 -0
- package/lib/rules-commands.js +177 -20
- package/lib/rules-commands.js.map +1 -1
- package/lib/rules.d.ts +184 -7
- package/lib/rules.js +312 -27
- package/lib/rules.js.map +1 -1
- package/lib/save-core.d.ts +31 -1
- package/lib/save-core.js +153 -17
- package/lib/save-core.js.map +1 -1
- package/lib/session-rules.d.ts +222 -0
- package/lib/session-rules.js +454 -0
- package/lib/session-rules.js.map +1 -0
- package/lib/store.d.ts +22 -1
- package/lib/store.js +94 -17
- package/lib/store.js.map +1 -1
- package/lib/sync.js +13 -1
- package/lib/sync.js.map +1 -1
- package/lib/tools.d.ts +14 -0
- package/lib/tools.js +311 -32
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +12 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +9 -0
- package/lib/version.js +10 -0
- package/lib/version.js.map +1 -0
- package/lib/warnings.d.ts +10 -0
- package/lib/warnings.js +18 -0
- package/lib/warnings.js.map +1 -0
- package/package.json +9 -3
package/lib/engine.js
CHANGED
|
@@ -13,14 +13,16 @@
|
|
|
13
13
|
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
|
|
14
14
|
import { extractSignature, enrichSignatureWithCodegraph, turnSpanOf } from "./signature.js";
|
|
15
15
|
import { appendCollection } from "./registry.js";
|
|
16
|
-
import {
|
|
16
|
+
import { diagnosticNote, diagnosticWarn, ensureDiagnosticsDefault } from "./diag.js";
|
|
17
|
+
import { makeChildArchiveListener } from "./auto-save.js";
|
|
17
18
|
import z from '@deepseek-ai/schemastery';
|
|
18
|
-
import { CHAPTERS_PROVIDER, DETERMINISTIC_MODEL, buildFinalizedChapters, findOpenCompactionId, planSummarize, reconstructShadowedSeqs, scanChaptersSummaries, } from "./engine-core.js";
|
|
19
|
+
import { assertReservationMatchesPlan, CHAPTERS_PROVIDER, DETERMINISTIC_MODEL, buildFinalizedChapters, findOpenCompactionId, planSummarize, reconstructShadowedSeqs, scanChaptersSummaries, scanAndFinalize, } from "./engine-core.js";
|
|
19
20
|
import { composeChapters } from "./compose.js";
|
|
20
21
|
import { carryActivePlan, detectPlans } from "./plans.js";
|
|
21
|
-
import { stripReminderSpans } from "./injections.js";
|
|
22
|
+
import { isInjectedProse, stripReminderSpans } from "./injections.js";
|
|
22
23
|
import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
23
24
|
import { applyArrivalStubs } from "./arrival.js";
|
|
25
|
+
import { lastResortCeilingTokens } from "./last-resort.js";
|
|
24
26
|
import { extractPlot } from "./engine-core.js";
|
|
25
27
|
import { appendChapters, isFinalized, markFinalized, rememberPlan, reserve } from "./registry.js";
|
|
26
28
|
import { acquireChapterStore, makeAllocator, makeArchiveFs, resolveStoreRoot } from "./store.js";
|
|
@@ -29,6 +31,10 @@ import { registerProvider } from "./provider.js";
|
|
|
29
31
|
import { createTreedxProvider } from "./treedx/provider.js";
|
|
30
32
|
import { createEnrichWiring } from "./enrich-wire.js";
|
|
31
33
|
import { writeArchive } from "./archive.js";
|
|
34
|
+
import { ENRICH_MAX_TOKENS_DEFAULT } from "./enrich.js";
|
|
35
|
+
import { hostname } from 'node:os';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
import { captureMinedRules, makeHeaderParentReader, makeRuleCaptureListener, renderRulesBlock } from "./session-rules.js";
|
|
32
38
|
/**
|
|
33
39
|
* Per-turn deterministic signature listener (knowledge-repo.md §4.1), as a
|
|
34
40
|
* plain factory so the LOGIC is unit-testable against the real host Session
|
|
@@ -65,13 +71,19 @@ export function makeSignatureListener(deps) {
|
|
|
65
71
|
// CodeGraph symbol verification is async and fire-and-forget. The cwd
|
|
66
72
|
// comes from the session header; missing cwd or missing CLI → silent no-op.
|
|
67
73
|
const cwd = session?.header?.cwd;
|
|
74
|
+
// #61: the compaction realm is where the SAIP finalization failures happened, and all three left
|
|
75
|
+
// zero trace because the sink had to be configured before it existed. Resolve it from the session
|
|
76
|
+
// that is compacting, first one wins.
|
|
77
|
+
if (cwd !== undefined && deps.storeRoot !== undefined) {
|
|
78
|
+
ensureDiagnosticsDefault(path.join(cwd, deps.storeRoot, 'diagnostics.log'));
|
|
79
|
+
}
|
|
68
80
|
const enrichPromise = cwd !== undefined
|
|
69
81
|
? enrichSignatureWithCodegraph(sig, span, deps.subprocess, cwd).catch(() => { })
|
|
70
82
|
: Promise.resolve();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}).then(() => undefined).catch(deps.warn);
|
|
83
|
+
// One serialized section (#59): the signature listener used to get→append→put with the whole
|
|
84
|
+
// row snapshot, so a turn that landed during another door's await could erase that door's
|
|
85
|
+
// committed state. The lost signature is what dropped a COMPLETED turn out of the archive.
|
|
86
|
+
const job = () => enrichPromise.then(() => deps.store()).then(({ store }) => store.mutate(id, (st) => appendCollection(st, sig))).then(() => undefined).catch(deps.warn);
|
|
75
87
|
const prev = chains.get(id);
|
|
76
88
|
const next = prev === undefined ? job() : prev.then(job);
|
|
77
89
|
chains.set(id, next);
|
|
@@ -106,6 +118,28 @@ export function makeFirstTurnPullListener(deps) {
|
|
|
106
118
|
}
|
|
107
119
|
};
|
|
108
120
|
}
|
|
121
|
+
export function carryOpenRequest(parts, messages, capChars) {
|
|
122
|
+
// ONE predicate for "is this injected machine prose" — `src/injections.ts`, the same screen the
|
|
123
|
+
// archive body and the title chooser use. A second list here is the #59 bug re-made: it would lag
|
|
124
|
+
// the first, and a drift row exists precisely to catch that (it caught this, minutes after I wrote it).
|
|
125
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
126
|
+
const m = messages[i];
|
|
127
|
+
if (m?.role !== 'user')
|
|
128
|
+
continue;
|
|
129
|
+
const c = m.content;
|
|
130
|
+
const text = typeof c === 'string' ? c
|
|
131
|
+
: Array.isArray(c) ? c.filter((b) => b?.type === 'text').map((b) => b.text ?? '').join('\n') : '';
|
|
132
|
+
const t = text.trim();
|
|
133
|
+
if (t === '' || isInjectedProse(t))
|
|
134
|
+
continue;
|
|
135
|
+
const clipped = t.length > capChars;
|
|
136
|
+
const body = clipped
|
|
137
|
+
? `${t.slice(0, capChars)}\n[carried in part: ${String(t.length)} characters total, ${String(capChars)} included — the whole turn stays in the durable log]`
|
|
138
|
+
: t;
|
|
139
|
+
return [...parts, { type: 'text', text: `OPEN REQUEST (verbatim; this relief shadows the turn that asked it):\n"""\n${body}\n"""` }];
|
|
140
|
+
}
|
|
141
|
+
return [...parts];
|
|
142
|
+
}
|
|
109
143
|
export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
110
144
|
// Base inject is ['llm','tokenMeter','sessions']; subclassing INHERITS statics,
|
|
111
145
|
// so re-declare with what the chapter flow adds (r12's crash, FINDINGS).
|
|
@@ -136,6 +170,10 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
136
170
|
})),
|
|
137
171
|
auto: z.boolean(),
|
|
138
172
|
artifactStoreRoot: z.string(),
|
|
173
|
+
knowledgeMirrorDir: z.string().default('.dsh-knowledge'),
|
|
174
|
+
openRequestCarryMaxChars: z.number().step(1).min(200).default(4000),
|
|
175
|
+
/** #78 A: the last-resort deferral switch. Operator-visible, default on. */
|
|
176
|
+
lastResortArtifact: z.boolean().default(true),
|
|
139
177
|
chapterTokenTarget: z.number().step(1).min(1),
|
|
140
178
|
toolResultDeferFloorTokens: z.number().step(1).min(0),
|
|
141
179
|
// The composition knobs ride the same row so the engine path and the fork
|
|
@@ -145,12 +183,29 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
145
183
|
chapterLimit: z.number().step(1).min(1).default(8000),
|
|
146
184
|
syncDebounceMs: z.number().step(1).min(0).default(30000),
|
|
147
185
|
toolResultArtifactTokens: z.number().step(1).min(256).default(8000),
|
|
186
|
+
// #61 Amendment A — the seventh archive moment. A `ch-*` continuation child has origin `user`, so
|
|
187
|
+
// door 5 never sees it: the field case archived NOTHING across 2,363 events because no moment was
|
|
188
|
+
// watching. Save-only, fires at turn/end, thresholds are config and never literals.
|
|
189
|
+
childAutoArchive: z.boolean().default(true),
|
|
190
|
+
childAutoArchiveMinTurns: z.number().step(1).min(1).default(4),
|
|
148
191
|
elicitedPlot: z.boolean().default(true),
|
|
192
|
+
harnessId: z.string().default(''),
|
|
193
|
+
sessionRulesEnabled: z.boolean().default(true),
|
|
194
|
+
rulesMarkerPrefix: z.string().default('ADD RULE:'),
|
|
195
|
+
coreRulesBudgetTokens: z.number().step(1).min(1).default(1200),
|
|
196
|
+
projectRulesDisplayCap: z.number().step(1).min(0).default(12),
|
|
197
|
+
sessionRulesDisplayCap: z.number().step(1).min(0).default(12),
|
|
198
|
+
rulesRelevance: z.boolean().default(true),
|
|
199
|
+
rulesEnrichment: z.boolean().default(true),
|
|
200
|
+
rulesMinedPerChapter: z.number().step(1).min(0).default(3),
|
|
201
|
+
rulesAutoActivate: z.boolean().default(true),
|
|
202
|
+
rulesModel: z.string().default(''),
|
|
149
203
|
enrichmentEnabled: z.boolean().default(true),
|
|
150
204
|
enrichmentModel: z.string().default(''),
|
|
151
205
|
enrichmentTrigger: z.string().default('both'),
|
|
152
206
|
enrichmentIdleMs: z.number().step(1).min(0).default(60000),
|
|
153
207
|
enrichmentBatchCap: z.number().step(1).min(1).default(5),
|
|
208
|
+
enrichmentMaxTokens: z.number().step(1).min(400).default(ENRICH_MAX_TOKENS_DEFAULT),
|
|
154
209
|
vocabApply: z.boolean().default(false),
|
|
155
210
|
vocabCoMin: z.number().step(1).min(2).default(3),
|
|
156
211
|
vocabOverlapMin: z.number().default(0.5),
|
|
@@ -162,13 +217,33 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
162
217
|
treedxLeaseRetryDelayMs: z.number().step(1).min(0).default(1000),
|
|
163
218
|
});
|
|
164
219
|
chaptersConfig;
|
|
220
|
+
/** #50 §7.5/§7.3: capture + rendering numbers, host plane mirrored in. */
|
|
221
|
+
ruleCfg = { harnessId: '', captureEnabled: false, markerPrefix: 'ADD RULE:', budgetTokens: 1200, projectCap: 12, sessionCap: 12, relevance: true, mine: true, mineCap: 3, autoActivate: true, model: '' };
|
|
165
222
|
storePromise = null;
|
|
223
|
+
/** #52: the delegation-edge reader, built lazily because a field initializer
|
|
224
|
+
* cannot see the constructor parameter and the realm may hold neither service.
|
|
225
|
+
* null = not built yet; undefined = built and unavailable (registry edges only). */
|
|
226
|
+
parentOfReader = null;
|
|
227
|
+
/** The reader the rule-ancestry walk uses. Absent ⇒ delegation contributes no edge. */
|
|
228
|
+
ruleParentOf() {
|
|
229
|
+
if (this.parentOfReader === null) {
|
|
230
|
+
this.parentOfReader = makeHeaderParentReader((name) => {
|
|
231
|
+
try {
|
|
232
|
+
return this.ctx.get?.(name);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return this.parentOfReader ?? undefined;
|
|
240
|
+
}
|
|
166
241
|
constructor(ctx, config = {}) {
|
|
167
242
|
// The base's resolveConfig rejects unknown keys at runtime (measured r18:
|
|
168
243
|
// `BasicCompactionConfig: unknown key "artifactStoreRoot"`) — the archive's
|
|
169
244
|
// own keys are peeled off and held here; the base receives exactly its
|
|
170
245
|
// documented shape. (The loader does not strip them for us either.)
|
|
171
|
-
const { artifactStoreRoot, chapterTokenTarget, toolResultDeferFloorTokens, mergeThreshold, chapterLimit, syncDebounceMs, toolResultArtifactTokens, elicitedPlot, planInlineChars, callArgArtifactTokens, plansInCheckpoint, planCheckpointChars, enrichmentEnabled, enrichmentModel, enrichmentTrigger, enrichmentIdleMs, enrichmentBatchCap, vocabApply, vocabCoMin, vocabOverlapMin, treedxFetchTimeoutMs, treedxWorkspaceTtlSeconds, treedxLeaseRetries, treedxLeaseRetryDelayMs, ...baseConfig } = config;
|
|
246
|
+
const { artifactStoreRoot, knowledgeMirrorDir, openRequestCarryMaxChars, lastResortArtifact, chapterTokenTarget, toolResultDeferFloorTokens, mergeThreshold, chapterLimit, syncDebounceMs, toolResultArtifactTokens, elicitedPlot, childAutoArchive, childAutoArchiveMinTurns, planInlineChars, callArgArtifactTokens, plansInCheckpoint, planCheckpointChars, harnessId, sessionRulesEnabled, rulesMarkerPrefix, coreRulesBudgetTokens, projectRulesDisplayCap, sessionRulesDisplayCap, rulesRelevance, rulesEnrichment, rulesMinedPerChapter, rulesAutoActivate, rulesModel, enrichmentEnabled, enrichmentModel, enrichmentTrigger, enrichmentIdleMs, enrichmentBatchCap, vocabApply, vocabCoMin, vocabOverlapMin, enrichmentMaxTokens, treedxFetchTimeoutMs, treedxWorkspaceTtlSeconds, treedxLeaseRetries, treedxLeaseRetryDelayMs, ...baseConfig } = config;
|
|
172
247
|
super(ctx, baseConfig);
|
|
173
248
|
// The realm's module copy owns its own provider registry (cordis isolation
|
|
174
249
|
// between planes): register the configured TreeDX transport HERE, exactly
|
|
@@ -182,11 +257,16 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
182
257
|
}));
|
|
183
258
|
this.chaptersConfig = {
|
|
184
259
|
artifactStoreRoot: artifactStoreRoot ?? '.dsh-chapters',
|
|
260
|
+
knowledgeMirrorDir: knowledgeMirrorDir ?? DEFAULT_CLONE_DIR,
|
|
261
|
+
openRequestCarryMaxChars: openRequestCarryMaxChars ?? 4000,
|
|
262
|
+
lastResortArtifact: lastResortArtifact ?? true,
|
|
185
263
|
chapterTokenTarget: chapterTokenTarget ?? 8000,
|
|
186
264
|
toolResultDeferFloorTokens: toolResultDeferFloorTokens ?? 200,
|
|
187
265
|
mergeThreshold: mergeThreshold ?? 0.3,
|
|
188
266
|
chapterLimit: chapterLimit ?? 8000,
|
|
189
267
|
toolResultArtifactTokens: toolResultArtifactTokens ?? 8000,
|
|
268
|
+
childAutoArchive: childAutoArchive ?? true,
|
|
269
|
+
childAutoArchiveMinTurns: childAutoArchiveMinTurns ?? 4,
|
|
190
270
|
planInlineChars: planInlineChars ?? 900,
|
|
191
271
|
callArgArtifactTokens: callArgArtifactTokens ?? 0,
|
|
192
272
|
plansInCheckpoint: plansInCheckpoint ?? true,
|
|
@@ -198,11 +278,30 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
198
278
|
this.enrichCfg = {
|
|
199
279
|
enabled: enrichmentEnabled ?? true,
|
|
200
280
|
model: enrichmentModel ?? '',
|
|
281
|
+
mineRules: rulesEnrichment ?? true,
|
|
282
|
+
mineCap: rulesMinedPerChapter ?? 3,
|
|
283
|
+
rulesAutoActivate: rulesAutoActivate ?? true,
|
|
284
|
+
rulesModel: rulesModel ?? '',
|
|
285
|
+
maxTokens: enrichmentMaxTokens ?? ENRICH_MAX_TOKENS_DEFAULT,
|
|
201
286
|
trigger: (enrichmentTrigger ?? 'both'),
|
|
202
287
|
idleMs: enrichmentIdleMs ?? 60000,
|
|
203
288
|
batchCap: enrichmentBatchCap ?? 5,
|
|
204
289
|
};
|
|
290
|
+
this.ruleCfg = {
|
|
291
|
+
harnessId: harnessId ?? '',
|
|
292
|
+
captureEnabled: sessionRulesEnabled ?? true,
|
|
293
|
+
markerPrefix: rulesMarkerPrefix ?? 'ADD RULE:',
|
|
294
|
+
budgetTokens: coreRulesBudgetTokens ?? 1200,
|
|
295
|
+
projectCap: projectRulesDisplayCap ?? 12,
|
|
296
|
+
sessionCap: sessionRulesDisplayCap ?? 12,
|
|
297
|
+
relevance: rulesRelevance ?? true,
|
|
298
|
+
mine: rulesEnrichment ?? true,
|
|
299
|
+
mineCap: rulesMinedPerChapter ?? 3,
|
|
300
|
+
autoActivate: rulesAutoActivate ?? true,
|
|
301
|
+
model: rulesModel ?? '',
|
|
302
|
+
};
|
|
205
303
|
this.#listenForSignatures();
|
|
304
|
+
this.#listenForRuleCapture();
|
|
206
305
|
this.#listenForFirstTurnPull();
|
|
207
306
|
ctx.logger?.info?.('dsh-chapters: engine constructed for a mount (signature listener live)');
|
|
208
307
|
}
|
|
@@ -217,7 +316,7 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
217
316
|
* lock and debounce keep the two honest against each other. */
|
|
218
317
|
syncDebounceMs = 30000;
|
|
219
318
|
vocabCfg = { apply: false, coMin: 3, overlapMin: 0.5 };
|
|
220
|
-
enrichCfg = { enabled: false, model: '', trigger: 'both', idleMs: 60000, batchCap: 5 };
|
|
319
|
+
enrichCfg = { enabled: false, model: '', trigger: 'both', idleMs: 60000, batchCap: 5, mineRules: true, mineCap: 3 };
|
|
221
320
|
enrichPromise = null;
|
|
222
321
|
lastCwd = null;
|
|
223
322
|
lastRoute = null;
|
|
@@ -254,9 +353,30 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
254
353
|
const r = route ?? this.lastRoute;
|
|
255
354
|
if (r === null || r.provider === '' || r.model === '')
|
|
256
355
|
throw new Error('no auxiliary route yet');
|
|
257
|
-
return await this.#auxComplete(prompt, r,
|
|
356
|
+
return await this.#auxComplete(prompt, r, this.enrichCfg.maxTokens ?? ENRICH_MAX_TOKENS_DEFAULT);
|
|
258
357
|
},
|
|
259
358
|
conversationRoute: () => this.lastRoute,
|
|
359
|
+
// §7.5 the sink: the realm plane owns disk, identity and the registry, so it
|
|
360
|
+
// writes what the pass mined. Session rules are LOCAL — nothing is armed on
|
|
361
|
+
// the scheduler here, because the pool never carries them.
|
|
362
|
+
onMinedRules: async ({ sessionId, chapterNumber, humanText, rules, max, autoActivate }) => {
|
|
363
|
+
const cwd = this.lastCwd ?? undefined;
|
|
364
|
+
if (cwd === undefined)
|
|
365
|
+
return { written: 0, stated: 0, inferred: 0, pending: 0 };
|
|
366
|
+
const { store } = await this.store();
|
|
367
|
+
const res = captureMinedRules({
|
|
368
|
+
storeDir: resolveStoreRoot(cwd, this.chaptersConfig.artifactStoreRoot),
|
|
369
|
+
harnessId: (this.ruleCfg.harnessId !== '' ? this.ruleCfg.harnessId : hostname()),
|
|
370
|
+
sessionId,
|
|
371
|
+
records: store.rules(),
|
|
372
|
+
humanText,
|
|
373
|
+
rules,
|
|
374
|
+
maxPerChapter: max,
|
|
375
|
+
chapterNumber,
|
|
376
|
+
autoActivate,
|
|
377
|
+
});
|
|
378
|
+
return { written: res.ids.length, stated: res.stated, inferred: res.inferred, pending: res.pending };
|
|
379
|
+
},
|
|
260
380
|
scheduler: { schedule: (cwd, why) => { void this.#scheduler().then((s) => s.schedule(cwd, why)).catch(() => undefined); } },
|
|
261
381
|
log: (m) => this.ctx.logger?.info?.(`dsh-chapters: ${m}`),
|
|
262
382
|
warn: (m) => this.ctx.logger?.warn?.(`dsh-chapters: ${m}`),
|
|
@@ -318,16 +438,67 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
318
438
|
store: () => this.store(),
|
|
319
439
|
warn: (error) => { this.ctx.logger?.warn?.(`dsh-chapters: signature capture failed (${String(error)})`); },
|
|
320
440
|
subprocess,
|
|
441
|
+
storeRoot: this.chaptersConfig.artifactStoreRoot,
|
|
321
442
|
onCollected: (id, sig) => {
|
|
322
443
|
this.ctx.logger?.info?.(`dsh-chapters: signature collected for ${id} (${sig.seqs.length} events, ${sig.paths.length} paths, ${sig.size} est tokens)`);
|
|
323
444
|
},
|
|
324
445
|
}));
|
|
446
|
+
// #61: the seventh moment, registered where the other turn/end doors already live. Its own
|
|
447
|
+
// watermark makes a re-run a no-op, and it creates NO session — the mid-step rule stays intact.
|
|
448
|
+
this.ctx.on('session/event', makeChildArchiveListener({
|
|
449
|
+
ctx: { logger: this.ctx.logger },
|
|
450
|
+
// the store opens lazily and once — same accessor the signature listener uses; a row that
|
|
451
|
+
// resolved it per turn would reopen the domain on every turn/end.
|
|
452
|
+
store: () => this.store().then(({ store }) => store),
|
|
453
|
+
wiring: {
|
|
454
|
+
presetId: 'chapters',
|
|
455
|
+
artifactStoreRoot: this.chaptersConfig.artifactStoreRoot,
|
|
456
|
+
chapterTokenTarget: this.chaptersConfig.chapterTokenTarget,
|
|
457
|
+
toolResultDeferFloorTokens: this.chaptersConfig.toolResultDeferFloorTokens,
|
|
458
|
+
mergeThreshold: this.chaptersConfig.mergeThreshold,
|
|
459
|
+
chapterLimit: this.chaptersConfig.chapterLimit,
|
|
460
|
+
scheduler: { schedule: (cwd, reason) => { void this.#scheduler().then((sc) => sc.schedule(cwd, reason)); } },
|
|
461
|
+
},
|
|
462
|
+
enabled: this.chaptersConfig.childAutoArchive ?? true,
|
|
463
|
+
minTurns: this.chaptersConfig.childAutoArchiveMinTurns ?? 4,
|
|
464
|
+
}));
|
|
325
465
|
}
|
|
326
466
|
catch {
|
|
327
467
|
// A non-cordis ctx (L0 construction) has no event bus — signatures are
|
|
328
468
|
// optional there by design; the compaction path is unaffected.
|
|
329
469
|
}
|
|
330
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* #50 §7.5: capture `ADD RULE:` markers at the SAME `turn/end` boundary the
|
|
473
|
+
* topic signatures use — this extends an existing door, it is not a sixth
|
|
474
|
+
* archive moment. Deterministic and model-free (invariant 1): no marker in the
|
|
475
|
+
* turn costs one string scan and nothing else. Gated off when `harnessId` is
|
|
476
|
+
* absent, because a rule with no author partition is a rule nothing can
|
|
477
|
+
* revoke.
|
|
478
|
+
*/
|
|
479
|
+
#listenForRuleCapture() {
|
|
480
|
+
try {
|
|
481
|
+
this.ctx.on('session/event', makeRuleCaptureListener({
|
|
482
|
+
enabled: () => this.ruleCfg.captureEnabled,
|
|
483
|
+
prefix: () => this.ruleCfg.markerPrefix,
|
|
484
|
+
// The realm row's harnessId, or the same default the host plane's config
|
|
485
|
+
// uses (`hostname()`). A custom host-plane override does NOT strand the
|
|
486
|
+
// rule: the directory is only an author partition, and rendering walks
|
|
487
|
+
// every dir under `rules/` and activates by scope+ancestry (§7.5) — the
|
|
488
|
+
// id prefix differs, nothing else.
|
|
489
|
+
harnessId: () => (this.ruleCfg.harnessId !== '' ? this.ruleCfg.harnessId : hostname()),
|
|
490
|
+
storeRoot: (cwd) => resolveStoreRoot(cwd, this.chaptersConfig.artifactStoreRoot),
|
|
491
|
+
store: async () => ({ store: (await this.store()).store }),
|
|
492
|
+
warn: (error) => { this.ctx.logger?.warn?.(`dsh-chapters: session-rule capture failed (${String(error)})`); },
|
|
493
|
+
onCaptured: (sessionId, ids) => {
|
|
494
|
+
this.ctx.logger?.info?.(`dsh-chapters: ${ids.length} session rule(s) captured for ${sessionId} (${ids.join(', ')})`);
|
|
495
|
+
},
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// same containment as signatures: a non-cordis ctx has no event bus
|
|
500
|
+
}
|
|
501
|
+
}
|
|
331
502
|
/** Lazy, once. `storageDomain` is a host-plane singleton the realm resolves;
|
|
332
503
|
* acquire (never bare-open): a mounted tools-plugin may already own the reservation. */
|
|
333
504
|
store() {
|
|
@@ -349,7 +520,7 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
349
520
|
// digest; NO chapter path is cited, so nothing can dangle. A distinct
|
|
350
521
|
// provider tag keeps the finalizer's scan from claiming this record.
|
|
351
522
|
return {
|
|
352
|
-
summary: [{ type: 'text', text: `Compacted ${input.messages.length} messages (deterministic digest; no chapter: ${cwd === null ? 'no workspace cwd' : cid === null ? 'no open transaction' : 'empty region'}).` }],
|
|
523
|
+
summary: carryOpenRequest([{ type: 'text', text: `Compacted ${input.messages.length} messages (deterministic digest; no chapter: ${cwd === null ? 'no workspace cwd' : cid === null ? 'no open transaction' : 'empty region'}).` }], input.messages, this.chaptersConfig.openRequestCarryMaxChars),
|
|
353
524
|
provider: `${CHAPTERS_PROVIDER}-degraded`,
|
|
354
525
|
model: DETERMINISTIC_MODEL,
|
|
355
526
|
};
|
|
@@ -414,13 +585,38 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
414
585
|
this.ctx.logger?.warn?.(`dsh-chapters: plan carriage skipped (${String(error)}) — the archive still holds the plan`);
|
|
415
586
|
}
|
|
416
587
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
588
|
+
// #50 §7.6: the rules ride the checkpoint the way the plot and the plan do.
|
|
589
|
+
// Anything that goes wrong here DEGRADES TO NOTHING and is reported on the
|
|
590
|
+
// operator channel — the compaction critical path stays zero-model-token and
|
|
591
|
+
// never fails because of a rules read (§1 invariant 1, §7.3's non-blocking rule).
|
|
592
|
+
let rulesCarry = null;
|
|
593
|
+
try {
|
|
594
|
+
rulesCarry = await this.#rulesForCheckpoint(session);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
this.#diagnostic(`dsh-chapters: rule carriage skipped (${String(error)}) — the archive still holds every rule`);
|
|
598
|
+
}
|
|
599
|
+
// #59: everything ABOVE this line (composition, plot elicitation, rules, plan carry) is
|
|
600
|
+
// expensive and may cross a model call; the WRITE re-runs on a fresh row inside one section, so
|
|
601
|
+
// the reservation and manifest can no longer be rewound by a stale whole-row put — and the TOC
|
|
602
|
+
// text we are about to hand the host cites numbers that are durable at this point.
|
|
603
|
+
let tocText = '';
|
|
604
|
+
await store.mutate(session.id, (fresh) => {
|
|
605
|
+
// #69: the realm resolves an ABSOLUTE store root; hand planSummarize the session cwd so the record it
|
|
606
|
+
// mints is workspace-relative like every door's.
|
|
607
|
+
const wcwd = session.header?.cwd;
|
|
608
|
+
const { plan, state: next } = planSummarize(session, fresh, input, { ...this.chaptersConfig, workspaceCwd: wcwd }, reserve, cid, composition, plot, planCarry, rulesCarry);
|
|
609
|
+
const stored = rememberPlan(next, cid, {
|
|
610
|
+
number: plan.numbers[0], path: plan.chapter.path, title: plan.chapter.title, summary: plan.chapter.summary,
|
|
611
|
+
...(plan.chapters !== undefined ? { chapters: plan.chapters } : {}),
|
|
612
|
+
});
|
|
613
|
+
// The drift check that has existed with zero callers: a manifest whose reservation is not
|
|
614
|
+
// durable (or not the same numbers) means the TOC we are about to emit cites the unreachable.
|
|
615
|
+
assertReservationMatchesPlan(stored, cid, plan);
|
|
616
|
+
tocText = plan.tocText;
|
|
617
|
+
return stored;
|
|
421
618
|
});
|
|
422
|
-
|
|
423
|
-
return { summary: [{ type: 'text', text: plan.tocText }], provider: CHAPTERS_PROVIDER, model: DETERMINISTIC_MODEL };
|
|
619
|
+
return { summary: carryOpenRequest([{ type: 'text', text: tocText }], input.messages, this.chaptersConfig.openRequestCarryMaxChars), provider: CHAPTERS_PROVIDER, model: DETERMINISTIC_MODEL };
|
|
424
620
|
}
|
|
425
621
|
// ------------------------------------------------------------ entry points (finalize after commit)
|
|
426
622
|
/**
|
|
@@ -494,11 +690,19 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
494
690
|
async #arrive(agent) {
|
|
495
691
|
const session = agent.session;
|
|
496
692
|
const cwd = session.header?.cwd;
|
|
497
|
-
if (cwd === undefined || cwd === '')
|
|
693
|
+
if (cwd === undefined || cwd === '') {
|
|
694
|
+
// #78: two bare returns made arrival artifacting vanish COMPLETELY, silently — the very
|
|
695
|
+
// silence that lets a leg fail while nobody can say whether the code path ran. And the
|
|
696
|
+
// reason is printed as the SHAPE it found, because `session.header?.cwd` is precisely the
|
|
697
|
+
// assumption a host format bump retires (the v3→v4 log lesson).
|
|
698
|
+
diagnosticNote(this.ctx.logger, 'realm:engine:arrive', `arrival stubbing skipped: no session cwd (session keys=${Object.keys(session ?? {}).slice(0, 10).join(',')})`);
|
|
498
699
|
return;
|
|
700
|
+
}
|
|
499
701
|
const meter = this.ctx.tokenMeter;
|
|
500
|
-
if (meter === undefined)
|
|
702
|
+
if (meter === undefined) {
|
|
703
|
+
diagnosticNote(this.ctx.logger, 'realm:engine:arrive', 'arrival stubbing skipped: no tokenMeter on the realm context');
|
|
501
704
|
return;
|
|
705
|
+
}
|
|
502
706
|
const r = await applyArrivalStubs(session, {
|
|
503
707
|
cwd,
|
|
504
708
|
storeRoot: this.chaptersConfig.artifactStoreRoot,
|
|
@@ -508,7 +712,145 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
508
712
|
if (r.detail !== undefined)
|
|
509
713
|
this.ctx.logger?.warn?.(`dsh-chapters: arrival stubbing degraded (${r.detail})`);
|
|
510
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* One line per compaction decision — the second half of "is relief actually running?".
|
|
717
|
+
* `realm:engine:*` so the site is attributable, note-level so a routine decision never reads as
|
|
718
|
+
* a failure, and it names which entry point (`compactIfNeeded` on pressure/auto vs `compactNow`
|
|
719
|
+
* on `/compact`) because those two disagreeing is exactly the kind of drift a field report needs
|
|
720
|
+
* to be able to tell apart. (#78)
|
|
721
|
+
*/
|
|
722
|
+
/**
|
|
723
|
+
* The last-resort deferral (#78 option A). Returns how many nodes it stubbed, and 0 — never a
|
|
724
|
+
* guess — unless every input to the ceiling is real: `resolveStoreRoot`-style derivation lives in
|
|
725
|
+
* `lastResortCeilingTokens`, so this method only supplies measured values. Runs only from the
|
|
726
|
+
* declined branch; the operator's `toolResultArtifactTokens` still governs every ordinary case.
|
|
727
|
+
*/
|
|
728
|
+
async #lastResortRelief(agent, signal) {
|
|
729
|
+
try {
|
|
730
|
+
if (this.chaptersConfig.lastResortArtifact !== true)
|
|
731
|
+
return 0;
|
|
732
|
+
const route = this.lastRoute;
|
|
733
|
+
const meter = this.ctx.tokenMeter;
|
|
734
|
+
const llm = this.ctx.llm;
|
|
735
|
+
if (route === null || meter === undefined || typeof llm?.resolveModelInfo !== 'function')
|
|
736
|
+
return 0;
|
|
737
|
+
const info = await llm.resolveModelInfo(route.provider, route.model, signal);
|
|
738
|
+
const base = this;
|
|
739
|
+
// Quiet unless there is a problem to solve. Relief declines on EVERY ordinary pre-step because
|
|
740
|
+
// the surface is below the trigger, and the first two notes this feature ever emitted were that
|
|
741
|
+
// non-event — noise that also hid the case worth naming. Every value here is measured at this
|
|
742
|
+
// point in the method: no invented field and no assumed window (my first attempt reached for a
|
|
743
|
+
// `lastWindowTokens` that does not exist, and `tsc` was the only thing that caught it).
|
|
744
|
+
const surfaceNow = Number(meter.measure?.(agent.session)?.totalTokens ?? NaN);
|
|
745
|
+
const ratioNow = this.config?.thresholdRatio;
|
|
746
|
+
const windowNow = Number(info?.context?.contextWindow ?? NaN);
|
|
747
|
+
const overTrigger = Number.isFinite(surfaceNow) && Number.isFinite(windowNow)
|
|
748
|
+
&& typeof ratioNow === 'number' && surfaceNow > windowNow * ratioNow;
|
|
749
|
+
const retainTokens = base.config?.retainTokens;
|
|
750
|
+
const reserved = Number(info?.context?.maxTokens ?? base.config?.maxTokens ?? NaN);
|
|
751
|
+
const ceiling = lastResortCeilingTokens({
|
|
752
|
+
windowTokens: Number(info?.context?.contextWindow ?? NaN),
|
|
753
|
+
retainRatio: Number(base.config?.retainRatio ?? NaN),
|
|
754
|
+
// optional, not `| undefined`: `exactOptionalPropertyTypes` is on, and a property that can
|
|
755
|
+
// hold undefined is a different promise from one that may be absent.
|
|
756
|
+
...(retainTokens !== undefined ? { retainTokens } : {}),
|
|
757
|
+
// The completion budget comes from the MODEL, measured: `resolveConfig` defaults
|
|
758
|
+
// `maxTokens` to `headroomTokens` (65,536 — a routing headroom, not a per-request output
|
|
759
|
+
// budget), so `32,000 − 5,120 − 65,536` is negative and the ceiling is permanently
|
|
760
|
+
// underivable. `info.context.maxTokens` (15,000 here) sits in the same object as
|
|
761
|
+
// `contextWindow`; the base config stays only as a fallback so the abstention path still
|
|
762
|
+
// fires and says so when neither is present.
|
|
763
|
+
reservedCompletionTokens: reserved,
|
|
764
|
+
});
|
|
765
|
+
if (ceiling === null) {
|
|
766
|
+
// Measured tonight: relief declined twice with `surface=39983 trigger=16000` and the last
|
|
767
|
+
// resort NEVER fired — and said nothing about why. A feature that abstains silently is
|
|
768
|
+
// indistinguishable from a feature that isn't installed, which is the exact complaint this
|
|
769
|
+
// branch has filed against everything else (#61a). So: name the missing input.
|
|
770
|
+
diagnosticNote(this.ctx.logger, 'realm:engine:last-resort', `not applied: coexistence ceiling underivable (window=${String(info?.context?.contextWindow)}, retainRatio=${String(base.config?.retainRatio)}, retainTokens=${String(base.config?.retainTokens)}, maxTokens=${String(base.config?.maxTokens)}) — check the realm row's compaction keys, not this feature`);
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
773
|
+
const session = agent.session;
|
|
774
|
+
const cwd = session.header?.cwd ?? '';
|
|
775
|
+
if (cwd === '')
|
|
776
|
+
return 0; // no workspace identity, no deferral — same rule as `#arrive`
|
|
777
|
+
const before = [...session.surface.nodes].length;
|
|
778
|
+
const r = await applyArrivalStubs(session, {
|
|
779
|
+
cwd,
|
|
780
|
+
storeRoot: resolveStoreRoot(cwd, this.chaptersConfig.artifactStoreRoot),
|
|
781
|
+
floorTokens: ceiling,
|
|
782
|
+
estimate: (m) => meter.estimateMessage?.(m) ?? 0,
|
|
783
|
+
});
|
|
784
|
+
if (r.stubbed > 0) {
|
|
785
|
+
diagnosticWarn(this.ctx.logger, 'realm:engine:last-resort', `relief declined, so ${String(r.stubbed)} tool/result node(s) larger than the coexistence ceiling (${String(ceiling)} tok) were artifact-deferred regardless of toolResultArtifactTokens — survivability over economy (#78 A); the archive keeps every byte and chapters_artifact answers for them (surface nodes ${String(before)} → ${String(session.surface.nodes.length)})`);
|
|
786
|
+
}
|
|
787
|
+
return r.stubbed;
|
|
788
|
+
}
|
|
789
|
+
catch { /* a last resort must never become the reason relief failed */
|
|
790
|
+
return 0;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/** See the call site: a decline while OVER the trigger is a statement, not a shrug. Never throws. */
|
|
794
|
+
async #reportDecline(agent, signal) {
|
|
795
|
+
try {
|
|
796
|
+
const route = this.lastRoute;
|
|
797
|
+
const meter = this.ctx.tokenMeter;
|
|
798
|
+
if (route === null || meter === undefined)
|
|
799
|
+
return;
|
|
800
|
+
const surface = meter.measure?.(agent.session)?.totalTokens;
|
|
801
|
+
const llm = this.ctx.llm;
|
|
802
|
+
if (typeof surface !== 'number' || typeof llm?.resolveModelInfo !== 'function')
|
|
803
|
+
return;
|
|
804
|
+
const info = await llm.resolveModelInfo(route.provider, route.model, signal);
|
|
805
|
+
const window = info?.context?.contextWindow;
|
|
806
|
+
const ratio = this.config?.thresholdRatio;
|
|
807
|
+
if (typeof window !== 'number' || typeof ratio !== 'number')
|
|
808
|
+
return;
|
|
809
|
+
const triggerTokens = Math.floor(window * ratio);
|
|
810
|
+
if (surface <= triggerTokens)
|
|
811
|
+
return;
|
|
812
|
+
const session = agent.session;
|
|
813
|
+
let oversized = 0;
|
|
814
|
+
let biggest = 0;
|
|
815
|
+
for (const seq of session.surface.nodes) {
|
|
816
|
+
const ev = session.eventAt(seq);
|
|
817
|
+
if (ev?.type !== 'tool/result')
|
|
818
|
+
continue;
|
|
819
|
+
const est = meter.estimateMessage?.(ev.data?.message) ?? 0;
|
|
820
|
+
if (est > biggest)
|
|
821
|
+
biggest = est;
|
|
822
|
+
if (est > triggerTokens)
|
|
823
|
+
oversized += 1;
|
|
824
|
+
}
|
|
825
|
+
diagnosticWarn(this.ctx.logger, 'realm:engine:decline', `relief declined while the surface is over the trigger (surface=${String(surface)} trigger=${String(triggerTokens)} window=${String(window)} ratio=${String(ratio)}): no safe range to compact, and ${String(oversized)} inline tool/result node(s) individually exceed the trigger (largest ${String(biggest)} tok). Context relief needs the arrival floor below that node size — toolResultArtifactTokens is the operator switch, and the archive keeps every byte either way.`);
|
|
826
|
+
}
|
|
827
|
+
catch { /* a report must never become the reason compaction failed */ }
|
|
828
|
+
}
|
|
829
|
+
#noteDecision(where, trigger, result, agent) {
|
|
830
|
+
// The INPUTS, not just the verdict: `decided: none` is one word covering two opposite bugs —
|
|
831
|
+
// no routed target (the base returns null on line one) and under-threshold (a real decision).
|
|
832
|
+
// Heavy's leg sat exactly there, so the note now carries route, surface tokens and ratio.
|
|
833
|
+
// All of it wrapped: a diagnostic that can throw is a diagnostic that will eat the step.
|
|
834
|
+
let route = 'unknown';
|
|
835
|
+
let surface = '? tok';
|
|
836
|
+
try {
|
|
837
|
+
route = this.lastRoute === null ? 'none' : `${this.lastRoute.provider}/${this.lastRoute.model}`;
|
|
838
|
+
const meter = this.ctx.tokenMeter;
|
|
839
|
+
const measured = meter?.measure?.(agent.session);
|
|
840
|
+
if (typeof measured?.totalTokens === 'number')
|
|
841
|
+
surface = `${String(measured.totalTokens)} tok`;
|
|
842
|
+
}
|
|
843
|
+
catch { /* never let instrumentation break compaction */ }
|
|
844
|
+
diagnosticNote(this.ctx.logger, `realm:engine:${where}`, `${where} decided: ${result === null ? 'none' : `compaction ${String(result?.compactionId ?? '?')}`} (trigger=${JSON.stringify(trigger)}, session=${String(agent?.session?.id ?? '?')}, route=${route}, surface=${surface}, ratio=${String(this.config?.thresholdRatio ?? '?')})`);
|
|
845
|
+
}
|
|
511
846
|
async compactIfNeeded(agent, trigger, signal) {
|
|
847
|
+
// #78: the entry line that proves the HOST called this. Every row in the lifecycle suite
|
|
848
|
+
// CALLS `compactIfNeeded` directly — the test has always been the caller, which is exactly
|
|
849
|
+
// how a suite stays green while the product relieves nothing. Field evidence on 0.2.0-rc.2:
|
|
850
|
+
// 35,428 tokens against a 16,000 trigger, ZERO `compaction/*` events, and no line anywhere
|
|
851
|
+
// saying whether relief ran. One info line per entry, carrying the trigger the host passed,
|
|
852
|
+
// makes "is context relief wired at all?" answerable from a session log instead of inferred.
|
|
853
|
+
diagnosticNote(this.ctx.logger, 'realm:engine:compact', `compactIfNeeded entered (trigger=${JSON.stringify(trigger)}, session=${String(agent?.session?.id ?? '?')})`);
|
|
512
854
|
this.#observeRoute(agent);
|
|
513
855
|
await this.#arrive(agent);
|
|
514
856
|
// Finalization runs whether super resolves OR throws: the pressure retry
|
|
@@ -522,10 +864,49 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
522
864
|
}
|
|
523
865
|
catch (error) {
|
|
524
866
|
await this.#finalizeGuarded(agent);
|
|
867
|
+
// A throw while the surface is OVER the trigger is the same field failure as a null —
|
|
868
|
+
// arguably worse, because the user sees the endpoint's 400 rather than our explanation.
|
|
869
|
+
// Report first, then rethrow untouched; the report itself can never mask the error.
|
|
870
|
+
await this.#reportDecline(agent, signal);
|
|
871
|
+
// no result exists on this path — the base engine threw — so the line says `none` and the
|
|
872
|
+
// reason is the rethrown error, not a value invented for the log
|
|
873
|
+
this.#noteDecision('compactIfNeeded', trigger, null, agent);
|
|
525
874
|
throw error;
|
|
526
875
|
}
|
|
527
876
|
if (result !== null)
|
|
528
877
|
await this.#finalizeGuarded(agent);
|
|
878
|
+
// #78: entry alone is half the question. Three `trigger="pressure"` entries on 0.2.0-rc.2
|
|
879
|
+
// produced no `compaction/*` event at all, and nothing could say whether the base engine
|
|
880
|
+
// declined (a meter/threshold disagreement) or acted and our finalization lost the evidence.
|
|
881
|
+
// The decision now sits beside the entry, in the same artifact.
|
|
882
|
+
// #78: the quiet failure was the worst part. Six compactions, then a raw `400 … exceeds the
|
|
883
|
+
// context window` from the endpoint, and nothing the product said about WHY relief stopped.
|
|
884
|
+
// This reports the numbers it actually had — surface, trigger, window, ratio, and how many
|
|
885
|
+
// inline tool/results are individually larger than the trigger — and changes no behavior:
|
|
886
|
+
// the arrival floor is the operator's switch (AGENTS: a behavior switch, not a knob), and the
|
|
887
|
+
// engine does not get to reinterpret it silently.
|
|
888
|
+
if (result === null) {
|
|
889
|
+
// #78 option A: relief declined, so a node that cannot coexist with the mandatory tail is no
|
|
890
|
+
// longer an economy question — the session is stuck. Defer THAT node as a last resort and give
|
|
891
|
+
// relief one more chance. Ordering is the whole design: this sits inside the declined branch,
|
|
892
|
+
// never in `#arrive`, so `heavy` still proves pressure compaction first. An eager version would
|
|
893
|
+
// arrival-stub the surface and make the leg's claim untestable (AGENTS' named trap), which is
|
|
894
|
+
// what the anti-vacuity rows below guard against.
|
|
895
|
+
const deferred = await this.#lastResortRelief(agent, signal);
|
|
896
|
+
if (deferred > 0) {
|
|
897
|
+
try {
|
|
898
|
+
const retry = await super.compactIfNeeded(agent, trigger, signal);
|
|
899
|
+
if (retry !== null) {
|
|
900
|
+
await this.#finalizeGuarded(agent);
|
|
901
|
+
this.#noteDecision('compactIfNeeded', trigger, retry, agent);
|
|
902
|
+
return retry;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch { /* still stuck: fall through to the decline report, then rethrow nothing */ }
|
|
906
|
+
}
|
|
907
|
+
await this.#reportDecline(agent, signal);
|
|
908
|
+
}
|
|
909
|
+
this.#noteDecision('compactIfNeeded', trigger, result, agent);
|
|
529
910
|
return result;
|
|
530
911
|
}
|
|
531
912
|
async compactNow(agent, signal, sourceCommandId) {
|
|
@@ -537,10 +918,15 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
537
918
|
}
|
|
538
919
|
catch (error) {
|
|
539
920
|
await this.#finalizeGuarded(agent);
|
|
921
|
+
this.#noteDecision('compactNow', 'manual', null, agent);
|
|
540
922
|
throw error;
|
|
541
923
|
}
|
|
542
924
|
if (result !== null)
|
|
543
925
|
await this.#finalizeGuarded(agent);
|
|
926
|
+
// #78: the same decision line on the manual path. Reporting only the pressure route would
|
|
927
|
+
// leave `/compact` — the door a human actually pulls — as silent as the bug this instrument
|
|
928
|
+
// exists to catch.
|
|
929
|
+
this.#noteDecision('compactNow', 'manual', result, agent);
|
|
544
930
|
return result;
|
|
545
931
|
}
|
|
546
932
|
/**
|
|
@@ -557,6 +943,54 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
557
943
|
this.#diagnostic(`dsh-chapters: chapter finalization deferred (session ${session.id}): ${String(error?.stack ?? error)}`);
|
|
558
944
|
}
|
|
559
945
|
}
|
|
946
|
+
/**
|
|
947
|
+
* §7.3/§7.6: the block this session's rebuilt head should carry, from the ONE
|
|
948
|
+
* builder that renders it for the notice too — a second rule-ranking or
|
|
949
|
+
* rule-selecting implementation is forbidden (record §8: two doors, one
|
|
950
|
+
* scorer). Zero bytes when nothing is active.
|
|
951
|
+
*
|
|
952
|
+
* Relevance topics come from the session's OWN stored turn signatures
|
|
953
|
+
* (deterministic, already on disk, no model call), newest last. With
|
|
954
|
+
* `rulesRelevance` off the selection falls back to category→id order, which is
|
|
955
|
+
* the order the tapes were distilled against.
|
|
956
|
+
*/
|
|
957
|
+
async #rulesForCheckpoint(session) {
|
|
958
|
+
const cwd = session.header?.cwd;
|
|
959
|
+
if (cwd === undefined || cwd === '')
|
|
960
|
+
return null;
|
|
961
|
+
const { store } = await this.store();
|
|
962
|
+
const harness = this.ruleCfg.harnessId !== '' ? this.ruleCfg.harnessId : hostname();
|
|
963
|
+
const project = projectForCwd(store.projects(), cwd);
|
|
964
|
+
// the SAME renderer the doors use — one builder for every rebuilt head (§7.3)
|
|
965
|
+
const built = await renderRulesBlock({
|
|
966
|
+
// #85: the configured root, not the constant — an e2e boot pins this per port so a notice
|
|
967
|
+
// cannot pick up rules from whatever the developer's repo happens to contain.
|
|
968
|
+
mirrorDir: resolveStoreRoot(cwd, this.chaptersConfig.knowledgeMirrorDir ?? DEFAULT_CLONE_DIR),
|
|
969
|
+
storeDir: resolveStoreRoot(cwd, this.chaptersConfig.artifactStoreRoot),
|
|
970
|
+
projectKey: project?.projectKey ?? '',
|
|
971
|
+
harnessId: harness,
|
|
972
|
+
budgetTokens: this.ruleCfg.budgetTokens,
|
|
973
|
+
projectCap: this.ruleCfg.projectCap,
|
|
974
|
+
sessionCap: this.ruleCfg.sessionCap,
|
|
975
|
+
relevance: this.ruleCfg.relevance,
|
|
976
|
+
includeSession: true,
|
|
977
|
+
store,
|
|
978
|
+
sessionId: session.id,
|
|
979
|
+
// #52: the delegation edge. A subagent that compacts in place must see the
|
|
980
|
+
// rules of the session that delegated it, or the whole point of the tier is
|
|
981
|
+
// lost exactly where the work actually happens.
|
|
982
|
+
...(() => { const p = this.ruleParentOf(); return p !== undefined ? { parentOf: p } : {}; })(),
|
|
983
|
+
});
|
|
984
|
+
if (built.kind === 'none')
|
|
985
|
+
return null;
|
|
986
|
+
if (built.kind === 'refusal') {
|
|
987
|
+
// announced, never clipped: the head says the rules did not fit rather
|
|
988
|
+
// than silently carrying none of them
|
|
989
|
+
this.#diagnostic(`dsh-chapters: rules over budget at this checkpoint — ${built.reason}`);
|
|
990
|
+
return `Rules NOT carried into this checkpoint (their token budget was exceeded, so nothing was clipped): ${built.reason}`;
|
|
991
|
+
}
|
|
992
|
+
return built.text;
|
|
993
|
+
}
|
|
560
994
|
/**
|
|
561
995
|
* The operator-visible warn channel (r19: headless profiles route ctx.logger
|
|
562
996
|
* where nobody can see it; measured again 2026-09-27: even the in-process
|
|
@@ -565,24 +999,30 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
565
999
|
* not a tunable. Any degradation that leaves the user wondering "did the
|
|
566
1000
|
* feature run?" belongs here.
|
|
567
1001
|
*/
|
|
568
|
-
#diagnostic(message) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
catch { /* diagnostics must never mask the real failure */ }
|
|
576
|
-
}
|
|
1002
|
+
#diagnostic(message, error) {
|
|
1003
|
+
// #54: ONE writer, in src/diag.ts. This held a private three-line copy of the sink while
|
|
1004
|
+
// the two places a user's door can actually fail — the host-plane tool and the shared
|
|
1005
|
+
// continuation core — wrote nothing anywhere. Which is how a kernel refusal that ended the
|
|
1006
|
+
// whole acceptance chain left no trace readable without a browser.
|
|
1007
|
+
diagnosticWarn(this.ctx.logger, 'realm:engine', message, error);
|
|
577
1008
|
}
|
|
578
1009
|
/** Every provider-tagged summary without a finalized manifest, oldest first. */
|
|
1010
|
+
/**
|
|
1011
|
+
* Per-item containment (#59). The scan used to await `#finalizeOne` in a bare loop, and
|
|
1012
|
+
* `#finalizeOne` THROWS when a manifest is missing — so one erased manifest (exactly what the
|
|
1013
|
+
* SAIP lost-update produced) aborted the scan on its first iteration forever: every later
|
|
1014
|
+
* compaction in that session could never write its chapters, and their numbers stayed burned.
|
|
1015
|
+
* One bad item may never poison the items behind it: report it, name it, continue.
|
|
1016
|
+
*/
|
|
579
1017
|
async #finalizeAllOutstanding(session) {
|
|
580
1018
|
const { store } = await this.store();
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
1019
|
+
const out = await scanAndFinalize(await store.get(session.id), scanChaptersSummaries(session), async (found) => { await this.#finalizeOne(session, store, found.compactionId, found.shadowedSeqs); }, (reason, error) => this.#diagnostic(`dsh-chapters: chapter finalization deferred for compaction ${reason.compactionId} (${reason.reason}: `
|
|
1020
|
+
+ `${String(error?.message ?? error)}) — its chapter was never written and the `
|
|
1021
|
+
+ 'rest of the queue still proceeds. Run /chapters-repair (issue #60) to materialize it.'));
|
|
1022
|
+
if (out.done.length > 0) {
|
|
1023
|
+
const cwd = session.header?.cwd;
|
|
1024
|
+
if (cwd !== undefined)
|
|
1025
|
+
void this.#scheduler().then((s2) => s2.schedule(cwd, 'archive:compaction')).catch(() => undefined);
|
|
586
1026
|
}
|
|
587
1027
|
}
|
|
588
1028
|
async #finalizeOne(session, store, compactionId, shadowedSeqs) {
|
|
@@ -618,9 +1058,10 @@ export class ChaptersCompactionEngine extends BasicCompactionEngine {
|
|
|
618
1058
|
// jointly account for what the surface replacement hid (legacy single-
|
|
619
1059
|
// chapter behavior generalized).
|
|
620
1060
|
const records = wrote.records.map((r) => ({ ...r, shadowedSeqs: [...shadowedSeqs] }));
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1061
|
+
// #59: `state` was read BEFORE writeArchive (render + every artifact write). Committing from it
|
|
1062
|
+
// would erase anything another door committed meanwhile — which is how the SAIP compaction lost
|
|
1063
|
+
// its record while its 960 KB file stayed on disk. The commit re-reads inside one section.
|
|
1064
|
+
await store.mutate(session.id, (fresh) => markFinalized(appendChapters(fresh, records), compactionId, numbers));
|
|
624
1065
|
// §5.1: compaction finalization is a push point (debounced).
|
|
625
1066
|
const cwdNow = session.header?.cwd;
|
|
626
1067
|
if (cwdNow !== undefined)
|