Skip to content

feat(agent-core-v2): add full compaction to the human agent domain - #3703

Merged
sailist merged 1 commit into
MoonshotAI:devfrom
sailist:feat-human-full-compaction
Sep 10, 2026
Merged

feat(agent-core-v2): add full compaction to the human agent domain#3703
sailist merged 1 commit into
MoonshotAI:devfrom
sailist:feat-human-full-compaction

Conversation

@sailist

@sailist sailist commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

Internal feature task (feat-177) — no external issue.

Problem

The event-sourced "human" agent domain (agent-core-v2 src/human) has no context compaction: once history grows past the model window, turns fail with context overflow and there is no recovery path. The legacy domain (src/agent/fullCompaction) has full compaction, but its implementation is tied to the old loop/context-memory services and cannot drive the event-sourced agent.

What changed

Compaction orchestration — new human/compaction/ module. An external controller (createCompactionController) drives compaction through the agent machine's control surface: pause the agent so the active turn ends at a step boundary, snapshot the event-sourced store, run a summary turn, then switch the agent onto a fresh parent-less branch seeded with kept user messages (head/elision/tail selection ported from the legacy handoff), the summary block, and un-consumed queued inputs. Turn ids stay continuous across the switch, and undo cannot cross the compaction point (topological guard). The legacy production path is untouched, and the controller is not wired into production assembly yet (opt-in).

Compaction run as an xstate machine. Each run is an explicit machine (quiescing → summarizing → switching → resuming → completed/cancelled) whose transitions produce the observability surface the legacy domain had: observable events (compaction.started/blocked/cancelled/completed — cancels and failures both surface as cancelled with a cause, fixing the semantic inversion), durable lifecycle records in the _session log (compaction.started/completed/cancelled), a status() getter (the legacy compacting equivalent), an onWillCompact hook (with tokenCount), and rich terminal-event payloads (originTurnId, tokens before/after, summary usage/trace id/attempts/dropped count) so assembly can map telemetry (compaction_finished/failed/cancel) without the module depending on a telemetry client.

Cancellation and interruption. cancel() aborts a run while quiescing/summarizing (switching/resuming is the point of no return); a user abort (turn.aborting) cascades to cancel the compaction and leaves the agent paused — inputs and notifications accumulate and no new turn starts until an explicit input.continue. Inputs submitted/steered/notified during summarization are validated (input.*-only delta, the legacy historySafeToCompact semantics) and replayed onto the new branch instead of cancelling the compaction; non-input writes still cancel it as drift.

Budget trigger before the step. The turn machine gains a generic onBeforeStep hook (new gating state ahead of every step); the controller's implementation estimates request tokens (system prompt and tools included) and, when over budget, ends the turn before the over-budget request is sent, then compacts and resumes — the legacy beforeStep shouldBlock interception point (the legacy in-turn retry becomes end-turn + resume, same shape as the overflow path). Overflow recovery is driven by turn.failed with an attempt cap.

Agent primitives and store support. input.pause/input.continue on the agent machine (pause ends the turn at the step boundary and gates idle draining; continue resumes pending work or revives a tool-chain-ending history); SessionStores.switchBranch (parent-less branch + seed dispatch + reset + agent.switched, generalized from undo); agent.switched gains optional numeric stats.

Tests: new controller suite (8 cases — manual, step-boundary pause with queued-input survival, input merge during summarization (submit + steer), cancel API, abort cascade with paused accumulation, drift guard, budget gate before an over-budget step, overflow attempt cap), agent machine pause/continue cases, switchBranch store cases. Legacy src/agent behavior is unchanged.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (internal task; no external issue).
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset — the feature is not wired into the shipped CLI yet and is not user-perceivable.
  • Ran gen-docs skill, or this PR needs no doc update (internal mechanics only).

@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 28ab86a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 10, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@28ab86a
npx https://pkg.pr.new/@moonshot-ai/kimi-code@28ab86a

commit: 28ab86a

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7acab6ad6e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

