Skip to content

feat(events): close NDJSON schema gaps for downstream consumers (#63)#64

Merged
byapparov merged 14 commits into
mainfrom
feature/ndjson-schema-gaps
Apr 23, 2026
Merged

feat(events): close NDJSON schema gaps for downstream consumers (#63)#64
byapparov merged 14 commits into
mainfrom
feature/ndjson-schema-gaps

Conversation

@byapparov
Copy link
Copy Markdown
Contributor

Summary

Closes #63. Pins the NDJSON event stream to schema v1 and closes the five gaps + one nice-to-have the downstream aictrl executor flagged.

  • permission_rejected now carries tool, input, callID in a stable flat shape. Tool id + args are threaded through PermissionNext.ask metadata from the per-tool execute closure.
  • permission_granted emitted symmetrically (new PermissionNext.Event.Granted bus event fired from the allow-short-circuit path).
  • session_error emitted before session_complete on failure, with {reason, code, message}. reason vocabulary: rate_limit, auth, timeout, oom, provider, unknown. Classifier in run.errors.ts has unit tests.
  • session_complete.error kept for back-compat; marked deprecated in docs.
  • session_start now carries schemaVersion: "1" and the fully resolved permissions ruleset (merge of agent + session rules).
  • sequenceNum added to text / reasoning / tool_use — monotonic per-session counter keyed on part.sessionID so subagent sessions have independent counters.
  • aictrl events subcommand prints the bundled EVENTS.md (embedded via Bun with { type: "text" } import so it works in the compiled single-file binary). --schema-version prints 1.

Downstream-facing design notes live in docs/superpowers/specs/2026-04-23-ndjson-schema-gaps-design.md; the execution breakdown is in docs/superpowers/plans/2026-04-23-ndjson-schema-gaps.md. Happy to drop those if they're noise.

Caveats

  • permission_rejected.input / permission_granted.input now ship raw tool arguments into NDJSON (bash command strings, file contents for write/edit). This is what NDJSON event schema gaps for downstream observability consumers #63 explicitly asked for, but worth flagging — anyone piping the stream to durable logs should treat it accordingly.
  • Schema tests in packages/cli/test/cli/run-schema-v1.test.ts are source-level assertions, matching the style of run-event-race.test.ts. A runtime-parse test would catch more regressions; happy to add if preferred.
  • Compiled-binary bundling of EVENTS.md via with { type: "text" } is Bun's documented behaviour but was not empirically verified (would require a full build).

Test plan

  • `bun turbo typecheck` green
  • `cd packages/cli && bun test test/cli/` green (66 tests)
  • `bun run --cwd packages/cli dev events --schema-version` prints `1`
  • `bun run --cwd packages/cli dev events` prints the v1 EVENTS.md
  • End-to-end `aictrl run --format json` against a real provider — confirm `session_start` carries `schemaVersion` + `permissions`, and a denied tool produces an enriched `permission_rejected`

🤖 Generated with Claude Code

Bulat Yapparov and others added 10 commits April 23, 2026 11:00
Design doc closing the 5 asks + permission_granted nice-to-have
from issue #63. Fixes schemaVersion to v1; defers reason field on
permission_rejected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Embed EVENTS.md via Bun's text import so the schema ships with both
`bun run dev` and the compiled single-file binary. Add `--schema-version`
flag for quick version discovery. Register command in both index.ts and
headless.ts entrypoints.

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

aictrl-dev Bot commented Apr 23, 2026

Now I have all the data. S = 255 for bug hunter, so N = 1. Dispatching 3 subagents (Security, Consistency, Bug Hunter) in parallel.## PR Review: #64 — feat(events): close NDJSON schema gaps for downstream consumers (#63)


BLOCKER / MAJOR Findings

1. packages/cli/src/permission/next.ts:159 — Object spread order silently overwrites computed id (MAJOR, object-spread-order)

The Granted event payload is built as { id, ...request }. If the request object (type Request) ever contains an id property, it silently overwrites the explicitly computed id from input.id ?? Identifier.ascending("permission"). Reverse the spread to { ...request, id } so the computed id always wins. Even if Request currently lacks id, this is a latent bug that will break silently if the type is extended.

2. packages/cli/src/cli/cmd/run.ts:645 — permission_granted never fires for user-approved grants (MAJOR, missing-event-coverage)

The permission_granted NDJSON event is only emitted when the permission.granted bus event fires, which is published exclusively in the auto-grant path of next.ts. User-approved permissions (manual allow through the UI prompt) never publish this bus event, so downstream consumers see permission_rejected for user denials but get no corresponding permission_granted for user approvals. This creates asymmetric observability. The reply handler needs a Granted publish on the accept path.

3. packages/cli/src/cli/cmd/events.ts:6 — Duplicated schema version constant (MAJOR, duplicated-constant)

SCHEMA_VERSION is hardcoded as "1" in events.ts and again as a string literal in run.ts's emit("session_start", { schemaVersion: "1" }). These must stay in sync but have no shared reference. Extract to a shared constant (e.g., in run.errors.ts or a new schema-constants module) and import in both files to prevent version drift.


MINOR Findings

4. packages/cli/src/cli/cmd/events.ts:17 — Unnecessary double type cast (MINOR, unnecessary-type-cast)

EVENTS_MD as unknown as string defeats type safety. The *.md module declaration in md.d.ts already types the default export as string, so the with { type: "text" } import should resolve to string directly. If it doesn't, the as unknown as chain silently masks the real type mismatch instead of surfacing it as a compile error.

5. packages/cli/src/cli/cmd/run.ts:649 — Fragile type cast on metadata.tool (MINOR, fragile-type-cast)

The permission_granted handler casts permission.metadata?.tool with as string | undefined, but metadata.tool is set from toolId (a string) in prompt.ts. The cast masks any future type drift between the metadata shape defined in prompt.ts and the shape destructured here. Prefer a runtime guard or shared type.

6. packages/cli/src/cli/cmd/run.ts:694 — Potentially large permissions payload on session_start (MINOR, potentially-large-payload)

session_start now emits the entire merged ruleset via PermissionNext.merge(agentInfo.permission, rules). For projects with large permission configs, this can produce very large JSON lines. Consider whether the full ruleset is needed at session start or if a summary/hash would suffice.

7. packages/cli/src/permission/next.ts:161 — Granted only fires for auto-allow, not user approval (MINOR, missing-grant-on-user-approval)

Event.Granted is only published at the end of ask() after all patterns pass through the allow-check loop. When a permission hits 'ask' and is later approved interactively via reply(), the Granted event is never published. The doc says permission_granted covers "an allow rule or a cached always approval", so user one-time approvals are intentionally excluded, but this asymmetry with permission_rejected (emitted for both auto-reject and user-reject) may surprise consumers.

8. packages/cli/src/session/prompt.ts:785 — Raw tool args forwarded into metadata.input without sanitization (MINOR, sensitive-data-flow)

Tool arguments (args) are unconditionally forwarded into metadata.input for the permission/ask flow. Depending on the tool, args can contain secrets (API keys, file contents, database URLs). If any downstream consumer serializes or persists this metadata without redaction, secrets could leak. Consider sanitizing or truncating args before embedding it in metadata, or documenting that downstream handlers must treat metadata.input as secret-bearing.

9. packages/cli/test/cli/events-command.test.ts:8 — Source-string-matching test doesn't exercise runtime behavior (MINOR, inconsistent-test-style)

This test reads the source file as a string and checks for substring presence rather than importing and invoking EventsCommand.handler. classify-session-error.test.ts in the same directory properly imports the function under test and exercises it. The string-matching approach is fragile to renames/formatting and won't catch runtime regressions.

10. packages/cli/test/cli/run-schema-v1.test.ts:6 — All five tests verify source text, not runtime behavior (MINOR, inconsistent-test-style)

All tests read run.ts source as text and assert on substring presence. They verify the source code was written, not that it behaves correctly at runtime. This is inconsistent with classify-session-error.test.ts. A refactoring that renames a variable or reorders emit arguments would silently pass these tests while breaking behavior.


NIT Findings

11. packages/cli/src/cli/cmd/events.ts:21 — Redundant double-assertion cast (NIT, redundant-cast)

The cast EVENTS_MD as unknown as string is redundant given md.d.ts types the default export as string. If Bun's with { type: "text" } import produces a different runtime type requiring the cast, add a comment explaining why; otherwise drop the double-assertion.

12. packages/cli/src/cli/cmd/run.ts:658 — Inconsistent error string sources (NIT, inconsistent-error-string)

In the second error handler (prompt rejection path), the local error variable still uses String(e) while session_complete.error now uses classified.message. These can diverge when the raw error object's toString differs from extractMessage's logic. Both should use the same source of truth.

13. packages/cli/src/session/prompt.ts:784 — Repeated as any casts bypass type checking (NIT, type-safety)

Repeated as any casts on req.metadata bypass TypeScript's type checker and can mask future unsafe property accesses. Introduce a typed interface (e.g., PermissionMetadata) for the metadata shape to get compile-time guarantees.

14. packages/cli/test/cli/run-schema-v1.test.ts:1 — Weak test strategy: string matching over runtime execution (NIT, weak-test-strategy)

Tests read the raw .ts source and assert on string patterns (e.g., source.contains("sequenceNum")). They verify the file was edited but not that the code actually runs or emits correctly shaped events. A typo inside a template literal or a dead code branch would still pass. These should be supplemented with at least one integration test that exercises the emit function and validates the JSON output.

- next.ts: reverse spread order in Event.Granted publish so computed id
  always wins over any id that might appear in request (latent bug).
- run.errors.ts: export SCHEMA_VERSION as the shared source of truth;
  events.ts and run.ts now import it instead of duplicating "1".
- events.ts: drop unnecessary `as unknown as string` cast; md.d.ts
  already types the import as string.
- run.ts: in the prompt-rejection catch, use classified.message for the
  local error string too, so session_complete.error matches session_error.

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

Addressed in a99c6ba:

Intentionally skipping the rest:

All 67 tests green after the fixes.

@aictrl-dev
Copy link
Copy Markdown

aictrl-dev Bot commented Apr 23, 2026

I'll start by fetching the PR metadata and per-file patches.Now let me examine the per-file patches to build the manifests.

@github-actions
Copy link
Copy Markdown

Review: NDJSON Schema v1 Gaps

Overall this is a well-structured PR. The error classification, schema versioning, and enriched permission events are clean. A few issues worth addressing:


Bug: Subagent permission events silently dropped

run.ts filters both permission.asked and permission.granted by permission.sessionID !== sessionID, which silently drops all subagent permission events. But subagent tool_use events are emitted (the part.sessionID !== sessionID branch explicitly emits them for childSessions). This creates an inconsistency: a downstream consumer sees a subagent tool_use with status: "error" but gets no corresponding permission_rejected to explain why.

Consider emitting subagent permission events the same way subagent tool_use events are emitted — check childSessions.has(permission.sessionID).

Bug: session_complete.error can be non-null without a session_error event

The error variable is populated from session.error bus events during the loop (non-fatal errors). If the loop then completes normally, the .then() handler emits session_complete with a non-null error but no session_error event. A consumer that relies on session_error to trigger alerting logic would miss these. Consider either:

  • Emitting session_error for each non-fatal session.error event in the loop body, or
  • Documenting that session_error is strictly for abnormal termination while session_complete.error may carry non-fatal warnings.

Security: Raw tool arguments piped to stdout

permission_rejected.input and permission_granted.input serialize full tool arguments (bash commands, file contents for write/edit) into the NDJSON stream. The PR caveats mention this, but it's worth emphasizing: anyone piping --format json output to durable logs or a SIEM should be aware these fields may contain secrets, file contents, or destructive commands. Consider documenting a recommendation to sanitize or omit input at the consumer layer.

Minor: permission_granted not emitted when patterns is empty

In next.ts:161, the guard if ((request.patterns ?? []).length > 0) prevents Event.Granted from firing when patterns is an empty array. This is technically an auto-grant (no patterns to deny), so a consumer tracking permission resolution won't see a corresponding event. Low severity but could confuse event consumers expecting symmetry.

Minor: Untyped assertion on agent permissions

run.ts line: (entry.permission ?? []) as PermissionNext.Ruleset — if the agent config format diverges from PermissionNext.Ruleset, this silently produces incorrect permissions in session_start. A runtime validation (e.g., PermissionNext.Ruleset.parse()) would be more defensive.


Reviewed SHA: a95d7dc

- run.ts: emit permission_rejected/permission_granted for subagent
  sessions too (previously filtered out), matching the existing
  subagent tool_use pass-through. Fixes an asymmetry where a
  subagent's denied tool surfaced as a failed tool_use with no
  explaining permission_rejected event.
- EVENTS.md: document that session_error is strictly for abnormal
  termination; session_complete.error may carry concatenated non-fatal
  errors without a preceding session_error. Alerting should key on
  session_error.

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

Addressed in 7a5fbdc:

  • Subagent permission events — both permission_rejected and permission_granted now emit for childSessions too, matching the existing tool_use pass-through. Fixes the asymmetry where a denied subagent tool showed up as a failed tool_use with no corresponding permission_rejected.
  • session_error vs session_complete.error — added a note to EVENTS.md clarifying that session_error is strictly for abnormal termination, while session_complete.error may carry concatenated non-fatal errors without a preceding session_error. Alerting should key on session_error, and individual non-fatal failures surface via the existing error event.

Skipping:

  • Raw args in input — already documented; this is what NDJSON event schema gaps for downstream observability consumers #63 asked for.
  • Empty-patterns granted skip — intentional (no permission was actually needed, no event to emit for).
  • Runtime Ruleset.parse() on session_start — trades hot-path predictability for defensiveness we don't need when Agent.Info.permission is already typed. If agent configs start drifting from the Ruleset schema, that's a bug upstream.

67/67 tests green.

@aictrl-dev
Copy link
Copy Markdown

aictrl-dev Bot commented Apr 23, 2026

I'll start by fetching the PR metadata and per-file patches.Now let me build the three manifests.Now compute the total changed lines for bug hunter sharding:S = 263, which is ≤ 1000, so N = 1 bug-hunter shard. That means 2 + 1 = 3 subagents total. Let me extract the full patch data for each manifest:Now I have all the data. Step 4: dispatch 3 subagents (Security + Consistency + 1 Bug Hunter shard) in parallel.Now I'll merge, deduplicate, sort, and emit the final output. Let me process all three subagent results.

Security subagent returned 2 findings.
Consistency subagent returned 6 findings.
Bug Hunter subagent returned 8 findings.

Deduplicating by (file, line, category):

  • Multiple findings on packages/cli/src/cli/cmd/run.ts at various lines with different categories — all unique.
  • The BLOCKER on run.ts:627 (broken-emit-field-mapping) and run.ts:752 (breaking-change on session_complete.error) are distinct categories.
  • The BLOCKER on run.ts:627 from Consistency and the MAJOR on run.ts:648 from Bug Hunter overlap on concern but differ in category/line — keep both.
  • Deduplicate: run.ts:752 BLOCKER (breaking-change) vs run.ts:711 MINOR (deprecated-field-behavior-change) — same file, different lines, different categories. Keep both.
  • Deduplicate: run.ts:629 (correctness, tool fallback) and run.ts:632 (performance, input size) are different categories/lines.

After deduplication, sorting by severity descending, then file, then line:

BLOCKER findings (2):
1. run.ts:627 - broken-emit-field-mapping (Consistency)
2. run.ts:752 - breaking-change (Bug Hunter)

MAJOR findings (4):
3. prompt.ts:785 - secret-leakage (Security)
4. run.ts:648 - permission_granted-shape-mismatch (Consistency)
5. run.ts:646 - missing-permission-granted-for-user-approval (Consistency)
6. next.ts:161 - correctness (Bug Hunter)

MINOR findings (6):
7. prompt.ts:784 - type-safety (Security)
8. run.ts:711 - deprecated-field-behavior-change (Consistency)
9. run.ts:629 - correctness tool fallback (Bug Hunter)
10. run.ts:632 - performance input size (Bug Hunter)
11. run.ts:708 - correctness stdout flush (Bug Hunter)
12. EVENTS.md:221 - doc-says-required-but-code-emits-null (Consistency)

NIT findings (3):
13. events.ts:12 - misleading-flag-name (Consistency)
14. events-command.test.ts:8 - test-quality (Bug Hunter)
15. run-schema-v1.test.ts:8 - test-quality (Bug Hunter)

Total: 15 findings, well under the 40 cap. No need to drop NIT or MINOR.


Security Concerns

packages/cli/src/session/prompt.ts:785 — MAJOR — secret-leakage

The raw args object is unconditionally spread into metadata.input (input: (req.metadata as any)?.input ?? args). Tool arguments frequently contain sensitive values — API keys, tokens, file contents, connection strings — that will now flow into the permission subsystem and may be logged, persisted, or emitted in the NDJSON event stream. Consider sanitizing or redacting known-sensitive fields (by schema secret/sensitive annotations) before embedding args into metadata, or only store a hash/redacted summary.

packages/cli/src/session/prompt.ts:784 — MINOR — type-safety

Repeated (req.metadata as any) casts bypass TypeScript's type checker. If the metadata shape is now expected to carry tool and input, define a proper typed interface for it (e.g. PermissionMetadata) so misuse is caught at compile time rather than silently producing undefined at runtime.


Breaking Changes / Wire Format

packages/cli/src/cli/cmd/run.ts:627 — BLOCKER — broken-emit-field-mapping

The permission_rejected handler reads permission.permission, permission.patterns, permission.tool?.callID, and permission.metadata from the permission.replied BusEvent, but that event's schema (defined in next.ts Event.Replied) only carries sessionID, requestID, and reply. None of the emitted NDJSON fields (tool, callID, permission, patterns, input) will be populated — they will all be undefined/null. This pre-existed for permission/patterns but the PR widens the gap by adding callID, tool, input that are also always undefined. Fix: extend the Event.Replied payload to carry the original request fields, or look them up from a pending map before emitting.

packages/cli/src/cli/cmd/run.ts:752 — BLOCKER — breaking-change

The session_complete.error field silently changes format from String(e) (e.g. "Error: boom") to classified.message (e.g. "boom"). Downstream consumers matching or parsing error strings will get different values with no version gate protecting them, since schemaVersion is only emitted in session_start and isn't enforced on individual event shapes.

packages/cli/src/cli/cmd/run.ts:648 — MAJOR — wire-format

New fields callID, tool, and input are added to permission_rejected and the brand-new permission_granted event, plus schemaVersion and permissions on session_start, plus a wholly new session_error event type. Consumers with strict schema validation (e.g. JSON Schema additionalProperties: false) will reject these events. The schema version is advisory only and not enforced per-event.


Correctness

packages/cli/src/cli/cmd/run.ts:646 — MAJOR — missing-permission-granted-for-user-approval

permission_granted is only emitted when all patterns auto-match allow rules (via the new Event.Granted in next.ts). When a user manually approves an ask-rule permission, the Event.Replied event fires with reply: "allow"/"always", but run.ts only handles the "reject" branch. This creates an asymmetry: permission_rejected is emitted for user-rejections, but permission_granted is NOT emitted for user-approvals. Downstream consumers cannot track all grants. Fix: add a permission.replied + reply === "allow" handler that emits permission_granted with the same shape.

packages/cli/src/permission/next.ts:161 — MAJOR — correctness

The Granted event is published with id: input.id ?? Identifier.ascending("permission"). If input.id is undefined but request already carries an id assigned when the Requested event was published, the Granted event gets a different correlation ID. Consumers tracking the permission lifecycle (Requested → Granted/Rejected) by id will fail to correlate.

packages/cli/src/cli/cmd/run.ts:629 — MINOR — correctness

The tool field fallback is (permission.metadata?.tool as string | undefined) ?? permission.permission. When metadata.tool is absent, the value becomes permission.permission—the permission type name (e.g. "file_write") rather than the invoking tool name (e.g. "Write"). Consumers relying on this field to identify the tool will receive a semantically different value.

packages/cli/src/cli/cmd/run.ts:708 — MINOR — correctness

Both session_error and session_complete are emitted before process.exit(1). If stdout is piped and buffered, the NDJSON lines may not flush before the process terminates. Using process.exitCode = 1 and allowing the event loop to drain (or calling process.stdout.write with a callback) would be safer.


Documentation

EVENTS.md:221 — MINOR — doc-says-required-but-code-emits-null

The input field on permission_rejected and permission_granted is documented as (any, **required**) with a note that it is null when unavailable. Since required in schema terminology typically means the key must be present (not that the value is non-null), the code correctly always sets it to null as fallback. However, the EVENTS.md convention elsewhere uses (string, optional) for nullable fields — marking an always-present-but-nullable field as required is internally inconsistent with the doc's own convention.


CLI Ergonomics

packages/cli/src/cli/cmd/events.ts:12 — NIT — misleading-flag-name

The --schema-version option is a boolean flag (type: "boolean"), but its name implies it should accept a version string value. A clearer name would be --print-schema-version or --only-schema-version to avoid confusion for CLI users.


Test Quality

packages/cli/test/cli/events-command.test.ts:8 — NIT — test-quality

Tests read the source file as a string and assert on substring presence (e.g. source.toContain("EventsCommand")). These tests pass even if the module fails to compile or export correctly and will break on trivial renames. Prefer importing and exercising the actual module or spawning the CLI and asserting on stdout.

packages/cli/test/cli/run-schema-v1.test.ts:8 — NIT — test-quality

Same source-string assertion pattern as events-command.test.ts. Tests like source.indexOf('emit("permission_granted"')) verify code was typed, not that it runs correctly at runtime. A refactor that moves or renames the emit call breaks the test without any functional regression.

@github-actions
Copy link
Copy Markdown

Review: NDJSON Schema v1 Gaps

Overall this is a solid, well-structured PR. The event schema additions are coherent, the classifier is clean, and the permission plumbing correctly threads tool ID + args through metadata. A few issues worth addressing:


Bug: process.exit(1) may truncate the last NDJSON events

In both error paths (run.ts .catch() handler and Promise.race rejection), you call process.exit(1) immediately after emit("session_error", ...) + emit("session_complete", ...). If stdout is a pipe (which it will be for any consumer parsing NDJSON), Node/Bun may not have flushed the write buffer before process.exit tears down the process. This means downstream consumers could miss the session_error and session_complete events that describe why the session failed — the very events they need for alerting.

Fix: Replace process.exit(1) with process.exitCode = 1 and let the event loop drain naturally (the process will exit on its own since there is nothing left to run). Alternatively, do process.stdout.once("drain", () => process.exit(1)) as a safety net.


Logic gap: permission_granted silently skipped for empty-pattern requests

In permission/next.ts:165-168:

if ((request.patterns ?? []).length > 0) {
  const id = input.id ?? Identifier.ascending("permission")
  Bus.publish(Event.Granted, { ...request, id } as Request)
}

If a tool calls ask() with patterns: [], the permission is implicitly granted (no error thrown, promise resolves), but no permission_granted event is emitted. Consumers tracking permission state via events will see a grant without a corresponding event, breaking their accounting. If empty patterns are not expected, the guard should log a warning; if they are valid, the event should fire unconditionally.


Docs vs reality: sequenceNum description is misleading for subagent sessions

EVENTS.md says sequenceNum is a "monotonic per-session counter shared across text, reasoning, and tool_use events." But for subagent sessions in JSON mode, only tool_use events are emitted (the text and reasoning paths are gated by part.sessionID !== sessionID → continue). So subagent counters only increment on tool_use, making them not shared across the three event types in practice. The docs should note this asymmetry, or the code should emit text/reasoning for subagents too.


Minor: extractStatus accepts arbitrary numeric strings

run.errors.ts:44extractStatus converts any all-digit string to a number (e.g., "99999"). This means non-HTTP status codes could flow into classifySessionError and potentially match the 5xx provider branch. Low risk since the source is error objects from SDKs, but worth clamping to valid HTTP ranges (100–599) if you want to be defensive.


Acknowledged: sensitive data in input field

Noting for the record (already called out in the PR description): permission_rejected.input and permission_granted.input ship raw tool args into NDJSON, including bash commands and file contents for write/edit. Consumers piping to durable logs should be aware. This is by-design per #63.


Reviewed SHA: 7a5fbdc

…subagents

- run.ts: replace process.exit(1) with process.exitCode = 1 in both
  error paths so stdout drains before the process exits. Piped consumers
  were at risk of missing the session_error / session_complete lines
  that describe why the session failed.
- EVENTS.md: note that in JSON mode subagent sessions only emit tool_use
  events, so subagent sequenceNum counters only advance on tool_use —
  not shared across text/reasoning/tool_use the way the primary session
  counter is.

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

Addressed in `ce225d5`:

  • process.exit(1) truncating NDJSON — swapped both error paths to `process.exitCode = 1` so stdout drains before exit. Good catch — on a pipe this was a real risk of losing the session_error / session_complete lines. Exit code still non-zero for shell callers.
  • sequenceNum doc for subagents — added a note on all three bullets (text / reasoning / tool_use) that subagent sessions only emit tool_use in JSON mode, so their counters only advance on tool_use. The "shared across three types" claim is true for the primary session only.

Skipping:

  • Empty-patterns granted — same reasoning as before: if patterns: [], no permission was actually checked, so no event to emit. If we ever hit this in practice and consumers need symmetry, happy to revisit.
  • extractStatus accepting arbitrary digits — agreed low-risk; not clamping for now.

67/67 green.

@byapparov byapparov merged commit 7aec2b8 into main Apr 23, 2026
5 checks passed
@byapparov byapparov mentioned this pull request Apr 23, 2026
2 tasks
@byapparov byapparov deleted the feature/ndjson-schema-gaps branch April 23, 2026 21:05
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.

NDJSON event schema gaps for downstream observability consumers

1 participant