Skip to content

refactor(agent-core-v2): derive session busy from agent activity - #1751

Merged
kermanx merged 25 commits into
mainfrom
redo/session-busy
Jul 15, 2026
Merged

refactor(agent-core-v2): derive session busy from agent activity#1751
kermanx merged 25 commits into
mainfrom
redo/session-busy

Conversation

@kermanx

@kermanx kermanx commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue; this PR addresses the reported session-activity and duplicate-stream regressions described below.

Problem

Session activity was represented by a separate five-value state machine even though the underlying work already belonged to individual agents. The duplicated state could diverge at turn boundaries: background work could keep the chat moon alive after the main turn ended, while the Stop button, sidebar spinner, and aborted badge each interpreted activity differently.

Two independent lifecycle races could also duplicate streamed content. An LLM retry detached the abandoned branch without clearing its partial assistant bubble, and concurrent session activation could create multiple broadcaster states and event-bus subscriptions. Those leaked listeners advanced the same current tracker, so identical deltas received consecutive offsets and reached the client as valid AABBCC content rather than replay duplicates.

What changed

  • Added agent-scoped activity views derived from turn, approval, step, and background-task facts, and removed the session activity/admission state. Session busy is now aggregated from agent activity (turn != null || background.length > 0).
  • Replaced the protocol's five-value session status with busy and last_turn_reason, and added durable event.session.work_changed updates across snapshots, routes, projections, and Web reducers.
  • Made main-turn liveness the shared condition for the chat moon, Stop button, sidebar spinner, and aborted-badge quiet gate; background tasks and subagent turns do not keep those UI indicators active.
  • Reused the abandoned assistant bubble when an LLM step retries, preventing partial text and tool calls from being rendered twice.
  • Made broadcaster session activation single-flight and tightened journal flushing so concurrent subscribers cannot install duplicate listeners or leave final durable events pending.
  • Added contract and regression coverage, including a deterministic concurrent-activation test that proves one agent event is emitted once.
  • Added one patch changeset for @moonshot-ai/kimi-code.

Validation

  • packages/protocol: 28 test files / 518 tests passed; typecheck passed.
  • packages/agent-core: 222 test files passed (3,803 tests passed, expected failures/skips unchanged); typecheck passed.
  • packages/kap-server: 56 test files / 651 tests passed; typecheck passed.
  • packages/agent-core-v2: typecheck and domain-layer check passed; the three resource-sensitive test files passed in isolation (101 tests).
  • apps/kimi-web: resync regression passed in isolation; typecheck, style baseline check, and production build passed.
  • Repository-wide type-aware oxlint completed with 0 errors (existing warnings remain).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue or explained why there is no linked issue.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run the gen-changesets skill and added the required changeset.
  • I have run the gen-docs skill; no independent documentation update is needed because this changes runtime status semantics without adding a user-facing command or configuration surface.

kermanx added 14 commits July 15, 2026 17:16
… web moon stuck after stop

The web moon occasionally stayed visible after a session's turn stopped.
Root cause chain: the session's activity status was a derived state machine
fed by registered bookings, which could wedge; the web then trusted it.

Redesign around a single source of truth:

- agent-core-v2: IAgentActivityView (Agent scope) is the only activity
  model, folded purely from the agent's own event bus: { lifecycle, turn?,
  lastTurn?, background[] }. background = live background tasks (any kind:
  process/agent/question), seeded from IAgentTaskService and folded from
  task.started/terminated. busy = turn != null || background.length > 0.
- Session level stores no activity state: the admission/drain registry
  (admitWork/canAccept/hasBusyAgents/SessionCommand/closing/quiesce) is
  deleted, the loop no longer reports to the session, agent creation has
  no gate, and session.rejected is retired.
- fork is unconditional: a mid-work copy is crash-equivalent, which replay
  already normalizes on every restore.
- kap-server derives the wire facts from the agents' activity: Session.busy
  and event.session.work_changed aggregate any agent's turn/background;
  last_turn_reason passes through the main agent's latest outcome.
  SessionEventJournal now chains a follow-up flush when appends arrive
  mid-flush, so the final events of a turn are not parked indefinitely.
- protocol/web: Session.busy + last_turn_reason replace the five-value
  status enum; the web moon keys off the main agent's turn boundaries,
  the sidebar spins while any agent is busy, and the terminated label
  reads last_turn_reason.
# Conflicts:
#	apps/kimi-web/test/event-batcher.test.ts
#	packages/agent-core-v2/src/activity/activity.ts
#	packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts
…and the aborted badge

