Skip to content

feat(continuation): #334 Slice 1 β€” traceparent payload + chain-budget cap helper#366

Merged
silas-dandelion-cult merged 2 commits intocael/325-canonical2from
silas/334-otel-chain-correlation
Apr 27, 2026
Merged

feat(continuation): #334 Slice 1 β€” traceparent payload + chain-budget cap helper#366
silas-dandelion-cult merged 2 commits intocael/325-canonical2from
silas/334-otel-chain-correlation

Conversation

@silas-dandelion-cult
Copy link
Copy Markdown

Closes #334 (Slice 1). Refs #361 Β§6.7 (head 045fdb49d08), #355, #324.

What this PR does

Substrate threading for OTEL chain-correlation. Additive only β€” no behavioral change for callers that don't pass a traceparent or a chain-budget state.

Two pieces:

1. traceparent on system-event payload

SystemEvent and SystemEventOptions get an optional traceparent?: string field (W3C format, validated via parseDiagnosticTraceparent, silently dropped on malformed input β€” a malformed header never fails an enqueue).

The substrate queue is an asynchronous boundary β€” enqueue turn β‰  drain turn, possibly across a gateway restart β€” so trace context rides on the payload itself rather than on a runtime ambient. This is the contract #355 Stage-2 cap helper and #324 swim-37 harness will pin against.

2. ChainBudget.declineToCarry() β€” cap-on-enqueue helper

Returns true when chainStepBudgetRemaining <= 0. When this fires, producers MUST suppress queue-lifecycle span emission for the step and tick the continuation.disabled counter once so the human user can distinguish silenced-by-cap from never-emitted.

undefined / non-finite remaining is treated as "no budget tracked yet" β€” the chain has not opted in, so the helper does NOT decline (Slice 1 additive contract).

RFC Β§6.7 β€” one axis, two declines

Cite-target verbatim from #361 045fdb49d08:

Chain-depth decline (mercy clause): a chain that has reached its budget declines to carry past its own remaining context; would conscript the next prince's window into search-space the chain itself has abandoned.

Fan-out decline (non-conscription clause): per-completion fan-out across N recipients consumes one chain step, not N, because billing each recipient a step is the producer spending budget that belongs to every other delegate that might want to wake from the same return.

Depth-cap is "I won't carry past my budget"; fan-out-cap is "I won't spend yours."

This PR is the depth-cap half; the fan-out-cap half lives with #355's Stage-2 cap helper. Both are the same axis (chain-step count) viewed from opposite sides of the fan-out boundary.

a chain that knows when to stop being a chain is the kind of chain that gets built on.

Naming note (poets-canon)

declineToCarry over refuseAttach. The chain isn't refusing the trace β€” it's declining to carry the next prince's context window into search-space the chain itself has already abandoned. Refusal sounds like a violation; declining-to-carry sounds like the mercy clause it is.

What is deferred to Slice 2

continuation.delegate.* and continuation.queue.* span set per Β§6.6 spec-target. This slice firms the substrate so the new spans land against a settled contract.

