Skip to content

feat(summarize): report failures to Sentry + configurable retries#1051

Open
rexplx wants to merge 16 commits into
rohitg00:mainfrom
rexplx:summarize-observability-retry
Open

feat(summarize): report failures to Sentry + configurable retries#1051
rexplx wants to merge 16 commits into
rohitg00:mainfrom
rexplx:summarize-observability-retry

Conversation

@rexplx

@rexplx rexplx commented Jul 12, 2026

Copy link
Copy Markdown

Summary

mem::summarize failures were only written to the structured logger, so they never surfaced in Sentry even when a DSN was configured — the engine imports no @sentry SDK. In a deployment with a lifetime ~20.7% summarize failure rate (empty/unparseable/schema-invalid LLM output, plus thrown provider errors), the error tracker showed nothing, making the failures effectively invisible.

This PR wires the four mem::summarize failure sites into optional Sentry reporting and makes the retry counts configurable.

Changes

1. Optional Sentry reporting (src/observability/sentry.ts, new)

  • Hard no-op unless SENTRY_DSN is set — builds, tests, and deploys without a DSN are completely unaffected.
  • initSentry() called once at worker startup in src/index.ts.
  • Reports all four failure paths in registerSummarizeFunction with tags (provider, mode, observationCount, latency):
    • empty_provider_response, parse_failed, validation_failedcaptureMessage (warning)
    • thrown provider error (catch block) → captureException
  • Reporting is wrapped so it can never throw back into the summarize path.

2. Configurable retries

  • Top-level produce-and-parse loop: hard 2SUMMARIZE_MAX_ATTEMPTS (default 3). A third attempt recovers a meaningful fraction of empty/parse failures from LLMs that intermittently return empty or wrapped output.
  • Chunk-level summarizeChunkWithRetry: hard 2SUMMARIZE_CHUNK_MAX_ATTEMPTS (default 3), kept as a separate knob because chunk retries multiply across every chunk and concurrency slot; ops can tune it down independently under provider throttling. The existing MAX_SKIP_RATIO bailout still guards against half-blind merges.

Why opt-in

@sentry/node is added as a dependency, but nothing initializes or calls Sentry unless SENTRY_DSN is present in the environment. Zero behavior change for existing users who don't set it.

Testing

  • tsc --noEmit: 0 errors in the changed files (summarize.ts, sentry.ts, index.ts).
  • npm run build (tsdown): passes; new env knobs (SUMMARIZE_MAX_ATTEMPTS, SUMMARIZE_CHUNK_MAX_ATTEMPTS) and initSentry/captureFailure confirmed present in the bundle.
  • Validated against a live backend: 64 real mem::summarize invocations across production sessions succeeded post-build with no regressions.

New env vars (all optional)

Var Default Effect
SENTRY_DSN unset Enables Sentry reporting of summarize failures
SUMMARIZE_MAX_ATTEMPTS 3 Top-level produce-and-parse retry count
SUMMARIZE_CHUNK_MAX_ATTEMPTS 3 Per-chunk retry count (chunked/large-session mode)

Summary by CodeRabbit

  • Reliability
    • Added configurable retry limits with exponential backoff + jitter for chunking and full-summary flows, including cached chunk partial reuse and shared deadline-based chunk skipping.
  • Bug Fixes
    • Standardized safe handling for provider non-OK HTTP responses and unexpected response shapes, with clearer structured failure/exception reporting.
  • Observability
    • Improved Sentry integration with DSN-gated enablement, best-effort flushing, non-throwing reporting, safer error sanitization, and health now reflecting Sentry availability.
  • Deployment / Documentation
    • Updated Fly Docker build to package a local tarball, refreshed Fly docs, and tightened Docker/Git ignore rules; added build config comments.
  • Tests
    • Expanded coverage for retry semantics, reporting paths, caching behavior, and deadline skip handling.

rexplx added 2 commits July 12, 2026 04:50
mem::summarize failures (empty_provider_response, parse_failed,
validation_failed, and thrown provider errors) were only written to the
structured logger, so they never surfaced in Sentry even when the SDK was
available. Wire all four failure sites into Sentry and make the top-level
produce-and-parse retry configurable (default 3, was a hard 2).

