Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e96f255
feat(agent-core): add cron ClockSources abstraction
sailist May 27, 2026
4edb7ce
feat(agent-core): add 5-field cron expression parser
sailist May 27, 2026
7507ac7
feat(agent-core): add deterministic cron jitter
sailist May 27, 2026
27ddbc8
feat(agent-core): add CronTask type and cron prompt origins
sailist May 27, 2026
aed8d24
test(agent-core): guard against Date.now() in cron scheduler files
sailist May 27, 2026
1785457
feat(agent-core): add cron telemetry event-name constants
sailist May 27, 2026
2e09ef8
feat(agent-core): add in-memory SessionCronStore
sailist May 27, 2026
e5a01a3
feat(agent-core): add session-only CronScheduler engine
sailist May 27, 2026
60a87f7
feat(agent-core): add CronManager Agent integration layer
sailist May 27, 2026
fbb6fb6
feat(agent-core): add CronCreate tool (session-only path)
sailist May 27, 2026
d79b95c
feat(agent-core): add CronList tool
sailist May 27, 2026
72c0af4
feat(agent-core): add CronDelete tool
sailist May 27, 2026
1b828ab
feat(agent-core): wire CronManager + cron tools into Agent
sailist May 27, 2026
825cf7b
feat(agent-core): add manual-tick env + SIGUSR1 bench hook
sailist May 27, 2026
9916c0d
test(agent-core): add end-to-end session cron smoke
sailist May 27, 2026
601c90c
test(agent-core): extract shared cron test harness
sailist May 27, 2026
31eb542
test(agent-core): trim cron test file headers
sailist May 27, 2026
a30f163
test(agent-core): convert cron tool output assertions to inline snaps…
sailist May 27, 2026
be1e864
refactor(cron): remove durable flag, env clock source, and enable cro…
sailist May 28, 2026
a387914
fix(agent-core): address PR #136 typecheck and Codex review for cron …
sailist May 28, 2026
6c476ee
fix(agent-core): address deep-review findings on cron implementation
sailist May 28, 2026
f7ba348
fix(agent-core): close remaining deep-review gaps on cron
sailist May 28, 2026
3f2204e
fix(agent-core): address Codex P2 findings on PR #136
sailist May 28, 2026
a6777c4
fix(agent-core,tui): address Codex P2 review on PR #136 round 4
sailist May 28, 2026
c263009
test(agent-core): unblock tsgo on cron-stop-on-close
sailist May 28, 2026
9dfd5ed
test(agent-core): anchor kaos type import to satisfy tsgo
sailist May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cron-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code": minor
---

Add session-scoped scheduled prompts so the assistant can register, list, and cancel recurring or one-shot reminders that fire later in the same session.
6 changes: 6 additions & 0 deletions apps/kimi-code/src/tui/controllers/session-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ export class SessionReplayRenderer {
if (message.origin?.kind === 'injection') {
return;
}
// WHY: cron fires are not user turns (see isReplayUserTurnRecord); skip
// visual render and turn advance so the raw <cron-fire ...> envelope never
// surfaces in the resumed transcript.
if (message.origin?.kind === 'cron_job' || message.origin?.kind === 'cron_missed') {
return;
}

this.flushAssistant(context);
const skill = skillActivationFromOrigin(message.origin);
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/src/tui/utils/message-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ function isReplayUserTurnRecord(record: AgentReplayRecord): boolean {
return message.origin.trigger === 'user-slash';
case 'background_task':
case 'compaction_summary':
case 'cron_job':
case 'cron_missed':
Comment on lines +249 to +250

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep cron replay limiting consistent with rendering

These cron-origin messages are excluded from limitReplayRecordsByTurn, but renderReplayUserMessage has no cron branch and falls through to advanceReplayTurn() for the same records. In a session with many cron-fired turns and few real user turns, resume can therefore include far more than REPLAY_TURN_LIMIT rendered turns while also showing cron prompts as plain user messages; either count these as turn starts or render/skip them consistently as internal cron events.

Useful? React with 👍 / 👎.

Comment on lines +249 to +250

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle cron replay origins consistently

When a resumed session contains cron fires, this filter stops counting cron_job/cron_missed records as user turn starts, but SessionReplayController.renderUserMessage has no matching branch for these origins and still falls through to advanceTurn() and renders the raw <cron-fire> envelope as a user message. That can both display internal cron XML as if the user typed it and let cron-generated turns bypass the replay turn limit because they were excluded here; please either render/skip cron origins explicitly in replay or count them consistently.

Useful? React with 👍 / 👎.

case 'hook_result':
case 'injection':
case 'system_trigger':
Expand Down
48 changes: 48 additions & 0 deletions apps/kimi-code/test/tui/message-replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,54 @@ describe('KimiTUI resume message replay', () => {
]);
});