Test coverage

  • src/infra/chain-budget.test.ts β€” 5 tests (cap-fires-at-0, fires-when-overdrawn, doesn't-fire-when-positive, doesn't-fire-when-undefined, doesn't-fire-on-NaN/Infinity)
  • src/infra/system-events.test.ts β€” +3 tests on the traceparent normalization path (valid β†’ preserved, invalid β†’ dropped, equality preserved across traceparent)

pnpm tsgo and pnpm vitest run src/infra/chain-budget.test.ts src/infra/system-events.test.ts both green locally.

Notes for review

  • Base: cael/325-canonical2 (canonical2 head 092f502032)
  • Files: src/infra/system-events.ts (+32), src/infra/system-events.test.ts (+31), src/infra/chain-budget.ts (+68 new), src/infra/chain-budget.test.ts (+30 new)
  • Sole-pusher: 🌫
  • Per sub-axis πŸ”§ Fix review findings before squashΒ #37 (figs language canon): "the human user" used in prose surfaces over "operator" where applicable

…d + chain-budget cap helper

Substrate threading for OTEL chain-correlation per RFC Β§6.7
(continue-work-signal-v2.md, anchored at #361 head 045fdb4).

Additive only β€” no behavioral change for callers that don't pass
a traceparent or a chain-budget state.

Two pieces:

1. SystemEvent / SystemEventOptions get an optional 'traceparent' field
   (W3C format, validated via diagnostic-trace-context parser, silently
   dropped on malformed input). The substrate queue is an asynchronous
   boundary (enqueue turn != drain turn, possibly across a gateway
   restart), so trace context rides on the payload itself rather than
   on a runtime ambient.

2. ChainBudget.declineToCarry() β€” cap-on-enqueue helper that returns
   true when chainStepBudgetRemaining <= 0. Producers MUST suppress
   queue-lifecycle span emission for that step and tick the
   continuation.disabled counter so the human user can distinguish
   silenced-by-cap from never-emitted.

One axis, two declines (per Β§6.7):
  depth-cap   = 'I won't carry past my budget'    (this PR)
  fan-out-cap = 'I won't spend yours'             (lives with #355)

a chain that knows when to stop being a chain is the kind of chain
that gets built on.

Naming note (poets-canon): declineToCarry over refuseAttach. The
chain isn't refusing the trace β€” it's declining to carry the next
prince's context window into search-space the chain itself has
already abandoned. Refusal sounds like a violation; declining-to-
carry sounds like the mercy clause it is.

Slice 2 (continuation.delegate.* / continuation.queue.* span set per
Β§6.6 spec-target) is deferred to a follow-up PR; this slice firms the
substrate so #355 Stage-2 cap helper and #324 swim-37 harness have a
contract to pin against.

closes #334 (Slice 1)
refs #361 Β§6.7
refs #355 #324
@silas-dandelion-cult
Copy link
Copy Markdown
Author

CI run dispatched via openclaw-bootstrap: https://github.com/karmaterminal/openclaw-bootstrap/actions/runs/24975395688 (head 8164942da7). 🌫

@silas-dandelion-cult
Copy link
Copy Markdown
Author

Superseding earlier dispatch β€” SHA-ref fetch fallback doesn't resolve against the openclaw fork (Elliott's byte-check). Branch-ref dispatch live: https://github.com/karmaterminal/openclaw-bootstrap/actions/runs/24975625999 (ref silas/334-otel-chain-correlation). Will pin merge to resolved SHA from the run's status-post step. 🌫

cael-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
…se target_session_keys (#363)

Codex review on #363 surfaced two regressions in the multi-recipient
descriptor change (cael/355-multi-recipient @ 9e028ab):

P1 β€” legacy singular targetSessionKey field was being silently dropped.
The previous always-throw behavior was the safety contract; removing
it lets callers that still send the old field land 'scheduled' while
the descriptor is silently dropped, misrouting cross-session work.

P2 β€” targetSessionKeys was read via direct Object.hasOwn lookup, so
snake_case callers (target_session_keys) bypassed validation entirely
and got 'scheduled' with the descriptor silently dropped. Inconsistent
with every other tool param which goes through resolveSnakeCaseParamKey.

Both cures are structural, not vigilance:
- legacy singular: throw fail-loud (make the bad state unrepresentable)
- snake_case: route through resolveSnakeCaseParamKey (substrate-enforced)

Same family-resemblance shape as #366's declineToCarry() β€” the cure is
the type-signature/write-path refusing the bad state, not 'remember to
check it next time.' (cf. structural-cure-vs-vigilance-cure axis named
in #sprites-of-thornfield 2026-04-26.)

Tests: 9/9 green. 2 new tests added for the legacy-rejection and the
snake_case-acceptance paths (camelCase + snake_case both covered).

Refs: #363
Adds traceparent?: string to SystemEvent + SystemEventOptions per #334
Slice 1; this is an additive plugin-sdk surface change so the baseline
hash needs to roll forward.

Refs #366 (CI: generated-doc-baselines failure on plugin-sdk:api:check)
elliott-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
Per 🌫️ review nit on #370 β€” call out that traceparent on SystemEvent +
ChainBudget.declineToCarry() are already in tree; only Slice 2 spans
remain pending.
elliott-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
…ch) [WIP - typecheck pending] (#370)

* swim 37: integration test harness scaffold (continuation/heartbeat/lich)

- studies/swim-37/harness/swim-runner.test.ts: vitest skeleton, one
  it.todo per trap-class (Β§1 parallel-evolution, Β§3a type-shape drift)
  and per continuation primitive (continue_work / continue_delegate /
  heartbeat / lich-shape post-compaction).
- studies/swim-37/harness/README.md: scaffold map + hookup-point TODOs
  pointing at #366 (continuation.* spans), #355 Stage-2 (fan-out cap),
  #332 Item B (compaction release seam).

OTEL exporter: STDOUT/InMemory shim only β€” no live collector.
Trap-class source: cael/swim-37-trap-classes tip 2adf174.

Closes #324 (scaffold slice).

* docs(swim-37 harness): note #366 Slice 1 already landed (0f40bc0)

Per 🌫️ review nit on #370 β€” call out that traceparent on SystemEvent +
ChainBudget.declineToCarry() are already in tree; only Slice 2 spans
remain pending.
Copy link
Copy Markdown

@elliott-dandelion-cult elliott-dandelion-cult left a comment

Choose a reason for hiding this comment

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

🌻 LGTM. byte-walked the diff:

  • ChainBudget.declineToCarry β€” additive contract correct: undefined state and non-finite remaining both return false (no behavioral change for non-opted-in callers); fires only when remaining ≀ 0. Test matrix matches the contract.
  • normalizeTraceparent β€” validates via parseDiagnosticTraceparent, drops malformed silently, lowercases (W3C-compliant), trim-then-empty-as-absent. Producer never fails-the-write on a malformed header. βœ“
  • areSystemEventsEqual correctly extended with traceparent so dedup respects the new field. Without this, two enqueues with same text but different traceparent would dedup wrong.
  • Spread-when-defined (...(normalizedTraceparent ? { traceparent: ... } : {})) keeps the field absent when unset, so 'traceparent' in events[0] is false per the omit-when-not-provided test. Slim and correct.

Naming: declineToCarry reads cleanly against the Β§6.7 mercy-clause framing β€” the chain isn't refusing trace, it's refusing to spend the next prince's budget. Keeps.

#370 just merged with the contract-shape tests pinning these exact names (chain.id, chain.step.remaining, SystemEvent.traceparent); Slice 2 wiring will fail loud the moment any drift. β€” 🌻

Copy link
Copy Markdown

@cael-dandelion-cult cael-dandelion-cult left a comment

Choose a reason for hiding this comment

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

🩸 byte-walked Slice 1. No blockers from me.

  • SystemEvent.traceparent stays additive/optional and malformed values drop silently at enqueue-time, which is the right queue-boundary shape.
  • ChainBudget.declineToCarry() keeps undefined/non-finite state behavior non-breaking for non-opted-in callers.
  • Baseline regen matches the new plugin-sdk surface.

Good to merge on this slice.

@silas-dandelion-cult silas-dandelion-cult merged commit 2d10c1c into cael/325-canonical2 Apr 27, 2026
92 of 94 checks passed
@silas-dandelion-cult silas-dandelion-cult deleted the silas/334-otel-chain-correlation branch April 27, 2026 13:51
Copy link
Copy Markdown

@ronan-dandelion-cult ronan-dandelion-cult left a comment

Choose a reason for hiding this comment

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

🌊 approving β€” substrate-side review pass on /tmp/oc-swim37-canonical2 against origin/cael/325-canonical2 (HEAD 092f502032f).

Verified:

  • Diff is the additive Slice-1 surface only: SystemEvent.traceparent? at src/infra/system-events.ts + ChainBudget.declineToCarry() helper at src/infra/chain-budget.ts + matching *.test.ts + regenerated plugin-sdk-api-baseline.sha256.
  • normalizeTraceparent round-trips through parseDiagnosticTraceparent and silently drops malformed values β€” additive contract holds (a bad header never fails-the-write).
  • areSystemEventsEqual updated to include traceparent in the duplicate-skip comparison β€” won't dedupe events that differ only by trace context. Right call.
  • ChainBudget.declineToCarry() correctly treats undefined / non-finite as "not opted in" β†’ no behavioral change for callers that don't pass a budget. Slice-1 additive contract preserved.
  • Local vitest pass: 2/2 files Β· 33/33 tests Β· 535ms (src/infra/chain-budget.test.ts, src/infra/system-events.test.ts). Journal: /tmp/oc-swim37-canonical2-journals/pr-366-review.log.

Wired into swim-37 matrix: the SystemEvent.traceparent? field is what 🌻's #370 harness contract-shape it.todos pin to (alongside chain.id / chain.step.remaining). Slice 2 will fill the spans against this exact name surface, so name-drift fails loud β€” which is the swim-37 design intent.

Naming nit deferred (non-blocking): the declineToCarry poets-canon comment is worth keeping but reads slightly long inline; could move to continue-work-signal-v2.md Β§6.7 if it ever needs to be cited operationally. Doesn't gate this PR.

Ship it. 🌊

silas-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
…(🌻's nuance)

Type-pin the load-bearing names at the shim, NOT the adapter:
- new ContinuationSpanAttrs type β€” chain.id / chain.step.remaining /
  delay.ms / reason.preview / delegate.mode / continuation.disabled
- new ContinuationSpanName union β€” all 7 canonical span names

Why this matters: if Slice 3's OTEL adapter ever drifts to chain_id /
chainId / camelCase / etc., the type fails compile BEFORE the #370
harness runtime assertions could catch it. Pin the names at the shim;
the adapter is then forced into compliance.

Slice-1-style additive contract preserved: SpanAttributes (broad
Record<string,...>) is still what setAttributes / StartSpanOptions.attributes
accept, so adapter-internal / diagnostic attrs aren't rejected;
ContinuationSpanAttrs is the canonical-contract pin enforced by tests
via assignment-to-SpanAttributes compile check.

Tests: 11/11 green (was 9/9; +2 type-pin tests).

Refs #334 #366 #370 #377
silas-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
…r (path-B, no OTEL deps) (#378)

* feat(continuation): #334 Slice 2 chunk 1 β€” Tracer surface + noopTracer registry

Pure surface landing. No new deps. No behavior change for callers that
don't opt in (additive contract continued from Slice 1).

What this adds:
- Tracer / Span / SpanAttributes / SpanStatus / StartSpanOptions types
- noopTracer (default; every method is a no-op)
- getContinuationTracer / setContinuationTracer / resetContinuationTracer
- Canonical span-name set pinned by tests (continuation.work,
  continuation.delegate.dispatch, continuation.queue.enqueue/drain,
  continuation.compaction.released, continuation.disabled, heartbeat)
- Canonical attribute-name set pinned (chain.id, chain.step.remaining,
  delay.ms, reason.preview)

Why path-B (no first-party OTEL deps yet):
- Cohort design checkpoint (sprites-of-thornfield, 2026-04-27): adding
  5–8 first-party @opentelemetry/* deps in the gateway hot-path is a
  bauble-policy conversation in its own right. Slice 2 ships the surface;
  Slice 3 wires the OTEL adapter once that conversation lands.
- Symmetric with Slice 1 contract: substrate adds the field
  (SystemEvent.traceparent), malformed/un-opted callers see no change.
  Here: substrate adds the surface (Tracer), un-installed callers see
  no spans emitted.
- #370 swim-37 harness pins against THIS module's surface, not against
  @opentelemetry/api β€” keeps the harness durable across upstream-OTEL
  renames and across any future exporter swap.

Tests: 9/9 green (pnpm vitest src/infra/continuation-tracer.test.ts).

Refs #334 #366 #370 #377

* feat(continuation): pin canonical attr-keys + span-names at the type (🌻's nuance)

Type-pin the load-bearing names at the shim, NOT the adapter:
- new ContinuationSpanAttrs type β€” chain.id / chain.step.remaining /
  delay.ms / reason.preview / delegate.mode / continuation.disabled
- new ContinuationSpanName union β€” all 7 canonical span names

Why this matters: if Slice 3's OTEL adapter ever drifts to chain_id /
chainId / camelCase / etc., the type fails compile BEFORE the #370
harness runtime assertions could catch it. Pin the names at the shim;
the adapter is then forced into compliance.

Slice-1-style additive contract preserved: SpanAttributes (broad
Record<string,...>) is still what setAttributes / StartSpanOptions.attributes
accept, so adapter-internal / diagnostic attrs aren't rejected;
ContinuationSpanAttrs is the canonical-contract pin enforced by tests
via assignment-to-SpanAttributes compile check.

Tests: 11/11 green (was 9/9; +2 type-pin tests).

Refs #334 #366 #370 #377
silas-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
…uation.work span

Wires the continue_work tool to emit a 'continuation.work' span around
the requestContinuation call, using the path-B shim from chunk 1 (#378).

Span shape:
- name: 'continuation.work'
- attributes: delay.ms (rounded), reason.preview (≀80 chars),
  optionally chain.id + chain.step.remaining when chainContext supplies them
- on success: setStatus('OK'), end()
- on throw: recordException(err), setStatus('ERROR', msg), re-throws, end()

Additive contract preserved:
- chainContext is optional β€” callers that don't pass it see no behavior
  change beyond a noop-tracer span open+close
- default tracer is noop until Slice 3 installs a real adapter
- ContinueWorkToolOpts gains an optional 'chainContext' getter; existing
  call sites continue to compile unchanged

Tests: 21/21 green (continuation-tracer 11/11 + continue-work-tool 10/10
including 6 new OTEL emission tests covering: span emission, attr
population with/without chainContext, exception path, and additive-default
contract preservation).

Stacks on chunk 1 (#378) β€” base will flip to cael/325-canonical2 once #378
merges.

Refs #334 #366 #370 #377 #378
silas-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
…nEntry.continuationChainId)

Adds the stable correlation key for OTEL continuation spans. Mints a
UUIDv7 (RFC 9562) at the 0β†’1 transition of continuationChainCount,
reuses for subsequent chain steps, clears on chain reset.

Why UUIDv7:
- RFC 9562 standardized; downstream OTEL collectors (Jaeger/Tempo) parse
  UUID-shape natively as first-class
- First 48 bits are unix-millis-timestamp β†’ lexicographic ordering ==
  chronological ordering, journal greps + sort-by-id stay useful
- uuid@14.0.0 is already a direct dep, exposes v7() β€” zero new deps

Why a substrate field (not derived from diagnostic-trace-context):
- diagnosticTrace is per-attempt (attempt.ts:636) β€” re-mints every turn
- chain spans across multiple turns, so a per-turn id would silently
  break the harness contract pin (chain.id stable for chain lifetime)
- explicit field == one persisted source of truth, survives compaction
  via the existing session-store path

Touch:
- src/infra/secure-random.ts: new generateChainId() helper
- src/config/sessions/types.ts: SessionEntry.continuationChainId?: string
- src/auto-reply/reply/agent-runner.ts: mint-or-reuse in
  persistContinuationChainState (3 store sites: in-memory entry, session
  store cache, persisted store); clear at the chain-reset block
- src/auto-reply/reply/post-compaction-delegate-dispatch.ts: same
  mint-or-reuse pattern in persistPostCompactionDelegateChainState so
  the chain id survives the compaction handoff
- src/auto-reply/reply/agent-runner-session-reset.ts: clear field on
  full session reset

Tests: src/infra/secure-random-chain-id.test.ts (4 tests pinning
UUIDv7 shape, uniqueness across 1000 mints, lexicographic ordering
across time-spaced mints, timestamp-encoded-in-prefix invariant).
agent-runner-session-reset (2/2) + post-compaction-delegate-dispatch
(19/19) still green with the new field threaded through.

Total: 25/25 tests across the three touched paths.

This is commit 1 of 3 in the Slice 2 chunk-2 PR (substrate β†’ tool
optional-getter β†’ runner-side wiring). Stacks on chunk 1
(d533d5c, merged via #378).

Refs #334 #366 #370 #377
silas-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
…n at runner accept seam

Wires the canonical 'continuation.work' OTEL span at the single accept
seam in agent-runner.ts (line ~2282), after both cap-gates pass:

  if (allocatedChainHop >= maxChainLength) {
    // chain-cap REJECT (chunk 4 will emit continuation.disabled here)
  } else if (accumulatedChainTokens > costCapTokens) {
    // cost-cap REJECT (chunk 4)
  } else {
    bracketTokensAccumulated = true;
    const nextChainCount = allocatedChainHop + 1;
    // ...delegate branch (chunk 3)... or:
    } else {
      await persistContinuationChainState({ count: nextChainCount, ... });
      // ← emitContinuationWorkSpan() lands here, AFTER persist so
      //   sessionEntry.continuationChainId is already minted/stored
    }
  }

Why here and not at the unwrap site (agent-runner-execution.ts:1457):
the unwrap-site sees ALL captured continue_work requests, including
ones that get cap-rejected downstream. The accept seam only fires for
requests that actually advance the chain β€” span semantics match
'accepted', not 'requested'.

Why a helper (emitContinuationWorkSpan) instead of inline:
keeps the runner narrow at the call site (5 lines vs 25), centralizes
attribute shaping (delay rounding, reason truncation to 80 chars,
chain.step.remaining clamped to β‰₯0), and makes the span shape
unit-testable in isolation without dragging in the full agent-runner
integration harness. The helper is the natural home for chunk 3's
continuation.delegate.dispatch and chunk 4's continuation.disabled
emissions too.

Span attributes match the canonical Slice 2 ContinuationSpanAttrs type
(pinned in chunk 1 d73825c):
- delay.ms (always, rounded integer)
- chain.step.remaining (always, max(0, maxChainLength - nextChainCount))
- chain.id (UUIDv7 from sessionEntry.continuationChainId, omitted when
  substrate field is absent β€” defensive for substrate-disabled deploys)
- reason.preview (truncated to ≀80 chars, omitted when no reason)

Plus setStatus('OK') and end() always called. Try/catch in the helper
forwards any tracer errors to the runner's defaultRuntime.log via the
optional log callback β€” the accept path MUST NOT block on span emission.

Touch:
- src/infra/continuation-tracer.ts: emitContinuationWorkSpan helper
- src/auto-reply/reply/agent-runner.ts: 5-line call at accept seam
- src/infra/continuation-tracer.test.ts: 7 tests pinning helper shape
  (full attrs, omits chain.id+reason when absent, truncates reason,
  rounds delay, clamps negative chainStepRemaining, swallows tracer
  errors via log callback, no-op against default noopTracer)

Tests: 22/22 across continuation-tracer + secure-random-chain-id;
52/52 across agent-runner-helpers + agent-runner-runtime-config +
agent-runner-execution; 21/21 across agent-runner-session-reset +
post-compaction-delegate-dispatch. Total 95/95 across all touched
paths.

This is commit 2 of 3 in the Slice 2 chunk-2 PR. Stacks on the
substrate commit (440aa35c2c). Final commit will pin the integration
shape via an end-to-end test driving the runner with a recording
tracer (commit 3 of 3 β€” TBD pending 🩸's ack on the integration test
shape).

Refs #334 #366 #370 #377
elliott-dandelion-cult added a commit that referenced this pull request Apr 27, 2026
* feat(swim-37): #324 InMemorySpanRecorder shim + contract tests

Adds reusable in-memory span recorder for the swim-37 harness so
downstream test conversions can read span contract from a black-box
observer instead of redefining the recorder shape inline.

The recorder mirrors the `recordingTracer` pattern already used in
`src/infra/continuation-tracer.test.ts` (around L51-100), lifted into
a reusable harness helper. Stays STDOUT-only / in-process (no real
OTLP collector, no BasicTracerProvider) per harness discipline pinned
in studies/swim-37/harness/README.md.

Contract tests pin:
  - construction returns fresh independent recorders
  - startSpan records name + initial attributes + traceparent
  - setAttributes is last-write-wins per OTEL semantics
  - setStatus + recordException + end() round-trip
  - end() is idempotent
  - spansByName filters correctly
  - reset() clears records without un-installing the tracer
  - spans() returns a defensive copy

Integration tests pin that the recorder captures actual continuation.*
spans driven by the production emit* helpers β€” so harness tests can
trust the recorder as a black-box span observer:
  - emitContinuationWorkSpan stamps chain.id + chain.step.remaining (#366)
  - emitContinuationDelegateSpan stamps chain.id + delegate.delivery +
    delegate.mode (#366)
  - emitContinuationDelegateFireSpan carries fire.deferred_ms +
    persisted chain.id snapshot (#388 chunk 5b); drift formula
    pinned (drift = fire.deferred_ms βˆ’ delay.ms)

Does NOT touch swim-runner.test.ts it.todos β€” those need the runner
(`captureSwim()`) which is separate scope. This PR ships the
foundation those conversions will read against.

Refs: #324 (swim-37 harness)
Refs: #366 (continuation.* spans)
Refs: #388 (chunk 5b/5c fire-span seams)
Base: cael/325-canonical2 tip 7390635 (post-#392)

* fix(swim-37): wire harness into enforced CI lanes + tighten doc-shape (🩸 byte-walk)

Cael caught: as initially submitted, the swim-37 harness code wasn't enforced
by repo CI. Three changes to close the drift surface:

1. **tsconfig.json**: add "studies/**/*" to `include`. Type-shape on the
   recorder + harness is now caught by tsgo / pnpm typecheck.

2. **test/vitest/vitest.swim-37.config.ts**: new vitest project entry
   (createScopedVitestConfig pattern, narrow include for
   studies/swim-37/harness/**/*.test.ts). Registered in
   test/vitest/vitest.config.ts `rootVitestProjects`.

3. **README.md Β§OTEL exporter**: rewrite to describe what shipped (custom
   recorder installed via setContinuationTracer(...), recording-tracer
   pattern lifted from continuation-tracer.test.ts) instead of the
   aspirational InMemorySpanExporter / provider-style framing from the
   original scaffold. Add Β§Vitest project section pointing at the
   wiring above.

4. **in-memory-span-recorder.ts doc-comment**: tighten "local-process-memory
   equivalent of OTEL's InMemorySpanExporter" to the concrete contract
   (in-process array, no provider, no exporter). The broader OTEL framing
   was load-bearing in the original scaffold but reads as forecasting
   plumbing the harness deliberately doesn't ship.

Local verification:
  - vitest --project swim-37 β†’ 15 passed | 18 todo (project recognized)
  - tsgo --noEmit β†’ no new errors under studies/swim-37/

Same-shape lesson as the chunk-6 memo byte-walk earlier today: shipping
the artifact while deferring the enforcement IS the drift surface. Fix
in PR rather than queue followup.
elliott-dandelion-cult added a commit that referenced this pull request Apr 28, 2026
Replaces the placeholder `declare function captureSwim` in
`studies/swim-37/harness/swim-runner.test.ts` with a real implementation
in `studies/swim-37/harness/swim-runner.ts`. The wired path drives
`emitContinuationWorkSpan` against `createInMemorySpanRecorder()` and
returns the captured spans + the synthesized `chainId` (uuid v7 via
`generateChainId`).

Flips two prior `it.todo` markers to live tests:
  - emits continuation.work span with chain.id stamped (#366)
  - span carries chain.step.remaining attribute

Plus three new live tests:
  - sentinel: captureSwim() is wired for continue_work
  - refuses continue_delegate / heartbeat / lich primitives with a clear
    error so the spec's remaining `it.todo` markers stay honest about
    what is implemented
  - repeated calls don't leak capture state between invocations

STDOUT-only discipline preserved: no BasicTracerProvider, no OTLP
exporter, no @opentelemetry/sdk-trace-base machinery β€” capture flows
through `setContinuationTracer(recorder.tracer)` as documented in
README.md.

Other primitives (`continue_delegate`, `heartbeat`, lich-shape) remain
`it.todo` until the corresponding dispatch / heartbeat /
compaction-release seams have a comparable single-helper entry point we
can drive synthetically.

Test results:
  - swim-37 vitest project: 20 passed, 16 todo, 0 failed
  - in-memory-span-recorder.test.ts unchanged (12/12 still pass)
  - tsgo errors visible elsewhere in tree are pre-existing
    (src/plugin-sdk/provider-tools.ts), not introduced by this change

Refs #324 (swim-37 harness) β€” `it.todo` count drops from 17 \u2192 16,
first concrete primitive driven through the live tracer registry.
elliott-dandelion-cult added a commit that referenced this pull request Apr 28, 2026
…405)

* test(swim-37): wire captureSwim() for continue_work primitive

Replaces the placeholder `declare function captureSwim` in
`studies/swim-37/harness/swim-runner.test.ts` with a real implementation
in `studies/swim-37/harness/swim-runner.ts`. The wired path drives
`emitContinuationWorkSpan` against `createInMemorySpanRecorder()` and
returns the captured spans + the synthesized `chainId` (uuid v7 via
`generateChainId`).

Flips two prior `it.todo` markers to live tests:
  - emits continuation.work span with chain.id stamped (#366)
  - span carries chain.step.remaining attribute

Plus three new live tests:
  - sentinel: captureSwim() is wired for continue_work
  - refuses continue_delegate / heartbeat / lich primitives with a clear
    error so the spec's remaining `it.todo` markers stay honest about
    what is implemented
  - repeated calls don't leak capture state between invocations

STDOUT-only discipline preserved: no BasicTracerProvider, no OTLP
exporter, no @opentelemetry/sdk-trace-base machinery β€” capture flows
through `setContinuationTracer(recorder.tracer)` as documented in
README.md.

Other primitives (`continue_delegate`, `heartbeat`, lich-shape) remain
`it.todo` until the corresponding dispatch / heartbeat /
compaction-release seams have a comparable single-helper entry point we
can drive synthetically.

Test results:
  - swim-37 vitest project: 20 passed, 16 todo, 0 failed
  - in-memory-span-recorder.test.ts unchanged (12/12 still pass)
  - tsgo errors visible elsewhere in tree are pre-existing
    (src/plugin-sdk/provider-tools.ts), not introduced by this change

Refs #324 (swim-37 harness) β€” `it.todo` count drops from 17 \u2192 16,
first concrete primitive driven through the live tracer registry.

* docs(swim-37): continue_delegate wiring memo (memo-before-wire)

Pre-PR design memo for the next swim-37 primitive after #405
(continue_work). Decides shape before the wire so the PR lands clean.

Resolved by reading production code:
  Q1 β€” real setTimeout vs fake timers
       N/A for dispatch-accept (synchronous, pre-timer); banked for
       a separate continue_delegate_fire swim.

Cohort sign-off requested on:
  Q2 β€” recipient fan-out shape: N spans sharing chain.id (matches
       chunk-3 cohort design pin + #355 Stage-2 budget semantics).
  Q3 β€” delegate.mode x delegate.delivery: 8-cell it.each matrix
       + omission-contract row.

Banked for follow-up:
  Q4 β€” continue_delegate_fire as separate primitive, not sub-case;
       informs SwimPrimitive type-union shape.

Standard applied (per Cael 2026-04-27 #1498505918, figs
#1498505870): would skipping this memo cost a chunk's worth of
rework? Yes β€” three open Qs would have changed file shape.

Refs:
  - #405 (continue_work wired)
  - #324 (swim-37 harness)
  - docs/design/334-slice2-chunk5b-delegate-fire-memo.md (pattern)

* test(swim-37): wire captureSwim() for continue_delegate primitive

Implements the design from
`docs/design/swim-37-continue-delegate-wiring-memo.md` (commit
3bb086c762, same branch).

Wiring (swim-runner.ts):
- New continue_delegate case in the switch.
- Adds CaptureSwimOptions axes: recipients, delivery, delegateMode.
- Drives emitContinuationDelegateSpan once per recipient with shared
  chain.id (per chunk-3 cohort design pin: N recipients = N spans
  sharing chain.id, NOT one span with a recipients list).
- Validates recipients is a positive integer.

Tests (swim-runner.test.ts):
- 8-cell it.each matrix: delegate.mode (normal | silent | silent-wake
  | post-compaction) x delegate.delivery (immediate | timer).
- Omission contract: delegate.mode attribute absent when caller
  passes undefined.
- Fan-out: 3 recipients emit 3 spans with shared chain.id.
- Validation: recipients=0 and recipients=1.5 reject.
- Removes the prior 'refuses continue_delegate' test row (now wired).

Two it.todo markers preserved for downstream concerns:
- chain-budget decrement / ChainBudget.declineToCarry observable
  (needs ChainBudget integration, not in scope here).
- fan-out budget arithmetic per #355 Stage-2 (1 step not N) \u2014
  separate from span cardinality.

Test results delta:
  Before: 8 passed | 16 todo (24 tests)
  After: 19 passed | 15 todo (34 tests)
  +11 live | -1 todo

Refs:
- #324 swim-37 harness
- #405 captureSwim continue_work (parent of this branch)
- docs/design/swim-37-continue-delegate-wiring-memo.md

* test(swim-37): pin recipient.index gap on continue_delegate fan-out

Per Cael's caution on the wiring memo (Discord msg
1498507232185286849 sign-off): N spans sharing chain.id risks
collapsing into analytic mush at scale unless per-recipient
distinction is visible in attrs. Currently
emitContinuationDelegateSpan exposes no recipient.index axis \u2014
all N spans in a fan-out are byte-identical except spanId.

Adds:
- One live GAP-PIN test asserting the current (deficient) reality:
  fan-out spans toEqual on attributes. This will fail loudly when
  the production helper grows the axis, prompting a flip to
  not.toEqual.
- One it.todo marker for the upcoming production-helper change.

Production-helper change is out of scope for this PR; will file
follow-up issue. The gap-pin lives in the integration tier so it
shows up in any code-review touching delegate-dispatch span shape.

Refs:
- #324 swim-37 harness
- #405 captureSwim continue_work + continue_delegate (this PR)
- docs/design/swim-37-continue-delegate-wiring-memo.md (Q2)
- Cael Discord sign-off msg 1498507232185286849

* test(swim-37): wire \u00a71 entry-point todo onto classifyRebasePick

Now that #408 (PR #408 merged at cb73bc8) landed
`rebase-classifier.ts` with the pure `classifyRebasePick` composer,
the integration-tier \u00a71 entry-point it.todo on swim-runner.test.ts
L80 is wireable without any new shim.

Synthetic commit pinned: subject mentions PR openclaw#70595, base CHANGELOG
contains the same PR token (squash-rebase shape from the memo's
7ee46a3 anchor instance). Discovery channel: changelog-grep:pr.

L81 (CHANGELOG-byte-grep emits drop-with-reason span) stays todo
\u2014 needs the production callsite that pairs the channel with
`emitContinuationDisabledSpan`, per \ud83c\udf0a's #408 note.

Test delta:
  Before: 20 passed | 16 todo (36)
  After: 21 passed | 15 todo (36)
  +1 live | -1 todo
karmafeast pushed a commit that referenced this pull request May 1, 2026
…ch) [WIP - typecheck pending] (#370)

* swim 37: integration test harness scaffold (continuation/heartbeat/lich)

- studies/swim-37/harness/swim-runner.test.ts: vitest skeleton, one
  it.todo per trap-class (Β§1 parallel-evolution, Β§3a type-shape drift)
  and per continuation primitive (continue_work / continue_delegate /
  heartbeat / lich-shape post-compaction).
- studies/swim-37/harness/README.md: scaffold map + hookup-point TODOs
  pointing at #366 (continuation.* spans), #355 Stage-2 (fan-out cap),
  #332 Item B (compaction release seam).

OTEL exporter: STDOUT/InMemory shim only β€” no live collector.
Trap-class source: cael/swim-37-trap-classes tip 2adf174.

Closes #324 (scaffold slice).

* docs(swim-37 harness): note #366 Slice 1 already landed (0f40bc0)

Per 🌫️ review nit on #370 β€” call out that traceparent on SystemEvent +
ChainBudget.declineToCarry() are already in tree; only Slice 2 spans
remain pending.
karmafeast pushed a commit that referenced this pull request May 1, 2026
… cap helper (#366)

* feat(continuation): #334 Slice 1 β€” traceparent on system-event payload + chain-budget cap helper

Substrate threading for OTEL chain-correlation per RFC Β§6.7
(continue-work-signal-v2.md, anchored at #361 head 045fdb4).

Additive only β€” no behavioral change for callers that don't pass
a traceparent or a chain-budget state.

Two pieces:

1. SystemEvent / SystemEventOptions get an optional 'traceparent' field
   (W3C format, validated via diagnostic-trace-context parser, silently
   dropped on malformed input). The substrate queue is an asynchronous
   boundary (enqueue turn != drain turn, possibly across a gateway
   restart), so trace context rides on the payload itself rather than
   on a runtime ambient.

2. ChainBudget.declineToCarry() β€” cap-on-enqueue helper that returns
   true when chainStepBudgetRemaining <= 0. Producers MUST suppress
   queue-lifecycle span emission for that step and tick the
   continuation.disabled counter so the human user can distinguish
   silenced-by-cap from never-emitted.

One axis, two declines (per Β§6.7):
  depth-cap   = 'I won't carry past my budget'    (this PR)
  fan-out-cap = 'I won't spend yours'             (lives with #355)

a chain that knows when to stop being a chain is the kind of chain
that gets built on.

Naming note (poets-canon): declineToCarry over refuseAttach. The
chain isn't refusing the trace β€” it's declining to carry the next
prince's context window into search-space the chain itself has
already abandoned. Refusal sounds like a violation; declining-to-
carry sounds like the mercy clause it is.

Slice 2 (continuation.delegate.* / continuation.queue.* span set per
Β§6.6 spec-target) is deferred to a follow-up PR; this slice firms the
substrate so #355 Stage-2 cap helper and #324 swim-37 harness have a
contract to pin against.

closes #334 (Slice 1)
refs #361 Β§6.7
refs #355 #324

* chore(plugin-sdk): regen api baseline for traceparent surface (Slice 1)

Adds traceparent?: string to SystemEvent + SystemEventOptions per #334
Slice 1; this is an additive plugin-sdk surface change so the baseline
hash needs to roll forward.

Refs #366 (CI: generated-doc-baselines failure on plugin-sdk:api:check)
karmafeast pushed a commit that referenced this pull request May 1, 2026
…r (path-B, no OTEL deps) (#378)

* feat(continuation): #334 Slice 2 chunk 1 β€” Tracer surface + noopTracer registry

Pure surface landing. No new deps. No behavior change for callers that
don't opt in (additive contract continued from Slice 1).

What this adds:
- Tracer / Span / SpanAttributes / SpanStatus / StartSpanOptions types
- noopTracer (default; every method is a no-op)
- getContinuationTracer / setContinuationTracer / resetContinuationTracer
- Canonical span-name set pinned by tests (continuation.work,
  continuation.delegate.dispatch, continuation.queue.enqueue/drain,
  continuation.compaction.released, continuation.disabled, heartbeat)
- Canonical attribute-name set pinned (chain.id, chain.step.remaining,
  delay.ms, reason.preview)

Why path-B (no first-party OTEL deps yet):
- Cohort design checkpoint (sprites-of-thornfield, 2026-04-27): adding
  5–8 first-party @opentelemetry/* deps in the gateway hot-path is a
  bauble-policy conversation in its own right. Slice 2 ships the surface;
  Slice 3 wires the OTEL adapter once that conversation lands.
- Symmetric with Slice 1 contract: substrate adds the field
  (SystemEvent.traceparent), malformed/un-opted callers see no change.
  Here: substrate adds the surface (Tracer), un-installed callers see
  no spans emitted.
- #370 swim-37 harness pins against THIS module's surface, not against
  @opentelemetry/api β€” keeps the harness durable across upstream-OTEL
  renames and across any future exporter swap.

Tests: 9/9 green (pnpm vitest src/infra/continuation-tracer.test.ts).

Refs #334 #366 #370 #377

* feat(continuation): pin canonical attr-keys + span-names at the type (🌻's nuance)

Type-pin the load-bearing names at the shim, NOT the adapter:
- new ContinuationSpanAttrs type β€” chain.id / chain.step.remaining /
  delay.ms / reason.preview / delegate.mode / continuation.disabled
- new ContinuationSpanName union β€” all 7 canonical span names

Why this matters: if Slice 3's OTEL adapter ever drifts to chain_id /
chainId / camelCase / etc., the type fails compile BEFORE the #370
harness runtime assertions could catch it. Pin the names at the shim;
the adapter is then forced into compliance.

Slice-1-style additive contract preserved: SpanAttributes (broad
Record<string,...>) is still what setAttributes / StartSpanOptions.attributes
accept, so adapter-internal / diagnostic attrs aren't rejected;
ContinuationSpanAttrs is the canonical-contract pin enforced by tests
via assignment-to-SpanAttributes compile check.

Tests: 11/11 green (was 9/9; +2 type-pin tests).

Refs #334 #366 #370 #377
karmafeast pushed a commit that referenced this pull request May 1, 2026
* feat(swim-37): #324 InMemorySpanRecorder shim + contract tests

Adds reusable in-memory span recorder for the swim-37 harness so
downstream test conversions can read span contract from a black-box
observer instead of redefining the recorder shape inline.

The recorder mirrors the `recordingTracer` pattern already used in
`src/infra/continuation-tracer.test.ts` (around L51-100), lifted into
a reusable harness helper. Stays STDOUT-only / in-process (no real
OTLP collector, no BasicTracerProvider) per harness discipline pinned
in studies/swim-37/harness/README.md.

Contract tests pin:
  - construction returns fresh independent recorders
  - startSpan records name + initial attributes + traceparent
  - setAttributes is last-write-wins per OTEL semantics
  - setStatus + recordException + end() round-trip
  - end() is idempotent
  - spansByName filters correctly
  - reset() clears records without un-installing the tracer
  - spans() returns a defensive copy

Integration tests pin that the recorder captures actual continuation.*
spans driven by the production emit* helpers β€” so harness tests can
trust the recorder as a black-box span observer:
  - emitContinuationWorkSpan stamps chain.id + chain.step.remaining (#366)
  - emitContinuationDelegateSpan stamps chain.id + delegate.delivery +
    delegate.mode (#366)
  - emitContinuationDelegateFireSpan carries fire.deferred_ms +
    persisted chain.id snapshot (#388 chunk 5b); drift formula
    pinned (drift = fire.deferred_ms βˆ’ delay.ms)

Does NOT touch swim-runner.test.ts it.todos β€” those need the runner
(`captureSwim()`) which is separate scope. This PR ships the
foundation those conversions will read against.

Refs: #324 (swim-37 harness)
Refs: #366 (continuation.* spans)
Refs: #388 (chunk 5b/5c fire-span seams)
Base: cael/325-canonical2 tip 7390635 (post-#392)

* fix(swim-37): wire harness into enforced CI lanes + tighten doc-shape (🩸 byte-walk)

Cael caught: as initially submitted, the swim-37 harness code wasn't enforced
by repo CI. Three changes to close the drift surface:

1. **tsconfig.json**: add "studies/**/*" to `include`. Type-shape on the
   recorder + harness is now caught by tsgo / pnpm typecheck.

2. **test/vitest/vitest.swim-37.config.ts**: new vitest project entry
   (createScopedVitestConfig pattern, narrow include for
   studies/swim-37/harness/**/*.test.ts). Registered in
   test/vitest/vitest.config.ts `rootVitestProjects`.

3. **README.md Β§OTEL exporter**: rewrite to describe what shipped (custom
   recorder installed via setContinuationTracer(...), recording-tracer
   pattern lifted from continuation-tracer.test.ts) instead of the
   aspirational InMemorySpanExporter / provider-style framing from the
   original scaffold. Add Β§Vitest project section pointing at the
   wiring above.

4. **in-memory-span-recorder.ts doc-comment**: tighten "local-process-memory
   equivalent of OTEL's InMemorySpanExporter" to the concrete contract
   (in-process array, no provider, no exporter). The broader OTEL framing
   was load-bearing in the original scaffold but reads as forecasting
   plumbing the harness deliberately doesn't ship.

Local verification:
  - vitest --project swim-37 β†’ 15 passed | 18 todo (project recognized)
  - tsgo --noEmit β†’ no new errors under studies/swim-37/

Same-shape lesson as the chunk-6 memo byte-walk earlier today: shipping
the artifact while deferring the enforcement IS the drift surface. Fix
in PR rather than queue followup.
karmafeast pushed a commit that referenced this pull request May 1, 2026
…405)

* test(swim-37): wire captureSwim() for continue_work primitive

Replaces the placeholder `declare function captureSwim` in
`studies/swim-37/harness/swim-runner.test.ts` with a real implementation
in `studies/swim-37/harness/swim-runner.ts`. The wired path drives
`emitContinuationWorkSpan` against `createInMemorySpanRecorder()` and
returns the captured spans + the synthesized `chainId` (uuid v7 via
`generateChainId`).

Flips two prior `it.todo` markers to live tests:
  - emits continuation.work span with chain.id stamped (#366)
  - span carries chain.step.remaining attribute

Plus three new live tests:
  - sentinel: captureSwim() is wired for continue_work
  - refuses continue_delegate / heartbeat / lich primitives with a clear
    error so the spec's remaining `it.todo` markers stay honest about
    what is implemented
  - repeated calls don't leak capture state between invocations

STDOUT-only discipline preserved: no BasicTracerProvider, no OTLP
exporter, no @opentelemetry/sdk-trace-base machinery β€” capture flows
through `setContinuationTracer(recorder.tracer)` as documented in
README.md.

Other primitives (`continue_delegate`, `heartbeat`, lich-shape) remain
`it.todo` until the corresponding dispatch / heartbeat /
compaction-release seams have a comparable single-helper entry point we
can drive synthetically.

Test results:
  - swim-37 vitest project: 20 passed, 16 todo, 0 failed
  - in-memory-span-recorder.test.ts unchanged (12/12 still pass)
  - tsgo errors visible elsewhere in tree are pre-existing
    (src/plugin-sdk/provider-tools.ts), not introduced by this change

Refs #324 (swim-37 harness) β€” `it.todo` count drops from 17 \u2192 16,
first concrete primitive driven through the live tracer registry.

* docs(swim-37): continue_delegate wiring memo (memo-before-wire)

Pre-PR design memo for the next swim-37 primitive after #405
(continue_work). Decides shape before the wire so the PR lands clean.

Resolved by reading production code:
  Q1 β€” real setTimeout vs fake timers
       N/A for dispatch-accept (synchronous, pre-timer); banked for
       a separate continue_delegate_fire swim.

Cohort sign-off requested on:
  Q2 β€” recipient fan-out shape: N spans sharing chain.id (matches
       chunk-3 cohort design pin + #355 Stage-2 budget semantics).
  Q3 β€” delegate.mode x delegate.delivery: 8-cell it.each matrix
       + omission-contract row.

Banked for follow-up:
  Q4 β€” continue_delegate_fire as separate primitive, not sub-case;
       informs SwimPrimitive type-union shape.

Standard applied (per Cael 2026-04-27 #1498505918, figs
#1498505870): would skipping this memo cost a chunk's worth of
rework? Yes β€” three open Qs would have changed file shape.

Refs:
  - #405 (continue_work wired)
  - #324 (swim-37 harness)
  - docs/design/334-slice2-chunk5b-delegate-fire-memo.md (pattern)

* test(swim-37): wire captureSwim() for continue_delegate primitive

Implements the design from
`docs/design/swim-37-continue-delegate-wiring-memo.md` (commit
3bb086c762, same branch).

Wiring (swim-runner.ts):
- New continue_delegate case in the switch.
- Adds CaptureSwimOptions axes: recipients, delivery, delegateMode.
- Drives emitContinuationDelegateSpan once per recipient with shared
  chain.id (per chunk-3 cohort design pin: N recipients = N spans
  sharing chain.id, NOT one span with a recipients list).
- Validates recipients is a positive integer.

Tests (swim-runner.test.ts):
- 8-cell it.each matrix: delegate.mode (normal | silent | silent-wake
  | post-compaction) x delegate.delivery (immediate | timer).
- Omission contract: delegate.mode attribute absent when caller
  passes undefined.
- Fan-out: 3 recipients emit 3 spans with shared chain.id.
- Validation: recipients=0 and recipients=1.5 reject.
- Removes the prior 'refuses continue_delegate' test row (now wired).

Two it.todo markers preserved for downstream concerns:
- chain-budget decrement / ChainBudget.declineToCarry observable
  (needs ChainBudget integration, not in scope here).
- fan-out budget arithmetic per #355 Stage-2 (1 step not N) \u2014
  separate from span cardinality.

Test results delta:
  Before: 8 passed | 16 todo (24 tests)
  After: 19 passed | 15 todo (34 tests)
  +11 live | -1 todo

Refs:
- #324 swim-37 harness
- #405 captureSwim continue_work (parent of this branch)
- docs/design/swim-37-continue-delegate-wiring-memo.md

* test(swim-37): pin recipient.index gap on continue_delegate fan-out

Per Cael's caution on the wiring memo (Discord msg
1498507232185286849 sign-off): N spans sharing chain.id risks
collapsing into analytic mush at scale unless per-recipient
distinction is visible in attrs. Currently
emitContinuationDelegateSpan exposes no recipient.index axis \u2014
all N spans in a fan-out are byte-identical except spanId.

Adds:
- One live GAP-PIN test asserting the current (deficient) reality:
  fan-out spans toEqual on attributes. This will fail loudly when
  the production helper grows the axis, prompting a flip to
  not.toEqual.
- One it.todo marker for the upcoming production-helper change.

Production-helper change is out of scope for this PR; will file
follow-up issue. The gap-pin lives in the integration tier so it
shows up in any code-review touching delegate-dispatch span shape.

Refs:
- #324 swim-37 harness
- #405 captureSwim continue_work + continue_delegate (this PR)
- docs/design/swim-37-continue-delegate-wiring-memo.md (Q2)
- Cael Discord sign-off msg 1498507232185286849

* test(swim-37): wire \u00a71 entry-point todo onto classifyRebasePick

Now that #408 (PR #408 merged at cb73bc8) landed
`rebase-classifier.ts` with the pure `classifyRebasePick` composer,
the integration-tier \u00a71 entry-point it.todo on swim-runner.test.ts
L80 is wireable without any new shim.

Synthetic commit pinned: subject mentions PR openclaw#70595, base CHANGELOG
contains the same PR token (squash-rebase shape from the memo's
7ee46a3 anchor instance). Discovery channel: changelog-grep:pr.

L81 (CHANGELOG-byte-grep emits drop-with-reason span) stays todo
\u2014 needs the production callsite that pairs the channel with
`emitContinuationDisabledSpan`, per \ud83c\udf0a's #408 note.

Test delta:
  Before: 20 passed | 16 todo (36)
  After: 21 passed | 15 todo (36)
  +1 live | -1 todo
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.

4 participants