Skip to content

feat: add scoped logger context via AsyncLocalStorage#3054

Merged
amikofalvy merged 8 commits intomainfrom
feat/logger-scoped-context
Apr 7, 2026
Merged

feat: add scoped logger context via AsyncLocalStorage#3054
amikofalvy merged 8 commits intomainfrom
feat/logger-scoped-context

Conversation

@amikofalvy
Copy link
Copy Markdown
Collaborator

Summary

  • Add runWithLogContext(bindings, fn) and PinoLogger.with(bindings) to @inkeep/agents-core for automatic request-scoped log context propagation via AsyncLocalStorage
  • Add Hono middleware in createApp.ts that sets tenantId/projectId/agentId for all /run/* and /manage/tenants/* routes — every downstream log call automatically includes these fields
  • Convert 3 high-impact files as proof-of-concept: AgentSession (class member pattern), TriggerService (runWithLogContext at function entry), github.ts routes (runWithLogContext per handler)

How it works

  • AsyncLocalStorage<PinoLoggerInstance> stores pino child loggers with pre-serialized bindings
  • Module-scope const logger = getLogger('X') works unchanged across ~177 files — PinoLogger methods resolve the active ALS instance at call time via a WeakMap-cached proxy (~10ns overhead)
  • .with() uses snapshot semantics: captures current ALS context at call time, does not continue proxying
  • recreateInstance() clears WeakMap cache to ensure transport/option changes propagate
  • Nested runWithLogContext calls compose: inner scope inherits parent bindings + adds new ones

Test plan

  • 202-line unit test suite for ALS proxy, nesting, fallback, .with() snapshot semantics
  • Test mocks updated in agents-api and agents-work-apps setup files
  • pnpm typecheck passes (16/16 tasks)
  • agents-api tests: 2,237 passed
  • agents-core tests: 2,099 passed
  • Review the 3 converted files for correctness of field removal

Follow-up (PR 2)

Broad sweep of remaining ~220 files to adopt the pattern — mechanical but large, deferred intentionally.

📋 Spec: specs/2026-04-06-logger-scoped-context/SPEC.md

🤖 Generated with Claude Code

amikofalvy and others added 2 commits April 7, 2026 00:23
Add runWithLogContext() and PinoLogger.with() for automatic context
propagation. Middleware in createApp.ts sets tenantId/projectId/agentId
for run and manage routes. Convert AgentSession, TriggerService, and
github routes as proof-of-concept adoption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 7, 2026

🦋 Changeset detected

Latest commit: e299804

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

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-api Patch
@inkeep/agents-core Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-cli Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp 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

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Apr 7, 2026 10:18pm
agents-manage-ui Ready Ready Preview, Comment Apr 7, 2026 10:18pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
agents-docs Skipped Skipped Apr 7, 2026 10:18pm

Request Review

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 7, 2026

TL;DR — Introduces AsyncLocalStorage-backed scoped logger context so that fields like tenantId, projectId, and agentId are automatically attached to every log line within a request scope, eliminating the need to pass them manually at each call site.

Key changes

  • Add runWithLogContext and resolveInstance to PinoLogger — Core logger gains an AsyncLocalStorage-based context store; each log method resolves the current ALS scope (with a WeakMap cache) to auto-inject bindings. A new .with() method creates snapshot loggers that freeze context at call time.
  • Add string-only overloads to PinoLogger log methodserror, warn, info, and debug now accept a single string argument alongside the existing (data, message) form, enabling logger.info('message') instead of logger.info({}, 'message'). Converts ~40 empty-object call sites to the cleaner form.
  • Add log-context middleware in createApp.ts — Two Hono middleware layers (/run/* and /manage/tenants/*) seed the ALS context with tenantId, projectId, and agentId extracted from the request.
  • Remove manual tenantId/sessionId from log call sitesAgentSession switches to a this.logger instance (via .with({ sessionId })), and GitHub manage routes wrap handler bodies in runWithLogContext({ installationId }), stripping redundant fields from every log call.
  • Wrap processWebhook and dispatchExecution in runWithLogContextTriggerService scopes triggerId, invocationId, and conversationId into the logger context so all downstream log calls inherit them automatically.
  • Add comprehensive logger unit tests — 10 new tests covering ALS scoping, nested contexts, .with() snapshot semantics, WeakMap caching, async propagation, and level delegation.
  • Update test mocks and changesets — All test setup files add .with() and runWithLogContext stubs; two changesets cover agents-core (patch) and agents-api (patch).

Summary | 24 files | 8 commits | base: mainfeat/logger-scoped-context


AsyncLocalStorage-based scoped logger context

Before: Every log call manually included { tenantId, sessionId, installationId, ... } as the first argument — repetitive, error-prone, and easy to forget.
After: runWithLogContext(bindings, fn) seeds an AsyncLocalStorage scope; all PinoLogger methods automatically resolve the current scope and inject the bindings. Call sites pass only event-specific data.

The implementation lives in PinoLogger.resolveInstance(), which checks the ALS store and uses a WeakMap<PinoLoggerInstance, PinoLoggerInstance> cache keyed on the current ALS pino instance to avoid creating duplicate children on every log call. A module-level basePinoInstance is set whenever a non-snapshot PinoLogger is constructed, ensuring runWithLogContext always has a valid parent even when called outside an existing scope.

How does .with() differ from the ALS proxy?

.with(bindings) creates a snapshot logger: it resolves the current ALS context at call time, creates a pino child with the extra bindings baked in, and marks the resulting PinoLogger with isSnapshot: true. A snapshot logger always uses its own fixed pinoInstance — it does not re-resolve ALS on subsequent calls. This is useful for class-level loggers (e.g. AgentSession) that want to capture sessionId once in the constructor. A .child() alias is provided for backward compatibility.

packages/agents-core/src/utils/logger.ts · agents-api/src/logger.ts · packages/agents-core/src/utils/__tests__/logger.test.ts


String-only overloads for log methods

Before: Every log call required a data object as the first argument — calls with no structured data passed an empty {} (e.g. logger.info({}, 'message')).
After: Each log level method (error, warn, info, debug) accepts a single string argument directly, matching the native pino API. ~40 ({}, 'message') call sites across AgentSession, TriggerService, and github.ts are cleaned up.

packages/agents-core/src/utils/logger.ts · agents-api/src/domains/run/session/AgentSession.ts · agents-api/src/domains/run/services/TriggerService.ts · agents-api/src/domains/manage/routes/github.ts


Request-scoped middleware in createApp.ts

Before: No automatic context propagation — each handler had to extract and pass IDs to every logger call.
After: Two middleware layers run early in the Hono stack and call runWithLogContext with IDs pulled from the execution context or request params.

Middleware path Bindings seeded
/run/* tenantId, projectId, agentId (from executionContext)
/manage/tenants/* tenantId, projectId (from Hono context / route param)

agents-api/src/createApp.ts


Adoption in AgentSession, GitHub routes, and TriggerService

Before: ~50 log calls across these files manually threaded sessionId, tenantId, installationId, etc.
After: AgentSession uses this.logger = getLogger('AgentSession').with({ sessionId }) in the constructor; GitHub routes wrap per-installation handlers in runWithLogContext({ installationId }); TriggerService wraps processWebhook and dispatchExecution to scope triggerId / invocationId / conversationId.

The net effect is significantly less boilerplate at call sites — log statements pass only event-specific data (e.g. { repositoryCount }) while identity/scope fields come from context automatically.

agents-api/src/domains/run/session/AgentSession.ts · agents-api/src/domains/manage/routes/github.ts · agents-api/src/domains/run/services/TriggerService.ts

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Solid foundation for automatic log context propagation. The ALS + WeakMap caching approach is well-designed, the snapshot semantics on .with() are correct, and the test coverage for the core primitives is thorough. Three issues worth addressing before merge — one medium (type-safety of the middleware context lookup), one medium (dead code), and a few minor items in the proof-of-concept conversions.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread agents-api/src/createApp.ts
Comment on lines +36 to +41
export function getLogContext(): Record<string, unknown> {
const store = loggerStorage.getStore();
if (!store) return {};
const raw = (store as any).bindings?.() ?? {};
return raw;
}
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.

Medium: getLogContext() is exported but has zero callers.

This function uses pino's internal .bindings() API via (store as any).bindings?.(), which is undocumented and could break across pino major versions. If this is intentional future API, add a test for it. If it's speculative, remove it — it can always be added later when there's a concrete use case.

Comment on lines +82 to +84
if (!this.isSnapshot) {
setBasePinoInstance(this.pinoInstance);
}
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.

Minor: setBasePinoInstance called from fromInstance path may be surprising.

When a PinoLogger is constructed with fromInstance and snapshot: false (the default for non-.with() callers), it overwrites the global basePinoInstance. This means any code that creates a PinoLogger via fromInstance without realizing the side effect will silently change the ALS fallback for all loggers.

Currently only the LoggerFactory and tests use fromInstance, but this is an implicit global mutation that could bite future callers. Consider documenting this in PinoLoggerConfig.fromInstance's JSDoc.

Comment thread agents-api/src/createApp.ts
Comment on lines +646 to +647

const execStartedAt = Date.now();
const dispatchDelayMs = dispatchedAt ? execStartedAt - dispatchedAt : undefined;
return runWithLogContext({ triggerId, invocationId, conversationId }, async () => {
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.

Minor: redundant nested runWithLogContext in executeAgentAsync.

dispatchExecution (line 528) already calls runWithLogContext({ triggerId, invocationId, conversationId }, ...) around the call to executeAgentAsync. Then executeAgentAsync itself wraps its body in another runWithLogContext({ triggerId, invocationId, conversationId }, ...) with the exact same bindings.

The nesting composes correctly (inner inherits parent), but the duplication is pointless — the inner scope re-binds the same keys with the same values. Since executeAgentAsync is also exported and called independently (e.g. from eval workflows), keeping its own scope is fine, but you could at least drop triggerId from the inner call since the outer already binds it. Alternatively, document this is intentional for standalone-call safety.

};
return {
getLogger: vi.fn(() => mockLogger),
runWithLogContext: vi.fn((_bindings: any, fn: any) => fn()),
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.

Nit: runWithLogContext mock is correct but withRequestContext mock (line 27) appears to be a leftover. No code in the PR imports withRequestContext from ../logger. If it existed before and was removed, the mock is harmless but dead. If it's needed for other tests not in this PR, ignore.

{ sessionId, messageId, agentId: executionContext.agentId },
'AgentSession created'
);
this.logger = getLogger('AgentSession').with({ sessionId });
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.

Good pattern. Using .with({ sessionId }) to create a session-scoped logger instance that automatically includes sessionId in every log line is the right approach — eliminates the repetitive { sessionId: this.sessionId } spread across ~30 call sites.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(6) Total Issues | Risk: Medium

🟠⚠️ Major (2) 🟠⚠️

🟠 1) logger.ts:136-163 recreateInstance() doesn't update basePinoInstance

Issue: When recreateInstance() is called after adding transports or updating options, it creates a new pino instance but does not call setBasePinoInstance(). This means the module-level basePinoInstance used by runWithLogContext as the default parent remains stale.

Why: Creates a split-brain scenario where logs from new ALS scopes (via runWithLogContext) use the old instance, while direct logger method calls use the new instance with updated transports. If a team adds a new transport for observability, ALS-scoped logs silently won't reach it.

Fix: Add setBasePinoInstance(this.pinoInstance) at the end of recreateInstance().

Refs:


🟠 2) TriggerService.ts:566-583 ALS context lost in .catch() handler

Issue: The error handler attached via .catch() to executionPromise runs after the runWithLogContext scope has exited. The ALS store is empty when this code executes, so logger.error falls back to the base logger without tenant/project/agent/trigger/invocation context.

Why: Production incident investigation becomes significantly harder when error logs from background trigger failures can't be correlated with the originating request.

Fix: Capture bindings before the .catch() and wrap the error handler in runWithLogContext.

Refs:

Inline Comments:

  • 🟠 Major: logger.ts:163 recreateInstance doesn't update basePinoInstance
  • 🟠 Major: TriggerService.ts:569 ALS context lost in catch handler

🟡 Minor (3) 🟡

🟡 1) setup.ts (agents-work-apps) Missing runWithLogContext mock export

Issue: The test setup mocks the logger module but doesn't export runWithLogContext, while agents-api's setup.ts does.

Why: If agents-work-apps code adopts the runWithLogContext pattern, tests will fail with undefined function error.

Fix: Add runWithLogContext: vi.fn((_bindings: any, fn: any) => fn()) to both mock blocks.

Refs:


🟡 2) logger.test.ts No tests for async function behavior with runWithLogContext

Issue: All current tests use synchronous callbacks, but production code wraps async functions. ALS context propagation across await boundaries is untested.

Why: If context is lost after an await (which shouldn't happen with proper ALS usage, but is a critical invariant), logs would silently lose scoped fields in production.

Fix: Add a test like:

it('preserves context across await boundaries', async () => {
  await runWithLogContext({ tenantId: 't1' }, async () => {
    logger.info({}, 'before');
    await Promise.resolve();
    logger.info({}, 'after await');
  });
  // verify both logs have tenantId
});

🟡 3) logger.test.ts No tests for error propagation in runWithLogContext

Issue: No tests verify that thrown errors (sync) or rejected promises (async) correctly bubble up from within runWithLogContext.

Why: If errors are swallowed or transformed, callers won't receive expected exceptions. TriggerService relies on errors propagating for status updates.

Fix: Add tests for both sync throws and async rejections, verifying the exact error instance propagates.

Inline Comments:

  • 🟡 Minor: setup.ts:26 Missing runWithLogContext mock (first block)
  • 🟡 Minor: setup.ts:48 Missing runWithLogContext mock (second block)

💭 Consider (1) 💭

💭 1) AgentSession.ts:238 .with() snapshot semantics may miss runtime context updates

Issue: The session logger is created via .with({ sessionId }) at construction time, using snapshot semantics. If the AgentSession is constructed before middleware sets ALS context, or if context changes during the session's lifecycle, logs will have stale/missing context.

Why: This is the intended design (frozen context at construction), but differs from module-scope loggers that continue proxying ALS. For long-running sessions, this is a subtle contract.

Fix: Document that AgentSession must be constructed within a runWithLogContext scope, or consider whether session-level loggers need dynamic context.


Discarded (5)
Location Issue Reason Discarded
logger.ts:43 Module-level mutable basePinoInstance shared across requests By design — only one logger config per process is expected. Racing writes are an edge case.
logger.ts:36-41 getLogContext() uses undocumented pino internals Function is exported but not used in converted files. Low risk.
logger.ts:71 WeakMap cache per PinoLogger creates unbounded memory growth WeakMap allows GC when ALS instance is unreachable. Memory is bounded by concurrent requests × modules.
createApp.ts:273 Middleware depends on upstream setting executionContext Correct behavior — fallback to next() without context is intentional for excluded routes.
github.ts Inconsistent pattern — some handlers don't use runWithLogContext Intentional — handlers without installationId rely on middleware-provided tenantId.
Reviewers (5)
Reviewer Returned Main Findings Consider Inline Comments Discarded
pr-review-sre 6 2 0 2 2
pr-review-architecture 8 0 1 0 5
pr-review-tests 5 2 0 0 0
pr-review-consistency 5 1 0 2 2
pr-review-standards 0 0 0 0 0
Total 24 5 1 4 9

Note: pr-review-standards returned no findings — the core implementation is solid.


💡 APPROVE WITH SUGGESTIONS

Summary: The AsyncLocalStorage-based scoped logger context is a well-designed feature with good test coverage. The core implementation is solid. However, there are two issues worth addressing:

  1. recreateInstance() bug — ALS-scoped logs won't use updated transports after addTransport() calls (1-line fix)
  2. Context loss in error handlers — Background trigger failure logs lose correlation IDs, hurting incident investigation

Both fixes are straightforward. The test mock inconsistency in agents-work-apps is minor but worth fixing for consistency. Nice work on this foundational infrastructure change! 🎉

const safeExecutionPromise = executionPromise.catch(async (error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
logger.error({ err: errorMessage, errorStack }, 'Background trigger execution failed');
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.

🟠 MAJOR: ALS context lost in .catch() handler — logs will lack correlation IDs

Issue: The safeExecutionPromise.catch() handler executes after the runWithLogContext scope has exited (via waitUntil). At this point, the ALS store is empty, so logger.error falls back to the base logger without tenantId, projectId, agentId, triggerId, or invocationId.

Why: When investigating production incidents, error logs from background trigger failures won't correlate with the triggering request. This makes debugging significantly harder in multi-tenant environments.

Fix: Capture the bindings before the catch and wrap the error handler:

// Before the .catch():
const errorBindings = { tenantId, projectId, agentId, triggerId, invocationId };

const safeExecutionPromise = executionPromise.catch(async (error) => {
  return runWithLogContext(errorBindings, async () => {
    const errorMessage = error instanceof Error ? error.message : String(error);
    // ... rest of error handling
  });
});

Refs:

@github-actions github-actions Bot deleted a comment from claude Bot Apr 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

Preview URLs

Use these stable preview aliases for testing this PR:

These point to the same Vercel preview deployment as the bot comment, but they stay stable and easier to find.

Raw Vercel deployment URLs

amikofalvy and others added 2 commits April 7, 2026 14:24
…cs, async test

- Add JSDoc to .with() documenting snapshot semantics (captures ALS context
  at call time, returned logger does not continue proxying)
- Add async propagation test to verify ALS context survives await boundaries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(2) Total Issues | Risk: Medium

Re-review scope: This review covers only the 3 commits since the prior automated review. Two major issues from that review remain unresolved.

✅ Delta Changes Addressed Prior Feedback

The following items from prior reviews have been addressed in this delta:

  • getLogContext() dead code — Removed (pullfrog feedback)
  • Missing async await boundary test — Added comprehensive test at logger.test.ts:203-214
  • JSDoc for .with() snapshot semantics — Added clear documentation explaining the behavior
  • Type safety in createApp.ts — Improved from (c as any).get() to typed key access

🟡 Minor (1) 🟡

🟡 1) logger.test.ts:203-214 Async test uses standalone PinoLogger instead of factory pattern

Issue: The new async await boundary test creates a direct PinoLogger via createTestLogger(), but production code obtains loggers through loggerFactory.getLogger().

Why: The test proves ALS propagation works for direct instances but doesn't verify factory-cached loggers also correctly resolve ALS context across await boundaries. Low risk since other tests validate factory loggers with ALS, but coverage would be more representative with a factory-based async test.

Fix: Consider adding a companion test using loggerFactory.configure() and loggerFactory.getLogger() with async/await.

Refs:

💭 Consider (1) 💭

💭 1) logger.test.ts:207 Test only verifies single await boundary

Issue: The async test validates context survives one setTimeout await but doesn't exercise multiple sequential awaits or nested async calls.

Why: AsyncLocalStorage is designed to handle this correctly by specification, so risk is low. Adding multiple awaits would marginally increase confidence.

Fix: Optionally enhance with multiple awaits in sequence.

🕐 Pending Recommendations (2)

These issues were raised in the prior review and remain unresolved:

  • 🟠 logger.ts:129-156recreateInstance() doesn't call setBasePinoInstance(), causing split-brain where ALS-scoped logs miss transport updates
  • 🟠 TriggerService.ts:568-585 — ALS context lost in .catch() handler after runWithLogContext scope exits via waitUntil

💡 APPROVE WITH SUGGESTIONS

Summary: The delta commits addressed several feedback items nicely — the dead code removal, JSDoc additions, and async test are all good improvements. However, the two major issues from the prior review (recreateInstance() not updating basePinoInstance and ALS context loss in the .catch() handler) remain unresolved. These should be addressed before merge to ensure:

  1. Transport changes (e.g., adding observability transports) propagate to ALS-scoped logs
  2. Error logs from background trigger failures can be correlated with the originating request
Discarded (1)
Location Issue Reason Discarded
setup.ts (agents-work-apps) Missing runWithLogContext mock Not blocking — agents-work-apps doesn't currently use the pattern. Can be added when adoption spreads.
Reviewers (3)
Reviewer Returned Main Findings Consider Pending Recs Discarded
pr-review-sre 1 0 0 1 0
pr-review-standards 1 0 0 1 0
pr-review-tests 2 1 1 0 0
Total 4 1 1 2 0

Note: Major findings routed to Pending Recommendations as they were raised in prior review.

@github-actions github-actions Bot deleted a comment from claude Bot Apr 7, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) New Issues | (2) Pending | Risk: Medium

Re-review scope: This review covers only the 1 commit since the prior automated review.

✅ Delta Analysis

The delta since the last review consists of a single commit:

  • 47481b4e0chore: remove accidentally committed pr-context skill

This commit removes .agents/skills/pr-context/SKILL.md (385 lines deleted), which was accidentally committed and shouldn't have been part of the PR. No code changes. The core logger implementation and adoption files remain unchanged.

🕐 Pending Recommendations (2)

These major issues were raised in prior reviews and remain unresolved. They should be addressed before merge:

  • 🟠 logger.ts:129-156recreateInstance() doesn't call setBasePinoInstance() after creating a new pino instance, causing split-brain where ALS-scoped logs miss transport updates
  • 🟠 TriggerService.ts:567-585 — ALS context lost in .catch() handler after runWithLogContext scope exits via waitUntil; error logs won't have correlation IDs

🚫 REQUEST CHANGES

Summary: The delta commit is a trivial cleanup (removing an accidentally committed file). However, two major issues identified in prior reviews remain unaddressed:

  1. recreateInstance() bug — When transports are added via addTransport() or options updated via updateOptions(), the new pino instance is created but basePinoInstance isn't updated. This means ALS-scoped logs will continue using the old instance, missing new transports. Fix: Add setBasePinoInstance(this.pinoInstance) at the end of recreateInstance().

  2. Context loss in error handlers — The .catch() handler in dispatchExecution runs after the runWithLogContext scope exits. Error logs will lack tenantId, projectId, agentId, triggerId, and invocationId, making incident investigation significantly harder. Fix: Capture bindings before the .catch() and wrap the error handler in runWithLogContext.

Both fixes are straightforward 1-5 line changes. The core ALS implementation is solid — just these two edge cases need attention before this is ready to merge.

Reviewers (0)
Reviewer Returned Main Findings Consider Pending Recs Discarded
N/A 0 0 0 0 0
Total 0 0 0 0 0

Note: No sub-reviewers dispatched — delta was trivial (skill file deletion only).

Allow logger.info('message') without an empty data object, alongside the
existing logger.info({ data }, 'message') form. Converts 41 empty-object
calls in AgentSession, TriggerService, and github.ts to the cleaner form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) New Issues | (2) Pending | Risk: Medium

Re-review scope: This review covers only the 1 commit since the prior automated review.

✅ Delta Analysis

The delta since the last review consists of a single commit:

  • fc7e62b94feat(agents-core): add string-only overloads to PinoLogger methods

What changed:

  • Added function overloads to error(), warn(), info(), debug() methods allowing logger.info('message') without requiring an empty data object
  • Updated ~60 call sites across github.ts, TriggerService.ts, and AgentSession.ts to use the cleaner syntax

Assessment: This is a clean DX improvement. The implementation correctly distinguishes between single-argument (message-only) and two-argument (data + message) calls. TypeScript overloads provide type safety. The changes are mechanical and low-risk.

💭 Consider (1) 💭

💭 1) logger.test.ts No tests for string-only overloads

Issue: The new function overloads (logger.info('message') without data object) are exercised by production code but not by any unit tests. All existing tests use the two-argument form.

Why: Low risk since CI passes and TypeScript provides type safety, but explicit test coverage would document the expected behavior and catch regressions if the overload logic changes.

Fix: Consider adding a test case:

it('supports string-only log calls', () => {
  runWithLogContext({ tenantId: 'tenant-1' }, () => {
    logger.info('message only');
  });
  expect(lines[0].tenantId).toBe('tenant-1');
  expect(lines[0].msg).toBe('message only');
});

Refs:

🕐 Pending Recommendations (2)

These major issues were raised in prior reviews and remain unresolved. They should be addressed before merge:

  • 🟠 logger.ts:129-156recreateInstance() doesn't call setBasePinoInstance() after creating a new pino instance, causing split-brain where ALS-scoped logs miss transport updates
  • 🟠 TriggerService.ts:567-585 — ALS context lost in .catch() handler after runWithLogContext scope exits via waitUntil; error logs won't have correlation IDs

🚫 REQUEST CHANGES

Summary: The delta commit (fc7e62b94) is a clean DX improvement — adding string-only overloads to logger methods simplifies call sites from logger.info({}, 'message') to logger.info('message'). No new issues introduced.

However, two major issues identified in prior reviews remain unaddressed:

  1. recreateInstance() bug — When transports are added via addTransport() or options updated via updateOptions(), the new pino instance is created but basePinoInstance isn't updated. This means ALS-scoped logs will continue using the old instance, missing new transports. Fix: Add setBasePinoInstance(this.pinoInstance) at the end of recreateInstance().

  2. Context loss in error handlers — The .catch() handler in dispatchExecution runs after the runWithLogContext scope exits. Error logs will lack tenantId, projectId, agentId, triggerId, and invocationId, making incident investigation significantly harder. Fix: Capture bindings before the .catch() and wrap the error handler in runWithLogContext.

Both fixes are straightforward 1-5 line changes. The core ALS implementation is solid — just these two edge cases need attention before this is ready to merge.

Discarded (0)

No findings discarded in this review cycle.

Reviewers (0)
Reviewer Returned Main Findings Consider Pending Recs Discarded
N/A 0 0 0 0 0
Total 0 0 0 0 0

Note: No sub-reviewers dispatched — delta was mechanical (method overloads + call site simplifications).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel vercel Bot temporarily deployed to Preview – agents-docs April 7, 2026 22:15 Inactive
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 7, 2026

Acknowledged the author's reply on the executionContext cast narrowing in createApp.ts. The cast was already changed from (c as any).get(...) to c.get('executionContext' as keyof typeof c.var), which is a tighter approach. Resolved the thread — no further code changes needed.

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) New Issues | (2) Pending | Risk: Medium

Re-review scope: This review covers only the 1 commit since the prior automated review.

✅ Delta Analysis

The delta since the last review consists of a single commit:

  • e29980463fix: remove remaining empty object arg from multiline logger.warn call

What changed:

-        logger.warn(
-          {},
-          'waitUntil is NOT available — background execution will be abandoned on serverless'
-        );
+        logger.warn(
+          'waitUntil is NOT available — background execution will be abandoned on serverless'
+        );

Assessment: This is a correct cleanup using the new string-only overload added in fc7e62b94. The change is mechanical, low-risk, and consistent with the pattern established across other converted call sites. ✅

🕐 Pending Recommendations (2)

These major issues were raised in prior reviews and remain unresolved. They should be addressed before merge:

  • 🟠 logger.ts:129-156recreateInstance() doesn't call setBasePinoInstance() after creating a new pino instance, causing split-brain where ALS-scoped logs miss transport updates
  • 🟠 TriggerService.ts:568-585 — ALS context lost in .catch() handler after runWithLogContext scope exits via waitUntil; error logs won't have correlation IDs

🚫 REQUEST CHANGES

Summary: The delta commit (e29980463) is a trivial 1-line cleanup removing an empty object argument from logger.warn — correctly using the new string-only overload. No issues with this change.

However, two major issues identified in prior reviews remain unaddressed:

  1. recreateInstance() bug (logger.ts:129-156) — When transports are added via addTransport() or options updated via updateOptions(), the new pino instance is created but basePinoInstance isn't updated. This means ALS-scoped logs will continue using the old instance, missing new transports.

    Fix: Add setBasePinoInstance(this.pinoInstance) at the end of recreateInstance().

  2. Context loss in error handlers (TriggerService.ts:568-585) — The .catch() handler runs after the runWithLogContext scope exits via waitUntil. Error logs will lack tenantId, projectId, agentId, triggerId, and invocationId, making incident investigation significantly harder.

    Fix: Capture bindings before the .catch() and wrap the error handler in runWithLogContext:

    const errorBindings = { tenantId, projectId, agentId, triggerId, invocationId, conversationId };
    
    const safeExecutionPromise = executionPromise.catch(async (error) => {
      return runWithLogContext(errorBindings, async () => {
        // ... existing error handling
      });
    });

Both fixes are straightforward 1-5 line changes. The core ALS implementation is solid — just these two edge cases need attention before this is ready to merge.

Discarded (0)

No findings discarded in this review cycle.

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
N/A 0 0 0 0 0 2 0
Total 0 0 0 0 0 2 0

Note: No sub-reviewers dispatched — delta was trivial (1-line cleanup to use string-only logger overload).

@itoqa
Copy link
Copy Markdown

itoqa Bot commented Apr 7, 2026

Ito Test Report ✅

17 test cases ran. 1 additional finding, 16 passed.

The unified run executed 17 test cases with 16 passed and 1 failed, showing strong stability across GitHub manage routes, installation lifecycle actions, callback/connect flows, webhook/run-auth paths, and adversarial scenarios with expected controlled contracts (200/202/4xx/404/503) and no middleware crashes or data leaks. The single confirmed issue is a minor docs regression where API reference pages initialize and call PostHog even when NEXT_PUBLIC_POSTHOG_KEY is unset, causing repeated console errors on load/reload that add debugging noise but do not indicate a backend failure.

✅ Passed (16)
Category Summary Screenshot
Adversarial Tenant-A access to tenant-B GitHub API/UI was denied (403/404) with no protected data exposure. ADV-1
Adversarial Path/query injection probes returned controlled responses without crashes, stack traces, or sensitive leaks. ADV-2
Adversarial Unauthenticated forged-origin POSTs to disconnect/reconnect/sync were rejected (401) with no installation state change. ADV-3
Adversarial Malformed webhook calls produced controlled 4xx responses (400/401) and a subsequent run route probe still returned normal 404, indicating no crash cascade. ADV-4
Adversarial After logout, protected GitHub routes redirected to login with returnUrl, and access resumed normally after re-login. ADV-5
Edge Rapid repeated sync interactions did not wedge the UI; controls remained available and page state stayed stable. EDGE-1
Edge Rapid repeated connect clicks produced controlled errors, and the page remained interactive without a stuck loading state. EDGE-2
Edge Success/error callback replay via refresh/back-forward normalized to the GitHub settings route without crash or replay loop. EDGE-3
Edge On iPhone 13 viewport, core GitHub detail controls stayed visible and tappable before/after sync. EDGE-4
Edge Probing /run/auth/nonexistent returned controlled 404 response and did not crash middleware. EDGE-5
Happy-path Tenant GitHub settings view loaded and /manage/tenants/default/github/installations returned HTTP 200 before and after return navigation, confirming session continuity. ROUTE-1
Happy-path Confirmed prior blocked state was harness-related; Connect GitHub Organization opened the GitHub installation URL with signed state. ROUTE-2
Happy-path Detail page rendered successfully; sync action returned controlled sanitized 503 contract (no 500) and UI stayed usable. ROUTE-3
Happy-path Disconnect transition worked and marked installation disconnected; reconnect produced controlled non-500 error path (sanitized 503) without crash. ROUTE-4
Happy-path Happy-path webhook call returned 202 Accepted with success=true and both invocationId and conversationId after creating local trigger fixture. ROUTE-5
Happy-path Tenant-scoped feedback route returned a controlled HTTP 404 domain response (project fixture missing) without middleware crash or HTTP 500. ROUTE-6
ℹ️ Additional Findings (1)

These findings are unrelated to the current changes but were observed during testing.

Category Summary Screenshot
Happy-path 🟡 API docs client initializes PostHog with an empty token when the key is unset, causing runtime console errors during docs page loads/reloads. ROUTE-7
🟡 API docs pages render generated operation lists
  • What failed: Client analytics initializes and emits PostHog calls even when the key is missing, producing runtime console exceptions instead of safely no-oping.
  • Impact: Users and developers see repeated runtime console errors on docs pages in environments without a PostHog key. This increases noise and can mask real client-side issues during debugging.
  • Steps to reproduce:
    1. Start the docs app with NEXT_PUBLIC_POSTHOG_KEY unset.
    2. Open an API reference page such as /api-reference/feedback.
    3. Reload the page and inspect the browser console for PostHog initialization errors.
  • Stub / mock context: Development-only UI overlays were disabled in local Vite/Next dev configs to avoid overlay noise, and no route mocks, API stubs, or auth bypasses were used for this docs validation.
  • Code analysis: I reviewed the docs analytics client initialization path and pageview usage. agents-docs/src/instrumentation-client.ts always calls posthog.init with process.env.NEXT_PUBLIC_POSTHOG_KEY || '', while agents-docs/src/app/layout.tsx always mounts PostHogPageview, and agents-docs/src/app/posthog-pageview.tsx always calls register_once/capture on route changes; there is no client-side guard for a missing key.
  • Why this is likely a bug: The client code lacks a missing-key guard but still initializes and uses PostHog, directly explaining the observed exception in normal docs-page navigation/reload flows.

Relevant code:

agents-docs/src/instrumentation-client.ts (lines 5-12)

if (typeof window !== 'undefined') {
  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY || '', {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
    capture_pageview: false,
    person_profiles: 'always',
    disable_surveys: true,
    disable_external_dependency_loading: true,

agents-docs/src/app/layout.tsx (lines 115-119)

<Suspense>
  <PostHogPageview />
</Suspense>
<PostHogProvider>
  <RootProvider search={{ SearchDialog }}>

agents-docs/src/app/posthog-pageview.tsx (lines 58-60)

posthog.capture('$pageview', {
  $current_url: url,
});

Commit: e299804

View Full Run


Tell us how we did: Give Ito Feedback

Merged via the queue into main with commit 93eb31e Apr 7, 2026
34 checks passed
@amikofalvy amikofalvy deleted the feat/logger-scoped-context branch April 7, 2026 22:59
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

🔎💬 Inkeep AI search and chat service is syncing content for source 'Inkeep Agent Framework Docs'

amikofalvy added a commit that referenced this pull request Apr 8, 2026
…ull and projectFull

Replace the pre-ALS logger DI pattern with module-scope getLogger() calls.
In agentFull.ts, the default logger was a no-op, silently swallowing all
167 logger calls in production. This change turns on real logging for
agent CRUD operations and aligns both files with the scoped logger context
pattern established in PR #3054.

- Delete AgentLogger interface and no-op defaultLogger
- Delete ProjectLogger type export
- Add module-scope getLogger('agentFull') / getLogger('projectFull')
- Remove logger parameter from all exported curried functions
- Update all internal and external call sites

Closes PRD-6465

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
github-merge-queue Bot pushed a commit that referenced this pull request Apr 8, 2026
#3072)

* refactor(agents-core): remove logger dependency injection from agentFull and projectFull

Replace the pre-ALS logger DI pattern with module-scope getLogger() calls.
In agentFull.ts, the default logger was a no-op, silently swallowing all
167 logger calls in production. This change turns on real logging for
agent CRUD operations and aligns both files with the scoped logger context
pattern established in PR #3054.

- Delete AgentLogger interface and no-op defaultLogger
- Delete ProjectLogger type export
- Add module-scope getLogger('agentFull') / getLogger('projectFull')
- Remove logger parameter from all exported curried functions
- Update all internal and external call sites

Closes PRD-6465

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update skipped SDK test and clarify changeset for logger DI removal

- Fix type error in agent-refactor.test.ts (skipped test) that
  destructured 2 args from the outer curried function which now only
  takes 1 (db). The test was already broken before this change — it
  named the params (tenantId, agentData) but the outer function takes
  (db, logger).
- Clarify in changeset that removed types had zero external consumers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unused tenantId variable in skipped SDK test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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