- New src/observability/sentry.ts: hard no-op unless SENTRY_DSN is set, so
  builds/tests/deploys without a DSN are unaffected (@sentry/node added).
- initSentry() called once at worker startup in src/index.ts.
- SUMMARIZE_MAX_ATTEMPTS env (default 3) controls the retry loop; a third
  attempt recovers a meaningful fraction of empty/parse failures.

Motivated by a lifetime ~20.7% summarize failure rate that was invisible in
Sentry because the engine imported no @sentry SDK.
summarizeChunkWithRetry used a hard 2 attempts. Bump to a configurable
default of 3 via SUMMARIZE_CHUNK_MAX_ATTEMPTS, kept as a separate knob from
SUMMARIZE_MAX_ATTEMPTS because chunk retries multiply across every chunk and
concurrency slot — large sessions under provider throttling can amplify load,
so ops can tune the chunk knob down independently of the cheap top-level retry.
The skip-ratio bailout (MAX_SKIP_RATIO) still guards against half-blind merges.
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

@rexplx is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds configurable chunk and top-level summarization retries with backoff and caching, optional Sentry lifecycle integration, safe provider error handling, and Fly deployment builds from a locally packed repository tarball.

Changes

Summarization and observability

Layer / File(s) Summary
Sentry integration and lifecycle wiring
package.json, src/observability/sentry.ts, src/index.ts, src/health/monitor.ts, src/types.ts, test/sentry.test.ts, test/health-thresholds.test.ts
Initializes optional Sentry reporting, captures bounded structured failures, flushes events during shutdown, exposes enablement in health snapshots, and tests these paths.
Configurable summarization retries
src/functions/summarize-retry.ts, src/functions/summarize.ts
Adds environment-controlled attempt limits, jittered backoff, chunk deadlines, cached partials, and retry reuse.
Summarization failure reporting
src/functions/summarize.ts, test/summarize.test.ts
Reports empty responses, parse failures, validation failures, and exceptions with structured context and latency fields.

Provider error safety

Layer / File(s) Summary
Shared and provider-specific safe errors
src/providers/_fetch.ts, src/providers/anthropic.ts, src/providers/minimax.ts, src/providers/openai.ts, src/providers/openrouter.ts
Logs detailed provider failures locally while exposing fixed HTTP, shape, and SDK error messages without forwarding response bodies.

Fly local package build

Layer / File(s) Summary
Multi-stage deployment build
.dockerignore, .gitignore, deploy/fly/Dockerfile, deploy/fly/fly.toml
Builds the checked-out source into an npm tarball, installs it in the runtime image, and uses the Fly-specific lockfile and Dockerfile path.
Deployment instructions
deploy/fly/README.md
Documents repository-root Fly commands, local packaging behavior, version overrides, and lockfile regeneration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Worker
  participant Summarizer
  participant Provider
  participant Sentry
  Worker->>Summarizer: start configured retry loop
  Summarizer->>Provider: request chunk or final summary
  Provider-->>Summarizer: response or safe error
  Summarizer->>Summarizer: back off, reuse cached chunks, or classify failure
  Summarizer->>Sentry: capture structured failure or exception
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main changes: Sentry failure reporting and configurable summarize retries.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/observability/sentry.ts (1)

15-35: 🩺 Stability & Availability | 🔵 Trivial

Consider exporting a flushSentry() helper for graceful shutdown.

The shutdown handler in src/index.ts calls process.exit(0) without flushing Sentry's event queue. Events captured just before shutdown (e.g., a captureSummarizeException during the final invocation) could be lost. Export a flushSentry(timeout) wrapper from this module and call it in the shutdown path before process.exit.

📋 Suggested addition
/** Flush pending Sentry events. No-op unless Sentry is enabled. */
export async function flushSentry(timeoutMs = 2000): Promise<void> {
  if (!enabled) return;
  try {
    await Sentry.flush(timeoutMs);
  } catch {
    /* swallow */
  }
}

Then in the shutdown handler in src/index.ts:

   await sdk.shutdown();
