feat(runtime): proposal_created carries the proposal body (content)#292
Conversation
The proposal_created RuntimeStreamEvent carried only {proposalId, title,
status} — the proposal BODY was dropped at the event boundary, so every
produced-state consumer had to re-fetch it out-of-band from its own database to
grade the deliverable. The artifact event already carries content; proposals
now match it.
- types.ts: proposal_created gains content?: string (same role/vocabulary as
the artifact variant's content).
- backends.ts: mapCommonBackendEvent populates content from data.content /
data.body / record.content so SSE-emitting backends thread the body.
- sanitize.ts: surface content under includeControlPayloads, mirroring the
artifact variant's gating.
Additive + backward-compatible: a title-only filing omits content.
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved PR — 086ca079
Blanket team auto-approval is enabled for this reviewer service.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: blanket_auto_approve · 2026-06-14T11:18:47Z
tangletools
left a comment
There was a problem hiding this comment.
🟢 Value Audit — sound
| Verdict | sound |
| Concerns | 0 (none) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 113.4s (2 bridge agents) |
| Total | 113.4s |
💰 Value — sound
Adds optional content to proposal_created stream events, mirroring the existing artifact pattern so proposal bodies flow in-band; clean, additive, and consistent with the codebase.
- What it does: Adds an optional
content?: stringfield to theproposal_createdvariant ofRuntimeStreamEvent(src/types.ts:371), populates it inmapCommonBackendEventfromdata.content/data.body/record.content(src/backends.ts:498), and gates it behindincludeControlPayloadsin sanitization (src/sanitize.ts:272) — exactly mirroring howartifactcarries and redacts itscontent. - Goals it achieves: Eliminates the thin-contract gap where produced-state consumers grading a proposal's body had to re-fetch it out-of-band from their own database; the event stream can now carry the assessable proposal body in-band, like it already does for artifact deliverables.
- Assessment: Good change. It is additive and backward-compatible (no fields removed, optional field, callers that don't use it are unaffected). It follows the established grain of the codebase by reusing the same vocabulary, mapping fallbacks, and redaction gating already used for
artifactcontent. - Better / existing approach: none — this is the right approach. The codebase already models produced deliverable bodies as
content?: stringonartifactevents and gates them underincludeControlPayloads. Extending the same design toproposal_createdis the minimal, idiomatic fix; no existing equivalent or better architecture was found after searchingsrc/forproposal_created,includeControlPayloads, and the `st
🎯 Usefulness — sound
A thin, additive fix that mirrors the existing artifact content contract and closes a real event-boundary gap for downstream eval consumers.
- Integration: Wires correctly.
mapCommonBackendEventinsrc/backends.ts:498now threadscontentthrough the default backend-event mapper, which is invoked bycreateIterableBackend(src/backends.ts:67) and the OpenAI-compatible backend tool-delta path (src/backends.ts:785).sanitizeRuntimeStreamEventinsrc/sanitize.ts:272gates it underincludeControlPayloads. Theproposal_createdtype is ex - Fit with existing patterns: Fits the established artifact pattern precisely: the same field name (
content), the same doc-comment vocabulary (same role as content on the artifact variant), the same redaction gate (includeControlPayloads), and the same fallback chain (data.content ?? data.body ?? record.content) already used for artifacts atsrc/backends.ts:477. No competing pattern exists. - Real-world viability: Holds up. The mapping is stateless and uses the existing
stringValuehelper (src/backends.ts:912), which rejects non-strings and empty strings, so bogus payloads fall back toundefinedsafely. The redaction path defaults to hidingcontentandtitleunlessincludeControlPayloadsis set, matching every other payload-bearing event. No concurrency or error-path complications are introduced
No concerns — sound change, no better or existing approach found. ✅
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
| deepseek | glm | aggregate | |
|---|---|---|---|
| Readiness | 86 | 86 | 86 |
| Confidence | 75 | 75 | 75 |
| Correctness | 86 | 86 | 86 |
| Security | 86 | 86 | 86 |
| Testing | 86 | 86 | 86 |
| Architecture | 86 | 86 | 86 |
Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision.
🟡 LOW No test coverage for proposal content extraction — src/backends.ts
runtime.test.ts:619 sends
{"type":"proposal_created","proposalId":"prop-9","title":"File amendment","status":"pending"}with nocontent/bodyfield, and the assertion at line 640-645 does not checkcontent. The new line at backends.ts:498 (content: stringValue(data.content) ?? stringValue(data.body) ?? stringValue(record.content)) is therefore untested. The artifact sibling at runtime.test.ts:618-637 includes"content":"# Brief body"in wire data and asserts it. Fix: add"content":"Amendment body text"to the proposal wire data at line 619
🟡 LOW proposal_created content field not exercise in tests — src/sanitize.ts
Neither the redaction test (line 806 in tests/runtime.test.ts) nor the opt-in test (line 886) sends
contentonproposal_createdevents. The redaction test only validatestitle(line 846), and the opt-in test only checkstitleinclusion (line 912) plus loosetoMatchObject([line 916](https://github.com/tangle-network/
🟡 LOW proposal_created.content sanitize path is not exercised by any test fixture — src/sanitize.ts
Both relevant test fixtures (tests/runtime.test.ts:806-814 redaction case and :885-893 opt-in case) construct proposal_created events WITHOUT a
contentfield. grep confirmscontent:appears only on artifact events (lines 637, 802, 881), never on proposal_created. Consequence: (a) the redaction test at :846 (expect(serialized).not.toContain('confidential-proposal-title')) would still pass if the includeControlPayloads gating on content were removed, because there's no content payload to leak; (b) the opt-in test at :911-912 would pass even if the new line at src/sanitize.ts:272 were deleted entirely. Impact is low because the line is a mechanical mirror of the artifact
🟡 LOW Comment verbosity for a single optional field — src/types.ts
Lines 371-373 add a 3-line comment for one optional
content?: stringfield. The comment is accurate and useful (cross-references the artifact variant and produced-state grading), but 3 lines of prose for one field is heavier than the surrounding style — e.g., theartifactvariant'scontent?: stringat line 360 carries no comment, and the detailed doc-comments in this file are reserved for non-obvious invariants (see theerror?: BackendErrorDetailblocks at [lines 384-394](https://github.com/tangle-network/agent-runtime/blob/086ca0794692b056429715643e2
🟡 LOW No test coverage for new content field on proposal_created — src/types.ts
The new content?: string field on the proposal_created event is not exercised in any test. The wire-mapping test (tests/runtime.test.ts:619) sends a proposal_created SSE payload without content, and the redaction/opt-in tests (tests/runtime.test.ts:~806, ~885) also omit the field. The artifact.content path was added together with its tests — proposal_created.content should get the same treatment. Add an assertion that content maps correctly from data.content/data.body and is redacted when includeControlPayloads is false.
tangletools · 2026-06-14T11:37:33Z · trace
Why
The
proposal_createdRuntimeStreamEventcarried only{proposalId, title, status}— the proposal body was dropped at the event boundary. Theartifactvariant already carriescontent; proposals didn't. Consequence: every produced-state consumer that grades a proposal's content had to re-fetch the body out-of-band from its own database, because the in-band event never had it. That's the thin-contract root cause behind a class of consumer band-aids.What
proposal_createdgainscontent?: string(same role + vocabulary as theartifactvariant'scontent). A title-only filing omits it.mapCommonBackendEventpopulatescontentfromdata.content/data.body/record.content, so SSE-emitting backends (sandbox agents) thread the body through.contentunderincludeControlPayloads, mirroring theartifactvariant's redaction gating (safe by default).Additive + backward-compatible — no field removed, no caller breaks. Unblocks every produced-state eval consumer to read the proposal body in-band instead of reaching into the product DB.
899 tests green.