The new wire busy fact is approval-blind (turn != null || background), while
the retired five-value status substituted awaiting_* for running/aborted
while input was pending. Without the client-side adjustment the sidebar
spinner kept spinning through approval/question waits and the aborted badge
could show while a new turn was awaiting input:

- isSessionEffectivelyRunning suppresses the spinner while the session has
  pending approvals/questions (mirrors awaiting_* not being 'running').
- The aborted badge (desktop + mobile) hides while attention is pending
  (mirrors awaiting_* superseding 'aborted').
- Snapshot seeding of turnActiveBySession now also requires the snapshot's
  busy fact, so a stale in-flight tracker entry (turn.ended lost on abrupt
  disposal) can never relight the moon.
Two symptoms, one root: after sending a prompt the moon spun but the Stop
button stayed hidden (nothing covered the optimistic submit window), and
after the main turn ended with only background tasks left the chat-area
indicators kept spinning (the wire busy fact deliberately includes
background tasks, while the retired turn-scoped status went idle).

The activity computed (Stop button, composer/page-title spinners,
send-vs-queue gating, skill activation) now derives 'running' from
inFlight || turnActive — exactly the working moon's condition — instead of
the wire busy fact. The sidebar spinner keeps the wire busy (turn or
background task), unchanged.
…rn-end gate starvation

- The chat moon, the sidebar spinner, and the Stop button now share ONE
  condition: prompt submitted to main turn ended (inFlight || turnActive).
  Background tasks and subagent turns no longer light any of them (the
  sidebar previously used the wire busy fact, which deliberately includes
  them). The aborted badge inherits the same quiet gate (plus the existing
  attention suppression).
- agentEventProjector's turn.ended arm now emits turnActiveChanged FIRST:
  the onMainTurnEnd side effect gates on seq > lastSeqBySession, and the
  sibling events (messageUpdated/sessionUsageUpdated) advance that cursor,
  which starved the gate — the prompt-finish cleanup never ran, so inFlight
  stuck true and the moon stayed on whenever a turn ended with background
  work still running (no work_changed(busy:false) fallback in that case).
…ad of stacking a duplicate

A retried step restarts its stream from offset 0, but the projector only
unlinked the failed attempt's assistant bubble (currentAssistantMsgId =
undefined) and let the retried step.started create a fresh one — the
partial bubble stayed rendered next to the retry's full stream, so text and
tool cards showed twice. Much more visible since the per-step LLM retry
budget grew (#1740), which makes retries routine.

turn.step.retrying now strips the abandoned bubble's streamed parts (text /
thinking / toolUse) via messageUpdated and records it in retryReuseMsgId;
the retried step.started refills that same bubble in place. turn.step.
interrupted keeps the discard semantics. Verified live against dev:v2:
multi-resync + retry sequences no longer duplicate content across bubbles.
…aster

Two concurrent activations of the same session (a WS subscribe racing a
REST snapshot / replay / cursor read) each built their own SessionState:
two agent bus subscriptions, two journal writers on one file, and — once a
re-subscribe landed the connection's target in BOTH states — every delta
fanned out twice. The blind twin state (created mid-turn, its tracker never
saw turn.started) annotated no offset, so the client-side alignDelta dedup
(identity by offset) appended those copies too: streamed text and tool
cards showed twice, interleaved per chunk ('AABBCC').

ensureState/ensureGlobalState now share an in-flight promise per session,
so activation is atomic and exactly one SessionState exists per session.
Includes a regression test driving six concurrent activations.
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 645fa2f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

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

@kermanx
kermanx marked this pull request as ready for review July 15, 2026 12:09
@pkg-pr-new

pkg-pr-new Bot commented Jul 15, 2026

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

commit: 645fa2f

@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: e0793d832b

ℹ️ 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 +1749 to +1750
if (snapshot.inFlightTurn !== null) return;
if (snapshot.status !== 'idle' && snapshot.status !== 'aborted') return;
if (snapshot.busy) return;

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 Don’t block terminal snapshot cleanup on aggregate busy

When a resync snapshot arrives after the main turn has ended but the session is still busy because background/subagent work remains, inFlightTurn is null yet this return skips finishPromptLocal; the same stale-local-state problem also occurs if the snapshot carries a stale inFlightTurn while busy is false because the previous line returns first. Since working still includes rawState.inFlightBySession, the moon and queued prompt drain can stay stuck until all aggregate work ends, which reintroduces the background-work case this refactor is trying to separate from main-turn liveness.

Useful? React with 👍 / 👎.

Comment on lines 185 to 187
if (reason === 'completed' || reason === 'cancelled' || reason === 'failed') {
this._lastTurnReasonBySession.set(sessionId, reason);
}

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 Map blocked turns to a terminal last-turn reason

