iterate-plugin 3.5.6 → 3.5.7
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/dist/skill-prompt.js +28 -8
- package/dist/tools/decision-log.js +11 -0
- package/dist/tools/prune.js +7 -1
- package/package.json +8 -7
- package/src/skill-prompt.ts +28 -8
- package/src/tools/decision-log.ts +12 -0
- package/src/tools/prune.ts +7 -1
package/dist/skill-prompt.js
CHANGED
|
@@ -84,6 +84,9 @@ const rounds = [] // raw per-round findings
|
|
|
84
84
|
// round where every reviewer subagent failed (or output stayed schema-invalid
|
|
85
85
|
// after retries) is INCONCLUSIVE — it must never make the run look converged.
|
|
86
86
|
let lastRoundOk = true
|
|
87
|
+
// Slot index of the current round within this run's rounds array. A resumed
|
|
88
|
+
// run starts at startRound > 1, so the round NUMBER is not a valid array index.
|
|
89
|
+
let roundSlot = -1
|
|
87
90
|
|
|
88
91
|
phase('transcript')
|
|
89
92
|
// Read any steering nudge written (via iterate_transcript nudge) for this run's reviewers.
|
|
@@ -96,9 +99,14 @@ const steering = transRead && typeof transRead.nudge === 'string' && transRead.n
|
|
|
96
99
|
phase('review')
|
|
97
100
|
for (let r = 1; r <= maxRounds; r++) {
|
|
98
101
|
log('round ' + r + ' of ' + maxRounds + ' — finding NEW issues only')
|
|
102
|
+
roundSlot = rounds.length
|
|
99
103
|
let agg = null
|
|
100
104
|
let schemaInvalid = false
|
|
101
105
|
let attempts = 0
|
|
106
|
+
// Declared OUTSIDE the do-block: it is read after the retry loop exits, and a
|
|
107
|
+
// block-scoped const here raised ReferenceError at runtime (crashing both
|
|
108
|
+
// canonical workflows after the first round).
|
|
109
|
+
let reviewersOk = true
|
|
102
110
|
do {
|
|
103
111
|
attempts += 1
|
|
104
112
|
// Schema validation retry: on the 2nd+ pass, nudge reviewers toward strict JSON.
|
|
@@ -126,7 +134,7 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
126
134
|
// the convergence math would then read "0 new findings". An all-failed
|
|
127
135
|
// round is NOT a clean pass — it is inconclusive, so the convergence gate
|
|
128
136
|
// below refuses it before any "converged" signal fires.
|
|
129
|
-
|
|
137
|
+
reviewersOk = raw.length > 0 ? raw.some(x => x !== null && typeof x === 'object') : true
|
|
130
138
|
const thisRound = {
|
|
131
139
|
round: r,
|
|
132
140
|
findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])),
|
|
@@ -134,7 +142,7 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
134
142
|
// gate can compare self-reported reads against the assigned inventory.
|
|
135
143
|
readFiles: [].concat(...raw.map(x => x && Array.isArray(x.readFiles) ? x.readFiles : [])),
|
|
136
144
|
}
|
|
137
|
-
if (rounds.length
|
|
145
|
+
if (roundSlot < rounds.length) rounds[roundSlot] = thisRound; else rounds.push(thisRound)
|
|
138
146
|
// Deterministic aggregate: cross-round dedupe + known_intentional filter + severity sort.
|
|
139
147
|
agg = await agent(
|
|
140
148
|
'Call iterate_review({operation:"aggregate", mode:"dry-run", rounds:' + JSON.stringify(rounds) + ', maxReviewRounds:' + maxRounds + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
@@ -299,6 +307,12 @@ let schemaFailed = false // last round produced no usable reviewer output
|
|
|
299
307
|
let failedCommands = []
|
|
300
308
|
let configErrors = [] // validation commands NOT in validation.commands (config gap, no rollback)
|
|
301
309
|
const fixRecords = [] // observatory fix records collected round by round
|
|
310
|
+
// Slot index of the current round within this run's rounds array. A resumed
|
|
311
|
+
// run starts at startRound > 1, so the round NUMBER is not a valid array index.
|
|
312
|
+
let roundSlot = -1
|
|
313
|
+
// Highest round number actually executed by THIS run (carried so a resumed run
|
|
314
|
+
// reports its true round, not the count of rounds it happened to run).
|
|
315
|
+
let lastRound = startRound - 1
|
|
302
316
|
|
|
303
317
|
// Read any steering nudge intended for this run's reviewers.
|
|
304
318
|
const transRead = await agent(
|
|
@@ -310,6 +324,8 @@ const steering = transRead && typeof transRead.nudge === 'string' && transRead.n
|
|
|
310
324
|
phase('loop')
|
|
311
325
|
for (let r = startRound; r <= maxRounds; r++) {
|
|
312
326
|
log('round ' + r + ' of ' + maxRounds + ' — review current state, fix atomics via iterate_fix, validate')
|
|
327
|
+
roundSlot = rounds.length
|
|
328
|
+
lastRound = r
|
|
313
329
|
// Audit-trail: record the round start (SKILL.md Phase 4 requires per-round records).
|
|
314
330
|
await agent(
|
|
315
331
|
'Call iterate_decision_log({operation:"append", type:"round_start", round:' + r + ', data:{maxRounds:' + maxRounds + ', fixedSoFar:' + fixedCount + '}})',
|
|
@@ -318,6 +334,10 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
318
334
|
let agg = null
|
|
319
335
|
let schemaInvalid = false
|
|
320
336
|
let attempts = 0
|
|
337
|
+
// Declared OUTSIDE the do-block: it is read after the retry loop exits, and a
|
|
338
|
+
// block-scoped const here raised ReferenceError at runtime (crashing both
|
|
339
|
+
// canonical workflows after the first round).
|
|
340
|
+
let reviewersOk = true
|
|
321
341
|
do {
|
|
322
342
|
attempts += 1
|
|
323
343
|
// Schema validation retry: on the 2nd+ pass, nudge reviewers toward strict JSON.
|
|
@@ -341,13 +361,13 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
341
361
|
// ALL reviewers returning null (child failures) means this round produced
|
|
342
362
|
// no usable output — an empty aggregate must not read like a clean
|
|
343
363
|
// "nothing to fix" convergence. Gate the loop exit + convergence on it.
|
|
344
|
-
|
|
364
|
+
reviewersOk = raw.length > 0 ? raw.some(x => x !== null && typeof x === 'object') : true
|
|
345
365
|
const thisRound = {
|
|
346
366
|
round: r,
|
|
347
367
|
findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])),
|
|
348
368
|
readFiles: [].concat(...raw.map(x => x && Array.isArray(x.readFiles) ? x.readFiles : [])),
|
|
349
369
|
}
|
|
350
|
-
if (rounds.length
|
|
370
|
+
if (roundSlot < rounds.length) rounds[roundSlot] = thisRound; else rounds.push(thisRound)
|
|
351
371
|
|
|
352
372
|
// Deterministic dedupe / known_intentional filter / severity sort for this round.
|
|
353
373
|
// \`fixedCount\` is threaded into the report summary so the client dashboard can
|
|
@@ -502,7 +522,7 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
502
522
|
|
|
503
523
|
phase('report')
|
|
504
524
|
await agent(
|
|
505
|
-
'Call iterate_decision_log({operation:"append", type:"report", round:' +
|
|
525
|
+
'Call iterate_decision_log({operation:"append", type:"report", round:' + lastRound +
|
|
506
526
|
', data:{mode:"normal", fixed:' + fixedCount + ', architectural:' + architectural.length + '}})',
|
|
507
527
|
{ label: 'report:log' }
|
|
508
528
|
)
|
|
@@ -525,7 +545,7 @@ if (!abortedByValidation && !schemaFailed) {
|
|
|
525
545
|
// would hide the very thing F5 is meant to resume. Reflect what is on disk.
|
|
526
546
|
const obsCheckpoint = {
|
|
527
547
|
mode: 'normal',
|
|
528
|
-
round:
|
|
548
|
+
round: lastRound,
|
|
529
549
|
maxRounds: maxRounds,
|
|
530
550
|
fixedCount: fixedCount,
|
|
531
551
|
resumeCount: effectiveResumeCount,
|
|
@@ -536,13 +556,13 @@ if (!abortedByValidation && !schemaFailed) {
|
|
|
536
556
|
? (configErrors.length > 0 ? 'aborted_by_config' : 'aborted_by_validation')
|
|
537
557
|
: (converged ? 'converged' : 'max_rounds_reached')
|
|
538
558
|
await agent(
|
|
539
|
-
'Call iterate_transcript({operation:"capture", mode:"normal", goal:' + JSON.stringify(plan.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' +
|
|
559
|
+
'Call iterate_transcript({operation:"capture", mode:"normal", goal:' + JSON.stringify(plan.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' + lastRound + ', findingsByRound:' + JSON.stringify(rounds.map(rr => (rr.findings && rr.findings.length) ? rr.findings.length : 0)) + ', stoppedReason:"' + stopReason + '", fixes:' + JSON.stringify(fixRecords) + ', checkpoint:' + JSON.stringify(obsCheckpoint) + ', rounds:' + JSON.stringify(rounds.map(rr => ({ round: rr.round, findings: rr.findings, readFiles: rr.readFiles }))) + '}). Return {operation:"ok"}.',
|
|
540
560
|
{ label: 'transcript:capture' }
|
|
541
561
|
)
|
|
542
562
|
return {
|
|
543
563
|
mode: 'normal',
|
|
544
564
|
goal: plan.goal,
|
|
545
|
-
roundsExecuted:
|
|
565
|
+
roundsExecuted: lastRound,
|
|
546
566
|
maxRounds: maxRounds,
|
|
547
567
|
converged: converged,
|
|
548
568
|
abortedByValidation: abortedByValidation,
|
|
@@ -122,6 +122,17 @@ function recountEntries(filePath) {
|
|
|
122
122
|
return 1;
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Drop the cached entry count for a log file. The cache is keyed by the log
|
|
127
|
+
* path and assumes "same byte size ⇒ same entry count"; a prune rewrite
|
|
128
|
+
* (temp + atomic rename) replaces the file with a DIFFERENT number of entries,
|
|
129
|
+
* so only a stale-cache invalidation keeps the fast path honest. Without it, a
|
|
130
|
+
* rewrite that lands on the exact same byte size as the previous append would
|
|
131
|
+
* make the next append report a wrong `count`.
|
|
132
|
+
*/
|
|
133
|
+
export function invalidateLogCountCache(filePath) {
|
|
134
|
+
entryCountCache.delete(filePath);
|
|
135
|
+
}
|
|
125
136
|
/** All valid DecisionLogEntry `type` values (must stay in sync with Types). */
|
|
126
137
|
const VALID_ENTRY_TYPES = new Set([
|
|
127
138
|
'round_start',
|
package/dist/tools/prune.js
CHANGED
|
@@ -28,7 +28,7 @@ import { join } from 'node:path';
|
|
|
28
28
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
29
29
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
30
30
|
import { writeTextAtomic, writeJsonAtomic } from "../atomic-fs.js";
|
|
31
|
-
import { readDecisionEntries, appendDecisionEntry, acquireLogLock } from "./decision-log.js";
|
|
31
|
+
import { readDecisionEntries, appendDecisionEntry, acquireLogLock, invalidateLogCountCache } from "./decision-log.js";
|
|
32
32
|
import { readRegistry, recomputeRoundCounts } from "./fix.js";
|
|
33
33
|
import { readExperienceBank, writeExperienceBank } from "./experience-store.js";
|
|
34
34
|
import { readDefenseEvents, writeDefenseEvents } from "./defense-store.js";
|
|
@@ -263,6 +263,12 @@ function rewriteDecisionLogKeepingRecent(projectRoot, cutoff) {
|
|
|
263
263
|
if (deleted === 0)
|
|
264
264
|
return { deleted: 0 };
|
|
265
265
|
writeTextAtomic(logPath, kept.map((e) => JSON.stringify(e)).join('\n') + '\n');
|
|
266
|
+
// The append-side entry-count cache is keyed by path and assumes "same
|
|
267
|
+
// byte size ⇒ same count"; the rename above replaced the file with a
|
|
268
|
+
// different number of entries, so drop the stale cache or the next
|
|
269
|
+
// append can report a wrong entryCount when the sizes coincidentally
|
|
270
|
+
// match.
|
|
271
|
+
invalidateLogCountCache(logPath);
|
|
266
272
|
// A concurrent appender (cross-process, lock-unavailable path) may have
|
|
267
273
|
// landed new lines after our read. If the log grew during the write
|
|
268
274
|
// window, re-read and prune again instead of accepting a lost audit trail.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.7",
|
|
4
4
|
"description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"0.1.6-alpha.2": "compatible",
|
|
43
43
|
"0.1.7-alpha.1": "compatible",
|
|
44
44
|
"0.1.7-alpha.2": "compatible",
|
|
45
|
-
"0.1.7-rc.1": "compatible"
|
|
45
|
+
"0.1.7-rc.1": "compatible",
|
|
46
|
+
"0.1.7-rc.2": "compatible"
|
|
46
47
|
}
|
|
47
48
|
},
|
|
48
49
|
"client": {
|
|
@@ -80,14 +81,14 @@
|
|
|
80
81
|
},
|
|
81
82
|
"dependencies": {
|
|
82
83
|
"@deepseek-ai/cordis": "4.0.4",
|
|
83
|
-
"@deepseek-ai/dsh-tools": "0.1.7-rc.
|
|
84
|
-
"@deepseek-ai/dsh-util-values": "0.1.7-rc.
|
|
84
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-util-values": "0.1.7-rc.2",
|
|
85
86
|
"js-yaml": "4.3.2"
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
88
|
-
"@deepseek-ai/dsh-agent": "0.1.7-rc.
|
|
89
|
-
"@deepseek-ai/dsh-jobs": "0.1.7-rc.
|
|
90
|
-
"@deepseek-ai/dsh-session": "0.1.7-rc.
|
|
89
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-jobs": "0.1.7-rc.2",
|
|
91
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
91
92
|
"@types/js-yaml": "4.0.9",
|
|
92
93
|
"@types/node": "22.15.0",
|
|
93
94
|
"@types/react": "19.2.2",
|
package/src/skill-prompt.ts
CHANGED
|
@@ -85,6 +85,9 @@ const rounds = [] // raw per-round findings
|
|
|
85
85
|
// round where every reviewer subagent failed (or output stayed schema-invalid
|
|
86
86
|
// after retries) is INCONCLUSIVE — it must never make the run look converged.
|
|
87
87
|
let lastRoundOk = true
|
|
88
|
+
// Slot index of the current round within this run's rounds array. A resumed
|
|
89
|
+
// run starts at startRound > 1, so the round NUMBER is not a valid array index.
|
|
90
|
+
let roundSlot = -1
|
|
88
91
|
|
|
89
92
|
phase('transcript')
|
|
90
93
|
// Read any steering nudge written (via iterate_transcript nudge) for this run's reviewers.
|
|
@@ -97,9 +100,14 @@ const steering = transRead && typeof transRead.nudge === 'string' && transRead.n
|
|
|
97
100
|
phase('review')
|
|
98
101
|
for (let r = 1; r <= maxRounds; r++) {
|
|
99
102
|
log('round ' + r + ' of ' + maxRounds + ' — finding NEW issues only')
|
|
103
|
+
roundSlot = rounds.length
|
|
100
104
|
let agg = null
|
|
101
105
|
let schemaInvalid = false
|
|
102
106
|
let attempts = 0
|
|
107
|
+
// Declared OUTSIDE the do-block: it is read after the retry loop exits, and a
|
|
108
|
+
// block-scoped const here raised ReferenceError at runtime (crashing both
|
|
109
|
+
// canonical workflows after the first round).
|
|
110
|
+
let reviewersOk = true
|
|
103
111
|
do {
|
|
104
112
|
attempts += 1
|
|
105
113
|
// Schema validation retry: on the 2nd+ pass, nudge reviewers toward strict JSON.
|
|
@@ -127,7 +135,7 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
127
135
|
// the convergence math would then read "0 new findings". An all-failed
|
|
128
136
|
// round is NOT a clean pass — it is inconclusive, so the convergence gate
|
|
129
137
|
// below refuses it before any "converged" signal fires.
|
|
130
|
-
|
|
138
|
+
reviewersOk = raw.length > 0 ? raw.some(x => x !== null && typeof x === 'object') : true
|
|
131
139
|
const thisRound = {
|
|
132
140
|
round: r,
|
|
133
141
|
findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])),
|
|
@@ -135,7 +143,7 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
135
143
|
// gate can compare self-reported reads against the assigned inventory.
|
|
136
144
|
readFiles: [].concat(...raw.map(x => x && Array.isArray(x.readFiles) ? x.readFiles : [])),
|
|
137
145
|
}
|
|
138
|
-
if (rounds.length
|
|
146
|
+
if (roundSlot < rounds.length) rounds[roundSlot] = thisRound; else rounds.push(thisRound)
|
|
139
147
|
// Deterministic aggregate: cross-round dedupe + known_intentional filter + severity sort.
|
|
140
148
|
agg = await agent(
|
|
141
149
|
'Call iterate_review({operation:"aggregate", mode:"dry-run", rounds:' + JSON.stringify(rounds) + ', maxReviewRounds:' + maxRounds + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
@@ -300,6 +308,12 @@ let schemaFailed = false // last round produced no usable reviewer output
|
|
|
300
308
|
let failedCommands = []
|
|
301
309
|
let configErrors = [] // validation commands NOT in validation.commands (config gap, no rollback)
|
|
302
310
|
const fixRecords = [] // observatory fix records collected round by round
|
|
311
|
+
// Slot index of the current round within this run's rounds array. A resumed
|
|
312
|
+
// run starts at startRound > 1, so the round NUMBER is not a valid array index.
|
|
313
|
+
let roundSlot = -1
|
|
314
|
+
// Highest round number actually executed by THIS run (carried so a resumed run
|
|
315
|
+
// reports its true round, not the count of rounds it happened to run).
|
|
316
|
+
let lastRound = startRound - 1
|
|
303
317
|
|
|
304
318
|
// Read any steering nudge intended for this run's reviewers.
|
|
305
319
|
const transRead = await agent(
|
|
@@ -311,6 +325,8 @@ const steering = transRead && typeof transRead.nudge === 'string' && transRead.n
|
|
|
311
325
|
phase('loop')
|
|
312
326
|
for (let r = startRound; r <= maxRounds; r++) {
|
|
313
327
|
log('round ' + r + ' of ' + maxRounds + ' — review current state, fix atomics via iterate_fix, validate')
|
|
328
|
+
roundSlot = rounds.length
|
|
329
|
+
lastRound = r
|
|
314
330
|
// Audit-trail: record the round start (SKILL.md Phase 4 requires per-round records).
|
|
315
331
|
await agent(
|
|
316
332
|
'Call iterate_decision_log({operation:"append", type:"round_start", round:' + r + ', data:{maxRounds:' + maxRounds + ', fixedSoFar:' + fixedCount + '}})',
|
|
@@ -319,6 +335,10 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
319
335
|
let agg = null
|
|
320
336
|
let schemaInvalid = false
|
|
321
337
|
let attempts = 0
|
|
338
|
+
// Declared OUTSIDE the do-block: it is read after the retry loop exits, and a
|
|
339
|
+
// block-scoped const here raised ReferenceError at runtime (crashing both
|
|
340
|
+
// canonical workflows after the first round).
|
|
341
|
+
let reviewersOk = true
|
|
322
342
|
do {
|
|
323
343
|
attempts += 1
|
|
324
344
|
// Schema validation retry: on the 2nd+ pass, nudge reviewers toward strict JSON.
|
|
@@ -342,13 +362,13 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
342
362
|
// ALL reviewers returning null (child failures) means this round produced
|
|
343
363
|
// no usable output — an empty aggregate must not read like a clean
|
|
344
364
|
// "nothing to fix" convergence. Gate the loop exit + convergence on it.
|
|
345
|
-
|
|
365
|
+
reviewersOk = raw.length > 0 ? raw.some(x => x !== null && typeof x === 'object') : true
|
|
346
366
|
const thisRound = {
|
|
347
367
|
round: r,
|
|
348
368
|
findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])),
|
|
349
369
|
readFiles: [].concat(...raw.map(x => x && Array.isArray(x.readFiles) ? x.readFiles : [])),
|
|
350
370
|
}
|
|
351
|
-
if (rounds.length
|
|
371
|
+
if (roundSlot < rounds.length) rounds[roundSlot] = thisRound; else rounds.push(thisRound)
|
|
352
372
|
|
|
353
373
|
// Deterministic dedupe / known_intentional filter / severity sort for this round.
|
|
354
374
|
// \`fixedCount\` is threaded into the report summary so the client dashboard can
|
|
@@ -503,7 +523,7 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
503
523
|
|
|
504
524
|
phase('report')
|
|
505
525
|
await agent(
|
|
506
|
-
'Call iterate_decision_log({operation:"append", type:"report", round:' +
|
|
526
|
+
'Call iterate_decision_log({operation:"append", type:"report", round:' + lastRound +
|
|
507
527
|
', data:{mode:"normal", fixed:' + fixedCount + ', architectural:' + architectural.length + '}})',
|
|
508
528
|
{ label: 'report:log' }
|
|
509
529
|
)
|
|
@@ -526,7 +546,7 @@ if (!abortedByValidation && !schemaFailed) {
|
|
|
526
546
|
// would hide the very thing F5 is meant to resume. Reflect what is on disk.
|
|
527
547
|
const obsCheckpoint = {
|
|
528
548
|
mode: 'normal',
|
|
529
|
-
round:
|
|
549
|
+
round: lastRound,
|
|
530
550
|
maxRounds: maxRounds,
|
|
531
551
|
fixedCount: fixedCount,
|
|
532
552
|
resumeCount: effectiveResumeCount,
|
|
@@ -537,13 +557,13 @@ if (!abortedByValidation && !schemaFailed) {
|
|
|
537
557
|
? (configErrors.length > 0 ? 'aborted_by_config' : 'aborted_by_validation')
|
|
538
558
|
: (converged ? 'converged' : 'max_rounds_reached')
|
|
539
559
|
await agent(
|
|
540
|
-
'Call iterate_transcript({operation:"capture", mode:"normal", goal:' + JSON.stringify(plan.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' +
|
|
560
|
+
'Call iterate_transcript({operation:"capture", mode:"normal", goal:' + JSON.stringify(plan.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' + lastRound + ', findingsByRound:' + JSON.stringify(rounds.map(rr => (rr.findings && rr.findings.length) ? rr.findings.length : 0)) + ', stoppedReason:"' + stopReason + '", fixes:' + JSON.stringify(fixRecords) + ', checkpoint:' + JSON.stringify(obsCheckpoint) + ', rounds:' + JSON.stringify(rounds.map(rr => ({ round: rr.round, findings: rr.findings, readFiles: rr.readFiles }))) + '}). Return {operation:"ok"}.',
|
|
541
561
|
{ label: 'transcript:capture' }
|
|
542
562
|
)
|
|
543
563
|
return {
|
|
544
564
|
mode: 'normal',
|
|
545
565
|
goal: plan.goal,
|
|
546
|
-
roundsExecuted:
|
|
566
|
+
roundsExecuted: lastRound,
|
|
547
567
|
maxRounds: maxRounds,
|
|
548
568
|
converged: converged,
|
|
549
569
|
abortedByValidation: abortedByValidation,
|
|
@@ -121,6 +121,18 @@ function recountEntries(filePath: string): number {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Drop the cached entry count for a log file. The cache is keyed by the log
|
|
126
|
+
* path and assumes "same byte size ⇒ same entry count"; a prune rewrite
|
|
127
|
+
* (temp + atomic rename) replaces the file with a DIFFERENT number of entries,
|
|
128
|
+
* so only a stale-cache invalidation keeps the fast path honest. Without it, a
|
|
129
|
+
* rewrite that lands on the exact same byte size as the previous append would
|
|
130
|
+
* make the next append report a wrong `count`.
|
|
131
|
+
*/
|
|
132
|
+
export function invalidateLogCountCache(filePath: string): void {
|
|
133
|
+
entryCountCache.delete(filePath)
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
/** All valid DecisionLogEntry `type` values (must stay in sync with Types). */
|
|
125
137
|
const VALID_ENTRY_TYPES = new Set<DecisionLogEntry['type']>([
|
|
126
138
|
'round_start',
|
package/src/tools/prune.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
30
30
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
31
31
|
import { resolveProjectRootForExec } from '../config-loader.ts'
|
|
32
32
|
import { writeTextAtomic, writeJsonAtomic } from '../atomic-fs.ts'
|
|
33
|
-
import { readDecisionEntries, appendDecisionEntry, acquireLogLock } from './decision-log.ts'
|
|
33
|
+
import { readDecisionEntries, appendDecisionEntry, acquireLogLock, invalidateLogCountCache } from './decision-log.ts'
|
|
34
34
|
import { readRegistry, removeRecord, recomputeRoundCounts } from './fix.ts'
|
|
35
35
|
import { readExperienceBank, writeExperienceBank } from './experience-store.ts'
|
|
36
36
|
import { readDefenseEvents, writeDefenseEvents } from './defense-store.ts'
|
|
@@ -291,6 +291,12 @@ function rewriteDecisionLogKeepingRecent(projectRoot: string, cutoff: string): {
|
|
|
291
291
|
const deleted = entries.length - kept.length
|
|
292
292
|
if (deleted === 0) return { deleted: 0 }
|
|
293
293
|
writeTextAtomic(logPath, kept.map((e) => JSON.stringify(e)).join('\n') + '\n')
|
|
294
|
+
// The append-side entry-count cache is keyed by path and assumes "same
|
|
295
|
+
// byte size ⇒ same count"; the rename above replaced the file with a
|
|
296
|
+
// different number of entries, so drop the stale cache or the next
|
|
297
|
+
// append can report a wrong entryCount when the sizes coincidentally
|
|
298
|
+
// match.
|
|
299
|
+
invalidateLogCountCache(logPath)
|
|
294
300
|
// A concurrent appender (cross-process, lock-unavailable path) may have
|
|
295
301
|
// landed new lines after our read. If the log grew during the write
|
|
296
302
|
// window, re-read and prune again instead of accepting a lost audit trail.
|