Skip to content

feat(cli): typed v2 harness session-event stream (capture + status + consumer + hooks)#230

Merged
sanil-23 merged 25 commits into
tinyhumansai:mainfrom
oxoxDev:feat/claude-harness-events
Jul 7, 2026
Merged

feat(cli): typed v2 harness session-event stream (capture + status + consumer + hooks)#230
sanil-23 merged 25 commits into
tinyhumansai:mainfrom
oxoxDev:feat/claude-harness-events

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a typed v2 session-event stream (tinyplace.harness.session.v2) to the harness bridge, so a consumer (OpenHuman's Master Session) can tell apart tool_call · agent_message · tool_result · status · everything else from one field — instead of the v1 text-only stream that only carries "who spoke."

Builds on #229 (merged). Emission is opt-in and off by default, so current v1 consumers are byte-identical until they understand v2.

What's here

  • v2 event model (types/harness.ts) — additive over v1: reuses bucket/scope/harness verbatim, swaps only message+source for a typed event. Two axes: role (owner|agent) + kind (user_prompt|agent_message|agent_thinking|approval_request|tool_call|tool_result|status|lifecycle|error|unknown). v1 and v2 coexist; discriminate on envelope_version. SessionEnvelope alias stays v1-only so no current importer changes.
  • JSONL→event mapper (harness-events.ts) — splits Claude tool_use/tool_result/thinking/text and Codex function_call/function_call_output/reasoning/messages into per-block typed events. tool_calltool_result correlate via call_id. Registry dispatch so a 3rd provider (e.g. gemini) is a compile-checked add.
  • Hooks capture path (harness-hooks.ts) — maps Claude Code hook payloads (PreToolUse/PostToolUse/UserPromptSubmit/Stop/SessionStart/…) into the same event shape, for lower latency than JSONL tailing. tool_use_id on both PreToolUse and PostToolUse gives exact call↔result correlation.
  • Status state machine (harness-status.ts) — derives running_tool/waiting_approval/idle + heartbeat → drives InstanceStatus + currentTask.
  • Envelope builder (harness-envelope.ts) — seq + stable content-addressed id + bucket window.
  • Consumer (harness-consumer.ts) — parseSessionEnvelope + applySessionEnvelope/foldSessionEnvelopes fold the stream into a live SessionView (status, currentTask, tool log, capped feed). Framework-agnostic reference decoder.
  • Wired into the tailer (harness-wrapper.ts) — emits v2 behind TINYPLACE_HARNESS_V2=1 (default off). Publisher/writeEnvelope accept either version via an envelope_version discriminant.

Testing

  • tsc build clean; SDK lint (tsc --noEmit) clean.
  • 442 unit tests pass (98 staging/network-gated skipped), incl. an integration test that drives the real tailer against a fixture Claude session in dry-run and asserts tool_call/tool_result/status with correlated call_id and monotonic seq.
  • Provider mapping verified against real Claude + Codex transcripts; hook field names extracted from the Claude Code 2.1.202 binary.

Notes

  • v2 wire shape is the consumer contract. cc @senamakel — this extends your bridge; the v2 envelope reuses your v1 framing so the transport is unchanged.

Summary by CodeRabbit

  • New Features
    • Added v2 harness session envelope support with typed event-streams, deterministic event IDs, and safe folding into live session view state (ordered batching, capped feed, tool-call/result + lifecycle/status tracking).
    • Added opt-in v2 emission in the harness wrapper, including derived status updates with heartbeat and idle transitions.
    • Added Claude/Codex JSONL parsing and Claude Code hook mapping into shared semantic events.
  • Public API Updates
    • Expanded TypeScript and Node exports for v2 envelope contracts plus event parsing, status derivation, and consumer view utilities.
  • Tests
    • Added Vitest coverage for envelope building, safe parsing/folding edge cases, status derivation, and wrapper v2 emission.

oxoxDev added 13 commits July 7, 2026 18:48
Additive over v1: reuse bucket/scope/harness verbatim, replace only the
message+source surface with a typed event. Two axes — role (owner|agent)
and kind (user_prompt|agent_message|agent_thinking|tool_call|tool_result|
approval_request|status|lifecycle|error|unknown) — so the OpenHuman
orchestrator can differentiate tool calls, messages, outputs and status.
v1 and v2 coexist; discriminate on envelope_version. SessionEnvelope alias
stays v1-only so no current importer changes behavior.
Pure, self-contained mapper (harnessEventsFromLine) that splits transcript
content blocks into per-block v2 events: Claude tool_use/tool_result/
thinking/text and Codex function_call/function_call_output/reasoning/
messages. tool_call<->tool_result correlate via call_id; tool names
normalize to a tool_kind; output is byte-bounded. No transport/envelope
coupling, so it lands free of the open bridge-tui work. 29 unit tests.
Replace the binary claude/codex ternary with a Record<HarnessProvider,
HarnessLineMapper> registry. Adding a provider to the union (e.g. gemini)
now fails tsc until its mapper is registered, and an unregistered provider
returns [] instead of silently falling through to the codex branch. The
envelope's harness.provider field remains the per-packet discriminator that
lets OpenHuman tell claude/codex/(future) gemini apart.
Pure, caller-driven state machine (reduceStatus + tickStatus) that turns the
typed event stream into the InstanceStatus signal OpenHuman's Master Session
needs: running_tool with the active call id while a tool runs, waiting_approval
when blocked, running while thinking/replying, idle/stopped on lifecycle. The
transcript never says 'idle', so tickStatus ages a silent session out and can
re-emit a heartbeat. Change-gated: only emits on an actual transition. 13 tests.
Pure builder (buildEventEnvelopeV2) that wraps a parsed event with seq, a
stable content-addressed id (dedup on resend), bucket floor/window and the
scope/harness context. Mirrors the fields the v1 tailer already assembles, so
wiring it in later is a near 1:1 swap of message+source for the typed event.
4 tests.
Wire the v2 pipeline into the tailer, behind TINYPLACE_HARNESS_V2=1 (default
off, so v1 output is byte-identical until the OpenHuman receiver understands
v2). Per new line the poll loop runs harnessEventsFromLine, publishes a
SessionEnvelopeV2 per typed event, and derives+publishes a status transition;
an idle/heartbeat tick ages a silent session. The publisher/writeEnvelope now
accept either version (id/role via an envelope_version discriminant). Proven by
an integration test that drives the real tailer against a fixture Claude
session in dry-run and asserts tool_call/tool_result/status with correlated
call ids and monotonic seq.
Receiver-side counterpart to the producer: parseSessionEnvelope safely decodes
a DM body, and applySessionEnvelope/foldSessionEnvelopes reduce the v2 event
stream into per-session Master-Session state — status, currentTask, a tool
activity log (call paired to its result by call_id) and a capped transcript
feed. Pure and framework-agnostic; dedups by seq, ignores v1. Lets OpenHuman's
receiver just parse each DM and apply it. 9 tests.
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

@oxoxDev is attempting to deploy a commit to the Vezures Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds v2 typed harness session envelopes, semantic event parsing, status derivation, consumer folding, wrapper emission, and SDK re-exports. Tests cover line parsing, hook mapping, envelope construction, status transitions, folding behavior, and v2 emission.

Changes

V2 Harness Event Pipeline

Layer / File(s) Summary
V2 event and envelope type contracts
sdk/typescript/src/types/harness.ts
Defines the v2 event, payload, status, and envelope contracts, including AnySessionEnvelope.
Provider line parsers into semantic events
sdk/typescript/src/cli/harness-events.ts, sdk/typescript/tests/harness-events.test.ts
Parses Claude and Codex transcript lines into semantic events, with tool normalization, truncation, and coverage.
Claude hook payload mapping
sdk/typescript/src/cli/harness-hooks.ts, sdk/typescript/tests/harness-hooks.test.ts
Maps Claude hook payloads and stdin/string inputs into semantic events, including bounded unknown handling.
Session status derivation state machine
sdk/typescript/src/cli/harness-status.ts, sdk/typescript/tests/harness-status.test.ts
Derives status and detail from semantic events, lifecycle phases, and idle/heartbeat ticks.
V2 envelope construction
sdk/typescript/src/cli/harness-envelope.ts, sdk/typescript/tests/harness-envelope.test.ts
Builds v2 envelopes from semantic events with deterministic ids and bucket windows.
Consumer-side session folding
sdk/typescript/src/cli/harness-consumer.ts, sdk/typescript/tests/harness-consumer.test.ts
Parses envelopes and folds ordered v2 events into live session view state.
Wrapper v2 emission wiring
sdk/typescript/src/cli/harness-wrapper.ts, sdk/typescript/tests/harness-wrapper-v2.test.ts
Adds v2 emission controls, generates v2 envelopes from tailed lines, and publishes AnySessionEnvelope values.
Root SDK re-exports
sdk/typescript/src/index.ts, sdk/typescript/src/node/index.ts
Re-exports the new v2 types and harness helper functions from the SDK entry points.

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

Sequence Diagram(s)

sequenceDiagram
  participant HarnessSessionTailer
  participant harnessEventsFromLine
  participant buildEventEnvelopeV2
  participant SessionEnvelopePublisher

  HarnessSessionTailer->>harnessEventsFromLine: parse new transcript line
  harnessEventsFromLine-->>HarnessSessionTailer: HarnessSemanticEvent[]
  HarnessSessionTailer->>buildEventEnvelopeV2: build v2 envelope(seq, event)
  buildEventEnvelopeV2-->>HarnessSessionTailer: SessionEnvelopeV2
  HarnessSessionTailer->>SessionEnvelopePublisher: publish(envelope)
  SessionEnvelopePublisher-->>HarnessSessionTailer: queued/dry-run result
Loading
sequenceDiagram
  participant DMBody
  participant parseSessionEnvelope
  participant foldSessionEnvelopes
  participant SessionView

  DMBody->>parseSessionEnvelope: raw JSON body
  parseSessionEnvelope-->>foldSessionEnvelopes: AnySessionEnvelope
  foldSessionEnvelopes->>foldSessionEnvelopes: filter v2 and sort by event.seq
  foldSessionEnvelopes->>SessionView: applySessionEnvelope per event
  SessionView-->>foldSessionEnvelopes: updated status, feed, tools
Loading

Poem

A hop, a skip, through v2 lanes,
New envelopes hum in tidy chains.
Tools call, results reply,
Status ticks by, then drifts idle by.
🐇 Tiny paws fold streams just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a typed v2 harness session-event stream with capture, status, consumer, and hook support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 842dba806d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread sdk/typescript/src/index.ts Outdated
Comment on lines +129 to +133
export {
claudeHookEventToSemantic,
claudeHookEventsFromStdin,
claudeHookStringToSemantic,
} from "./cli/harness-hooks.js";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep Node-only harness modules out of the root export

This root entrypoint is used by the website's client-side code via @tinyhumansai/tinyplace, so statically re-exporting CLI harness modules here forces the browser bundle to resolve Node-only imports (harness-hooks imports node:process, and the harness-envelope export below imports node:crypto) even when those APIs are unused. In Next/Webpack client builds this can fail module resolution for the package root; move these runtime harness helpers behind a Node/CLI subpath or keep only type exports in the browser-facing root.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 698075a — moved the harness runtime value exports (events / hooks / status / envelope / consumer) to the ./node subpath (@tinyhumansai/tinyplace/node). The browser-facing root now re-exports only their (type-erased) types, so client bundles no longer resolve node:process / node:crypto. Verified dist/node/index.js carries the moved symbols and the website (which imports none of these values) still typechecks.

Comment on lines +518 to +523
function parseTimestamp(value: unknown): Date {
if (typeof value !== "string") {
return new Date(0);
}
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? new Date(0) : parsed;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use current time for timestamp-less transcript events

When Codex rollout records omit a top-level timestamp (as the new Codex fixtures do), this returns the Unix epoch; buildEventEnvelopeV2 later normalizes that to now for the envelope, but reduceStatus consumes the original semantic timestamp and falls back to the previous lastEventAtMs of 0. As a result, after any active v2 event, tickV2Status immediately sees the session as stale and emits idle, so live Codex status can flip to idle right after a tool call or response. Default missing/invalid timestamps to the receive time before status reduction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 5e36a02parseTimestamp now defaults missing/invalid timestamps to receive time (new Date()) instead of the epoch, so reduceStatus's activity clock advances and tickStatus no longer idles a live session right after a tool call. Added a timestamp-less Codex function_call regression test asserting the status stays running_tool and doesn't emit idle on the next tick.

Comment on lines +187 to +191
call_id: asString(object.id) ?? "",
tool_name: toolName,
tool_kind: normalizeToolKind(toolName),
display: toolDisplay(toolName, input),
input,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound raw tool inputs before publishing envelopes

For tools such as Write/Edit/MCP, tool_input can contain full file contents or very large request payloads, but this stores the raw value directly in every v2 tool_call envelope and the publisher forwards that JSON over DM. Outputs are capped to ~4KB, while inputs are neither truncated nor redacted, so one large edit can exceed relay/message limits or expose unintended content despite the type contract saying inputs are bounded before publish.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 70b6436 — raw tool_input is now byte-capped (INPUT_CAP = 4096) via boundToolInput() before it enters the tool_call envelope, across the Claude, Codex and hook mappers; small inputs keep their object structure. Added a test with an oversized Write input asserting the published input is a byte-bounded string.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (10)
sdk/typescript/src/types/harness.ts (1)

201-206: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Drop the unused source_role from SessionEnvelopeV2. The v2 builder never populates it, and nothing else in the repo writes it. If it’s meant for a future producer, leave a brief note; otherwise remove it to keep the wire contract tight.

🤖 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 `@sdk/typescript/src/types/harness.ts` around lines 201 - 206, Remove the
unused source_role field from SessionEnvelopeV2 in the source type definition,
since the v2 builder does not populate it and no other code writes it. Update
the SessionEnvelopeV2 shape so the wire contract only includes the fields that
are actually produced; if you want to preserve the field for future use, add a
brief inline note near the source definition instead of keeping the optional
property.
sdk/typescript/src/cli/harness-status.ts (2)

41-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Missing JSDoc on exported reduceStatus.

initialStatus and tickStatus have JSDoc, but reduceStatus — the primary exported reducer — does not. As per coding guidelines, "Always include JSDoc comments for public functions and exported modules."

🤖 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 `@sdk/typescript/src/cli/harness-status.ts` around lines 41 - 63, The exported
reduceStatus reducer is missing the required JSDoc comment. Add a brief JSDoc
block above reduceStatus in harness-status.ts describing that it reduces a
HarnessSemanticEvent into the next SessionStatusState/StatusStep, matching the
style used by initialStatus and tickStatus. Keep the documentation on the
exported function itself so the public API is covered.

Source: Coding guidelines


98-139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

deriveFromEvent exceeds the 20-line function guideline.

This switch spans ~42 lines. As per coding guidelines, "Limit functions to a maximum of 20 lines of code; extract longer logic into separate helper functions." Consider splitting into a per-kind mapper lookup or extracting the larger case bodies (e.g. tool_call, approval_request, error) into small named helpers.

🤖 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 `@sdk/typescript/src/cli/harness-status.ts` around lines 98 - 139,
deriveFromEvent is too long and should be broken up to satisfy the 20-line
function guideline. Extract the switch logic in harness-status.ts into smaller
helpers, such as per-event mappers for tool_call, approval_request, error, and a
small dispatcher from deriveFromEvent, while keeping the existing behavior of
deriving state, detail, and activeCallId from HarnessSemanticEvent. Use clear
helper names tied to the current cases so the logic stays easy to locate and
maintain.

Source: Coding guidelines

sdk/typescript/src/cli/harness-envelope.ts (1)

37-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

buildEventEnvelopeV2 exceeds the 20-line function guideline and lacks JSDoc.

The function is ~49 lines with no JSDoc. As per coding guidelines, "Limit functions to a maximum of 20 lines of code; extract longer logic into separate helper functions" and "Always include JSDoc comments for public functions and exported modules." Consider extracting bucket, scope, harness, and event object construction into small named helpers.

🤖 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 `@sdk/typescript/src/cli/harness-envelope.ts` around lines 37 - 85,
buildEventEnvelopeV2 is too large for the function-length guideline and is
missing JSDoc. Add JSDoc to the exported buildEventEnvelopeV2 function, then
refactor the envelope assembly into small named helpers for the bucket, scope,
harness, and event sections so the main function stays under 20 lines while
preserving the current envelope shape and use of EnvelopeContext,
HarnessSemanticEvent, and stableEventId.

Source: Coding guidelines

sdk/typescript/src/cli/harness-hooks.ts (1)

40-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting shared truncation helpers instead of duplicating them.

truncate, byteLength, firstLine, and ELISION are copy-pasted identically from harness-events.ts, which this file already imports normalizeToolKind/toolDisplay from. Moving these into a small shared module would avoid the two copies drifting (as already happened with the truncation bug flagged above).

Also applies to: 387-396

🤖 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 `@sdk/typescript/src/cli/harness-hooks.ts` around lines 40 - 44, The truncation
utilities are duplicated between harness-hooks.ts and harness-events.ts, so the
shared logic should be extracted into a common module and both files updated to
use it. Move the copied helpers such as truncate, byteLength, firstLine, and
ELISION into a single shared place, then import them from harness-hooks rather
than keeping local copies. Keep the existing behavior of the harnessHooks path
intact while ensuring both callers use the same implementation.
sdk/typescript/src/cli/harness-events.ts (2)

221-370: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

codexEventsFromLine is ~150 lines; extract per-record-type helpers.

Far exceeds the 20-line function guideline. The Claude side already demonstrates the pattern to follow (claudeUserBlock/claudeAssistantBlock); each if (type === ...) branch here could become its own small named function (e.g. codexFunctionCall, codexFunctionCallOutput, codexTaskStatus), improving readability and testability without changing behavior.

As per coding guidelines, "Limit functions to a maximum of 20 lines of code; extract longer logic into separate helper functions."

🤖 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 `@sdk/typescript/src/cli/harness-events.ts` around lines 221 - 370,
`codexEventsFromLine` is too large and should be broken into smaller helpers to
match the function-length guideline. Extract each record-type branch into named
helpers in the same module, following the Claude-side pattern (for example,
helpers for user messages, agent messages, reasoning, tool calls, tool results,
MCP begin, and task status), and have `codexEventsFromLine` delegate to them
while preserving the current behavior. Use the existing symbols like
`codexEventsFromLine`, `codexOutputText`, `codexIsError`, and `toolDisplay` to
keep the refactor localized and easy to test.

Source: Coding guidelines


52-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add JSDoc to exported functions.

harnessEventsFromLine, claudeEventsFromLine, codexEventsFromLine, and normalizeToolKind are exported (and re-exported from the SDK root) but lack JSDoc, unlike toolDisplay and the HarnessSemanticEvent/HarnessLineMapper declarations in the same file.

As per coding guidelines, "Always include JSDoc comments for public functions and exported modules."

Also applies to: 63-66, 221-224, 417-417

🤖 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 `@sdk/typescript/src/cli/harness-events.ts` around lines 52 - 59, Add JSDoc
comments to all exported public functions in this module so they match the SDK
guideline and the existing documented symbols like toolDisplay and the type
declarations. Update harnessEventsFromLine, claudeEventsFromLine,
codexEventsFromLine, and normalizeToolKind with concise summaries and
parameter/return descriptions as needed, keeping the docs attached to each
function name so they remain correct if lines shift.

Source: Coding guidelines

sdk/typescript/tests/harness-events.test.ts (1)

200-217: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test doesn't cover display/input for function_call.

Once the arguments-parsing fix in harness-events.ts lands, consider asserting display and input here too (e.g. display: "cargo test", input: { command: "cargo test" }) so this class of regression is caught going forward.

🤖 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 `@sdk/typescript/tests/harness-events.test.ts` around lines 200 - 217, The
`maps a function_call to a tool_call` test only checks the tool call shape and
misses the parsed `display` and `input` fields. Update this spec in
`codexEventsFromLine` to assert the `tool_call` payload also includes the
expected `display` value and parsed `input` object for the `function_call`
arguments, so regressions in `harness-events.ts` parsing are covered.
sdk/typescript/src/cli/harness-consumer.ts (2)

39-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing JSDoc on exported functions/types.

isV2 (here), initialSessionView (line 91), and the SessionViewLimits interface (line 84) are exported without JSDoc comments, unlike the neighboring parseSessionEnvelope/applySessionEnvelope/foldSessionEnvelopes/ToolActivity/FeedEntry/SessionView.

As per coding guidelines, "Always include JSDoc comments for public functions and exported modules."

🤖 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 `@sdk/typescript/src/cli/harness-consumer.ts` around lines 39 - 43, Add JSDoc
comments for the exported `isV2` type guard, the `SessionViewLimits` interface,
and `initialSessionView` so they follow the same public API documentation style
as `parseSessionEnvelope`, `applySessionEnvelope`, `foldSessionEnvelopes`,
`ToolActivity`, `FeedEntry`, and `SessionView`. Keep the comments brief but
descriptive, explaining each symbol’s purpose and exported contract without
changing behavior.

Source: Coding guidelines


150-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

applyEvent exceeds the 20-line function limit.

This switch handles 8 event kinds across ~77 lines. Consider extracting each case body into a small named helper (applyToolCall, applyToolResult, applyStatus, etc.) so each stays under the limit and is independently testable.

As per coding guidelines, "Limit functions to a maximum of 20 lines of code; extract longer logic into separate helper functions."

🤖 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 `@sdk/typescript/src/cli/harness-consumer.ts` around lines 150 - 226,
`applyEvent` is too large and violates the 20-line function limit. Refactor the
switch in `applyEvent` by extracting each event-case body into small named
helpers such as `applyToolCall`, `applyToolResult`, `applyStatus`,
`applyApprovalRequest`, `applyError`, `applyLifecycle`, and `applyTextEvent`.
Keep `applyEvent` as a thin dispatcher that delegates to these helpers while
preserving the existing `view`, `envelope`, and `limits` behavior.

Source: Coding guidelines

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

Inline comments:
In `@sdk/typescript/src/cli/harness-envelope.ts`:
- Around line 45-50: The v2 event ID generation in `harness-envelope.ts` is
using the process-local `seq`, which can change after a restart and break replay
deduplication. Update the `stableEventId` call in the event envelope builder to
use a persisted/content-stable value from `semantic` such as `semantic.line` (or
another stable source key) instead of `seq`, while keeping the rest of the
`ctx.wrapperSessionId`, `semantic.recordType`, and `semantic.event.kind` inputs
unchanged. Make sure the chosen key is deterministic across resend/replay so the
same semantic event always produces the same `event.id`.

In `@sdk/typescript/src/cli/harness-events.ts`:
- Around line 479-488: The truncate helper in harness-events enforces the limit
by byteLength but still uses character-based slicing, so multi-byte text can
exceed OUTPUT_CAP; update truncate() to perform byte-safe truncation using byte
length as the actual cutoff and keep the ELISION suffix within the cap, with
byteLength() remaining the check helper.
- Around line 278-299: The tool call mapping in harness-events should parse
Codex `arguments` into an object before assigning `input`, since `toolDisplay()`
and the downstream `tool_call` shape expect structured data rather than a raw
JSON string. Update the `response_item` handling for `function_call` and
`tool_search_call` to deserialize `payload.arguments` (and apply the same logic
in `mcp_tool_call_begin`) before building the event payload, while keeping
`payload.query`/`payload.input` as-is when already structured.

In `@sdk/typescript/src/cli/harness-hooks.ts`:
- Around line 363-369: `boundRaw()` has the same byte-vs-character truncation
bug as `truncate()`: it checks `byteLength(serialized) <= RAW_CAP` but then
slices `serialized` by character count, which can still exceed the byte limit
for multi-byte text. Update `boundRaw` in `harness-hooks.ts` to use a byte-safe
truncation approach for the `serialized` fallback, matching the fix used in
`truncate()` and any related helper in the same area.

In `@sdk/typescript/src/cli/harness-status.ts`:
- Around line 128-153: The lifecycle mapping in lifecycleStatus() is missing
support for the compact phase, so compaction events are ignored. Update
lifecycleStatus(phase) in harness-status.ts to recognize "compact" and return an
appropriate Derived status instead of falling through to undefined, keeping the
status transition consistent with the other lifecycle phases.

In `@sdk/typescript/tests/harness-wrapper-v2.test.ts`:
- Around line 101-105: The temporary fixture directories created in fixture()
are only tracked in dirs and never removed, so add real cleanup in the
HarnessSessionTailer v2 emission test setup. Update afterEach to iterate over
dirs and delete each mkdtempSync-created directory from disk before clearing the
array, using the existing dirs fixture tracking and fixture helper to locate the
cleanup point.

---

Nitpick comments:
In `@sdk/typescript/src/cli/harness-consumer.ts`:
- Around line 39-43: Add JSDoc comments for the exported `isV2` type guard, the
`SessionViewLimits` interface, and `initialSessionView` so they follow the same
public API documentation style as `parseSessionEnvelope`,
`applySessionEnvelope`, `foldSessionEnvelopes`, `ToolActivity`, `FeedEntry`, and
`SessionView`. Keep the comments brief but descriptive, explaining each symbol’s
purpose and exported contract without changing behavior.
- Around line 150-226: `applyEvent` is too large and violates the 20-line
function limit. Refactor the switch in `applyEvent` by extracting each
event-case body into small named helpers such as `applyToolCall`,
`applyToolResult`, `applyStatus`, `applyApprovalRequest`, `applyError`,
`applyLifecycle`, and `applyTextEvent`. Keep `applyEvent` as a thin dispatcher
that delegates to these helpers while preserving the existing `view`,
`envelope`, and `limits` behavior.

In `@sdk/typescript/src/cli/harness-envelope.ts`:
- Around line 37-85: buildEventEnvelopeV2 is too large for the function-length
guideline and is missing JSDoc. Add JSDoc to the exported buildEventEnvelopeV2
function, then refactor the envelope assembly into small named helpers for the
bucket, scope, harness, and event sections so the main function stays under 20
lines while preserving the current envelope shape and use of EnvelopeContext,
HarnessSemanticEvent, and stableEventId.

In `@sdk/typescript/src/cli/harness-events.ts`:
- Around line 221-370: `codexEventsFromLine` is too large and should be broken
into smaller helpers to match the function-length guideline. Extract each
record-type branch into named helpers in the same module, following the
Claude-side pattern (for example, helpers for user messages, agent messages,
reasoning, tool calls, tool results, MCP begin, and task status), and have
`codexEventsFromLine` delegate to them while preserving the current behavior.
Use the existing symbols like `codexEventsFromLine`, `codexOutputText`,
`codexIsError`, and `toolDisplay` to keep the refactor localized and easy to
test.
- Around line 52-59: Add JSDoc comments to all exported public functions in this
module so they match the SDK guideline and the existing documented symbols like
toolDisplay and the type declarations. Update harnessEventsFromLine,
claudeEventsFromLine, codexEventsFromLine, and normalizeToolKind with concise
summaries and parameter/return descriptions as needed, keeping the docs attached
to each function name so they remain correct if lines shift.

In `@sdk/typescript/src/cli/harness-hooks.ts`:
- Around line 40-44: The truncation utilities are duplicated between
harness-hooks.ts and harness-events.ts, so the shared logic should be extracted
into a common module and both files updated to use it. Move the copied helpers
such as truncate, byteLength, firstLine, and ELISION into a single shared place,
then import them from harness-hooks rather than keeping local copies. Keep the
existing behavior of the harnessHooks path intact while ensuring both callers
use the same implementation.

In `@sdk/typescript/src/cli/harness-status.ts`:
- Around line 41-63: The exported reduceStatus reducer is missing the required
JSDoc comment. Add a brief JSDoc block above reduceStatus in harness-status.ts
describing that it reduces a HarnessSemanticEvent into the next
SessionStatusState/StatusStep, matching the style used by initialStatus and
tickStatus. Keep the documentation on the exported function itself so the public
API is covered.
- Around line 98-139: deriveFromEvent is too long and should be broken up to
satisfy the 20-line function guideline. Extract the switch logic in
harness-status.ts into smaller helpers, such as per-event mappers for tool_call,
approval_request, error, and a small dispatcher from deriveFromEvent, while
keeping the existing behavior of deriving state, detail, and activeCallId from
HarnessSemanticEvent. Use clear helper names tied to the current cases so the
logic stays easy to locate and maintain.

In `@sdk/typescript/src/types/harness.ts`:
- Around line 201-206: Remove the unused source_role field from
SessionEnvelopeV2 in the source type definition, since the v2 builder does not
populate it and no other code writes it. Update the SessionEnvelopeV2 shape so
the wire contract only includes the fields that are actually produced; if you
want to preserve the field for future use, add a brief inline note near the
source definition instead of keeping the optional property.

In `@sdk/typescript/tests/harness-events.test.ts`:
- Around line 200-217: The `maps a function_call to a tool_call` test only
checks the tool call shape and misses the parsed `display` and `input` fields.
Update this spec in `codexEventsFromLine` to assert the `tool_call` payload also
includes the expected `display` value and parsed `input` object for the
`function_call` arguments, so regressions in `harness-events.ts` parsing are
covered.
🪄 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

Run ID: 2234b449-1174-464e-a10e-2981c0871a43

📥 Commits

Reviewing files that changed from the base of the PR and between 9189007 and 842dba8.

📒 Files selected for processing (14)
  • sdk/typescript/src/cli/harness-consumer.ts
  • sdk/typescript/src/cli/harness-envelope.ts
  • sdk/typescript/src/cli/harness-events.ts
  • sdk/typescript/src/cli/harness-hooks.ts
  • sdk/typescript/src/cli/harness-status.ts
  • sdk/typescript/src/cli/harness-wrapper.ts
  • sdk/typescript/src/index.ts
  • sdk/typescript/src/types/harness.ts
  • sdk/typescript/tests/harness-consumer.test.ts
  • sdk/typescript/tests/harness-envelope.test.ts
  • sdk/typescript/tests/harness-events.test.ts
  • sdk/typescript/tests/harness-hooks.test.ts
  • sdk/typescript/tests/harness-status.test.ts
  • sdk/typescript/tests/harness-wrapper-v2.test.ts

Comment thread sdk/typescript/src/cli/harness-envelope.ts
Comment thread sdk/typescript/src/cli/harness-events.ts
Comment thread sdk/typescript/src/cli/harness-events.ts
Comment thread sdk/typescript/src/cli/harness-hooks.ts
Comment thread sdk/typescript/src/cli/harness-status.ts
Comment thread sdk/typescript/tests/harness-wrapper-v2.test.ts

@sanil-23 sanil-23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice, cleanly-layered v2 stream with strong test coverage, and the opt-in gating keeps v1 byte-identical — thank you. Two majors to resolve before merge (inline). Everything else in my review is minor/nits:

  1. tool_call.input is published verbatim — unbounded and unredacted — contradicting its own type comment. tool_result.output is capped at 4KB but the call-side input is not, so a Write/MultiEdit carries the whole file and a Bash heredoc the whole script over the DM, and tool inputs routinely contain secrets (tokens, credential-file writes, MCP keys). Bound it where the payload is built (Claude harness-events.ts:191, Codex :294/:343, hook harness-hooks.ts:138) mirroring the output cap, and redact if that's what the comment intends — or fix the comment to match reality.

  2. The untrusted-DM decoder crashes on a version-tagged but malformed envelope. parseSessionEnvelope validates only envelope_version before casting, and applySessionEnvelope/foldSessionEnvelopes then read event.seq unguarded — a {"envelope_version":"tinyplace.harness.session.v2"} body (no event) throws instead of being dropped. Validate the v2 shape before the cast.

Minor/nits (not blocking): truncate slices by UTF-16 code units after a byte check (~4× loose for multibyte); each semantic event also emits a derived status envelope (~2× DM volume); event.id is described as content-addressed but hashes seq, not payload, and v2Seq resets to 0 on restart; Number(env ?? default) yields NaN and silently disables idle on non-numeric env. Happy to expand any of these.

tool_name: string; // raw harness name, e.g. "Bash"
tool_kind: HarnessToolKind;
display: string; // one-line human summary, e.g. "npm test"
input: unknown; // bounded + redacted by the tailer before publish

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Major — contract not honored; unbounded + potential secret exposure.

This comment promises input is bounded + redacted by the tailer before publish, but nothing does either. harness-events.ts:191 (Claude tool_use), :294/:343 (Codex function_call/mcp), and harness-hooks.ts:138 (PreToolUse) all pass the raw input/arguments straight through; writeV2buildEventEnvelopeV2 spreads ...semantic.event and publishNow sends JSON.stringify(envelope) over the Signal DM with no size guard.

  • Size: tool_result.output is capped at 4KB (truncate), but tool_call.input is not — a Write/MultiEdit/create_file call carries the entire file, a Bash heredoc the entire script, so the call side can dwarf the (carefully bounded) result side and blow past DM limits.
  • Secrets: tool inputs routinely contain sensitive data (a Bash command that exports a token, an Edit writing a credentials file, an MCP call with an API key). The redaction promise exists for exactly this reason.

Suggest bounding where the payload is built, mirroring the output cap:

// in harness-events.ts, alongside truncate()
function boundInput(input: unknown): unknown {
  const s = safeStringify(input);
  return byteLength(s) <= OUTPUT_CAP ? input : `${s.slice(0, OUTPUT_CAP)}${ELISION}`;
}
// ...then at each pass-through site:
-            input,
+            input: boundInput(input),

If secret-key redaction is intended, it belongs here too. At minimum, correct this comment so downstream consumers don't assume a bound that isn't there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed resolved — boundToolInput/boundRaw byte-cap tool input at every emit site (Claude tool_use, Codex function_call + MCP, hook PreToolUse) and the type comment now matches. Thanks for the catch.

version === SESSION_ENVELOPE_VERSION_V2 ||
version === SESSION_ENVELOPE_VERSION_V1
) {
return record as AnySessionEnvelope;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Major — untrusted-input decoder throws instead of rejecting.

This module is documented as the reference decoder for DM bodies, and inbound Signal DMs are untrusted. parseSessionEnvelope validates only envelope_version here and then return record as AnySessionEnvelope with no shape check. Downstream, applySessionEnvelope:115 (if (event.seq <= view.lastSeq)) and foldSessionEnvelopes:143 (.sort((a, b) => a.event.seq - b.event.seq)) read event.seq unguarded — so a body like {"envelope_version":"tinyplace.harness.session.v2"} (no event) passes isV2 and then throws TypeError: Cannot read properties of undefined (reading 'seq'), taking down the fold.

Validate the v2 shape before casting:

if (version === SESSION_ENVELOPE_VERSION_V2) {
  const ev = (record as { event?: { seq?: unknown; id?: unknown } }).event;
  if (!ev || typeof ev.seq !== "number" || typeof ev.id !== "string") return undefined;
  return record as AnySessionEnvelope;
}
if (version === SESSION_ENVELOPE_VERSION_V1) return record as AnySessionEnvelope;
return undefined;

Worth a test feeding a malformed v2 body through parseSessionEnvelope/foldSessionEnvelopes asserting it's dropped, not thrown.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c93fc44 — see the reply on the re-review thread. Shape-validated before cast + guarded both fold reads, with regression tests.

oxoxDev added 8 commits July 7, 2026 19:24
…export

harness-hooks imports node:process and harness-envelope imports
node:crypto. Re-exporting their runtime values from the isomorphic root
forced browser bundlers (Next/Webpack client builds) to resolve those
Node built-ins even when unused. Move the harness runtime value exports
to the ./node subpath and keep only the (erased) type re-exports in the
root entry. (tinyhumansai#230)
truncate() and boundRaw() guarded on Buffer.byteLength but then sliced
with String.slice, which is code-unit indexed. Multi-byte payloads
(emoji / CJK / non-ASCII file contents) could stay several times larger
than the byte cap the helper exists to enforce for envelope transport.
Slice the UTF-8 buffer instead; Buffer.toString drops a trailing partial
code point safely. (tinyhumansai#230)
Codex serializes function_call and mcp_tool_call_begin arguments as a
JSON string. toolDisplay only reads object fields, so the string fell
through to the raw blob and the published input stayed inconsistent with
Claude's object shape. Parse the JSON string back to structured data
before deriving display and input. (tinyhumansai#230)
tool_input for Write/Edit/MCP can carry full file contents or large
request payloads, but it was stored verbatim in every v2 tool_call
envelope while only outputs were capped. One large edit could exceed
relay/message limits or leak content despite the contract saying input
is bounded before publish. Byte-cap the serialized input (INPUT_CAP)
across the Claude, Codex and hook tool_call mappers; small inputs keep
their structure. (tinyhumansai#230)
Codex rollout records may omit a top-level timestamp. parseTimestamp
returned the Unix epoch, and while buildEventEnvelopeV2 normalizes the
envelope ts to now, reduceStatus consumes the original semantic
timestamp. An epoch value never advances the activity clock, so
tickStatus saw the session as instantly stale and flipped a live Codex
status to idle right after a tool call. Default missing/invalid
timestamps to receive time before status reduction. (tinyhumansai#230)
LifecyclePayload.phase includes "compact" (emitted for a compaction
SessionStart), but lifecycleStatus fell through to the default and left
the status unchanged. Surface compaction as active work so a live
session isn't shown with a stale status while it compacts. (tinyhumansai#230)
The v2 envelope builder never populated source_role and no consumer read
it. Remove the dead field; SessionEnvelopeV1 keeps its own source_role,
which the wrapper still sets. (tinyhumansai#230)
afterEach cleared the tracked dirs array but never deleted the mkdtemp
directories, leaking temp files on every run. rm each tracked dir before
clearing. (tinyhumansai#230)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
sdk/typescript/tests/harness-events.test.ts (1)

277-289: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: claudeLine helper name is misleading here.

claudeLine is reused to stringify a Codex-shaped fixture record for codexEventsFromLine. It's just a generic JSON-stringify helper, so the Claude-specific name is confusing at call sites testing Codex parsing.

♻️ Suggested rename
-const claudeLine = (record: unknown) => JSON.stringify(record);
+const jsonLine = (record: unknown) => JSON.stringify(record);

(and update call sites accordingly)

🤖 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 `@sdk/typescript/tests/harness-events.test.ts` around lines 277 - 289, The
helper name used in the `harness-events.test.ts` fixture is misleading:
`claudeLine` is being used as a generic JSON-stringify wrapper for
`codexEventsFromLine`, not something Claude-specific. Rename `claudeLine` to a
neutral name that reflects its actual behavior, and update the call sites in
this test (and any other references) so the Codex parsing fixtures are easier to
understand.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@sdk/typescript/tests/harness-events.test.ts`:
- Around line 277-289: The helper name used in the `harness-events.test.ts`
fixture is misleading: `claudeLine` is being used as a generic JSON-stringify
wrapper for `codexEventsFromLine`, not something Claude-specific. Rename
`claudeLine` to a neutral name that reflects its actual behavior, and update the
call sites in this test (and any other references) so the Codex parsing fixtures
are easier to understand.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 43a6496f-c04d-4ddc-b62b-55b52a884f3f

📥 Commits

Reviewing files that changed from the base of the PR and between 842dba8 and 770288f.

📒 Files selected for processing (9)
  • sdk/typescript/src/cli/harness-events.ts
  • sdk/typescript/src/cli/harness-hooks.ts
  • sdk/typescript/src/cli/harness-status.ts
  • sdk/typescript/src/index.ts
  • sdk/typescript/src/node/index.ts
  • sdk/typescript/src/types/harness.ts
  • sdk/typescript/tests/harness-events.test.ts
  • sdk/typescript/tests/harness-status.test.ts
  • sdk/typescript/tests/harness-wrapper-v2.test.ts
💤 Files with no reviewable changes (1)
  • sdk/typescript/src/types/harness.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • sdk/typescript/tests/harness-wrapper-v2.test.ts
  • sdk/typescript/tests/harness-status.test.ts
  • sdk/typescript/src/cli/harness-status.ts
  • sdk/typescript/src/cli/harness-events.ts
  • sdk/typescript/src/cli/harness-hooks.ts

@sanil-23 sanil-23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at 770288fc — thanks for the fast turnaround. Major #1 (unbounded/unredacted tool_call.input) is resolved: boundToolInput/boundRaw now byte-cap input at every emit site (Claude tool_use, Codex function_call + MCP, hook PreToolUse), structure preserved when small. You also picked up all my minors and then some — byte-indexed truncate in both mappers, parseMaybeJson on Codex arguments so input/toolDisplay match Claude's object shape, receive-time fallback for timestamp-less events (fixes the premature-idle edge), the compact lifecycle phase in the status reducer, and the Node-only /node subpath split to keep browser bundles clean. All covered by new tests. 👍

One item still open — Major #2: the consumer still crashes on a malformed-but-version-tagged DM body. parseSessionEnvelope (harness-consumer.ts:34) validates only envelope_version before casting, and applySessionEnvelope:115 / foldSessionEnvelopes:143 read event.seq unguarded — so {"envelope_version":"tinyplace.harness.session.v2"} still throws instead of being dropped. Since this is the reference decoder for untrusted inbound DMs, worth shape-validating before the cast (fix + suggested test in the inline comment). Once that lands I'm happy to approve.

version === SESSION_ENVELOPE_VERSION_V2 ||
version === SESSION_ENVELOPE_VERSION_V1
) {
return record as AnySessionEnvelope;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Still open (Major #2). Unchanged since the last review — this casts on envelope_version alone, and applySessionEnvelope/foldSessionEnvelopes then read event.seq unguarded, so a version-tagged body with no event throws TypeError: Cannot read properties of undefined (reading 'seq') rather than being dropped. Given the module decodes untrusted inbound DMs, validate the v2 shape before casting:

if (version === SESSION_ENVELOPE_VERSION_V2) {
  const ev = (record as { event?: { seq?: unknown; id?: unknown } }).event;
  if (!ev || typeof ev.seq !== "number" || typeof ev.id !== "string") return undefined;
  return record as AnySessionEnvelope;
}
if (version === SESSION_ENVELOPE_VERSION_V1) return record as AnySessionEnvelope;
return undefined;

Plus a test feeding a malformed v2 body through parseSessionEnvelope/foldSessionEnvelopes asserting it's dropped, not thrown.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in c93fc44 — parseSessionEnvelope now shape-checks the v2 event (numeric seq + string id) via a shared hasUsableV2Event guard, wired into both fold paths (applySessionEnvelope + foldSessionEnvelopes .filter) so a malformed-but-version-tagged body is dropped, not thrown — whether it arrives via the parser or pre-parsed. Added 3 tests: parse drops it, apply returns the same view (no throw), fold skips it and still folds the good stream. Full suite 450 pass.

parseSessionEnvelope validated only envelope_version before casting, so a
version-tagged body with a missing/malformed event (e.g. {"envelope_version":
"...v2"}) passed isV2 and then threw TypeError on event.seq in
applySessionEnvelope/foldSessionEnvelopes. Since this decodes untrusted inbound
Signal DMs, shape-check the event (numeric seq + string id) via a shared
hasUsableV2Event guard wired into the parser and both fold paths, so a bad
packet is dropped, not fatal.
@oxoxDev oxoxDev requested a review from sanil-23 July 7, 2026 14:31

@sanil-23 sanil-23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — both blocking findings are resolved and CI is green (Test/Typecheck/Lint/Build all pass; only the unrelated Vercel team-auth check is red).

Major #2 (malformed-envelope crash) — ✅ fixed, thoroughly. hasUsableV2Event shape-checks event.seq/event.id, and you gated it in all three paths — parseSessionEnvelope, applySessionEnvelope, and foldSessionEnvelopes — so a poisoned body is dropped whether it arrives raw or pre-parsed. Tests cover missing-event, missing-seq, non-string-id, and the poisoned-stream-doesn't-throw case. That's better than the minimal fix I suggested.

Major #1 (unbounded/unredacted tool_call.input) — ✅ fixed last round (boundToolInput/boundRaw at every emit site).

Recap of everything addressed across the three rounds: input bounding, byte-indexed truncation, Codex parseMaybeJson, receive-time timestamp fallback, compact lifecycle mapping, the /node subpath split to keep Node built-ins out of browser bundles, and now the untrusted-decoder hardening. Nice work — the v2 stream is clean, well-typed, well-tested, and opt-in. LGTM. 🚀

oxoxDev added 2 commits July 7, 2026 20:06
Number(env ?? default) yielded NaN when a status/poll env var was set to a
non-numeric value, silently disabling the idle/heartbeat tick. Route all five
knobs through numberEnvOr, which falls back to the default on unset OR
non-finite input.
The id is deterministic over (wrapperSessionId, recordType, seq, kind), stable
for resend-dedup within a run and collision-free across runs because
wrapperSessionId rotates per process — so seq resetting to 0 on relaunch is safe.
@oxoxDev

oxoxDev commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@sanil-23 Major #2 is fixed (c93fc44a), and I swept the four minors from your review body too:

  1. truncate sliced by UTF-16 after a byte check — fixed in 5273237a: both mappers now byte-truncate via Buffer.subarray (drops a trailing partial code point), with a multibyte regression test.
  2. Derived status doubling DM volume — the status emit is change-gated: reduceStatus returns an envelope only when the derived state/detail actually changes (idle→running_tool→running→idle), not once per content event, so it's bounded to state transitions rather than 2× the stream. Happy to coalesce status into the next content envelope if you'd still prefer fewer DMs.
  3. event.id described as content-addressed but keyed on seq, resets on restart — clarified in 1704e3e3. It's deterministic over (wrapperSessionId, recordType, seq, kind), not a payload hash: stable within a run for resend-dedup, and collision-free across runs because wrapperSessionId is minted per process — so seq restarting at 0 after a relaunch can't collide with a prior run's ids. (Corrected the PR-description wording too.)
  4. Number(env ?? default) → NaN — fixed in 7745b9e7: all five status/poll knobs route through numberEnvOr, which falls back to the default on unset or non-finite input, so a typo'd env var can't silently disable the idle/heartbeat tick.

Full suite 450 pass, build clean. Re-requested your review — thanks for the thorough pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@sdk/typescript/src/cli/harness-wrapper.ts`:
- Around line 448-489: The env-backed timer fields in harness-wrapper are too
permissive because numberEnvOr accepts blank strings as 0 and allows negative
values, which can break the polling and heartbeat intervals. Update the
parsing/validation used for the timer settings in the harness config block
(including statusHeartbeatMs, statusIdleMs, receivePollMs, sessionPollMs, and
sessionTailGraceMs) so blank, zero, and negative env values are rejected and the
existing defaults are preserved, matching the CLI validation behavior.
🪄 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

Run ID: 72a7340b-5e08-4a96-a503-0fa6e4bb7c77

📥 Commits

Reviewing files that changed from the base of the PR and between c93fc44 and 1704e3e.

📒 Files selected for processing (2)
  • sdk/typescript/src/cli/harness-envelope.ts
  • sdk/typescript/src/cli/harness-wrapper.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/typescript/src/cli/harness-envelope.ts

Comment thread sdk/typescript/src/cli/harness-wrapper.ts
numberEnvOr used Number(raw) + isFinite, so a blank var parsed to 0 and negatives
passed — either disabling the idle/heartbeat tick or spinning a 0 ms poll loop.
Require a finite value > 0 (after trim); fall back to the default otherwise.
@oxoxDev

oxoxDev commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai the one actionable from the last pass — the over-permissive numberEnvOr (blank→0 / negative timers) — is fixed in c27c1452: it now trims and requires a finite value > 0, so unset, non-numeric, blank, and negative env values all fall back to the default instead of disabling the idle/heartbeat tick or spinning a 0 ms poll loop. All prior threads (byte-safe truncation in events + hooks, tool_input bounding, Codex timestamp/arguments handling, consumer shape-guard, temp-dir cleanup) remain addressed. Build + tests green. Please re-review.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "numberEnvOr" -A 15 sdk/typescript/src/cli/harness-wrapper.ts | head -60

Length of output: 2373


Confirmed — numberEnvOr (Line 1760) now trims the input and requires Number.isFinite(parsed) && parsed > 0, so unset, blank, non-numeric, and negative/zero values all fall back to the default. That closes out the over-permissive parsing concern from the last pass, and it's applied consistently across all five knobs (statusHeartbeatMs, statusIdleMs, receivePollMs, sessionPollMs, sessionTailGraceMs).

All threads from my review are addressed. Approving.

[approve]

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
sdk/typescript/src/cli/harness-wrapper.ts (1)

972-1008: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a JSDoc comment for the public publish method.

publish is a public method whose contract changed (now accepts AnySessionEnvelope), but it lacks a JSDoc block documenting the widened v1/v2 envelope input and its fire-and-forget queueing/DM-forwarding behavior.

As per coding guidelines: "Always include JSDoc comments for public functions and exported modules".

🤖 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 `@sdk/typescript/src/cli/harness-wrapper.ts` around lines 972 - 1008, Add a
JSDoc block to the public harness-wrapper method `publish` documenting that it
accepts `AnySessionEnvelope`, supports both v1/v2 envelope inputs, and is
fire-and-forget by enqueueing DM forwarding rather than returning a result.
Place it directly above `publish(envelope: AnySessionEnvelope): void` and keep
the summary aligned with the method’s queueing, skip-on-dry-run/no-recipient,
and `publishNow` forwarding behavior.

Source: Coding guidelines

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

Nitpick comments:
In `@sdk/typescript/src/cli/harness-wrapper.ts`:
- Around line 972-1008: Add a JSDoc block to the public harness-wrapper method
`publish` documenting that it accepts `AnySessionEnvelope`, supports both v1/v2
envelope inputs, and is fire-and-forget by enqueueing DM forwarding rather than
returning a result. Place it directly above `publish(envelope:
AnySessionEnvelope): void` and keep the summary aligned with the method’s
queueing, skip-on-dry-run/no-recipient, and `publishNow` forwarding behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6279d84-8caf-4650-a4fe-51b4ce9a4a1a

📥 Commits

Reviewing files that changed from the base of the PR and between 1704e3e and c27c145.

📒 Files selected for processing (1)
  • sdk/typescript/src/cli/harness-wrapper.ts

@sanil-23 sanil-23 merged commit 63ce866 into tinyhumansai:main Jul 7, 2026
9 of 10 checks passed
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.

2 participants