In the legacy engine path, turn.ended can carry reason: 'blocked' (for example when a user-prompt hook blocks the turn), and the old status code treated that as an aborted terminal state. After this change, the busy-false event.session.work_changed emitted for that turn keeps the previous/undefined last_turn_reason, so clients no longer get the cancelled/failed fact needed to render the terminal aborted/failed state; the v2 broadcaster maps non-completed/non-cancelled reasons to failed, so this path should do the same.

Useful? React with 👍 / 👎.

@kermanx

kermanx commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@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: 7fccfc0e2e

ℹ️ 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".

status: s.status,
busy: isSessionEffectivelyRunning(s),
time: formatTime(s.updatedAt),
busy: isMainTurnActive(s.id),

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 Use aggregate busy for sidebar rows

When the server reports event.session.work_changed with busy: true for background-only/subagent work, that value is stored on the AppSession as s.busy, but this sidebar view replaces it with isMainTurnActive(s.id). As a result the desktop/mobile session rows render no spinner, and the aborted badge can appear, while aggregate session work is still running. Keep the main-turn helper for the chat moon/Stop path, but pass the server aggregate s.busy into sidebar session rows.

Useful? React with 👍 / 👎.

delete rawState.queuedBySession[sessionId];
delete rawState.promptIdBySession[sessionId];
delete rawState.sendingBySession[sessionId];
delete rawState.inFlightBySession[sessionId];

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 Clear stale turn activity when forgetting sessions

This cleanup now clears the optimistic inFlightBySession half but leaves the new authoritative turnActiveBySession entry behind. If a session is archived/not-found while its main turn is active, then restored or otherwise upserted again in the same tab, isMainTurnActive() will keep returning true from the stale map entry even though the restored session is idle. Delete rawState.turnActiveBySession[sessionId] here along with the other per-session liveness state.

Useful? React with 👍 / 👎.

@kermanx

kermanx commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@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: cd21150277

ℹ️ 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 +311 to +312
loop.cancel(undefined, abortError('Agent removed'));
await loop.settled();

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 Cancel queued turns before awaiting loop settlement

When an agent is removed while it has a running turn plus one or more queued turns, loop.cancel(undefined, ...) only aborts the active turn; queued turns are cancelled only when a specific turnId is passed. Those jobs remain in pendingTurns, and settled() waits for them to empty, so after the active turn aborts the loop can pump and execute queued prompts during session close/archive instead of shutting down. Please cancel the pending turn ids as well before awaiting settlement.

Useful? React with 👍 / 👎.

@kermanx

kermanx commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@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: 97a0e09278

ℹ️ 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".

status: s.status,
busy: isSessionEffectivelyRunning(s),
time: formatTime(s.updatedAt),
busy: isMainTurnActive(s.id),

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 Preserve listed sessions' busy state before snapshot

For sessions that are already running when the page loads (or that are not among the retained WS subscriptions), turnActiveBySession and inFlightBySession have not been seeded yet, so isMainTurnActive(s.id) returns false even though the REST session row has s.busy === true. The sidebar then renders those running sessions as idle (and can even show the aborted tag from a previous turn) until the user opens the session and a snapshot seeds the local turn flag; keep the server busy fact as a fallback or expose a list-level main-turn flag.

Useful? React with 👍 / 👎.

<Tooltip :text="t('workspace.awaitingAnswerTitle')">
<Badge
v-if="!renaming && (questionCount > 0 || session.status === 'awaitingQuestion')"
v-if="!renaming && questionCount > 0"

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 an action-required fallback for unseen sessions

When an unselected session is already awaiting a question after a refresh, questionCount is still 0 because pending questions are only populated by a session snapshot or session-scoped WS events; sessions that have not been opened (or whose subscription was evicted) do not have those counts. The removed session.status === 'awaitingQuestion' fallback was the only list-level signal, so the sidebar no longer shows the required-action badge for those sessions; the same issue applies to the approval badge below.

Useful? React with 👍 / 👎.

@kermanx

kermanx commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@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: 5fc10fac84

