feat(sdk): typed multi-listener registry replaces on* callback fields#936
Conversation
Replaces the 13 single-callback `on*: EventHook<T> = null` fields on
`AgentRelay` with a typed multi-listener registry:
relay.onAgentSpawned = handler; // 6.x — one consumer wins
// becomes
const off = relay.addListener('agentSpawned', handler); // 7.0
Adds four new call-site hooks that fire at the SDK boundary rather than
from broker events, so handlers can observe (and, for spawn, optionally
mutate) the spawn input before it reaches the broker:
- `beforeAgentSpawn` — handlers may return a `SpawnPatch` that is
shallow-merged into the input in registration order. The first
consumer is a burn integration in Pear that preallocates Claude
session ids via `--session-id <uuid>` so the resulting session can
be stamped by exact id rather than via the sidecar manifest path.
- `afterAgentSpawn` — observe-only; payload includes `resolvedInput`,
`durationMs`, and either `result` or `error`.
- `beforeAgentRelease` / `afterAgentRelease` — observe-only spawn-side
counterparts for the release verb.
Channel subscribe / unsubscribe handler signatures change from
positional `(agent, channels)` args to a single `{ agent, channels }`
object payload for shape consistency with the rest of the registry.
Per-call option callbacks (`onStart` / `onSuccess` / `onError` /
`onStderr` passed as method arguments) and `Agent.onOutput()` are
unchanged — those are scoped local APIs, not global hooks.
Migration sweep covers `spawn-from-env.ts`, `workflows/runner.ts`
(unsubs tracked in `unsubRelayListeners`), `scripts/spawn-reviewers.ts`,
`tests/workflows/run-e2e-owner-review.ts`, fourteen `__tests__` files in
`packages/sdk`, `shadow.ts`, `packages/acp-bridge/src/acp-agent.ts`,
`packages/openclaw/src/spawn/process.ts`, and the four example scripts
in `src/examples`.
New types and runtime exported from `@agent-relay/sdk`:
- `EventBus<E>`, `EventHandler<Args>`, `EventMap`
- `AgentRelayEvents` map
- `BeforeAgentSpawnContext` / `AfterAgentSpawnContext`
- `BeforeAgentReleaseContext` / `AfterAgentReleaseContext`
- `BeforeAgentSpawnHandler` / `SpawnPatch`
Tests: `packages/sdk/src/__tests__/event-bus.test.ts` (10 tests covering
add/remove, ordering, async dispatch, error isolation, snapshot safety)
and `packages/sdk/src/__tests__/lifecycle-hooks.test.ts` (11 tests
covering the four call-site hooks, patch-merge semantics, array
replace-vs-spread contract, error-path `afterAgentSpawn` shape, and
`removeListener`). Vitest run passes 21/21.
Docs: rewrites `web/content/docs/event-handlers.mdx` for the new API
and includes a 6.x → 7.0 migration block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR migrates the ChangesMulti-listener Event System
Test Suite and Consumer Migration
Public API and Documentation
Sequence DiagramsequenceDiagram
participant App
participant Client as AgentRelayClient
participant EventBus
participant Relay as AgentRelay
participant Broker
App->>Client: addListener('beforeAgentSpawn', hook)
Client->>EventBus: addListener hook registered
App->>Client: spawnPty(name, task, options)
Client->>EventBus: emit('beforeAgentSpawn', context)
EventBus->>App: invoke hook, return SpawnPatch
Client->>Client: merge SpawnPatch into resolvedInput
Client->>Broker: POST /api/spawn with resolvedInput
Broker-->>Client: success (Agent)
Client->>EventBus: emit('afterAgentSpawn', context, result)
EventBus->>App: invoke afterAgentSpawn listeners
App->>Relay: addListener('agentExited', handler)
Relay->>EventBus: addListener handler registered
Broker-->>Relay: agent exited event
Relay->>EventBus: emit('agentExited', agent)
EventBus->>App: invoke handler with Agent
App->>Client: release(name, reason)
Client->>EventBus: emit('beforeAgentRelease', context)
Client->>Broker: DELETE /api/agents/{name}
Broker-->>Client: success
Client->>EventBus: emit('afterAgentRelease', context, durationMs)
EventBus->>App: invoke afterAgentRelease listeners
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
| function runQualityChecks(): { passed: boolean; output: string } { | ||
| try { | ||
| const output = execSync(QUALITY_CMD, { encoding: "utf-8", stdio: "pipe" }); | ||
| const output = execSync(QUALITY_CMD, { encoding: 'utf-8', stdio: 'pipe' }); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8334a56d15
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (set!.size === 0) { | ||
| this.handlers.delete(event); |
There was a problem hiding this comment.
Prevent stale unsubscribe from clearing newer listeners
The unsubscribe closure returned by addListener always runs this.handlers.delete(event) when its captured set is empty, even if that set is no longer the one stored for the event. If a caller unsubscribes, re-subscribes later, and then invokes the old unsubscribe again (double-cleanup is common in teardown/finally paths), this stale closure will delete the new set and silently drop active listeners for that event. Make the delete conditional on identity (e.g., only delete when this.handlers.get(event) === set).
Useful? React with 👍 / 👎.
| relay.addListener('agentExited', (agent: Agent, code?: number) => { | ||
| console.log(`[exited] ${agent.name} code=${code}`); | ||
| }; | ||
| }); |
There was a problem hiding this comment.
🟡 agentExited handler's code parameter is always undefined — exit code never printed
The agentExited event in AgentRelayEvents is typed as [Agent] (single-element tuple at packages/sdk/src/lifecycle-hooks.ts:148), so the handler receives exactly one argument. The handler at line 23 declares a second code?: number parameter that will always be undefined at runtime, causing every log line to print code=undefined instead of the actual exit code. The fix is to read agent.exitCode from the Agent object (as done correctly in packages/openclaw/src/spawn/process.ts:183).
| relay.addListener('agentExited', (agent: Agent, code?: number) => { | |
| console.log(`[exited] ${agent.name} code=${code}`); | |
| }; | |
| }); | |
| relay.addListener('agentExited', (agent: Agent) => { | |
| console.log(`[exited] ${agent.name} code=${agent.exitCode}`); | |
| }); |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
web/content/docs/event-handlers.mdx (1)
24-31:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDefine a named handler before showing
removeListener.Line 27 uses
myHandlerwithout defining it in the snippet, so copy-paste users can’t apply the “or” path. Consider naming the handler up front and using it for both add/remove.Proposed doc fix
-const off = relay.addListener('messageReceived', (msg) => { +const myHandler = (msg) => { console.log(`${msg.from} -> ${msg.to}: ${msg.text}`); -}); +}; +const off = relay.addListener('messageReceived', myHandler); ... relay.removeListener('messageReceived', myHandler);🤖 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 `@web/content/docs/event-handlers.mdx` around lines 24 - 31, The snippet shows removeListener('messageReceived', myHandler) but never defines myHandler; update the example to declare a named handler function (e.g., myHandler) before registering it so the same function reference can be used for both adding and removing listeners (use the same named handler when calling relay.on/relay.addListener and when calling relay.removeListener or off()). Ensure the example also shows relay = AgentRelay() remains and that off() usage is demonstrated with the named handler for clarity.packages/sdk/src/shadow.ts (1)
19-28:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix JSDoc example:
interceptis called with the wrong arity.The example passes a third argument (
msg) tointercept, butinterceptonly accepts(from, to). This can mislead SDK users copying the snippet.Suggested doc fix
- const copies = shadows.intercept(msg.from, msg.to, msg); + const copies = shadows.intercept(msg.from, msg.to);🤖 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 `@packages/sdk/src/shadow.ts` around lines 19 - 28, The JSDoc example calls shadows.intercept with three arguments but intercept only accepts (from, to); update the example to call shadows.intercept(msg.from, msg.to) (remove the third arg) so it matches the intercept(from, to) signature used later when iterating over the returned copies; ensure any surrounding comments or variable names (shadows, intercept, relay.addListener, relay.human) remain consistent with this corrected call.
🤖 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 `@CHANGELOG.md`:
- Line 109: Edit the CHANGELOG.md entry that reads "Detect opencode api key
completion (`#934`) (`#934`)" and remove the issue/PR references so it becomes
"Detect opencode api key completion" (remove both "(`#934`)" instances); ensure no
other PR/issue tokens remain on that bullet and save the file.
- Line 12: Change the long single CHANGELOG entry for `@agent-relay/sdk` /
`AgentRelay` into 3–5 short, impact-first bullets: one bullet describing the
listener API migration (mention `addListener(event, handler)` /
`removeListener(event, handler)` and that this replaces the old single-callback
`on*` fields like `onMessageReceived`, `onAgentSpawned`, etc., with a short
migration hint `relay.onX = handler;` → `relay.addListener('x', handler);`), one
bullet describing the channel subscribe/unsubscribe payload shape change (note
handlers now receive a single `{ agent, channels }` object instead of `(agent,
channels)`), and one bullet listing the new lifecycle hooks (`beforeAgentSpawn`,
`afterAgentSpawn`, `beforeAgentRelease`, `afterAgentRelease`) with a short note
that `beforeAgentSpawn` listeners can return a `SpawnPatch` to mutate spawn
input; drop implementation backstory and keep each bullet focused on
user-visible impact.
In `@packages/sdk/package.json`:
- Line 3: package.json was bumped to version "7.0.0" but the package-lock.json
was not regenerated, causing npm ci to fail; run npm install (or npm ci) locally
in the packages/sdk directory to regenerate and update package-lock.json so it
matches the new version and dependency tree, then commit the updated
package-lock.json alongside the package.json change (ensure `@agent-relay/sdk`
version entries in package-lock.json reflect 7.0.0).
In `@packages/sdk/src/event-bus.ts`:
- Around line 30-43: The returned off() closure in addListener currently closes
over the local `set` which becomes stale; change the off closure in
`addListener` to re-read the current set from `this.handlers` (e.g. `const cur =
this.handlers.get(event)`) and operate on `cur` (delete the handler and if
`cur.size === 0` delete the key), making the off() idempotent and avoiding
removal of a newly-registered set; update `addListener` to no longer rely on the
captured `set` and add a regression test in `event-bus.test.ts` that
double-calls the original `off()` after re-registering a listener to confirm the
new listener is not dropped.
---
Outside diff comments:
In `@packages/sdk/src/shadow.ts`:
- Around line 19-28: The JSDoc example calls shadows.intercept with three
arguments but intercept only accepts (from, to); update the example to call
shadows.intercept(msg.from, msg.to) (remove the third arg) so it matches the
intercept(from, to) signature used later when iterating over the returned
copies; ensure any surrounding comments or variable names (shadows, intercept,
relay.addListener, relay.human) remain consistent with this corrected call.
In `@web/content/docs/event-handlers.mdx`:
- Around line 24-31: The snippet shows removeListener('messageReceived',
myHandler) but never defines myHandler; update the example to declare a named
handler function (e.g., myHandler) before registering it so the same function
reference can be used for both adding and removing listeners (use the same named
handler when calling relay.on/relay.addListener and when calling
relay.removeListener or off()). Ensure the example also shows relay =
AgentRelay() remains and that off() usage is demonstrated with the named handler
for clarity.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 93ae9994-9400-4413-9ea2-df42ccba93da
📒 Files selected for processing (38)
CHANGELOG.mdpackages/acp-bridge/src/acp-agent.tspackages/openclaw/src/spawn/process.tspackages/sdk/package.jsonpackages/sdk/src/__tests__/agent-activity.test.tspackages/sdk/src/__tests__/builder-resume-persistence.test.tspackages/sdk/src/__tests__/completion-pipeline.test.tspackages/sdk/src/__tests__/e2e-owner-review.test.tspackages/sdk/src/__tests__/event-bus.test.tspackages/sdk/src/__tests__/facade.test.tspackages/sdk/src/__tests__/idle-nudge.test.tspackages/sdk/src/__tests__/lifecycle-hooks.test.tspackages/sdk/src/__tests__/orchestration-upgrades.test.tspackages/sdk/src/__tests__/quickstart.test.tspackages/sdk/src/__tests__/relay-channel-ops.test.tspackages/sdk/src/__tests__/resume-fallback.test.tspackages/sdk/src/__tests__/start-from.test.tspackages/sdk/src/__tests__/workflow-runner.test.tspackages/sdk/src/client.tspackages/sdk/src/event-bus.tspackages/sdk/src/examples/demo.tspackages/sdk/src/examples/persona-spawn.tspackages/sdk/src/examples/quickstart.tspackages/sdk/src/examples/ralph-loop.tspackages/sdk/src/index.tspackages/sdk/src/lifecycle-hooks.tspackages/sdk/src/relay.tspackages/sdk/src/shadow.tspackages/sdk/src/spawn-from-env.tspackages/sdk/src/workflows/__tests__/budget-enforcement.test.tspackages/sdk/src/workflows/__tests__/e2e-permissions.test.tspackages/sdk/src/workflows/__tests__/permissions-integration.test.tspackages/sdk/src/workflows/__tests__/verification-custom.test.tspackages/sdk/src/workflows/__tests__/verification-traceback.test.tspackages/sdk/src/workflows/runner.tsscripts/spawn-reviewers.tstests/workflows/run-e2e-owner-review.tsweb/content/docs/event-handlers.mdx
|
|
||
| #### User-Impacting Fixes | ||
|
|
||
| - Detect opencode api key completion (#934) (#934) |
There was a problem hiding this comment.
Remove issue/PR references from this changelog bullet.
Line 109 includes (#934) (#934), which should be removed per changelog style rules.
As per coding guidelines, “Drop issue/PR links… unless that text clearly explains the shipped impact.”
🤖 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 `@CHANGELOG.md` at line 109, Edit the CHANGELOG.md entry that reads "Detect
opencode api key completion (`#934`) (`#934`)" and remove the issue/PR references so
it becomes "Detect opencode api key completion" (remove both "(`#934`)"
instances); ensure no other PR/issue tokens remain on that bullet and save the
file.
The previous commit set @agent-relay/sdk to 7.0.0 in the PR, which broke CI because the package-lock.json + sibling workspace package.json files all reference 6.3.3. The publish.yml workflow takes a `version` input (patch | minor | major | …) on workflow_dispatch and bumps every package in lockstep, so version bumps belong in the release, not the PR that ships the breaking change. The release-notes "Breaking Changes" entry in CHANGELOG already covers the v7-bound migration guidance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EventBus.addListener — stale-closure bug (CodeRabbit + Codex P1): the unsubscribe returned by `addListener` captured `set` by reference. After unsubscribing, registering a new handler for the same event, and calling the original `off` again, the closure would observe the stale (empty) `Set` and call `this.handlers.delete(event)` — silently dropping every fresh listener for that event. Fix: re-read the current Set from `this.handlers` inside the closure and bail out if it isn't the one we originally inserted. `removeListener` already had this shape; bringing `addListener`'s unsubscribe in line. Regression test added — covers off()→addListener→off() → new listener must still be called on emit. spawn-reviewers script — `agentExited` handler had a stray `code?: number` second parameter that's always undefined under the new single-arg listener contract. Read `agent.exitCode` from the Agent payload instead, matching the pattern in `packages/openclaw/src/spawn/process.ts`. CHANGELOG — split the single long `@agent-relay/sdk` breaking-change bullet into three impact-first bullets (listener API migration, channel-event payload shape change, new lifecycle hooks) per the repo's changelog-style rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
barryollama
left a comment
There was a problem hiding this comment.
Code Review: Approved ✅
This is a well-designed, comprehensive PR that improves the SDK's event system architecture.
Highlights:
- EventBus implementation is robust with proper stale-closure protection and sequential async dispatch
- Lifecycle hooks (, etc.) provide clean interception points for integrations
- SpawnPatch design correctly narrows mutable fields to prevent identity rewriting
- Test coverage is thorough, including edge cases like self-removal during emit
- Migration docs are clear with before/after examples
Minor Suggestions (non-blocking):
- In line 50, consider adding a brief inline comment explaining why the Set identity check matters - the stale closure protection is subtle
- The patch-merging in registration order (later wins) is documented and tested - good
Files Reviewed:
-
- Clean generic EventBus implementation
-
- Well-designed SpawnPatch contract
-
- Proper hook integration in spawn/release paths
-
- Clean facade wrapper
-
- Comprehensive test coverage
-
- Good migration documentation
LGTM - ship it!
Reviewed by Hermes Agent
barryollama
left a comment
There was a problem hiding this comment.
Code Review: Approved ✅
This is a well-designed, comprehensive PR that improves the SDK's event system architecture.
Highlights:
- EventBus implementation is robust with proper stale-closure protection (line 50 identity check pattern) and sequential async dispatch
- Lifecycle hooks (beforeAgentSpawn, etc.) provide clean interception points for integrations like burn cost-tracking
- SpawnPatch design correctly narrows mutable fields to prevent identity rewriting
- Test coverage is thorough, including edge cases like self-removal during emit (event-bus.test.ts line 66)
- Migration docs are clear with before/after examples
Minor Notes:
- The patch-merging in registration order (later wins) is documented and tested
- Handler isolation via try/catch prevents cascade failures
- All 37 files touched are appropriate to the change
- Channel subscription payload now uses object shape instead of positional args - good API design
One Suggestion (non-blocking):
In event-bus.ts line 50, consider adding a brief inline comment explaining why the Set identity check matters - the stale closure protection is subtle and future readers might miss it without context.
LGTM - ship it!
Reviewed by Hermes Agent
Summary
@agent-relay/sdk7.0 —AgentRelayswaps its 13 single-callbackon*: EventHook<T> = nullfields for a typed multi-listener registry.relay.onAgentSpawned = handlerbecomesconst off = relay.addListener('agentSpawned', handler)(returning unsubscribe). Multiple consumers can coexist for the same event.beforeAgentSpawn,afterAgentSpawn,beforeAgentRelease,afterAgentRelease.beforeAgentSpawnlisteners may return aSpawnPatch(shallow-merged in registration order) to mutate the spawn input before the broker POST. The other three are observe-only.--session-id <uuid>from abeforeAgentSpawnhandler and stamps the session in burn via the newwriteStampverb (shipped in burn#426, published as@relayburn/sdk@2.9.0). The Pear PR follows once this lands.Why
The single-callback shape was OK when only the SDK itself wrote to those fields, but as soon as a second consumer wanted in (burn cost-tagging, Pear UI), they had to chain through one slot — fragile and order-dependent. Multi-listener via
addListeneris the standard NodeEventEmittershape and matches the existingAgent.onOutput()pattern we ship today, so this is unification, not a new concept.The call-site
beforeAgentSpawnhook specifically unlocks integrations that need to intercept the spawn input (not just observe). Burn is the concrete case — preallocating--session-idis the most reliable way to tag a Claude session — but the contract is generic: any consumer can return aSpawnPatchto amendargs,channels,task,model,team, oragentToken.What's in the change
New files
packages/sdk/src/event-bus.ts—EventBus<E extends EventMap>withaddListener(returns unsubscribe),removeListener,emit,listenerCount,listeners()snapshot. Sequential async dispatch; handler errors caught + logged so one bad listener never blocks the chain or the originating operation.packages/sdk/src/lifecycle-hooks.ts—AgentRelayEventsmap (17 events),SpawnPatch, the four call-site context types, andBeforeAgentSpawnHandler. Type-only imports fromrelay.tsto avoid runtime cycle.SDK changes
packages/sdk/src/relay.ts: removes the 13on*fields, addsbus: EventBus<AgentRelayEvents>+ thinaddListener/removeListenermethods. All 13 internalthis.onX?.(...)callsites becomethis.bus.emit('x', ...). JSDoc +Agentexit-event references updated to the new event names.packages/sdk/src/client.ts:AgentRelayClientacceptseventBus?(the facade shares its bus).spawnPty/spawnProviderbuild aBeforeAgentSpawnContext, fold patches throughbeforeAgentSpawnhandlers to produceresolvedInput, POST that, then emitafterAgentSpawnwithresultorerrorplusdurationMs.releaseemits before/after. The pre-existing 30+ line POST body construction is extracted intobuildSpawnPtyBody/buildSpawnProviderBodyhelpers so the resolved input is serialized once.packages/sdk/src/index.ts: re-exportsEventBus, the event map, and all call-site context types.Migration sweep (no behavior change)
packages/sdk/src/spawn-from-env.ts,workflows/runner.ts(unsubscribes tracked inunsubRelayListenersand cleared in the existing teardown block), foursrc/examples/scripts.scripts/spawn-reviewers.ts,tests/workflows/run-e2e-owner-review.ts.packages/acp-bridge/src/acp-agent.ts,packages/openclaw/src/spawn/process.ts.packages/sdk/src/__tests__/andpackages/sdk/src/workflows/__tests__/. Mocks that exposedonXfields are restructured to exposeaddListener+ a local listener registry (relayListenersMap +emitRelayEventhelper) so tests can fire events at the SUT after capture.Special-case migration
onChannelSubscribed/onChannelUnsubscribedhandler signatures change from positional(agent, channels)to a single{ agent, channels }object — documented in the changelog. The unsubscribe-via-addListenerreturns an unsub function; oldrelay.onX = nullcallsites are deleted (they no longer apply).Docs + changelog
web/content/docs/event-handlers.mdxrewritten for the new API with a 6.x → 7.0 migration block. Python examples preserved unchanged.[Unreleased]block inCHANGELOG.mdgains a "Breaking Changes" entry and a "Migration Guidance" snippet.packages/sdk/package.jsonbumps to 7.0.0.Test plan
npx tsc -p tsconfig.json --noEmitinpackages/sdk— clean production buildnpx vitest run src/__tests__/event-bus.test.ts src/__tests__/lifecycle-hooks.test.ts— 21/21 new tests pass (10 EventBus + 11 lifecycle-hook integration via mocked transport)eslint --fixvialint-stagedpre-commit — cleanPer-call options unchanged
spawnPersona({ onStart, onSuccess, onError }),onStderronAgentRelaySpawnOptions, andAgent.onOutput(cb, opts) → unsubscribeare all unchanged — those are scoped per-call APIs (or already multi-listener), not global hooks.Follow-up: Pear PR
A separate PR in
pearconsumes:@agent-relay/sdk@^7.0.0forBeforeAgentSpawnContext+addListener@relayburn/sdk@^2.9.0forwriteStamp(from burn#426)It registers a
beforeAgentSpawnlistener on every broker session that preallocates a UUID for Claude spawns, returns aSpawnPatchinjecting--session-id <uuid>, and stamps the session with{ spawner: 'pear', on_relay: 'true', spawned_by: 'direct' }so the session appears inburn summary --tags spawner=pear. That PR opens once this one is published.🤖 Generated with Claude Code