it('skips cron_job origin records during replay', async () => {
const cronFire =
'<cron-fire jobId="job-1" cron="*/5 * * * *" recurring="true" coalescedCount="1">\nrun nightly\n</cron-fire>';
const driver = await replayIntoDriver([
message('user', [{ type: 'text', text: 'real prompt' }]),
message('assistant', [{ type: 'text', text: 'real answer' }]),
message('user', [{ type: 'text', text: cronFire }], {
origin: {
kind: 'cron_job',
jobId: 'job-1',
cron: '*/5 * * * *',
recurring: true,
coalescedCount: 1,
stale: false,
},
}),
]);

const transcript = driver.state.transcriptContainer.render(120).join('\n');
expect(transcript).not.toContain('<cron-fire');
expect(
driver.state.transcriptEntries
.filter((entry) => entry.kind === 'user')
.map((entry) => entry.content),
).toEqual(['real prompt']);
});

it('skips cron_missed origin records during replay', async () => {
const cronMissed =
'<cron-fire jobId="job-2" missed="true" count="3">\n3 one-shot tasks missed while offline\n</cron-fire>';
const driver = await replayIntoDriver([
message('user', [{ type: 'text', text: 'real prompt' }]),
message('assistant', [{ type: 'text', text: 'real answer' }]),
message('user', [{ type: 'text', text: cronMissed }], {
origin: { kind: 'cron_missed', count: 3 },
}),
]);

const transcript = driver.state.transcriptContainer.render(120).join('\n');
expect(transcript).not.toContain('<cron-fire');
expect(transcript).not.toContain('missed while offline');
expect(
driver.state.transcriptEntries
.filter((entry) => entry.kind === 'user')
.map((entry) => entry.content),
).toEqual(['real prompt']);
});

it('renders user-slash skill activation once without exposing injected prompt text', async () => {
const activation = message(
'user',
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/src/agent/context/projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ function isInjectionUserMessage(message: Message): boolean {
if (trimmed.startsWith('<notification ')) return true;
if (trimmed.startsWith('<system-reminder>')) return true;
if (trimmed.startsWith('<hook_result ')) return true;
if (trimmed.startsWith('<cron-fire ')) return true;
return false;
}

Expand Down
19 changes: 19 additions & 0 deletions packages/agent-core/src/agent/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ export interface BackgroundTaskOrigin {
readonly notificationId: string;
}

export interface CronJobOrigin {
readonly kind: 'cron_job';
readonly jobId: string;
readonly cron: string;
readonly recurring: boolean;
/** Number of theoretical fires that were collapsed into this single delivery (>= 1). */
readonly coalescedCount: number;
/** True for recurring tasks past the 7-day age threshold. */
readonly stale: boolean;
}

export interface CronMissedOrigin {
readonly kind: 'cron_missed';
/** Number of one-shot tasks bundled into this missed-fire notification. */
readonly count: number;
}

export interface HookResultOrigin {
readonly kind: 'hook_result';
readonly event: string;
Expand All @@ -55,6 +72,8 @@ export type PromptOrigin =
| CompactionSummaryOrigin
| SystemTriggerOrigin
| BackgroundTaskOrigin
| CronJobOrigin
| CronMissedOrigin
| HookResultOrigin;

export type ContextMessage = Message & {
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/src/agent/cron/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CronManager, type CronManagerOptions } from './manager';
Loading
Loading