ℹ️ 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 +307 to +313
/** Agent teardown aborts the in-flight compaction via scope disposal — no
* registry: the service owns its own background work. */
override dispose(): void {
if (this._compacting !== null && !this._compacting.abortController.signal.aborted) {
this._compacting.abortController.abort();
}
super.dispose();

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 Keep compaction in the activity fold

When manual or auto full compaction runs while no turn is active, this now only aborts it on scope disposal and no longer registers it as background activity. Session busy is derived from IAgentActivityView.state().turn/background, and agentLifecycle.remove now waits only for loop.settled(), so status/snapshots can report idle during compaction and archive/close can dispose the agent before the compaction worker finishes applying the compacted context. Keep compaction visible to the activity view or model it as an agent task so busy/drain semantics still include it.

Useful? React with 👍 / 👎.

};
state.activityByAgent.set(agentId, next);
if (
previous !== undefined &&

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 Seed late-agent activity before suppressing first background update

When an agent is created after this broadcaster has activated a session, attachAgents only subscribes it and never inserts an idle fold into activityByAgent. If that agent's first activity update is background-only, such as a detached/restored task without a turn boundary, previous is undefined, so this guard suppresses enqueueWorkChanged; the map becomes busy but emittedBusy remains false, and the terminating update later dedupes as false→false. In that scenario the v1 WS stream/journal/replay never publishes the session busy transition for that background work.

Useful? React with 👍 / 👎.

status: event.status,
currentPromptId: event.currentPromptId,
busy: event.busy,
mainTurnActive: event.mainTurnActive ?? s.mainTurnActive,

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 Reset stale main-turn state on busy-false events

When a sessionWorkChanged event arrives with busy: false but omits mainTurnActive (the protocol marks it optional, and processEvent explicitly treats this as an authoritative quiet fallback), this keeps the prior s.mainTurnActive. The active-session turnActive computed and sidebar row both fall back to the session field, so a terminal partial/compat work_changed can clear turnActiveBySession while the stale session object immediately relights the moon/Stop/sidebar spinner.

Useful? React with 👍 / 👎.

* the smooth typewriter/fade reveal; all other turns render statically.
*/
running?: boolean;
turnActive?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Update side-chat callers for the renamed liveness prop

After renaming ChatPane's liveness prop here, SideChatPanel.vue still renders <ChatPane :running="running" :sending="sending">, so those values no longer reach props.turnActive/props.working and side-chat assistant turns are never marked as streaming. In a BTW side chat while the agent is replying, the panel loses the streaming render behavior; update that caller or keep compatibility aliases for the old prop names.

Useful? React with 👍 / 👎.

kermanx added 2 commits July 15, 2026 22:45
# Conflicts:
#	packages/kap-server/src/routes/sessions.ts
#	packages/kap-server/src/services/snapshot/snapshotReader.ts
#	packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts
@kermanx

kermanx commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

# Conflicts:
#	apps/kimi-web/test/agent-event-projector.test.ts

@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: 5672a9fbd7

ℹ️ 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".

content: msg.content.map((c) => ({ ...c })),
status: 'pending',
});
s.retryReuseMsgId = msgId;

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 Clear retry reuse state when the turn exits

When turn.step.retrying fires and the user aborts or the turn fails before the retried turn.step.started arrives, this stores the old assistant message id indefinitely. The turn.ended path clears currentAssistantMsgId but not retryReuseMsgId, so the next prompt's first step will reuse the previous turn's emptied bubble instead of creating a new assistant message, causing the new response to render under the wrong prompt. Clear this field on terminal/interrupted turn paths.

Useful? React with 👍 / 👎.

@kermanx

kermanx commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@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: bc539545d8

ℹ️ 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".

}

