Skip to content

Commit c16fd4f

Browse files
committed
fix(agent-core): count cron deliveries as replay turn boundaries
1 parent 25be5dc commit c16fd4f

3 files changed

Lines changed: 50 additions & 10 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Fix slow resume replay for sessions with many cron turns.

packages/agent-core/src/agent/replay/turns.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import type { AgentReplayRecord } from '../../rpc/resumed';
55
*
66
* A record starts a new user turn when it is a user-role message that came
77
* from an actual user action — a typed prompt, a user-invoked skill/plugin
8-
* slash command, or a `!` shell command's input line. System-originated user
9-
* messages (compaction summaries, cron fires, hook results, retries, goal
10-
* reminders, background-task results, injections) continue the current turn
11-
* instead — with one exception: `goal_continuation` prompts. The goal driver
12-
* fires one synthetic continuation prompt per goal turn (see
13-
* agent/turn/index.ts), and the goal system itself counts those as turns, so
14-
* replay trimming treats them as turn boundaries; otherwise a 100-round goal
15-
* would count as a single user turn and resume would replay the entire run.
8+
* slash command, a `!` shell command's input line, or a cron delivery
9+
* (`cron_job` / `cron_missed`). Scheduled fires are real rounds of work with
10+
* their own prompts and reports; counting them keeps resume replay bounded for
11+
* sessions dominated by many scheduled turns. Other system-originated user
12+
* messages (compaction summaries, hook results, retries, goal reminders,
13+
* background-task results, injections) continue the current turn instead —
14+
* with one exception: `goal_continuation` prompts. The goal driver fires one
15+
* synthetic continuation prompt per goal turn (see agent/turn/index.ts), and
16+
* the goal system itself counts those as turns, so replay trimming treats them
17+
* as turn boundaries; otherwise a 100-round goal would count as a single user
18+
* turn and resume would replay the entire run.
1619
*
1720
* Source of truth for turn-boundary detection; the TUI mirrors this through
1821
* the SDK re-export instead of keeping its own predicate.
@@ -32,10 +35,11 @@ export function isAgentReplayUserTurnRecord(record: AgentReplayRecord): boolean
3235
case 'shell_command':
3336
// A `!` command's input is a user-turn anchor; its output is not.
3437
return message.origin.phase === 'input';
35-
case 'background_task':
36-
case 'compaction_summary':
3738
case 'cron_job':
3839
case 'cron_missed':
40+
return true;
41+
case 'background_task':
42+
case 'compaction_summary':
3943
case 'hook_result':
4044
case 'injection':
4145
case 'retry':

packages/agent-core/test/agent/resume.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,4 +1764,35 @@ describe('limitAgentReplayByTurns', () => {
17641764
// trailing reminder stays attached to the last kept turn.
17651765
expect(limited).toEqual(records.slice(11));
17661766
});
1767+
1768+
it('treats cron deliveries as turn boundaries so scheduled storms stay bounded', () => {
1769+
const records: AgentReplayRecord[] = [];
1770+
for (let turn = 0; turn < 20; turn += 1) {
1771+
records.push(
1772+
replayMessage('user', `cron fire ${turn}`, {
1773+
kind: 'cron_job',
1774+
jobId: 'job-1',
1775+
cron: '*/15 * * * *',
1776+
recurring: true,
1777+
coalescedCount: 1,
1778+
stale: false,
1779+
}),
1780+
);
1781+
records.push(replayMessage('assistant', `report ${turn}`));
1782+
}
1783+
const limited = limitAgentReplayByTurns(records, 5);
1784+
expect(JSON.stringify(limited)).toContain('cron fire 15');
1785+
expect(JSON.stringify(limited)).not.toContain('cron fire 14');
1786+
});
1787+
1788+
it('treats cron missed deliveries as turn boundaries', () => {
1789+
const records: AgentReplayRecord[] = [];
1790+
for (let turn = 0; turn < 20; turn += 1) {
1791+
records.push(replayMessage('user', `missed ${turn}`, { kind: 'cron_missed', count: 3 }));
1792+
records.push(replayMessage('assistant', `report ${turn}`));
1793+
}
1794+
const limited = limitAgentReplayByTurns(records, 5);
1795+
expect(JSON.stringify(limited)).toContain('missed 15');
1796+
expect(JSON.stringify(limited)).not.toContain('missed 14');
1797+
});
17671798
});

0 commit comments

Comments
 (0)