feat(scale): foundation for paginated event plan (>50-event projects); driver follow-up#604
feat(scale): foundation for paginated event plan (>50-event projects); driver follow-up#604kelsonpw wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Batch merge loses callsites from earlier batches
- Added readLocalEventPlanRich() that preserves callsites when reading the on-disk event plan, and switched the batch-merge path to use it instead of readLocalEventPlan() which stripped callsites.
Or push these changes by commenting:
@cursor push 09650d5681
Preview (09650d5681)
diff --git a/src/lib/event-plan-parser.ts b/src/lib/event-plan-parser.ts
--- a/src/lib/event-plan-parser.ts
+++ b/src/lib/event-plan-parser.ts
@@ -162,3 +162,104 @@
return parsed.filter((e) => e.name.trim().length > 0);
}
+
+/**
+ * Like {@link readLocalEventPlan} but preserves optional `callsites` arrays
+ * written by `persistEventPlan`. Used by the batch-merge path so earlier
+ * batches' callsite annotations survive when subsequent batches are merged in.
+ */
+export function readLocalEventPlanRich(installDir: string): Array<{
+ name: string;
+ description: string;
+ callsites?: Array<{ filePath: string; anchor?: string }>;
+}> {
+ const candidates = [
+ getEventsFile(installDir),
+ path.join(installDir, '.amplitude-events.json'),
+ ];
+
+ let winner: string | null = null;
+ let winnerMtime = -Infinity;
+ for (const candidate of candidates) {
+ try {
+ const stat = fs.statSync(candidate);
+ const mtime = stat.mtimeMs;
+ if (mtime > winnerMtime) {
+ winner = candidate;
+ winnerMtime = mtime;
+ }
+ } catch (e) {
+ const err = e as NodeJS.ErrnoException;
+ if (err.code !== 'ENOENT') {
+ logToFile(
+ `[readLocalEventPlanRich] stat ${candidate} failed: ${
+ err.message ?? err
+ }`,
+ );
+ }
+ }
+ }
+
+ if (!winner) return [];
+
+ let raw: string;
+ try {
+ raw = fs.readFileSync(winner, 'utf8');
+ } catch (e) {
+ const err = e as NodeJS.ErrnoException;
+ logToFile(
+ `[readLocalEventPlanRich] read ${winner} failed: ${err.message ?? err}`,
+ );
+ return [];
+ }
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(raw);
+ } catch {
+ logToFile(`[readLocalEventPlanRich] ${winner} could not be parsed as JSON`);
+ return [];
+ }
+
+ if (
+ parsed &&
+ typeof parsed === 'object' &&
+ !Array.isArray(parsed) &&
+ Array.isArray((parsed as { events?: unknown }).events)
+ ) {
+ parsed = (parsed as { events: unknown[] }).events;
+ }
+
+ if (!Array.isArray(parsed)) return [];
+
+ return (parsed as Array<Record<string, unknown>>)
+ .map((e) => {
+ const name =
+ (e.name as string) ??
+ (e.event as string) ??
+ (e.eventName as string) ??
+ (e.event_name as string) ??
+ '';
+ const description =
+ (e.description as string) ??
+ (e.event_description as string) ??
+ (e.eventDescription as string) ??
+ (e.eventDescriptionAndReasoning as string) ??
+ '';
+ const result: {
+ name: string;
+ description: string;
+ callsites?: Array<{ filePath: string; anchor?: string }>;
+ } = {
+ name,
+ description,
+ };
+ if (Array.isArray(e.callsites) && e.callsites.length > 0) {
+ result.callsites = (
+ e.callsites as Array<{ filePath: string; anchor?: string }>
+ ).filter((c) => c && typeof c.filePath === 'string');
+ }
+ return result;
+ })
+ .filter((e) => e.name.trim().length > 0);
+}
diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts
--- a/src/lib/wizard-tools.ts
+++ b/src/lib/wizard-tools.ts
@@ -14,8 +14,11 @@
import { z } from 'zod';
import { logToFile } from '../utils/debug';
import { atomicWriteJSON } from '../utils/atomic-write';
-import { readLocalEventPlan } from './event-plan-parser.js';
import {
+ readLocalEventPlan,
+ readLocalEventPlanRich,
+} from './event-plan-parser.js';
+import {
ensureDir,
getDashboardFile,
getEventsFile,
@@ -1785,10 +1788,16 @@
let toPersist: typeof events = events;
if (!isFirstBatch) {
// Merge with existing on-disk plan so we don't lose earlier batches.
- const existing = readLocalEventPlan(workingDirectory);
+ // Use the rich reader that preserves callsites so annotations from
+ // prior batches accumulate across compaction boundaries.
+ const existing = readLocalEventPlanRich(workingDirectory);
const byName = new Map<
string,
- { name: string; description: string }
+ {
+ name: string;
+ description: string;
+ callsites?: Array<{ filePath: string; anchor?: string }>;
+ }
>();
for (const e of existing) byName.set(e.name, e);
for (const e of events) byName.set(e.name, e);You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Duplicated file-discovery logic in
readLocalEventPlanRich- Extracted a shared
readFreshestEventPlanFilehelper that encapsulates the candidate enumeration, stat loop with mtime comparison, winner selection, and file reading, eliminating ~35 lines of duplication betweenreadLocalEventPlanandreadLocalEventPlanRich.
- Extracted a shared
Or push these changes by commenting:
@cursor push ff9e71f6b5
Preview (ff9e71f6b5)
diff --git a/src/lib/event-plan-parser.ts b/src/lib/event-plan-parser.ts
--- a/src/lib/event-plan-parser.ts
+++ b/src/lib/event-plan-parser.ts
@@ -93,25 +93,14 @@
}
/**
- * Read the agent-written event plan from a project's install dir.
- *
- * Tries the canonical path first (`<installDir>/.amplitude/events.json`),
- * then the legacy dotfile (`<installDir>/.amplitude-events.json`) that
- * older context-hub integration skills still emit. Returns whichever has
- * the more recent mtime so a stale canonical from a previous run can't
- * shadow a fresh legacy write — mirroring the `pickFreshestExisting`
- * logic in `agent-interface.ts`.
- *
- * Returns `[]` for any non-fatal failure (file missing, malformed JSON,
- * schema mismatch). Empty entries (no name) are dropped. Logs to the
- * debug file so issues are recoverable post-mortem.
- *
- * Used by the Event Verification screen to render the planned tracking
- * list inline so users can see what they need to trigger.
+ * Find the freshest event-plan file among canonical and legacy paths,
+ * read it, and return its raw content. Returns `null` when no candidate
+ * exists or the read fails.
*/
-export function readLocalEventPlan(
+function readFreshestEventPlanFile(
installDir: string,
-): Array<{ name: string; description: string }> {
+ caller: string,
+): string | null {
const candidates = [
getEventsFile(installDir),
path.join(installDir, '.amplitude-events.json'),
@@ -131,32 +120,49 @@
const err = e as NodeJS.ErrnoException;
if (err.code !== 'ENOENT') {
logToFile(
- `[readLocalEventPlan] stat ${candidate} failed: ${
- err.message ?? err
- }`,
+ `[${caller}] stat ${candidate} failed: ${err.message ?? err}`,
);
}
}
}
- if (!winner) return [];
+ if (!winner) return null;
- let raw: string;
try {
- raw = fs.readFileSync(winner, 'utf8');
+ return fs.readFileSync(winner, 'utf8');
} catch (e) {
const err = e as NodeJS.ErrnoException;
- logToFile(
- `[readLocalEventPlan] read ${winner} failed: ${err.message ?? err}`,
- );
- return [];
+ logToFile(`[${caller}] read ${winner} failed: ${err.message ?? err}`);
+ return null;
}
+}
+/**
+ * Read the agent-written event plan from a project's install dir.
+ *
+ * Tries the canonical path first (`<installDir>/.amplitude/events.json`),
+ * then the legacy dotfile (`<installDir>/.amplitude-events.json`) that
+ * older context-hub integration skills still emit. Returns whichever has
+ * the more recent mtime so a stale canonical from a previous run can't
+ * shadow a fresh legacy write — mirroring the `pickFreshestExisting`
+ * logic in `agent-interface.ts`.
+ *
+ * Returns `[]` for any non-fatal failure (file missing, malformed JSON,
+ * schema mismatch). Empty entries (no name) are dropped. Logs to the
+ * debug file so issues are recoverable post-mortem.
+ *
+ * Used by the Event Verification screen to render the planned tracking
+ * list inline so users can see what they need to trigger.
+ */
+export function readLocalEventPlan(
+ installDir: string,
+): Array<{ name: string; description: string }> {
+ const raw = readFreshestEventPlanFile(installDir, 'readLocalEventPlan');
+ if (raw === null) return [];
+
const parsed = parseEventPlanContent(raw);
if (parsed === null) {
- logToFile(
- `[readLocalEventPlan] ${winner} could not be parsed as an event plan`,
- );
+ logToFile(`[readLocalEventPlan] event plan file could not be parsed`);
return [];
}
@@ -173,51 +179,16 @@
description: string;
callsites?: Array<{ filePath: string; anchor?: string }>;
}> {
- const candidates = [
- getEventsFile(installDir),
- path.join(installDir, '.amplitude-events.json'),
- ];
+ const raw = readFreshestEventPlanFile(installDir, 'readLocalEventPlanRich');
+ if (raw === null) return [];
- let winner: string | null = null;
- let winnerMtime = -Infinity;
- for (const candidate of candidates) {
- try {
- const stat = fs.statSync(candidate);
- const mtime = stat.mtimeMs;
- if (mtime > winnerMtime) {
- winner = candidate;
- winnerMtime = mtime;
- }
- } catch (e) {
- const err = e as NodeJS.ErrnoException;
- if (err.code !== 'ENOENT') {
- logToFile(
- `[readLocalEventPlanRich] stat ${candidate} failed: ${
- err.message ?? err
- }`,
- );
- }
- }
- }
-
- if (!winner) return [];
-
- let raw: string;
- try {
- raw = fs.readFileSync(winner, 'utf8');
- } catch (e) {
- const err = e as NodeJS.ErrnoException;
- logToFile(
- `[readLocalEventPlanRich] read ${winner} failed: ${err.message ?? err}`,
- );
- return [];
- }
-
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
- logToFile(`[readLocalEventPlanRich] ${winner} could not be parsed as JSON`);
+ logToFile(
+ `[readLocalEventPlanRich] event plan file could not be parsed as JSON`,
+ );
return [];
}You can send follow-ups to the cloud agent here.
328c097 to
ee14860
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed:
readLocalEventPlanRichcrashes on non-string field values- Replaced unsafe
as stringcasts with a runtimestr()helper that returnsundefinedfor non-string values, allowing??to correctly fall through to defaults.
- Replaced unsafe
- ✅ Fixed:
readLocalEventPlanRichmay return empty callsites array- Added a post-filter length check so
result.callsitesis only assigned when the filtered array has at least one valid entry.
- Added a post-filter length check so
Or push these changes by commenting:
@cursor push e8c745269a
Preview (e8c745269a)
diff --git a/src/lib/event-plan-parser.ts b/src/lib/event-plan-parser.ts
--- a/src/lib/event-plan-parser.ts
+++ b/src/lib/event-plan-parser.ts
@@ -209,19 +209,22 @@
if (!Array.isArray(parsed)) return [];
+ const str = (v: unknown): string | undefined =>
+ typeof v === 'string' ? v : undefined;
+
return (parsed as Array<Record<string, unknown>>)
.map((e) => {
const name =
- (e.name as string) ??
- (e.event as string) ??
- (e.eventName as string) ??
- (e.event_name as string) ??
+ str(e.name) ??
+ str(e.event) ??
+ str(e.eventName) ??
+ str(e.event_name) ??
'';
const description =
- (e.description as string) ??
- (e.event_description as string) ??
- (e.eventDescription as string) ??
- (e.eventDescriptionAndReasoning as string) ??
+ str(e.description) ??
+ str(e.event_description) ??
+ str(e.eventDescription) ??
+ str(e.eventDescriptionAndReasoning) ??
'';
const result: {
name: string;
@@ -232,9 +235,12 @@
description,
};
if (Array.isArray(e.callsites) && e.callsites.length > 0) {
- result.callsites = (
+ const filtered = (
e.callsites as Array<{ filePath: string; anchor?: string }>
).filter((c) => c && typeof c.filePath === 'string');
+ if (filtered.length > 0) {
+ result.callsites = filtered;
+ }
}
return result;
})You can send follow-ups to the cloud agent here.
7361393 to
aaf6818
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Batch validation error uses bare string, not structured helper
- Replaced the bare
{ content: [{ type: 'text', text: '...' }] }response withtoWizardToolErrorContent()which addsisError: trueand the structured{ success, error, guidance, suggestedTool, context }envelope, consistent with every other error path in the file.
- Replaced the bare
Or push these changes by commenting:
@cursor push 7ecd7717ff
Preview (7ecd7717ff)
diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts
--- a/src/lib/wizard-tools.ts
+++ b/src/lib/wizard-tools.ts
@@ -1947,14 +1947,13 @@
});
if (batchError) {
logToFile(`confirm_event_plan: rejected batch metadata: ${batchError}`);
- return {
- content: [
- {
- type: 'text' as const,
- text: `error: invalid batch metadata — ${batchError}. Re-call without batch fields to fall back to single-batch behavior, or fix the metadata and retry.`,
- },
- ],
- };
+ return toWizardToolErrorContent({
+ error: `invalid batch metadata — ${batchError}`,
+ guidance:
+ 'Re-call confirm_event_plan without batch fields to fall back to single-batch behavior, or fix the metadata and retry.',
+ suggestedTool: 'mcp__wizard-tools__confirm_event_plan',
+ context: `batchIndex: ${args.batchIndex}; totalBatches: ${args.totalBatches}; chunkSize: ${args.chunkSize}; eventCount: ${args.events.length}`,
+ });
}
// Soft-gate the name format. Agents historically saw conflicting
// guidance (commandments said Title Case, the tool schema saidYou can send follow-ups to the cloud agent here.
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
aaf6818 to
2293362
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Schema min(1) contradicts validator minimum of 5
- Changed the zod schema's
.min(1)to.min(5)forchunkSizeinconfirm_event_planto match theMIN_CHUNK_SIZEenforced byvalidateBatchMetadata.
- Changed the zod schema's
Or push these changes by commenting:
@cursor push 3b40d961f5
Preview (3b40d961f5)
diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts
--- a/src/lib/wizard-tools.ts
+++ b/src/lib/wizard-tools.ts
@@ -1911,7 +1911,7 @@
chunkSize: z
.number()
.int()
- .min(1)
+ .min(5)
.optional()
.describe(
'Events per batch (declared by the agent for transparency). Default 25; configurable via AMPLITUDE_WIZARD_EVENT_PLAN_CHUNK_SIZE. The runner is the source of truth for chunking — this field is informational.',You can send follow-ups to the cloud agent here.
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
2293362 to
0abe035
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Unguarded analytics block can prevent critical event commit
- Wrapped the pagination analytics block (dynamic import + computation) in a try/catch so any failure is logged but cannot prevent commitPlannedEventsStep from executing.
- ✅ Fixed: Proposed chunkSize can be below validation minimum
- Changed propose_event_plan's response payload to use the resolved chunkSize (always >= MIN_CHUNK_SIZE) instead of effectiveChunk, which could be below MIN_CHUNK_SIZE for small projects.
Or push these changes by commenting:
@cursor push b502a5376e
Preview (b502a5376e)
diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts
--- a/src/lib/agent-runner.ts
+++ b/src/lib/agent-runner.ts
@@ -1531,7 +1531,7 @@
// codebases hit the wizard before the chunked write phase is end-to-end
// wired through the SDK (the schema + tools land in this PR; the
// multi-session driver is a follow-up — see PR description).
- {
+ try {
const eventCount = agentResult.plannedEvents?.length ?? 0;
const { shouldPaginate, resolveChunkSize, buildBatches } = await import(
'./event-plan-pagination.js'
@@ -1551,6 +1551,12 @@
'total batches': totalBatches,
integration: config.metadata.integration,
});
+ } catch (err) {
+ logToFile(
+ `[agent-runner] pagination analytics threw: ${
+ err instanceof Error ? err.message : String(err)
+ }`,
+ );
}
const plannedEventsSummary = await commitPlannedEventsStep(
diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts
--- a/src/lib/wizard-tools.ts
+++ b/src/lib/wizard-tools.ts
@@ -2257,7 +2257,7 @@
const payload = {
totalEvents,
paginated,
- chunkSize: effectiveChunk,
+ chunkSize,
suggestedBatches,
};
logToFile(You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 0abe035. Configure here.
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
0abe035 to
33c8054
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
33c8054 to
ac80e4b
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
ac80e4b to
6aec44c
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
6aec44c to
c31bc99
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
c31bc99 to
b50ef95
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
b50ef95 to
e1887d1
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
e1887d1 to
3157e31
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
3157e31 to
9e0c4ea
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
fe1b6d0 to
7815e26
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
e64b786 to
33c0f8e
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
33c0f8e to
eba4775
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
eba4775 to
d28577b
Compare
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
a59ec22 to
d802810
Compare
SCALE_RESEARCH §C.2 + LLM_RELIABILITY_RESEARCH §B.2 converge on the same
recommendation: at ≥50 events the single-batch confirm_event_plan flow
runs into context-rot (~120 events) and the rich tracking-plan doesn't
survive context compaction (~200 events). The fix is to chunk the write
phase into ~25-event batches with a fresh context per chunk.
This PR lands the schema, tools, and persistence groundwork; the
multi-session runner driver is a follow-up (rationale in PR description).
Schema extensions to confirm_event_plan (additive — legacy shape still
works as today):
- decision?: 'confirm' | 'edit' | 'skip' -- agent self-declared intent
- batchIndex?: number -- 0-indexed batch when paginated
- totalBatches?: number -- total batches in the paginated plan
- chunkSize?: number -- events per batch (informational)
- events[].callsites?: [{filePath, anchor?}] -- per-event call sites,
persisted to .amplitude/events.json so the rich plan survives compaction
New propose_event_plan tool: pure pre-flight that takes a candidate event
list and returns suggested batch boundaries
({ totalEvents, paginated, chunkSize, suggestedBatches: [...] }).
events.json now optionally carries callsites[] per event (additive — small
projects remain {name, description} only). persistEventPlan strips empty
callsites so the file stays scannable.
Threshold + override:
- PAGINATION_THRESHOLD = 50 (event count)
- DEFAULT_CHUNK_SIZE = 25
- AMPLITUDE_WIZARD_EVENT_PLAN_CHUNK_SIZE env override (clamped [5, 100])
Runner-side: after the agent finishes, the runner logs/captures whether
the run crossed the pagination threshold (analytics breadcrumb
'event plan paginated'). Useful for measuring how often >50-event
codebases hit the wizard before the multi-session driver lands.
Validation: confirm_event_plan rejects malformed batch metadata
(batchIndex without totalBatches, batchIndex >= totalBatches, negative
values, empty events with batch fields, out-of-range chunkSize).
Tests: 30 new pagination unit tests + 4 new persistEventPlan callsite
tests + the WIZARD_TOOL_NAMES allowlist updated for propose_event_plan.
All 3118 existing unit tests + 100 BDD scenarios still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The batch-merge logic used readLocalEventPlan() which delegates to
parseEventPlanContent() — a function that maps events to only
{name, description}, stripping any persisted callsites. When batch 2+
merged with earlier batches, all callsite annotations from prior
batches were permanently lost.
Add readLocalEventPlanRich() that parses the on-disk JSON preserving
the optional callsites array, and use it in the merge path so
call-site annotations correctly accumulate across batches.
Applied via @cursor push command
…elper readLocalEventPlan and readLocalEventPlanRich shared ~35 lines of verbatim file-discovery (canonical / legacy candidate stat, freshest-by- mtime winner selection, read with logged failure paths). Lift the duplication into a private readFreshestEventPlanFile(installDir, label) helper so future callers don't drift. Behaviour is unchanged — both public readers continue to log under their own label and return [] on any non-fatal failure.
… JSON A user-edited `events.json` with non-string values (numeric `name`, `null` description, object-typed fields) crashed the batch-merge path inside `String.prototype.trim`. Coerce field values to safe strings, drop entries whose name can't be coerced, and guard non-object array entries / malformed callsites instead of trusting `as string` casts. Also align the return contract with `parseEventPlanContent`: return `null` for "missing or unparseable" and `[]` for "parsed cleanly but contained zero events" so the merge call site can distinguish the two — currently both produced `[]`. Update the only caller in `wizard-tools.ts` to default to `[]` when the rich reader returns `null` (nothing to merge). Adds regression tests covering numeric / null / object / boolean field values, primitive top-level entries, malformed callsites, and the new null-vs-empty distinction.
…ection Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata validation path returned a bare-text content block instead of going through toWizardToolErrorContent. Without the structured envelope the agent SDK doesn't see isError=true and the agent has no deterministic recovery path — it tends to retry the same malformed call until the circuit breaker fires. Switch to toWizardToolErrorContent so the response includes the standard {success:false, error, guidance, suggestedTool, context} shape and isError=true. Adds a regression test that exercises the batchIndex-without-totalBatches case.
- agent-runner: wrap pagination analytics block in try/catch so a dynamic import or analytics throw can never block the critical commitPlannedEventsStep that follows. - wizard-tools: align confirm_event_plan chunkSize zod schema with the business validator — schema now enforces [MIN_CHUNK_SIZE, MAX_CHUNK_SIZE] (5..100) instead of min(1), so the agent gets a clear API contract. - wizard-tools: propose_event_plan now echoes the resolved (validator- compatible) chunkSize unconditionally instead of returning totalEvents for non-paginated plans, which could yield 1–4 on tiny projects and be rejected by confirm_event_plan. - event-plan-pagination: export MIN_CHUNK_SIZE / MAX_CHUNK_SIZE so the schema can reference the same constants the validator uses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d802810 to
6411a90
Compare


Summary
Convergent recommendation from SCALE_RESEARCH §C.2 and
LLM_RELIABILITY_RESEARCH §B.2: at ≥50 events the single-batch
confirm_event_planflow runs into context-rot (~120 events) and therich tracking-plan doesn't survive context compaction (~200 events).
The fix is to chunk the write phase into ~25-event batches with a fresh
context per chunk.
This PR lands the schema, tools, and persistence groundwork for that
chunked write phase. The multi-session runner driver (driving the agent
through chunks via successive
runAgent()calls with reset context) isdeliberately deferred — see "Architectural punt" below.
What changed
confirm_event_planschema (additive — legacy shape still works)When
batchIndexandtotalBatchesare absent the tool behaves exactlyas today (single full plan, single persistence). When present they MUST
satisfy
0 ≤ batchIndex < totalBatches— invalid combinations arerejected with a structured error string before any persistence work runs.
New
propose_event_plantoolPure pre-flight that takes a candidate event list and returns suggested
batch boundaries:
{ "totalEvents": 75, "paginated": true, "chunkSize": 25, "suggestedBatches": [ { "batchIndex": 0, "totalBatches": 3, "start": 0, "end": 25, "eventNames": [...] }, { "batchIndex": 1, "totalBatches": 3, "start": 25, "end": 50, "eventNames": [...] }, { "batchIndex": 2, "totalBatches": 3, "start": 50, "end": 75, "eventNames": [...] } ] }For
≤50events the response is a single-batch covering everything (sothe agent can iterate the array unconditionally).
events.jsonschema additionsOptional
callsites: [{filePath, anchor?}]per event. Persisted onlywhen present — small-project runs round-trip the original
{name, description}shape unchanged.persistEventPlanstrips emptycallsites so the file stays scannable.
Per SCALE_RESEARCH §C.1: this is the field that lets the rich plan
survive compaction. After a compact the agent can re-read
.amplitude/events.jsonand remember where each track call was going togo without re-deriving placement from scratch.
Threshold + override
PAGINATION_THRESHOLDDEFAULT_CHUNK_SIZEAMPLITUDE_WIZARD_EVENT_PLAN_CHUNK_SIZEThe env override is clamped to
[5, 100]— out-of-range values fallback to the default with a debug-log breadcrumb. Defined in
src/lib/event-plan-pagination.ts.Where pagination kicks in
src/lib/event-plan-pagination.tsshouldPaginate,buildBatches,validateBatchMetadata,resolveChunkSize(pure helpers)src/lib/wizard-tools.tsconfirm_event_planvalidates batch metadata before persistencesrc/lib/wizard-tools.tspropose_event_plantool computes suggested batchessrc/lib/agent-runner.tssrc/lib/commandments.tspropose_event_planfirst when the candidate list >50Tests added
src/lib/__tests__/event-plan-pagination.test.ts— 30 unit tests coveringthe threshold, env override (valid / non-numeric / 0 / negative / out-of-range),
buildBatchescorrectness (75 events → 3 × 25, 200 events → 8 × 25,remainder, fractional chunk size),
validateBatchMetadata(legacy shape,partial fields, batchIndex >= totalBatches, negative / non-integer values,
out-of-range chunkSize), and the integration regression case (5 events
→ single batch, behavior unchanged).
src/lib/__tests__/wizard-tools.test.ts— 4 newpersistEventPlantests covering callsites round-trip, callsite-omission for legacy
callers, empty-callsites pruning, and per-callsite anchor pruning.
WIZARD_TOOL_NAMESallowlist test updated forpropose_event_plan.Architectural punt (rationale)
The full multi-session runner driver — the orchestrator that runs
runAgent()once per batch with a clean context per chunk — is notin this PR. That refactor lives in
src/lib/agent-interface.ts'srunAgentandsrc/lib/agent-runner.ts'srunAgentWizardBody, andbreaks the implicit assumption that one wizard run = one Claude Agent
SDK
query()call (SCALE_RESEARCH §C.2 final paragraph). It needs:propose_event_plan's response.batch's events plus the persisted callsites.
summary, session checkpoint refresh).
runtime-start/pre-compactcheckpoint flow.That's a separate, focused PR. What this PR provides is the
contract: schema, persistence shape, validation, and the
propose_event_planhelper the driver will call. The runner-side analytics breadcrumb
(
event plan paginated) lets us measure how often real-world runs crossthe threshold before the driver ships.
Strategic posture
Aligns with
MIGRATION_PLAN.md: this is "in-tree evolution" of theexisting wizard surface, additive and backwards-compatible — no new
binding model, no UX overhaul. Small projects (<50 events) get the same
behavior as today.
Test plan
pnpm test— all 3118 unit tests passpnpm test:bdd— all 100 BDD scenarios passpnpm lint— prettier + eslint cleanpnpm build— compiles, smoke test passes🤖 Generated with Claude Code
Note
Medium Risk
Introduces new event-plan batching and merge/persistence paths that affect
confirm_event_planbehavior and on-diskevents.jsonshape for large plans; mistakes could lead to incorrect plan persistence/merging or agent retry loops, though changes are additive and heavily tested.Overview
Adds pagination groundwork for large event plans: a new pure
event-plan-paginationhelper module (threshold >50, configurable chunk size, batch building, and batch-metadata validation) plus a newwizard-tools:propose_event_plantool that returns suggested batch boundaries for an event list.Extends
confirm_event_planto accept optionalbatchIndex/totalBatches/chunkSizeand per-eventcallsites, rejects invalid batch metadata with structured tool errors, and for non-first batches merges newly approved events into the existing.amplitude/events.jsonby name while preserving callsite annotations.persistEventPlannow normalizes/omits emptycallsites, andevent-plan-parseraddsreadLocalEventPlanRichto read/merge the richer schema safely.Adds runner/agent guidance breadcrumbs: a new commandment describing the paginated flow, and
agent-runnerlogs + emits analytics about whether a run crossed the pagination threshold. Comprehensive unit tests were added/updated to cover batching math, env overrides, rich parsing, callsites persistence, tool allowlists, and the new structured error path.Reviewed by Cursor Bugbot for commit 6411a90. Bugbot is set up for automated code reviews on this repo. Configure here.