+  await flushSentry();
   clearWorkerPidfile();
   process.exit(0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/observability/sentry.ts` around lines 15 - 35, Export a
flushSentry(timeoutMs) helper alongside initSentry that returns immediately when
Sentry is disabled, awaits Sentry.flush with the provided timeout, and swallows
flush errors. Update the shutdown handler in the process lifecycle code to await
flushSentry before calling process.exit(0).
src/functions/summarize.ts (1)

309-319: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale comment references "2-attempt loop".

The comment on lines 309-313 says "the same 2-attempt loop" but the code now uses getMaxAttempts() (default 3, configurable via SUMMARIZE_MAX_ATTEMPTS). This could mislead developers into thinking the retry count is hardcoded.

📝 Update comment to reflect configurable retries
-        // `#783`: chunk-level produceSummaryXml retries internally, but
-        // the final merge used to parse once and bail. Wrap the
-        // produce-and-parse pair in the same 2-attempt loop so a
-        // markdown-wrapped or otherwise wrapped response gets a
-        // second roll-of-the-dice instead of dropping the summary.
+        // `#783`: chunk-level produceSummaryXml retries internally, but
+        // the final merge used to parse once and bail. Wrap the
+        // produce-and-parse pair in a configurable retry loop
+        // (SUMMARIZE_MAX_ATTEMPTS, default 3) so a markdown-wrapped
+        // or otherwise wrapped response gets another roll-of-the-dice
+        // instead of dropping the summary.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/summarize.ts` around lines 309 - 319, The comment above the
retry loop in the summarize flow references a fixed “2-attempt loop,” but the
loop uses configurable getMaxAttempts(). Update that comment to describe the
shared/configurable retry behavior without stating a hardcoded attempt count;
leave the implementation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/functions/summarize.ts`:
- Around line 454-459: Update the context object passed to
captureSummarizeException in the summarize catch path to include the enclosing
mode and chunks values, alongside sessionId, provider, observationCount, and
latencyMs.

In `@src/observability/sentry.ts`:
- Line 28: In the Sentry initialization flow, extract the resolved environment
value used by the Sentry configuration into a variable and reuse that variable
in the “Sentry initialized” logger.info call. Ensure the log matches the
existing NODE_ENV, production fallback, and FLY_APP_NAME resolution exactly.

---

Nitpick comments:
In `@src/functions/summarize.ts`:
- Around line 309-319: The comment above the retry loop in the summarize flow
references a fixed “2-attempt loop,” but the loop uses configurable
getMaxAttempts(). Update that comment to describe the shared/configurable retry
behavior without stating a hardcoded attempt count; leave the implementation
unchanged.

In `@src/observability/sentry.ts`:
- Around line 15-35: Export a flushSentry(timeoutMs) helper alongside initSentry
that returns immediately when Sentry is disabled, awaits Sentry.flush with the
provided timeout, and swallows flush errors. Update the shutdown handler in the
process lifecycle code to await flushSentry before calling process.exit(0).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56c7e477-b5bf-4eae-9607-14d7c9629c94

📥 Commits

Reviewing files that changed from the base of the PR and between 93ae9bc and d88b7c2.

📒 Files selected for processing (4)
  • package.json
  • src/functions/summarize.ts
  • src/index.ts
  • src/observability/sentry.ts

Comment thread src/functions/summarize.ts
release: process.env.AGENTMEMORY_COMMIT_SHA || undefined,
});
enabled = true;
logger.info("Sentry initialized", { environment: process.env.FLY_APP_NAME });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Logged environment may not match the Sentry environment.

Line 28 logs process.env.FLY_APP_NAME as the environment, but the actual value sent to Sentry (line 23-24) could be NODE_ENV or "production" when FLY_APP_NAME is unset. This mismatch will cause confusion when correlating logs with Sentry environments.

🔧 Fix: extract environment to a variable and reuse
 export function initSentry(): void {
   const dsn = process.env.SENTRY_DSN;
   if (!dsn) return;
   try {
+    const environment =
+      process.env.FLY_APP_NAME || process.env.NODE_ENV || "production";
     Sentry.init({
       dsn,
       tracesSampleRate: 0,
-      environment:
-        process.env.FLY_APP_NAME || process.env.NODE_ENV || "production",
+      environment,
       release: process.env.AGENTMEMORY_COMMIT_SHA || undefined,
     });
     enabled = true;
-    logger.info("Sentry initialized", { environment: process.env.FLY_APP_NAME });
+    logger.info("Sentry initialized", { environment });
   } catch (err) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.info("Sentry initialized", { environment: process.env.FLY_APP_NAME });
export function initSentry(): void {
const dsn = process.env.SENTRY_DSN;
if (!dsn) return;
try {
const environment =
process.env.FLY_APP_NAME || process.env.NODE_ENV || "production";
Sentry.init({
dsn,
tracesSampleRate: 0,
environment,
release: process.env.AGENTMEMORY_COMMIT_SHA || undefined,
});
enabled = true;
logger.info("Sentry initialized", { environment });
} catch (err) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/observability/sentry.ts` at line 28, In the Sentry initialization flow,
extract the resolved environment value used by the Sentry configuration into a
variable and reuse that variable in the “Sentry initialized” logger.info call.
Ensure the log matches the existing NODE_ENV, production fallback, and
FLY_APP_NAME resolution exactly.

rexplx added 8 commits July 13, 2026 09:00
Error messages from openai/openrouter/minimax/anthropic could embed raw
HTTP response bodies or JSON-stringified provider responses, which then
reached Sentry.captureException verbatim once error reporting was wired
up. Log the full body/response locally only; throw fixed, content-free
messages carrying just the status code. Extracted the shared pattern
(openai/openrouter/minimax) into throwSafeHttpError/throwSafeShapeError
in _fetch.ts. anthropic.ts uses the official SDK, whose APIError also
embeds the raw body in .message -- wrapped every SDK call site in a new
callSafely() helper that sanitizes before rethrowing.
… modes

- initSentry() now checks Sentry.isInitialized() after Sentry.init() so a
  malformed SENTRY_DSN that silently no-ops doesn't get logged as a false
  "initialized" success.
- Added flushSentry() so buffered events get a bounded chance to flush on
  shutdown instead of being dropped on process.exit.
- captureFailure/captureException's internal try/catch now logs via the
  local logger when it swallows an SDK-level error, instead of silently
  swallowing with no trace.
- captureException runs every error through a new toSafeError() that caps
  the forwarded message length as defense-in-depth (root-fix for the raw
  content is in the provider files, a separate commit on this branch).
…e failure classification

- Both retry loops (chunk-level and top-level) now back off with jitter
  between attempts instead of retrying immediately.
- SUMMARIZE_MAX_ATTEMPTS/SUMMARIZE_CHUNK_MAX_ATTEMPTS are clamped to a
  shared MAX_ATTEMPTS_CEILING=5 via a new getEnvInt() helper, replacing 4
  near-duplicate env-parsing functions.
- A new ChunkPartialCache persists resolved chunk summaries across
  top-level retry attempts, so a retry only re-runs the failed
  reduce/merge step instead of re-summarizing every already-succeeded
  chunk from scratch.
- A chunk map-phase soft deadline (CHUNK_MAP_PHASE_DEADLINE_MS) stops
  starting new chunk batches once the map phase risks exceeding iii's
  180s invocation timeout, turning a silent hard-timeout into a graceful
  partial skip subject to the existing MAX_SKIP_RATIO bailout. The
  deadline is a single absolute timestamp stored on ChunkPartialCache and
  shared across every top-level attempt -- NOT recomputed fresh per
  attempt, which would have let repeated retries each burn close to the
  full budget and blow past the 180s ceiling in aggregate.
- The empty_provider_response/parse_failed Sentry classification now
  tracks outcomes across the whole retry loop (sawEmptyResponse/
  sawParseFailure), not just the last attempt's state, so a real parse
  failure earlier in the loop isn't masked by a later empty response.
- validation_failed now forwards only Zod error field paths, not the
  free-text messages, so a future schema change can't start leaking real
  values through this capture site unnoticed.
- Added test/sentry.test.ts and a vi.mock for sentry.js in
  test/summarize.test.ts (previously real Sentry calls were possible if
  SENTRY_DSN were ever set in the test environment), plus new coverage
  for the env-var ceiling, mixed-failure classification, chunk-cache
  reuse, and the map-phase deadline. Also fixed two pre-existing test
  fixtures that assumed a hardcoded 2-attempt chunk retry count after an
  earlier commit on this branch bumped the default to 3 with no test
  update.
Also don't let a failed sdk.shutdown() prevent flushSentry() from
running -- buffered events from the same shutdown sequence would
otherwise be silently dropped.
…arball

The Dockerfile previously installed either the published npm package (no
way to ship a fork patch) or a manually-built, untracked tarball (no
build-time check that it matched the checked-out source -- a fresh
checkout with no local tarball would break, and a source change with no
tarball rebuild would silently ship stale code).

Replaced with a multi-stage build: a builder stage runs npm install +
npm run build + npm pack against the checked-out source, and the final
stage installs from that stage's output tarball. Build context moves
from deploy/fly/ to the repo root (documented in fly.toml and the
deploy README's fly deploy invocation) so the builder stage can see the
source tree. Uses npm install rather than npm ci in the builder stage
since this repo intentionally gitignores its lockfile. Added a root
.dockerignore to keep the larger build context from uploading
node_modules/docs/benchmark/etc. on every deploy.
…npm ci

Addresses a P2 finding from this branch's own code review: the builder
stage's npm install (no lockfile) meant two builds of the same commit
could resolve different transitive dependency versions, with the
locally-packed tarball as the sole artifact shipped to production.

deploy/fly/package-lock.json is scoped ONLY to the Fly build context --
generated via npm install --package-lock-only against the current
package.json and exempted from the repo-wide lockfile gitignore rule via
a single !deploy/fly/package-lock.json negation. Local dev workflow and
the root package-lock.json policy are untouched.
…e-retry.ts

summarize.ts had grown to mix chunk splitting, XML parsing, function
registration, AND all the retry/backoff/env-config/cache machinery in
one file. Split the latter (getEnvInt and its 4 callers, sleep/
backoffDelayMs, MAX_ATTEMPTS_CEILING, ChunkPartialCache, the map-phase
deadline constant) into a new summarize-retry.ts module. No behavior
change -- pure extraction, re-exported and imported back in.
Previously initSentry()'s isInitialized() check (added this branch) was
only ever visible via boot logs -- no way for a deploy-verification
script or monitoring agent to confirm error reporting is actually live
without tailing stdout at process start. Added isSentryEnabled() to
observability/sentry.ts and wired it into HealthSnapshot.sentryEnabled,
already exposed via the existing GET /agentmemory/health endpoint.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/functions/summarize-retry.ts (1)

1-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Several comments explain WHAT rather than WHY.

Per coding guidelines, TypeScript source should avoid comments explaining WHAT and rely on clear naming instead. The module-level comment (lines 1-5), the getEnvInt description (lines 36-40), and the backoffDelayMs parameter explanation (lines 96-98) describe what the code does rather than why it exists. The WHY comments (e.g., deadline rationale at lines 104-115, cache design at lines 117-127) are valuable and should be retained.

As per coding guidelines: "In TypeScript source code, avoid code comments explaining WHAT — use clear naming instead."

Also applies to: 36-40, 96-98

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/summarize-retry.ts` around lines 1 - 5, Remove the WHAT-only
comments in the module header, the getEnvInt description, and the backoffDelayMs
parameter explanation. Retain the WHY-focused comments covering deadline
rationale and cache design, leaving the implementation unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/functions/summarize-retry.ts`:
- Around line 1-5: Remove the WHAT-only comments in the module header, the
getEnvInt description, and the backoffDelayMs parameter explanation. Retain the
WHY-focused comments covering deadline rationale and cache design, leaving the
implementation unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b88d4c87-a6f0-4754-8060-cc0e68e22421

📥 Commits

Reviewing files that changed from the base of the PR and between 8982ee1 and b984ed5.

📒 Files selected for processing (7)
  • src/functions/summarize-retry.ts
  • src/functions/summarize.ts
  • src/health/monitor.ts
  • src/observability/sentry.ts
  • src/types.ts
  • test/health-thresholds.test.ts
  • test/sentry.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/observability/sentry.ts
  • test/sentry.test.ts
  • src/functions/summarize.ts

rexplx added 2 commits July 13, 2026 14:32
flyctl resolves [build].dockerfile relative to the directory containing
the config file (deploy/fly/), not the build context root. The prior
value "deploy/fly/Dockerfile" doubled up to
deploy/fly/deploy/fly/Dockerfile at build time, breaking every deploy.
Verified via fly deploy --build-only against a throwaway app (destroyed
after validation, zero cost): fails with the old value, succeeds with
the corrected "Dockerfile".
- Fix a real deploy-blocking bug: $VOLUME was derived from $APP
  (agentmemory_<user>_data) but fly.toml hardcodes the mount source to
  agentmemory_data. Any reader using a non-'agentmemory' app name (which
  the doc itself tells them to do, since 'agentmemory' is taken) would
  create a volume that never attaches to the app. VOLUME is now a fixed
  literal matching fly.toml.
- Drop the --dockerfile CLI flag from both fly launch/deploy examples;
  verified empirically this flyctl version ignores it entirely and only
  reads fly.toml's own [build].dockerfile field.
- Correct the image size claim (~114 MB -> ~124 MB, measured this
  session) and the 512MB shared-cpu-1x monthly cost ($1.94 -> $3.69,
  per Fly's current pricing page).
- Add the missing 'fly machine list' lookup step before both
  <machine-id> placeholders (rotate + restore sections), which were
  previously unresolvable dead ends.
- Minor: cold-start/grace-period ratio is ~3.3x, not exactly 3x.

Found via an independent two-reviewer pass (coherence + feasibility
lenses) cross-checked against the actual fly.toml/Dockerfile/entrypoint.sh.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deploy/fly/README.md`:
- Around line 151-153: Update the active Fly compute estimate in the pricing
section of README.md from $3.69/month to about $3.19/month, while preserving the
existing 24/7, shared-cpu-1x, 512 MB, and auto_stop_machines context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da83804b-1e21-45e8-b000-2e3f9dc03c5f

📥 Commits

Reviewing files that changed from the base of the PR and between b984ed5 and ac8d73e.

📒 Files selected for processing (2)
  • deploy/fly/README.md
  • deploy/fly/fly.toml

Comment thread deploy/fly/README.md
Comment on lines 151 to +153
- Active (machine running on `shared-cpu-1x` with 512 MB): about
$1.94/month if it ran 24/7; in practice `auto_stop_machines` keeps
that well under $1.
$3.69/month if it ran 24/7 (per Fly's current pricing page); in
practice `auto_stop_machines` keeps that well under $1.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant section of the README
sed -n '130,170p' deploy/fly/README.md

# Look for the region and pricing references nearby
rg -n "iad|shared-cpu-1x|3\\.69|3\\.32|pricing" deploy/fly/README.md

Repository: rohitg00/agentmemory

Length of output: 2023


🌐 Web query:

Fly pricing shared-cpu-1x 512 MB iad monthly current pricing

💡 Result:

As of July 13, 2026, the monthly cost for a shared-cpu-1x machine with 512 MB of RAM on Fly.io is $3.19 [1][2][3]. This pricing is based on the resource usage rate of $0.0044 per hour [1]. Fly.io billing for compute resources is consistent across its regions, including iad (Northern Virginia), and is calculated based on the specific machine configuration [1][4]. Please note that while individual resource usage is billed at these rates, your account may also be subject to the costs associated with the specific Fly.io plan you are subscribed to (e.g., Hobby or Launch plans), which may have separate monthly base fees [3].

Citations:


Update the Fly compute estimate. The current shared-cpu-1x 512 MB rate is about $3.19/month, and Fly compute pricing is region-independent, so the $3.69/month figure is outdated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/fly/README.md` around lines 151 - 153, Update the active Fly compute
estimate in the pricing section of README.md from $3.69/month to about
$3.19/month, while preserving the existing 24/7, shared-cpu-1x, 512 MB, and
auto_stop_machines context.

Source: MCP tools

rexplx added 4 commits July 13, 2026 15:21
The builder stage never copied tsdown.config.ts, so tsdown silently
fell back to a single-entry build (src/index.ts only) instead of the
config's 3-target + 13-hook multi-entry build. The packed tarball's
dist/ was missing cli.mjs entirely, so package.json's bin field
(agentmemory -> dist/cli.mjs) pointed at a file that never existed.
npm therefore skipped creating the node_modules/.bin/agentmemory
symlink, and the container crash-looped on every boot with
'exec: "agentmemory": executable file not found in $PATH'
(exit code 1, immediately hit Fly's 10-restart cap).

Caught by running the full deploy runbook end-to-end (fly launch +
volume create + real fly deploy, not just --build-only) against a
throwaway app -- the build-only dry run can't catch this since tsdown
exits 0 either way, it just silently builds less than intended.
Verified fixed: redeployed, /agentmemory/livez returned 200 with
{"status":"ok"}, machine health check passing. Test app + volume
destroyed after verification.
Sentry.init() replaces the global client on every call rather than
merging options. Production runs with NODE_OPTIONS='--require
/opt/agentmemory/sentry-preload.cjs', an out-of-band preload script
that already calls Sentry.init() with its own tracesSampleRate and
release before this module ever runs. Calling init() again here
(as this module unconditionally did) would silently wipe that
configuration -- verified empirically: a second init() call creates
a brand-new client instance and drops the first call's
tracesSampleRate/release entirely.

initSentry() now checks Sentry.isInitialized() before calling init(),
and reuses an already-initialized client (just flips the local enabled
flag) instead of re-initializing over it. Added a test for this exact
scenario; updated four existing tests whose isInitialized() mock
modeled only the post-init state to also model the pre-init check
this adds.
Production's NODE_OPTIONS references
'--require /opt/agentmemory/sentry-preload.cjs' for broad crash
reporting that runs before this package's own initSentry() gets a
chance to. That file existed only in the currently-deployed production
image via some process outside this git repo -- not in main, not in
this branch, nowhere tracked. Deploying this fork's rebuilt image
without it caused a real production outage (~7 min, ryclar-agentmemory):
the process crashed on every boot with 'Cannot find module
.../sentry-preload.cjs', hit Fly's restart cap, and went fully down.
Rolled back to the prior image immediately to restore service; no data
was lost (deploy only replaces the app image, never touches /data).

Root-cause fix: the preload script is now a tracked file
(deploy/fly/sentry-preload.cjs, content matches what was already
running in production) baked into the image via the Dockerfile, so
this fork's own build is self-contained and reproducible instead of
depending on an undocumented prior manual patch. Verified in a fresh
throwaway app with NODE_OPTIONS set to the same --require path:
'Sentry disabled: SENTRY_DSN not set' logs cleanly (no crash), engine
boots, health check passes. Composes correctly with the double-init
fix in the previous commit (initSentry() reuses the client this
preload creates instead of re-initializing over it).
The endpoint accepted `limit` but never read it: every request did an
unconditional full kv.list() and then hydrated a summary for every session
in the store. At ~10k sessions that is a ~13MB response, which is what
breaks the handoff/session-history skills -- they ask for 20 and get all of
them. The MCP proxy has always sent limit=20 (src/mcp/standalone.ts:149),
so this was purely a server-side omission; the Cloudflare gateway had
already added client-side defensive truncation to work around it.

Two independent paths served memory_sessions and both ignored limit:
  - api::sessions (REST; reached via standalone.ts and the gateway)
  - server.ts's in-process MCP (reached via POST /agentmemory/mcp/call)
Fixing only the first would have left the connector path unbounded.

Absent `limit` still returns every session, deliberately.
scripts/agentmemory-import-with-skip.py builds its already-imported
skip-set from a single unbounded call and implements no pagination loop by
design; a default cap would silently shrink that set and re-import the
remainder as duplicates. Explicit limits are honored as given rather than
clamped, so summarize-backfill.py's bulk fetch keeps working.

Ordering is load-bearing: sort newest-first BEFORE slicing, or a limit
returns an arbitrary N instead of the recent sessions callers expect. Page
before hydrating summaries to drop the N+1 (one kv.get per returned
session, not one per session in the store). Response now carries the
unpaged `total` so a bounded reply is not mistaken for the whole store.

memory_sessions' inputSchema was `properties: {}`, which advertises a
tool that takes no arguments -- limit was undiscoverable to MCP clients.

Tests assert against each handler body rather than the whole file: a
file-wide regex for `limit` also matches api::commits and would pass
vacuously. All 7 fail on the pre-fix source.
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