Skip to content

fix: make CloudAppender shutdown durable - #2574

Open
Correctover wants to merge 3 commits into
MoonshotAI:mainfrom
Correctover:fix/cloud-appender-durable-shutdown-v2
Open

fix: make CloudAppender shutdown durable#2574
Correctover wants to merge 3 commits into
MoonshotAI:mainfrom
Correctover:fix/cloud-appender-durable-shutdown-v2

Conversation

@Correctover

Copy link
Copy Markdown

Make CloudAppender shutdown durable:

  • Serialize flush() to prevent concurrent buffer races (chained-promise lock)
  • Deadline-bounded shutdown() with AbortController that cancels in-flight sends
  • Hand unsent events to durable storage before completing shutdown
  • Replay v2 spool data for at-least-once delivery across ambiguous cancellation boundaries
  • Update CloudTransport to support AbortSignal for graceful cancellation
  • Update kap-server telemetry to use new shutdown API
  • Add comprehensive tests for shutdown scenarios

This replaces closed PR #2252. Fork main has been synced to upstream (071b6a5).

Original PR was closed because the fork was 48 commits behind upstream main.

…AI#2252)

- Serialize flush() to prevent concurrent buffer races
- Add deadline-bounded shutdown() with AbortController
- Hand unsent events to durable storage before completing
- Replay v2 spool data for at-least-once delivery
- Update CloudTransport to support AbortSignal
- Update kap-server telemetry to use new shutdown API
- Add comprehensive test coverage for shutdown scenarios

Fork main has been synced to upstream (071b6a5).
Original PR MoonshotAI#2252 was closed due to fork being 48 commits behind.
@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 59199bc

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/agent-core-v2 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

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

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

}

try {
await this.flush().catch(() => {});

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 Bound shutdown even with a flush already running

When shutdown overlaps a timer/threshold flush that already entered doFlush, that send was started before shutdownController existed, so it has no abort signal; this await this.flush() first waits for that old flushInFlight, and packages/kap-server/src/services/telemetry.ts now relies solely on appender.shutdown(deadlineMs) instead of its previous Promise.race. A wedged telemetry request can therefore keep server close blocked well past the 3s deadline; keep an outer timeout or make already-started sends abortable.

Useful? React with 👍 / 👎.

Comment on lines +254 to +255
client_version: bootstrap.clientVersion,
version: bootstrap.clientVersion,

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 Read the version from clientIdentity

In production/server initialization via createCloudAppender, bootstrap is the real IBootstrapService, whose host version lives at bootstrap.clientIdentity.version; clientVersion is only added by the updated unit-test stub. These lines therefore serialize undefined/omit the client version from every cloud telemetry payload, so use bootstrap.clientIdentity.version for both context fields.

Useful? React with 👍 / 👎.

});
});

describe('shutdown durability', () => {

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 Nest the shutdown durability tests in the suite

Because this new describe starts after the outer describe('CloudAppender') closes, the tests no longer share the let homeDir plus beforeEach/afterEach setup defined inside that suite. Each new case that passes homeDir will hit ReferenceError: homeDir is not defined when executed, so move this block back inside the outer suite or hoist the fixture setup.

Useful? React with 👍 / 👎.

Comment on lines +11 to +15
* Shutdown lifecycle:
* - `flush()` is guarded by a chained-promise lock so concurrent periodic /
* threshold / manual / shutdown triggers serialize without two waiters
* racing on a cleared `flushInFlight` flag.
* - `shutdown(deadlineMs?)` provides a deadline-bounded, idempotent close:

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 CloudAppender header to external role

For this agent-core-v2 file, the added shutdown lifecycle block documents the chained lock, AbortController deadline, spool replay, and delivery mechanics inside the module header. The scoped guide says top-of-file comments should state the external role and not narrate implementation steps, so this should move to code/tests or be removed to keep headers stable.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L15-L20

Useful? React with 👍 / 👎.

Correctover added 2 commits August 3, 2026 20:42
When shutdown() overlaps a timer/threshold flush that already entered
doFlush(), the in-flight send was started before shutdownController
existed and has no abort signal. A hung telemetry request could block
server close past the deadline.

Add Promise.race to ensure flush() is bounded by remaining deadline,
regardless of whether the in-flight send respects the abort signal.
- Use bootstrap.clientIdentity.version instead of bootstrap.clientVersion
  (IBootstrapService exposes clientIdentity, not clientVersion directly)
- Move 'shutdown durability' describe block inside outer CloudAppender
  suite so tests share homeDir fixture and beforeEach/afterEach setup
- Remove implementation-detail narrative from module header per AGENTS.md
  scoped guide (headers state external role, not internal steps)
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