private onTurnStarted(turnId: number, origin?: PromptOrigin): void {
this.turn = new MutableTurn(turnId, origin ?? USER_PROMPT_ORIGIN);

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 Clear the previous outcome when a new turn starts

When a main turn starts after a prior cancelled/failed turn, lastTurn is left intact here. The v1 broadcaster folds agent.activity.updated.lastTurn into event.session.work_changed.last_turn_reason, so the new running turn (and REST session facts via resolveSessionFacts) keep reporting the previous outcome instead of clearing it until another terminal event arrives. Clear this.lastTurn in onTurnStarted before publishing the activity update.

Useful? React with 👍 / 👎.

busy: event.busy,
mainTurnActive: event.mainTurnActive ?? (event.busy ? s.mainTurnActive : false),
pendingInteraction: event.pendingInteraction ?? s.pendingInteraction,
lastTurnReason: event.lastTurnReason ?? s.lastTurnReason,

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 Allow work_changed events to clear turn outcomes

For event.session.work_changed, an omitted last_turn_reason is how the new protocol represents “no current outcome” (for example, a main turn.started should clear a previous cancelled/failed outcome). This nullish fallback preserves the stale value instead, so once a session has a failed/cancelled reason the client cannot clear it from a work_changed event and can later show an outdated aborted state. Apply the event value authoritatively instead of falling back to the old session value.

Useful? React with 👍 / 👎.

private readonly _abortedTurns = new Set<string>();
/** MAIN-agent latest turn outcome per session — an orthogonal wire fact
* clients may present as an "aborted" tag (busy=false + cancelled/failed). */
private readonly _lastTurnReasonBySession = new Map<string, 'completed' | 'cancelled' | 'failed'>();

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 Reset the legacy last-turn reason on turn start

This new cache is populated on cancelled/failed turns but is never cleared when a later turn starts. In the legacy agent-core path, a session that previously failed will keep emitting/returning that last_turn_reason during the next active turn, unlike the old _abortedTurns state which was cleared on new work. Clear _lastTurnReasonBySession on turn.started (or prompt submission) so v1 and v2 expose the same fresh-turn semantics.

Useful? React with 👍 / 👎.

@kermanx

kermanx commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@kermanx
kermanx merged commit df75a0f into main Jul 15, 2026
14 checks passed
@kermanx
kermanx deleted the redo/session-busy branch July 15, 2026 15:34
@github-actions github-actions Bot mentioned this pull request Jul 15, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 645fa2f88a

ℹ️ 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".

sailist added a commit to sailist/kimi-code that referenced this pull request Jul 16, 2026
The engine retired its sessionActivity service in MoonshotAI#1751 (session busy is
now derived from agent activity views), but the facade still called the
deleted wire channel and imported the deleted engine module, breaking
typecheck and every klient suite at import time.

- drop the sessionActivity contract/registry entries and mirror the
  agentActivityView service instead (agent scope)
- compose session status() client-side from the pending interaction lists
  and each agent's agentActivityView, keeping the retired service's
  precedence and typing SessionStatus locally in the facade
- replace the deleted-channel call in the v2 smoke suite with a
  sessionInteractionService probe
- fix the legacy image-file suite to wait with the harness's
  waitForSessionBusy
sailist added a commit that referenced this pull request Jul 16, 2026
…1768)

* feat(klient): contract-driven facade with http/ipc/memory transports

- add zod-validated contract sections (global/session/agent) under
  src/contract and a facade exposing global.*, session(id).*, agent(id).*
- select transport once at creation via subpath entries
  (@moonshot-ai/klient/http|ipc|memory); drop legacy channel/client/
  httpChannel/wsChannel/wsKlient/proxy implementations
- absorb packages/server-e2e into packages/klient test/e2e suites
  (dual-backend, legacy v1, v2 wire) and remove the server-e2e workspace
- expose model registry and catalog services on kap-server v2 RPC surface

* fix(klient): derive session status from agentActivityView

The engine retired its sessionActivity service in #1751 (session busy is
now derived from agent activity views), but the facade still called the
deleted wire channel and imported the deleted engine module, breaking
typecheck and every klient suite at import time.

- drop the sessionActivity contract/registry entries and mirror the
  agentActivityView service instead (agent scope)
- compose session status() client-side from the pending interaction lists
  and each agent's agentActivityView, keeping the retired service's
  precedence and typing SessionStatus locally in the facade
- replace the deleted-channel call in the v2 smoke suite with a
  sessionInteractionService probe
- fix the legacy image-file suite to wait with the harness's
  waitForSessionBusy
ywh114 pushed a commit to ywh114/kimi-code that referenced this pull request Jul 19, 2026
…oonshotAI#1768)

* feat(klient): contract-driven facade with http/ipc/memory transports

- add zod-validated contract sections (global/session/agent) under
  src/contract and a facade exposing global.*, session(id).*, agent(id).*
- select transport once at creation via subpath entries
  (@moonshot-ai/klient/http|ipc|memory); drop legacy channel/client/
  httpChannel/wsChannel/wsKlient/proxy implementations
- absorb packages/server-e2e into packages/klient test/e2e suites
  (dual-backend, legacy v1, v2 wire) and remove the server-e2e workspace
- expose model registry and catalog services on kap-server v2 RPC surface

* fix(klient): derive session status from agentActivityView

The engine retired its sessionActivity service in MoonshotAI#1751 (session busy is
now derived from agent activity views), but the facade still called the
deleted wire channel and imported the deleted engine module, breaking
typecheck and every klient suite at import time.

- drop the sessionActivity contract/registry entries and mirror the
  agentActivityView service instead (agent scope)
- compose session status() client-side from the pending interaction lists
  and each agent's agentActivityView, keeping the retired service's
  precedence and typing SessionStatus locally in the facade
- replace the deleted-channel call in the v2 smoke suite with a
  sessionInteractionService probe
- fix the legacy image-file suite to wait with the harness's
  waitForSessionBusy
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