actor.on('compaction.completed', (event) => {
deps.onEvent?.(event);
record(compactionCompleted({ agentId: deps.agentId, branch: event.branchId }));
lastCompactedTokens = event.stats.tokensBefore;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Compare budget progress using the same token total

When the system prompt or tool schemas keep the request above the trigger after compaction, budgetExceeded() compares that request-inclusive total with lastCompactedTokens, but this assignment records tokensBefore, which is calculated from history alone. With short/stable summaries and fixed overhead large enough to exceed the threshold, every continuation triggers another successful compaction, causing an unbounded summary/branch-switch loop without sending an ordinary LLM request. Record and compare equivalent request-inclusive totals, or terminate when compaction cannot reduce the request below the budget.

Useful? React with 👍 / 👎.

request: context.input.request,
}),
onDone: { target: 'thinking' },
onError: { target: 'done' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not mark blocked steps done before compaction succeeds

When the budget hook rejects, this transition completes the turn as done with no response while compaction is only scheduled asynchronously. If the automatic compaction then fails—for example because the summary request times out—the cancellation path merely unpauses the agent; the initiating prompt has already been drained into history, there is no pending work, and no new turn starts, so the user receives neither an answer nor a turn failure. Preserve/retry the blocked turn or propagate the hook failure unless compaction successfully schedules its continuation.

Useful? React with 👍 / 👎.

throw new CompactError('unknown-agent', `unknown agent: '${deps.agentId}'`);
}
deps.actor.send({ type: 'input.pause' });
const waiting = waitFor(deps.actor, (s) => s.matches('idle'), { timeout: PAUSE_TIMEOUT_MS });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wait for background tools before taking the snapshot

When a detached tool is still running, the agent is in idle.waiting, which still satisfies matches('idle'), so compaction proceeds immediately instead of reaching true quiescence. The later store reset aborts the agent scope and clears context.background; if the tool completes during summarization, its queued notification is also discarded by the reset. This silently cancels or loses background work, so the snapshot should wait for idle.ready/an empty background set or explicitly preserve those tasks and notifications.

Useful? React with 👍 / 👎.

Comment on lines +240 to +241
history: state.history,
queue: state.queue,

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 Carry pending reminders into the compacted branch

When an agent with existing history has a keyed reminder pending while idle, the snapshot retains only history and queue even though reminders are intentionally stored until the next turn drain. buildCompactionSeed() therefore emits no input.reminded events, and resetting onto the fresh branch initializes the reminder slice empty, silently removing plugin or system guidance before it is delivered. Include the snapshot's reminder state in the branch seed, as is already done for queued input.

Useful? React with 👍 / 👎.

@sailist
sailist force-pushed the feat-human-full-compaction branch from 7acab6a to 28ab86a Compare September 10, 2026 09:40
@sailist
sailist merged commit 58fb1a6 into MoonshotAI:dev Sep 10, 2026
13 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28ab86a74f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +211 to +213
deps.actor.on('turn.aborting', () => {
active?.actor.send({ type: 'cancel', cause: 'user-abort' });
}),

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 Cancel compaction when abort arrives after quiescence

When an abort is sent after quiescing has placed the agent in idle—for example while a slow summary request is running—agent/machine.ts ignores input.abort because it only emits turn.aborting from running.active. Consequently this subscription never cancels the compaction, and the request may run until its 120-second timeout and still switch branches despite the user's abort. Route aborts to the active compaction independently of whether an agent turn is still running.

Useful? React with 👍 / 👎.

Comment on lines +240 to +244
dispose: () => {
active?.actor.send({ type: 'cancel', cause: 'cancelled' });
for (const subscription of subscriptions) {
subscription.unsubscribe();
}

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 Prevent queued compactions from running after dispose

If onBeforeStep has already queued run('budget'), or an active run has populated pendingAuto, calling dispose() does not invalidate either scheduled run. The microtask can therefore start a new compaction after disposal, and cancelling the active actor can itself call firePending() and queue another run. This may summarize or switch the agent's branch after its owning controller has been torn down; mark the controller disposed and make run/firePending discard pending work.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant