Skip to content

fix(cloud-agent-sdk): tidy session manager state updates - #8516

Merged
iscekic merged 2 commits into
mainfrom
drain/session-manager-state-updates
Oct 22, 2026
Merged

iscekic merged 2 commits into
mainfrom
drain/session-manager-state-updates

Conversation

@iscekic

@iscekic iscekic commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Change

Three state-update fixes in packages/cloud-agent-sdk/src.

  1. Pending-delivery map (session-manager.ts): the service-state subscriber published new Map(getPendingMessages()) on every notification. The service state notifies for every event and mutates one shared map in place, so preparation step output and stale preparation events replaced the atom with an equivalent new map and invalidated every dependent atom. The write is now gated on an entry comparison (pendingDeliveryStatesEqual): same size, and for each id the same status, and for a failed delivery the same error, reason, and attempts. A changed entry still publishes a fresh map.

  2. Interrupt abort guard (session-manager.ts): the guard that suppresses the Aborted error produced by the manager's own Stop was cleared only by a matching Aborted or a full reset, so a rejected or acknowledged-without-abort interrupt left it armed and swallowed a later genuine Aborted instead of storing it in errorAtom. It now clears when the interrupt call rejects, and expires INTERRUPT_ABORT_SUPPRESSION_MS (30 s) after the call settles; the matching-Aborted and reset paths use the same clearPendingInterrupt() helper, which also drops the pending expiry timer, and re-arming through armPendingInterruptExpiry() drops a previous expiry first so overlapping interrupt calls cannot end the newer call's window early. A late abort — including one replayed after a reconnect, whose backoff caps at 30 s per attempt — still stays suppressed inside the window. The expiry timer does not hold a Node host open (scheduleUnrefdTimer).

  3. Storage deleteMessage (storage/memory.ts, storage/jotai.ts): both backends drop the message row first and returned early when the row was absent, but both create part rows from a delta before the row exists. A removal for such an id left parts, the parts snapshot, and the ordering evidence behind. Parts cleanup now runs regardless of the row, and parts:<id> is notified when part state existed; message:<id> and the id-list notification stay tied to the row existing.

End-to-end evidence

Run on the work VM, in the package:

  • pnpm exec jest in packages/cloud-agent-sdk: 35 suites, 1756 tests passed.
  • pnpm exec tsgo --noEmit: clean. oxlint and oxfmt --check on the six changed files: clean.

Behavior observed, not just asserted:

  • Real service state through the real manager (service state integration test): a createServiceState instance feeds the manager's subscriber. A preparation step-output event and a question event leave the published pending map at the same identity, and a cloud.message.queued event publishes the new entry.
  • Before/after on the pre-change code (pre-change file planted over the branch file, same tests):
    • the unchanged-content test fails with the atom replaced by a new map of equal content;
    • the rejected-interrupt and post-window tests read a null errorAtom instead of the surfaced Aborted;
    • the part-only deleteMessage tests, in both backends, read the parts back after the delete.
  • All three new groups pass on the branch.

The package is a library with no runnable surface of its own; the service-state integration test above is the runtime proof, driven by the real notifier rather than a mocked subscriber.

Proxy answers

The owner's proxy gave these answers in the owner's place.

  1. Question: Guard lifetime for the one-shot Aborted suppression: bounded window after the interrupt settles, next user turn start, or both?
    Answer: Bounded window after the interrupt settles
    Confidence: 0.60 · Precedent: Speed and least work: do the least amount of work possible, with no drop in quality. One clearing path that never leaves the guard armed indefinitely.
  2. Question: If a bounded window is used, how long after the interrupt RPC settles should a matching Aborted still be suppressed?
    Answer: 30 seconds
    Confidence: 0.55 · Precedent: Autonomy: use "is this reasonable?" as the test. 30 s matches the SDK's reconnect backoff cap.
  3. Question: Finding 1 also suggests coalescing service-state notifications to one subscriber pass per frame; the work item asks only for the write to be gated. Which scope?
    Answer: Gate the atom write only
    Confidence: 0.75 · Precedent: Speed and least work: skip work that does not need to be done. Put each separate concern in its own PR.

- Publish the pending-delivery map only when its entries differ from the
  published one, so a notification for preparation output or a stale event
  does not replace the atom with an equivalent new map.
- Scope the interrupt abort guard to the interrupt call: clear it when the
  call rejects and expire it 30s after it settles, so a later Aborted reaches
  errorAtom.
- Remove parts, the parts snapshot and the ordering evidence for an id whose
  message row does not exist, in both storage backends.
@iscekic iscekic added drain Work item from the findings drain janitor:performance-reliability Janitor performance-reliability findings labels Oct 22, 2026
@kilo-code-bot

kilo-code-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental change extracts the interrupt suppression expiry into armPendingInterruptExpiry(), which clears any tracked timer before re-arming, resolving the previously reported orphaned-timer defect; the new overlap test covers the two-call window ordering.

Assumptions: [INFERENCE] No new issues were found in the 2 files changed since 796257373; the earlier deleteMessage and pending-delivery gating findings remain verified on unchanged code.

Files Reviewed (2 files)
  • packages/cloud-agent-sdk/src/session-manager.ts
  • packages/cloud-agent-sdk/src/session-manager.test.ts

Prior finding resolved: re-arming now goes through armPendingInterruptExpiry() (packages/cloud-agent-sdk/src/session-manager.ts:1367), which clearTimeouts the tracked expiry first, so an earlier call's timer cannot end a newer call's suppression window.

Previous Review Summary (commit 7962573)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 7962573)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The pending-delivery gating and the storage deleteMessage cleanup are correct, but the new interrupt expiry timer can be orphaned by overlapping interrupt calls and later clear a newer guard.

Assumptions: [INFERENCE] Concurrent interrupt() calls are not reachable through the current Stop button, which disables on the synchronously written canInterruptAtom, so the timer finding depends on a programmatic or same-frame double invocation.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/cloud-agent-sdk/src/session-manager.ts 3301 Re-arming the expiry overwrites an existing pendingInterruptClearTimer handle without clearing it, so an untracked timer can fire later and clear a newer abort guard, surfacing a delayed Aborted into errorAtom.
Files Reviewed (6 files)
  • packages/cloud-agent-sdk/src/session-manager.ts - 1 issue
  • packages/cloud-agent-sdk/src/session-manager.test.ts - 0 issues
  • packages/cloud-agent-sdk/src/storage/memory.ts - 0 issues
  • packages/cloud-agent-sdk/src/storage/memory.test.ts - 0 issues
  • packages/cloud-agent-sdk/src/storage/jotai.ts - 0 issues
  • packages/cloud-agent-sdk/src/storage/jotai.test.ts - 0 issues

Reviewed by deepseek-flash-latest · Input: 34K · Output: 3.1K · Cached: 207.4K

Review guidance: REVIEW.md from base branch main

Re-arming the abort guard for an overlapping interrupt call now drops the
previous expiry timer, so its callback cannot end the newer call's window
early and let a delayed Aborted reach errorAtom.
@iscekic iscekic added the merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. label Oct 22, 2026
@iscekic
iscekic enabled auto-merge October 22, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drain Work item from the findings drain janitor:performance-reliability Janitor performance-reliability findings merge:bot Bot-owned review; merge when ready once readiness passes and a review approves.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants