Skip to content

feat(sessions): opt-in sessions.captureText for response-body capture#153

Merged
antonbabenko merged 3 commits into
masterfrom
feat/sessions-capture-text-plan
Jun 22, 2026
Merged

feat(sessions): opt-in sessions.captureText for response-body capture#153
antonbabenko merged 3 commits into
masterfrom
feat/sessions-capture-text-plan

Conversation

@antonbabenko

@antonbabenko antonbabenko commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Part C, now implemented (was a draft plan; #152 merged, this rebased on top).

What

A uniform, default-OFF sessions.captureText flag gating persistence of each provider's raw response body (opinion.text) in the session store.

Discovery that shaped it: opinion.text was already persisted unconditionally for ask-all + the server-side consensus tool (scrubbed+capped), but not for consensus-step. So captureText is a uniform gate at the single write chokepoint (persistRun):

  • OFF (default): opinion.text dropped for all callers (ask-all, consensus, consensus-step, session-revisit). Only question + verdict/issue summaries persist. This intentionally tightens the prior ask-all/consensus default (which always stored bodies) - body capture is now opt-in everywhere.
  • ON (+ persist on): opinion.text stored, secret-scrubbed (mandatory) then best-effort PII-stripped, then capped.
  • consensus-step now retains the raw peer response on its in-memory loop result so it can be captured when ON; the wire response still omits text.
  • debug.jsonl untouched - metrics-only, never body text, regardless of the flag.

Security

  • Secret-scrub mandatory, never gated by captureText; stripPII (email + separator-phone) is best-effort defense-in-depth, not the gate.
  • ReDoS-hardened: stripPII uses RFC-bounded quantifiers -> linear. An unbounded email regex was O(n^2) (968ms -> 12ms on a 50k pathological input); regression test PII3.
  • Forward-gating (documented): does not retroactively strip records already on disk; toggling off stops new capture, it is not a purge.

Cross-model review

ask-all (security-analyst lens): grok LOW, codex MEDIUM, kimi HIGH, gemini timed out, glm thrashed. Acted on the real findings:

  • kimi HIGH (ReDoS): confirmed empirically (968ms), fixed with bounded quantifiers + PII3.
  • codex MEDIUM (lifecycle): documented the forward-gating semantics.
  • grok+kimi (wire-leak proof): added CS17 asserting dispatch_peers never returns raw text.
  • kimi (bypass audit): verified persistRun is the only record-creating writeSession caller (annotateSession only re-writes existing records).

Tests

574/574 pass. New: PII1-3 (redaction, scrub-then-strip ordering, ReDoS-linearity), SESS8 (config validation), CS14/CS15 (consensus-step gating), CS16 (ask-all gating, shared chokepoint), CS17 (no wire leak).

Behavior change to flag for reviewers

ask-all / consensus records no longer store opinion.text by default (only when captureText is enabled). No functional consumer depends on it - session-revisit re-runs from question, analyze uses verdict.

Deferred out of the consensus-step persistence work (PR #152). Captures
per-opinion prompt/response BODY text in the session store ONLY behind a new
default-off sessions.captureText flag - never in debug.jsonl (metrics-only
invariant holds). Records the locked-in hard requirements: mandatory secret
scrub as the primary control, PII-strip as best-effort defense-in-depth (not the
gate), bounded session-revisit re-capture, and the plaintext-on-disk threat
model. Implementation depends on PR #152 landing first.
Adds a uniform, default-OFF gate for persisting each provider's raw RESPONSE
body (opinion.text) in the session store.

Discovery: opinion.text was ALREADY persisted unconditionally for ask-all and the
server-side consensus tool (scrubbed+capped in sanitizeRecord), but NOT for
consensus-step (loop results carry no text). captureText now gates it uniformly
at the single write chokepoint (persistRun):

- default OFF drops opinion.text for ALL callers (ask-all, consensus, consensus-
  step, session-revisit) - only question + verdict/criticalIssues summaries are
  stored. This intentionally TIGHTENS the prior ask-all/consensus default (which
  always stored bodies) - body capture is now opt-in everywhere.
- ON (and persist on) stores opinion.text, secret-scrubbed (MANDATORY) then
  best-effort PII-stripped (email + separator-phone only; not a guarantee), then
  capped.
- consensus-step dispatch_peers retains the raw peer response on the in-memory
  loop result so it can be captured when ON; the wire response still omits text.
- debug.jsonl is untouched - metrics-only, never body text, regardless of the flag.

stripPII uses RFC-bounded quantifiers so it stays LINEAR on long attacker-
influenced responses (an unbounded email regex was O(n^2): 968ms -> 12ms on 50k).

config: sessions.captureText (boolean, default false) in the schema and
resolveSessions validation (non-boolean -> false + warning).

Tests: stripPII redaction + scrub-then-strip ordering + ReDoS-linearity (PII1-3);
captureText config validation (SESS8); chokepoint gating for consensus-step
(CS14/CS15) and ask-all (CS16); and no raw text on the dispatch_peers wire (CS17).

Lifecycle note (docs): captureText is forward-gating - it does not retroactively
strip records already on disk; toggling off stops new capture, it is not a purge.
@antonbabenko antonbabenko force-pushed the feat/sessions-capture-text-plan branch from e199706 to 7303cc0 Compare June 22, 2026 14:19
@antonbabenko antonbabenko changed the title docs: Part C plan - opt-in sessions.captureText body capture feat(sessions): opt-in sessions.captureText for response-body capture Jun 22, 2026
@antonbabenko antonbabenko marked this pull request as ready for review June 22, 2026 14:20
- stripPII: drop the phone-number heuristic, keep email-only (lower false-positive
  risk on legitimate content; PII1 test updated). Mandatory secret-scrub and the
  default-off captureText gate remain the real controls.
- Remove docs/captureText-plan.md (planning scratch; superseded by the real docs).
- Document consensus-step terminal persistence (#152) and sessions.captureText
  (#153) across README, TECHNICAL (Session persistence: config table, record
  shape, scrub/PII note, which tools persist), SETUP, CLAUDE, AGENTS, and
  config.default.json. Regenerated host artifacts (POWER.md, SKILL.md) via
  scripts/sync-hosts.js from the AGENTS.md source.
- CLAUDE.md: add a MANDATORY "Documentation upkeep on feature completion" section
  - a feature is not done until README/TECHNICAL/SETUP/CLAUDE/AGENTS + config
  schema/default are updated in the same PR, generated artifacts regenerated via
  sync-hosts, and npm run check (host-artifacts + validate drift guards) passes.
@antonbabenko antonbabenko merged commit 4000dd4 into master Jun 22, 2026
1 check passed
@antonbabenko antonbabenko deleted the feat/sessions-capture-text-plan branch June 22, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant