MCPcopy Create free account
hub / github.com/QodeXcli/QodeX / pruneMessages

Method pruneMessages

src/agent/loop.ts:2575–2657  ·  view source on GitHub ↗

Drop oldest turn groups when context exceeds budget. Preserves tool_call/tool_result coupling.

(messages: Message[], maxTokens: number)

Source from the content-addressed store, hash-verified

2573 this.planGateSatisfied = true; // good — the model is planning; let it through
2574 } else if (tc.function.name.startsWith('artifact_')) {
2575 // Artifact tools produce a standalone deliverable (a page/component the user keeps),
2576 // not a refactor of the project codebase — the architecture-plan discipline doesn't
2577 // apply, and gating them just derails the create→preview→review flow. Let them through.
2578 } else if (!this.planGateSatisfied && !this.planGateFired) {
2579 this.planGateFired = true; // fire at most once per run
2580 logger.info('Pre-flight gate: requiring a plan before the first build action', { tool: tc.function.name });
2581 uiEvents.push({ type: 'progress', message: '🧭 Architecture gate: plan first, then build' } as any);
2582 return { content: PREFLIGHT_MESSAGE, isError: true, uiEvents };
2583 }
2584 }
2585
2586 // ─── Read-before-write gate (stateful tool gating) ───
2587 // The system prompt's "Read before write" rule, enforced in code instead of prose.
2588 // A mutating filesystem tool on an EXISTING file is refused unless the file was
2589 // successfully read this session — and refused if it changed on disk since the
2590 // last read (stale knowledge). The refusal is a normal tool-result observation,
2591 // so the model self-corrects by calling read_file. New-file creation always passes.
2592 // Off-switch: discipline.readBeforeWrite: false
2593 if (isGatedMutationTool(tc.function.name) &&
2594 (this.config as any)?.discipline?.readBeforeWrite !== false) {
2595 for (const p of extractMutationPaths(tc.function.name, args)) {
2596 const absP = path.isAbsolute(p) ? p : path.resolve(this.effectiveCwd ?? this.cwd, p);
2597 let st: fsSync.Stats | null = null;
2598 try { st = fsSync.statSync(absP); } catch { st = null; }
2599 if (!st || !st.isFile()) continue; // new file (or non-file): creation is allowed
2600 const verdict = this.readLedger.check(absP, st.mtimeMs);
2601 if (!verdict.ok) {
2602 const rel = path.relative(this.effectiveCwd ?? this.cwd, absP) || p;
2603 logger.info('Read-before-write gate refused mutation', {
2604 tool: tc.function.name, path: rel, kind: verdict.kind,
2605 });
2606 uiEvents.push({
2607 type: 'progress',
2608 message: `🔒 Read-before-write: ${rel} ${verdict.kind === 'unread' ? 'not read yet' : 'changed since last read'} — asking model to read it first`,
2609 } as any);
2610 return { content: buildGateMessage(rel, verdict.kind), isError: true, uiEvents };
2611 }
2612 }
2613 }
2614
2615 // ─── Auto-snapshot: take ONE snapshot per turn, before the first mutating tool ───
2616 // Why per-turn instead of per-tool: a single user request often triggers many
2617 // edits (read → edit_text × 3 → bash test → edit_text × 2). One snapshot at
2618 // the start of the work captures the pre-change state. /undo rolls back the
2619 // whole turn — exactly what the user wants if "make the change" went sideways.
2620 //
2621 // This is the safety net Hamed asked for: every mutation has a back button.
2622 if (this.snapshotService && !this.turnSnapshotTaken) {
2623 const isMutating = tool && !tool.isReadOnly;
2624 if (isMutating) {
2625 try {
2626 const rec = this.snapshotService.takeSnapshot(
2627 `turn-${this.currentTurn}: before ${tc.function.name}`,
2628 this.currentTurn,
2629 );
2630 if (rec) {
2631 this.turnSnapshotTaken = true;
2632 logger.info('Auto-snapshot taken before mutating tool', {

Callers 2

runMethod · 0.95

Calls 4

estimateTokensMethod · 0.95
warnMethod · 0.80
infoMethod · 0.80
pushMethod · 0.45

Tested by

no test coverage detected